mirror of
https://github.com/mailcow/mailcow-dockerized.git
synced 2026-10-21 18:28:13 +00:00
Compare commits
517
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2bad774bcc | ||
|
|
015fd04a37 | ||
|
|
ca8546fa4a | ||
|
|
6f6edfb85e | ||
|
|
6c877fc0ac | ||
|
|
4a29767a31 | ||
|
|
cd29426933 | ||
|
|
37be306f1c | ||
|
|
a351017455 | ||
|
|
887b8a3594 | ||
|
|
571c1b5b29 | ||
|
|
8cd231c9b9 | ||
|
|
74facc4b5b | ||
|
|
ff6329d01b | ||
|
|
e8209d6504 | ||
|
|
626d006a3d | ||
|
|
e88c734d0a | ||
|
|
86bba266f6 | ||
|
|
e3ed8d2fc2 | ||
|
|
88d92261e4 | ||
|
|
017d0c7ff0 | ||
|
|
a28fa19044 | ||
|
|
fa2ad14fc4 | ||
|
|
a480a96e10 | ||
|
|
42d387ca42 | ||
|
|
fe4e675754 | ||
|
|
b05f59b705 | ||
|
|
c4d0ca56d2 | ||
|
|
7ec61fde8a | ||
|
|
d80b15b06c | ||
|
|
ed4fe41d7a | ||
|
|
6b9d875773 | ||
|
|
4d5b9d1c80 | ||
|
|
8abf453e1c | ||
|
|
8bd9e12a73 | ||
|
|
c665d430de | ||
|
|
2195a674ff | ||
|
|
089c44aee1 | ||
|
|
6644699e65 | ||
|
|
91d3b7246a | ||
|
|
b362c7105d | ||
|
|
555e7ed706 | ||
|
|
59e2a9ac77 | ||
|
|
feed5ad183 | ||
|
|
f1f7a9800b | ||
|
|
8c85044781 | ||
|
|
8fb32e4b59 | ||
|
|
b5fa926bc0 | ||
|
|
d1a2f4e168 | ||
|
|
f7a536d634 | ||
|
|
8b456a33e7 | ||
|
|
54da526d41 | ||
|
|
50be483c39 | ||
|
|
d64c923aca | ||
|
|
2d5f166ba8 | ||
|
|
94b5a623ab | ||
|
|
e406ecd461 | ||
|
|
68a95c3af3 | ||
|
|
bd037d5644 | ||
|
|
7036dbf4f8 | ||
|
|
2f10a4633f | ||
|
|
36c70db86c | ||
|
|
9cd16f0001 | ||
|
|
f4961c4023 | ||
|
|
38de21592c | ||
|
|
f44bd2f36a | ||
|
|
95a77f2dcb | ||
|
|
e856510fb2 | ||
|
|
d51d06d716 | ||
|
|
54170d075a | ||
|
|
b4bb1a625b | ||
|
|
2e5a29bf69 | ||
|
|
e245ac04d9 | ||
|
|
c877fdf0a5 | ||
|
|
fea38c8e1b | ||
|
|
cc9af65852 | ||
|
|
92cc8bec90 | ||
|
|
145745329e | ||
|
|
8e72d22c56 | ||
|
|
ddd76d99cd | ||
|
|
8c9524a2fe | ||
|
|
2ebd32d2ee | ||
|
|
4b62af9d02 | ||
|
|
90ca1bf25a | ||
|
|
14772c3a20 | ||
|
|
e5d0ed8c4c | ||
|
|
c17cc8a792 | ||
|
|
4cba448671 | ||
|
|
7dab6c63d1 | ||
|
|
5408bc809d | ||
|
|
bd5c18b145 | ||
|
|
0f06c8e563 | ||
|
|
fffe1aec3d | ||
|
|
6f7fee49cd | ||
|
|
c1d75cf808 | ||
|
|
473fe2885d | ||
|
|
31e1550668 | ||
|
|
315f55bc65 | ||
|
|
e696ee2f6c | ||
|
|
d65aa11870 | ||
|
|
f6630e5b43 | ||
|
|
98e26c5603 | ||
|
|
3098caebbc | ||
|
|
94e23a3cd3 | ||
|
|
1840074991 | ||
|
|
99768e16f5 | ||
|
|
1f8b4679cc | ||
|
|
9e723e942d | ||
|
|
47e4552b44 | ||
|
|
ddcce811c1 | ||
|
|
7e6c36dce1 | ||
|
|
fa154c71f3 | ||
|
|
83a045ed3e | ||
|
|
064817ac70 | ||
|
|
2bd7a24b8c | ||
|
|
ecc2462e4c | ||
|
|
2d8db72d46 | ||
|
|
277a307fb9 | ||
|
|
d455555621 | ||
|
|
9ea2ff1dff | ||
|
|
24cc369d84 | ||
|
|
5f5367f2f9 | ||
|
|
03c31f825a | ||
|
|
c0050e8836 | ||
|
|
15891960f7 | ||
|
|
cce02e2b15 | ||
|
|
0fafda696b | ||
|
|
843db5854c | ||
|
|
23e4e4f373 | ||
|
|
175878f8f1 | ||
|
|
3fcda21c4e | ||
|
|
eac1bf02fc | ||
|
|
a7f2b2145f | ||
|
|
faac56a611 | ||
|
|
be37bc5a68 | ||
|
|
b227c76156 | ||
|
|
db4e3b4d54 | ||
|
|
ffbc37a00c | ||
|
|
e6fa0c5099 | ||
|
|
ce3ceaf5b8 | ||
|
|
efc2e76159 | ||
|
|
7d17715e2e | ||
|
|
aed440bcb3 | ||
|
|
4f2348631a | ||
|
|
da586682f6 | ||
|
|
22ae1f5363 | ||
|
|
c9cf8d7f38 | ||
|
|
cbd3d8b9bc | ||
|
|
f553f38635 | ||
|
|
07373907e3 | ||
|
|
104af58628 | ||
|
|
4ddcee28e4 | ||
|
|
b0d16bbcee | ||
|
|
9175e5f086 | ||
|
|
ff3d571054 | ||
|
|
a8e945f3da | ||
|
|
33547d1d73 | ||
|
|
539c32d99c | ||
|
|
d42c3823c4 | ||
|
|
8ead057ce9 | ||
|
|
886dbcc419 | ||
|
|
b0873edb6a | ||
|
|
694d751915 | ||
|
|
dc15994d40 | ||
|
|
ec24825280 | ||
|
|
08278a8be9 | ||
|
|
5a00b5124b | ||
|
|
8c039f694f | ||
|
|
95bf46c1e4 | ||
|
|
edde35156d | ||
|
|
84e3c32f13 | ||
|
|
ecb848493b | ||
|
|
8a65b9d1c6 | ||
|
|
ed9264fd2a | ||
|
|
7817dda43f | ||
|
|
018e292854 | ||
|
|
127fb1e8f5 | ||
|
|
09f09cb850 | ||
|
|
d4bf377a96 | ||
|
|
abd6fe8c79 | ||
|
|
5f8382ef44 | ||
|
|
03eccd4e42 | ||
|
|
1da8d1c894 | ||
|
|
d1feebf164 | ||
|
|
293b885a85 | ||
|
|
1e0850193a | ||
|
|
33acf56526 | ||
|
|
bea9ad7e8f | ||
|
|
e7ea3aa608 | ||
|
|
5888e248c3 | ||
|
|
2e176339ba | ||
|
|
8f883f3d37 | ||
|
|
709117fe19 | ||
|
|
82ea418423 | ||
|
|
fd24163c6e | ||
|
|
3caef45a12 | ||
|
|
8760e7e5db | ||
|
|
e848226062 | ||
|
|
efaeb77e13 | ||
|
|
569b4cf985 | ||
|
|
6c857243ec | ||
|
|
905e93627c | ||
|
|
598ea21827 | ||
|
|
6012cf4486 | ||
|
|
fe71f84c82 | ||
|
|
763ecbc93e | ||
|
|
97312c1a9d | ||
|
|
baf0e7e658 | ||
|
|
a4ca5f9363 | ||
|
|
dcbea71e67 | ||
|
|
524f19ff77 | ||
|
|
4e33c7143f | ||
|
|
4c184b25f4 | ||
|
|
ddc2309f1e | ||
|
|
bccfcefd17 | ||
|
|
d40252ccce | ||
|
|
4abb5cbfab | ||
|
|
b695936273 | ||
|
|
d9463c7950 | ||
|
|
579542381a | ||
|
|
ce5659f300 | ||
|
|
1967cb642f | ||
|
|
ba0eb04ebe | ||
|
|
609ce6b0d6 | ||
|
|
af61e2d303 | ||
|
|
c6e3f517e1 | ||
|
|
6eaa8d43e9 | ||
|
|
c033cd6254 | ||
|
|
7562578b74 | ||
|
|
43f570e761 | ||
|
|
5e478a32df | ||
|
|
22c94a4d2f | ||
|
|
99dc0f6616 | ||
|
|
4732f568fa | ||
|
|
96d4802cb2 | ||
|
|
ad5b94af5e | ||
|
|
7fce984cac | ||
|
|
404e2f0190 | ||
|
|
1c52eaa3a4 | ||
|
|
c7e04b4146 | ||
|
|
075959aea9 | ||
|
|
885ba2510e | ||
|
|
87563249f1 | ||
|
|
fb1686065d | ||
|
|
0428f5c9bd | ||
|
|
a70c23065c | ||
|
|
caaa4a414d | ||
|
|
c3d841340c | ||
|
|
2e8897c2cf | ||
|
|
b8cd00111f | ||
|
|
81cda80651 | ||
|
|
c1d4f04c22 | ||
|
|
82276cd1ca | ||
|
|
56ea4302ed | ||
|
|
c06112b26e | ||
|
|
aa5a4f0998 | ||
|
|
bf4f471cfd | ||
|
|
978bff9dbc | ||
|
|
869d9af7dd | ||
|
|
af10499ecb | ||
|
|
a1a4d8ff98 | ||
|
|
95d61e8aa2 | ||
|
|
ec8dd1a54f | ||
|
|
382ee34d0e | ||
|
|
0999c9e9ab | ||
|
|
c485968e7f | ||
|
|
e727620bd3 | ||
|
|
71fa3ecebc | ||
|
|
70101d1187 | ||
|
|
c060c205d3 | ||
|
|
5ca900749c | ||
|
|
b005803fe0 | ||
|
|
ec77406dba | ||
|
|
ee15721550 | ||
|
|
038b2efb75 | ||
|
|
1fe4cd03e9 | ||
|
|
12e02e67ff | ||
|
|
b6f57dfb78 | ||
|
|
3ebf2c2d2d | ||
|
|
1bac6f1ee7 | ||
|
|
67e7acd6bd | ||
|
|
910ce573d6 | ||
|
|
689336b3e1 | ||
|
|
01cf72cdef | ||
|
|
4cdb97c699 | ||
|
|
1bd795a9c6 | ||
|
|
39f29e6c30 | ||
|
|
1ab6af21e3 | ||
|
|
5d95c48e0d | ||
|
|
dbb9e474b0 | ||
|
|
f8eed8c786 | ||
|
|
ef010aa39c | ||
|
|
79171ea6f5 | ||
|
|
4e3294b273 | ||
|
|
32a6ecddb6 | ||
|
|
f3d9833ecf | ||
|
|
930ca76ea7 | ||
|
|
9a2887cf46 | ||
|
|
9950914086 | ||
|
|
470cfb0026 | ||
|
|
6c106b4e4d | ||
|
|
3d6253a2b2 | ||
|
|
b873812588 | ||
|
|
514fefd2ed | ||
|
|
6f9ee2d151 | ||
|
|
9832006141 | ||
|
|
890295bbfc | ||
|
|
0413d26855 | ||
|
|
7b29c1f304 | ||
|
|
ae3ef391ee | ||
|
|
7313f996d3 | ||
|
|
62d16c9e56 | ||
|
|
a52e977b89 | ||
|
|
674b41ce08 | ||
|
|
1b833be760 | ||
|
|
88adb1adf5 | ||
|
|
ec472f13cf | ||
|
|
2e1d98cc7c | ||
|
|
07d7e3dc30 | ||
|
|
b0f5aee628 | ||
|
|
d3065612fd | ||
|
|
9912e41f78 | ||
|
|
04200c99a4 | ||
|
|
45666d2c4e | ||
|
|
9a806e64ce | ||
|
|
95e0608749 | ||
|
|
22a09b9795 | ||
|
|
04d5c43550 | ||
|
|
fbcb8cbeb9 | ||
|
|
0338a36ecf | ||
|
|
23fb5e2fca | ||
|
|
3507ff2773 | ||
|
|
a4970397f1 | ||
|
|
4132f6bd48 | ||
|
|
586b3a2ed1 | ||
|
|
6af2addf3c | ||
|
|
f6eed6c441 | ||
|
|
b85837c803 | ||
|
|
653fc40d4c | ||
|
|
c17d80a6fd | ||
|
|
980bfa3aa0 | ||
|
|
664a954393 | ||
|
|
d5a27c4ccb | ||
|
|
6a8a2e2136 | ||
|
|
b859a52b8e | ||
|
|
10e0c42eff | ||
|
|
f47df263d7 | ||
|
|
2642d9109e | ||
|
|
6708b94ebb | ||
|
|
79cf0abc6e | ||
|
|
7de70322d6 | ||
|
|
417835dea8 | ||
|
|
3dcacc4187 | ||
|
|
69f0552d4f | ||
|
|
c443a9400a | ||
|
|
5c9f387d94 | ||
|
|
e9414d17e4 | ||
|
|
6bfa58611e | ||
|
|
df4d3bb6e0 | ||
|
|
e31b6d9a07 | ||
|
|
455ef084b4 | ||
|
|
c2948735f2 | ||
|
|
24c62b2f09 | ||
|
|
1ef0149076 | ||
|
|
922d173540 | ||
|
|
fd088cb504 | ||
|
|
721ee2394e | ||
|
|
c217be06c6 | ||
|
|
871c422ec1 | ||
|
|
3cc28af607 | ||
|
|
796e131c3a | ||
|
|
dd160cd508 | ||
|
|
732b321962 | ||
|
|
c51a769aec | ||
|
|
45a61755a5 | ||
|
|
769c57c355 | ||
|
|
2e7eb7c0fd | ||
|
|
4c83147d01 | ||
|
|
ca0bec4fc2 | ||
|
|
6f50dd17da | ||
|
|
4a331929d0 | ||
|
|
748bc893b6 | ||
|
|
e462602ddc | ||
|
|
4e0f435d12 | ||
|
|
46f0581936 | ||
|
|
20f04ecf6b | ||
|
|
ff43799763 | ||
|
|
85ca197615 | ||
|
|
d06d23bbaf | ||
|
|
702ed85dfd | ||
|
|
8abe74a562 | ||
|
|
2f8a181281 | ||
|
|
5c5287ca21 | ||
|
|
83ba8d5840 | ||
|
|
ce219668cf | ||
|
|
5b1b49a418 | ||
|
|
8978a9ad79 | ||
|
|
5f4a4fd759 | ||
|
|
171c591da4 | ||
|
|
9133b9899c | ||
|
|
701c9fb1b4 | ||
|
|
eabd22188b | ||
|
|
7028619742 | ||
|
|
c915bf2ee2 | ||
|
|
011edd5ac9 | ||
|
|
7ba3de4ced | ||
|
|
8ead77083f | ||
|
|
b2774fb50b | ||
|
|
4440bd46ad | ||
|
|
28985973eb | ||
|
|
f2c4697ca3 | ||
|
|
383b5affb5 | ||
|
|
ed4dcff63b | ||
|
|
caca32bbba | ||
|
|
d31e74c778 | ||
|
|
6c00e29276 | ||
|
|
9940c503a2 | ||
|
|
4b2862cb3c | ||
|
|
a36485f0f1 | ||
|
|
78168ee80a | ||
|
|
610609378f | ||
|
|
260906e350 | ||
|
|
2891bbf82a | ||
|
|
eb26bcbc94 | ||
|
|
ef0f366d1c | ||
|
|
84e230de8f | ||
|
|
f67a12d157 | ||
|
|
34b48eedfc | ||
|
|
0d900d4fc8 | ||
|
|
642ac6d02c | ||
|
|
4db1569c93 | ||
|
|
94c1a6c4e1 | ||
|
|
7ce3b0faed | ||
|
|
262fe04286 | ||
|
|
b1c088a57f | ||
|
|
1c438330c6 | ||
|
|
8cb25709ae | ||
|
|
221f2989b0 | ||
|
|
3d05207bc7 | ||
|
|
8c8497d885 | ||
|
|
56d083ced4 | ||
|
|
a90b3544a7 | ||
|
|
08aea7fb26 | ||
|
|
13f7f9830b | ||
|
|
2f75039194 | ||
|
|
1e192e14f4 | ||
|
|
9cd1f931fc | ||
|
|
8d7235b535 | ||
|
|
8446abd484 | ||
|
|
f67c0530f5 | ||
|
|
06db1d6a72 | ||
|
|
81775ab4d5 | ||
|
|
34877ecf9c | ||
|
|
dbde144014 | ||
|
|
5361a4a4ee | ||
|
|
0997548d7f | ||
|
|
921de02a2b | ||
|
|
48e90a72dc | ||
|
|
c0b7a98e6c | ||
|
|
6dc90186f9 | ||
|
|
0b0a65a3f3 | ||
|
|
6c5d82c4df | ||
|
|
5e66ffa366 | ||
|
|
4d88e19106 | ||
|
|
29e28b47ed | ||
|
|
1cb38bacdb | ||
|
|
169aafec50 | ||
|
|
3826c4b5be | ||
|
|
e1410baaeb | ||
|
|
c39712af67 | ||
|
|
53c35493a5 | ||
|
|
af871fdacb | ||
|
|
2b93b59cdd | ||
|
|
2b2da1679e | ||
|
|
8cdb0b869e | ||
|
|
1e42b8dd21 | ||
|
|
842cb235b6 | ||
|
|
e91d678bd1 | ||
|
|
ef5739c32f | ||
|
|
88bf9b02e1 | ||
|
|
3803b5d351 | ||
|
|
14d58c8163 | ||
|
|
728fcdb375 | ||
|
|
1fc36263dc | ||
|
|
69420113f7 | ||
|
|
360fe03497 | ||
|
|
7557802933 | ||
|
|
2e9ba1e9b3 | ||
|
|
795bcdc5d2 | ||
|
|
ad9b328ed5 | ||
|
|
3d5b57889a | ||
|
|
6b8e981bdc | ||
|
|
2f1eb4b004 | ||
|
|
3ee3d7d969 | ||
|
|
95eb350f15 | ||
|
|
1e5fcfe392 | ||
|
|
527f27d249 | ||
|
|
02557b2098 | ||
|
|
4c7a9ed195 | ||
|
|
d5b30a7a08 | ||
|
|
b7acef4d9d | ||
|
|
fc43c26c48 | ||
|
|
b12ce1eacd | ||
|
|
ec6dbb099a | ||
|
|
2fbbbbe9a9 | ||
|
|
1e4f3c55d8 | ||
|
|
a0f5454c2a | ||
|
|
4e7adacda9 | ||
|
|
4c64cf18a6 | ||
|
|
8a89f5c685 | ||
|
|
cc0e4fee9d | ||
|
|
5861c9af29 | ||
|
|
dd475c0ab3 | ||
|
|
407e9d3584 | ||
|
|
d4f899b091 | ||
|
|
372923ae2f | ||
|
|
3bd01190bf |
No files matched your search
@@ -11,22 +11,35 @@ body:
|
|||||||
required: true
|
required: true
|
||||||
- type: checkboxes
|
- type: checkboxes
|
||||||
attributes:
|
attributes:
|
||||||
label: I've found a bug and checked that ...
|
label: Checklist prior issue creation
|
||||||
description: Prior to placing the issue, please check following:** *(fill out each checkbox with an `X` once done)*
|
description: Prior to creating the issue...
|
||||||
options:
|
options:
|
||||||
- label: ... I understand that not following the below instructions will result in immediate closure and/or deletion of my issue.
|
- label: I understand that failure to follow below instructions may cause this issue to be closed.
|
||||||
required: true
|
required: true
|
||||||
- label: ... I have understood that this bug report is dedicated for bugs, and not for support-related inquiries.
|
- label: I understand that vague, incomplete or inaccurate information may cause this issue to be closed.
|
||||||
required: true
|
required: true
|
||||||
- label: ... I have understood that answers are voluntary and community-driven, and not commercial support.
|
- label: I understand that this form is intended solely for reporting software bugs and not for support-related inquiries.
|
||||||
required: true
|
required: true
|
||||||
- label: ... I have verified that my issue has not been already answered in the past. I also checked previous [issues](https://github.com/mailcow/mailcow-dockerized/issues).
|
- label: I understand that all responses are voluntary and community-driven, and do not constitute commercial support.
|
||||||
|
required: true
|
||||||
|
- label: I confirm that I have reviewed previous [issues](https://github.com/mailcow/mailcow-dockerized/issues) to ensure this matter has not already been addressed.
|
||||||
|
required: true
|
||||||
|
- label: I confirm that my environment meets all [prerequisite requirements](https://docs.mailcow.email/getstarted/prerequisite-system/) as specified in the official documentation.
|
||||||
required: true
|
required: true
|
||||||
- type: textarea
|
- type: textarea
|
||||||
attributes:
|
attributes:
|
||||||
label: Description
|
label: Description
|
||||||
description: Please provide a brief description of the bug in 1-2 sentences. If applicable, add screenshots to help explain your problem. Very useful for bugs in mailcow UI.
|
description: Please provide a brief description of the bug. If applicable, add screenshots to help explain your problem. (Very useful for bugs in mailcow UI.)
|
||||||
render: plain text
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: "Steps to reproduce:"
|
||||||
|
description: "Please describe the steps to reproduce the bug. Screenshots can be added, if helpful."
|
||||||
|
placeholder: |-
|
||||||
|
1. ...
|
||||||
|
2. ...
|
||||||
|
3. ...
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: textarea
|
- type: textarea
|
||||||
@@ -36,45 +49,36 @@ body:
|
|||||||
render: plain text
|
render: plain text
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: textarea
|
|
||||||
attributes:
|
|
||||||
label: "Steps to reproduce:"
|
|
||||||
description: "Please describe the steps to reproduce the bug. Screenshots can be added, if helpful."
|
|
||||||
render: plain text
|
|
||||||
placeholder: |-
|
|
||||||
1. ...
|
|
||||||
2. ...
|
|
||||||
3. ...
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
value: |
|
value: |
|
||||||
## System information
|
## System information
|
||||||
### In this stage we would kindly ask you to attach general system information about your setup.
|
In this stage we would kindly ask you to attach general system information about your setup.
|
||||||
- type: dropdown
|
- type: dropdown
|
||||||
attributes:
|
attributes:
|
||||||
label: "Which branch are you using?"
|
label: "Which branch are you using?"
|
||||||
description: "#### `git rev-parse --abbrev-ref HEAD`"
|
description: "#### Run: `git rev-parse --abbrev-ref HEAD`"
|
||||||
multiple: false
|
multiple: false
|
||||||
options:
|
options:
|
||||||
- master
|
- master (stable)
|
||||||
|
- staging
|
||||||
- nightly
|
- nightly
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: dropdown
|
- type: dropdown
|
||||||
attributes:
|
attributes:
|
||||||
label: "Which architecture are you using?"
|
label: "Which architecture are you using?"
|
||||||
description: "#### `uname -m`"
|
description: "#### Run: `uname -m`"
|
||||||
multiple: false
|
multiple: false
|
||||||
options:
|
options:
|
||||||
- x86
|
- x86_64
|
||||||
- ARM64 (aarch64)
|
- ARM64 (aarch64)
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: input
|
- type: input
|
||||||
attributes:
|
attributes:
|
||||||
label: "Operating System:"
|
label: "Operating System:"
|
||||||
|
description: "#### Run: `lsb_release -ds`"
|
||||||
placeholder: "e.g. Ubuntu 22.04 LTS"
|
placeholder: "e.g. Ubuntu 22.04 LTS"
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
@@ -93,43 +97,44 @@ body:
|
|||||||
- type: input
|
- type: input
|
||||||
attributes:
|
attributes:
|
||||||
label: "Virtualization technology:"
|
label: "Virtualization technology:"
|
||||||
placeholder: "KVM, VMware, Xen, etc - **LXC and OpenVZ are not supported**"
|
description: "LXC and OpenVZ are not supported!"
|
||||||
|
placeholder: "KVM, VMware ESXi, Xen, etc"
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: input
|
- type: input
|
||||||
attributes:
|
attributes:
|
||||||
label: "Docker version:"
|
label: "Docker version:"
|
||||||
description: "#### `docker version`"
|
description: "#### Run: `docker version`"
|
||||||
placeholder: "20.10.21"
|
placeholder: "20.10.21"
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: input
|
- type: input
|
||||||
attributes:
|
attributes:
|
||||||
label: "docker-compose version or docker compose version:"
|
label: "docker-compose version or docker compose version:"
|
||||||
description: "#### `docker-compose version` or `docker compose version`"
|
description: "#### Run: `docker-compose version` or `docker compose version`"
|
||||||
placeholder: "v2.12.2"
|
placeholder: "v2.12.2"
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: input
|
- type: input
|
||||||
attributes:
|
attributes:
|
||||||
label: "mailcow version:"
|
label: "mailcow version:"
|
||||||
description: "#### ```git describe --tags `git rev-list --tags --max-count=1` ```"
|
description: "#### Run: ```git describe --tags `git rev-list --tags --max-count=1` ```"
|
||||||
placeholder: "2022-08"
|
placeholder: "2022-08x"
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: input
|
- type: input
|
||||||
attributes:
|
attributes:
|
||||||
label: "Reverse proxy:"
|
label: "Reverse proxy:"
|
||||||
placeholder: "e.g. Nginx/Traefik"
|
placeholder: "e.g. nginx/Traefik, or none"
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: textarea
|
- type: textarea
|
||||||
attributes:
|
attributes:
|
||||||
label: "Logs of git diff:"
|
label: "Logs of git diff:"
|
||||||
description: "#### Output of `git diff origin/master`, any other changes to the code? If so, **please post them**:"
|
description: "#### Output of `git diff origin/master`, any other changes to the code? Sanitize if needed. If so, **please post them**:"
|
||||||
render: plain text
|
render: plain text
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: false
|
||||||
- type: textarea
|
- type: textarea
|
||||||
attributes:
|
attributes:
|
||||||
label: "Logs of iptables -L -vn:"
|
label: "Logs of iptables -L -vn:"
|
||||||
|
|||||||
@@ -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@v9.1.0
|
uses: actions/stale@v11.0.0
|
||||||
with:
|
with:
|
||||||
repo-token: ${{ secrets.STALE_ACTION_PAT }}
|
repo-token: ${{ secrets.STALE_ACTION_PAT }}
|
||||||
days-before-stale: 60
|
days-before-stale: 60
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ jobs:
|
|||||||
- "watchdog-mailcow"
|
- "watchdog-mailcow"
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v7
|
||||||
- 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@v4
|
uses: actions/checkout@v7
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- name: Run the Action
|
- name: Run the Action
|
||||||
uses: devops-infra/action-pull-request@v0.6.0
|
uses: devops-infra/action-pull-request@v1.4.0
|
||||||
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}}
|
||||||
|
|||||||
@@ -13,24 +13,24 @@ jobs:
|
|||||||
packages: write
|
packages: write
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v7
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v3
|
uses: docker/setup-qemu-action@v4
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v4
|
||||||
|
|
||||||
- name: Login to GHCR
|
- name: Login to GHCR
|
||||||
if: github.event_name != 'pull_request'
|
if: github.event_name != 'pull_request'
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v4
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Build and push
|
- name: Build and push
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@v7
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
|
|||||||
@@ -15,14 +15,14 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v7
|
||||||
|
|
||||||
- name: Generate postscreen_access.cidr
|
- name: Generate postscreen_access.cidr
|
||||||
run: |
|
run: |
|
||||||
bash helper-scripts/update_postscreen_whitelist.sh
|
bash helper-scripts/update_postscreen_whitelist.sh
|
||||||
|
|
||||||
- name: Create Pull Request
|
- name: Create Pull Request
|
||||||
uses: peter-evans/create-pull-request@v7
|
uses: peter-evans/create-pull-request@v8
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.mailcow_action_Update_postscreen_access_cidr_pat }}
|
token: ${{ secrets.mailcow_action_Update_postscreen_access_cidr_pat }}
|
||||||
commit-message: update postscreen_access.cidr
|
commit-message: update postscreen_access.cidr
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ data/conf/sogo/cron.creds
|
|||||||
data/conf/sogo/custom-fulllogo.svg
|
data/conf/sogo/custom-fulllogo.svg
|
||||||
data/conf/sogo/custom-shortlogo.svg
|
data/conf/sogo/custom-shortlogo.svg
|
||||||
data/conf/sogo/custom-fulllogo.png
|
data/conf/sogo/custom-fulllogo.png
|
||||||
|
data/conf/acme/dns-01.conf
|
||||||
data/gitea/
|
data/gitea/
|
||||||
data/gogs/
|
data/gogs/
|
||||||
data/hooks/dovecot/*
|
data/hooks/dovecot/*
|
||||||
@@ -63,6 +64,8 @@ data/web/templates/cache/*
|
|||||||
!data/web/templates/cache/.gitkeep
|
!data/web/templates/cache/.gitkeep
|
||||||
data/web/.well-known/acme-challenge
|
data/web/.well-known/acme-challenge
|
||||||
data/web/css/build/0081-custom-mailcow.css
|
data/web/css/build/0081-custom-mailcow.css
|
||||||
|
data/web/css/themes/*/
|
||||||
|
!data/web/css/themes/servercow/
|
||||||
data/web/inc/vars.local.inc.php
|
data/web/inc/vars.local.inc.php
|
||||||
data/web/inc/app_info.inc.php
|
data/web/inc/app_info.inc.php
|
||||||
data/web/nextcloud*/
|
data/web/nextcloud*/
|
||||||
@@ -75,3 +78,4 @@ refresh_images.sh
|
|||||||
update_diffs/
|
update_diffs/
|
||||||
create_cold_standby.sh
|
create_cold_standby.sh
|
||||||
!data/conf/nginx/mailcow_auth.conf
|
!data/conf/nginx/mailcow_auth.conf
|
||||||
|
data/conf/postfix/postfix-tlspol
|
||||||
+7
-3
@@ -1,11 +1,11 @@
|
|||||||
# Contribution Guidelines
|
# Contribution Guidelines
|
||||||
**_Last modified on 15th August 2024_**
|
**_Last modified on 12th November 2025_**
|
||||||
|
|
||||||
First of all, thank you for wanting to provide a bugfix or a new feature for the mailcow community, it's because of your help that the project can continue to grow!
|
First of all, thank you for wanting to provide a bugfix or a new feature for the mailcow community, it's because of your help that the project can continue to grow!
|
||||||
|
|
||||||
As we want to keep mailcow's development structured we setup these Guidelines which helps you to create your issue/pull request accordingly.
|
As we want to keep mailcow's development structured we setup these Guidelines which helps you to create your issue/pull request accordingly.
|
||||||
|
|
||||||
**PLEASE NOTE, THAT WE MIGHT CLOSE ISSUES/PULL REQUESTS IF THEY DON'T FULLFIL OUR WRITTEN GUIDELINES WRITTEN INSIDE THIS DOCUMENT**. So please check this guidelines before you propose a Issue/Pull Request.
|
**PLEASE NOTE, THAT WE WILL CLOSE ISSUES/PULL REQUESTS IF THEY DON'T FULFILL OUR WRITTEN GUIDELINES WRITTEN INSIDE THIS DOCUMENT**. So please check this guidelines before you propose a Issue/Pull Request.
|
||||||
|
|
||||||
## Topics
|
## Topics
|
||||||
|
|
||||||
@@ -27,14 +27,18 @@ However, please note the following regarding pull requests:
|
|||||||
6. Please **ALWAYS** create the actual pull request against the staging branch and **NEVER** directly against the master branch. *If you forget to do this, our moobot will remind you to switch the branch to staging.*
|
6. Please **ALWAYS** create the actual pull request against the staging branch and **NEVER** directly against the master branch. *If you forget to do this, our moobot will remind you to switch the branch to staging.*
|
||||||
7. Wait for a merge commit: It may happen that we do not accept your pull request immediately or sometimes not at all for various reasons. Please do not be disappointed if this is the case. We always endeavor to incorporate any meaningful changes from the community into the mailcow project.
|
7. Wait for a merge commit: It may happen that we do not accept your pull request immediately or sometimes not at all for various reasons. Please do not be disappointed if this is the case. We always endeavor to incorporate any meaningful changes from the community into the mailcow project.
|
||||||
8. If you are planning larger and therefore more complex pull requests, it would be advisable to first announce this in a separate issue and then start implementing it after the idea has been accepted in order to avoid unnecessary frustration and effort!
|
8. If you are planning larger and therefore more complex pull requests, it would be advisable to first announce this in a separate issue and then start implementing it after the idea has been accepted in order to avoid unnecessary frustration and effort!
|
||||||
|
9. If your PR requires a Docker image rebuild (changes to Dockerfiles or files in data/Dockerfiles/), update the image tag in docker-compose.yml. Use the base-image versioning (e.g. ghcr.io/mailcow/sogo:5.12.4 → :5.12.5 for version bumps; append a letter for patch fixes, e.g. :5.12.4a). Follow this scheme.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Issue Reporting
|
## Issue Reporting
|
||||||
**_Last modified on 15th August 2024_**
|
**_Last modified on 12th November 2025_**
|
||||||
|
|
||||||
If you plan to report a issue within mailcow please read and understand the following rules:
|
If you plan to report a issue within mailcow please read and understand the following rules:
|
||||||
|
|
||||||
|
### Security disclosures / Security-related fixes
|
||||||
|
- Security vulnerabilities and security fixes must always be reported confidentially first to the contact address specified in SECURITY.md before they are integrated, published, or publicly disclosed in issues/PRs. Please wait for a response from the specified contact to ensure coordinated and responsible disclosure.
|
||||||
|
|
||||||
### Issue Reporting Guidelines
|
### Issue Reporting Guidelines
|
||||||
|
|
||||||
1. **ONLY** use the issue tracker for bug reports or improvement requests and NOT for support questions. For support questions you can either contact the [mailcow community on Telegram](https://docs.mailcow.email/#community-support-and-chat) or the mailcow team directly in exchange for a [support fee](https://docs.mailcow.email/#commercial-support).
|
1. **ONLY** use the issue tracker for bug reports or improvement requests and NOT for support questions. For support questions you can either contact the [mailcow community on Telegram](https://docs.mailcow.email/#community-support-and-chat) or the mailcow team directly in exchange for a [support fee](https://docs.mailcow.email/#commercial-support).
|
||||||
|
|||||||
@@ -23,10 +23,15 @@ A big thank you to everyone supporting us on GitHub Sponsors—your contribution
|
|||||||
<a href="https://www.maehdros.com/" target=_blank><img
|
<a href="https://www.maehdros.com/" target=_blank><img
|
||||||
src="https://avatars.githubusercontent.com/u/173894712" height="58"
|
src="https://avatars.githubusercontent.com/u/173894712" height="58"
|
||||||
/></a>
|
/></a>
|
||||||
|
<a href="https://garske-systems.de/" target=_blank><img
|
||||||
|
src="https://garske-systems.de/img/logo_min.png" height="58"
|
||||||
|
/></a>
|
||||||
|
|
||||||
### 50$/Month Sponsors
|
### 50$/Month Sponsors
|
||||||
<a href="https://github.com/vnukhr" target=_blank><img
|
<a href="https://github.com/vnukhr" target=_blank><img
|
||||||
src="https://avatars.githubusercontent.com/u/7805987?s=52&v=4" height="58"
|
src="https://avatars.githubusercontent.com/u/7805987?s=52&v=4" height="58"
|
||||||
|
/></a><a href="https://github.com/humiico" target=_blank><img
|
||||||
|
src="https://avatars.githubusercontent.com/u/110134705?s=52&v=4" height="58"
|
||||||
/></a>
|
/></a>
|
||||||
|
|
||||||
## Info, documentation and support
|
## Info, documentation and support
|
||||||
|
|||||||
@@ -0,0 +1,230 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# _modules/scripts/core.sh
|
||||||
|
# THIS SCRIPT IS DESIGNED TO BE RUNNING BY MAILCOW SCRIPTS ONLY!
|
||||||
|
# DO NOT, AGAIN, NOT TRY TO RUN THIS SCRIPT STANDALONE!!!!!!
|
||||||
|
|
||||||
|
# ANSI color for red errors
|
||||||
|
RED='\e[31m'
|
||||||
|
GREEN='\e[32m'
|
||||||
|
YELLOW='\e[33m'
|
||||||
|
BLUE='\e[34m'
|
||||||
|
MAGENTA='\e[35m'
|
||||||
|
LIGHT_RED='\e[91m'
|
||||||
|
LIGHT_GREEN='\e[92m'
|
||||||
|
NC='\e[0m'
|
||||||
|
|
||||||
|
caller="${BASH_SOURCE[1]##*/}"
|
||||||
|
|
||||||
|
get_installed_tools(){
|
||||||
|
for bin in openssl curl docker git awk sha1sum grep cut jq; do
|
||||||
|
if [[ -z $(command -v ${bin}) ]]; then
|
||||||
|
echo "Error: Cannot find command '${bin}'. Cannot proceed."
|
||||||
|
echo "Solution: Please review system requirements and install requirements. Then, re-run the script."
|
||||||
|
echo "See System Requirements: https://docs.mailcow.email/getstarted/install/"
|
||||||
|
echo "Exiting..."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if grep --help 2>&1 | head -n 1 | grep -q -i "busybox"; then echo -e "${LIGHT_RED}BusyBox grep detected, please install gnu grep, \"apk add --no-cache --upgrade grep\"${NC}"; exit 1; fi
|
||||||
|
# This will also cover sort
|
||||||
|
if cp --help 2>&1 | head -n 1 | grep -q -i "busybox"; then echo -e "${LIGHT_RED}BusyBox cp detected, please install coreutils, \"apk add --no-cache --upgrade coreutils\"${NC}"; exit 1; fi
|
||||||
|
if sed --help 2>&1 | head -n 1 | grep -q -i "busybox"; then echo -e "${LIGHT_RED}BusyBox sed detected, please install gnu sed, \"apk add --no-cache --upgrade sed\"${NC}"; exit 1; fi
|
||||||
|
}
|
||||||
|
|
||||||
|
get_docker_version(){
|
||||||
|
# Check Docker Version (need at least 24.X)
|
||||||
|
docker_version=$(docker version --format '{{.Server.Version}}' | cut -d '.' -f 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
get_compose_type(){
|
||||||
|
if docker compose > /dev/null 2>&1; then
|
||||||
|
if docker compose version --short | grep -e "^[2-9]\." -e "^v[2-9]\." -e "^[1-9][0-9]\." -e "^v[1-9][0-9]\." > /dev/null 2>&1; then
|
||||||
|
COMPOSE_VERSION=native
|
||||||
|
COMPOSE_COMMAND="docker compose"
|
||||||
|
if [[ "$caller" == "update.sh" ]]; then
|
||||||
|
sed -i 's/^DOCKER_COMPOSE_VERSION=.*/DOCKER_COMPOSE_VERSION=native/' "$SCRIPT_DIR/mailcow.conf"
|
||||||
|
fi
|
||||||
|
echo -e "\e[33mFound Docker Compose Plugin (native).\e[0m"
|
||||||
|
echo -e "\e[33mSetting the DOCKER_COMPOSE_VERSION Variable to native\e[0m"
|
||||||
|
sleep 2
|
||||||
|
echo -e "\e[33mNotice: You'll have to update this Compose Version via your Package Manager manually!\e[0m"
|
||||||
|
else
|
||||||
|
echo -e "\e[31mCannot find Docker Compose with a Version Higher than 2.X.X.\e[0m"
|
||||||
|
echo -e "\e[31mPlease update/install it manually regarding to this doc site: https://docs.mailcow.email/install/\e[0m"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
elif docker-compose > /dev/null 2>&1; then
|
||||||
|
if ! [[ $(alias docker-compose 2> /dev/null) ]] ; then
|
||||||
|
if docker-compose version --short | grep -e "^[2-9]\." -e "^[1-9][0-9]\." > /dev/null 2>&1; then
|
||||||
|
COMPOSE_VERSION=standalone
|
||||||
|
COMPOSE_COMMAND="docker-compose"
|
||||||
|
if [[ "$caller" == "update.sh" ]]; then
|
||||||
|
sed -i 's/^DOCKER_COMPOSE_VERSION=.*/DOCKER_COMPOSE_VERSION=standalone/' "$SCRIPT_DIR/mailcow.conf"
|
||||||
|
fi
|
||||||
|
echo -e "\e[33mFound Docker Compose Standalone.\e[0m"
|
||||||
|
echo -e "\e[33mSetting the DOCKER_COMPOSE_VERSION Variable to standalone\e[0m"
|
||||||
|
sleep 2
|
||||||
|
echo -e "\e[33mNotice: For an automatic update of docker-compose please use the update_compose.sh scripts located at the helper-scripts folder.\e[0m"
|
||||||
|
else
|
||||||
|
echo -e "\e[31mCannot find Docker Compose with a Version Higher than 2.X.X.\e[0m"
|
||||||
|
echo -e "\e[31mPlease update/install manually regarding to this doc site: https://docs.mailcow.email/install/\e[0m"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo -e "\e[31mCannot find Docker Compose.\e[0m"
|
||||||
|
echo -e "\e[31mPlease install it regarding to this doc site: https://docs.mailcow.email/install/\e[0m"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
detect_bad_asn() {
|
||||||
|
echo -e "\e[33mDetecting if your IP is listed on Spamhaus Bad ASN List...\e[0m"
|
||||||
|
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
|
||||||
|
if [ -z "$SPAMHAUS_DQS_KEY" ]; then
|
||||||
|
echo -e "\e[33mYour server's public IP uses an AS that is blocked by Spamhaus to use their DNS public blocklists for Postfix.\e[0m"
|
||||||
|
echo -e "\e[33mmailcow did not detected a value for the variable SPAMHAUS_DQS_KEY inside mailcow.conf!\e[0m"
|
||||||
|
sleep 2
|
||||||
|
echo ""
|
||||||
|
echo -e "\e[33mTo use the Spamhaus DNS Blocklists again, you will need to create a FREE account for their Data Query Service (DQS) at: https://www.spamhaus.com/free-trial/sign-up-for-a-free-data-query-service-account\e[0m"
|
||||||
|
echo -e "\e[33mOnce done, enter your DQS API key in mailcow.conf and mailcow will do the rest for you!\e[0m"
|
||||||
|
echo ""
|
||||||
|
sleep 2
|
||||||
|
else
|
||||||
|
echo -e "\e[33mYour server's public IP uses an AS that is blocked by Spamhaus to use their DNS public blocklists for Postfix.\e[0m"
|
||||||
|
echo -e "\e[32mmailcow detected a Value for the variable SPAMHAUS_DQS_KEY inside mailcow.conf. Postfix will use DQS with the given API key...\e[0m"
|
||||||
|
fi
|
||||||
|
elif [ "$response" -eq 200 ]; then
|
||||||
|
echo -e "\e[33mCheck completed! Your IP is \e[32mclean\e[0m"
|
||||||
|
elif [ "$response" -eq 429 ]; then
|
||||||
|
echo -e "\e[33mCheck completed! \e[31mYour IP seems to be rate limited on the ASN Check service... please try again later!\e[0m"
|
||||||
|
else
|
||||||
|
echo -e "\e[31mCheck failed! \e[0mMaybe a DNS or Network problem?\e[0m"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
check_online_status() {
|
||||||
|
CHECK_ONLINE_DOMAINS=('https://github.com' 'https://hub.docker.com')
|
||||||
|
for domain in "${CHECK_ONLINE_DOMAINS[@]}"; do
|
||||||
|
if timeout 6 curl --head --silent --output /dev/null ${domain}; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
prefetch_images() {
|
||||||
|
[[ -z ${BRANCH} ]] && { echo -e "\e[33m\nUnknown branch...\e[0m"; exit 1; }
|
||||||
|
git fetch origin #${BRANCH}
|
||||||
|
while read image; do
|
||||||
|
RET_C=0
|
||||||
|
until docker pull "${image}"; do
|
||||||
|
RET_C=$((RET_C + 1))
|
||||||
|
echo -e "\e[33m\nError pulling $image, retrying...\e[0m"
|
||||||
|
[ ${RET_C} -gt 3 ] && { echo -e "\e[31m\nToo many failed retries, exiting\e[0m"; exit 1; }
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
done < <(git show "origin/${BRANCH}:docker-compose.yml" | grep "image:" | awk '{ gsub("image:","", $3); print $2 }')
|
||||||
|
}
|
||||||
|
|
||||||
|
docker_garbage() {
|
||||||
|
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )/../.." && pwd )"
|
||||||
|
IMGS_TO_DELETE=()
|
||||||
|
|
||||||
|
declare -A IMAGES_INFO
|
||||||
|
COMPOSE_IMAGES=($(grep -oP "image: \K(ghcr\.io/)?mailcow.+" "${SCRIPT_DIR}/docker-compose.yml"))
|
||||||
|
|
||||||
|
for existing_image in $(docker images --format "{{.ID}}:{{.Repository}}:{{.Tag}}" | grep -E '(mailcow/|ghcr\.io/mailcow/)'); do
|
||||||
|
ID=$(echo "$existing_image" | cut -d ':' -f 1)
|
||||||
|
REPOSITORY=$(echo "$existing_image" | cut -d ':' -f 2)
|
||||||
|
TAG=$(echo "$existing_image" | cut -d ':' -f 3)
|
||||||
|
|
||||||
|
if [[ "$REPOSITORY" == "mailcow/backup" || "$REPOSITORY" == "ghcr.io/mailcow/backup" ]]; then
|
||||||
|
if [[ "$TAG" != "<none>" ]]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ " ${COMPOSE_IMAGES[@]} " =~ " ${REPOSITORY}:${TAG} " ]]; then
|
||||||
|
continue
|
||||||
|
else
|
||||||
|
IMGS_TO_DELETE+=("$ID")
|
||||||
|
IMAGES_INFO["$ID"]="$REPOSITORY:$TAG"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ ! -z ${IMGS_TO_DELETE[*]} ]]; then
|
||||||
|
echo "The following unused mailcow images were found:"
|
||||||
|
for id in "${IMGS_TO_DELETE[@]}"; do
|
||||||
|
echo " ${IMAGES_INFO[$id]} ($id)"
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ -z "$FORCE" ]; then
|
||||||
|
read -r -p "Do you want to delete them to free up some space? [y/N] " response
|
||||||
|
if [[ "$response" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
|
||||||
|
docker rmi ${IMGS_TO_DELETE[*]}
|
||||||
|
else
|
||||||
|
echo "OK, skipped."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "Running in forced mode! Force removing old mailcow images..."
|
||||||
|
docker rmi ${IMGS_TO_DELETE[*]}
|
||||||
|
fi
|
||||||
|
echo -e "\e[32mFurther cleanup...\e[0m"
|
||||||
|
echo "If you want to cleanup further garbage collected by Docker, please make sure all containers are up and running before cleaning your system by executing \"docker system prune\""
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
in_array() {
|
||||||
|
local e match="$1"
|
||||||
|
shift
|
||||||
|
for e; do [[ "$e" == "$match" ]] && return 0; done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
detect_major_update() {
|
||||||
|
if [ ${BRANCH} == "master" ]; then
|
||||||
|
# Array with major versions
|
||||||
|
# Add major versions here
|
||||||
|
MAJOR_VERSIONS=(
|
||||||
|
"2025-02"
|
||||||
|
"2025-03"
|
||||||
|
"2025-09"
|
||||||
|
)
|
||||||
|
|
||||||
|
current_version=""
|
||||||
|
if [[ -f "${SCRIPT_DIR}/data/web/inc/app_info.inc.php" ]]; then
|
||||||
|
current_version=$(grep 'MAILCOW_GIT_VERSION' ${SCRIPT_DIR}/data/web/inc/app_info.inc.php | sed -E 's/.*MAILCOW_GIT_VERSION="([^"]+)".*/\1/')
|
||||||
|
fi
|
||||||
|
if [[ -z "$current_version" ]]; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
release_url="https://github.com/mailcow/mailcow-dockerized/releases/tag"
|
||||||
|
|
||||||
|
updates_to_apply=()
|
||||||
|
|
||||||
|
for version in "${MAJOR_VERSIONS[@]}"; do
|
||||||
|
if [[ "$current_version" < "$version" ]]; then
|
||||||
|
updates_to_apply+=("$version")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ ${#updates_to_apply[@]} -gt 0 ]]; then
|
||||||
|
echo -e "\e[33m\nMAJOR UPDATES to be applied:\e[0m"
|
||||||
|
for update in "${updates_to_apply[@]}"; do
|
||||||
|
echo "$update - $release_url/$update"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo -e "\nPlease read the release notes before proceeding."
|
||||||
|
read -p "Do you want to proceed with the update? [y/n] " response
|
||||||
|
if [[ "${response}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
|
||||||
|
echo "Proceeding with the update..."
|
||||||
|
else
|
||||||
|
echo "Update canceled. Exiting."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
@@ -0,0 +1,239 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# _modules/scripts/ipv6_controller.sh
|
||||||
|
# THIS SCRIPT IS DESIGNED TO BE RUNNING BY MAILCOW SCRIPTS ONLY!
|
||||||
|
# DO NOT, AGAIN, NOT TRY TO RUN THIS SCRIPT STANDALONE!!!!!!
|
||||||
|
|
||||||
|
# 1) Check if the host supports IPv6
|
||||||
|
get_ipv6_support() {
|
||||||
|
# ---- helper: probe external IPv6 connectivity without DNS ----
|
||||||
|
_probe_ipv6_connectivity() {
|
||||||
|
# Use literal, always-on IPv6 echo responders (no DNS required)
|
||||||
|
local PROBE_IPS=("2001:4860:4860::8888" "2606:4700:4700::1111")
|
||||||
|
local ip rc=1
|
||||||
|
|
||||||
|
for ip in "${PROBE_IPS[@]}"; do
|
||||||
|
if command -v ping6 &>/dev/null; then
|
||||||
|
ping6 -c1 -W2 "$ip" &>/dev/null || ping6 -c1 -w2 "$ip" &>/dev/null
|
||||||
|
rc=$?
|
||||||
|
elif command -v ping &>/dev/null; then
|
||||||
|
ping -6 -c1 -W2 "$ip" &>/dev/null || ping -6 -c1 -w2 "$ip" &>/dev/null
|
||||||
|
rc=$?
|
||||||
|
else
|
||||||
|
rc=1
|
||||||
|
fi
|
||||||
|
[[ $rc -eq 0 ]] && return 0
|
||||||
|
done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if [[ ! -f /proc/net/if_inet6 ]] || grep -qs '^1' /proc/sys/net/ipv6/conf/all/disable_ipv6 2>/dev/null; then
|
||||||
|
DETECTED_IPV6=false
|
||||||
|
echo -e "${YELLOW}IPv6 not detected on host – ${LIGHT_RED}IPv6 is administratively disabled${YELLOW}.${NC}"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ip -6 route show default 2>/dev/null | grep -qE '^default'; then
|
||||||
|
echo -e "${YELLOW}Default IPv6 route found – testing external IPv6 connectivity...${NC}"
|
||||||
|
if _probe_ipv6_connectivity; then
|
||||||
|
DETECTED_IPV6=true
|
||||||
|
echo -e "IPv6 detected on host – ${LIGHT_GREEN}leaving IPv6 support enabled${YELLOW}.${NC}"
|
||||||
|
else
|
||||||
|
DETECTED_IPV6=false
|
||||||
|
echo -e "${YELLOW}Default IPv6 route present but external IPv6 connectivity failed – ${LIGHT_RED}disabling IPv6 support${YELLOW}.${NC}"
|
||||||
|
fi
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ip -6 addr show scope global 2>/dev/null | grep -q 'inet6'; then
|
||||||
|
DETECTED_IPV6=false
|
||||||
|
echo -e "${YELLOW}Global IPv6 address present but no default route – ${LIGHT_RED}disabling IPv6 support${YELLOW}.${NC}"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ip -6 addr show scope link 2>/dev/null | grep -q 'inet6'; then
|
||||||
|
echo -e "${YELLOW}Only link-local IPv6 addresses found – testing external IPv6 connectivity...${NC}"
|
||||||
|
if _probe_ipv6_connectivity; then
|
||||||
|
DETECTED_IPV6=true
|
||||||
|
echo -e "External IPv6 connectivity available – ${LIGHT_GREEN}leaving IPv6 support enabled${YELLOW}.${NC}"
|
||||||
|
else
|
||||||
|
DETECTED_IPV6=false
|
||||||
|
echo -e "${YELLOW}Only link-local IPv6 present and no external connectivity – ${LIGHT_RED}disabling IPv6 support${YELLOW}.${NC}"
|
||||||
|
fi
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
DETECTED_IPV6=false
|
||||||
|
echo -e "${YELLOW}IPv6 not detected on host – ${LIGHT_RED}disabling IPv6 support${YELLOW}.${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 2) Ensure Docker daemon.json has (or create) the required IPv6 settings
|
||||||
|
docker_daemon_edit(){
|
||||||
|
DOCKER_DAEMON_CONFIG="/etc/docker/daemon.json"
|
||||||
|
DOCKER_MAJOR=$(docker version --format '{{.Server.Version}}' 2>/dev/null | cut -d. -f1)
|
||||||
|
MISSING=()
|
||||||
|
|
||||||
|
_has_kv() { grep -Eq "\"$1\"[[:space:]]*:[[:space:]]*$2" "$DOCKER_DAEMON_CONFIG" 2>/dev/null; }
|
||||||
|
|
||||||
|
if [[ -f "$DOCKER_DAEMON_CONFIG" ]]; then
|
||||||
|
|
||||||
|
# reject empty or whitespace-only file immediately
|
||||||
|
if [[ ! -s "$DOCKER_DAEMON_CONFIG" ]] || ! grep -Eq '[{}]' "$DOCKER_DAEMON_CONFIG"; then
|
||||||
|
echo -e "${RED}ERROR: $DOCKER_DAEMON_CONFIG exists but is empty or contains no JSON braces – please initialize it with valid JSON (e.g. {}).${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Validate JSON if jq is present
|
||||||
|
if command -v jq &>/dev/null && ! jq empty "$DOCKER_DAEMON_CONFIG" &>/dev/null; then
|
||||||
|
echo -e "${RED}ERROR: Invalid JSON in $DOCKER_DAEMON_CONFIG – please correct manually.${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Gather missing keys
|
||||||
|
! _has_kv ipv6 true && MISSING+=("ipv6: true")
|
||||||
|
|
||||||
|
# For Docker < 28, keep requiring fixed-cidr-v6 (default bridge needs it on old engines)
|
||||||
|
if [[ -n "$DOCKER_MAJOR" && "$DOCKER_MAJOR" -lt 28 ]]; then
|
||||||
|
! grep -Eq '"fixed-cidr-v6"[[:space:]]*:[[:space:]]*".+"' "$DOCKER_DAEMON_CONFIG" \
|
||||||
|
&& MISSING+=('fixed-cidr-v6: "fd00:dead:beef:c0::/80"')
|
||||||
|
fi
|
||||||
|
|
||||||
|
# For Docker < 27, ip6tables needed and was tied to experimental in older releases
|
||||||
|
if [[ -n "$DOCKER_MAJOR" && "$DOCKER_MAJOR" -lt 27 ]]; then
|
||||||
|
_has_kv ipv6 true && ! _has_kv ip6tables true && MISSING+=("ip6tables: true")
|
||||||
|
! _has_kv experimental true && MISSING+=("experimental: true")
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Fix if needed
|
||||||
|
if ((${#MISSING[@]}>0)); then
|
||||||
|
echo -e "${MAGENTA}Your daemon.json is missing: ${YELLOW}${MISSING[*]}${NC}"
|
||||||
|
if [[ -n "$FORCE" ]]; then
|
||||||
|
ans=Y
|
||||||
|
else
|
||||||
|
read -p "Would you like to update $DOCKER_DAEMON_CONFIG now? [Y/n] " ans
|
||||||
|
ans=${ans:-Y}
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ $ans =~ ^[Yy]$ ]]; then
|
||||||
|
cp "$DOCKER_DAEMON_CONFIG" "${DOCKER_DAEMON_CONFIG}.bak"
|
||||||
|
if command -v jq &>/dev/null; then
|
||||||
|
TMP=$(mktemp)
|
||||||
|
# Base filter: ensure ipv6 = true
|
||||||
|
JQ_FILTER='.ipv6 = true'
|
||||||
|
|
||||||
|
# Add fixed-cidr-v6 only for Docker < 28
|
||||||
|
if [[ -n "$DOCKER_MAJOR" && "$DOCKER_MAJOR" -lt 28 ]]; then
|
||||||
|
JQ_FILTER+=' | .["fixed-cidr-v6"] = (.["fixed-cidr-v6"] // "fd00:dead:beef:c0::/80")'
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Add ip6tables/experimental only for Docker < 27
|
||||||
|
if [[ -n "$DOCKER_MAJOR" && "$DOCKER_MAJOR" -lt 27 ]]; then
|
||||||
|
JQ_FILTER+=' | .ip6tables = true | .experimental = true'
|
||||||
|
fi
|
||||||
|
|
||||||
|
jq "$JQ_FILTER" "$DOCKER_DAEMON_CONFIG" >"$TMP" && mv "$TMP" "$DOCKER_DAEMON_CONFIG"
|
||||||
|
echo -e "${LIGHT_GREEN}daemon.json updated. Restarting Docker...${NC}"
|
||||||
|
(command -v systemctl &>/dev/null && systemctl restart docker) || service docker restart
|
||||||
|
echo -e "${YELLOW}Docker restarted.${NC}"
|
||||||
|
else
|
||||||
|
echo -e "${RED}Please install jq or manually update daemon.json and restart Docker.${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo -e "${YELLOW}User declined Docker update – please insert these changes manually:${NC}"
|
||||||
|
echo "${MISSING[*]}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
else
|
||||||
|
# Create new daemon.json if missing
|
||||||
|
if [[ -n "$FORCE" ]]; then
|
||||||
|
ans=Y
|
||||||
|
else
|
||||||
|
read -p "$DOCKER_DAEMON_CONFIG not found. Create it with IPv6 settings? [Y/n] " ans
|
||||||
|
ans=${ans:-Y}
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ $ans =~ ^[Yy]$ ]]; then
|
||||||
|
mkdir -p "$(dirname "$DOCKER_DAEMON_CONFIG")"
|
||||||
|
if [[ -n "$DOCKER_MAJOR" && "$DOCKER_MAJOR" -lt 27 ]]; then
|
||||||
|
cat > "$DOCKER_DAEMON_CONFIG" <<EOF
|
||||||
|
{
|
||||||
|
"ipv6": true,
|
||||||
|
"fixed-cidr-v6": "fd00:dead:beef:c0::/80",
|
||||||
|
"ip6tables": true,
|
||||||
|
"experimental": true
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
elif [[ -n "$DOCKER_MAJOR" && "$DOCKER_MAJOR" -lt 28 ]]; then
|
||||||
|
cat > "$DOCKER_DAEMON_CONFIG" <<EOF
|
||||||
|
{
|
||||||
|
"ipv6": true,
|
||||||
|
"fixed-cidr-v6": "fd00:dead:beef:c0::/80"
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
else
|
||||||
|
# Docker 28+: ipv6 works without fixed-cidr-v6
|
||||||
|
cat > "$DOCKER_DAEMON_CONFIG" <<EOF
|
||||||
|
{
|
||||||
|
"ipv6": true
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
fi
|
||||||
|
echo -e "${GREEN}Created $DOCKER_DAEMON_CONFIG with IPv6 settings.${NC}"
|
||||||
|
echo "Restarting Docker..."
|
||||||
|
(command -v systemctl &>/dev/null && systemctl restart docker) || service docker restart
|
||||||
|
echo "Docker restarted."
|
||||||
|
else
|
||||||
|
echo "User declined to create daemon.json – please manually merge the docker daemon with these configs:"
|
||||||
|
echo "${MISSING[*]}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# 3) Main wrapper for generate_config.sh and update.sh
|
||||||
|
configure_ipv6() {
|
||||||
|
# detect manual override if mailcow.conf is present
|
||||||
|
if [[ -n "$MAILCOW_CONF" && -f "$MAILCOW_CONF" ]] && grep -q '^ENABLE_IPV6=' "$MAILCOW_CONF"; then
|
||||||
|
MANUAL_SETTING=$(grep '^ENABLE_IPV6=' "$MAILCOW_CONF" | cut -d= -f2)
|
||||||
|
elif [[ -z "$MAILCOW_CONF" ]] && [[ -n "${ENABLE_IPV6:-}" ]]; then
|
||||||
|
MANUAL_SETTING="$ENABLE_IPV6"
|
||||||
|
else
|
||||||
|
MANUAL_SETTING=""
|
||||||
|
fi
|
||||||
|
|
||||||
|
get_ipv6_support
|
||||||
|
|
||||||
|
# if user manually set it, check for mismatch
|
||||||
|
if [[ "$DETECTED_IPV6" != "true" ]]; then
|
||||||
|
if [[ -n "$MAILCOW_CONF" && -f "$MAILCOW_CONF" ]]; then
|
||||||
|
if grep -q '^ENABLE_IPV6=' "$MAILCOW_CONF"; then
|
||||||
|
sed -i 's/^ENABLE_IPV6=.*/ENABLE_IPV6=false/' "$MAILCOW_CONF"
|
||||||
|
else
|
||||||
|
echo "ENABLE_IPV6=false" >> "$MAILCOW_CONF"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
export IPV6_BOOL=false
|
||||||
|
fi
|
||||||
|
echo "Skipping Docker IPv6 configuration because host does not support IPv6."
|
||||||
|
echo "Make sure to check if your docker daemon.json does not include \"enable_ipv6\": true if you do not want IPv6."
|
||||||
|
echo "IPv6 configuration complete: ENABLE_IPV6=false"
|
||||||
|
sleep 2
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
docker_daemon_edit
|
||||||
|
|
||||||
|
if [[ -n "$MAILCOW_CONF" && -f "$MAILCOW_CONF" ]]; then
|
||||||
|
if grep -q '^ENABLE_IPV6=' "$MAILCOW_CONF"; then
|
||||||
|
sed -i 's/^ENABLE_IPV6=.*/ENABLE_IPV6=true/' "$MAILCOW_CONF"
|
||||||
|
else
|
||||||
|
echo "ENABLE_IPV6=true" >> "$MAILCOW_CONF"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
export IPV6_BOOL=true
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "IPv6 configuration complete: ENABLE_IPV6=true"
|
||||||
|
}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# _modules/scripts/migrate_options.sh
|
||||||
|
# THIS SCRIPT IS DESIGNED TO BE RUNNING BY MAILCOW SCRIPTS ONLY!
|
||||||
|
# DO NOT, AGAIN, NOT TRY TO RUN THIS SCRIPT STANDALONE!!!!!!
|
||||||
|
|
||||||
|
migrate_config_options() {
|
||||||
|
|
||||||
|
sed -i --follow-symlinks '$a\' mailcow.conf
|
||||||
|
|
||||||
|
KEYS=(
|
||||||
|
SOLR_HEAP
|
||||||
|
SKIP_SOLR
|
||||||
|
SOLR_PORT
|
||||||
|
FLATCURVE_EXPERIMENTAL
|
||||||
|
DISABLE_IPv6
|
||||||
|
ACME_CONTACT
|
||||||
|
)
|
||||||
|
|
||||||
|
for key in "${KEYS[@]}"; do
|
||||||
|
if grep -q "${key}" mailcow.conf; then
|
||||||
|
case "${key}" in
|
||||||
|
SOLR_HEAP)
|
||||||
|
echo "Removing ${key} in mailcow.conf"
|
||||||
|
sed -i '/# Solr heap size in MB\b/d' mailcow.conf
|
||||||
|
sed -i '/# Solr is a prone to run\b/d' mailcow.conf
|
||||||
|
sed -i '/SOLR_HEAP\b/d' mailcow.conf
|
||||||
|
;;
|
||||||
|
SKIP_SOLR)
|
||||||
|
echo "Removing ${key} in mailcow.conf"
|
||||||
|
sed -i '/\bSkip Solr on low-memory\b/d' mailcow.conf
|
||||||
|
sed -i '/\bSolr is disabled by default\b/d' mailcow.conf
|
||||||
|
sed -i '/\bDisable Solr or\b/d' mailcow.conf
|
||||||
|
sed -i '/\bSKIP_SOLR\b/d' mailcow.conf
|
||||||
|
;;
|
||||||
|
SOLR_PORT)
|
||||||
|
echo "Removing ${key} in mailcow.conf"
|
||||||
|
sed -i '/\bSOLR_PORT\b/d' mailcow.conf
|
||||||
|
;;
|
||||||
|
FLATCURVE_EXPERIMENTAL)
|
||||||
|
echo "Removing ${key} in mailcow.conf"
|
||||||
|
sed -i '/\bFLATCURVE_EXPERIMENTAL\b/d' mailcow.conf
|
||||||
|
;;
|
||||||
|
DISABLE_IPv6)
|
||||||
|
echo "Migrating ${key} to ENABLE_IPv6 in mailcow.conf"
|
||||||
|
local old=$(grep '^DISABLE_IPv6=' "mailcow.conf" | cut -d'=' -f2)
|
||||||
|
local new
|
||||||
|
if [[ "$old" == "y" ]]; then
|
||||||
|
new="false"
|
||||||
|
else
|
||||||
|
new="true"
|
||||||
|
fi
|
||||||
|
sed -i '/^DISABLE_IPv6=/d' "mailcow.conf"
|
||||||
|
echo "ENABLE_IPV6=$new" >> "mailcow.conf"
|
||||||
|
;;
|
||||||
|
ACME_CONTACT)
|
||||||
|
echo "Deleting obsoleted ${key} in mailcow.conf"
|
||||||
|
sed -i '/^# Lets Encrypt registration contact information/d' mailcow.conf
|
||||||
|
sed -i '/^# Optional: Leave empty for none/d' mailcow.conf
|
||||||
|
sed -i '/^# This value is only used on first order!/d' mailcow.conf
|
||||||
|
sed -i '/^# Setting it at a later point will require the following steps:/d' mailcow.conf
|
||||||
|
sed -i '/^# https:\/\/docs.mailcow.email\/troubleshooting\/debug-reset_tls\//d' mailcow.conf
|
||||||
|
sed -i '/^ACME_CONTACT=.*/d' mailcow.conf
|
||||||
|
sed -i '/^#ACME_CONTACT=.*/d' mailcow.conf
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
solr_volume=$(docker volume ls -qf name=^${COMPOSE_PROJECT_NAME}_solr-vol-1)
|
||||||
|
if [[ -n $solr_volume ]]; then
|
||||||
|
echo -e "\e[34mSolr has been replaced within mailcow since 2025-01.\nThe volume $solr_volume is unused.\e[0m"
|
||||||
|
sleep 1
|
||||||
|
if [ ! "$FORCE" ]; then
|
||||||
|
read -r -p "Remove $solr_volume? [y/N] " response
|
||||||
|
if [[ "$response" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
|
||||||
|
echo -e "\e[33mRemoving $solr_volume...\e[0m"
|
||||||
|
docker volume rm $solr_volume || echo -e "\e[31mFailed to remove. Remove it manually!\e[0m"
|
||||||
|
echo -e "\e[32mSuccessfully removed $solr_volume!\e[0m"
|
||||||
|
else
|
||||||
|
echo -e "Not removing $solr_volume. Run \`docker volume rm $solr_volume\` manually if needed."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo -e "\e[33mForce removing $solr_volume...\e[0m"
|
||||||
|
docker volume rm $solr_volume || echo -e "\e[31mFailed to remove. Remove it manually!\e[0m"
|
||||||
|
echo -e "\e[32mSuccessfully removed $solr_volume!\e[0m"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Delete old fts.conf before forced switch to flatcurve to ensure update is working properly
|
||||||
|
FTS_CONF_PATH="${SCRIPT_DIR}/data/conf/dovecot/conf.d/fts.conf"
|
||||||
|
if [[ -f "$FTS_CONF_PATH" ]]; then
|
||||||
|
if grep -q "Autogenerated by mailcow" "$FTS_CONF_PATH"; then
|
||||||
|
rm -rf $FTS_CONF_PATH
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
@@ -0,0 +1,317 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# _modules/scripts/new_options.sh
|
||||||
|
# THIS SCRIPT IS DESIGNED TO BE RUNNING BY MAILCOW SCRIPTS ONLY!
|
||||||
|
# DO NOT, AGAIN, NOT TRY TO RUN THIS SCRIPT STANDALONE!!!!!!
|
||||||
|
|
||||||
|
adapt_new_options() {
|
||||||
|
|
||||||
|
CONFIG_ARRAY=(
|
||||||
|
"AUTODISCOVER_SAN"
|
||||||
|
"SKIP_LETS_ENCRYPT"
|
||||||
|
"SKIP_SOGO"
|
||||||
|
"USE_WATCHDOG"
|
||||||
|
"WATCHDOG_NOTIFY_EMAIL"
|
||||||
|
"WATCHDOG_NOTIFY_WEBHOOK"
|
||||||
|
"WATCHDOG_NOTIFY_WEBHOOK_BODY"
|
||||||
|
"WATCHDOG_NOTIFY_BAN"
|
||||||
|
"WATCHDOG_NOTIFY_START"
|
||||||
|
"WATCHDOG_EXTERNAL_CHECKS"
|
||||||
|
"WATCHDOG_SUBJECT"
|
||||||
|
"SKIP_CLAMD"
|
||||||
|
"SKIP_OLEFY"
|
||||||
|
"SKIP_IP_CHECK"
|
||||||
|
"ADDITIONAL_SAN"
|
||||||
|
"DOVEADM_PORT"
|
||||||
|
"IPV4_NETWORK"
|
||||||
|
"IPV6_NETWORK"
|
||||||
|
"LOG_LINES"
|
||||||
|
"SNAT_TO_SOURCE"
|
||||||
|
"SNAT6_TO_SOURCE"
|
||||||
|
"COMPOSE_PROJECT_NAME"
|
||||||
|
"DOCKER_COMPOSE_VERSION"
|
||||||
|
"SQL_PORT"
|
||||||
|
"API_KEY"
|
||||||
|
"API_KEY_READ_ONLY"
|
||||||
|
"API_ALLOW_FROM"
|
||||||
|
"MAILDIR_GC_TIME"
|
||||||
|
"MAILDIR_SUB"
|
||||||
|
"ACL_ANYONE"
|
||||||
|
"FTS_HEAP"
|
||||||
|
"FTS_PROCS"
|
||||||
|
"SKIP_FTS"
|
||||||
|
"ENABLE_SSL_SNI"
|
||||||
|
"ALLOW_ADMIN_EMAIL_LOGIN"
|
||||||
|
"SKIP_HTTP_VERIFICATION"
|
||||||
|
"SOGO_EXPIRE_SESSION"
|
||||||
|
"SOGO_URL_ENCRYPTION_KEY"
|
||||||
|
"REDIS_PORT"
|
||||||
|
"REDISPASS"
|
||||||
|
"DOVECOT_MASTER_USER"
|
||||||
|
"DOVECOT_MASTER_PASS"
|
||||||
|
"MAILCOW_PASS_SCHEME"
|
||||||
|
"ADDITIONAL_SERVER_NAMES"
|
||||||
|
"WATCHDOG_VERBOSE"
|
||||||
|
"WEBAUTHN_ONLY_TRUSTED_VENDORS"
|
||||||
|
"SPAMHAUS_DQS_KEY"
|
||||||
|
"SKIP_UNBOUND_HEALTHCHECK"
|
||||||
|
"DISABLE_NETFILTER_ISOLATION_RULE"
|
||||||
|
"HTTP_REDIRECT"
|
||||||
|
"ENABLE_IPV6"
|
||||||
|
"ACME_DNS_CHALLENGE"
|
||||||
|
"ACME_DNS_PROVIDER"
|
||||||
|
"ACME_ACCOUNT_EMAIL"
|
||||||
|
)
|
||||||
|
|
||||||
|
sed -i --follow-symlinks '$a\' mailcow.conf
|
||||||
|
for option in ${CONFIG_ARRAY[@]}; do
|
||||||
|
if grep -q "^#\?${option}=" mailcow.conf; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Adding new option \"${option}\" to mailcow.conf"
|
||||||
|
|
||||||
|
case "${option}" in
|
||||||
|
AUTODISCOVER_SAN)
|
||||||
|
echo '# Obtain certificates for autodiscover.* and autoconfig.* domains.' >> mailcow.conf
|
||||||
|
echo '# This can be useful to switch off in case you are in a scenario where a reverse proxy already handles those.' >> mailcow.conf
|
||||||
|
echo '# There are mixed scenarios where ports 80,443 are occupied and you do not want to share certs' >> mailcow.conf
|
||||||
|
echo '# between services. So acme-mailcow obtains for maildomains and all web-things get handled' >> mailcow.conf
|
||||||
|
echo '# in the reverse proxy.' >> mailcow.conf
|
||||||
|
echo 'AUTODISCOVER_SAN=y' >> mailcow.conf
|
||||||
|
;;
|
||||||
|
|
||||||
|
DOCKER_COMPOSE_VERSION)
|
||||||
|
echo "# Used Docker Compose version" >> mailcow.conf
|
||||||
|
echo "# Switch here between native (compose plugin) and standalone" >> mailcow.conf
|
||||||
|
echo "# For more informations take a look at the mailcow docs regarding the configuration options." >> mailcow.conf
|
||||||
|
echo "# Normally this should be untouched but if you decided to use either of those you can switch it manually here." >> mailcow.conf
|
||||||
|
echo "# Please be aware that at least one of those variants should be installed on your machine or mailcow will fail." >> mailcow.conf
|
||||||
|
echo "" >> mailcow.conf
|
||||||
|
echo "DOCKER_COMPOSE_VERSION=${DOCKER_COMPOSE_VERSION}" >> mailcow.conf
|
||||||
|
;;
|
||||||
|
|
||||||
|
DOVEADM_PORT)
|
||||||
|
echo "DOVEADM_PORT=127.0.0.1:19991" >> mailcow.conf
|
||||||
|
;;
|
||||||
|
|
||||||
|
LOG_LINES)
|
||||||
|
echo '# Max log lines per service to keep in Redis logs' >> mailcow.conf
|
||||||
|
echo "LOG_LINES=9999" >> mailcow.conf
|
||||||
|
;;
|
||||||
|
IPV4_NETWORK)
|
||||||
|
echo '# Internal IPv4 /24 subnet, format n.n.n. (expands to n.n.n.0/24)' >> mailcow.conf
|
||||||
|
echo "IPV4_NETWORK=172.22.1" >> mailcow.conf
|
||||||
|
;;
|
||||||
|
IPV6_NETWORK)
|
||||||
|
echo '# Internal IPv6 subnet in fc00::/7' >> mailcow.conf
|
||||||
|
echo "IPV6_NETWORK=fd4d:6169:6c63:6f77::/64" >> mailcow.conf
|
||||||
|
;;
|
||||||
|
SQL_PORT)
|
||||||
|
echo '# Bind SQL to 127.0.0.1 on port 13306' >> mailcow.conf
|
||||||
|
echo "SQL_PORT=127.0.0.1:13306" >> mailcow.conf
|
||||||
|
;;
|
||||||
|
API_KEY)
|
||||||
|
echo '# Create or override API key for web UI' >> mailcow.conf
|
||||||
|
echo "#API_KEY=" >> mailcow.conf
|
||||||
|
;;
|
||||||
|
API_KEY_READ_ONLY)
|
||||||
|
echo '# Create or override read-only API key for web UI' >> mailcow.conf
|
||||||
|
echo "#API_KEY_READ_ONLY=" >> mailcow.conf
|
||||||
|
;;
|
||||||
|
API_ALLOW_FROM)
|
||||||
|
echo '# Must be set for API_KEY to be active' >> mailcow.conf
|
||||||
|
echo '# IPs only, no networks (networks can be set via UI)' >> mailcow.conf
|
||||||
|
echo "#API_ALLOW_FROM=" >> mailcow.conf
|
||||||
|
;;
|
||||||
|
SNAT_TO_SOURCE)
|
||||||
|
echo '# Use this IPv4 for outgoing connections (SNAT)' >> mailcow.conf
|
||||||
|
echo "#SNAT_TO_SOURCE=" >> mailcow.conf
|
||||||
|
;;
|
||||||
|
SNAT6_TO_SOURCE)
|
||||||
|
echo '# Use this IPv6 for outgoing connections (SNAT)' >> mailcow.conf
|
||||||
|
echo "#SNAT6_TO_SOURCE=" >> mailcow.conf
|
||||||
|
;;
|
||||||
|
MAILDIR_GC_TIME)
|
||||||
|
echo '# Garbage collector cleanup' >> mailcow.conf
|
||||||
|
echo '# Deleted domains and mailboxes are moved to /var/vmail/_garbage/timestamp_sanitizedstring' >> mailcow.conf
|
||||||
|
echo '# How long should objects remain in the garbage until they are being deleted? (value in minutes)' >> mailcow.conf
|
||||||
|
echo '# Check interval is hourly' >> mailcow.conf
|
||||||
|
echo 'MAILDIR_GC_TIME=1440' >> mailcow.conf
|
||||||
|
;;
|
||||||
|
ACL_ANYONE)
|
||||||
|
echo '# Set this to "allow" to enable the anyone pseudo user. Disabled by default.' >> mailcow.conf
|
||||||
|
echo '# When enabled, ACL can be created, that apply to "All authenticated users"' >> mailcow.conf
|
||||||
|
echo '# This should probably only be activated on mail hosts, that are used exclusively by one organisation.' >> mailcow.conf
|
||||||
|
echo '# Otherwise a user might share data with too many other users.' >> mailcow.conf
|
||||||
|
echo 'ACL_ANYONE=disallow' >> mailcow.conf
|
||||||
|
;;
|
||||||
|
FTS_HEAP)
|
||||||
|
echo '# Dovecot Indexing (FTS) Process maximum heap size in MB, there is no recommendation, please see Dovecot docs.' >> mailcow.conf
|
||||||
|
echo '# Flatcurve is used as FTS Engine. It is supposed to be pretty efficient in CPU and RAM consumption.' >> mailcow.conf
|
||||||
|
echo '# Please always monitor your Resource consumption!' >> mailcow.conf
|
||||||
|
echo "FTS_HEAP=128" >> mailcow.conf
|
||||||
|
;;
|
||||||
|
SKIP_FTS)
|
||||||
|
echo '# Skip FTS (Fulltext Search) for Dovecot on low-memory, low-threaded systems or if you simply want to disable it.' >> mailcow.conf
|
||||||
|
echo "# Dovecot inside mailcow use Flatcurve as FTS Backend." >> mailcow.conf
|
||||||
|
echo "SKIP_FTS=y" >> mailcow.conf
|
||||||
|
;;
|
||||||
|
FTS_PROCS)
|
||||||
|
echo '# Controls how many processes the Dovecot indexing process can spawn at max.' >> mailcow.conf
|
||||||
|
echo '# Too many indexing processes can use a lot of CPU and Disk I/O' >> mailcow.conf
|
||||||
|
echo '# Please visit: https://doc.dovecot.org/configuration_manual/service_configuration/#indexer-worker for more informations' >> mailcow.conf
|
||||||
|
echo "FTS_PROCS=1" >> mailcow.conf
|
||||||
|
;;
|
||||||
|
ENABLE_SSL_SNI)
|
||||||
|
echo '# Create seperate certificates for all domains - y/n' >> mailcow.conf
|
||||||
|
echo '# this will allow adding more than 100 domains, but some email clients will not be able to connect with alternative hostnames' >> mailcow.conf
|
||||||
|
echo '# see https://wiki.dovecot.org/SSL/SNIClientSupport' >> mailcow.conf
|
||||||
|
echo "ENABLE_SSL_SNI=n" >> mailcow.conf
|
||||||
|
;;
|
||||||
|
SKIP_SOGO)
|
||||||
|
echo '# Skip SOGo: Will disable SOGo integration and therefore webmail, DAV protocols and ActiveSync support (experimental, unsupported, not fully implemented) - y/n' >> mailcow.conf
|
||||||
|
echo "SKIP_SOGO=n" >> mailcow.conf
|
||||||
|
;;
|
||||||
|
MAILDIR_SUB)
|
||||||
|
echo '# MAILDIR_SUB defines a path in a users virtual home to keep the maildir in. Leave empty for updated setups.' >> mailcow.conf
|
||||||
|
echo "#MAILDIR_SUB=Maildir" >> mailcow.conf
|
||||||
|
echo "MAILDIR_SUB=" >> mailcow.conf
|
||||||
|
;;
|
||||||
|
WATCHDOG_NOTIFY_WEBHOOK)
|
||||||
|
echo '# Send notifications to a webhook URL that receives a POST request with the content type "application/json".' >> mailcow.conf
|
||||||
|
echo '# You can use this to send notifications to services like Discord, Slack and others.' >> mailcow.conf
|
||||||
|
echo '#WATCHDOG_NOTIFY_WEBHOOK=https://discord.com/api/webhooks/XXXXXXXXXXXXXXXXXXX/XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX' >> mailcow.conf
|
||||||
|
;;
|
||||||
|
WATCHDOG_NOTIFY_WEBHOOK_BODY)
|
||||||
|
echo '# JSON body included in the webhook POST request. Needs to be in single quotes.' >> mailcow.conf
|
||||||
|
echo '# Following variables are available: SUBJECT, BODY' >> mailcow.conf
|
||||||
|
WEBHOOK_BODY='{"username": "mailcow Watchdog", "content": "**${SUBJECT}**\n${BODY}"}'
|
||||||
|
echo "#WATCHDOG_NOTIFY_WEBHOOK_BODY='${WEBHOOK_BODY}'" >> mailcow.conf
|
||||||
|
;;
|
||||||
|
WATCHDOG_NOTIFY_BAN)
|
||||||
|
echo '# Notify about banned IP. Includes whois lookup.' >> mailcow.conf
|
||||||
|
echo "WATCHDOG_NOTIFY_BAN=y" >> mailcow.conf
|
||||||
|
;;
|
||||||
|
WATCHDOG_NOTIFY_START)
|
||||||
|
echo '# Send a notification when the watchdog is started.' >> mailcow.conf
|
||||||
|
echo "WATCHDOG_NOTIFY_START=y" >> mailcow.conf
|
||||||
|
;;
|
||||||
|
WATCHDOG_SUBJECT)
|
||||||
|
echo '# Subject for watchdog mails. Defaults to "Watchdog ALERT" followed by the error message.' >> mailcow.conf
|
||||||
|
echo "#WATCHDOG_SUBJECT=" >> mailcow.conf
|
||||||
|
;;
|
||||||
|
WATCHDOG_EXTERNAL_CHECKS)
|
||||||
|
echo '# Checks if mailcow is an open relay. Requires a SAL. More checks will follow.' >> mailcow.conf
|
||||||
|
echo '# No data is collected. Opt-in and anonymous.' >> mailcow.conf
|
||||||
|
echo '# Will only work with unmodified mailcow setups.' >> mailcow.conf
|
||||||
|
echo "WATCHDOG_EXTERNAL_CHECKS=n" >> mailcow.conf
|
||||||
|
;;
|
||||||
|
SOGO_EXPIRE_SESSION)
|
||||||
|
echo '# SOGo session timeout in minutes' >> mailcow.conf
|
||||||
|
echo "SOGO_EXPIRE_SESSION=480" >> mailcow.conf
|
||||||
|
;;
|
||||||
|
REDIS_PORT)
|
||||||
|
echo "REDIS_PORT=127.0.0.1:7654" >> mailcow.conf
|
||||||
|
;;
|
||||||
|
DOVECOT_MASTER_USER)
|
||||||
|
echo '# DOVECOT_MASTER_USER and _PASS must _both_ be provided. No special chars.' >> mailcow.conf
|
||||||
|
echo '# Empty by default to auto-generate master user and password on start.' >> mailcow.conf
|
||||||
|
echo '# User expands to DOVECOT_MASTER_USER@mailcow.local' >> mailcow.conf
|
||||||
|
echo '# LEAVE EMPTY IF UNSURE' >> mailcow.conf
|
||||||
|
echo "DOVECOT_MASTER_USER=" >> mailcow.conf
|
||||||
|
;;
|
||||||
|
DOVECOT_MASTER_PASS)
|
||||||
|
echo '# LEAVE EMPTY IF UNSURE' >> mailcow.conf
|
||||||
|
echo "DOVECOT_MASTER_PASS=" >> mailcow.conf
|
||||||
|
;;
|
||||||
|
MAILCOW_PASS_SCHEME)
|
||||||
|
echo '# Password hash algorithm' >> mailcow.conf
|
||||||
|
echo '# Only certain password hash algorithm are supported. For a fully list of supported schemes,' >> mailcow.conf
|
||||||
|
echo '# see https://docs.mailcow.email/models/model-passwd/' >> mailcow.conf
|
||||||
|
echo "MAILCOW_PASS_SCHEME=BLF-CRYPT" >> mailcow.conf
|
||||||
|
;;
|
||||||
|
ADDITIONAL_SERVER_NAMES)
|
||||||
|
echo '# Additional server names for mailcow UI' >> mailcow.conf
|
||||||
|
echo '#' >> mailcow.conf
|
||||||
|
echo '# Specify alternative addresses for the mailcow UI to respond to' >> mailcow.conf
|
||||||
|
echo '# This is useful when you set mail.* as ADDITIONAL_SAN and want to make sure mail.maildomain.com will always point to the mailcow UI.' >> mailcow.conf
|
||||||
|
echo '# If the server name does not match a known site, Nginx decides by best-guess and may redirect users to the wrong web root.' >> mailcow.conf
|
||||||
|
echo '# You can understand this as server_name directive in Nginx.' >> mailcow.conf
|
||||||
|
echo '# Comma separated list without spaces! Example: ADDITIONAL_SERVER_NAMES=a.b.c,d.e.f' >> mailcow.conf
|
||||||
|
echo 'ADDITIONAL_SERVER_NAMES=' >> mailcow.conf
|
||||||
|
;;
|
||||||
|
WEBAUTHN_ONLY_TRUSTED_VENDORS)
|
||||||
|
echo "# WebAuthn device manufacturer verification" >> mailcow.conf
|
||||||
|
echo '# After setting WEBAUTHN_ONLY_TRUSTED_VENDORS=y only devices from trusted manufacturers are allowed' >> mailcow.conf
|
||||||
|
echo '# root certificates can be placed for validation under mailcow-dockerized/data/web/inc/lib/WebAuthn/rootCertificates' >> mailcow.conf
|
||||||
|
echo 'WEBAUTHN_ONLY_TRUSTED_VENDORS=n' >> mailcow.conf
|
||||||
|
;;
|
||||||
|
SPAMHAUS_DQS_KEY)
|
||||||
|
echo "# Spamhaus Data Query Service Key" >> mailcow.conf
|
||||||
|
echo '# Optional: Leave empty for none' >> mailcow.conf
|
||||||
|
echo '# Enter your key here if you are using a blocked ASN (OVH, AWS, Cloudflare e.g) for the unregistered Spamhaus Blocklist.' >> mailcow.conf
|
||||||
|
echo '# If empty, it will completely disable Spamhaus blocklists if it detects that you are running on a server using a blocked AS.' >> mailcow.conf
|
||||||
|
echo '# Otherwise it will work as usual.' >> mailcow.conf
|
||||||
|
echo 'SPAMHAUS_DQS_KEY=' >> mailcow.conf
|
||||||
|
;;
|
||||||
|
WATCHDOG_VERBOSE)
|
||||||
|
echo '# Enable watchdog verbose logging' >> mailcow.conf
|
||||||
|
echo 'WATCHDOG_VERBOSE=n' >> mailcow.conf
|
||||||
|
;;
|
||||||
|
SKIP_UNBOUND_HEALTHCHECK)
|
||||||
|
echo '# Skip Unbound (DNS Resolver) Healthchecks (NOT Recommended!) - y/n' >> mailcow.conf
|
||||||
|
echo 'SKIP_UNBOUND_HEALTHCHECK=n' >> mailcow.conf
|
||||||
|
;;
|
||||||
|
DISABLE_NETFILTER_ISOLATION_RULE)
|
||||||
|
echo '# Prevent netfilter from setting an iptables/nftables rule to isolate the mailcow docker network - y/n' >> mailcow.conf
|
||||||
|
echo '# CAUTION: Disabling this may expose container ports to other neighbors on the same subnet, even if the ports are bound to localhost' >> mailcow.conf
|
||||||
|
echo 'DISABLE_NETFILTER_ISOLATION_RULE=n' >> mailcow.conf
|
||||||
|
;;
|
||||||
|
HTTP_REDIRECT)
|
||||||
|
echo '# Redirect HTTP connections to HTTPS - y/n' >> mailcow.conf
|
||||||
|
echo 'HTTP_REDIRECT=n' >> mailcow.conf
|
||||||
|
;;
|
||||||
|
ENABLE_IPV6)
|
||||||
|
echo '# IPv6 Controller Section' >> mailcow.conf
|
||||||
|
echo '# This variable controls the usage of IPv6 within mailcow.' >> mailcow.conf
|
||||||
|
echo '# Can either be true or false | Defaults to true' >> mailcow.conf
|
||||||
|
echo '# WARNING: MAKE SURE TO PROPERLY CONFIGURE IPv6 ON YOUR HOST FIRST BEFORE ENABLING THIS AS FAULTY CONFIGURATIONS CAN LEAD TO OPEN RELAYS!' >> mailcow.conf
|
||||||
|
echo '# A COMPLETE DOCKER STACK REBUILD (compose down && compose up -d) IS NEEDED TO APPLY THIS.' >> mailcow.conf
|
||||||
|
echo ENABLE_IPV6=${IPV6_BOOL} >> mailcow.conf
|
||||||
|
;;
|
||||||
|
SKIP_CLAMD)
|
||||||
|
echo '# Skip ClamAV (clamd-mailcow) anti-virus (Rspamd will auto-detect a missing ClamAV container) - y/n' >> mailcow.conf
|
||||||
|
echo 'SKIP_CLAMD=n' >> mailcow.conf
|
||||||
|
;;
|
||||||
|
SKIP_OLEFY)
|
||||||
|
echo '# Skip Olefy (olefy-mailcow) anti-virus for Office documents (Rspamd will auto-detect a missing Olefy container) - y/n' >> mailcow.conf
|
||||||
|
echo 'SKIP_OLEFY=n' >> mailcow.conf
|
||||||
|
;;
|
||||||
|
REDISPASS)
|
||||||
|
echo "REDISPASS=$(LC_ALL=C </dev/urandom tr -dc A-Za-z0-9 2>/dev/null | head -c 28)" >> mailcow.conf
|
||||||
|
;;
|
||||||
|
SOGO_URL_ENCRYPTION_KEY)
|
||||||
|
echo '# SOGo URL encryption key (exactly 16 characters, limited to A–Z, a–z, 0–9)' >> mailcow.conf
|
||||||
|
echo '# This key is used to encrypt email addresses within SOGo URLs' >> mailcow.conf
|
||||||
|
echo "SOGO_URL_ENCRYPTION_KEY=$(LC_ALL=C </dev/urandom tr -dc A-Za-z0-9 2>/dev/null | head -c 16)" >> mailcow.conf
|
||||||
|
;;
|
||||||
|
ACME_DNS_CHALLENGE)
|
||||||
|
echo '# Enable DNS-01 challenge for ACME (acme-mailcow) - y/n' >> mailcow.conf
|
||||||
|
echo '# This requires you to set ACME_DNS_PROVIDER and ACME_ACCOUNT_EMAIL below' >> mailcow.conf
|
||||||
|
echo 'ACME_DNS_CHALLENGE=n' >> mailcow.conf
|
||||||
|
;;
|
||||||
|
ACME_DNS_PROVIDER)
|
||||||
|
echo '# DNS provider for DNS-01 challenge (e.g. dns_cf, dns_azure, dns_gd, etc.)' >> mailcow.conf
|
||||||
|
echo '# See the dns-01 provider documentation for more information.' >> mailcow.conf
|
||||||
|
echo 'ACME_DNS_PROVIDER=dns_xxx' >> mailcow.conf
|
||||||
|
;;
|
||||||
|
ACME_ACCOUNT_EMAIL)
|
||||||
|
echo '# Account email for ACME DNS-01 challenge registration' >> mailcow.conf
|
||||||
|
echo 'ACME_ACCOUNT_EMAIL=me@example.com' >> mailcow.conf
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "${option}=" >> mailcow.conf
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM alpine:3.21
|
FROM alpine:3.23
|
||||||
|
|
||||||
LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
|
LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
|
||||||
|
|
||||||
@@ -14,11 +14,22 @@ RUN apk upgrade --no-cache \
|
|||||||
tini \
|
tini \
|
||||||
tzdata \
|
tzdata \
|
||||||
python3 \
|
python3 \
|
||||||
acme-tiny
|
acme-tiny \
|
||||||
|
git \
|
||||||
|
socat \
|
||||||
|
&& git clone --depth 1 https://github.com/acmesh-official/acme.sh.git /opt/acme.sh \
|
||||||
|
&& chmod +x /opt/acme.sh/acme.sh \
|
||||||
|
&& mkdir -p /var/lib/acme/acme-sh
|
||||||
|
|
||||||
|
ENV ACME_SH_BIN=/opt/acme.sh/acme.sh \
|
||||||
|
ACME_SH_HOME=/opt/acme.sh \
|
||||||
|
ACME_SH_CONFIG_HOME=/var/lib/acme/acme-sh
|
||||||
|
|
||||||
COPY acme.sh /srv/acme.sh
|
COPY acme.sh /srv/acme.sh
|
||||||
COPY functions.sh /srv/functions.sh
|
COPY functions.sh /srv/functions.sh
|
||||||
COPY obtain-certificate.sh /srv/obtain-certificate.sh
|
COPY obtain-certificate.sh /srv/obtain-certificate.sh
|
||||||
|
COPY obtain-certificate-dns.sh /srv/obtain-certificate-dns.sh
|
||||||
|
COPY load-dns-config.sh /srv/load-dns-config.sh
|
||||||
COPY reload-configurations.sh /srv/reload-configurations.sh
|
COPY reload-configurations.sh /srv/reload-configurations.sh
|
||||||
COPY expand6.sh /srv/expand6.sh
|
COPY expand6.sh /srv/expand6.sh
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,17 @@ until [[ $(${REDIS_CMDLINE} PING) == "PONG" ]]; do
|
|||||||
sleep 2
|
sleep 2
|
||||||
done
|
done
|
||||||
|
|
||||||
|
# Create DNS-01 configuration template if it doesn't exist
|
||||||
|
if [[ ! -f /etc/acme/dns-01.conf ]]; then
|
||||||
|
mkdir -p /etc/acme
|
||||||
|
cat > /etc/acme/dns-01.conf <<'EOF'
|
||||||
|
# Add here your DNS-01 challenge configuration
|
||||||
|
# For more information, visit the acme.sh documentation:
|
||||||
|
# https://github.com/acmesh-official/acme.sh/wiki/dnsapi
|
||||||
|
EOF
|
||||||
|
echo "Created DNS-01 configuration template at /etc/acme/dns-01.conf"
|
||||||
|
fi
|
||||||
|
|
||||||
source /srv/functions.sh
|
source /srv/functions.sh
|
||||||
# Thanks to https://github.com/cvmiller -> https://github.com/cvmiller/expand6
|
# Thanks to https://github.com/cvmiller -> https://github.com/cvmiller/expand6
|
||||||
source /srv/expand6.sh
|
source /srv/expand6.sh
|
||||||
@@ -42,6 +53,10 @@ if [[ "${ENABLE_SSL_SNI}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
|
|||||||
ENABLE_SSL_SNI=y
|
ENABLE_SSL_SNI=y
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [[ "${ACME_DNS_CHALLENGE}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
|
||||||
|
ACME_DNS_CHALLENGE=y
|
||||||
|
fi
|
||||||
|
|
||||||
if [[ "${SKIP_LETS_ENCRYPT}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
|
if [[ "${SKIP_LETS_ENCRYPT}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
|
||||||
log_f "SKIP_LETS_ENCRYPT=y, skipping Let's Encrypt..."
|
log_f "SKIP_LETS_ENCRYPT=y, skipping Let's Encrypt..."
|
||||||
sleep 365d
|
sleep 365d
|
||||||
@@ -159,18 +174,6 @@ while true; do
|
|||||||
fi
|
fi
|
||||||
if [[ ! -f ${ACME_BASE}/acme/account.pem ]]; then
|
if [[ ! -f ${ACME_BASE}/acme/account.pem ]]; then
|
||||||
log_f "Generating missing Lets Encrypt account key..."
|
log_f "Generating missing Lets Encrypt account key..."
|
||||||
if [[ ! -z ${ACME_CONTACT} ]]; then
|
|
||||||
if ! verify_email "${ACME_CONTACT}"; then
|
|
||||||
log_f "Invalid email address, will not start registration!"
|
|
||||||
sleep 365d
|
|
||||||
exec $(readlink -f "$0")
|
|
||||||
else
|
|
||||||
ACME_CONTACT_PARAMETER="--contact mailto:${ACME_CONTACT}"
|
|
||||||
log_f "Valid email address, using ${ACME_CONTACT} for registration"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
ACME_CONTACT_PARAMETER=""
|
|
||||||
fi
|
|
||||||
openssl genrsa 4096 > ${ACME_BASE}/acme/account.pem
|
openssl genrsa 4096 > ${ACME_BASE}/acme/account.pem
|
||||||
else
|
else
|
||||||
log_f "Using existing Lets Encrypt account key ${ACME_BASE}/acme/account.pem"
|
log_f "Using existing Lets Encrypt account key ${ACME_BASE}/acme/account.pem"
|
||||||
@@ -218,7 +221,7 @@ while true; do
|
|||||||
|
|
||||||
if [[ ${AUTODISCOVER_SAN} == "y" ]]; then
|
if [[ ${AUTODISCOVER_SAN} == "y" ]]; then
|
||||||
# Fetch certs for autoconfig and autodiscover subdomains
|
# Fetch certs for autoconfig and autodiscover subdomains
|
||||||
ADDITIONAL_WC_ARR+=('autodiscover' 'autoconfig')
|
ADDITIONAL_WC_ARR+=('autodiscover' 'autoconfig' 'mta-sts')
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ ${SKIP_IP_CHECK} != "y" ]]; then
|
if [[ ${SKIP_IP_CHECK} != "y" ]]; then
|
||||||
@@ -246,18 +249,71 @@ while true; do
|
|||||||
done <<< "${SQL_DOMAINS}"
|
done <<< "${SQL_DOMAINS}"
|
||||||
|
|
||||||
if [[ ${ONLY_MAILCOW_HOSTNAME} != "y" ]]; then
|
if [[ ${ONLY_MAILCOW_HOSTNAME} != "y" ]]; then
|
||||||
|
# Fetch all domains with an active MTA-STS policy once.
|
||||||
|
unset MTA_STS_ACTIVE_DOMAINS
|
||||||
|
declare -A MTA_STS_ACTIVE_DOMAINS
|
||||||
|
if [[ ${AUTODISCOVER_SAN} == "y" ]]; then
|
||||||
|
SQL_MTA_STS_DOMAINS=$(mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT domain FROM mta_sts WHERE active = 1" -Bs)
|
||||||
|
if [[ $? -eq 0 ]]; then
|
||||||
|
while read mta_sts_domain; do
|
||||||
|
if [[ -z "${mta_sts_domain}" ]]; then
|
||||||
|
# ignore empty lines
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
MTA_STS_ACTIVE_DOMAINS["${mta_sts_domain}"]=1
|
||||||
|
done <<< "${SQL_MTA_STS_DOMAINS}"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
for SQL_DOMAIN in "${SQL_DOMAIN_ARR[@]}"; do
|
for SQL_DOMAIN in "${SQL_DOMAIN_ARR[@]}"; do
|
||||||
unset VALIDATED_CONFIG_DOMAINS_SUBDOMAINS
|
unset VALIDATED_CONFIG_DOMAINS_SUBDOMAINS
|
||||||
declare -a VALIDATED_CONFIG_DOMAINS_SUBDOMAINS
|
declare -a VALIDATED_CONFIG_DOMAINS_SUBDOMAINS
|
||||||
for SUBDOMAIN in "${ADDITIONAL_WC_ARR[@]}"; do
|
for SUBDOMAIN in "${ADDITIONAL_WC_ARR[@]}"; do
|
||||||
if [[ "${SUBDOMAIN}.${SQL_DOMAIN}" != "${MAILCOW_HOSTNAME}" ]]; then
|
FULL_SUBDOMAIN="${SUBDOMAIN}.${SQL_DOMAIN}"
|
||||||
if check_domain "${SUBDOMAIN}.${SQL_DOMAIN}"; then
|
|
||||||
VALIDATED_CONFIG_DOMAINS_SUBDOMAINS+=("${SUBDOMAIN}.${SQL_DOMAIN}")
|
# Skip mta-sts subdomain unless MTA-STS is enabled (active) for this domain
|
||||||
fi
|
if [[ "${SUBDOMAIN}" == "mta-sts" && -z "${MTA_STS_ACTIVE_DOMAINS[${SQL_DOMAIN}]}" ]]; then
|
||||||
|
log_f "MTA-STS is not enabled for ${SQL_DOMAIN} - skipping mta-sts subdomain certificate"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Skip if subdomain matches MAILCOW_HOSTNAME
|
||||||
|
if [[ "${FULL_SUBDOMAIN}" == "${MAILCOW_HOSTNAME}" ]]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
# Skip if subdomain is covered by a wildcard in ADDITIONAL_SAN
|
||||||
|
if is_covered_by_wildcard "${FULL_SUBDOMAIN}"; then
|
||||||
|
log_f "Subdomain '${FULL_SUBDOMAIN}' is covered by wildcard - skipping explicit subdomain"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
# Validate and add subdomain
|
||||||
|
if check_domain "${FULL_SUBDOMAIN}"; then
|
||||||
|
VALIDATED_CONFIG_DOMAINS_SUBDOMAINS+=("${FULL_SUBDOMAIN}")
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
VALIDATED_CONFIG_DOMAINS+=("${VALIDATED_CONFIG_DOMAINS_SUBDOMAINS[*]}")
|
VALIDATED_CONFIG_DOMAINS+=("${VALIDATED_CONFIG_DOMAINS_SUBDOMAINS[*]}")
|
||||||
done
|
done
|
||||||
|
|
||||||
|
# Fetch alias domains where target domain has MTA-STS enabled
|
||||||
|
if [[ ${AUTODISCOVER_SAN} == "y" ]]; then
|
||||||
|
SQL_ALIAS_DOMAINS=$(mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT ad.alias_domain FROM alias_domain ad INNER JOIN mta_sts m ON ad.target_domain = m.domain WHERE ad.active = 1 AND m.active = 1" -Bs)
|
||||||
|
if [[ $? -eq 0 ]]; then
|
||||||
|
while read alias_domain; do
|
||||||
|
if [[ -z "${alias_domain}" ]]; then
|
||||||
|
# ignore empty lines
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
# Only add mta-sts subdomain for alias domains
|
||||||
|
if [[ "mta-sts.${alias_domain}" != "${MAILCOW_HOSTNAME}" ]]; then
|
||||||
|
# Skip if mta-sts subdomain is covered by a wildcard
|
||||||
|
if is_covered_by_wildcard "mta-sts.${alias_domain}"; then
|
||||||
|
log_f "Alias domain mta-sts subdomain 'mta-sts.${alias_domain}' is covered by wildcard - skipping"
|
||||||
|
elif check_domain "mta-sts.${alias_domain}"; then
|
||||||
|
VALIDATED_CONFIG_DOMAINS+=("mta-sts.${alias_domain}")
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done <<< "${SQL_ALIAS_DOMAINS}"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if check_domain ${MAILCOW_HOSTNAME}; then
|
if check_domain ${MAILCOW_HOSTNAME}; then
|
||||||
@@ -286,20 +342,38 @@ while true; do
|
|||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Check if MAILCOW_HOSTNAME is covered by a wildcard in ADDITIONAL_SAN
|
||||||
|
MAILCOW_HOSTNAME_COVERED=0
|
||||||
|
if [[ ! -z ${VALIDATED_MAILCOW_HOSTNAME} ]]; then
|
||||||
|
if is_covered_by_wildcard "${VALIDATED_MAILCOW_HOSTNAME}"; then
|
||||||
|
MAILCOW_PARENT_DOMAIN=$(echo ${VALIDATED_MAILCOW_HOSTNAME} | cut -d. -f2-)
|
||||||
|
log_f "MAILCOW_HOSTNAME '${VALIDATED_MAILCOW_HOSTNAME}' is covered by wildcard '*.${MAILCOW_PARENT_DOMAIN}' - skipping explicit hostname"
|
||||||
|
MAILCOW_HOSTNAME_COVERED=1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
# Unique domains for server certificate
|
# Unique domains for server certificate
|
||||||
if [[ ${ENABLE_SSL_SNI} == "y" ]]; then
|
if [[ ${ENABLE_SSL_SNI} == "y" ]]; then
|
||||||
# create certificate for server name and fqdn SANs only
|
# create certificate for server name and fqdn SANs only
|
||||||
SERVER_SAN_VALIDATED=(${VALIDATED_MAILCOW_HOSTNAME} $(echo ${ADDITIONAL_VALIDATED_SAN[*]} | xargs -n1 | sort -u | xargs))
|
if [[ ${MAILCOW_HOSTNAME_COVERED} == "1" ]]; then
|
||||||
|
SERVER_SAN_VALIDATED=($(echo ${ADDITIONAL_VALIDATED_SAN[*]} | xargs -n1 | sort -u | xargs))
|
||||||
|
else
|
||||||
|
SERVER_SAN_VALIDATED=(${VALIDATED_MAILCOW_HOSTNAME} $(echo ${ADDITIONAL_VALIDATED_SAN[*]} | xargs -n1 | sort -u | xargs))
|
||||||
|
fi
|
||||||
else
|
else
|
||||||
# create certificate for all domains, including all subdomains from other domains [*]
|
# create certificate for all domains, including all subdomains from other domains [*]
|
||||||
SERVER_SAN_VALIDATED=(${VALIDATED_MAILCOW_HOSTNAME} $(echo ${VALIDATED_CONFIG_DOMAINS[*]} ${ADDITIONAL_VALIDATED_SAN[*]} | xargs -n1 | sort -u | xargs))
|
if [[ ${MAILCOW_HOSTNAME_COVERED} == "1" ]]; then
|
||||||
|
SERVER_SAN_VALIDATED=($(echo ${VALIDATED_CONFIG_DOMAINS[*]} ${ADDITIONAL_VALIDATED_SAN[*]} | xargs -n1 | sort -u | xargs))
|
||||||
|
else
|
||||||
|
SERVER_SAN_VALIDATED=(${VALIDATED_MAILCOW_HOSTNAME} $(echo ${VALIDATED_CONFIG_DOMAINS[*]} ${ADDITIONAL_VALIDATED_SAN[*]} | xargs -n1 | sort -u | xargs))
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
if [[ ! -z ${SERVER_SAN_VALIDATED[*]} ]]; then
|
if [[ ! -z ${SERVER_SAN_VALIDATED[*]} ]]; then
|
||||||
CERT_NAME=${SERVER_SAN_VALIDATED[0]}
|
CERT_NAME=${SERVER_SAN_VALIDATED[0]}
|
||||||
VALIDATED_CERTIFICATES+=("${CERT_NAME}")
|
VALIDATED_CERTIFICATES+=("${CERT_NAME}")
|
||||||
|
|
||||||
# obtain server certificate if required
|
# obtain server certificate if required
|
||||||
ACME_CONTACT_PARAMETER=${ACME_CONTACT_PARAMETER} DOMAINS=${SERVER_SAN_VALIDATED[@]} /srv/obtain-certificate.sh rsa
|
DOMAINS=${SERVER_SAN_VALIDATED[@]} /srv/obtain-certificate.sh rsa
|
||||||
RETURN="$?"
|
RETURN="$?"
|
||||||
if [[ "$RETURN" == "0" ]]; then # 0 = cert created successfully
|
if [[ "$RETURN" == "0" ]]; then # 0 = cert created successfully
|
||||||
CERT_AMOUNT_CHANGED=1
|
CERT_AMOUNT_CHANGED=1
|
||||||
|
|||||||
@@ -80,6 +80,11 @@ check_domain(){
|
|||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [[ ${ACME_DNS_CHALLENGE} == "y" ]]; then
|
||||||
|
log_f "ACME_DNS_CHALLENGE=y - skipping IP and HTTP validation for ${DOMAIN}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
# Check if CNAME without v6 enabled target
|
# Check if CNAME without v6 enabled target
|
||||||
if [[ ! -z ${AAAA_DOMAIN} ]] && [[ -z $(echo ${AAAA_DOMAIN} | grep "^\([0-9a-fA-F]\{0,4\}:\)\{1,7\}[0-9a-fA-F]\{0,4\}$") ]]; then
|
if [[ ! -z ${AAAA_DOMAIN} ]] && [[ -z $(echo ${AAAA_DOMAIN} | grep "^\([0-9a-fA-F]\{0,4\}:\)\{1,7\}[0-9a-fA-F]\{0,4\}$") ]]; then
|
||||||
AAAA_DOMAIN=
|
AAAA_DOMAIN=
|
||||||
@@ -130,3 +135,32 @@ verify_challenge_path(){
|
|||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Check if a domain is covered by a wildcard (*.example.com) in ADDITIONAL_SAN
|
||||||
|
# Usage: is_covered_by_wildcard "subdomain.example.com"
|
||||||
|
# Returns: 0 if covered, 1 if not covered
|
||||||
|
# Note: Only returns 0 (covered) when DNS-01 challenge is enabled,
|
||||||
|
# as wildcards cannot be validated with HTTP-01 challenge
|
||||||
|
is_covered_by_wildcard() {
|
||||||
|
local DOMAIN=$1
|
||||||
|
|
||||||
|
# Only skip if DNS challenge is enabled (wildcards require DNS-01)
|
||||||
|
if [[ ${ACME_DNS_CHALLENGE} != "y" ]]; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Return early if no ADDITIONAL_SAN is set
|
||||||
|
if [[ -z ${ADDITIONAL_SAN} ]]; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Extract parent domain (e.g., mail.example.com -> example.com)
|
||||||
|
local PARENT_DOMAIN=$(echo ${DOMAIN} | cut -d. -f2-)
|
||||||
|
|
||||||
|
# Check if ADDITIONAL_SAN contains a wildcard for this parent domain
|
||||||
|
if [[ "${ADDITIONAL_SAN}" == *"*.${PARENT_DOMAIN}"* ]]; then
|
||||||
|
return 0 # Covered by wildcard
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 1 # Not covered
|
||||||
|
}
|
||||||
Executable
+57
@@ -0,0 +1,57 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
SCRIPT_SOURCE="${BASH_SOURCE[0]:-${0}}"
|
||||||
|
if [[ "${SCRIPT_SOURCE}" == "${0}" ]]; then
|
||||||
|
__dns_loader_standalone=1
|
||||||
|
else
|
||||||
|
__dns_loader_standalone=0
|
||||||
|
fi
|
||||||
|
|
||||||
|
CONFIG_PATH="${ACME_DNS_CONFIG_FILE:-/etc/acme/dns-01.conf}"
|
||||||
|
|
||||||
|
if [[ ! -f "${CONFIG_PATH}" ]]; then
|
||||||
|
if [[ $__dns_loader_standalone -eq 1 ]]; then
|
||||||
|
exit 0
|
||||||
|
else
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
source /srv/functions.sh
|
||||||
|
|
||||||
|
log_f "Loading DNS-01 configuration from ${CONFIG_PATH}"
|
||||||
|
|
||||||
|
LINE_NO=0
|
||||||
|
while IFS= read -r line || [[ -n "${line}" ]]; do
|
||||||
|
LINE_NO=$((LINE_NO+1))
|
||||||
|
line="${line%$'\r'}"
|
||||||
|
line_trimmed="$(printf '%s' "${line}" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')"
|
||||||
|
[[ -z "${line_trimmed}" ]] && continue
|
||||||
|
[[ "${line_trimmed:0:1}" == "#" ]] && continue
|
||||||
|
if [[ "${line_trimmed}" != *=* ]]; then
|
||||||
|
log_f "Skipping invalid DNS config line ${LINE_NO} (missing key=value)"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
KEY="${line_trimmed%%=*}"
|
||||||
|
VALUE="${line_trimmed#*=}"
|
||||||
|
KEY="$(printf '%s' "${KEY}" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')"
|
||||||
|
VALUE="$(printf '%s' "${VALUE}" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')"
|
||||||
|
if [[ -z "${KEY}" ]]; then
|
||||||
|
log_f "Skipping invalid DNS config line ${LINE_NO} (empty key)"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
if [[ "${VALUE}" =~ ^\".*\"$ ]]; then
|
||||||
|
VALUE="${VALUE:1:-1}"
|
||||||
|
elif [[ "${VALUE}" =~ ^\'.*\'$ ]]; then
|
||||||
|
VALUE="${VALUE:1:-1}"
|
||||||
|
fi
|
||||||
|
export "${KEY}"="${VALUE}"
|
||||||
|
log_f "Exported DNS config key ${KEY}"
|
||||||
|
|
||||||
|
done < "${CONFIG_PATH}"
|
||||||
|
|
||||||
|
if [[ $__dns_loader_standalone -eq 1 ]]; then
|
||||||
|
exit 0
|
||||||
|
else
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
@@ -0,0 +1,177 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Return values / exit codes
|
||||||
|
# 0 = cert created successfully
|
||||||
|
# 1 = cert renewed successfully
|
||||||
|
# 2 = cert not due for renewal
|
||||||
|
# * = errors
|
||||||
|
|
||||||
|
source /srv/functions.sh
|
||||||
|
|
||||||
|
CERT_DOMAINS=(${DOMAINS[@]})
|
||||||
|
CERT_DOMAIN=${CERT_DOMAINS[0]}
|
||||||
|
ACME_BASE=/var/lib/acme
|
||||||
|
|
||||||
|
# Load optional DNS provider secrets from /etc/acme/dns-01.conf
|
||||||
|
if [[ -f /srv/load-dns-config.sh ]]; then
|
||||||
|
source /srv/load-dns-config.sh
|
||||||
|
if declare -F log_f >/dev/null; then
|
||||||
|
log_f "ACME_DNS_CHALLENGE is enabled, DNS provider secrets loaded"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
TYPE=${1}
|
||||||
|
PREFIX=""
|
||||||
|
# only support rsa certificates for now
|
||||||
|
if [[ "${TYPE}" != "rsa" ]]; then
|
||||||
|
log_f "Unknown certificate type '${TYPE}' requested"
|
||||||
|
exit 5
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "${ACME_DNS_PROVIDER}" ]]; then
|
||||||
|
log_f "ACME_DNS_PROVIDER is required when ACME_DNS_CHALLENGE is enabled"
|
||||||
|
exit 6
|
||||||
|
fi
|
||||||
|
|
||||||
|
DOMAINS_FILE=${ACME_BASE}/${CERT_DOMAIN}/domains
|
||||||
|
CERT=${ACME_BASE}/${CERT_DOMAIN}/${PREFIX}cert.pem
|
||||||
|
SHARED_KEY=${ACME_BASE}/acme/${PREFIX}key.pem # must already exist
|
||||||
|
KEY=${ACME_BASE}/${CERT_DOMAIN}/${PREFIX}key.pem
|
||||||
|
CSR=${ACME_BASE}/${CERT_DOMAIN}/${PREFIX}acme.csr
|
||||||
|
|
||||||
|
if [[ -z ${CERT_DOMAINS[*]} ]]; then
|
||||||
|
log_f "Missing CERT_DOMAINS to obtain a certificate"
|
||||||
|
exit 3
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "${LE_STAGING}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
|
||||||
|
if [[ ! -z "${DIRECTORY_URL}" ]]; then
|
||||||
|
log_f "Cannot use DIRECTORY_URL with LE_STAGING=y - ignoring DIRECTORY_URL"
|
||||||
|
fi
|
||||||
|
log_f "Using Let's Encrypt staging servers"
|
||||||
|
ACME_SH_SERVER_ARGS=("--staging")
|
||||||
|
elif [[ ! -z "${DIRECTORY_URL}" ]]; then
|
||||||
|
log_f "Using custom directory URL ${DIRECTORY_URL}"
|
||||||
|
ACME_SH_SERVER_ARGS=("--server" "${DIRECTORY_URL}")
|
||||||
|
else
|
||||||
|
log_f "Using Let's Encrypt production servers"
|
||||||
|
ACME_SH_SERVER_ARGS=("--server" "letsencrypt")
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -f ${DOMAINS_FILE} && "$(cat ${DOMAINS_FILE})" == "${CERT_DOMAINS[*]}" ]]; then
|
||||||
|
if [[ ! -f ${CERT} || ! -f "${KEY}" || -f "${ACME_BASE}/force_renew" ]]; then
|
||||||
|
log_f "Certificate ${CERT} doesn't exist yet or forced renewal - start obtaining"
|
||||||
|
elif ! openssl x509 -checkend 2592000 -noout -in ${CERT} > /dev/null; then
|
||||||
|
log_f "Certificate ${CERT} is due for renewal (< 30 days) - start renewing"
|
||||||
|
else
|
||||||
|
log_f "Certificate ${CERT} validation done, neither changed nor due for renewal."
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
log_f "Certificate ${CERT} missing or changed domains '${CERT_DOMAINS[*]}' - start obtaining"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Make backup
|
||||||
|
if [[ -f ${CERT} ]]; then
|
||||||
|
DATE=$(date +%Y-%m-%d_%H_%M_%S)
|
||||||
|
BACKUP_DIR=${ACME_BASE}/backups/${CERT_DOMAIN}/${PREFIX}${DATE}
|
||||||
|
log_f "Creating backups in ${BACKUP_DIR} ..."
|
||||||
|
mkdir -p ${BACKUP_DIR}/
|
||||||
|
[[ -f ${DOMAINS_FILE} ]] && cp ${DOMAINS_FILE} ${BACKUP_DIR}/
|
||||||
|
[[ -f ${CERT} ]] && cp ${CERT} ${BACKUP_DIR}/
|
||||||
|
[[ -f ${KEY} ]] && cp ${KEY} ${BACKUP_DIR}/
|
||||||
|
[[ -f ${CSR} ]] && cp ${CSR} ${BACKUP_DIR}/
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p ${ACME_BASE}/${CERT_DOMAIN}
|
||||||
|
if [[ ! -f ${KEY} ]]; then
|
||||||
|
log_f "Copying shared private key for this certificate..."
|
||||||
|
cp ${SHARED_KEY} ${KEY}
|
||||||
|
chmod 600 ${KEY}
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Generating CSR to keep layout parity with HTTP challenge flow
|
||||||
|
printf "[SAN]\nsubjectAltName=" > /tmp/_SAN
|
||||||
|
printf "DNS:%s," "${CERT_DOMAINS[@]}" >> /tmp/_SAN
|
||||||
|
sed -i '$s/,$//' /tmp/_SAN
|
||||||
|
openssl req -new -sha256 -key ${KEY} -subj "/" -reqexts SAN -config <(cat "$(openssl version -d | sed 's/.*\"\(.*\)\"/\1/g')/openssl.cnf" /tmp/_SAN) > ${CSR}
|
||||||
|
|
||||||
|
log_f "Checking resolver..."
|
||||||
|
until dig letsencrypt.org +time=3 +tries=1 @unbound > /dev/null; do
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
log_f "Resolver OK"
|
||||||
|
|
||||||
|
ACME_SH_BIN_PATH=${ACME_SH_BIN:-/opt/acme.sh/acme.sh}
|
||||||
|
ACME_SH_WORK_HOME=${ACME_SH_CONFIG_HOME:-/var/lib/acme/acme-sh}
|
||||||
|
mkdir -p ${ACME_SH_WORK_HOME}
|
||||||
|
|
||||||
|
if [[ ! -x ${ACME_SH_BIN_PATH} ]]; then
|
||||||
|
log_f "acme.sh binary not found at ${ACME_SH_BIN_PATH}"
|
||||||
|
exit 7
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -f ${ACME_SH_WORK_HOME}/account.conf ]]; then
|
||||||
|
if [[ -z "${ACME_ACCOUNT_EMAIL}" ]]; then
|
||||||
|
log_f "ACME_ACCOUNT_EMAIL is required to register a new acme.sh account"
|
||||||
|
exit 8
|
||||||
|
fi
|
||||||
|
log_f "Registering acme.sh account for ${ACME_ACCOUNT_EMAIL}"
|
||||||
|
REGISTER_CMD=("${ACME_SH_BIN_PATH}" "--home" "${ACME_SH_WORK_HOME}" "--config-home" "${ACME_SH_WORK_HOME}" "--cert-home" "${ACME_SH_WORK_HOME}" "--register-account" "-m" "${ACME_ACCOUNT_EMAIL}")
|
||||||
|
REGISTER_CMD+=("${ACME_SH_SERVER_ARGS[@]}")
|
||||||
|
REGISTER_RESPONSE=$("${REGISTER_CMD[@]}" 2>&1)
|
||||||
|
if [[ $? -ne 0 ]]; then
|
||||||
|
log_f "Failed to register acme.sh account: ${REGISTER_RESPONSE}"
|
||||||
|
exit 9
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
TMP_CERT=$(mktemp /tmp/acme-cert.XXXXXX)
|
||||||
|
TMP_FULLCHAIN=$(mktemp /tmp/acme-fullchain.XXXXXX)
|
||||||
|
|
||||||
|
ACME_CMD=("${ACME_SH_BIN_PATH}" "--home" "${ACME_SH_WORK_HOME}" "--config-home" "${ACME_SH_WORK_HOME}" "--cert-home" "${ACME_SH_WORK_HOME}")
|
||||||
|
ACME_CMD+=("${ACME_SH_SERVER_ARGS[@]}")
|
||||||
|
ACME_CMD+=("--issue" "--dns" "${ACME_DNS_PROVIDER}" "--key-file" "${KEY}" "--cert-file" "${TMP_CERT}" "--fullchain-file" "${TMP_FULLCHAIN}" "--force")
|
||||||
|
for domain in "${CERT_DOMAINS[@]}"; do
|
||||||
|
ACME_CMD+=("-d" "${domain}")
|
||||||
|
done
|
||||||
|
|
||||||
|
log_f "Using command ${ACME_CMD[*]}"
|
||||||
|
if [[ -n "${ACME_DNS_PROVIDER}" ]]; then
|
||||||
|
log_f "DNS provider: ${ACME_DNS_PROVIDER}"
|
||||||
|
fi
|
||||||
|
if compgen -A variable | grep -Eq "^DNS_|^ACME_"; then
|
||||||
|
LOG_KEYS=$(env | grep -E "^(DNS_|ACME_)" | cut -d= -f1 | tr '\n' ' ')
|
||||||
|
log_f "Available DNS/ACME env keys: ${LOG_KEYS}" redis_only
|
||||||
|
fi
|
||||||
|
ACME_RESPONSE=$("${ACME_CMD[@]}" 2>&1 | tee /dev/fd/5; exit ${PIPESTATUS[0]})
|
||||||
|
SUCCESS="$?"
|
||||||
|
ACME_RESPONSE_B64=$(echo "${ACME_RESPONSE}" | openssl enc -e -A -base64)
|
||||||
|
log_f "${ACME_RESPONSE_B64}" redis_only b64
|
||||||
|
|
||||||
|
case "$SUCCESS" in
|
||||||
|
0)
|
||||||
|
log_f "Deploying certificate ${CERT}..."
|
||||||
|
if verify_hash_match ${TMP_FULLCHAIN} ${KEY}; then
|
||||||
|
RETURN=0
|
||||||
|
if [[ -f ${CERT} ]]; then
|
||||||
|
RETURN=1
|
||||||
|
fi
|
||||||
|
mv -f ${TMP_FULLCHAIN} ${CERT}
|
||||||
|
rm -f ${TMP_CERT}
|
||||||
|
echo -n ${CERT_DOMAINS[*]} > ${DOMAINS_FILE}
|
||||||
|
log_f "Certificate successfully obtained via DNS challenge"
|
||||||
|
exit ${RETURN}
|
||||||
|
else
|
||||||
|
log_f "Certificate was requested, but key and certificate hashes do not match"
|
||||||
|
rm -f ${TMP_CERT} ${TMP_FULLCHAIN}
|
||||||
|
exit 4
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
log_f "Failed to obtain certificate ${CERT} for domains '${CERT_DOMAINS[*]}' via DNS challenge"
|
||||||
|
redis-cli -h redis -a ${REDISPASS} --no-auth-warning SET ACME_FAIL_TIME "$(date +%s)"
|
||||||
|
rm -f ${TMP_CERT} ${TMP_FULLCHAIN}
|
||||||
|
exit 100${SUCCESS}
|
||||||
|
;;
|
||||||
|
esac
|
||||||
@@ -20,6 +20,10 @@ if [[ "${TYPE}" != "rsa" ]]; then
|
|||||||
log_f "Unknown certificate type '${TYPE}' requested"
|
log_f "Unknown certificate type '${TYPE}' requested"
|
||||||
exit 5
|
exit 5
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [[ "${ACME_DNS_CHALLENGE}" == "y" ]]; then
|
||||||
|
exec /srv/obtain-certificate-dns.sh "$@"
|
||||||
|
fi
|
||||||
DOMAINS_FILE=${ACME_BASE}/${CERT_DOMAIN}/domains
|
DOMAINS_FILE=${ACME_BASE}/${CERT_DOMAIN}/domains
|
||||||
CERT=${ACME_BASE}/${CERT_DOMAIN}/${PREFIX}cert.pem
|
CERT=${ACME_BASE}/${CERT_DOMAIN}/${PREFIX}cert.pem
|
||||||
SHARED_KEY=${ACME_BASE}/acme/${PREFIX}key.pem # must already exist
|
SHARED_KEY=${ACME_BASE}/acme/${PREFIX}key.pem # must already exist
|
||||||
@@ -93,8 +97,8 @@ until dig letsencrypt.org +time=3 +tries=1 @unbound > /dev/null; do
|
|||||||
sleep 2
|
sleep 2
|
||||||
done
|
done
|
||||||
log_f "Resolver OK"
|
log_f "Resolver OK"
|
||||||
log_f "Using command acme-tiny ${DIRECTORY_URL} ${ACME_CONTACT_PARAMETER} --account-key ${ACME_BASE}/acme/account.pem --disable-check --csr ${CSR} --acme-dir /var/www/acme/"
|
log_f "Using command acme-tiny ${DIRECTORY_URL} --account-key ${ACME_BASE}/acme/account.pem --disable-check --csr ${CSR} --acme-dir /var/www/acme/"
|
||||||
ACME_RESPONSE=$(acme-tiny ${DIRECTORY_URL} ${ACME_CONTACT_PARAMETER} \
|
ACME_RESPONSE=$(acme-tiny ${DIRECTORY_URL} \
|
||||||
--account-key ${ACME_BASE}/acme/account.pem \
|
--account-key ${ACME_BASE}/acme/account.pem \
|
||||||
--disable-check \
|
--disable-check \
|
||||||
--csr ${CSR} \
|
--csr ${CSR} \
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
FROM debian:bookworm-slim
|
FROM debian:trixie-slim
|
||||||
|
|
||||||
RUN apt update && apt install pigz -y --no-install-recommends
|
RUN apt update && apt install pigz zstd -y --no-install-recommends
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
FROM alpine:3.21 AS builder
|
FROM alpine:3.24 AS builder
|
||||||
|
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
ENV CLAMD_VERSION=1.4.2
|
ENV CLAMD_VERSION=1.4.6
|
||||||
|
|
||||||
RUN apk upgrade --no-cache \
|
RUN apk upgrade --no-cache \
|
||||||
&& apk add --update --no-cache \
|
&& apk add --update --no-cache \
|
||||||
@@ -68,7 +68,7 @@ RUN wget -P /src https://www.clamav.net/downloads/production/clamav-${CLAMD_VERS
|
|||||||
"/clamav/etc/clamav/clamav-milter.conf.sample" > "/clamav/etc/clamav/clamav-milter.conf" || exit 1
|
"/clamav/etc/clamav/clamav-milter.conf.sample" > "/clamav/etc/clamav/clamav-milter.conf" || exit 1
|
||||||
|
|
||||||
|
|
||||||
FROM alpine:3.21
|
FROM alpine:3.24
|
||||||
|
|
||||||
LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
|
LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ fi
|
|||||||
|
|
||||||
# Cleaning up garbage
|
# Cleaning up garbage
|
||||||
echo "Cleaning up tmp files..."
|
echo "Cleaning up tmp files..."
|
||||||
rm -rf /var/lib/clamav/clamav-*.tmp
|
rm -rf /var/lib/clamav/tmp.*
|
||||||
|
|
||||||
# Prepare whitelist
|
# Prepare whitelist
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM alpine:3.21
|
FROM alpine:3.23
|
||||||
|
|
||||||
LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
|
LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
|
||||||
|
|
||||||
|
|||||||
@@ -110,12 +110,12 @@ async def get_container(container_id : str):
|
|||||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||||
|
|
||||||
@app.get("/containers/json")
|
@app.get("/containers/json")
|
||||||
async def get_containers():
|
async def get_containers(all: bool = False):
|
||||||
global dockerapi
|
global dockerapi
|
||||||
|
|
||||||
containers = {}
|
containers = {}
|
||||||
try:
|
try:
|
||||||
for container in (await dockerapi.async_docker_client.containers.list()):
|
for container in (await dockerapi.async_docker_client.containers.list(all=all)):
|
||||||
container_info = await container.show()
|
container_info = await container.show()
|
||||||
containers.update({container_info['Id']: container_info})
|
containers.update({container_info['Id']: container_info})
|
||||||
return Response(content=json.dumps(containers, indent=4), media_type="application/json")
|
return Response(content=json.dumps(containers, indent=4), media_type="application/json")
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ FROM alpine:3.21
|
|||||||
LABEL maintainer="The Infrastructure Company GmbH <info@servercow.de>"
|
LABEL maintainer="The Infrastructure Company GmbH <info@servercow.de>"
|
||||||
|
|
||||||
# renovate: datasource=github-releases depName=tianon/gosu versioning=semver-coerced extractVersion=^(?<version>.*)$
|
# renovate: datasource=github-releases depName=tianon/gosu versioning=semver-coerced extractVersion=^(?<version>.*)$
|
||||||
ARG GOSU_VERSION=1.16
|
ARG GOSU_VERSION=1.19
|
||||||
|
|
||||||
ENV LANG=C.UTF-8
|
ENV LANG=C.UTF-8
|
||||||
ENV LC_ALL=C.UTF-8
|
ENV LC_ALL=C.UTF-8
|
||||||
|
|||||||
@@ -204,16 +204,17 @@ EOF
|
|||||||
# Create random master Password for SOGo SSO
|
# Create random master Password for SOGo SSO
|
||||||
RAND_PASS=$(cat /dev/urandom | tr -dc 'a-z0-9' | fold -w 32 | head -n 1)
|
RAND_PASS=$(cat /dev/urandom | tr -dc 'a-z0-9' | fold -w 32 | head -n 1)
|
||||||
echo -n ${RAND_PASS} > /etc/phpfpm/sogo-sso.pass
|
echo -n ${RAND_PASS} > /etc/phpfpm/sogo-sso.pass
|
||||||
# Creating additional creds file for SOGo notify crons (calendars, etc)
|
|
||||||
echo -n ${RAND_USER}@mailcow.local:${RAND_PASS} > /etc/sogo/cron.creds
|
|
||||||
cat <<EOF > /etc/dovecot/sogo-sso.conf
|
cat <<EOF > /etc/dovecot/sogo-sso.conf
|
||||||
# Autogenerated by mailcow
|
# Autogenerated by mailcow
|
||||||
passdb {
|
passdb {
|
||||||
driver = static
|
driver = static
|
||||||
args = allow_real_nets=${IPV4_NETWORK}.248/32 password={plain}${RAND_PASS}
|
args = allow_nets=${IPV4_NETWORK}.248/32 password={plain}${RAND_PASS}
|
||||||
}
|
}
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
|
# Creating additional creds file for SOGo notify crons (calendars, etc) (dummy user, sso password)
|
||||||
|
echo -n ${RAND_USER}@mailcow.local:${RAND_PASS} > /etc/sogo/cron.creds
|
||||||
|
|
||||||
if [[ "${MASTER}" =~ ^([nN][oO]|[nN])+$ ]]; then
|
if [[ "${MASTER}" =~ ^([nN][oO]|[nN])+$ ]]; then
|
||||||
# Toggling MASTER will result in a rebuild of containers, so the quota script will be recreated
|
# Toggling MASTER will result in a rebuild of containers, so the quota script will be recreated
|
||||||
cat <<'EOF' > /usr/local/bin/quota_notify.py
|
cat <<'EOF' > /usr/local/bin/quota_notify.py
|
||||||
|
|||||||
@@ -132,8 +132,8 @@ while ($row = $sth->fetchrow_arrayref()) {
|
|||||||
"--tmpdir", "/tmp",
|
"--tmpdir", "/tmp",
|
||||||
"--nofoldersizes",
|
"--nofoldersizes",
|
||||||
"--addheader",
|
"--addheader",
|
||||||
($timeout1 gt "0" ? () : ('--timeout1', $timeout1)),
|
($timeout1 le "0" ? () : ('--timeout1', $timeout1)),
|
||||||
($timeout2 gt "0" ? () : ('--timeout2', $timeout2)),
|
($timeout2 le "0" ? () : ('--timeout2', $timeout2)),
|
||||||
($exclude eq "" ? () : ("--exclude", $exclude)),
|
($exclude eq "" ? () : ("--exclude", $exclude)),
|
||||||
($subfolder2 eq "" ? () : ('--subfolder2', $subfolder2)),
|
($subfolder2 eq "" ? () : ('--subfolder2', $subfolder2)),
|
||||||
($maxage eq "0" ? () : ('--maxage', $maxage)),
|
($maxage eq "0" ? () : ('--maxage', $maxage)),
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ 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 jinja2
|
import jinja2
|
||||||
from jinja2 import Template
|
from jinja2 import TemplateError
|
||||||
|
from jinja2.sandbox import SandboxedEnvironment
|
||||||
import json
|
import json
|
||||||
import redis
|
import redis
|
||||||
import time
|
import time
|
||||||
@@ -46,7 +47,7 @@ try:
|
|||||||
if max_score == "":
|
if max_score == "":
|
||||||
max_score = 9999.0
|
max_score = 9999.0
|
||||||
|
|
||||||
def query_mysql(query, headers = True, update = False):
|
def query_mysql(query, params = None, headers = True, update = False):
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
cnx = MySQLdb.connect(user=os.environ.get('DBUSER'), password=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")
|
||||||
@@ -56,7 +57,10 @@ try:
|
|||||||
else:
|
else:
|
||||||
break
|
break
|
||||||
cur = cnx.cursor()
|
cur = cnx.cursor()
|
||||||
cur.execute(query)
|
if params:
|
||||||
|
cur.execute(query, params)
|
||||||
|
else:
|
||||||
|
cur.execute(query)
|
||||||
if not update:
|
if not update:
|
||||||
result = []
|
result = []
|
||||||
columns = tuple( [d[0] for d in cur.description] )
|
columns = tuple( [d[0] for d in cur.description] )
|
||||||
@@ -75,22 +79,27 @@ try:
|
|||||||
|
|
||||||
def notify_rcpt(rcpt, msg_count, quarantine_acl, category):
|
def notify_rcpt(rcpt, msg_count, quarantine_acl, category):
|
||||||
if category == "add_header": category = "add header"
|
if category == "add_header": category = "add header"
|
||||||
meta_query = query_mysql('SELECT SHA2(CONCAT(id, qid), 256) AS qhash, id, subject, score, sender, created, action FROM quarantine WHERE notified = 0 AND rcpt = "%s" AND score < %f AND (action = "%s" OR "all" = "%s")' % (rcpt, max_score, category, category))
|
meta_query = query_mysql('SELECT `qhash`, id, subject, score, sender, created, action FROM quarantine WHERE notified = 0 AND rcpt = %s AND score < %s AND (action = %s OR "all" = %s)', (rcpt, max_score, category, category))
|
||||||
print("%s: %d of %d messages qualify for notification" % (rcpt, len(meta_query), msg_count))
|
print("%s: %d of %d messages qualify for notification" % (rcpt, len(meta_query), msg_count))
|
||||||
if len(meta_query) == 0:
|
if len(meta_query) == 0:
|
||||||
return
|
return
|
||||||
msg_count = len(meta_query)
|
msg_count = len(meta_query)
|
||||||
|
env = SandboxedEnvironment()
|
||||||
if r.get('Q_HTML'):
|
if r.get('Q_HTML'):
|
||||||
try:
|
try:
|
||||||
template = Template(r.get('Q_HTML'))
|
template = env.from_string(r.get('Q_HTML'))
|
||||||
except:
|
except Exception:
|
||||||
print("Error: Cannot parse quarantine template, falling back to default template.")
|
print("Error: Cannot parse quarantine template, falling back to default template.")
|
||||||
with open('/templates/quarantine.tpl') as file_:
|
with open('/templates/quarantine.tpl') as file_:
|
||||||
template = Template(file_.read())
|
template = env.from_string(file_.read())
|
||||||
else:
|
else:
|
||||||
with open('/templates/quarantine.tpl') as file_:
|
with open('/templates/quarantine.tpl') as file_:
|
||||||
template = Template(file_.read())
|
template = env.from_string(file_.read())
|
||||||
html = template.render(meta=meta_query, username=rcpt, counter=msg_count, hostname=mailcow_hostname, quarantine_acl=quarantine_acl)
|
try:
|
||||||
|
html = template.render(meta=meta_query, username=rcpt, counter=msg_count, hostname=mailcow_hostname, quarantine_acl=quarantine_acl)
|
||||||
|
except (jinja2.exceptions.SecurityError, TemplateError) as ex:
|
||||||
|
print(f"SecurityError or TemplateError in template rendering: {ex}")
|
||||||
|
return
|
||||||
text = html2text.html2text(html)
|
text = html2text.html2text(html)
|
||||||
count = 0
|
count = 0
|
||||||
while count < 15:
|
while count < 15:
|
||||||
@@ -124,7 +133,7 @@ try:
|
|||||||
server.sendmail(msg['From'], [str(redirect)] + [str(bcc)], text)
|
server.sendmail(msg['From'], [str(redirect)] + [str(bcc)], text)
|
||||||
server.quit()
|
server.quit()
|
||||||
for res in meta_query:
|
for res in meta_query:
|
||||||
query_mysql('UPDATE quarantine SET notified = 1 WHERE id = "%d"' % (res['id']), update = True)
|
query_mysql('UPDATE quarantine SET notified = 1 WHERE id = %s', (res['id'],), update = True)
|
||||||
r.hset('Q_LAST_NOTIFIED', record['rcpt'], time_now)
|
r.hset('Q_LAST_NOTIFIED', record['rcpt'], time_now)
|
||||||
break
|
break
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
@@ -132,7 +141,7 @@ try:
|
|||||||
print('%s' % (ex))
|
print('%s' % (ex))
|
||||||
time.sleep(3)
|
time.sleep(3)
|
||||||
|
|
||||||
records = query_mysql('SELECT IFNULL(user_acl.quarantine, 0) AS quarantine_acl, count(id) AS counter, rcpt FROM quarantine LEFT OUTER JOIN user_acl ON user_acl.username = rcpt WHERE notified = 0 AND score < %f AND rcpt in (SELECT username FROM mailbox) GROUP BY rcpt' % (max_score))
|
records = query_mysql('SELECT IFNULL(user_acl.quarantine, 0) AS quarantine_acl, count(id) AS counter, rcpt FROM quarantine LEFT OUTER JOIN user_acl ON user_acl.username = rcpt WHERE notified = 0 AND score < %s AND rcpt in (SELECT username FROM mailbox) GROUP BY rcpt', (max_score,))
|
||||||
|
|
||||||
for record in records:
|
for record in records:
|
||||||
attrs = ''
|
attrs = ''
|
||||||
@@ -150,7 +159,7 @@ try:
|
|||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print('Could not determine last notification for %s, assuming never' % (record['rcpt']))
|
print('Could not determine last notification for %s, assuming never' % (record['rcpt']))
|
||||||
last_notification = 0
|
last_notification = 0
|
||||||
attrs_json = query_mysql('SELECT attributes FROM mailbox WHERE username = "%s"' % (record['rcpt']))
|
attrs_json = query_mysql('SELECT attributes FROM mailbox WHERE username = %s', (record['rcpt'],))
|
||||||
attrs = attrs_json[0]['attributes']
|
attrs = attrs_json[0]['attributes']
|
||||||
if isinstance(attrs, str):
|
if isinstance(attrs, str):
|
||||||
# if attr is str then just load it
|
# if attr is str then just load it
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ 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 jinja2
|
import jinja2
|
||||||
from jinja2 import Template
|
from jinja2.sandbox import SandboxedEnvironment
|
||||||
import redis
|
import redis
|
||||||
import time
|
import time
|
||||||
import json
|
import json
|
||||||
@@ -33,16 +33,24 @@ while True:
|
|||||||
|
|
||||||
if r.get('QW_HTML'):
|
if r.get('QW_HTML'):
|
||||||
try:
|
try:
|
||||||
template = Template(r.get('QW_HTML'))
|
env = SandboxedEnvironment()
|
||||||
except:
|
template = env.from_string(r.get('QW_HTML'))
|
||||||
print("Error: Cannot parse quarantine template, falling back to default template.")
|
except Exception:
|
||||||
|
print("Error: Cannot parse quota template, falling back to default template.")
|
||||||
with open('/templates/quota.tpl') as file_:
|
with open('/templates/quota.tpl') as file_:
|
||||||
template = Template(file_.read())
|
env = SandboxedEnvironment()
|
||||||
|
template = env.from_string(file_.read())
|
||||||
else:
|
else:
|
||||||
with open('/templates/quota.tpl') as file_:
|
with open('/templates/quota.tpl') as file_:
|
||||||
template = Template(file_.read())
|
env = SandboxedEnvironment()
|
||||||
|
template = env.from_string(file_.read())
|
||||||
|
|
||||||
|
try:
|
||||||
|
html = template.render(username=username, percent=percent)
|
||||||
|
except (jinja2.exceptions.SecurityError, jinja2.TemplateError) as ex:
|
||||||
|
print(f"SecurityError or TemplateError in template rendering: {ex}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
html = template.render(username=username, percent=percent)
|
|
||||||
text = html2text.html2text(html)
|
text = html2text.html2text(html)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM alpine:3.21
|
FROM alpine:3.23
|
||||||
|
|
||||||
LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
|
LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
|
||||||
backend=iptables
|
backend=nftables
|
||||||
|
|
||||||
nft list table ip filter &>/dev/null
|
nft list table ip filter &>/dev/null
|
||||||
nftables_found=$?
|
nftables_found=$?
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
DEBUG = False
|
||||||
|
|
||||||
import re
|
import re
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
@@ -20,10 +22,13 @@ from modules.Logger import Logger
|
|||||||
from modules.IPTables import IPTables
|
from modules.IPTables import IPTables
|
||||||
from modules.NFTables import NFTables
|
from modules.NFTables import NFTables
|
||||||
|
|
||||||
|
def logdebug(msg):
|
||||||
|
if DEBUG:
|
||||||
|
logger.logInfo("DEBUG: %s" % msg)
|
||||||
|
|
||||||
# globals
|
# Globals
|
||||||
WHITELIST = []
|
WHITELIST = []
|
||||||
BLACKLIST= []
|
BLACKLIST = []
|
||||||
bans = {}
|
bans = {}
|
||||||
quit_now = False
|
quit_now = False
|
||||||
exit_code = 0
|
exit_code = 0
|
||||||
@@ -33,12 +38,10 @@ r = None
|
|||||||
pubsub = None
|
pubsub = None
|
||||||
clear_before_quit = False
|
clear_before_quit = False
|
||||||
|
|
||||||
|
|
||||||
def refreshF2boptions():
|
def refreshF2boptions():
|
||||||
global f2boptions
|
global f2boptions
|
||||||
global quit_now
|
global quit_now
|
||||||
global exit_code
|
global exit_code
|
||||||
|
|
||||||
f2boptions = {}
|
f2boptions = {}
|
||||||
|
|
||||||
if not r.get('F2B_OPTIONS'):
|
if not r.get('F2B_OPTIONS'):
|
||||||
@@ -52,8 +55,9 @@ def refreshF2boptions():
|
|||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
f2boptions = json.loads(r.get('F2B_OPTIONS'))
|
f2boptions = json.loads(r.get('F2B_OPTIONS'))
|
||||||
except ValueError:
|
except ValueError as e:
|
||||||
logger.logCrit('Error loading F2B options: F2B_OPTIONS is not json')
|
logger.logCrit(
|
||||||
|
'Error loading F2B options: F2B_OPTIONS is not json. Exception: %s' % e)
|
||||||
quit_now = True
|
quit_now = True
|
||||||
exit_code = 2
|
exit_code = 2
|
||||||
|
|
||||||
@@ -61,15 +65,15 @@ def refreshF2boptions():
|
|||||||
r.set('F2B_OPTIONS', json.dumps(f2boptions, ensure_ascii=False))
|
r.set('F2B_OPTIONS', json.dumps(f2boptions, ensure_ascii=False))
|
||||||
|
|
||||||
def verifyF2boptions(f2boptions):
|
def verifyF2boptions(f2boptions):
|
||||||
verifyF2boption(f2boptions,'ban_time', 1800)
|
verifyF2boption(f2boptions, 'ban_time', 1800)
|
||||||
verifyF2boption(f2boptions,'max_ban_time', 10000)
|
verifyF2boption(f2boptions, 'max_ban_time', 10000)
|
||||||
verifyF2boption(f2boptions,'ban_time_increment', True)
|
verifyF2boption(f2boptions, 'ban_time_increment', True)
|
||||||
verifyF2boption(f2boptions,'max_attempts', 10)
|
verifyF2boption(f2boptions, 'max_attempts', 10)
|
||||||
verifyF2boption(f2boptions,'retry_window', 600)
|
verifyF2boption(f2boptions, 'retry_window', 600)
|
||||||
verifyF2boption(f2boptions,'netban_ipv4', 32)
|
verifyF2boption(f2boptions, 'netban_ipv4', 32)
|
||||||
verifyF2boption(f2boptions,'netban_ipv6', 128)
|
verifyF2boption(f2boptions, 'netban_ipv6', 128)
|
||||||
verifyF2boption(f2boptions,'banlist_id', str(uuid.uuid4()))
|
verifyF2boption(f2boptions, 'banlist_id', str(uuid.uuid4()))
|
||||||
verifyF2boption(f2boptions,'manage_external', 0)
|
verifyF2boption(f2boptions, 'manage_external', 0)
|
||||||
|
|
||||||
def verifyF2boption(f2boptions, f2boption, f2bdefault):
|
def verifyF2boption(f2boptions, f2boption, f2bdefault):
|
||||||
f2boptions[f2boption] = f2boptions[f2boption] if f2boption in f2boptions and f2boptions[f2boption] is not None else f2bdefault
|
f2boptions[f2boption] = f2boptions[f2boption] if f2boption in f2boptions and f2boptions[f2boption] is not None else f2bdefault
|
||||||
@@ -111,7 +115,7 @@ def get_ip(address):
|
|||||||
def ban(address):
|
def ban(address):
|
||||||
global f2boptions
|
global f2boptions
|
||||||
global lock
|
global lock
|
||||||
|
logdebug("ban() called with address=%s" % address)
|
||||||
refreshF2boptions()
|
refreshF2boptions()
|
||||||
MAX_ATTEMPTS = int(f2boptions['max_attempts'])
|
MAX_ATTEMPTS = int(f2boptions['max_attempts'])
|
||||||
RETRY_WINDOW = int(f2boptions['retry_window'])
|
RETRY_WINDOW = int(f2boptions['retry_window'])
|
||||||
@@ -119,31 +123,43 @@ def ban(address):
|
|||||||
NETBAN_IPV6 = '/' + str(f2boptions['netban_ipv6'])
|
NETBAN_IPV6 = '/' + str(f2boptions['netban_ipv6'])
|
||||||
|
|
||||||
ip = get_ip(address)
|
ip = get_ip(address)
|
||||||
if not ip: return
|
if not ip:
|
||||||
|
logdebug("No valid IP -- skipping ban()")
|
||||||
|
return
|
||||||
address = str(ip)
|
address = str(ip)
|
||||||
self_network = ipaddress.ip_network(address)
|
self_network = ipaddress.ip_network(address)
|
||||||
|
|
||||||
with lock:
|
with lock:
|
||||||
temp_whitelist = set(WHITELIST)
|
temp_whitelist = set(WHITELIST)
|
||||||
if temp_whitelist:
|
logdebug("Checking if %s overlaps with any WHITELIST entries" % self_network)
|
||||||
for wl_key in temp_whitelist:
|
if temp_whitelist:
|
||||||
wl_net = ipaddress.ip_network(wl_key, False)
|
for wl_key in temp_whitelist:
|
||||||
if wl_net.overlaps(self_network):
|
wl_net = ipaddress.ip_network(wl_key, False)
|
||||||
logger.logInfo('Address %s is whitelisted by rule %s' % (self_network, wl_net))
|
logdebug("Checking overlap between %s and %s" % (self_network, wl_net))
|
||||||
return
|
if wl_net.overlaps(self_network):
|
||||||
|
logger.logInfo(
|
||||||
|
'Address %s is allowlisted 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 = ipaddress.ip_network(
|
||||||
|
(address + (NETBAN_IPV4 if type(ip) is ipaddress.IPv4Address else NETBAN_IPV6)), strict=False)
|
||||||
net = str(net)
|
net = str(net)
|
||||||
|
logdebug("Ban net: %s" % net)
|
||||||
|
|
||||||
if not net in bans:
|
if not net in bans:
|
||||||
bans[net] = {'attempts': 0, 'last_attempt': 0, 'ban_counter': 0}
|
bans[net] = {'attempts': 0, 'last_attempt': 0, 'ban_counter': 0}
|
||||||
|
logdebug("Initing new ban counter for %s" % net)
|
||||||
|
|
||||||
current_attempt = time.time()
|
current_attempt = time.time()
|
||||||
|
logdebug("Current attempt ts=%s, previous: %s, retry_window: %s" %
|
||||||
|
(current_attempt, bans[net]['last_attempt'], RETRY_WINDOW))
|
||||||
if current_attempt - bans[net]['last_attempt'] > RETRY_WINDOW:
|
if current_attempt - bans[net]['last_attempt'] > RETRY_WINDOW:
|
||||||
bans[net]['attempts'] = 0
|
bans[net]['attempts'] = 0
|
||||||
|
logdebug("Ban counter for %s reset as window expired" % net)
|
||||||
|
|
||||||
bans[net]['attempts'] += 1
|
bans[net]['attempts'] += 1
|
||||||
bans[net]['last_attempt'] = current_attempt
|
bans[net]['last_attempt'] = current_attempt
|
||||||
|
logdebug("%s attempts now %d" % (net, bans[net]['attempts']))
|
||||||
|
|
||||||
if bans[net]['attempts'] >= MAX_ATTEMPTS:
|
if bans[net]['attempts'] >= MAX_ATTEMPTS:
|
||||||
cur_time = int(round(time.time()))
|
cur_time = int(round(time.time()))
|
||||||
@@ -151,34 +167,41 @@ def ban(address):
|
|||||||
logger.logCrit('Banning %s for %d minutes' % (net, NET_BAN_TIME / 60 ))
|
logger.logCrit('Banning %s for %d minutes' % (net, NET_BAN_TIME / 60 ))
|
||||||
if type(ip) is ipaddress.IPv4Address and int(f2boptions['manage_external']) != 1:
|
if type(ip) is ipaddress.IPv4Address and int(f2boptions['manage_external']) != 1:
|
||||||
with lock:
|
with lock:
|
||||||
|
logdebug("Calling tables.banIPv4(%s)" % net)
|
||||||
tables.banIPv4(net)
|
tables.banIPv4(net)
|
||||||
elif int(f2boptions['manage_external']) != 1:
|
elif int(f2boptions['manage_external']) != 1:
|
||||||
with lock:
|
with lock:
|
||||||
|
logdebug("Calling tables.banIPv6(%s)" % net)
|
||||||
tables.banIPv6(net)
|
tables.banIPv6(net)
|
||||||
|
|
||||||
|
logdebug("Updating F2B_ACTIVE_BANS[%s]=%d" %
|
||||||
|
(net, cur_time + NET_BAN_TIME))
|
||||||
r.hset('F2B_ACTIVE_BANS', '%s' % net, cur_time + NET_BAN_TIME)
|
r.hset('F2B_ACTIVE_BANS', '%s' % net, cur_time + NET_BAN_TIME)
|
||||||
else:
|
else:
|
||||||
logger.logWarn('%d more attempts in the next %d seconds until %s is banned' % (MAX_ATTEMPTS - bans[net]['attempts'], RETRY_WINDOW, net))
|
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):
|
def unban(net):
|
||||||
global lock
|
global lock
|
||||||
|
logdebug("Calling unban() with net=%s" % net)
|
||||||
if not net in bans:
|
if not net in bans:
|
||||||
logger.logInfo('%s is not banned, skipping unban and deleting from queue (if any)' % net)
|
logger.logInfo(
|
||||||
r.hdel('F2B_QUEUE_UNBAN', '%s' % net)
|
'%s is not banned, skipping unban and deleting from queue (if any)' % net)
|
||||||
return
|
r.hdel('F2B_QUEUE_UNBAN', '%s' % net)
|
||||||
|
return
|
||||||
logger.logInfo('Unbanning %s' % net)
|
logger.logInfo('Unbanning %s' % net)
|
||||||
if type(ipaddress.ip_network(net)) is ipaddress.IPv4Network:
|
if type(ipaddress.ip_network(net)) is ipaddress.IPv4Network:
|
||||||
with lock:
|
with lock:
|
||||||
|
logdebug("Calling tables.unbanIPv4(%s)" % net)
|
||||||
tables.unbanIPv4(net)
|
tables.unbanIPv4(net)
|
||||||
else:
|
else:
|
||||||
with lock:
|
with lock:
|
||||||
|
logdebug("Calling tables.unbanIPv6(%s)" % net)
|
||||||
tables.unbanIPv6(net)
|
tables.unbanIPv6(net)
|
||||||
|
|
||||||
r.hdel('F2B_ACTIVE_BANS', '%s' % net)
|
r.hdel('F2B_ACTIVE_BANS', '%s' % net)
|
||||||
r.hdel('F2B_QUEUE_UNBAN', '%s' % net)
|
r.hdel('F2B_QUEUE_UNBAN', '%s' % net)
|
||||||
if net in bans:
|
if net in bans:
|
||||||
|
logdebug("Unban for %s, setting attempts=0, ban_counter+=1" % net)
|
||||||
bans[net]['attempts'] = 0
|
bans[net]['attempts'] = 0
|
||||||
bans[net]['ban_counter'] += 1
|
bans[net]['ban_counter'] += 1
|
||||||
|
|
||||||
@@ -204,17 +227,19 @@ def permBan(net, unban=False):
|
|||||||
|
|
||||||
if is_unbanned:
|
if is_unbanned:
|
||||||
r.hdel('F2B_PERM_BANS', '%s' % net)
|
r.hdel('F2B_PERM_BANS', '%s' % net)
|
||||||
logger.logCrit('Removed host/network %s from blacklist' % net)
|
logger.logCrit('Removed host/network %s from denylist' % net)
|
||||||
elif is_banned:
|
elif is_banned:
|
||||||
r.hset('F2B_PERM_BANS', '%s' % net, int(round(time.time())))
|
r.hset('F2B_PERM_BANS', '%s' % net, int(round(time.time())))
|
||||||
logger.logCrit('Added host/network %s to blacklist' % net)
|
logger.logCrit('Added host/network %s to denylist' % net)
|
||||||
|
|
||||||
def clear():
|
def clear():
|
||||||
global lock
|
global lock
|
||||||
logger.logInfo('Clearing all bans')
|
logger.logInfo('Clearing all bans')
|
||||||
for net in bans.copy():
|
for net in bans.copy():
|
||||||
|
logdebug("Unbanning net: %s" % net)
|
||||||
unban(net)
|
unban(net)
|
||||||
with lock:
|
with lock:
|
||||||
|
logdebug("Clearing IPv4/IPv6 table")
|
||||||
tables.clearIPv4Table()
|
tables.clearIPv4Table()
|
||||||
tables.clearIPv6Table()
|
tables.clearIPv6Table()
|
||||||
try:
|
try:
|
||||||
@@ -275,21 +300,35 @@ def snat6(snat_target):
|
|||||||
|
|
||||||
def autopurge():
|
def autopurge():
|
||||||
global f2boptions
|
global f2boptions
|
||||||
|
logdebug("autopurge thread started")
|
||||||
while not quit_now:
|
while not quit_now:
|
||||||
|
logdebug("autopurge tick")
|
||||||
time.sleep(10)
|
time.sleep(10)
|
||||||
refreshF2boptions()
|
refreshF2boptions()
|
||||||
MAX_ATTEMPTS = int(f2boptions['max_attempts'])
|
MAX_ATTEMPTS = int(f2boptions['max_attempts'])
|
||||||
QUEUE_UNBAN = r.hgetall('F2B_QUEUE_UNBAN')
|
QUEUE_UNBAN = r.hgetall('F2B_QUEUE_UNBAN')
|
||||||
|
logdebug("QUEUE_UNBAN: %s" % QUEUE_UNBAN)
|
||||||
if QUEUE_UNBAN:
|
if QUEUE_UNBAN:
|
||||||
for net in QUEUE_UNBAN:
|
for net in QUEUE_UNBAN:
|
||||||
|
logdebug("Autopurge: unbanning queued net: %s" % net)
|
||||||
unban(str(net))
|
unban(str(net))
|
||||||
for net in bans.copy():
|
# Only check expiry for actively banned IPs:
|
||||||
if bans[net]['attempts'] >= MAX_ATTEMPTS:
|
active_bans = r.hgetall('F2B_ACTIVE_BANS')
|
||||||
NET_BAN_TIME = calcNetBanTime(bans[net]['ban_counter'])
|
now = time.time()
|
||||||
TIME_SINCE_LAST_ATTEMPT = time.time() - bans[net]['last_attempt']
|
for net_str, expire_str in active_bans.items():
|
||||||
if TIME_SINCE_LAST_ATTEMPT > NET_BAN_TIME:
|
logdebug("Checking ban expiry for (actively banned): %s" % net_str)
|
||||||
unban(net)
|
# Defensive: always process if timer missing or expired
|
||||||
|
try:
|
||||||
|
expire = float(expire_str)
|
||||||
|
except Exception:
|
||||||
|
logdebug("Invalid expire time for %s; unbanning" % net_str)
|
||||||
|
unban(net_str)
|
||||||
|
continue
|
||||||
|
time_left = expire - now
|
||||||
|
logdebug("Time left for %s: %.1f seconds" % (net_str, time_left))
|
||||||
|
if time_left <= 0:
|
||||||
|
logdebug("Ban expired for %s" % net_str)
|
||||||
|
unban(net_str)
|
||||||
|
|
||||||
def mailcowChainOrder():
|
def mailcowChainOrder():
|
||||||
global lock
|
global lock
|
||||||
@@ -359,7 +398,7 @@ def whitelistUpdate():
|
|||||||
with lock:
|
with lock:
|
||||||
if Counter(new_whitelist) != Counter(WHITELIST):
|
if Counter(new_whitelist) != Counter(WHITELIST):
|
||||||
WHITELIST = new_whitelist
|
WHITELIST = new_whitelist
|
||||||
logger.logInfo('Whitelist was changed, it has %s entries' % len(WHITELIST))
|
logger.logInfo('Allowlist was changed, it has %s entries' % len(WHITELIST))
|
||||||
time.sleep(60.0 - ((time.time() - start_time) % 60.0))
|
time.sleep(60.0 - ((time.time() - start_time) % 60.0))
|
||||||
|
|
||||||
def blacklistUpdate():
|
def blacklistUpdate():
|
||||||
@@ -375,7 +414,7 @@ def blacklistUpdate():
|
|||||||
addban = set(new_blacklist).difference(BLACKLIST)
|
addban = set(new_blacklist).difference(BLACKLIST)
|
||||||
delban = set(BLACKLIST).difference(new_blacklist)
|
delban = set(BLACKLIST).difference(new_blacklist)
|
||||||
BLACKLIST = new_blacklist
|
BLACKLIST = new_blacklist
|
||||||
logger.logInfo('Blacklist was changed, it has %s entries' % len(BLACKLIST))
|
logger.logInfo('Denylist was changed, it has %s entries' % len(BLACKLIST))
|
||||||
if addban:
|
if addban:
|
||||||
for net in addban:
|
for net in addban:
|
||||||
permBan(net=net)
|
permBan(net=net)
|
||||||
@@ -386,42 +425,43 @@ def blacklistUpdate():
|
|||||||
|
|
||||||
def sigterm_quit(signum, frame):
|
def sigterm_quit(signum, frame):
|
||||||
global clear_before_quit
|
global clear_before_quit
|
||||||
|
logdebug("SIGTERM received, setting clear_before_quit to True and exiting")
|
||||||
clear_before_quit = True
|
clear_before_quit = True
|
||||||
sys.exit(exit_code)
|
sys.exit(exit_code)
|
||||||
|
|
||||||
def berfore_quit():
|
def before_quit():
|
||||||
|
logdebug("before_quit called, clear_before_quit=%s" % clear_before_quit)
|
||||||
if clear_before_quit:
|
if clear_before_quit:
|
||||||
clear()
|
clear()
|
||||||
if pubsub is not None:
|
if pubsub is not None:
|
||||||
pubsub.unsubscribe()
|
pubsub.unsubscribe()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
atexit.register(berfore_quit)
|
logger = Logger()
|
||||||
|
logdebug("Sys.argv: %s" % sys.argv)
|
||||||
|
atexit.register(before_quit)
|
||||||
signal.signal(signal.SIGTERM, sigterm_quit)
|
signal.signal(signal.SIGTERM, sigterm_quit)
|
||||||
|
|
||||||
# init Logger
|
|
||||||
logger = Logger()
|
|
||||||
|
|
||||||
# init backend
|
|
||||||
backend = sys.argv[1]
|
backend = sys.argv[1]
|
||||||
|
logdebug("Backend: %s" % backend)
|
||||||
if backend == "nftables":
|
if backend == "nftables":
|
||||||
logger.logInfo('Using NFTables backend')
|
logger.logInfo('Using NFTables backend')
|
||||||
tables = NFTables(chain_name, logger)
|
tables = NFTables(chain_name, logger)
|
||||||
else:
|
else:
|
||||||
logger.logInfo('Using IPTables backend')
|
logger.logInfo('Using IPTables backend')
|
||||||
|
logger.logWarn(
|
||||||
|
"DEPRECATION: iptables-legacy is deprecated and will be removed in future releases. "
|
||||||
|
"Please switch to nftables on your host to ensure complete compatibility."
|
||||||
|
)
|
||||||
|
time.sleep(5)
|
||||||
tables = IPTables(chain_name, logger)
|
tables = IPTables(chain_name, logger)
|
||||||
|
|
||||||
# In case a previous session was killed without cleanup
|
|
||||||
clear()
|
clear()
|
||||||
|
|
||||||
# Reinit MAILCOW chain
|
|
||||||
# Is called before threads start, no locking
|
|
||||||
logger.logInfo("Initializing mailcow netfilter chain")
|
logger.logInfo("Initializing mailcow netfilter chain")
|
||||||
tables.initChainIPv4()
|
tables.initChainIPv4()
|
||||||
tables.initChainIPv6()
|
tables.initChainIPv6()
|
||||||
|
|
||||||
if os.getenv("DISABLE_NETFILTER_ISOLATION_RULE").lower() in ("y", "yes"):
|
if os.getenv("DISABLE_NETFILTER_ISOLATION_RULE", "").lower() in ("y", "yes"):
|
||||||
logger.logInfo(f"Skipping {chain_name} isolation")
|
logger.logInfo(f"Skipping {chain_name} isolation")
|
||||||
else:
|
else:
|
||||||
logger.logInfo(f"Setting {chain_name} isolation")
|
logger.logInfo(f"Setting {chain_name} isolation")
|
||||||
@@ -432,23 +472,28 @@ if __name__ == '__main__':
|
|||||||
try:
|
try:
|
||||||
redis_slaveof_ip = os.getenv('REDIS_SLAVEOF_IP', '')
|
redis_slaveof_ip = os.getenv('REDIS_SLAVEOF_IP', '')
|
||||||
redis_slaveof_port = os.getenv('REDIS_SLAVEOF_PORT', '')
|
redis_slaveof_port = os.getenv('REDIS_SLAVEOF_PORT', '')
|
||||||
|
logdebug(
|
||||||
|
"Connecting redis (SLAVEOF_IP:%s, PORT:%s)" % (redis_slaveof_ip, redis_slaveof_port))
|
||||||
if "".__eq__(redis_slaveof_ip):
|
if "".__eq__(redis_slaveof_ip):
|
||||||
r = redis.StrictRedis(host=os.getenv('IPV4_NETWORK', '172.22.1') + '.249', decode_responses=True, port=6379, db=0, password=os.environ['REDISPASS'])
|
r = redis.StrictRedis(
|
||||||
|
host=os.getenv('IPV4_NETWORK', '172.22.1') + '.249', decode_responses=True, port=6379, db=0, password=os.environ['REDISPASS'])
|
||||||
else:
|
else:
|
||||||
r = redis.StrictRedis(host=redis_slaveof_ip, decode_responses=True, port=redis_slaveof_port, db=0, password=os.environ['REDISPASS'])
|
r = redis.StrictRedis(
|
||||||
|
host=redis_slaveof_ip, decode_responses=True, port=redis_slaveof_port, db=0, password=os.environ['REDISPASS'])
|
||||||
r.ping()
|
r.ping()
|
||||||
pubsub = r.pubsub()
|
pubsub = r.pubsub()
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print('%s - trying again in 3 seconds' % (ex))
|
logdebug(
|
||||||
|
'Redis connection failed: %s - trying again in 3 seconds' % (ex))
|
||||||
time.sleep(3)
|
time.sleep(3)
|
||||||
else:
|
else:
|
||||||
break
|
break
|
||||||
logger.set_redis(r)
|
logger.set_redis(r)
|
||||||
|
logdebug("Redis connection established, setting up F2B keys")
|
||||||
|
|
||||||
# rename fail2ban to netfilter
|
|
||||||
if r.exists('F2B_LOG'):
|
if r.exists('F2B_LOG'):
|
||||||
|
logdebug("Renaming F2B_LOG to NETFILTER_LOG")
|
||||||
r.rename('F2B_LOG', 'NETFILTER_LOG')
|
r.rename('F2B_LOG', 'NETFILTER_LOG')
|
||||||
# clear bans in redis
|
|
||||||
r.delete('F2B_ACTIVE_BANS')
|
r.delete('F2B_ACTIVE_BANS')
|
||||||
r.delete('F2B_PERM_BANS')
|
r.delete('F2B_PERM_BANS')
|
||||||
|
|
||||||
@@ -463,7 +508,7 @@ if __name__ == '__main__':
|
|||||||
snat_ip = os.getenv('SNAT_TO_SOURCE')
|
snat_ip = os.getenv('SNAT_TO_SOURCE')
|
||||||
snat_ipo = ipaddress.ip_address(snat_ip)
|
snat_ipo = ipaddress.ip_address(snat_ip)
|
||||||
if type(snat_ipo) is ipaddress.IPv4Address:
|
if type(snat_ipo) is ipaddress.IPv4Address:
|
||||||
snat4_thread = Thread(target=snat4,args=(snat_ip,))
|
snat4_thread = Thread(target=snat4, args=(snat_ip,))
|
||||||
snat4_thread.daemon = True
|
snat4_thread.daemon = True
|
||||||
snat4_thread.start()
|
snat4_thread.start()
|
||||||
except ValueError:
|
except ValueError:
|
||||||
@@ -499,4 +544,5 @@ if __name__ == '__main__':
|
|||||||
while not quit_now:
|
while not quit_now:
|
||||||
time.sleep(0.5)
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
logdebug("Exiting with code %s" % exit_code)
|
||||||
sys.exit(exit_code)
|
sys.exit(exit_code)
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import time
|
import time
|
||||||
import json
|
import json
|
||||||
|
import datetime
|
||||||
|
|
||||||
class Logger:
|
class Logger:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
@@ -8,17 +9,28 @@ class Logger:
|
|||||||
def set_redis(self, redis):
|
def set_redis(self, redis):
|
||||||
self.r = redis
|
self.r = redis
|
||||||
|
|
||||||
|
def _format_timestamp(self):
|
||||||
|
# Local time with milliseconds
|
||||||
|
return datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
|
||||||
def log(self, priority, message):
|
def log(self, priority, message):
|
||||||
tolog = {}
|
# build redis-friendly dict
|
||||||
tolog['time'] = int(round(time.time()))
|
tolog = {
|
||||||
tolog['priority'] = priority
|
'time': int(round(time.time())), # keep raw timestamp for Redis
|
||||||
tolog['message'] = message
|
'priority': priority,
|
||||||
print(message)
|
'message': message
|
||||||
|
}
|
||||||
|
|
||||||
|
# print human-readable message with timestamp
|
||||||
|
ts = self._format_timestamp()
|
||||||
|
print(f"{ts} {priority.upper()}: {message}", flush=True)
|
||||||
|
|
||||||
|
# also push JSON to Redis if connected
|
||||||
if self.r is not None:
|
if self.r is not None:
|
||||||
try:
|
try:
|
||||||
self.r.lpush('NETFILTER_LOG', json.dumps(tolog, ensure_ascii=False))
|
self.r.lpush('NETFILTER_LOG', json.dumps(tolog, ensure_ascii=False))
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print('Failed logging to redis: %s' % (ex))
|
print(f'{ts} WARN: Failed logging to redis: {ex}', flush=True)
|
||||||
|
|
||||||
def logWarn(self, message):
|
def logWarn(self, message):
|
||||||
self.log('warn', message)
|
self.log('warn', message)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM nginx:alpine
|
FROM nginx:1.30.4-alpine
|
||||||
LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>"
|
LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>"
|
||||||
|
|
||||||
ENV PIP_BREAK_SYSTEM_PACKAGES=1
|
ENV PIP_BREAK_SYSTEM_PACKAGES=1
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ def includes_conf(env, template_vars):
|
|||||||
server_name_config = f"server_name {template_vars['MAILCOW_HOSTNAME']} autodiscover.* autoconfig.* {' '.join(template_vars['ADDITIONAL_SERVER_NAMES'])};"
|
server_name_config = f"server_name {template_vars['MAILCOW_HOSTNAME']} autodiscover.* autoconfig.* {' '.join(template_vars['ADDITIONAL_SERVER_NAMES'])};"
|
||||||
listen_plain_config = f"listen {template_vars['HTTP_PORT']};"
|
listen_plain_config = f"listen {template_vars['HTTP_PORT']};"
|
||||||
listen_ssl_config = f"listen {template_vars['HTTPS_PORT']};"
|
listen_ssl_config = f"listen {template_vars['HTTPS_PORT']};"
|
||||||
if not template_vars['DISABLE_IPv6']:
|
if template_vars['ENABLE_IPV6']:
|
||||||
listen_plain_config += f"\nlisten [::]:{template_vars['HTTP_PORT']};"
|
listen_plain_config += f"\nlisten [::]:{template_vars['HTTP_PORT']};"
|
||||||
listen_ssl_config += f"\nlisten [::]:{template_vars['HTTPS_PORT']} ssl;"
|
listen_ssl_config += f"\nlisten [::]:{template_vars['HTTPS_PORT']} ssl;"
|
||||||
listen_ssl_config += "\nhttp2 on;"
|
listen_ssl_config += "\nhttp2 on;"
|
||||||
@@ -58,7 +58,7 @@ def prepare_template_vars():
|
|||||||
'SOGOHOST': os.getenv("SOGOHOST", ipv4_network + ".248"),
|
'SOGOHOST': os.getenv("SOGOHOST", ipv4_network + ".248"),
|
||||||
'RSPAMDHOST': os.getenv("RSPAMDHOST", "rspamd-mailcow"),
|
'RSPAMDHOST': os.getenv("RSPAMDHOST", "rspamd-mailcow"),
|
||||||
'PHPFPMHOST': os.getenv("PHPFPMHOST", "php-fpm-mailcow"),
|
'PHPFPMHOST': os.getenv("PHPFPMHOST", "php-fpm-mailcow"),
|
||||||
'DISABLE_IPv6': os.getenv("DISABLE_IPv6", "n").lower() in ("y", "yes"),
|
'ENABLE_IPV6': os.getenv("ENABLE_IPV6", "true").lower() != "false",
|
||||||
'HTTP_REDIRECT': os.getenv("HTTP_REDIRECT", "n").lower() in ("y", "yes"),
|
'HTTP_REDIRECT': os.getenv("HTTP_REDIRECT", "n").lower() in ("y", "yes"),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
FROM php:8.2-fpm-alpine3.21
|
FROM php:8.2-fpm-alpine3.24
|
||||||
|
|
||||||
LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
|
LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
|
||||||
|
|
||||||
# renovate: datasource=github-tags depName=krakjoe/apcu versioning=semver-coerced extractVersion=^v(?<version>.*)$
|
# renovate: datasource=github-tags depName=krakjoe/apcu versioning=semver-coerced extractVersion=^v(?<version>.*)$
|
||||||
ARG APCU_PECL_VERSION=5.1.24
|
ARG APCU_PECL_VERSION=5.1.28
|
||||||
# renovate: datasource=github-tags depName=Imagick/imagick versioning=semver-coerced extractVersion=(?<version>.*)$
|
# renovate: datasource=github-tags depName=Imagick/imagick versioning=semver-coerced extractVersion=(?<version>.*)$
|
||||||
ARG IMAGICK_PECL_VERSION=3.8.0
|
ARG IMAGICK_PECL_VERSION=3.8.1
|
||||||
# renovate: datasource=github-tags depName=php/pecl-mail-mailparse versioning=semver-coerced extractVersion=^v(?<version>.*)$
|
# renovate: datasource=github-tags depName=php/pecl-mail-mailparse versioning=semver-coerced extractVersion=^v(?<version>.*)$
|
||||||
ARG MAILPARSE_PECL_VERSION=3.1.8
|
ARG MAILPARSE_PECL_VERSION=3.2.0
|
||||||
# renovate: datasource=github-tags depName=php-memcached-dev/php-memcached versioning=semver-coerced extractVersion=^v(?<version>.*)$
|
# renovate: datasource=github-tags depName=php-memcached-dev/php-memcached versioning=semver-coerced extractVersion=^v(?<version>.*)$
|
||||||
ARG MEMCACHED_PECL_VERSION=3.2.0
|
ARG MEMCACHED_PECL_VERSION=3.4.0
|
||||||
# renovate: datasource=github-tags depName=phpredis/phpredis versioning=semver-coerced extractVersion=(?<version>.*)$
|
# renovate: datasource=github-tags depName=phpredis/phpredis versioning=semver-coerced extractVersion=(?<version>.*)$
|
||||||
ARG REDIS_PECL_VERSION=6.1.0
|
ARG REDIS_PECL_VERSION=6.3.0
|
||||||
# renovate: datasource=github-tags depName=composer/composer versioning=semver-coerced extractVersion=(?<version>.*)$
|
# renovate: datasource=github-tags depName=composer/composer versioning=semver-coerced extractVersion=(?<version>.*)$
|
||||||
ARG COMPOSER_VERSION=2.8.6
|
ARG COMPOSER_VERSION=2.10.2
|
||||||
|
|
||||||
RUN apk add -U --no-cache autoconf \
|
RUN apk add -U --no-cache autoconf \
|
||||||
aspell-dev \
|
aspell-dev \
|
||||||
@@ -47,6 +47,7 @@ RUN apk add -U --no-cache autoconf \
|
|||||||
libressl-dev \
|
libressl-dev \
|
||||||
librsvg \
|
librsvg \
|
||||||
libtool \
|
libtool \
|
||||||
|
libwebp \
|
||||||
libwebp-dev \
|
libwebp-dev \
|
||||||
libxml2-dev \
|
libxml2-dev \
|
||||||
libxpm \
|
libxpm \
|
||||||
@@ -63,13 +64,16 @@ RUN apk add -U --no-cache autoconf \
|
|||||||
samba-client \
|
samba-client \
|
||||||
zlib-dev \
|
zlib-dev \
|
||||||
tzdata \
|
tzdata \
|
||||||
&& pecl install APCu-${APCU_PECL_VERSION} \
|
&& docker-php-source extract \
|
||||||
&& pecl install imagick-${IMAGICK_PECL_VERSION} \
|
# PECL extensions from their GitHub tags (pecl.php.net is not reliable enough for builds)
|
||||||
&& pecl install mailparse-${MAILPARSE_PECL_VERSION} \
|
&& mkdir -p /usr/src/php/ext/apcu /usr/src/php/ext/imagick /usr/src/php/ext/mailparse /usr/src/php/ext/memcached /usr/src/php/ext/redis \
|
||||||
&& pecl install memcached-${MEMCACHED_PECL_VERSION} \
|
&& curl -fsSL https://github.com/krakjoe/apcu/archive/refs/tags/v${APCU_PECL_VERSION}.tar.gz | tar -xz --strip-components=1 -C /usr/src/php/ext/apcu \
|
||||||
&& pecl install redis-${REDIS_PECL_VERSION} \
|
&& curl -fsSL https://github.com/Imagick/imagick/archive/refs/tags/${IMAGICK_PECL_VERSION}.tar.gz | tar -xz --strip-components=1 -C /usr/src/php/ext/imagick \
|
||||||
&& docker-php-ext-enable apcu imagick memcached mailparse redis \
|
&& sed -i "s/@PACKAGE_VERSION@/${IMAGICK_PECL_VERSION}/" /usr/src/php/ext/imagick/php_imagick.h \
|
||||||
&& pecl clear-cache \
|
&& curl -fsSL https://github.com/php/pecl-mail-mailparse/archive/refs/tags/v${MAILPARSE_PECL_VERSION}.tar.gz | tar -xz --strip-components=1 -C /usr/src/php/ext/mailparse \
|
||||||
|
&& curl -fsSL https://github.com/php-memcached-dev/php-memcached/archive/refs/tags/v${MEMCACHED_PECL_VERSION}.tar.gz | tar -xz --strip-components=1 -C /usr/src/php/ext/memcached \
|
||||||
|
&& curl -fsSL https://github.com/phpredis/phpredis/archive/refs/tags/${REDIS_PECL_VERSION}.tar.gz | tar -xz --strip-components=1 -C /usr/src/php/ext/redis \
|
||||||
|
&& docker-php-ext-install -j 4 apcu imagick mailparse memcached redis \
|
||||||
&& docker-php-ext-configure intl \
|
&& docker-php-ext-configure intl \
|
||||||
&& docker-php-ext-configure exif \
|
&& docker-php-ext-configure exif \
|
||||||
&& docker-php-ext-configure gd --with-freetype=/usr/include/ \
|
&& docker-php-ext-configure gd --with-freetype=/usr/include/ \
|
||||||
@@ -83,6 +87,7 @@ RUN apk add -U --no-cache autoconf \
|
|||||||
&& curl --silent --show-error https://getcomposer.org/installer | php -- --version=${COMPOSER_VERSION} \
|
&& curl --silent --show-error https://getcomposer.org/installer | php -- --version=${COMPOSER_VERSION} \
|
||||||
&& mv composer.phar /usr/local/bin/composer \
|
&& mv composer.phar /usr/local/bin/composer \
|
||||||
&& chmod +x /usr/local/bin/composer \
|
&& chmod +x /usr/local/bin/composer \
|
||||||
|
&& docker-php-source delete \
|
||||||
&& apk del --purge autoconf \
|
&& apk del --purge autoconf \
|
||||||
aspell-dev \
|
aspell-dev \
|
||||||
cyrus-sasl-dev \
|
cyrus-sasl-dev \
|
||||||
|
|||||||
@@ -167,7 +167,7 @@ DELIMITER //
|
|||||||
CREATE EVENT clean_spamalias
|
CREATE EVENT clean_spamalias
|
||||||
ON SCHEDULE EVERY 1 DAY DO
|
ON SCHEDULE EVERY 1 DAY DO
|
||||||
BEGIN
|
BEGIN
|
||||||
DELETE FROM spamalias WHERE validity < UNIX_TIMESTAMP();
|
DELETE FROM spamalias WHERE validity < UNIX_TIMESTAMP() AND permanent = 0;
|
||||||
END;
|
END;
|
||||||
//
|
//
|
||||||
DELIMITER ;
|
DELIMITER ;
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
FROM golang:1.26-trixie AS builder
|
||||||
|
WORKDIR /src
|
||||||
|
|
||||||
|
ENV CGO_ENABLED=0 \
|
||||||
|
GO111MODULE=on \
|
||||||
|
NOOPT=1 \
|
||||||
|
VERSION=1.14.0
|
||||||
|
|
||||||
|
RUN git clone --branch v${VERSION} https://github.com/Zuplu/postfix-tlspol && \
|
||||||
|
cd /src/postfix-tlspol && \
|
||||||
|
scripts/build.sh build-only
|
||||||
|
|
||||||
|
|
||||||
|
FROM debian:trixie-slim
|
||||||
|
LABEL maintainer="The Infrastructure Company GmbH <info@servercow.de>"
|
||||||
|
|
||||||
|
ARG DEBIAN_FRONTEND=noninteractive
|
||||||
|
ENV LC_ALL=C
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
ca-certificates \
|
||||||
|
dirmngr \
|
||||||
|
dnsutils \
|
||||||
|
iputils-ping \
|
||||||
|
sudo \
|
||||||
|
supervisor \
|
||||||
|
redis-tools \
|
||||||
|
syslog-ng \
|
||||||
|
syslog-ng-core \
|
||||||
|
syslog-ng-mod-redis \
|
||||||
|
tzdata \
|
||||||
|
&& rm -rf /var/lib/apt/lists/* \
|
||||||
|
&& touch /etc/default/locale
|
||||||
|
|
||||||
|
COPY supervisord.conf /etc/supervisor/supervisord.conf
|
||||||
|
COPY syslog-ng.conf /etc/syslog-ng/syslog-ng.conf
|
||||||
|
COPY syslog-ng-redis_slave.conf /etc/syslog-ng/syslog-ng-redis_slave.conf
|
||||||
|
COPY postfix-tlspol.sh /opt/postfix-tlspol.sh
|
||||||
|
COPY stop-supervisor.sh /usr/local/sbin/stop-supervisor.sh
|
||||||
|
COPY docker-entrypoint.sh /docker-entrypoint.sh
|
||||||
|
COPY --from=builder /src/postfix-tlspol/build/postfix-tlspol /usr/local/bin/postfix-tlspol
|
||||||
|
|
||||||
|
RUN chmod +x /opt/postfix-tlspol.sh \
|
||||||
|
/usr/local/sbin/stop-supervisor.sh \
|
||||||
|
/docker-entrypoint.sh
|
||||||
|
RUN rm -rf /tmp/* /var/tmp/*
|
||||||
|
|
||||||
|
ENTRYPOINT ["/docker-entrypoint.sh"]
|
||||||
|
|
||||||
|
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/supervisord.conf"]
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
if [[ ! -z ${REDIS_SLAVEOF_IP} ]]; then
|
||||||
|
cp /etc/syslog-ng/syslog-ng-redis_slave.conf /etc/syslog-ng/syslog-ng.conf
|
||||||
|
fi
|
||||||
|
|
||||||
|
exec "$@"
|
||||||
+52
@@ -0,0 +1,52 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
LOGLVL=info
|
||||||
|
|
||||||
|
if [ ${DEV_MODE} != "n" ]; then
|
||||||
|
echo -e "\e[31mEnabling debug mode\e[0m"
|
||||||
|
set -x
|
||||||
|
LOGLVL=debug
|
||||||
|
fi
|
||||||
|
|
||||||
|
[[ ! -d /etc/postfix-tlspol ]] && mkdir -p /etc/postfix-tlspol
|
||||||
|
[[ ! -d /var/lib/postfix-tlspol ]] && mkdir -p /var/lib/postfix-tlspol
|
||||||
|
|
||||||
|
until dig +short mailcow.email > /dev/null; do
|
||||||
|
echo "Waiting for DNS..."
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
|
# Do not attempt to write to slave
|
||||||
|
if [[ ! -z ${REDIS_SLAVEOF_IP} ]]; then
|
||||||
|
export REDIS_CMDLINE="redis-cli -h ${REDIS_SLAVEOF_IP} -p ${REDIS_SLAVEOF_PORT} -a ${REDISPASS} --no-auth-warning"
|
||||||
|
else
|
||||||
|
export REDIS_CMDLINE="redis-cli -h redis -p 6379 -a ${REDISPASS} --no-auth-warning"
|
||||||
|
fi
|
||||||
|
|
||||||
|
until [[ $(${REDIS_CMDLINE} PING) == "PONG" ]]; do
|
||||||
|
echo "Waiting for Redis..."
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Waiting for Postfix..."
|
||||||
|
until ping postfix -c1 > /dev/null; do
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
echo "Postfix OK"
|
||||||
|
|
||||||
|
cat <<EOF > /etc/postfix-tlspol/config.yaml
|
||||||
|
server:
|
||||||
|
address: 0.0.0.0:8642
|
||||||
|
|
||||||
|
log-level: ${LOGLVL}
|
||||||
|
|
||||||
|
prefetch: true
|
||||||
|
|
||||||
|
cache-file: /var/lib/postfix-tlspol/cache.db
|
||||||
|
|
||||||
|
dns:
|
||||||
|
# must support DNSSEC
|
||||||
|
address: 127.0.0.11:53
|
||||||
|
EOF
|
||||||
|
|
||||||
|
/usr/local/bin/postfix-tlspol -config /etc/postfix-tlspol/config.yaml
|
||||||
+8
@@ -0,0 +1,8 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
printf "READY\n";
|
||||||
|
|
||||||
|
while read line; do
|
||||||
|
echo "Processing Event: $line" >&2;
|
||||||
|
kill -3 $(cat "/var/run/supervisord.pid")
|
||||||
|
done < /dev/stdin
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
[supervisord]
|
||||||
|
pidfile=/var/run/supervisord.pid
|
||||||
|
nodaemon=true
|
||||||
|
user=root
|
||||||
|
|
||||||
|
[program:syslog-ng]
|
||||||
|
command=/usr/sbin/syslog-ng --foreground --no-caps
|
||||||
|
stdout_logfile=/dev/stdout
|
||||||
|
stdout_logfile_maxbytes=0
|
||||||
|
stderr_logfile=/dev/stderr
|
||||||
|
stderr_logfile_maxbytes=0
|
||||||
|
autostart=true
|
||||||
|
|
||||||
|
[program:postfix-tlspol]
|
||||||
|
startsecs=10
|
||||||
|
autorestart=true
|
||||||
|
command=/opt/postfix-tlspol.sh
|
||||||
|
stdout_logfile=/dev/stdout
|
||||||
|
stdout_logfile_maxbytes=0
|
||||||
|
stderr_logfile=/dev/stderr
|
||||||
|
stderr_logfile_maxbytes=0
|
||||||
|
|
||||||
|
[eventlistener:processes]
|
||||||
|
command=/usr/local/sbin/stop-supervisor.sh
|
||||||
|
events=PROCESS_STATE_STOPPED, PROCESS_STATE_EXITED, PROCESS_STATE_FATAL
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
@version: 4.8
|
||||||
|
@include "scl.conf"
|
||||||
|
options {
|
||||||
|
chain_hostnames(off);
|
||||||
|
flush_lines(0);
|
||||||
|
use_dns(no);
|
||||||
|
dns_cache(no);
|
||||||
|
use_fqdn(no);
|
||||||
|
owner("root"); group("adm"); perm(0640);
|
||||||
|
stats(freq(0));
|
||||||
|
bad_hostname("^gconfd$");
|
||||||
|
};
|
||||||
|
source s_src {
|
||||||
|
unix-stream("/dev/log");
|
||||||
|
internal();
|
||||||
|
};
|
||||||
|
destination d_stdout { pipe("/dev/stdout"); };
|
||||||
|
destination d_redis_ui_log {
|
||||||
|
redis(
|
||||||
|
host("`REDIS_SLAVEOF_IP`")
|
||||||
|
persist-name("redis1")
|
||||||
|
port(`REDIS_SLAVEOF_PORT`)
|
||||||
|
auth("`REDISPASS`")
|
||||||
|
command("LPUSH" "POSTFIX_MAILLOG" "$(format-json time=\"$S_UNIXTIME\" priority=\"$PRIORITY\" program=\"$PROGRAM\" message=\"$MESSAGE\")\n")
|
||||||
|
);
|
||||||
|
};
|
||||||
|
filter f_mail { facility(mail); };
|
||||||
|
# start
|
||||||
|
# overriding warnings are still displayed when the entrypoint runs its initial check
|
||||||
|
# warnings logged by postfix-mailcow to syslog are hidden to reduce repeating msgs
|
||||||
|
# Some other warnings are ignored
|
||||||
|
filter f_ignore {
|
||||||
|
not match("overriding earlier entry" value("MESSAGE"));
|
||||||
|
not match("TLS SNI from checks.mailcow.email" value("MESSAGE"));
|
||||||
|
not match("no SASL support" value("MESSAGE"));
|
||||||
|
not facility (local0, local1, local2, local3, local4, local5, local6, local7);
|
||||||
|
};
|
||||||
|
# end
|
||||||
|
log {
|
||||||
|
source(s_src);
|
||||||
|
filter(f_ignore);
|
||||||
|
destination(d_stdout);
|
||||||
|
filter(f_mail);
|
||||||
|
destination(d_redis_ui_log);
|
||||||
|
};
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
@version: 4.8
|
||||||
|
@include "scl.conf"
|
||||||
|
options {
|
||||||
|
chain_hostnames(off);
|
||||||
|
flush_lines(0);
|
||||||
|
use_dns(no);
|
||||||
|
dns_cache(no);
|
||||||
|
use_fqdn(no);
|
||||||
|
owner("root"); group("adm"); perm(0640);
|
||||||
|
stats(freq(0));
|
||||||
|
bad_hostname("^gconfd$");
|
||||||
|
};
|
||||||
|
source s_src {
|
||||||
|
unix-stream("/dev/log");
|
||||||
|
internal();
|
||||||
|
};
|
||||||
|
destination d_stdout { pipe("/dev/stdout"); };
|
||||||
|
destination d_redis_ui_log {
|
||||||
|
redis(
|
||||||
|
host("redis-mailcow")
|
||||||
|
persist-name("redis1")
|
||||||
|
port(6379)
|
||||||
|
auth("`REDISPASS`")
|
||||||
|
command("LPUSH" "POSTFIX_MAILLOG" "$(format-json time=\"$S_UNIXTIME\" priority=\"$PRIORITY\" program=\"$PROGRAM\" message=\"$MESSAGE\")\n")
|
||||||
|
);
|
||||||
|
};
|
||||||
|
filter f_mail { facility(mail); };
|
||||||
|
# start
|
||||||
|
# overriding warnings are still displayed when the entrypoint runs its initial check
|
||||||
|
# warnings logged by postfix-mailcow to syslog are hidden to reduce repeating msgs
|
||||||
|
# Some other warnings are ignored
|
||||||
|
filter f_ignore {
|
||||||
|
not match("overriding earlier entry" value("MESSAGE"));
|
||||||
|
not match("TLS SNI from checks.mailcow.email" value("MESSAGE"));
|
||||||
|
not match("no SASL support" value("MESSAGE"));
|
||||||
|
not facility (local0, local1, local2, local3, local4, local5, local6, local7);
|
||||||
|
};
|
||||||
|
# end
|
||||||
|
log {
|
||||||
|
source(s_src);
|
||||||
|
filter(f_ignore);
|
||||||
|
destination(d_stdout);
|
||||||
|
filter(f_mail);
|
||||||
|
destination(d_redis_ui_log);
|
||||||
|
};
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
FROM debian:bookworm-slim
|
FROM debian:trixie-slim
|
||||||
|
|
||||||
LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
|
LABEL maintainer="The Infrastructure Company GmbH <info@servercow.de>"
|
||||||
|
|
||||||
ARG DEBIAN_FRONTEND=noninteractive
|
ARG DEBIAN_FRONTEND=noninteractive
|
||||||
ENV LC_ALL C
|
ENV LC_ALL=C
|
||||||
|
|
||||||
RUN dpkg-divert --local --rename --add /sbin/initctl \
|
RUN dpkg-divert --local --rename --add /sbin/initctl \
|
||||||
&& ln -sf /bin/true /sbin/initctl \
|
&& ln -sf /bin/true /sbin/initctl \
|
||||||
|
|||||||
@@ -329,14 +329,17 @@ query = SELECT goto FROM alias
|
|||||||
SELECT id FROM alias
|
SELECT id FROM alias
|
||||||
WHERE address='%s'
|
WHERE address='%s'
|
||||||
AND (active='1' OR active='2')
|
AND (active='1' OR active='2')
|
||||||
|
AND sender_allowed='1'
|
||||||
), (
|
), (
|
||||||
SELECT id FROM alias
|
SELECT id FROM alias
|
||||||
WHERE address='@%d'
|
WHERE address='@%d'
|
||||||
AND (active='1' OR active='2')
|
AND (active='1' OR active='2')
|
||||||
|
AND sender_allowed='1'
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
AND active='1'
|
AND active='1'
|
||||||
|
AND sender_allowed='1'
|
||||||
AND (domain IN
|
AND (domain IN
|
||||||
(SELECT domain FROM domain
|
(SELECT domain FROM domain
|
||||||
WHERE domain='%d'
|
WHERE domain='%d'
|
||||||
@@ -390,7 +393,7 @@ hosts = unix:/var/run/mysqld/mysqld.sock
|
|||||||
dbname = ${DBNAME}
|
dbname = ${DBNAME}
|
||||||
query = SELECT goto FROM spamalias
|
query = SELECT goto FROM spamalias
|
||||||
WHERE address='%s'
|
WHERE address='%s'
|
||||||
AND validity >= UNIX_TIMESTAMP()
|
AND (validity >= UNIX_TIMESTAMP() OR permanent != 0)
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
if [ ! -f /opt/postfix/conf/dns_blocklists.cf ]; then
|
if [ ! -f /opt/postfix/conf/dns_blocklists.cf ]; then
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@version: 3.38
|
@version: 4.8
|
||||||
@include "scl.conf"
|
@include "scl.conf"
|
||||||
options {
|
options {
|
||||||
chain_hostnames(off);
|
chain_hostnames(off);
|
||||||
@@ -7,7 +7,7 @@ options {
|
|||||||
dns_cache(no);
|
dns_cache(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_src {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@version: 3.38
|
@version: 4.8
|
||||||
@include "scl.conf"
|
@include "scl.conf"
|
||||||
options {
|
options {
|
||||||
chain_hostnames(off);
|
chain_hostnames(off);
|
||||||
@@ -7,7 +7,7 @@ options {
|
|||||||
dns_cache(no);
|
dns_cache(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_src {
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
FROM debian:bookworm-slim
|
FROM debian:trixie-slim
|
||||||
LABEL maintainer="The Infrastructure Company GmbH <info@servercow.de>"
|
LABEL maintainer="The Infrastructure Company GmbH <info@servercow.de>"
|
||||||
|
|
||||||
ARG DEBIAN_FRONTEND=noninteractive
|
ARG DEBIAN_FRONTEND=noninteractive
|
||||||
ARG RSPAMD_VER=rspamd_3.11.1-1~ab0b44951
|
ARG RSPAMD_VER=rspamd_4.2.1-1~b7a16ae
|
||||||
ARG CODENAME=bookworm
|
ARG CODENAME=trixie
|
||||||
ENV LC_ALL=C
|
ENV LC_ALL=C
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
|||||||
@@ -86,7 +86,8 @@ if [[ "${SKIP_OLEFY}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
|
|||||||
rm /etc/rspamd/local.d/external_services.conf
|
rm /etc/rspamd/local.d/external_services.conf
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
cat <<EOF > /etc/rspamd/local.d/external_services.conf
|
if [[ ! -f /etc/rspamd/local.d/external_services.conf ]]; then
|
||||||
|
cat <<EOF > /etc/rspamd/local.d/external_services.conf
|
||||||
oletools {
|
oletools {
|
||||||
# default olefy settings
|
# default olefy settings
|
||||||
servers = "olefy:10055";
|
servers = "olefy:10055";
|
||||||
@@ -100,6 +101,7 @@ oletools {
|
|||||||
retransmits = 1;
|
retransmits = 1;
|
||||||
}
|
}
|
||||||
EOF
|
EOF
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Provide additional lua modules
|
# Provide additional lua modules
|
||||||
|
|||||||
@@ -1,47 +1,167 @@
|
|||||||
FROM debian:bookworm-slim
|
# SOGo built from source to enable security patch application
|
||||||
|
# Repository: https://github.com/Alinto/sogo
|
||||||
|
# Version: SOGo-5.12.11
|
||||||
|
#
|
||||||
|
# Applied security patches:
|
||||||
|
# -
|
||||||
|
#
|
||||||
|
# To add new patches, modify SOGO_SECURITY_PATCHES ARG below with space-separated commit hashes
|
||||||
|
|
||||||
|
FROM debian:bookworm
|
||||||
|
|
||||||
LABEL maintainer="The Infrastructure Company GmbH <info@servercow.de>"
|
LABEL maintainer="The Infrastructure Company GmbH <info@servercow.de>"
|
||||||
|
|
||||||
ARG DEBIAN_FRONTEND=noninteractive
|
ARG DEBIAN_FRONTEND=noninteractive
|
||||||
ARG DEBIAN_VERSION=bookworm
|
ARG SOGO_VERSION=SOGo-5.12.11
|
||||||
ARG SOGO_DEBIAN_REPOSITORY=https://packagingv2.sogo.nu/sogo-nightly-debian/
|
ARG SOPE_VERSION=SOPE-5.12.11
|
||||||
|
# Security patches to apply (space-separated commit hashes)
|
||||||
|
ARG SOGO_SECURITY_PATCHES=""
|
||||||
# renovate: datasource=github-releases depName=tianon/gosu versioning=semver-coerced extractVersion=^(?<version>.*)$
|
# renovate: datasource=github-releases depName=tianon/gosu versioning=semver-coerced extractVersion=^(?<version>.*)$
|
||||||
ARG GOSU_VERSION=1.17
|
ARG GOSU_VERSION=1.19
|
||||||
ENV LC_ALL=C
|
ENV LC_ALL=C
|
||||||
|
|
||||||
# Prerequisites
|
# Install dependencies, build SOPE and SOGo, then clean up (all in one layer to minimize image size)
|
||||||
RUN echo "Building from repository $SOGO_DEBIAN_REPOSITORY" \
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
&& apt-get update && apt-get install -y --no-install-recommends \
|
# Build dependencies
|
||||||
apt-transport-https \
|
git \
|
||||||
ca-certificates \
|
build-essential \
|
||||||
gettext \
|
gobjc \
|
||||||
gnupg \
|
pkg-config \
|
||||||
mariadb-client \
|
gnustep-make \
|
||||||
rsync \
|
gnustep-base-runtime \
|
||||||
supervisor \
|
libgnustep-base-dev \
|
||||||
syslog-ng \
|
libxml2-dev \
|
||||||
syslog-ng-core \
|
libldap2-dev \
|
||||||
syslog-ng-mod-redis \
|
libssl-dev \
|
||||||
dirmngr \
|
zlib1g-dev \
|
||||||
netcat-traditional \
|
libpq-dev \
|
||||||
psmisc \
|
libmariadb-dev-compat \
|
||||||
wget \
|
libmemcached-dev \
|
||||||
patch \
|
libsodium-dev \
|
||||||
|
libcurl4-openssl-dev \
|
||||||
|
libzip-dev \
|
||||||
|
libytnef0-dev \
|
||||||
|
libwbxml2-dev \
|
||||||
|
curl \
|
||||||
|
ca-certificates \
|
||||||
|
# Runtime dependencies
|
||||||
|
apt-transport-https \
|
||||||
|
gettext \
|
||||||
|
gnupg \
|
||||||
|
mariadb-client \
|
||||||
|
rsync \
|
||||||
|
supervisor \
|
||||||
|
syslog-ng \
|
||||||
|
syslog-ng-core \
|
||||||
|
syslog-ng-mod-redis \
|
||||||
|
dirmngr \
|
||||||
|
netcat-traditional \
|
||||||
|
psmisc \
|
||||||
|
wget \
|
||||||
|
patch \
|
||||||
|
libobjc4 \
|
||||||
|
libxml2 \
|
||||||
|
libldap-2.5-0 \
|
||||||
|
libssl3 \
|
||||||
|
zlib1g \
|
||||||
|
libmariadb3 \
|
||||||
|
libmemcached11 \
|
||||||
|
libsodium23 \
|
||||||
|
libcurl4 \
|
||||||
|
libzip4 \
|
||||||
|
libytnef0 \
|
||||||
|
libwbxml2-1 \
|
||||||
|
# Download gosu
|
||||||
&& dpkgArch="$(dpkg --print-architecture | awk -F- '{ print $NF }')" \
|
&& dpkgArch="$(dpkg --print-architecture | awk -F- '{ print $NF }')" \
|
||||||
&& wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$dpkgArch" \
|
&& wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$dpkgArch" \
|
||||||
&& chmod +x /usr/local/bin/gosu \
|
&& chmod +x /usr/local/bin/gosu \
|
||||||
&& gosu nobody true \
|
&& gosu nobody true \
|
||||||
&& mkdir /usr/share/doc/sogo \
|
# Build SOPE
|
||||||
&& touch /usr/share/doc/sogo/empty.sh \
|
&& git clone --depth 1 --branch ${SOPE_VERSION} https://github.com/Alinto/sope.git /tmp/sope \
|
||||||
&& wget -O- https://keys.openpgp.org/vks/v1/by-fingerprint/74FFC6D72B925A34B5D356BDF8A27B36A6E2EAE9 | gpg --dearmor | apt-key add - \
|
&& cd /tmp/sope \
|
||||||
&& echo "deb [trusted=yes] ${SOGO_DEBIAN_REPOSITORY} ${DEBIAN_VERSION} main" > /etc/apt/sources.list.d/sogo.list \
|
&& rm -rf .git \
|
||||||
&& apt-get update && apt-get install -y --no-install-recommends \
|
&& . /usr/share/GNUstep/Makefiles/GNUstep.sh \
|
||||||
sogo \
|
&& ./configure --prefix=/usr --disable-debug --disable-strip \
|
||||||
sogo-activesync \
|
&& make -j$(nproc) \
|
||||||
&& apt-get autoclean \
|
&& make install \
|
||||||
|
&& cd / \
|
||||||
|
&& rm -rf /tmp/sope \
|
||||||
|
# Build SOGo with security patches
|
||||||
|
&& git clone --depth 1 --branch ${SOGO_VERSION} https://github.com/Alinto/sogo.git /tmp/sogo \
|
||||||
|
&& cd /tmp/sogo \
|
||||||
|
&& git config user.email "builder@mailcow.local" \
|
||||||
|
&& git config user.name "SOGo Builder" \
|
||||||
|
&& for patch in ${SOGO_SECURITY_PATCHES}; do \
|
||||||
|
echo "Applying security patch: ${patch}"; \
|
||||||
|
git fetch origin ${patch} && git cherry-pick ${patch}; \
|
||||||
|
done \
|
||||||
|
&& rm -rf .git \
|
||||||
|
&& . /usr/share/GNUstep/Makefiles/GNUstep.sh \
|
||||||
|
&& ./configure --disable-debug --disable-strip \
|
||||||
|
&& make -j$(nproc) \
|
||||||
|
&& make install \
|
||||||
|
&& cd /tmp/sogo/ActiveSync \
|
||||||
|
&& . /usr/share/GNUstep/Makefiles/GNUstep.sh \
|
||||||
|
&& make -j$(nproc) install \
|
||||||
|
&& cd / \
|
||||||
|
&& rm -rf /tmp/sogo \
|
||||||
|
# Strip binaries
|
||||||
|
&& strip --strip-unneeded /usr/local/sbin/sogod 2>/dev/null || true \
|
||||||
|
&& strip --strip-unneeded /usr/local/sbin/sogo-tool 2>/dev/null || true \
|
||||||
|
&& strip --strip-unneeded /usr/local/sbin/sogo-ealarms-notify 2>/dev/null || true \
|
||||||
|
&& strip --strip-unneeded /usr/local/sbin/sogo-slapd-sockd 2>/dev/null || true \
|
||||||
|
# Remove build dependencies and clean up
|
||||||
|
&& apt-get purge -y --auto-remove \
|
||||||
|
git \
|
||||||
|
build-essential \
|
||||||
|
gobjc \
|
||||||
|
gnustep-make \
|
||||||
|
libgnustep-base-dev \
|
||||||
|
libxml2-dev \
|
||||||
|
libldap2-dev \
|
||||||
|
libssl-dev \
|
||||||
|
zlib1g-dev \
|
||||||
|
libpq-dev \
|
||||||
|
libmariadb-dev-compat \
|
||||||
|
libmemcached-dev \
|
||||||
|
libsodium-dev \
|
||||||
|
libcurl4-openssl-dev \
|
||||||
|
libzip-dev \
|
||||||
|
libytnef0-dev \
|
||||||
|
curl \
|
||||||
|
&& apt-get autoremove -y \
|
||||||
|
&& apt-get clean \
|
||||||
&& rm -rf /var/lib/apt/lists/* \
|
&& rm -rf /var/lib/apt/lists/* \
|
||||||
|
&& rm -rf /usr/share/doc/* \
|
||||||
|
&& rm -rf /usr/share/man/* \
|
||||||
|
&& rm -rf /var/cache/debconf/* \
|
||||||
|
&& rm -rf /tmp/* \
|
||||||
|
&& rm -rf /root/.cache \
|
||||||
|
&& find /usr/local/lib -name '*.a' -delete \
|
||||||
|
&& find /usr/lib -name '*.a' -delete \
|
||||||
|
&& mkdir -p /usr/share/doc/sogo \
|
||||||
|
&& touch /usr/share/doc/sogo/empty.sh \
|
||||||
&& touch /etc/default/locale
|
&& touch /etc/default/locale
|
||||||
|
|
||||||
|
# Configure library paths
|
||||||
|
RUN echo "/usr/lib64" > /etc/ld.so.conf.d/sogo.conf \
|
||||||
|
&& echo "/usr/local/lib/sogo" >> /etc/ld.so.conf.d/sogo.conf \
|
||||||
|
&& echo "/usr/local/lib/GNUstep/Frameworks/SOGo.framework/Versions/5/sogo" >> /etc/ld.so.conf.d/sogo.conf \
|
||||||
|
&& ldconfig
|
||||||
|
|
||||||
|
# Create sogo user and group
|
||||||
|
RUN groupadd -r -g 999 sogo \
|
||||||
|
&& useradd -r -u 999 -g sogo -d /var/lib/sogo -s /bin/bash -c "SOGo Daemon" sogo \
|
||||||
|
&& mkdir -p /var/lib/sogo /var/run/sogo /var/log/sogo /var/spool/sogo \
|
||||||
|
&& chown -R sogo:sogo /var/lib/sogo /var/run/sogo /var/log/sogo /var/spool/sogo
|
||||||
|
|
||||||
|
# Create symlinks for SOGo binaries
|
||||||
|
RUN ln -s /usr/local/sbin/sogod /usr/sbin/sogod \
|
||||||
|
&& ln -s /usr/local/sbin/sogo-tool /usr/sbin/sogo-tool \
|
||||||
|
&& ln -s /usr/local/sbin/sogo-ealarms-notify /usr/sbin/sogo-ealarms-notify \
|
||||||
|
&& ln -s /usr/local/sbin/sogo-slapd-sockd /usr/sbin/sogo-slapd-sockd
|
||||||
|
|
||||||
|
# Copy configuration files and scripts
|
||||||
COPY ./bootstrap-sogo.sh /bootstrap-sogo.sh
|
COPY ./bootstrap-sogo.sh /bootstrap-sogo.sh
|
||||||
COPY syslog-ng.conf /etc/syslog-ng/syslog-ng.conf
|
COPY syslog-ng.conf /etc/syslog-ng/syslog-ng.conf
|
||||||
COPY syslog-ng-redis_slave.conf /etc/syslog-ng/syslog-ng-redis_slave.conf
|
COPY syslog-ng-redis_slave.conf /etc/syslog-ng/syslog-ng-redis_slave.conf
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
--- /usr/lib/GNUstep/SOGo/Templates/UIxAclEditor.wox 2018-08-17 18:29:57.987504204 +0200
|
--- /usr/local/lib/GNUstep/SOGo/Templates/UIxAclEditor.wox 2018-08-17 18:29:57.987504204 +0200
|
||||||
+++ /usr/lib/GNUstep/SOGo/Templates/UIxAclEditor.wox 2018-08-17 18:29:35.918291298 +0200
|
+++ /usr/local/lib/GNUstep/SOGo/Templates/UIxAclEditor.wox 2018-08-17 18:29:35.918291298 +0200
|
||||||
@@ -46,7 +46,7 @@
|
@@ -46,7 +46,7 @@
|
||||||
</md-item-template>
|
</md-item-template>
|
||||||
</md-autocomplete>
|
</md-autocomplete>
|
||||||
|
|||||||
@@ -24,6 +24,10 @@ while [[ "${DBV_NOW}" != "${DBV_NEW}" ]]; do
|
|||||||
done
|
done
|
||||||
echo "DB schema is ${DBV_NOW}"
|
echo "DB schema is ${DBV_NOW}"
|
||||||
|
|
||||||
|
if [[ "${MASTER}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
|
||||||
|
mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "DROP TRIGGER IF EXISTS sogo_update_password"
|
||||||
|
fi
|
||||||
|
|
||||||
# cat /dev/urandom seems to hang here occasionally and is not recommended anyway, better use openssl
|
# cat /dev/urandom seems to hang here occasionally and is not recommended anyway, better use openssl
|
||||||
RAND_PASS=$(openssl rand -base64 16 | tr -dc _A-Z-a-z-0-9)
|
RAND_PASS=$(openssl rand -base64 16 | tr -dc _A-Z-a-z-0-9)
|
||||||
|
|
||||||
@@ -126,18 +130,22 @@ chmod 600 /var/lib/sogo/GNUstep/Defaults/sogod.plist
|
|||||||
# Patch ACLs
|
# Patch ACLs
|
||||||
#if [[ ${ACL_ANYONE} == 'allow' ]]; then
|
#if [[ ${ACL_ANYONE} == 'allow' ]]; then
|
||||||
# #enable any or authenticated targets for ACL
|
# #enable any or authenticated targets for ACL
|
||||||
# if patch -R -sfN --dry-run /usr/lib/GNUstep/SOGo/Templates/UIxAclEditor.wox < /acl.diff > /dev/null; then
|
# if patch -R -sfN --dry-run /usr/local/lib/GNUstep/SOGo/Templates/UIxAclEditor.wox < /acl.diff > /dev/null; then
|
||||||
# patch -R /usr/lib/GNUstep/SOGo/Templates/UIxAclEditor.wox < /acl.diff;
|
# patch -R /usr/local/lib/GNUstep/SOGo/Templates/UIxAclEditor.wox < /acl.diff;
|
||||||
# fi
|
# fi
|
||||||
#else
|
#else
|
||||||
# #disable any or authenticated targets for ACL
|
# #disable any or authenticated targets for ACL
|
||||||
# if patch -sfN --dry-run /usr/lib/GNUstep/SOGo/Templates/UIxAclEditor.wox < /acl.diff > /dev/null; then
|
# if patch -sfN --dry-run /usr/local/lib/GNUstep/SOGo/Templates/UIxAclEditor.wox < /acl.diff > /dev/null; then
|
||||||
# patch /usr/lib/GNUstep/SOGo/Templates/UIxAclEditor.wox < /acl.diff;
|
# patch /usr/local/lib/GNUstep/SOGo/Templates/UIxAclEditor.wox < /acl.diff;
|
||||||
# fi
|
# fi
|
||||||
#fi
|
#fi
|
||||||
|
|
||||||
if patch -R -sfN --dry-run /usr/lib/GNUstep/SOGo/Templates/UIxTopnavToolbar.wox < /navMailcowBtns.diff > /dev/null; then
|
# Apply custom UI patch (reverse patch to ADD buttons)
|
||||||
patch -R /usr/lib/GNUstep/SOGo/Templates/UIxTopnavToolbar.wox < /navMailcowBtns.diff;
|
if patch -R -sfN --dry-run /usr/local/lib/GNUstep/SOGo/Templates/UIxTopnavToolbar.wox < /navMailcowBtns.diff > /dev/null; then
|
||||||
|
echo "Applying navMailcowBtns patch (reverse to add buttons)..."
|
||||||
|
patch -R /usr/local/lib/GNUstep/SOGo/Templates/UIxTopnavToolbar.wox < /navMailcowBtns.diff;
|
||||||
|
else
|
||||||
|
echo "navMailcowBtns patch already applied or cannot be applied"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Rename custom logo, if any
|
# Rename custom logo, if any
|
||||||
@@ -145,7 +153,7 @@ fi
|
|||||||
|
|
||||||
# Rsync web content
|
# Rsync web content
|
||||||
echo "Syncing web content with named volume"
|
echo "Syncing web content with named volume"
|
||||||
rsync -a /usr/lib/GNUstep/SOGo/. /sogo_web/
|
rsync -a /usr/local/lib/GNUstep/SOGo/. /sogo_web/
|
||||||
|
|
||||||
# Chown backup path
|
# Chown backup path
|
||||||
chown -R sogo:sogo /sogo_backup
|
chown -R sogo:sogo /sogo_backup
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
FROM alpine:3.21
|
FROM alpine:3.23
|
||||||
|
|
||||||
LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
|
LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
|
||||||
|
|
||||||
|
# install unbound from alpine:edge to get security patches
|
||||||
|
# minimum version is pinned
|
||||||
|
RUN apk add --no-cache --repository=https://dl-cdn.alpinelinux.org/alpine/edge/main "unbound>=1.26.1-r0"
|
||||||
|
|
||||||
|
# install other packages from regular alpine stable repo
|
||||||
RUN apk add --update --no-cache \
|
RUN apk add --update --no-cache \
|
||||||
curl \
|
curl \
|
||||||
bind-tools \
|
bind-tools \
|
||||||
coreutils \
|
coreutils \
|
||||||
unbound \
|
|
||||||
bash \
|
bash \
|
||||||
openssl \
|
openssl \
|
||||||
drill \
|
drill \
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM alpine:3.21
|
FROM alpine:3.23
|
||||||
|
|
||||||
LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
|
LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
|
||||||
|
|
||||||
@@ -16,7 +16,6 @@ RUN apk add --update \
|
|||||||
fcgi \
|
fcgi \
|
||||||
openssl \
|
openssl \
|
||||||
nagios-plugins-mysql \
|
nagios-plugins-mysql \
|
||||||
nagios-plugins-dns \
|
|
||||||
nagios-plugins-disk \
|
nagios-plugins-disk \
|
||||||
bind-tools \
|
bind-tools \
|
||||||
redis \
|
redis \
|
||||||
@@ -32,9 +31,12 @@ RUN apk add --update \
|
|||||||
tzdata \
|
tzdata \
|
||||||
whois \
|
whois \
|
||||||
&& curl https://raw.githubusercontent.com/mludvig/smtp-cli/v3.10/smtp-cli -o /smtp-cli \
|
&& curl https://raw.githubusercontent.com/mludvig/smtp-cli/v3.10/smtp-cli -o /smtp-cli \
|
||||||
&& chmod +x smtp-cli
|
&& chmod +x smtp-cli \
|
||||||
|
&& mkdir /usr/lib/mailcow
|
||||||
|
|
||||||
COPY watchdog.sh /watchdog.sh
|
COPY watchdog.sh /watchdog.sh
|
||||||
COPY check_mysql_slavestatus.sh /usr/lib/nagios/plugins/check_mysql_slavestatus.sh
|
COPY check_mysql_slavestatus.sh /usr/lib/nagios/plugins/check_mysql_slavestatus.sh
|
||||||
|
COPY check_dns.sh /usr/lib/mailcow/check_dns.sh
|
||||||
|
COPY client.cnf /etc/my.cnf.d/client.cnf
|
||||||
|
|
||||||
CMD ["/watchdog.sh"]
|
CMD ["/watchdog.sh"]
|
||||||
Executable
+39
@@ -0,0 +1,39 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
while getopts "H:s:" opt; do
|
||||||
|
case "$opt" in
|
||||||
|
H) HOST="$OPTARG" ;;
|
||||||
|
s) SERVER="$OPTARG" ;;
|
||||||
|
*) echo "Usage: $0 -H host -s server"; exit 3 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ -z "$SERVER" ]; then
|
||||||
|
echo "No DNS Server provided"
|
||||||
|
exit 3
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$HOST" ]; then
|
||||||
|
echo "No host to test provided"
|
||||||
|
exit 3
|
||||||
|
fi
|
||||||
|
|
||||||
|
# run dig and measure the time it takes to run
|
||||||
|
START_TIME=$(perl -MTime::HiRes -e 'print Time::HiRes::time')
|
||||||
|
dig_output=$(dig +short +timeout=2 +tries=1 "$HOST" @"$SERVER" 2>/dev/null)
|
||||||
|
dig_rc=$?
|
||||||
|
END_TIME=$(perl -MTime::HiRes -e 'print Time::HiRes::time')
|
||||||
|
dig_output_ips=$(echo "$dig_output" | grep -E '^[0-9.]+$' | sort | paste -sd ',' -)
|
||||||
|
ELAPSED_TIME=$(perl -e "printf('%.3f', $END_TIME - $START_TIME)")
|
||||||
|
|
||||||
|
# validate and perform nagios like output and exit codes
|
||||||
|
if [ $dig_rc -ne 0 ] || [ -z "$dig_output" ]; then
|
||||||
|
echo "Domain $HOST was not found by the server"
|
||||||
|
exit 2
|
||||||
|
elif [ $dig_rc -eq 0 ]; then
|
||||||
|
echo "DNS OK: $ELAPSED_TIME seconds response time. $HOST returns $dig_output_ips"
|
||||||
|
exit 0
|
||||||
|
else
|
||||||
|
echo "Unknown error"
|
||||||
|
exit 3
|
||||||
|
fi
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
[client]
|
||||||
|
ssl = false
|
||||||
|
ssl-verify-server-cert = false
|
||||||
@@ -1,5 +1,10 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
|
if [ "${DEV_MODE}" != "n" ]; then
|
||||||
|
echo -e "\e[31mEnabled Debug Mode\e[0m"
|
||||||
|
set -x
|
||||||
|
fi
|
||||||
|
|
||||||
trap "exit" INT TERM
|
trap "exit" INT TERM
|
||||||
trap "kill 0" EXIT
|
trap "kill 0" EXIT
|
||||||
|
|
||||||
@@ -33,7 +38,7 @@ if [[ ! -p /tmp/com_pipe ]]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# Wait for containers
|
# Wait for containers
|
||||||
while ! mariadb-admin status --ssl=false --socket=/var/run/mysqld/mysqld.sock -u${DBUSER} -p${DBPASS} --silent; do
|
while ! mariadb-admin status --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u${DBUSER} -p${DBPASS} --silent; do
|
||||||
echo "Waiting for SQL..."
|
echo "Waiting for SQL..."
|
||||||
sleep 2
|
sleep 2
|
||||||
done
|
done
|
||||||
@@ -297,7 +302,7 @@ unbound_checks() {
|
|||||||
touch /tmp/unbound-mailcow; echo "$(tail -50 /tmp/unbound-mailcow)" > /tmp/unbound-mailcow
|
touch /tmp/unbound-mailcow; echo "$(tail -50 /tmp/unbound-mailcow)" > /tmp/unbound-mailcow
|
||||||
host_ip=$(get_container_ip unbound-mailcow)
|
host_ip=$(get_container_ip unbound-mailcow)
|
||||||
err_c_cur=${err_count}
|
err_c_cur=${err_count}
|
||||||
/usr/lib/nagios/plugins/check_dns -s ${host_ip} -H stackoverflow.com 2>> /tmp/unbound-mailcow 1>&2; err_count=$(( ${err_count} + $? ))
|
/usr/lib/mailcow/check_dns.sh -s ${host_ip} -H stackoverflow.com 2>> /tmp/unbound-mailcow 1>&2; err_count=$(( ${err_count} + $? ))
|
||||||
DNSSEC=$(dig com +dnssec | egrep 'flags:.+ad')
|
DNSSEC=$(dig com +dnssec | egrep 'flags:.+ad')
|
||||||
if [[ -z ${DNSSEC} ]]; then
|
if [[ -z ${DNSSEC} ]]; then
|
||||||
echo "DNSSEC failure" 2>> /tmp/unbound-mailcow 1>&2
|
echo "DNSSEC failure" 2>> /tmp/unbound-mailcow 1>&2
|
||||||
@@ -354,8 +359,8 @@ mysql_checks() {
|
|||||||
while [ ${err_count} -lt ${THRESHOLD} ]; do
|
while [ ${err_count} -lt ${THRESHOLD} ]; do
|
||||||
touch /tmp/mysql-mailcow; echo "$(tail -50 /tmp/mysql-mailcow)" > /tmp/mysql-mailcow
|
touch /tmp/mysql-mailcow; echo "$(tail -50 /tmp/mysql-mailcow)" > /tmp/mysql-mailcow
|
||||||
err_c_cur=${err_count}
|
err_c_cur=${err_count}
|
||||||
/usr/lib/nagios/plugins/check_mysql -s /var/run/mysqld/mysqld.sock -u ${DBUSER} -p ${DBPASS} -d ${DBNAME} 2>> /tmp/mysql-mailcow 1>&2; err_count=$(( ${err_count} + $? ))
|
/usr/lib/nagios/plugins/check_mysql -f /etc/my.cnf.d/client.cnf -s /var/run/mysqld/mysqld.sock -u ${DBUSER} -p ${DBPASS} -d ${DBNAME} 2>> /tmp/mysql-mailcow 1>&2; err_count=$(( ${err_count} + $? ))
|
||||||
/usr/lib/nagios/plugins/check_mysql_query -s /var/run/mysqld/mysqld.sock -u ${DBUSER} -p ${DBPASS} -d ${DBNAME} -q "SELECT COUNT(*) FROM information_schema.tables" 2>> /tmp/mysql-mailcow 1>&2; err_count=$(( ${err_count} + $? ))
|
/usr/lib/nagios/plugins/check_mysql_query -f /etc/my.cnf.d/client.cnf -s /var/run/mysqld/mysqld.sock -u ${DBUSER} -p ${DBPASS} -d ${DBNAME} -q "SELECT COUNT(*) FROM information_schema.tables" 2>> /tmp/mysql-mailcow 1>&2; err_count=$(( ${err_count} + $? ))
|
||||||
[ ${err_c_cur} -eq ${err_count} ] && [ ! $((${err_count} - 1)) -lt 0 ] && err_count=$((${err_count} - 1)) diff_c=1
|
[ ${err_c_cur} -eq ${err_count} ] && [ ! $((${err_count} - 1)) -lt 0 ] && err_count=$((${err_count} - 1)) diff_c=1
|
||||||
[ ${err_c_cur} -ne ${err_count} ] && diff_c=$(( ${err_c_cur} - ${err_count} ))
|
[ ${err_c_cur} -ne ${err_count} ] && diff_c=$(( ${err_c_cur} - ${err_count} ))
|
||||||
progress "MySQL/MariaDB" ${THRESHOLD} $(( ${THRESHOLD} - ${err_count} )) ${diff_c}
|
progress "MySQL/MariaDB" ${THRESHOLD} $(( ${THRESHOLD} - ${err_count} )) ${diff_c}
|
||||||
@@ -379,7 +384,7 @@ mysql_repl_checks() {
|
|||||||
while [ ${err_count} -lt ${THRESHOLD} ]; do
|
while [ ${err_count} -lt ${THRESHOLD} ]; do
|
||||||
touch /tmp/mysql_repl_checks; echo "$(tail -50 /tmp/mysql_repl_checks)" > /tmp/mysql_repl_checks
|
touch /tmp/mysql_repl_checks; echo "$(tail -50 /tmp/mysql_repl_checks)" > /tmp/mysql_repl_checks
|
||||||
err_c_cur=${err_count}
|
err_c_cur=${err_count}
|
||||||
/usr/lib/nagios/plugins/check_mysql_slavestatus.sh -S /var/run/mysqld/mysqld.sock -u root -p ${DBROOT} 2>> /tmp/mysql_repl_checks 1>&2; err_count=$(( ${err_count} + $? ))
|
/usr/lib/nagios/plugins/check_mysql_slavestatus.sh -o /etc/my.cnf.d/client.cnf -S /var/run/mysqld/mysqld.sock -u root -p ${DBROOT} 2>> /tmp/mysql_repl_checks 1>&2; err_count=$(( ${err_count} + $? ))
|
||||||
[ ${err_c_cur} -eq ${err_count} ] && [ ! $((${err_count} - 1)) -lt 0 ] && err_count=$((${err_count} - 1)) diff_c=1
|
[ ${err_c_cur} -eq ${err_count} ] && [ ! $((${err_count} - 1)) -lt 0 ] && err_count=$((${err_count} - 1)) diff_c=1
|
||||||
[ ${err_c_cur} -ne ${err_count} ] && diff_c=$(( ${err_c_cur} - ${err_count} ))
|
[ ${err_c_cur} -ne ${err_count} ] && diff_c=$(( ${err_c_cur} - ${err_count} ))
|
||||||
progress "MySQL/MariaDB replication" ${THRESHOLD} $(( ${THRESHOLD} - ${err_count} )) ${diff_c}
|
progress "MySQL/MariaDB replication" ${THRESHOLD} $(( ${THRESHOLD} - ${err_count} )) ${diff_c}
|
||||||
@@ -445,6 +450,31 @@ postfix_checks() {
|
|||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
postfix-tlspol_checks() {
|
||||||
|
err_count=0
|
||||||
|
diff_c=0
|
||||||
|
THRESHOLD=${POSTFIX_TLSPOL_THRESHOLD}
|
||||||
|
# Reduce error count by 2 after restarting an unhealthy container
|
||||||
|
trap "[ ${err_count} -gt 1 ] && err_count=$(( ${err_count} - 2 ))" USR1
|
||||||
|
while [ ${err_count} -lt ${THRESHOLD} ]; do
|
||||||
|
touch /tmp/postfix-tlspol-mailcow; echo "$(tail -50 /tmp/postfix-tlspol-mailcow)" > /tmp/postfix-tlspol-mailcow
|
||||||
|
host_ip=$(get_container_ip postfix-tlspol-mailcow)
|
||||||
|
err_c_cur=${err_count}
|
||||||
|
/usr/lib/nagios/plugins/check_tcp -4 -H ${host_ip} -p 8642 2>> /tmp/postfix-tlspol-mailcow 1>&2; err_count=$(( ${err_count} + $? ))
|
||||||
|
[ ${err_c_cur} -eq ${err_count} ] && [ ! $((${err_count} - 1)) -lt 0 ] && err_count=$((${err_count} - 1)) diff_c=1
|
||||||
|
[ ${err_c_cur} -ne ${err_count} ] && diff_c=$(( ${err_c_cur} - ${err_count} ))
|
||||||
|
progress "Postfix TLS Policy companion" ${THRESHOLD} $(( ${THRESHOLD} - ${err_count} )) ${diff_c}
|
||||||
|
if [[ $? == 10 ]]; then
|
||||||
|
diff_c=0
|
||||||
|
sleep 1
|
||||||
|
else
|
||||||
|
diff_c=0
|
||||||
|
sleep $(( ( RANDOM % 60 ) + 20 ))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
clamd_checks() {
|
clamd_checks() {
|
||||||
err_count=0
|
err_count=0
|
||||||
diff_c=0
|
diff_c=0
|
||||||
@@ -922,6 +952,18 @@ PID=$!
|
|||||||
echo "Spawned mailq_checks with PID ${PID}"
|
echo "Spawned mailq_checks with PID ${PID}"
|
||||||
BACKGROUND_TASKS+=(${PID})
|
BACKGROUND_TASKS+=(${PID})
|
||||||
|
|
||||||
|
(
|
||||||
|
while true; do
|
||||||
|
if ! postfix-tlspol_checks; then
|
||||||
|
log_msg "Postfix TLS Policy hit error limit"
|
||||||
|
echo postfix-tlspol-mailcow > /tmp/com_pipe
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
) &
|
||||||
|
PID=$!
|
||||||
|
echo "Spawned postfix-tlspol_checks with PID ${PID}"
|
||||||
|
BACKGROUND_TASKS+=(${PID})
|
||||||
|
|
||||||
(
|
(
|
||||||
while true; do
|
while true; do
|
||||||
if ! dovecot_checks; then
|
if ! dovecot_checks; then
|
||||||
|
|||||||
File renamed without changes.
@@ -80,23 +80,30 @@ if ($isSOGoRequest) {
|
|||||||
}
|
}
|
||||||
if ($result === false){
|
if ($result === false){
|
||||||
// If it's a SOGo Request, don't check for protocol access
|
// If it's a SOGo Request, don't check for protocol access
|
||||||
$service = ($isSOGoRequest) ? false : array($post['service'] => true);
|
if ($isSOGoRequest) {
|
||||||
$result = apppass_login($post['username'], $post['password'], $service, array(
|
$service = 'SOGO';
|
||||||
|
$post['service'] = 'NONE';
|
||||||
|
} else {
|
||||||
|
$service = $post['service'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = apppass_login($post['username'], $post['password'], array(
|
||||||
|
'service' => $post['service'],
|
||||||
'is_internal' => true,
|
'is_internal' => true,
|
||||||
'remote_addr' => $post['real_rip']
|
'remote_addr' => $post['real_rip']
|
||||||
));
|
));
|
||||||
if ($result) {
|
if ($result) {
|
||||||
error_log('MAILCOWAUTH: App auth for user ' . $post['username']);
|
error_log('MAILCOWAUTH: App auth for user ' . $post['username'] . " with service " . $service . " from IP " . $post['real_rip']);
|
||||||
set_sasl_log($post['username'], $post['real_rip'], $post['service']);
|
set_sasl_log($post['username'], $post['real_rip'], $service);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ($result === false){
|
if ($result === false){
|
||||||
// Init Identity Provider
|
// Init Identity Provider
|
||||||
$iam_provider = identity_provider('init');
|
$iam_provider = identity_provider('init');
|
||||||
$iam_settings = identity_provider('get');
|
$iam_settings = identity_provider('get');
|
||||||
$result = user_login($post['username'], $post['password'], array('is_internal' => true));
|
$result = user_login($post['username'], $post['password'], array('is_internal' => true, 'service' => $post['service']));
|
||||||
if ($result) {
|
if ($result) {
|
||||||
error_log('MAILCOWAUTH: User auth for user ' . $post['username']);
|
error_log('MAILCOWAUTH: User auth for user ' . $post['username'] . " with service " . $post['service'] . " from IP " . $post['real_rip']);
|
||||||
set_sasl_log($post['username'], $post['real_rip'], $post['service']);
|
set_sasl_log($post['username'], $post['real_rip'], $post['service']);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -105,7 +112,7 @@ if ($result) {
|
|||||||
http_response_code(200); // OK
|
http_response_code(200); // OK
|
||||||
$return['success'] = true;
|
$return['success'] = true;
|
||||||
} else {
|
} else {
|
||||||
error_log("MAILCOWAUTH: Login failed for user " . $post['username']);
|
error_log("MAILCOWAUTH: Login failed for user " . $post['username'] . " with service " . $post['service'] . " from IP " . $post['real_rip']);
|
||||||
http_response_code(401); // Unauthorized
|
http_response_code(401); // Unauthorized
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,8 +32,17 @@ function auth_password_verify(request, password)
|
|||||||
-- Returning PASSDB_RESULT_PASSWORD_MISMATCH will reset the user's auth cache entry.
|
-- Returning PASSDB_RESULT_PASSWORD_MISMATCH will reset the user's auth cache entry.
|
||||||
-- Returning PASSDB_RESULT_INTERNAL_FAILURE keeps the existing cache entry,
|
-- Returning PASSDB_RESULT_INTERNAL_FAILURE keeps the existing cache entry,
|
||||||
-- even if the TTL has expired. Useful to avoid cache eviction during backend issues.
|
-- even if the TTL has expired. Useful to avoid cache eviction during backend issues.
|
||||||
|
|
||||||
|
-- On a network-level failure (nginx unreachable, DNS failure, timeout) https.request
|
||||||
|
-- returns nil plus an error string, so c is not a numeric HTTP status code. Treat this
|
||||||
|
-- as a backend outage and keep the cache entry, rather than wiping it as a mismatch.
|
||||||
|
if type(c) ~= "number" then
|
||||||
|
dovecot.i_info("HTTP request to auth backend failed with " .. tostring(c) .. " for user " .. request.user)
|
||||||
|
return dovecot.auth.PASSDB_RESULT_INTERNAL_FAILURE, "Upstream unreachable"
|
||||||
|
end
|
||||||
|
|
||||||
if c ~= 200 and c ~= 401 then
|
if c ~= 200 and c ~= 401 then
|
||||||
dovecot.i_info("HTTP request failed with " .. c .. " for user " .. request.user)
|
dovecot.i_info("HTTP request failed with " .. tostring(c) .. " for user " .. request.user)
|
||||||
return dovecot.auth.PASSDB_RESULT_PASSWORD_MISMATCH, "Upstream error"
|
return dovecot.auth.PASSDB_RESULT_PASSWORD_MISMATCH, "Upstream error"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,3 @@
|
|||||||
# global_sieve_before script
|
# global_sieve_before script
|
||||||
# global_sieve_before -> user sieve_before (mailcow UI) -> user sieve_after (mailcow UI) -> global_sieve_after
|
# global_sieve_before -> user sieve_before (mailcow UI) -> user sieve_after (mailcow UI) -> global_sieve_after
|
||||||
|
|
||||||
require ["mailbox", "fileinto"];
|
|
||||||
|
|
||||||
if header :contains ["Chat-Version"] [""] {
|
|
||||||
if mailboxexists "DeltaChat" {
|
|
||||||
fileinto "DeltaChat";
|
|
||||||
} else {
|
|
||||||
fileinto :create "DeltaChat";
|
|
||||||
}
|
|
||||||
stop;
|
|
||||||
}
|
|
||||||
@@ -13,6 +13,7 @@ events {
|
|||||||
http {
|
http {
|
||||||
include /etc/nginx/mime.types;
|
include /etc/nginx/mime.types;
|
||||||
default_type application/octet-stream;
|
default_type application/octet-stream;
|
||||||
|
server_tokens off;
|
||||||
|
|
||||||
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||||||
'$status $body_bytes_sent "$http_referer" '
|
'$status $body_bytes_sent "$http_referer" '
|
||||||
@@ -41,20 +42,35 @@ http {
|
|||||||
https https;
|
https https;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
map $server_port $sogo_auth_internal {
|
||||||
|
65510 "1";
|
||||||
|
default "";
|
||||||
|
}
|
||||||
|
|
||||||
{% if HTTP_REDIRECT %}
|
{% if HTTP_REDIRECT %}
|
||||||
# HTTP to HTTPS redirect
|
# HTTP to HTTPS redirect
|
||||||
server {
|
server {
|
||||||
root /web;
|
root /web;
|
||||||
listen {{ HTTP_PORT }} default_server;
|
listen {{ HTTP_PORT }} default_server;
|
||||||
|
{% if ENABLE_IPV6 %}
|
||||||
listen [::]:{{ HTTP_PORT }} default_server;
|
listen [::]:{{ HTTP_PORT }} default_server;
|
||||||
|
{%endif%}
|
||||||
|
|
||||||
server_name {{ MAILCOW_HOSTNAME }} autodiscover.* autoconfig.* {{ ADDITIONAL_SERVER_NAMES | join(' ') }};
|
server_name {{ MAILCOW_HOSTNAME }} autodiscover.* autoconfig.* mta-sts.* {{ ADDITIONAL_SERVER_NAMES | join(' ') }};
|
||||||
|
|
||||||
if ( $request_uri ~* "%0A|%0D" ) { return 403; }
|
if ( $request_uri ~* "%0A|%0D" ) { return 403; }
|
||||||
location ^~ /.well-known/acme-challenge/ {
|
location ^~ /.well-known/acme-challenge/ {
|
||||||
allow all;
|
allow all;
|
||||||
default_type "text/plain";
|
default_type "text/plain";
|
||||||
}
|
}
|
||||||
|
location ^~ /.well-known/mta-sts.txt {
|
||||||
|
allow all;
|
||||||
|
fastcgi_split_path_info ^(.+\.php)(/.+)$;
|
||||||
|
fastcgi_pass {{ PHPFPMHOST }}:9002;
|
||||||
|
include /etc/nginx/fastcgi_params;
|
||||||
|
fastcgi_param SCRIPT_FILENAME $document_root/mta-sts.php;
|
||||||
|
fastcgi_param PATH_INFO $fastcgi_path_info;
|
||||||
|
}
|
||||||
location / {
|
location / {
|
||||||
return 301 https://$host$uri$is_args$args;
|
return 301 https://$host$uri$is_args$args;
|
||||||
}
|
}
|
||||||
@@ -70,7 +86,7 @@ http {
|
|||||||
{%endif%}
|
{%endif%}
|
||||||
listen {{ HTTPS_PORT }}{% if NGINX_USE_PROXY_PROTOCOL %} proxy_protocol{%endif%} ssl;
|
listen {{ HTTPS_PORT }}{% if NGINX_USE_PROXY_PROTOCOL %} proxy_protocol{%endif%} ssl;
|
||||||
|
|
||||||
{% if not DISABLE_IPv6 %}
|
{% if ENABLE_IPV6 %}
|
||||||
{% if not HTTP_REDIRECT %}
|
{% if not HTTP_REDIRECT %}
|
||||||
listen [::]:{{ HTTP_PORT }}{% if NGINX_USE_PROXY_PROTOCOL %} proxy_protocol{%endif%};
|
listen [::]:{{ HTTP_PORT }}{% if NGINX_USE_PROXY_PROTOCOL %} proxy_protocol{%endif%};
|
||||||
{%endif%}
|
{%endif%}
|
||||||
@@ -82,7 +98,7 @@ http {
|
|||||||
ssl_certificate /etc/ssl/mail/cert.pem;
|
ssl_certificate /etc/ssl/mail/cert.pem;
|
||||||
ssl_certificate_key /etc/ssl/mail/key.pem;
|
ssl_certificate_key /etc/ssl/mail/key.pem;
|
||||||
|
|
||||||
server_name {{ MAILCOW_HOSTNAME }} autodiscover.* autoconfig.*;
|
server_name {{ MAILCOW_HOSTNAME }} autodiscover.* autoconfig.* mta-sts.*;
|
||||||
|
|
||||||
include /etc/nginx/includes/sites-default.conf;
|
include /etc/nginx/includes/sites-default.conf;
|
||||||
}
|
}
|
||||||
@@ -97,7 +113,7 @@ http {
|
|||||||
{%endif%}
|
{%endif%}
|
||||||
listen {{ HTTPS_PORT }}{% if NGINX_USE_PROXY_PROTOCOL %} proxy_protocol{%endif%} ssl;
|
listen {{ HTTPS_PORT }}{% if NGINX_USE_PROXY_PROTOCOL %} proxy_protocol{%endif%} ssl;
|
||||||
|
|
||||||
{% if not DISABLE_IPv6 %}
|
{% if ENABLE_IPV6 %}
|
||||||
{% if not HTTP_REDIRECT %}
|
{% if not HTTP_REDIRECT %}
|
||||||
listen [::]:{{ HTTP_PORT }}{% if NGINX_USE_PROXY_PROTOCOL %} proxy_protocol{%endif%};
|
listen [::]:{{ HTTP_PORT }}{% if NGINX_USE_PROXY_PROTOCOL %} proxy_protocol{%endif%};
|
||||||
{%endif%}
|
{%endif%}
|
||||||
@@ -118,7 +134,7 @@ http {
|
|||||||
# rspamd dynmaps:
|
# rspamd dynmaps:
|
||||||
server {
|
server {
|
||||||
listen 8081;
|
listen 8081;
|
||||||
{% if not DISABLE_IPv6 %}
|
{% if ENABLE_IPV6 %}
|
||||||
listen [::]:8081;
|
listen [::]:8081;
|
||||||
{%endif%}
|
{%endif%}
|
||||||
index index.php index.html;
|
index index.php index.html;
|
||||||
@@ -191,7 +207,7 @@ http {
|
|||||||
{%endif%}
|
{%endif%}
|
||||||
listen {{ HTTPS_PORT }}{% if NGINX_USE_PROXY_PROTOCOL %} proxy_protocol{%endif%} ssl;
|
listen {{ HTTPS_PORT }}{% if NGINX_USE_PROXY_PROTOCOL %} proxy_protocol{%endif%} ssl;
|
||||||
|
|
||||||
{% if not DISABLE_IPv6 %}
|
{% if ENABLE_IPV6 %}
|
||||||
{% if not HTTP_REDIRECT %}
|
{% if not HTTP_REDIRECT %}
|
||||||
listen [::]:{{ HTTP_PORT }}{% if NGINX_USE_PROXY_PROTOCOL %} proxy_protocol{%endif%};
|
listen [::]:{{ HTTP_PORT }}{% if NGINX_USE_PROXY_PROTOCOL %} proxy_protocol{%endif%};
|
||||||
{%endif%}
|
{%endif%}
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ ssl_session_tickets off;
|
|||||||
|
|
||||||
add_header Strict-Transport-Security "max-age=15768000;";
|
add_header Strict-Transport-Security "max-age=15768000;";
|
||||||
add_header X-Content-Type-Options nosniff;
|
add_header X-Content-Type-Options nosniff;
|
||||||
add_header X-XSS-Protection "1; mode=block";
|
|
||||||
add_header X-Robots-Tag none;
|
add_header X-Robots-Tag none;
|
||||||
add_header X-Download-Options noopen;
|
add_header X-Download-Options noopen;
|
||||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||||
@@ -76,6 +75,14 @@ location ^~ /.well-known/acme-challenge/ {
|
|||||||
allow all;
|
allow all;
|
||||||
default_type "text/plain";
|
default_type "text/plain";
|
||||||
}
|
}
|
||||||
|
location ^~ /.well-known/mta-sts.txt {
|
||||||
|
allow all;
|
||||||
|
fastcgi_split_path_info ^(.+\.php)(/.+)$;
|
||||||
|
fastcgi_pass {{ PHPFPMHOST }}:9002;
|
||||||
|
include /etc/nginx/fastcgi_params;
|
||||||
|
fastcgi_param SCRIPT_FILENAME $document_root/mta-sts.php;
|
||||||
|
fastcgi_param PATH_INFO $fastcgi_path_info;
|
||||||
|
}
|
||||||
|
|
||||||
rewrite ^/.well-known/caldav$ /SOGo/dav/ permanent;
|
rewrite ^/.well-known/caldav$ /SOGo/dav/ permanent;
|
||||||
rewrite ^/.well-known/carddav$ /SOGo/dav/ permanent;
|
rewrite ^/.well-known/carddav$ /SOGo/dav/ permanent;
|
||||||
@@ -109,6 +116,9 @@ location ~ \.php$ {
|
|||||||
include /etc/nginx/fastcgi_params;
|
include /etc/nginx/fastcgi_params;
|
||||||
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||||
fastcgi_param PATH_INFO $fastcgi_path_info;
|
fastcgi_param PATH_INFO $fastcgi_path_info;
|
||||||
|
fastcgi_param HTTP_X_REAL_IP $remote_addr;
|
||||||
|
# trusted internal-auth marker; empty for external clients (see nginx.conf map)
|
||||||
|
fastcgi_param SOGO_AUTH_INTERNAL $sogo_auth_internal;
|
||||||
fastcgi_read_timeout 3600;
|
fastcgi_read_timeout 3600;
|
||||||
fastcgi_send_timeout 3600;
|
fastcgi_send_timeout 3600;
|
||||||
}
|
}
|
||||||
@@ -254,19 +264,19 @@ location ~* /sogo$ {
|
|||||||
}
|
}
|
||||||
|
|
||||||
location /SOGo.woa/WebServerResources/ {
|
location /SOGo.woa/WebServerResources/ {
|
||||||
alias /usr/lib/GNUstep/SOGo/WebServerResources/;
|
alias /usr/local/lib/GNUstep/SOGo/WebServerResources/;
|
||||||
}
|
}
|
||||||
|
|
||||||
location /.woa/WebServerResources/ {
|
location /.woa/WebServerResources/ {
|
||||||
alias /usr/lib/GNUstep/SOGo/WebServerResources/;
|
alias /usr/local/lib/GNUstep/SOGo/WebServerResources/;
|
||||||
}
|
}
|
||||||
|
|
||||||
location /SOGo/WebServerResources/ {
|
location /SOGo/WebServerResources/ {
|
||||||
alias /usr/lib/GNUstep/SOGo/WebServerResources/;
|
alias /usr/local/lib/GNUstep/SOGo/WebServerResources/;
|
||||||
}
|
}
|
||||||
|
|
||||||
location (^/SOGo/so/ControlPanel/Products/[^/]*UI/Resources/.*\.(jpg|png|gif|css|js)$) {
|
location (^/SOGo/so/ControlPanel/Products/[^/]*UI/Resources/.*\.(jpg|png|gif|css|js)$) {
|
||||||
alias /usr/lib/GNUstep/SOGo/$1.SOGo/Resources/$2;
|
alias /usr/local/lib/GNUstep/SOGo/$1.SOGo/Resources/$2;
|
||||||
}
|
}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,16 @@
|
|||||||
|
; NOTE: Restart phpfpm on ANY manual changes to PHP files!
|
||||||
|
|
||||||
|
; opcache
|
||||||
opcache.enable=1
|
opcache.enable=1
|
||||||
opcache.enable_cli=1
|
opcache.enable_cli=1
|
||||||
opcache.interned_strings_buffer=16
|
opcache.interned_strings_buffer=16
|
||||||
opcache.max_accelerated_files=10000
|
opcache.max_accelerated_files=10000
|
||||||
opcache.memory_consumption=128
|
opcache.memory_consumption=128
|
||||||
opcache.save_comments=1
|
opcache.save_comments=1
|
||||||
opcache.revalidate_freq=1
|
opcache.validate_timestamps=0
|
||||||
|
|
||||||
|
; JIT
|
||||||
|
; Disabled for now due to some PHP segmentation faults observed
|
||||||
|
; in certain environments. Possibly some PHP or PHP extension bug.
|
||||||
|
opcache.jit=disable
|
||||||
|
opcache.jit_buffer_size=0
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
max_execution_time = 3600
|
max_execution_time = 3600
|
||||||
max_input_time = 3600
|
max_input_time = 3600
|
||||||
memory_limit = 512M
|
memory_limit = 512M
|
||||||
|
; not used by mailcow, disabled as hardening (PHP security release 2026-10-21)
|
||||||
|
session.upload_progress.enabled = Off
|
||||||
@@ -37,7 +37,7 @@ postscreen_access_list = permit_mynetworks,
|
|||||||
cidr:/opt/postfix/conf/postscreen_access.cidr,
|
cidr:/opt/postfix/conf/postscreen_access.cidr,
|
||||||
tcp:127.0.0.1:10027
|
tcp:127.0.0.1:10027
|
||||||
postscreen_bare_newline_enable = no
|
postscreen_bare_newline_enable = no
|
||||||
postscreen_blacklist_action = drop
|
postscreen_denylist_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
|
||||||
@@ -107,8 +107,6 @@ smtpd_sender_restrictions = reject_authenticated_sender_login_mismatch,
|
|||||||
reject_unknown_sender_domain
|
reject_unknown_sender_domain
|
||||||
smtpd_soft_error_limit = 3
|
smtpd_soft_error_limit = 3
|
||||||
smtpd_tls_auth_only = yes
|
smtpd_tls_auth_only = yes
|
||||||
smtpd_tls_dh1024_param_file = /etc/ssl/mail/dhparams.pem
|
|
||||||
smtpd_tls_eecdh_grade = auto
|
|
||||||
smtpd_tls_exclude_ciphers = ECDHE-RSA-RC4-SHA, RC4, aNULL, DES-CBC3-SHA, ECDHE-RSA-DES-CBC3-SHA, EDH-RSA-DES-CBC3-SHA
|
smtpd_tls_exclude_ciphers = ECDHE-RSA-RC4-SHA, RC4, aNULL, DES-CBC3-SHA, ECDHE-RSA-DES-CBC3-SHA, EDH-RSA-DES-CBC3-SHA
|
||||||
smtpd_tls_loglevel = 1
|
smtpd_tls_loglevel = 1
|
||||||
|
|
||||||
@@ -152,7 +150,7 @@ smtp_sasl_auth_enable = yes
|
|||||||
smtp_sasl_password_maps = proxy:mysql:/opt/postfix/conf/sql/mysql_sasl_passwd_maps_sender_dependent.cf
|
smtp_sasl_password_maps = proxy:mysql:/opt/postfix/conf/sql/mysql_sasl_passwd_maps_sender_dependent.cf
|
||||||
smtp_sasl_security_options =
|
smtp_sasl_security_options =
|
||||||
smtp_sasl_mechanism_filter = plain, login
|
smtp_sasl_mechanism_filter = plain, login
|
||||||
smtp_tls_policy_maps = proxy:mysql:/opt/postfix/conf/sql/mysql_tls_policy_override_maps.cf
|
smtp_tls_policy_maps = proxy:mysql:/opt/postfix/conf/sql/mysql_tls_policy_override_maps.cf socketmap:inet:postfix-tlspol:8642:QUERYwithTLSRPT
|
||||||
smtp_header_checks = pcre:/opt/postfix/conf/anonymize_headers.pcre
|
smtp_header_checks = pcre:/opt/postfix/conf/anonymize_headers.pcre
|
||||||
mail_name = Postcow
|
mail_name = Postcow
|
||||||
# local_transport map catches local destinations and prevents routing local dests when the next map would route "*"
|
# local_transport map catches local destinations and prevents routing local dests when the next map would route "*"
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ smtps inet n - n - - smtpd
|
|||||||
# TLS protocol can be modified by setting submission_smtpd_tls_mandatory_protocols in extra.cf
|
# TLS protocol can be modified by setting submission_smtpd_tls_mandatory_protocols in extra.cf
|
||||||
submission inet n - n - - smtpd
|
submission inet n - n - - smtpd
|
||||||
-o smtpd_client_restrictions=permit_mynetworks,permit_sasl_authenticated,reject
|
-o smtpd_client_restrictions=permit_mynetworks,permit_sasl_authenticated,reject
|
||||||
-o smtpd_enforce_tls=yes
|
|
||||||
-o smtpd_tls_security_level=encrypt
|
-o smtpd_tls_security_level=encrypt
|
||||||
-o smtpd_tls_mandatory_protocols=$submission_smtpd_tls_mandatory_protocols
|
-o smtpd_tls_mandatory_protocols=$submission_smtpd_tls_mandatory_protocols
|
||||||
-o tls_preempt_cipherlist=yes
|
-o tls_preempt_cipherlist=yes
|
||||||
@@ -38,7 +37,6 @@ submission inet n - n - - smtpd
|
|||||||
10587 inet n - n - - smtpd
|
10587 inet n - n - - smtpd
|
||||||
-o smtpd_upstream_proxy_protocol=haproxy
|
-o smtpd_upstream_proxy_protocol=haproxy
|
||||||
-o smtpd_client_restrictions=permit_mynetworks,permit_sasl_authenticated,reject
|
-o smtpd_client_restrictions=permit_mynetworks,permit_sasl_authenticated,reject
|
||||||
-o smtpd_enforce_tls=yes
|
|
||||||
-o smtpd_tls_security_level=encrypt
|
-o smtpd_tls_security_level=encrypt
|
||||||
-o smtpd_tls_mandatory_protocols=$submission_smtpd_tls_mandatory_protocols
|
-o smtpd_tls_mandatory_protocols=$submission_smtpd_tls_mandatory_protocols
|
||||||
-o tls_preempt_cipherlist=yes
|
-o tls_preempt_cipherlist=yes
|
||||||
|
|||||||
File diff suppressed because it is too large.
Load diff
@@ -133,7 +133,7 @@ try {
|
|||||||
error_log("ALIAS EXPANDER: http pipe: goto address " . $goto . " is an alias branch for " . $goto_branch . PHP_EOL);
|
error_log("ALIAS EXPANDER: http pipe: goto address " . $goto . " is an alias branch for " . $goto_branch . PHP_EOL);
|
||||||
$goto_branch_array = explode(',', $goto_branch);
|
$goto_branch_array = explode(',', $goto_branch);
|
||||||
} else {
|
} else {
|
||||||
$stmt = $pdo->prepare("SELECT `target_domain` FROM `alias_domain` WHERE `alias_domain` = :domain AND `active` AND '1'");
|
$stmt = $pdo->prepare("SELECT `target_domain` FROM `alias_domain` WHERE `alias_domain` = :domain AND `active` = '1'");
|
||||||
$stmt->execute(array(':domain' => $parsed_goto['domain']));
|
$stmt->execute(array(':domain' => $parsed_goto['domain']));
|
||||||
$goto_branch = $stmt->fetch(PDO::FETCH_ASSOC)['target_domain'];
|
$goto_branch = $stmt->fetch(PDO::FETCH_ASSOC)['target_domain'];
|
||||||
if ($goto_branch) {
|
if ($goto_branch) {
|
||||||
|
|||||||
@@ -468,4 +468,36 @@ while ($row = array_shift($rows)) {
|
|||||||
<?php
|
<?php
|
||||||
}
|
}
|
||||||
?>
|
?>
|
||||||
|
|
||||||
|
<?php
|
||||||
|
// Start internal aliases
|
||||||
|
|
||||||
|
$stmt = $pdo->query("SELECT `id`, `address`, `domain` FROM `alias` WHERE `active` = '1' AND `internal` = '1'");
|
||||||
|
$aliases = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
while ($alias = array_shift($aliases)) {
|
||||||
|
// build allowed_domains regex and add target domain and alias domains
|
||||||
|
$stmt = $pdo->prepare("SELECT `alias_domain` FROM `alias_domain` WHERE `active` = '1' AND `target_domain` = :target_domain");
|
||||||
|
$stmt->execute(array(':target_domain' => $alias['domain']));
|
||||||
|
$allowed_domains = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
$allowed_domains = array_map(function($item) {
|
||||||
|
return str_replace('.', '\.', $item['alias_domain']);
|
||||||
|
}, $allowed_domains);
|
||||||
|
$allowed_domains[] = str_replace('.', '\.', $alias['domain']);
|
||||||
|
$allowed_domains = implode('|', $allowed_domains);
|
||||||
|
?>
|
||||||
|
internal_alias_<?=$alias['id'];?> {
|
||||||
|
priority = 10;
|
||||||
|
rcpt = "<?=$alias['address'];?>";
|
||||||
|
from = "/^((?!.*@(<?=$allowed_domains;?>)).)*$/";
|
||||||
|
apply "default" {
|
||||||
|
MAILCOW_INTERNAL_ALIAS = 9999.0;
|
||||||
|
}
|
||||||
|
symbols [
|
||||||
|
"MAILCOW_INTERNAL_ALIAS"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -3,8 +3,7 @@ rules {
|
|||||||
backend = "http";
|
backend = "http";
|
||||||
url = "http://nginx:9081/pipe.php";
|
url = "http://nginx:9081/pipe.php";
|
||||||
selector = "reject_no_global_bl";
|
selector = "reject_no_global_bl";
|
||||||
formatter = "default";
|
formatter = "multipart";
|
||||||
meta_headers = true;
|
|
||||||
}
|
}
|
||||||
RLINFO {
|
RLINFO {
|
||||||
backend = "http";
|
backend = "http";
|
||||||
@@ -16,8 +15,7 @@ rules {
|
|||||||
backend = "http";
|
backend = "http";
|
||||||
url = "http://nginx:9081/pushover.php";
|
url = "http://nginx:9081/pushover.php";
|
||||||
selector = "mailcow_rcpt";
|
selector = "mailcow_rcpt";
|
||||||
formatter = "json";
|
formatter = "multipart";
|
||||||
meta_headers = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ rspamd_config:register_symbol({
|
|||||||
local rcpt_split = rspamd_str_split(rcpt['addr'], '@')
|
local rcpt_split = rspamd_str_split(rcpt['addr'], '@')
|
||||||
if #rcpt_split == 2 then
|
if #rcpt_split == 2 then
|
||||||
if rcpt_split[1] == 'postmaster' then
|
if rcpt_split[1] == 'postmaster' then
|
||||||
task:set_pre_result('accept', 'whitelisting postmaster smtp rcpt')
|
task:set_pre_result('accept', 'whitelisting postmaster smtp rcpt', 'postmaster')
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -146,8 +146,171 @@ rspamd_config:register_symbol({
|
|||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Helper function to parse IPv6 into 8 segments
|
||||||
|
local function ipv6_to_segments(ip_str)
|
||||||
|
-- Remove zone identifier if present (e.g., %eth0)
|
||||||
|
ip_str = ip_str:gsub("%%.*$", "")
|
||||||
|
|
||||||
|
local segments = {}
|
||||||
|
|
||||||
|
-- Handle :: compression
|
||||||
|
if ip_str:find('::') then
|
||||||
|
local before, after = ip_str:match('^(.*)::(.*)$')
|
||||||
|
before = before or ''
|
||||||
|
after = after or ''
|
||||||
|
|
||||||
|
local before_parts = {}
|
||||||
|
local after_parts = {}
|
||||||
|
|
||||||
|
if before ~= '' then
|
||||||
|
for seg in before:gmatch('[^:]+') do
|
||||||
|
table.insert(before_parts, tonumber(seg, 16) or 0)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if after ~= '' then
|
||||||
|
for seg in after:gmatch('[^:]+') do
|
||||||
|
table.insert(after_parts, tonumber(seg, 16) or 0)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Add before segments
|
||||||
|
for _, seg in ipairs(before_parts) do
|
||||||
|
table.insert(segments, seg)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Add compressed zeros
|
||||||
|
local zeros_needed = 8 - #before_parts - #after_parts
|
||||||
|
for i = 1, zeros_needed do
|
||||||
|
table.insert(segments, 0)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Add after segments
|
||||||
|
for _, seg in ipairs(after_parts) do
|
||||||
|
table.insert(segments, seg)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
-- No compression
|
||||||
|
for seg in ip_str:gmatch('[^:]+') do
|
||||||
|
table.insert(segments, tonumber(seg, 16) or 0)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Ensure we have exactly 8 segments
|
||||||
|
while #segments < 8 do
|
||||||
|
table.insert(segments, 0)
|
||||||
|
end
|
||||||
|
|
||||||
|
return segments
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Generate all common IPv6 notations
|
||||||
|
local function get_ipv6_variants(ip_str)
|
||||||
|
local variants = {}
|
||||||
|
local seen = {}
|
||||||
|
|
||||||
|
local function add_variant(v)
|
||||||
|
if v and not seen[v] then
|
||||||
|
table.insert(variants, v)
|
||||||
|
seen[v] = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- For IPv4, just return the original
|
||||||
|
if not ip_str:find(':') then
|
||||||
|
add_variant(ip_str)
|
||||||
|
return variants
|
||||||
|
end
|
||||||
|
|
||||||
|
local segments = ipv6_to_segments(ip_str)
|
||||||
|
|
||||||
|
-- 1. Fully expanded form (all zeros shown as 0000)
|
||||||
|
local expanded_parts = {}
|
||||||
|
for _, seg in ipairs(segments) do
|
||||||
|
table.insert(expanded_parts, string.format('%04x', seg))
|
||||||
|
end
|
||||||
|
add_variant(table.concat(expanded_parts, ':'))
|
||||||
|
|
||||||
|
-- 2. Standard form (no leading zeros, but all segments present)
|
||||||
|
local standard_parts = {}
|
||||||
|
for _, seg in ipairs(segments) do
|
||||||
|
table.insert(standard_parts, string.format('%x', seg))
|
||||||
|
end
|
||||||
|
add_variant(table.concat(standard_parts, ':'))
|
||||||
|
|
||||||
|
-- 3. Find all possible :: compressions
|
||||||
|
-- RFC 5952: compress the longest run of consecutive zeros
|
||||||
|
-- But we need to check all possibilities since Redis might have any form
|
||||||
|
|
||||||
|
-- Find all zero runs
|
||||||
|
local zero_runs = {}
|
||||||
|
local in_run = false
|
||||||
|
local run_start = 0
|
||||||
|
local run_length = 0
|
||||||
|
|
||||||
|
for i = 1, 8 do
|
||||||
|
if segments[i] == 0 then
|
||||||
|
if not in_run then
|
||||||
|
in_run = true
|
||||||
|
run_start = i
|
||||||
|
run_length = 1
|
||||||
|
else
|
||||||
|
run_length = run_length + 1
|
||||||
|
end
|
||||||
|
else
|
||||||
|
if in_run then
|
||||||
|
if run_length >= 1 then -- Allow single zero compression too
|
||||||
|
table.insert(zero_runs, {start = run_start, length = run_length})
|
||||||
|
end
|
||||||
|
in_run = false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Don't forget the last run
|
||||||
|
if in_run and run_length >= 1 then
|
||||||
|
table.insert(zero_runs, {start = run_start, length = run_length})
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Generate variant for each zero run compression
|
||||||
|
for _, run in ipairs(zero_runs) do
|
||||||
|
local parts = {}
|
||||||
|
|
||||||
|
-- Before compression
|
||||||
|
for i = 1, run.start - 1 do
|
||||||
|
table.insert(parts, string.format('%x', segments[i]))
|
||||||
|
end
|
||||||
|
|
||||||
|
-- The compression
|
||||||
|
if run.start == 1 then
|
||||||
|
table.insert(parts, '')
|
||||||
|
table.insert(parts, '')
|
||||||
|
elseif run.start + run.length - 1 == 8 then
|
||||||
|
table.insert(parts, '')
|
||||||
|
table.insert(parts, '')
|
||||||
|
else
|
||||||
|
table.insert(parts, '')
|
||||||
|
end
|
||||||
|
|
||||||
|
-- After compression
|
||||||
|
for i = run.start + run.length, 8 do
|
||||||
|
table.insert(parts, string.format('%x', segments[i]))
|
||||||
|
end
|
||||||
|
|
||||||
|
local compressed = table.concat(parts, ':'):gsub('::+', '::')
|
||||||
|
add_variant(compressed)
|
||||||
|
end
|
||||||
|
|
||||||
|
return variants
|
||||||
|
end
|
||||||
|
|
||||||
local from_ip_string = tostring(ip)
|
local from_ip_string = tostring(ip)
|
||||||
ip_check_table = {from_ip_string}
|
local ip_check_table = {}
|
||||||
|
|
||||||
|
-- Add all variants of the exact IP
|
||||||
|
for _, variant in ipairs(get_ipv6_variants(from_ip_string)) do
|
||||||
|
table.insert(ip_check_table, variant)
|
||||||
|
end
|
||||||
|
|
||||||
local maxbits = 128
|
local maxbits = 128
|
||||||
local minbits = 32
|
local minbits = 32
|
||||||
@@ -155,10 +318,18 @@ rspamd_config:register_symbol({
|
|||||||
maxbits = 32
|
maxbits = 32
|
||||||
minbits = 8
|
minbits = 8
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Add all CIDR notations with variants
|
||||||
for i=maxbits,minbits,-1 do
|
for i=maxbits,minbits,-1 do
|
||||||
local nip = ip:apply_mask(i):to_string() .. "/" .. i
|
local masked_ip = ip:apply_mask(i)
|
||||||
table.insert(ip_check_table, nip)
|
local cidr_base = masked_ip:to_string()
|
||||||
|
|
||||||
|
for _, variant in ipairs(get_ipv6_variants(cidr_base)) do
|
||||||
|
local cidr = variant .. "/" .. i
|
||||||
|
table.insert(ip_check_table, cidr)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local function keep_spam_cb(err, data)
|
local function keep_spam_cb(err, data)
|
||||||
if err then
|
if err then
|
||||||
rspamd_logger.infox(rspamd_config, "keep_spam query request for ip %s returned invalid or empty data (\"%s\") or error (\"%s\")", ip, data, err)
|
rspamd_logger.infox(rspamd_config, "keep_spam query request for ip %s returned invalid or empty data (\"%s\") or error (\"%s\")", ip, data, err)
|
||||||
@@ -166,12 +337,15 @@ rspamd_config:register_symbol({
|
|||||||
else
|
else
|
||||||
for k,v in pairs(data) do
|
for k,v in pairs(data) do
|
||||||
if (v and v ~= userdata and v == '1') then
|
if (v and v ~= userdata and v == '1') then
|
||||||
rspamd_logger.infox(rspamd_config, "found ip in keep_spam map, setting pre-result")
|
rspamd_logger.infox(rspamd_config, "found ip %s (checked as: %s) in keep_spam map, setting pre-result accept", from_ip_string, ip_check_table[k])
|
||||||
task:set_pre_result('accept', 'ip matched with forward hosts')
|
task:set_pre_result('accept', 'ip matched with forward hosts', 'keep_spam')
|
||||||
|
task:set_flag('no_stat')
|
||||||
|
return
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
table.insert(ip_check_table, 1, 'KEEP_SPAM')
|
table.insert(ip_check_table, 1, 'KEEP_SPAM')
|
||||||
local redis_ret_user = rspamd_redis_make_request(task,
|
local redis_ret_user = rspamd_redis_make_request(task,
|
||||||
redis_params, -- connect params
|
redis_params, -- connect params
|
||||||
@@ -210,6 +384,7 @@ rspamd_config:register_symbol({
|
|||||||
rspamd_config:register_symbol({
|
rspamd_config:register_symbol({
|
||||||
name = 'TAG_MOO',
|
name = 'TAG_MOO',
|
||||||
type = 'postfilter',
|
type = 'postfilter',
|
||||||
|
flags = 'ignore_passthrough',
|
||||||
callback = function(task)
|
callback = function(task)
|
||||||
local util = require("rspamd_util")
|
local util = require("rspamd_util")
|
||||||
local rspamd_logger = require "rspamd_logger"
|
local rspamd_logger = require "rspamd_logger"
|
||||||
@@ -217,9 +392,7 @@ rspamd_config:register_symbol({
|
|||||||
local rspamd_http = require "rspamd_http"
|
local rspamd_http = require "rspamd_http"
|
||||||
local rcpts = task:get_recipients('smtp')
|
local rcpts = task:get_recipients('smtp')
|
||||||
local lua_util = require "lua_util"
|
local lua_util = require "lua_util"
|
||||||
|
|
||||||
local tagged_rcpt = task:get_symbol("TAGGED_RCPT")
|
local tagged_rcpt = task:get_symbol("TAGGED_RCPT")
|
||||||
local mailcow_domain = task:get_symbol("RCPT_MAILCOW_DOMAIN")
|
|
||||||
|
|
||||||
local function remove_moo_tag()
|
local function remove_moo_tag()
|
||||||
local moo_tag_header = task:get_header('X-Moo-Tag', false)
|
local moo_tag_header = task:get_header('X-Moo-Tag', false)
|
||||||
@@ -231,101 +404,147 @@ rspamd_config:register_symbol({
|
|||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
|
|
||||||
if tagged_rcpt and tagged_rcpt[1].options and mailcow_domain then
|
-- Check if we have exactly one recipient
|
||||||
local tag = tagged_rcpt[1].options[1]
|
if not (rcpts and #rcpts == 1) then
|
||||||
rspamd_logger.infox("found tag: %s", tag)
|
rspamd_logger.infox("TAG_MOO: not exactly one rcpt (%s), removing moo tag", rcpts and #rcpts or 0)
|
||||||
local action = task:get_metric_action('default')
|
remove_moo_tag()
|
||||||
rspamd_logger.infox("metric action now: %s", action)
|
return
|
||||||
|
end
|
||||||
|
|
||||||
if action ~= 'no action' and action ~= 'greylist' then
|
local rcpt_addr = rcpts[1]['addr']
|
||||||
rspamd_logger.infox("skipping tag handler for action: %s", action)
|
local rcpt_user = rcpts[1]['user']
|
||||||
remove_moo_tag()
|
local rcpt_domain = rcpts[1]['domain']
|
||||||
return true
|
|
||||||
|
-- Check if recipient has a tag (contains '+')
|
||||||
|
local tag = nil
|
||||||
|
if tagged_rcpt ~= nil then
|
||||||
|
tag = tagged_rcpt
|
||||||
|
rspamd_logger.infox("TAG_MOO: found tag in recipient: %s (base: %s, tag: %s)", rcpt_addr, base_user, tag)
|
||||||
|
end
|
||||||
|
|
||||||
|
if not tag then
|
||||||
|
rspamd_logger.infox("TAG_MOO: no tag found in recipient %s, removing moo tag", rcpt_addr)
|
||||||
|
remove_moo_tag()
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Optional: Check if domain is a mailcow domain
|
||||||
|
-- When KEEP_SPAM is active, RCPT_MAILCOW_DOMAIN might not be set
|
||||||
|
-- If the mail is being delivered, we can assume it's valid
|
||||||
|
local mailcow_domain = task:get_symbol("RCPT_MAILCOW_DOMAIN")
|
||||||
|
if not mailcow_domain then
|
||||||
|
rspamd_logger.infox("TAG_MOO: RCPT_MAILCOW_DOMAIN not set (possibly due to pre-result), proceeding anyway for domain %s", rcpt_domain)
|
||||||
|
end
|
||||||
|
|
||||||
|
local action = task:get_metric_action('default')
|
||||||
|
rspamd_logger.infox("TAG_MOO: metric action: %s", action)
|
||||||
|
|
||||||
|
-- Check if we have a pre-result (e.g., from KEEP_SPAM or POSTMASTER_HANDLER)
|
||||||
|
local allow_processing = false
|
||||||
|
|
||||||
|
if task.has_pre_result then
|
||||||
|
local has_pre, pre_action = task:has_pre_result()
|
||||||
|
if has_pre then
|
||||||
|
rspamd_logger.infox("TAG_MOO: pre-result detected: %s", tostring(pre_action))
|
||||||
|
if pre_action == 'accept' then
|
||||||
|
allow_processing = true
|
||||||
|
rspamd_logger.infox("TAG_MOO: pre-result is accept, will process")
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
local function http_callback(err_message, code, body, headers)
|
-- Allow processing for mild actions or when we have pre-result accept
|
||||||
if body ~= nil and body ~= "" then
|
if not allow_processing and action ~= 'no action' and action ~= 'greylist' then
|
||||||
rspamd_logger.infox(rspamd_config, "expanding rcpt to \"%s\"", body)
|
rspamd_logger.infox("TAG_MOO: skipping tag handler for action: %s", action)
|
||||||
|
remove_moo_tag()
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
local function tag_callback_subject(err, data)
|
rspamd_logger.infox("TAG_MOO: processing allowed")
|
||||||
if err or type(data) ~= 'string' then
|
|
||||||
rspamd_logger.infox(rspamd_config, "subject tag handler rcpt %s returned invalid or empty data (\"%s\") or error (\"%s\") - trying subfolder tag handler...", body, data, err)
|
|
||||||
|
|
||||||
local function tag_callback_subfolder(err, data)
|
local function http_callback(err_message, code, body, headers)
|
||||||
if err or type(data) ~= 'string' then
|
if body ~= nil and body ~= "" then
|
||||||
rspamd_logger.infox(rspamd_config, "subfolder tag handler for rcpt %s returned invalid or empty data (\"%s\") or error (\"%s\")", body, data, err)
|
rspamd_logger.infox(rspamd_config, "TAG_MOO: expanding rcpt to \"%s\"", body)
|
||||||
remove_moo_tag()
|
|
||||||
else
|
|
||||||
rspamd_logger.infox("Add X-Moo-Tag header")
|
|
||||||
task:set_milter_reply({
|
|
||||||
add_headers = {['X-Moo-Tag'] = 'YES'}
|
|
||||||
})
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local redis_ret_subfolder = rspamd_redis_make_request(task,
|
local function tag_callback_subject(err, data)
|
||||||
redis_params, -- connect params
|
if err or type(data) ~= 'string' or data == '' then
|
||||||
body, -- hash key
|
rspamd_logger.infox(rspamd_config, "TAG_MOO: subject tag handler rcpt %s returned invalid or empty data (\"%s\") or error (\"%s\") - trying subfolder tag handler...", body, data, err)
|
||||||
false, -- is write
|
|
||||||
tag_callback_subfolder, --callback
|
local function tag_callback_subfolder(err, data)
|
||||||
'HGET', -- command
|
if err or type(data) ~= 'string' or data == '' then
|
||||||
{'RCPT_WANTS_SUBFOLDER_TAG', body} -- arguments
|
rspamd_logger.infox(rspamd_config, "TAG_MOO: subfolder tag handler for rcpt %s returned invalid or empty data (\"%s\") or error (\"%s\")", body, data, err)
|
||||||
)
|
|
||||||
if not redis_ret_subfolder then
|
|
||||||
rspamd_logger.infox(rspamd_config, "cannot make request to load tag handler for rcpt")
|
|
||||||
remove_moo_tag()
|
remove_moo_tag()
|
||||||
|
else
|
||||||
|
rspamd_logger.infox("TAG_MOO: User wants subfolder tag, adding X-Moo-Tag header")
|
||||||
|
task:set_milter_reply({
|
||||||
|
add_headers = {['X-Moo-Tag'] = 'YES'}
|
||||||
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
else
|
|
||||||
rspamd_logger.infox("user wants subject modified for tagged mail")
|
|
||||||
local sbj = task:get_header('Subject')
|
|
||||||
new_sbj = '=?UTF-8?B?' .. tostring(util.encode_base64('[' .. tag .. '] ' .. sbj)) .. '?='
|
|
||||||
task:set_milter_reply({
|
|
||||||
remove_headers = {
|
|
||||||
['Subject'] = 1,
|
|
||||||
['X-Moo-Tag'] = 0
|
|
||||||
},
|
|
||||||
add_headers = {['Subject'] = new_sbj}
|
|
||||||
})
|
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
local redis_ret_subject = rspamd_redis_make_request(task,
|
local redis_ret_subfolder = rspamd_redis_make_request(task,
|
||||||
redis_params, -- connect params
|
redis_params, -- connect params
|
||||||
body, -- hash key
|
body, -- hash key
|
||||||
false, -- is write
|
false, -- is write
|
||||||
tag_callback_subject, --callback
|
tag_callback_subfolder, --callback
|
||||||
'HGET', -- command
|
'HGET', -- command
|
||||||
{'RCPT_WANTS_SUBJECT_TAG', body} -- arguments
|
{'RCPT_WANTS_SUBFOLDER_TAG', body} -- arguments
|
||||||
)
|
)
|
||||||
if not redis_ret_subject then
|
if not redis_ret_subfolder then
|
||||||
rspamd_logger.infox(rspamd_config, "cannot make request to load tag handler for rcpt")
|
rspamd_logger.infox(rspamd_config, "TAG_MOO: cannot make request to load tag handler for rcpt")
|
||||||
remove_moo_tag()
|
|
||||||
end
|
|
||||||
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if rcpts and #rcpts == 1 then
|
|
||||||
for _,rcpt in ipairs(rcpts) do
|
|
||||||
local rcpt_split = rspamd_str_split(rcpt['addr'], '@')
|
|
||||||
if #rcpt_split == 2 then
|
|
||||||
if rcpt_split[1] == 'postmaster' then
|
|
||||||
rspamd_logger.infox(rspamd_config, "not expanding postmaster alias")
|
|
||||||
remove_moo_tag()
|
remove_moo_tag()
|
||||||
else
|
|
||||||
rspamd_http.request({
|
|
||||||
task=task,
|
|
||||||
url='http://nginx:8081/aliasexp.php',
|
|
||||||
body='',
|
|
||||||
callback=http_callback,
|
|
||||||
headers={Rcpt=rcpt['addr']},
|
|
||||||
})
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
else
|
||||||
|
rspamd_logger.infox("TAG_MOO: user wants subject modified for tagged mail")
|
||||||
|
local sbj = task:get_header('Subject') or ''
|
||||||
|
local tag_value = tag[1] and tag[1].options and tag[1].options[1] or ''
|
||||||
|
new_sbj = '=?UTF-8?B?' .. tostring(util.encode_base64('[' .. tag_value .. '] ' .. sbj)) .. '?='
|
||||||
|
task:set_milter_reply({
|
||||||
|
remove_headers = {
|
||||||
|
['Subject'] = 1,
|
||||||
|
['X-Moo-Tag'] = 0
|
||||||
|
},
|
||||||
|
add_headers = {['Subject'] = new_sbj}
|
||||||
|
})
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local redis_ret_subject = rspamd_redis_make_request(task,
|
||||||
|
redis_params, -- connect params
|
||||||
|
body, -- hash key
|
||||||
|
false, -- is write
|
||||||
|
tag_callback_subject, --callback
|
||||||
|
'HGET', -- command
|
||||||
|
{'RCPT_WANTS_SUBJECT_TAG', body} -- arguments
|
||||||
|
)
|
||||||
|
if not redis_ret_subject then
|
||||||
|
rspamd_logger.infox(rspamd_config, "TAG_MOO: cannot make request to load tag handler for rcpt")
|
||||||
|
remove_moo_tag()
|
||||||
|
end
|
||||||
|
else
|
||||||
|
rspamd_logger.infox("TAG_MOO: alias expansion returned empty body")
|
||||||
|
remove_moo_tag()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local rcpt_split = rspamd_str_split(rcpt_addr, '@')
|
||||||
|
if #rcpt_split == 2 then
|
||||||
|
if rcpt_split[1]:match('^postmaster') then
|
||||||
|
rspamd_logger.infox(rspamd_config, "TAG_MOO: not expanding postmaster alias")
|
||||||
|
remove_moo_tag()
|
||||||
|
else
|
||||||
|
rspamd_logger.infox("TAG_MOO: requesting alias expansion for %s", rcpt_addr)
|
||||||
|
rspamd_http.request({
|
||||||
|
task=task,
|
||||||
|
url='http://nginx:8081/aliasexp.php',
|
||||||
|
body='',
|
||||||
|
callback=http_callback,
|
||||||
|
headers={Rcpt=rcpt_addr},
|
||||||
|
})
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
|
rspamd_logger.infox("TAG_MOO: invalid rcpt format")
|
||||||
remove_moo_tag()
|
remove_moo_tag()
|
||||||
end
|
end
|
||||||
end,
|
end,
|
||||||
@@ -335,6 +554,7 @@ rspamd_config:register_symbol({
|
|||||||
rspamd_config:register_symbol({
|
rspamd_config:register_symbol({
|
||||||
name = 'BCC',
|
name = 'BCC',
|
||||||
type = 'postfilter',
|
type = 'postfilter',
|
||||||
|
flags = 'ignore_passthrough',
|
||||||
callback = function(task)
|
callback = function(task)
|
||||||
local util = require("rspamd_util")
|
local util = require("rspamd_util")
|
||||||
local rspamd_http = require "rspamd_http"
|
local rspamd_http = require "rspamd_http"
|
||||||
@@ -363,11 +583,13 @@ rspamd_config:register_symbol({
|
|||||||
local email_content = tostring(task:get_content())
|
local email_content = tostring(task:get_content())
|
||||||
email_content = string.gsub(email_content, "\r\n%.", "\r\n..")
|
email_content = string.gsub(email_content, "\r\n%.", "\r\n..")
|
||||||
-- send mail
|
-- send mail
|
||||||
|
local from_smtp = task:get_from('smtp')
|
||||||
|
local from_addr = (from_smtp and from_smtp[1] and from_smtp[1].addr) or 'mailer-daemon@localhost'
|
||||||
lua_smtp.sendmail({
|
lua_smtp.sendmail({
|
||||||
task = task,
|
task = task,
|
||||||
host = os.getenv("IPV4_NETWORK") .. '.253',
|
host = os.getenv("IPV4_NETWORK") .. '.253',
|
||||||
port = 591,
|
port = 591,
|
||||||
from = task:get_from(stp)[1].addr,
|
from = from_addr,
|
||||||
recipients = bcc_dest,
|
recipients = bcc_dest,
|
||||||
helo = 'bcc',
|
helo = 'bcc',
|
||||||
timeout = 20,
|
timeout = 20,
|
||||||
@@ -397,27 +619,41 @@ rspamd_config:register_symbol({
|
|||||||
end
|
end
|
||||||
|
|
||||||
local action = task:get_metric_action('default')
|
local action = task:get_metric_action('default')
|
||||||
rspamd_logger.infox("metric action now: %s", action)
|
rspamd_logger.infox("BCC: metric action: %s", action)
|
||||||
|
|
||||||
|
-- Check for pre-result accept (e.g., from KEEP_SPAM)
|
||||||
|
local allow_bcc = false
|
||||||
|
if task.has_pre_result then
|
||||||
|
local has_pre, pre_action = task:has_pre_result()
|
||||||
|
if has_pre and pre_action == 'accept' then
|
||||||
|
allow_bcc = true
|
||||||
|
rspamd_logger.infox("BCC: pre-result accept detected, will send BCC")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Allow BCC for mild actions or when we have pre-result accept
|
||||||
|
if not allow_bcc and action ~= 'no action' and action ~= 'add header' and action ~= 'rewrite subject' then
|
||||||
|
rspamd_logger.infox("BCC: skipping for action: %s", action)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
local function rcpt_callback(err_message, code, body, headers)
|
local function rcpt_callback(err_message, code, body, headers)
|
||||||
if err_message == nil and code == 201 and body ~= nil then
|
if err_message == nil and code == 201 and body ~= nil then
|
||||||
if action == 'no action' or action == 'add header' or action == 'rewrite subject' then
|
rspamd_logger.infox("BCC: sending BCC to %s for rcpt match", body)
|
||||||
send_mail(task, body)
|
send_mail(task, body)
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local function from_callback(err_message, code, body, headers)
|
local function from_callback(err_message, code, body, headers)
|
||||||
if err_message == nil and code == 201 and body ~= nil then
|
if err_message == nil and code == 201 and body ~= nil then
|
||||||
if action == 'no action' or action == 'add header' or action == 'rewrite subject' then
|
rspamd_logger.infox("BCC: sending BCC to %s for from match", body)
|
||||||
send_mail(task, body)
|
send_mail(task, body)
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
if rcpt_table then
|
if rcpt_table then
|
||||||
for _,e in ipairs(rcpt_table) do
|
for _,e in ipairs(rcpt_table) do
|
||||||
rspamd_logger.infox(rspamd_config, "checking bcc for rcpt address %s", e)
|
rspamd_logger.infox(rspamd_config, "BCC: checking bcc for rcpt address %s", e)
|
||||||
rspamd_http.request({
|
rspamd_http.request({
|
||||||
task=task,
|
task=task,
|
||||||
url='http://nginx:8081/bcc.php',
|
url='http://nginx:8081/bcc.php',
|
||||||
@@ -430,7 +666,7 @@ rspamd_config:register_symbol({
|
|||||||
|
|
||||||
if from_table then
|
if from_table then
|
||||||
for _,e in ipairs(from_table) do
|
for _,e in ipairs(from_table) do
|
||||||
rspamd_logger.infox(rspamd_config, "checking bcc for from address %s", e)
|
rspamd_logger.infox(rspamd_config, "BCC: checking bcc for from address %s", e)
|
||||||
rspamd_http.request({
|
rspamd_http.request({
|
||||||
task=task,
|
task=task,
|
||||||
url='http://nginx:8081/bcc.php',
|
url='http://nginx:8081/bcc.php',
|
||||||
@@ -441,7 +677,7 @@ rspamd_config:register_symbol({
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
return true
|
-- Don't return true to avoid symbol being logged
|
||||||
end,
|
end,
|
||||||
priority = 20
|
priority = 20
|
||||||
})
|
})
|
||||||
@@ -454,12 +690,18 @@ rspamd_config:register_symbol({
|
|||||||
local redis_params = rspamd_parse_redis_server('dyn_rl')
|
local redis_params = rspamd_parse_redis_server('dyn_rl')
|
||||||
local rspamd_logger = require "rspamd_logger"
|
local rspamd_logger = require "rspamd_logger"
|
||||||
local envfrom = task:get_from(1)
|
local envfrom = task:get_from(1)
|
||||||
|
local envrcpt = task:get_recipients(1) or {}
|
||||||
local uname = task:get_user()
|
local uname = task:get_user()
|
||||||
if not envfrom or not uname then
|
if not envfrom or not uname then
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
local uname = uname:lower()
|
local uname = uname:lower()
|
||||||
|
|
||||||
|
if #envrcpt == 1 and envrcpt[1].addr:lower() == uname then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
local env_from_domain = envfrom[1].domain:lower() -- get smtp from domain in lower case
|
local env_from_domain = envfrom[1].domain:lower() -- get smtp from domain in lower case
|
||||||
|
|
||||||
local function redis_cb_user(err, data)
|
local function redis_cb_user(err, data)
|
||||||
@@ -614,30 +856,36 @@ rspamd_config:register_symbol({
|
|||||||
|
|
||||||
local seen_cte
|
local seen_cte
|
||||||
local newline_s = newline(task)
|
local newline_s = newline(task)
|
||||||
|
-- use the CTE add_text_footer actually applied
|
||||||
|
local new_cte = rewrite.new_cte or 'quoted-printable'
|
||||||
|
|
||||||
local function rewrite_ct_cb(name, hdr)
|
local function rewrite_ct_cb(name, hdr)
|
||||||
if rewrite.need_rewrite_ct then
|
if rewrite.need_rewrite_ct then
|
||||||
if name:lower() == 'content-type' then
|
if name:lower() == 'content-type' then
|
||||||
local nct = string.format('%s: %s/%s; charset=utf-8',
|
-- include boundary if present
|
||||||
'Content-Type', rewrite.new_ct.type, rewrite.new_ct.subtype)
|
local boundary_part = rewrite.new_ct.boundary and
|
||||||
|
string.format('; boundary="%s"', rewrite.new_ct.boundary) or ''
|
||||||
|
local nct = string.format('%s: %s/%s; charset=utf-8%s',
|
||||||
|
'Content-Type', rewrite.new_ct.type, rewrite.new_ct.subtype, boundary_part)
|
||||||
out[#out + 1] = nct
|
out[#out + 1] = nct
|
||||||
-- update Content-Type header
|
-- update Content-Type header (include boundary if present)
|
||||||
task:set_milter_reply({
|
task:set_milter_reply({
|
||||||
remove_headers = {['Content-Type'] = 0},
|
remove_headers = {['Content-Type'] = 0},
|
||||||
})
|
})
|
||||||
task:set_milter_reply({
|
task:set_milter_reply({
|
||||||
add_headers = {['Content-Type'] = string.format('%s/%s; charset=utf-8', rewrite.new_ct.type, rewrite.new_ct.subtype)}
|
add_headers = {['Content-Type'] = string.format('%s/%s; charset=utf-8%s',
|
||||||
|
rewrite.new_ct.type, rewrite.new_ct.subtype, boundary_part)}
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
elseif name:lower() == 'content-transfer-encoding' then
|
elseif name:lower() == 'content-transfer-encoding' then
|
||||||
out[#out + 1] = string.format('%s: %s',
|
out[#out + 1] = string.format('%s: %s',
|
||||||
'Content-Transfer-Encoding', 'quoted-printable')
|
'Content-Transfer-Encoding', new_cte)
|
||||||
-- update Content-Transfer-Encoding header
|
-- update Content-Transfer-Encoding header
|
||||||
task:set_milter_reply({
|
task:set_milter_reply({
|
||||||
remove_headers = {['Content-Transfer-Encoding'] = 0},
|
remove_headers = {['Content-Transfer-Encoding'] = 0},
|
||||||
})
|
})
|
||||||
task:set_milter_reply({
|
task:set_milter_reply({
|
||||||
add_headers = {['Content-Transfer-Encoding'] = 'quoted-printable'}
|
add_headers = {['Content-Transfer-Encoding'] = new_cte}
|
||||||
})
|
})
|
||||||
seen_cte = true
|
seen_cte = true
|
||||||
return
|
return
|
||||||
@@ -649,7 +897,7 @@ rspamd_config:register_symbol({
|
|||||||
task:headers_foreach(rewrite_ct_cb, {full = true})
|
task:headers_foreach(rewrite_ct_cb, {full = true})
|
||||||
|
|
||||||
if not seen_cte and rewrite.need_rewrite_ct then
|
if not seen_cte and rewrite.need_rewrite_ct then
|
||||||
out[#out + 1] = string.format('%s: %s', 'Content-Transfer-Encoding', 'quoted-printable')
|
out[#out + 1] = string.format('%s: %s', 'Content-Transfer-Encoding', new_cte)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- End of headers
|
-- End of headers
|
||||||
@@ -668,7 +916,7 @@ rspamd_config:register_symbol({
|
|||||||
out_parts[#out_parts + 1] = o
|
out_parts[#out_parts + 1] = o
|
||||||
out_parts[#out_parts + 1] = newline_s
|
out_parts[#out_parts + 1] = newline_s
|
||||||
else
|
else
|
||||||
local removePrefix = "--\x0D\x0AContent-Type"
|
local removePrefix = "--\x0D\x0AContent-"
|
||||||
if string.lower(string.sub(tostring(o[1]), 1, string.len(removePrefix))) == string.lower(removePrefix) then
|
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)
|
o[1] = string.sub(tostring(o[1]), string.len("--\x0D\x0A") + 1)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -32,47 +32,42 @@ function parse_email($email) {
|
|||||||
$a = strrpos($email, '@');
|
$a = strrpos($email, '@');
|
||||||
return array('local' => substr($email, 0, $a), 'domain' => substr(substr($email, $a), 1));
|
return array('local' => substr($email, 0, $a), 'domain' => substr(substr($email, $a), 1));
|
||||||
}
|
}
|
||||||
if (!function_exists('getallheaders')) {
|
// rspamd metadata_exporter (multipart formatter):
|
||||||
function getallheaders() {
|
// - $_POST['metadata'] JSON with the rspamd metadata
|
||||||
if (!is_array($_SERVER)) {
|
// - $_FILES['message'] raw RFC822 message
|
||||||
return array();
|
if (empty($_POST['metadata']) || !isset($_FILES['message']) || $_FILES['message']['error'] !== UPLOAD_ERR_OK) {
|
||||||
}
|
error_log("QUARANTINE: missing multipart parts from rspamd" . PHP_EOL);
|
||||||
$headers = array();
|
http_response_code(400);
|
||||||
foreach ($_SERVER as $name => $value) {
|
exit;
|
||||||
if (substr($name, 0, 5) == 'HTTP_') {
|
|
||||||
$headers[str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($name, 5)))))] = $value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return $headers;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$raw_data_content = file_get_contents('php://input');
|
$meta = json_decode($_POST['metadata'], true);
|
||||||
|
if (!is_array($meta)) {
|
||||||
|
error_log("QUARANTINE: cannot decode metadata JSON" . PHP_EOL);
|
||||||
|
http_response_code(400);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$raw_data_content = file_get_contents($_FILES['message']['tmp_name']);
|
||||||
$raw_data = mb_convert_encoding($raw_data_content, 'HTML-ENTITIES', "UTF-8");
|
$raw_data = mb_convert_encoding($raw_data_content, 'HTML-ENTITIES', "UTF-8");
|
||||||
$headers = getallheaders();
|
$raw_size = (int)$_FILES['message']['size'];
|
||||||
|
|
||||||
$qid = $headers['X-Rspamd-Qid'];
|
$qid = $meta['qid'] ?? 'unknown';
|
||||||
$fuzzy = $headers['X-Rspamd-Fuzzy'];
|
$subject = $meta['subject'] ?? '';
|
||||||
$subject = iconv_mime_decode($headers['X-Rspamd-Subject']);
|
$score = $meta['score'] ?? 0;
|
||||||
$score = $headers['X-Rspamd-Score'];
|
$rcpts = $meta['rcpt'] ?? array();
|
||||||
$rcpts = $headers['X-Rspamd-Rcpt'];
|
$user = $meta['user'] ?? 'unknown';
|
||||||
$user = $headers['X-Rspamd-User'];
|
$ip = $meta['ip'] ?? 'unknown';
|
||||||
$ip = $headers['X-Rspamd-Ip'];
|
$action = $meta['action'] ?? 'no action';
|
||||||
$action = $headers['X-Rspamd-Action'];
|
$sender = $meta['from'] ?? '';
|
||||||
$sender = $headers['X-Rspamd-From'];
|
$symbols = json_encode($meta['symbols'] ?? array());
|
||||||
$symbols = $headers['X-Rspamd-Symbols'];
|
$fuzzy = json_encode(is_array($meta['fuzzy'] ?? null) ? $meta['fuzzy'] : array());
|
||||||
|
|
||||||
$raw_size = (int)$_SERVER['CONTENT_LENGTH'];
|
|
||||||
|
|
||||||
if (empty($sender)) {
|
if (empty($sender)) {
|
||||||
error_log("QUARANTINE: Unknown sender, assuming empty-env-from@localhost" . PHP_EOL);
|
error_log("QUARANTINE: Unknown sender, assuming empty-env-from@localhost" . PHP_EOL);
|
||||||
$sender = 'empty-env-from@localhost';
|
$sender = 'empty-env-from@localhost';
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($fuzzy == 'unknown') {
|
|
||||||
$fuzzy = '[]';
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$max_size = (int)$redis->Get('Q_MAX_SIZE');
|
$max_size = (int)$redis->Get('Q_MAX_SIZE');
|
||||||
if (($max_size * 1048576) < $raw_size) {
|
if (($max_size * 1048576) < $raw_size) {
|
||||||
@@ -94,7 +89,7 @@ catch (RedisException $e) {
|
|||||||
$rcpt_final_mailboxes = array();
|
$rcpt_final_mailboxes = array();
|
||||||
|
|
||||||
// Loop through all rcpts
|
// Loop through all rcpts
|
||||||
foreach (json_decode($rcpts, true) as $rcpt) {
|
foreach ($rcpts as $rcpt) {
|
||||||
// Remove tag
|
// Remove tag
|
||||||
$rcpt = preg_replace('/^(.*?)\+.*(@.*)$/', '$1$2', $rcpt);
|
$rcpt = preg_replace('/^(.*?)\+.*(@.*)$/', '$1$2', $rcpt);
|
||||||
|
|
||||||
@@ -158,12 +153,12 @@ foreach (json_decode($rcpts, true) as $rcpt) {
|
|||||||
|
|
||||||
// Loop through all found gotos
|
// Loop through all found gotos
|
||||||
foreach ($gotos_array as $index => &$goto) {
|
foreach ($gotos_array as $index => &$goto) {
|
||||||
error_log("RCPT RESOVLER: http pipe: query " . $goto . " as username from mailbox" . PHP_EOL);
|
error_log("RCPT RESOLVER: http pipe: query " . $goto . " as username from mailbox" . PHP_EOL);
|
||||||
$stmt = $pdo->prepare("SELECT `username` FROM `mailbox` WHERE `username` = :goto AND (`active`= '1' OR `active`= '2');");
|
$stmt = $pdo->prepare("SELECT `username` FROM `mailbox` WHERE `username` = :goto AND (`active`= '1' OR `active`= '2');");
|
||||||
$stmt->execute(array(':goto' => $goto));
|
$stmt->execute(array(':goto' => $goto));
|
||||||
$username = $stmt->fetch(PDO::FETCH_ASSOC)['username'];
|
$username = $stmt->fetch(PDO::FETCH_ASSOC)['username'];
|
||||||
if (!empty($username)) {
|
if (!empty($username)) {
|
||||||
error_log("RCPT RESOVLER: http pipe: mailbox found: " . $username . PHP_EOL);
|
error_log("RCPT RESOLVER: http pipe: mailbox found: " . $username . PHP_EOL);
|
||||||
// Current goto is a mailbox, save to rcpt_final_mailboxes if not a duplicate
|
// Current goto is a mailbox, save to rcpt_final_mailboxes if not a duplicate
|
||||||
if (!in_array($username, $rcpt_final_mailboxes)) {
|
if (!in_array($username, $rcpt_final_mailboxes)) {
|
||||||
$rcpt_final_mailboxes[] = $username;
|
$rcpt_final_mailboxes[] = $username;
|
||||||
@@ -172,21 +167,21 @@ foreach (json_decode($rcpts, true) as $rcpt) {
|
|||||||
else {
|
else {
|
||||||
$parsed_goto = parse_email($goto);
|
$parsed_goto = parse_email($goto);
|
||||||
if (!$redis->hGet('DOMAIN_MAP', $parsed_goto['domain'])) {
|
if (!$redis->hGet('DOMAIN_MAP', $parsed_goto['domain'])) {
|
||||||
error_log("RCPT RESOVLER:" . $goto . " is not a mailcow handled mailbox or alias address" . PHP_EOL);
|
error_log("RCPT RESOLVER:" . $goto . " is not a mailcow handled mailbox or alias address" . PHP_EOL);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
$stmt = $pdo->prepare("SELECT `goto` FROM `alias` WHERE `address` = :goto AND `active` = '1'");
|
$stmt = $pdo->prepare("SELECT `goto` FROM `alias` WHERE `address` = :goto AND `active` = '1'");
|
||||||
$stmt->execute(array(':goto' => $goto));
|
$stmt->execute(array(':goto' => $goto));
|
||||||
$goto_branch = $stmt->fetch(PDO::FETCH_ASSOC)['goto'];
|
$goto_branch = $stmt->fetch(PDO::FETCH_ASSOC)['goto'];
|
||||||
if ($goto_branch) {
|
if ($goto_branch) {
|
||||||
error_log("RCPT RESOVLER: http pipe: goto address " . $goto . " is an alias branch for " . $goto_branch . PHP_EOL);
|
error_log("RCPT RESOLVER: http pipe: goto address " . $goto . " is an alias branch for " . $goto_branch . PHP_EOL);
|
||||||
$goto_branch_array = explode(',', $goto_branch);
|
$goto_branch_array = explode(',', $goto_branch);
|
||||||
} else {
|
} else {
|
||||||
$stmt = $pdo->prepare("SELECT `target_domain` FROM `alias_domain` WHERE `alias_domain` = :domain AND `active` AND '1'");
|
$stmt = $pdo->prepare("SELECT `target_domain` FROM `alias_domain` WHERE `alias_domain` = :domain AND `active` = '1'");
|
||||||
$stmt->execute(array(':domain' => $parsed_goto['domain']));
|
$stmt->execute(array(':domain' => $parsed_goto['domain']));
|
||||||
$goto_branch = $stmt->fetch(PDO::FETCH_ASSOC)['target_domain'];
|
$goto_branch = $stmt->fetch(PDO::FETCH_ASSOC)['target_domain'];
|
||||||
if ($goto_branch) {
|
if ($goto_branch) {
|
||||||
error_log("RCPT RESOVLER: http pipe: goto domain " . $parsed_goto['domain'] . " is a domain alias branch for " . $goto_branch . PHP_EOL);
|
error_log("RCPT RESOLVER: http pipe: goto domain " . $parsed_goto['domain'] . " is a domain alias branch for " . $goto_branch . PHP_EOL);
|
||||||
$goto_branch_array = array($parsed_goto['local'] . '@' . $goto_branch);
|
$goto_branch_array = array($parsed_goto['local'] . '@' . $goto_branch);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -208,11 +203,11 @@ foreach (json_decode($rcpts, true) as $rcpt) {
|
|||||||
// Force exit if loop cannot be solved
|
// Force exit if loop cannot be solved
|
||||||
// Postfix does not allow for alias loops, so this should never happen.
|
// Postfix does not allow for alias loops, so this should never happen.
|
||||||
$loop_c++;
|
$loop_c++;
|
||||||
error_log("RCPT RESOVLER: http pipe: goto array count on loop #". $loop_c . " is " . count($gotos_array) . PHP_EOL);
|
error_log("RCPT RESOLVER: http pipe: goto array count on loop #". $loop_c . " is " . count($gotos_array) . PHP_EOL);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (PDOException $e) {
|
catch (PDOException $e) {
|
||||||
error_log("RCPT RESOVLER: " . $e->getMessage() . PHP_EOL);
|
error_log("RCPT RESOLVER: " . $e->getMessage() . PHP_EOL);
|
||||||
http_response_code(502);
|
http_response_code(502);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
@@ -236,6 +231,9 @@ foreach ($rcpt_final_mailboxes as $rcpt_final) {
|
|||||||
':action' => $action,
|
':action' => $action,
|
||||||
':fuzzy_hashes' => $fuzzy
|
':fuzzy_hashes' => $fuzzy
|
||||||
));
|
));
|
||||||
|
$lastId = $pdo->lastInsertId();
|
||||||
|
$stmt_update = $pdo->prepare("UPDATE `quarantine` SET `qhash` = SHA2(CONCAT(`id`, `qid`), 256) WHERE `id` = :id");
|
||||||
|
$stmt_update->execute(array(':id' => $lastId));
|
||||||
$stmt = $pdo->prepare('DELETE FROM `quarantine` WHERE `rcpt` = :rcpt AND `id` NOT IN (
|
$stmt = $pdo->prepare('DELETE FROM `quarantine` WHERE `rcpt` = :rcpt AND `id` NOT IN (
|
||||||
SELECT `id`
|
SELECT `id`
|
||||||
FROM (
|
FROM (
|
||||||
|
|||||||
@@ -32,50 +32,46 @@ function parse_email($email) {
|
|||||||
$a = strrpos($email, '@');
|
$a = strrpos($email, '@');
|
||||||
return array('local' => substr($email, 0, $a), 'domain' => substr(substr($email, $a), 1));
|
return array('local' => substr($email, 0, $a), 'domain' => substr(substr($email, $a), 1));
|
||||||
}
|
}
|
||||||
if (!function_exists('getallheaders')) {
|
// rspamd metadata_exporter (multipart formatter): metadata JSON arrives as $_POST['metadata'].
|
||||||
function getallheaders() {
|
if (empty($_POST['metadata'])) {
|
||||||
if (!is_array($_SERVER)) {
|
error_log("NOTIFY: missing metadata part from rspamd" . PHP_EOL);
|
||||||
return array();
|
http_response_code(400);
|
||||||
}
|
exit;
|
||||||
$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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$headers = getallheaders();
|
$meta = json_decode($_POST['metadata'], true);
|
||||||
$json_body = json_decode(file_get_contents('php://input'));
|
if (!is_array($meta)) {
|
||||||
|
error_log("NOTIFY: cannot decode metadata JSON" . PHP_EOL);
|
||||||
|
http_response_code(400);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
$qid = $headers['X-Rspamd-Qid'];
|
$qid = $meta['qid'] ?? 'unknown';
|
||||||
$rcpts = $headers['X-Rspamd-Rcpt'];
|
$rcpts = $meta['rcpt'] ?? array();
|
||||||
$sender = $headers['X-Rspamd-From'];
|
$sender = $meta['from'] ?? '';
|
||||||
$ip = $headers['X-Rspamd-Ip'];
|
$ip = $meta['ip'] ?? 'unknown';
|
||||||
$subject = iconv_mime_decode($headers['X-Rspamd-Subject']);
|
$subject = $meta['subject'] ?? '';
|
||||||
$messageid= $json_body->message_id;
|
$messageid= $meta['message_id'] ?? '';
|
||||||
$priority = 0;
|
$priority = 0;
|
||||||
|
|
||||||
$symbols_array = json_decode($headers['X-Rspamd-Symbols'], true);
|
$symbols_array = $meta['symbols'] ?? array();
|
||||||
if (is_array($symbols_array)) {
|
if (is_array($symbols_array)) {
|
||||||
foreach ($symbols_array as $symbol) {
|
foreach ($symbols_array as $symbol) {
|
||||||
if ($symbol['name'] == 'HAS_X_PRIO_ONE') {
|
if (($symbol['name'] ?? null) == 'HAS_X_PRIO_ONE') {
|
||||||
$priority = 1;
|
$priority = 1;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$sender_address = $json_body->header_from[0];
|
$sender_address = $meta['header_from'][0] ?? '';
|
||||||
$sender_name = '-';
|
$sender_name = '-';
|
||||||
if (preg_match('/(?<name>.*?)<(?<address>.*?)>/i', $sender_address, $matches)) {
|
if (preg_match('/(?<name>.*?)<(?<address>.*?)>/i', $sender_address, $matches)) {
|
||||||
$sender_address = $matches['address'];
|
$sender_address = $matches['address'];
|
||||||
$sender_name = trim($matches['name'], '"\' ');
|
$sender_name = trim($matches['name'], '"\' ');
|
||||||
}
|
}
|
||||||
|
|
||||||
$to_address = $json_body->header_to[0];
|
$to_address = $meta['header_to'][0] ?? '';
|
||||||
$to_name = '-';
|
$to_name = '-';
|
||||||
if (preg_match('/(?<name>.*?)<(?<address>.*?)>/i', $to_address, $matches)) {
|
if (preg_match('/(?<name>.*?)<(?<address>.*?)>/i', $to_address, $matches)) {
|
||||||
$to_address = $matches['address'];
|
$to_address = $matches['address'];
|
||||||
@@ -85,7 +81,7 @@ if (preg_match('/(?<name>.*?)<(?<address>.*?)>/i', $to_address, $matches)) {
|
|||||||
$rcpt_final_mailboxes = array();
|
$rcpt_final_mailboxes = array();
|
||||||
|
|
||||||
// Loop through all rcpts
|
// Loop through all rcpts
|
||||||
foreach (json_decode($rcpts, true) as $rcpt) {
|
foreach ($rcpts as $rcpt) {
|
||||||
// Remove tag
|
// Remove tag
|
||||||
$rcpt = preg_replace('/^(.*?)\+.*(@.*)$/', '$1$2', $rcpt);
|
$rcpt = preg_replace('/^(.*?)\+.*(@.*)$/', '$1$2', $rcpt);
|
||||||
|
|
||||||
@@ -143,12 +139,12 @@ foreach (json_decode($rcpts, true) as $rcpt) {
|
|||||||
|
|
||||||
// Loop through all found gotos
|
// Loop through all found gotos
|
||||||
foreach ($gotos_array as $index => &$goto) {
|
foreach ($gotos_array as $index => &$goto) {
|
||||||
error_log("RCPT RESOVLER: http pipe: query " . $goto . " as username from mailbox" . PHP_EOL);
|
error_log("RCPT RESOLVER: http pipe: query " . $goto . " as username from mailbox" . PHP_EOL);
|
||||||
$stmt = $pdo->prepare("SELECT `username` FROM `mailbox` WHERE `username` = :goto AND (`active`= '1' OR `active`= '2');");
|
$stmt = $pdo->prepare("SELECT `username` FROM `mailbox` WHERE `username` = :goto AND (`active`= '1' OR `active`= '2');");
|
||||||
$stmt->execute(array(':goto' => $goto));
|
$stmt->execute(array(':goto' => $goto));
|
||||||
$username = $stmt->fetch(PDO::FETCH_ASSOC)['username'];
|
$username = $stmt->fetch(PDO::FETCH_ASSOC)['username'];
|
||||||
if (!empty($username)) {
|
if (!empty($username)) {
|
||||||
error_log("RCPT RESOVLER: http pipe: mailbox found: " . $username . PHP_EOL);
|
error_log("RCPT RESOLVER: http pipe: mailbox found: " . $username . PHP_EOL);
|
||||||
// Current goto is a mailbox, save to rcpt_final_mailboxes if not a duplicate
|
// Current goto is a mailbox, save to rcpt_final_mailboxes if not a duplicate
|
||||||
if (!in_array($username, $rcpt_final_mailboxes)) {
|
if (!in_array($username, $rcpt_final_mailboxes)) {
|
||||||
$rcpt_final_mailboxes[] = $username;
|
$rcpt_final_mailboxes[] = $username;
|
||||||
@@ -157,21 +153,21 @@ foreach (json_decode($rcpts, true) as $rcpt) {
|
|||||||
else {
|
else {
|
||||||
$parsed_goto = parse_email($goto);
|
$parsed_goto = parse_email($goto);
|
||||||
if (!$redis->hGet('DOMAIN_MAP', $parsed_goto['domain'])) {
|
if (!$redis->hGet('DOMAIN_MAP', $parsed_goto['domain'])) {
|
||||||
error_log("RCPT RESOVLER:" . $goto . " is not a mailcow handled mailbox or alias address" . PHP_EOL);
|
error_log("RCPT RESOLVER:" . $goto . " is not a mailcow handled mailbox or alias address" . PHP_EOL);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
$stmt = $pdo->prepare("SELECT `goto` FROM `alias` WHERE `address` = :goto AND `active` = '1'");
|
$stmt = $pdo->prepare("SELECT `goto` FROM `alias` WHERE `address` = :goto AND `active` = '1'");
|
||||||
$stmt->execute(array(':goto' => $goto));
|
$stmt->execute(array(':goto' => $goto));
|
||||||
$goto_branch = $stmt->fetch(PDO::FETCH_ASSOC)['goto'];
|
$goto_branch = $stmt->fetch(PDO::FETCH_ASSOC)['goto'];
|
||||||
if ($goto_branch) {
|
if ($goto_branch) {
|
||||||
error_log("RCPT RESOVLER: http pipe: goto address " . $goto . " is an alias branch for " . $goto_branch . PHP_EOL);
|
error_log("RCPT RESOLVER: http pipe: goto address " . $goto . " is an alias branch for " . $goto_branch . PHP_EOL);
|
||||||
$goto_branch_array = explode(',', $goto_branch);
|
$goto_branch_array = explode(',', $goto_branch);
|
||||||
} else {
|
} else {
|
||||||
$stmt = $pdo->prepare("SELECT `target_domain` FROM `alias_domain` WHERE `alias_domain` = :domain AND `active` AND '1'");
|
$stmt = $pdo->prepare("SELECT `target_domain` FROM `alias_domain` WHERE `alias_domain` = :domain AND `active` = '1'");
|
||||||
$stmt->execute(array(':domain' => $parsed_goto['domain']));
|
$stmt->execute(array(':domain' => $parsed_goto['domain']));
|
||||||
$goto_branch = $stmt->fetch(PDO::FETCH_ASSOC)['target_domain'];
|
$goto_branch = $stmt->fetch(PDO::FETCH_ASSOC)['target_domain'];
|
||||||
if ($goto_branch) {
|
if ($goto_branch) {
|
||||||
error_log("RCPT RESOVLER: http pipe: goto domain " . $parsed_goto['domain'] . " is a domain alias branch for " . $goto_branch . PHP_EOL);
|
error_log("RCPT RESOLVER: http pipe: goto domain " . $parsed_goto['domain'] . " is a domain alias branch for " . $goto_branch . PHP_EOL);
|
||||||
$goto_branch_array = array($parsed_goto['local'] . '@' . $goto_branch);
|
$goto_branch_array = array($parsed_goto['local'] . '@' . $goto_branch);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -193,11 +189,11 @@ foreach (json_decode($rcpts, true) as $rcpt) {
|
|||||||
// Force exit if loop cannot be solved
|
// Force exit if loop cannot be solved
|
||||||
// Postfix does not allow for alias loops, so this should never happen.
|
// Postfix does not allow for alias loops, so this should never happen.
|
||||||
$loop_c++;
|
$loop_c++;
|
||||||
error_log("RCPT RESOVLER: http pipe: goto array count on loop #". $loop_c . " is " . count($gotos_array) . PHP_EOL);
|
error_log("RCPT RESOLVER: http pipe: goto array count on loop #". $loop_c . " is " . count($gotos_array) . PHP_EOL);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (PDOException $e) {
|
catch (PDOException $e) {
|
||||||
error_log("RCPT RESOVLER: " . $e->getMessage() . PHP_EOL);
|
error_log("RCPT RESOLVER: " . $e->getMessage() . PHP_EOL);
|
||||||
http_response_code(502);
|
http_response_code(502);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -86,6 +86,12 @@
|
|||||||
SOGoMaximumFailedLoginInterval = 900;
|
SOGoMaximumFailedLoginInterval = 900;
|
||||||
SOGoFailedLoginBlockInterval = 900;
|
SOGoFailedLoginBlockInterval = 900;
|
||||||
|
|
||||||
|
// Enable SOGo URL Description for GDPR compliance, this may cause some issues with calendars and contacts. Also uncomment the encryption key below to use it.
|
||||||
|
//SOGoURLEncryptionEnabled = NO;
|
||||||
|
|
||||||
|
// Set a 16 character encryption key for SOGo URL Description, change this to your own value
|
||||||
|
//SOGoURLPathEncryptionKey = "SOGoSuperSecret0";
|
||||||
|
|
||||||
GCSChannelCollectionTimer = 60;
|
GCSChannelCollectionTimer = 60;
|
||||||
GCSChannelExpireAge = 60;
|
GCSChannelExpireAge = 60;
|
||||||
|
|
||||||
|
|||||||
@@ -2,18 +2,7 @@
|
|||||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
|
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
|
||||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.admin.inc.php';
|
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.admin.inc.php';
|
||||||
|
|
||||||
if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'domainadmin') {
|
protect_route(['admin']);
|
||||||
header('Location: /domainadmin/mailbox');
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user') {
|
|
||||||
header('Location: /user');
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
elseif (!isset($_SESSION['mailcow_cc_role']) || $_SESSION['mailcow_cc_role'] != "admin") {
|
|
||||||
header('Location: /admin');
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
|
|
||||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php';
|
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php';
|
||||||
$_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
|
$_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
|
||||||
|
|||||||
@@ -3,8 +3,11 @@ require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
|
|||||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.admin.inc.php';
|
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.admin.inc.php';
|
||||||
|
|
||||||
if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'admin') {
|
if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'admin') {
|
||||||
header('Location: /admin/dashboard');
|
// Only redirect to dashboard if NO pending actions
|
||||||
exit();
|
if (empty($_SESSION['pending_tfa_setup']) && empty($_SESSION['pending_pw_update'])) {
|
||||||
|
header('Location: /admin/dashboard');
|
||||||
|
exit();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'domainadmin') {
|
elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'domainadmin') {
|
||||||
header('Location: /domainadmin/mailbox');
|
header('Location: /domainadmin/mailbox');
|
||||||
|
|||||||
@@ -2,18 +2,7 @@
|
|||||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
|
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
|
||||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.admin.inc.php';
|
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.admin.inc.php';
|
||||||
|
|
||||||
if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'domainadmin') {
|
protect_route(['admin']);
|
||||||
header('Location: /domainadmin/mailbox');
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user') {
|
|
||||||
header('Location: /user');
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
elseif (!isset($_SESSION['mailcow_cc_role']) || $_SESSION['mailcow_cc_role'] != "admin") {
|
|
||||||
header('Location: /admin');
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
|
|
||||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php';
|
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php';
|
||||||
$_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
|
$_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
|
||||||
|
|||||||
@@ -2,19 +2,7 @@
|
|||||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
|
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
|
||||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.admin.inc.php';
|
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.admin.inc.php';
|
||||||
|
|
||||||
if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'domainadmin') {
|
protect_route(['admin']);
|
||||||
header('Location: /domainadmin/mailbox');
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user') {
|
|
||||||
header('Location: /user');
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
elseif (!isset($_SESSION['mailcow_cc_role']) || $_SESSION['mailcow_cc_role'] != "admin") {
|
|
||||||
header('Location: /admin');
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php';
|
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php';
|
||||||
$js_minifier->add('/web/js/site/queue.js');
|
$js_minifier->add('/web/js/site/queue.js');
|
||||||
|
|||||||
@@ -2,18 +2,7 @@
|
|||||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
|
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
|
||||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.admin.inc.php';
|
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.admin.inc.php';
|
||||||
|
|
||||||
if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'domainadmin') {
|
protect_route(['admin']);
|
||||||
header('Location: /domainadmin/mailbox');
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user') {
|
|
||||||
header('Location: /user');
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
elseif (!isset($_SESSION['mailcow_cc_role']) || $_SESSION['mailcow_cc_role'] != "admin") {
|
|
||||||
header('Location: /admin');
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
|
|
||||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php';
|
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php';
|
||||||
$_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
|
$_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
|
||||||
@@ -126,6 +115,8 @@ $template_data = [
|
|||||||
'logo_dark_specs' => customize('get', 'main_logo_dark_specs'),
|
'logo_dark_specs' => customize('get', 'main_logo_dark_specs'),
|
||||||
'ip_check' => customize('get', 'ip_check'),
|
'ip_check' => customize('get', 'ip_check'),
|
||||||
'custom_login' => customize('get', 'custom_login'),
|
'custom_login' => customize('get', 'custom_login'),
|
||||||
|
'ui_themes' => customize('get', 'ui_themes'),
|
||||||
|
'ui_theme' => customize('get', 'ui_theme'),
|
||||||
'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,
|
'cors_settings' => $cors_settings,
|
||||||
|
|||||||
+112
-2
@@ -185,6 +185,9 @@ paths:
|
|||||||
example:
|
example:
|
||||||
username: info@domain.tld
|
username: info@domain.tld
|
||||||
domain: domain.tld
|
domain: domain.tld
|
||||||
|
description: my time limited alias
|
||||||
|
validity: 8760
|
||||||
|
permanent: false
|
||||||
properties:
|
properties:
|
||||||
username:
|
username:
|
||||||
description: 'the mailbox an alias should be created for'
|
description: 'the mailbox an alias should be created for'
|
||||||
@@ -192,6 +195,15 @@ paths:
|
|||||||
domain:
|
domain:
|
||||||
description: "the domain"
|
description: "the domain"
|
||||||
type: string
|
type: string
|
||||||
|
description:
|
||||||
|
description: "a description for the alias, defaults to an empty string"
|
||||||
|
type: string
|
||||||
|
validity:
|
||||||
|
description: "how many hours the alias stays valid, 1 to 87600, defaults to 8760 (one year)"
|
||||||
|
type: integer
|
||||||
|
permanent:
|
||||||
|
description: "keep the alias after it expired, defaults to false"
|
||||||
|
type: boolean
|
||||||
type: object
|
type: object
|
||||||
summary: Create time limited alias
|
summary: Create time limited alias
|
||||||
/api/v1/add/app-passwd:
|
/api/v1/add/app-passwd:
|
||||||
@@ -1072,6 +1084,7 @@ paths:
|
|||||||
password2: "*"
|
password2: "*"
|
||||||
quota: "3072"
|
quota: "3072"
|
||||||
force_pw_update: "1"
|
force_pw_update: "1"
|
||||||
|
force_tfa: "1"
|
||||||
tls_enforce_in: "1"
|
tls_enforce_in: "1"
|
||||||
tls_enforce_out: "1"
|
tls_enforce_out: "1"
|
||||||
tags: ["tag1", "tag2"]
|
tags: ["tag1", "tag2"]
|
||||||
@@ -1118,6 +1131,7 @@ paths:
|
|||||||
password2: atedismonsin
|
password2: atedismonsin
|
||||||
quota: "3072"
|
quota: "3072"
|
||||||
force_pw_update: "1"
|
force_pw_update: "1"
|
||||||
|
force_tfa: "1"
|
||||||
tls_enforce_in: "1"
|
tls_enforce_in: "1"
|
||||||
tls_enforce_out: "1"
|
tls_enforce_out: "1"
|
||||||
tags: ["tag1", "tag2"]
|
tags: ["tag1", "tag2"]
|
||||||
@@ -1151,6 +1165,9 @@ paths:
|
|||||||
force_pw_update:
|
force_pw_update:
|
||||||
description: forces the user to update its password on first login
|
description: forces the user to update its password on first login
|
||||||
type: boolean
|
type: boolean
|
||||||
|
force_tfa:
|
||||||
|
description: force 2FA enrollment at login
|
||||||
|
type: boolean
|
||||||
tls_enforce_in:
|
tls_enforce_in:
|
||||||
description: force inbound email tls encryption
|
description: force inbound email tls encryption
|
||||||
type: boolean
|
type: boolean
|
||||||
@@ -2454,6 +2471,90 @@ paths:
|
|||||||
type: object
|
type: object
|
||||||
type: object
|
type: object
|
||||||
summary: Delete mails in Quarantine
|
summary: Delete mails in Quarantine
|
||||||
|
/api/v1/edit/qitem:
|
||||||
|
post:
|
||||||
|
responses:
|
||||||
|
"401":
|
||||||
|
$ref: "#/components/responses/Unauthorized"
|
||||||
|
"200":
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
examples:
|
||||||
|
release:
|
||||||
|
value:
|
||||||
|
- log:
|
||||||
|
- quarantine
|
||||||
|
- edit
|
||||||
|
- id:
|
||||||
|
- "33"
|
||||||
|
action: release
|
||||||
|
msg:
|
||||||
|
- item_released
|
||||||
|
- "33"
|
||||||
|
type: success
|
||||||
|
learnham:
|
||||||
|
value:
|
||||||
|
- log:
|
||||||
|
- quarantine
|
||||||
|
- edit
|
||||||
|
- id:
|
||||||
|
- "34"
|
||||||
|
action: learnham
|
||||||
|
msg:
|
||||||
|
- item_learned
|
||||||
|
- "34"
|
||||||
|
type: success
|
||||||
|
schema:
|
||||||
|
properties:
|
||||||
|
log:
|
||||||
|
description: contains request object
|
||||||
|
items: {}
|
||||||
|
type: array
|
||||||
|
msg:
|
||||||
|
items: {}
|
||||||
|
type: array
|
||||||
|
type:
|
||||||
|
enum:
|
||||||
|
- success
|
||||||
|
- danger
|
||||||
|
- error
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
description: OK
|
||||||
|
headers: {}
|
||||||
|
tags:
|
||||||
|
- Quarantine
|
||||||
|
description: >-
|
||||||
|
Using this endpoint you can perform actions on quarantine items. It is possible to release
|
||||||
|
emails from quarantine into to the inbox, or learn them as ham to improve Rspamd filtering.
|
||||||
|
You must provide the quarantine item IDs. You can get the IDs using the GET method.
|
||||||
|
operationId: Edit mails in Quarantine
|
||||||
|
requestBody:
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
example:
|
||||||
|
items:
|
||||||
|
- "33"
|
||||||
|
- "34"
|
||||||
|
attr:
|
||||||
|
action: release
|
||||||
|
properties:
|
||||||
|
items:
|
||||||
|
description: contains list of quarantine item IDs to release or learn as ham
|
||||||
|
type: object
|
||||||
|
attr:
|
||||||
|
description: attributes for the action
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
action:
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- release
|
||||||
|
- learnham
|
||||||
|
description: "release - return email to inbox; learnham - learn as ham to improve filtering"
|
||||||
|
type: object
|
||||||
|
summary: Edit mails in Quarantine
|
||||||
/api/v1/delete/recipient_map:
|
/api/v1/delete/recipient_map:
|
||||||
post:
|
post:
|
||||||
responses:
|
responses:
|
||||||
@@ -3330,6 +3431,7 @@ paths:
|
|||||||
- mailbox
|
- mailbox
|
||||||
- active: "1"
|
- active: "1"
|
||||||
force_pw_update: "0"
|
force_pw_update: "0"
|
||||||
|
force_tfa: "0"
|
||||||
name: Full name
|
name: Full name
|
||||||
password: "*"
|
password: "*"
|
||||||
password2: "*"
|
password2: "*"
|
||||||
@@ -3380,6 +3482,7 @@ paths:
|
|||||||
attr:
|
attr:
|
||||||
active: "1"
|
active: "1"
|
||||||
force_pw_update: "0"
|
force_pw_update: "0"
|
||||||
|
force_tfa: "0"
|
||||||
name: Full name
|
name: Full name
|
||||||
authsource: mailcow
|
authsource: mailcow
|
||||||
password: ""
|
password: ""
|
||||||
@@ -3403,6 +3506,9 @@ paths:
|
|||||||
force_pw_update:
|
force_pw_update:
|
||||||
description: force user to change password on next login
|
description: force user to change password on next login
|
||||||
type: boolean
|
type: boolean
|
||||||
|
force_tfa:
|
||||||
|
description: force 2FA enrollment at login
|
||||||
|
type: boolean
|
||||||
name:
|
name:
|
||||||
description: Full name of the mailbox user
|
description: Full name of the mailbox user
|
||||||
type: string
|
type: string
|
||||||
@@ -4797,6 +4903,7 @@ paths:
|
|||||||
- active: "1"
|
- active: "1"
|
||||||
attributes:
|
attributes:
|
||||||
force_pw_update: "0"
|
force_pw_update: "0"
|
||||||
|
force_tfa: "0"
|
||||||
mailbox_format: "maildir:"
|
mailbox_format: "maildir:"
|
||||||
quarantine_notification: never
|
quarantine_notification: never
|
||||||
sogo_access: "1"
|
sogo_access: "1"
|
||||||
@@ -4813,6 +4920,7 @@ paths:
|
|||||||
quota: 3221225472
|
quota: 3221225472
|
||||||
quota_used: 0
|
quota_used: 0
|
||||||
rl: false
|
rl: false
|
||||||
|
sender_acl: ["otherbox@doman3.tld", "@aliasdomain.tld"]
|
||||||
spam_aliases: 0
|
spam_aliases: 0
|
||||||
username: info@doman3.tld
|
username: info@doman3.tld
|
||||||
tags: ["tag1", "tag2"]
|
tags: ["tag1", "tag2"]
|
||||||
@@ -5721,6 +5829,7 @@ paths:
|
|||||||
- active: "1"
|
- active: "1"
|
||||||
attributes:
|
attributes:
|
||||||
force_pw_update: "0"
|
force_pw_update: "0"
|
||||||
|
force_tfa: "0"
|
||||||
mailbox_format: "maildir:"
|
mailbox_format: "maildir:"
|
||||||
quarantine_notification: never
|
quarantine_notification: never
|
||||||
sogo_access: "1"
|
sogo_access: "1"
|
||||||
@@ -5738,6 +5847,7 @@ paths:
|
|||||||
quota: 3221225472
|
quota: 3221225472
|
||||||
quota_used: 0
|
quota_used: 0
|
||||||
rl: false
|
rl: false
|
||||||
|
sender_acl: ["otherbox@domain3.tld", "@aliasdomain.tld"]
|
||||||
spam_aliases: 0
|
spam_aliases: 0
|
||||||
username: info@domain3.tld
|
username: info@domain3.tld
|
||||||
tags: ["tag1", "tag2"]
|
tags: ["tag1", "tag2"]
|
||||||
@@ -5760,7 +5870,7 @@ paths:
|
|||||||
response:
|
response:
|
||||||
value:
|
value:
|
||||||
- type: "success"
|
- type: "success"
|
||||||
log: ["cors", "edit", {"allowed_origins": ["*", "mail.mailcow.tld"], "allowed_methods": ["POST", "GET", "DELETE", "PUT"]}]
|
log: ["cors", "edit", {"allowed_origins": ["*", "https://mail.mailcow.tld"], "allowed_methods": ["POST", "GET", "DELETE", "PUT"]}]
|
||||||
msg: "cors_headers_edited"
|
msg: "cors_headers_edited"
|
||||||
description: OK
|
description: OK
|
||||||
headers: { }
|
headers: { }
|
||||||
@@ -5777,7 +5887,7 @@ paths:
|
|||||||
schema:
|
schema:
|
||||||
example:
|
example:
|
||||||
attr:
|
attr:
|
||||||
allowed_origins: ["*", "mail.mailcow.tld"]
|
allowed_origins: ["*", "https://mail.mailcow.tld"]
|
||||||
allowed_methods: ["POST", "GET", "DELETE", "PUT"]
|
allowed_methods: ["POST", "GET", "DELETE", "PUT"]
|
||||||
properties:
|
properties:
|
||||||
attr:
|
attr:
|
||||||
|
|||||||
@@ -29,8 +29,8 @@ header('Content-Type: application/xml');
|
|||||||
<clientConfig version="1.1">
|
<clientConfig version="1.1">
|
||||||
<emailProvider id="<?=$mailcow_hostname; ?>">
|
<emailProvider id="<?=$mailcow_hostname; ?>">
|
||||||
<domain>%EMAILDOMAIN%</domain>
|
<domain>%EMAILDOMAIN%</domain>
|
||||||
<displayName>A mailcow mail server</displayName>
|
<displayName><?=$autodiscover_config['displayName']; ?></displayName>
|
||||||
<displayShortName>mail server</displayShortName>
|
<displayShortName><?=$autodiscover_config['displayShortName']; ?></displayShortName>
|
||||||
|
|
||||||
<incomingServer type="imap">
|
<incomingServer type="imap">
|
||||||
<hostname><?=$autodiscover_config['imap']['server']; ?></hostname>
|
<hostname><?=$autodiscover_config['imap']['server']; ?></hostname>
|
||||||
|
|||||||
+150
-85
@@ -7,6 +7,8 @@ if(file_exists('inc/vars.local.inc.php')) {
|
|||||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.inc.php';
|
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.inc.php';
|
||||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.auth.inc.php';
|
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.auth.inc.php';
|
||||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/sessions.inc.php';
|
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/sessions.inc.php';
|
||||||
|
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.mailbox.inc.php';
|
||||||
|
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.ratelimit.inc.php';
|
||||||
$default_autodiscover_config = $autodiscover_config;
|
$default_autodiscover_config = $autodiscover_config;
|
||||||
$autodiscover_config = array_merge($default_autodiscover_config, $autodiscover_config);
|
$autodiscover_config = array_merge($default_autodiscover_config, $autodiscover_config);
|
||||||
|
|
||||||
@@ -58,101 +60,31 @@ $pdo = new PDO($dsn, $database_user, $database_pass, $opt);
|
|||||||
$iam_provider = identity_provider('init');
|
$iam_provider = identity_provider('init');
|
||||||
$iam_settings = identity_provider('get');
|
$iam_settings = identity_provider('get');
|
||||||
|
|
||||||
$login_user = strtolower(trim($_SERVER['PHP_AUTH_USER']));
|
// Passwordless autodiscover - no authentication required
|
||||||
$login_pass = trim(htmlspecialchars_decode($_SERVER['PHP_AUTH_PW']));
|
// Email will be extracted from the request body
|
||||||
|
$login_user = null;
|
||||||
|
$login_role = null;
|
||||||
|
|
||||||
if (empty($_SERVER['PHP_AUTH_USER']) || empty($_SERVER['PHP_AUTH_PW'])) {
|
header("Content-Type: application/xml");
|
||||||
$json = json_encode(
|
echo '<?xml version="1.0" encoding="utf-8" ?>' . PHP_EOL;
|
||||||
array(
|
|
||||||
"time" => time(),
|
|
||||||
"ua" => $_SERVER['HTTP_USER_AGENT'],
|
|
||||||
"user" => "none",
|
|
||||||
"ip" => $_SERVER['REMOTE_ADDR'],
|
|
||||||
"service" => "Error: must be authenticated"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
$redis->lPush('AUTODISCOVER_LOG', $json);
|
|
||||||
header('WWW-Authenticate: Basic realm="' . $_SERVER['HTTP_HOST'] . '"');
|
|
||||||
header('HTTP/1.0 401 Unauthorized');
|
|
||||||
exit(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
$login_role = check_login($login_user, $login_pass, array('eas' => TRUE));
|
|
||||||
|
|
||||||
if ($login_role === "user") {
|
|
||||||
header("Content-Type: application/xml");
|
|
||||||
echo '<?xml version="1.0" encoding="utf-8" ?>' . PHP_EOL;
|
|
||||||
?>
|
?>
|
||||||
<Autodiscover xmlns="http://schemas.microsoft.com/exchange/autodiscover/responseschema/2006">
|
<Autodiscover xmlns="http://schemas.microsoft.com/exchange/autodiscover/responseschema/2006">
|
||||||
<?php
|
<?php
|
||||||
if(!$data) {
|
if(!$data) {
|
||||||
try {
|
|
||||||
$json = json_encode(
|
|
||||||
array(
|
|
||||||
"time" => time(),
|
|
||||||
"ua" => $_SERVER['HTTP_USER_AGENT'],
|
|
||||||
"user" => $_SERVER['PHP_AUTH_USER'],
|
|
||||||
"ip" => $_SERVER['REMOTE_ADDR'],
|
|
||||||
"service" => "Error: invalid or missing request data"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
$redis->lPush('AUTODISCOVER_LOG', $json);
|
|
||||||
$redis->lTrim('AUTODISCOVER_LOG', 0, 100);
|
|
||||||
}
|
|
||||||
catch (RedisException $e) {
|
|
||||||
$_SESSION['return'][] = array(
|
|
||||||
'type' => 'danger',
|
|
||||||
'msg' => 'Redis: '.$e
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
list($usec, $sec) = explode(' ', microtime());
|
|
||||||
?>
|
|
||||||
<Response>
|
|
||||||
<Error Time="<?=date('H:i:s', $sec) . substr($usec, 0, strlen($usec) - 2);?>" Id="2477272013">
|
|
||||||
<ErrorCode>600</ErrorCode>
|
|
||||||
<Message>Invalid Request</Message>
|
|
||||||
<DebugData />
|
|
||||||
</Error>
|
|
||||||
</Response>
|
|
||||||
</Autodiscover>
|
|
||||||
<?php
|
|
||||||
exit(0);
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
$discover = new SimpleXMLElement($data);
|
|
||||||
$email = $discover->Request->EMailAddress;
|
|
||||||
} catch (Exception $e) {
|
|
||||||
$email = $_SERVER['PHP_AUTH_USER'];
|
|
||||||
}
|
|
||||||
|
|
||||||
$username = trim($email);
|
|
||||||
try {
|
|
||||||
$stmt = $pdo->prepare("SELECT `name` FROM `mailbox` WHERE `username`= :username");
|
|
||||||
$stmt->execute(array(':username' => $username));
|
|
||||||
$MailboxData = $stmt->fetch(PDO::FETCH_ASSOC);
|
|
||||||
}
|
|
||||||
catch(PDOException $e) {
|
|
||||||
die("Failed to determine name from SQL");
|
|
||||||
}
|
|
||||||
if (!empty($MailboxData['name'])) {
|
|
||||||
$displayname = $MailboxData['name'];
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
$displayname = $email;
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
$json = json_encode(
|
$json = json_encode(
|
||||||
array(
|
array(
|
||||||
"time" => time(),
|
"time" => time(),
|
||||||
"ua" => $_SERVER['HTTP_USER_AGENT'],
|
"ua" => $_SERVER['HTTP_USER_AGENT'],
|
||||||
"user" => $_SERVER['PHP_AUTH_USER'],
|
"user" => "none",
|
||||||
"ip" => $_SERVER['REMOTE_ADDR'],
|
"ip" => $_SERVER['REMOTE_ADDR'],
|
||||||
"service" => $autodiscover_config['autodiscoverType']
|
"service" => "Error: invalid or missing request data"
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
$redis->lPush('AUTODISCOVER_LOG', $json);
|
$redis->lPush('AUTODISCOVER_LOG', $json);
|
||||||
$redis->lTrim('AUTODISCOVER_LOG', 0, 100);
|
$redis->lTrim('AUTODISCOVER_LOG', 0, 100);
|
||||||
|
$redis->publish("F2B_CHANNEL", "Autodiscover: Invalid request by " . $_SERVER['REMOTE_ADDR']);
|
||||||
|
error_log("Autodiscover: Invalid request by " . $_SERVER['REMOTE_ADDR']);
|
||||||
}
|
}
|
||||||
catch (RedisException $e) {
|
catch (RedisException $e) {
|
||||||
$_SESSION['return'][] = array(
|
$_SESSION['return'][] = array(
|
||||||
@@ -161,7 +93,143 @@ if ($login_role === "user") {
|
|||||||
);
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if ($autodiscover_config['autodiscoverType'] == 'imap') {
|
list($usec, $sec) = explode(' ', microtime());
|
||||||
|
?>
|
||||||
|
<Response>
|
||||||
|
<Error Time="<?=date('H:i:s', $sec) . substr($usec, 0, strlen($usec) - 2);?>" Id="<?=rand(1000000000, 9999999999);?>">
|
||||||
|
<ErrorCode>600</ErrorCode>
|
||||||
|
<Message>Invalid Request</Message>
|
||||||
|
<DebugData />
|
||||||
|
</Error>
|
||||||
|
</Response>
|
||||||
|
</Autodiscover>
|
||||||
|
<?php
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
$discover = new SimpleXMLElement($data);
|
||||||
|
$email = $discover->Request->EMailAddress;
|
||||||
|
} catch (Exception $e) {
|
||||||
|
// If parsing fails, return error
|
||||||
|
try {
|
||||||
|
$json = json_encode(
|
||||||
|
array(
|
||||||
|
"time" => time(),
|
||||||
|
"ua" => $_SERVER['HTTP_USER_AGENT'],
|
||||||
|
"user" => "none",
|
||||||
|
"ip" => $_SERVER['REMOTE_ADDR'],
|
||||||
|
"service" => "Error: could not parse email from request"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
$redis->lPush('AUTODISCOVER_LOG', $json);
|
||||||
|
$redis->lTrim('AUTODISCOVER_LOG', 0, 100);
|
||||||
|
$redis->publish("F2B_CHANNEL", "Autodiscover: Malformed XML by " . $_SERVER['REMOTE_ADDR']);
|
||||||
|
error_log("Autodiscover: Malformed XML by " . $_SERVER['REMOTE_ADDR']);
|
||||||
|
}
|
||||||
|
catch (RedisException $e) {
|
||||||
|
// Silently fail
|
||||||
|
}
|
||||||
|
list($usec, $sec) = explode(' ', microtime());
|
||||||
|
?>
|
||||||
|
<Response>
|
||||||
|
<Error Time="<?=date('H:i:s', $sec) . substr($usec, 0, strlen($usec) - 2);?>" Id="<?=rand(1000000000, 9999999999);?>">
|
||||||
|
<ErrorCode>600</ErrorCode>
|
||||||
|
<Message>Invalid Request</Message>
|
||||||
|
<DebugData />
|
||||||
|
</Error>
|
||||||
|
</Response>
|
||||||
|
</Autodiscover>
|
||||||
|
<?php
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
$username = trim((string)$email);
|
||||||
|
try {
|
||||||
|
$stmt = $pdo->prepare("SELECT `mailbox`.`name`, `mailbox`.`active` FROM `mailbox`
|
||||||
|
INNER JOIN `domain` ON `mailbox`.`domain` = `domain`.`domain`
|
||||||
|
WHERE `mailbox`.`username` = :username
|
||||||
|
AND `mailbox`.`active` = '1'
|
||||||
|
AND `domain`.`active` = '1'");
|
||||||
|
$stmt->execute(array(':username' => $username));
|
||||||
|
$MailboxData = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
}
|
||||||
|
catch(PDOException $e) {
|
||||||
|
// Database error - return error response with complete XML
|
||||||
|
list($usec, $sec) = explode(' ', microtime());
|
||||||
|
?>
|
||||||
|
<Response>
|
||||||
|
<Error Time="<?=date('H:i:s', $sec) . substr($usec, 0, strlen($usec) - 2);?>" Id="<?=rand(1000000000, 9999999999);?>">
|
||||||
|
<ErrorCode>500</ErrorCode>
|
||||||
|
<Message>Database Error</Message>
|
||||||
|
<DebugData />
|
||||||
|
</Error>
|
||||||
|
</Response>
|
||||||
|
</Autodiscover>
|
||||||
|
<?php
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mailbox not found or not active - return generic error to prevent user enumeration
|
||||||
|
if (empty($MailboxData)) {
|
||||||
|
try {
|
||||||
|
$json = json_encode(
|
||||||
|
array(
|
||||||
|
"time" => time(),
|
||||||
|
"ua" => $_SERVER['HTTP_USER_AGENT'],
|
||||||
|
"user" => $email,
|
||||||
|
"ip" => $_SERVER['REMOTE_ADDR'],
|
||||||
|
"service" => "Error: mailbox not found or inactive"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
$redis->lPush('AUTODISCOVER_LOG', $json);
|
||||||
|
$redis->lTrim('AUTODISCOVER_LOG', 0, 100);
|
||||||
|
$redis->publish("F2B_CHANNEL", "Autodiscover: Invalid mailbox attempt by " . $_SERVER['REMOTE_ADDR']);
|
||||||
|
error_log("Autodiscover: Invalid mailbox attempt by " . $_SERVER['REMOTE_ADDR']);
|
||||||
|
}
|
||||||
|
catch (RedisException $e) {
|
||||||
|
// Silently fail
|
||||||
|
}
|
||||||
|
list($usec, $sec) = explode(' ', microtime());
|
||||||
|
?>
|
||||||
|
<Response>
|
||||||
|
<Error Time="<?=date('H:i:s', $sec) . substr($usec, 0, strlen($usec) - 2);?>" Id="<?=rand(1000000000, 9999999999);?>">
|
||||||
|
<ErrorCode>600</ErrorCode>
|
||||||
|
<Message>Invalid Request</Message>
|
||||||
|
<DebugData />
|
||||||
|
</Error>
|
||||||
|
</Response>
|
||||||
|
</Autodiscover>
|
||||||
|
<?php
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($MailboxData['name'])) {
|
||||||
|
$displayname = $MailboxData['name'];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$displayname = $email;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
$json = json_encode(
|
||||||
|
array(
|
||||||
|
"time" => time(),
|
||||||
|
"ua" => $_SERVER['HTTP_USER_AGENT'],
|
||||||
|
"user" => $email,
|
||||||
|
"ip" => $_SERVER['REMOTE_ADDR'],
|
||||||
|
"service" => $autodiscover_config['autodiscoverType']
|
||||||
|
)
|
||||||
|
);
|
||||||
|
$redis->lPush('AUTODISCOVER_LOG', $json);
|
||||||
|
$redis->lTrim('AUTODISCOVER_LOG', 0, 100);
|
||||||
|
}
|
||||||
|
catch (RedisException $e) {
|
||||||
|
$_SESSION['return'][] = array(
|
||||||
|
'type' => 'danger',
|
||||||
|
'msg' => 'Redis: '.$e
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if ($autodiscover_config['autodiscoverType'] == 'imap') {
|
||||||
?>
|
?>
|
||||||
<Response xmlns="http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a">
|
<Response xmlns="http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a">
|
||||||
<User>
|
<User>
|
||||||
@@ -236,6 +304,3 @@ if ($login_role === "user") {
|
|||||||
}
|
}
|
||||||
?>
|
?>
|
||||||
</Autodiscover>
|
</Autodiscover>
|
||||||
<?php
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
/*!
|
/*!
|
||||||
* Bootstrap-select v1.14.0-beta2 (https://developer.snapappointments.com/bootstrap-select)
|
* Bootstrap-select v1.14.0-beta3 (https://developer.snapappointments.com/bootstrap-select)
|
||||||
*
|
*
|
||||||
* Copyright 2012-2021 SnapAppointments, LLC
|
* Copyright 2012-2022 SnapAppointments, LLC
|
||||||
* Licensed under MIT (https://github.com/snapappointments/bootstrap-select/blob/master/LICENSE)
|
* Licensed under MIT (https://github.com/snapappointments/bootstrap-select/blob/master/LICENSE)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -35,8 +35,7 @@ select.selectpicker {
|
|||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
.bootstrap-select {
|
.bootstrap-select {
|
||||||
width: 220px \0;
|
width: 220px;
|
||||||
/*IE9 and below*/
|
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
.bootstrap-select > .dropdown-toggle {
|
.bootstrap-select > .dropdown-toggle {
|
||||||
|
|||||||
@@ -17,15 +17,15 @@ table.dataTable>tbody>tr.child ul.dtr-details>li {
|
|||||||
}
|
}
|
||||||
table.dataTable.dtr-inline.collapsed>tbody>tr>td.dtr-control:before:hover,
|
table.dataTable.dtr-inline.collapsed>tbody>tr>td.dtr-control:before:hover,
|
||||||
table.dataTable.dtr-inline.collapsed>tbody>tr>th.dtr-control:before:hover {
|
table.dataTable.dtr-inline.collapsed>tbody>tr>th.dtr-control:before:hover {
|
||||||
background-color: #5e5e5e;
|
background-color: var(--mc-dtr-hover-bg);
|
||||||
}
|
}
|
||||||
table.dataTable.dtr-inline.collapsed>tbody>tr>td.dtr-control:before,
|
table.dataTable.dtr-inline.collapsed>tbody>tr>td.dtr-control:before,
|
||||||
table.dataTable.dtr-inline.collapsed>tbody>tr>th.dtr-control:before,
|
table.dataTable.dtr-inline.collapsed>tbody>tr>th.dtr-control:before,
|
||||||
table.dataTable td.dt-control:before {
|
table.dataTable td.dt-control:before {
|
||||||
background-color: #979797 !important;
|
background-color: var(--mc-dtr-bg) !important;
|
||||||
border: 1.5px solid #616161 !important;
|
border: 1.5px solid var(--mc-dtr-border) !important;
|
||||||
border-radius: 2px !important;
|
border-radius: 2px !important;
|
||||||
color: #fff;
|
color: var(--mc-dtr-color);
|
||||||
height: 1em;
|
height: 1em;
|
||||||
width: 1em;
|
width: 1em;
|
||||||
line-height: 1.25em;
|
line-height: 1.25em;
|
||||||
@@ -37,7 +37,7 @@ table.dataTable td.dt-control:before {
|
|||||||
table.dataTable.dtr-inline.collapsed>tbody>tr.parent>td.dtr-control:before,
|
table.dataTable.dtr-inline.collapsed>tbody>tr.parent>td.dtr-control:before,
|
||||||
table.dataTable.dtr-inline.collapsed>tbody>tr.parent>th.dtr-control:before,
|
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: var(--mc-dtr-active-bg) !important;
|
||||||
}
|
}
|
||||||
table.dataTable.table-striped>tbody>tr>td {
|
table.dataTable.table-striped>tbody>tr>td {
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
|
|||||||
@@ -34,11 +34,44 @@
|
|||||||
url('/fonts/noto-sans-v12-latin_greek_cyrillic-700italic.woff') format('woff');
|
url('/fonts/noto-sans-v12-latin_greek_cyrillic-700italic.woff') format('woff');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* theme tokens, override in css/themes/<name>/theme.css */
|
||||||
|
:root {
|
||||||
|
--mc-font-family: "Noto Sans","Helvetica Neue",Helvetica,Arial,sans-serif;
|
||||||
|
--mc-font-size: 10.5pt;
|
||||||
|
--mc-body-bg: #fbfbfb;
|
||||||
|
--mc-primary: #0F688D;
|
||||||
|
--mc-primary-border: #0d526d;
|
||||||
|
--mc-info: #148DBC;
|
||||||
|
--mc-info-border: #127ea8;
|
||||||
|
--mc-border: #dfdfdf;
|
||||||
|
--mc-surface: #fff;
|
||||||
|
--mc-surface-alt: #f7f7f7;
|
||||||
|
--mc-muted: #959595;
|
||||||
|
--mc-hr: #ccc;
|
||||||
|
--mc-progress-bg: #d5d5d5;
|
||||||
|
--mc-placeholder: rgb(137 137 137);
|
||||||
|
--mc-btn-outline-color: #000000;
|
||||||
|
--mc-btn-outline-border: #cfcfcf;
|
||||||
|
--mc-btn-outline-hover-bg: #f0f0f0;
|
||||||
|
--mc-btn-outline-active-bg: #f0f0f0;
|
||||||
|
--mc-btn-light-active-color: #fff;
|
||||||
|
--mc-btn-light-active-bg: #555;
|
||||||
|
--mc-btn-light-active-border: #4d4d4d;
|
||||||
|
--mc-dtr-bg: #979797;
|
||||||
|
--mc-dtr-border: #616161;
|
||||||
|
--mc-dtr-color: #fff;
|
||||||
|
--mc-dtr-hover-bg: #5e5e5e;
|
||||||
|
--mc-dtr-active-bg: #979797;
|
||||||
|
--mc-login-user: #28b62c;
|
||||||
|
--mc-login-domainadmin: #ffc107;
|
||||||
|
--mc-login-admin: #ff4136;
|
||||||
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
background-color: #fbfbfb;
|
background-color: var(--mc-body-bg);
|
||||||
}
|
}
|
||||||
#maxmsgsize { min-width: 80px; }
|
#maxmsgsize { min-width: 80px; }
|
||||||
#slider1 .slider-selection {
|
#slider1 .slider-selection {
|
||||||
@@ -86,17 +119,17 @@ body {
|
|||||||
max-height: 1000px
|
max-height: 1000px
|
||||||
}
|
}
|
||||||
.nav-tabs .nav-link, .nav-tabs .nav-link.disabled, .nav-tabs .nav-link.disabled:hover, .nav-tabs .nav-link.disabled:focus {
|
.nav-tabs .nav-link, .nav-tabs .nav-link.disabled, .nav-tabs .nav-link.disabled:hover, .nav-tabs .nav-link.disabled:focus {
|
||||||
border-color: #dfdfdf;
|
border-color: var(--mc-border);
|
||||||
}
|
}
|
||||||
.nav-tabs .nav-link.active, .nav-tabs .nav-item.show .nav-link {
|
.nav-tabs .nav-link.active, .nav-tabs .nav-item.show .nav-link {
|
||||||
border-color: #dfdfdf;
|
border-color: var(--mc-border);
|
||||||
border-bottom: 1px solid #ffffff;
|
border-bottom: 1px solid var(--mc-surface);
|
||||||
}
|
}
|
||||||
.nav-tabs .nav-link:hover, .nav-tabs .nav-link:focus {
|
.nav-tabs .nav-link:hover, .nav-tabs .nav-link:focus {
|
||||||
border-color: #dfdfdf;
|
border-color: var(--mc-border);
|
||||||
}
|
}
|
||||||
.nav-tabs {
|
.nav-tabs {
|
||||||
border-bottom: 1px solid #dfdfdf;
|
border-bottom: 1px solid var(--mc-border);
|
||||||
}
|
}
|
||||||
.bi {
|
.bi {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
@@ -164,13 +197,13 @@ body.modal-open {
|
|||||||
padding-right: inherit !important;
|
padding-right: inherit !important;
|
||||||
}
|
}
|
||||||
body {
|
body {
|
||||||
font-family: "Noto Sans","Helvetica Neue",Helvetica,Arial,sans-serif;
|
font-family: var(--mc-font-family);
|
||||||
font-size: 10.5pt;
|
font-size: var(--mc-font-size);
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
html {
|
html {
|
||||||
font-family: "Noto Sans","Helvetica Neue",Helvetica,Arial,sans-serif;
|
font-family: var(--mc-font-family);
|
||||||
font-size: 10.5pt;
|
font-size: var(--mc-font-size);
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
#mailcow-alert {
|
#mailcow-alert {
|
||||||
@@ -206,7 +239,7 @@ legend {
|
|||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
.overlay {
|
.overlay {
|
||||||
background: #fff;
|
background: var(--mc-surface);
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 10000;
|
z-index: 10000;
|
||||||
top: 0; right: 0; bottom: 0; left: 0;
|
top: 0; right: 0; bottom: 0; left: 0;
|
||||||
@@ -215,9 +248,6 @@ legend {
|
|||||||
.bootstrap-select.btn-group .no-results {
|
.bootstrap-select.btn-group .no-results {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-secondary {
|
|
||||||
color: rgb(197, 197, 197) !important;
|
|
||||||
}
|
|
||||||
.haveibeenpwned {
|
.haveibeenpwned {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
-webkit-user-select: none;
|
-webkit-user-select: none;
|
||||||
@@ -239,7 +269,7 @@ legend {
|
|||||||
.footer {
|
.footer {
|
||||||
margin-top: 27px;
|
margin-top: 27px;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
color: #959595;
|
color: var(--mc-muted);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
@@ -337,7 +367,7 @@ code {
|
|||||||
overflow-x: auto!important;
|
overflow-x: auto!important;
|
||||||
}
|
}
|
||||||
.well {
|
.well {
|
||||||
border: 1px solid #dfdfdf;
|
border: 1px solid var(--mc-border);
|
||||||
background-color: #f9f9f9;
|
background-color: #f9f9f9;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
}
|
}
|
||||||
@@ -356,47 +386,47 @@ button[aria-expanded='true'] > .caret {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.list-group-details {
|
.list-group-details {
|
||||||
background: #fff;
|
background: var(--mc-surface);
|
||||||
}
|
}
|
||||||
.list-group-header {
|
.list-group-header {
|
||||||
background: #f7f7f7;
|
background: var(--mc-surface-alt);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.bg-primary, .alert-primary, .btn-primary {
|
.bg-primary, .alert-primary, .btn-primary {
|
||||||
background-color: #0F688D !important;
|
background-color: var(--mc-primary) !important;
|
||||||
border-color: #0d526d !important;
|
border-color: var(--mc-primary-border) !important;
|
||||||
}
|
}
|
||||||
.bg-info, .alert-info, .btn-info {
|
.bg-info, .alert-info, .btn-info {
|
||||||
background-color: #148DBC !important;
|
background-color: var(--mc-info) !important;
|
||||||
border-color: #127ea8 !important;
|
border-color: var(--mc-info-border) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-secondary {
|
.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-secondary {
|
||||||
color: rgb(137 137 137)!important;
|
color: var(--mc-placeholder) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.progress {
|
.progress {
|
||||||
height: 16px;
|
height: 16px;
|
||||||
background-color: #d5d5d5;
|
background-color: var(--mc-progress-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.btn-outline-secondary:hover {
|
.btn-outline-secondary:hover {
|
||||||
background-color: #f0f0f0;
|
background-color: var(--mc-btn-outline-hover-bg);
|
||||||
}
|
}
|
||||||
.btn.btn-outline-secondary {
|
.btn.btn-outline-secondary {
|
||||||
color: #000000 !important;
|
color: var(--mc-btn-outline-color) !important;
|
||||||
border-color: #cfcfcf !important;
|
border-color: var(--mc-btn-outline-border) !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: #f0f0f0 !important;
|
background-color: var(--mc-btn-outline-active-bg) !important;
|
||||||
}
|
}
|
||||||
.btn-check:checked+.btn-light, .btn-check:active+.btn-light, .btn-light:active, .btn-light.active, .show>.btn-light.dropdown-toggle {
|
.btn-check:checked+.btn-light, .btn-check:active+.btn-light, .btn-light:active, .btn-light.active, .show>.btn-light.dropdown-toggle {
|
||||||
color: #fff;
|
color: var(--mc-btn-light-active-color);
|
||||||
background-color: #555;
|
background-color: var(--mc-btn-light-active-bg);
|
||||||
background-image: none;
|
background-image: none;
|
||||||
border-color: #4d4d4d;
|
border-color: var(--mc-btn-light-active-border);
|
||||||
}
|
}
|
||||||
.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,
|
.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,
|
||||||
.btn-check:focus+.btn-light, .btn-light:focus {
|
.btn-check:focus+.btn-light, .btn-light:focus {
|
||||||
@@ -412,6 +442,26 @@ button[aria-expanded='true'] > .caret {
|
|||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* role marker on login pages, header icons differ too (color blindness) */
|
||||||
|
.login-card::before {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
height: 5px;
|
||||||
|
background: var(--mc-login-color);
|
||||||
|
border-radius: inherit;
|
||||||
|
border-bottom-left-radius: 0;
|
||||||
|
border-bottom-right-radius: 0;
|
||||||
|
}
|
||||||
|
.login-user {
|
||||||
|
--mc-login-color: var(--mc-login-user);
|
||||||
|
}
|
||||||
|
.login-domainadmin {
|
||||||
|
--mc-login-color: var(--mc-login-domainadmin);
|
||||||
|
}
|
||||||
|
.login-admin {
|
||||||
|
--mc-login-color: var(--mc-login-admin);
|
||||||
|
}
|
||||||
|
|
||||||
.hr-title {
|
.hr-title {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -423,7 +473,7 @@ button[aria-expanded='true'] > .caret {
|
|||||||
.hr-title::after {
|
.hr-title::after {
|
||||||
content: "";
|
content: "";
|
||||||
flex: 1;
|
flex: 1;
|
||||||
border-bottom: 1px solid #ccc;
|
border-bottom: 1px solid var(--mc-hr);
|
||||||
}
|
}
|
||||||
|
|
||||||
.hr-title:not(:empty)::before {
|
.hr-title:not(:empty)::before {
|
||||||
|
|||||||
+3750
-2935
File diff suppressed because it is too large.
Load diff
@@ -1,62 +1,103 @@
|
|||||||
|
/* theme tokens, override in css/themes/<name>/dark.css */
|
||||||
|
:root {
|
||||||
|
--mc-body-bg: #1c1c1e;
|
||||||
|
--mc-body-color: #f2f2f7;
|
||||||
|
--mc-surface: #2c2c2e;
|
||||||
|
--mc-surface-alt: #282828;
|
||||||
|
--mc-text: #ccc;
|
||||||
|
--mc-text-soft: #bbb;
|
||||||
|
--mc-muted: #8e8e93;
|
||||||
|
--mc-border: #555;
|
||||||
|
--mc-tab-border: #444444;
|
||||||
|
--mc-link: #6fc7e9;
|
||||||
|
--mc-link-hover: #3daedb;
|
||||||
|
--mc-btn-bg: #5e5e5e;
|
||||||
|
--mc-btn-border: #4c4c4e;
|
||||||
|
--mc-input-bg: #424242;
|
||||||
|
--mc-input-color: #e2e2e2;
|
||||||
|
--mc-input-border: #999;
|
||||||
|
--mc-dropdown-bg: #424242;
|
||||||
|
--mc-table-bg: #3a3a3a;
|
||||||
|
--mc-table-stripe: #282828;
|
||||||
|
--mc-table-stripe-row: #424242;
|
||||||
|
--mc-table-hover: #343434;
|
||||||
|
--mc-table-border: #5c5c5c;
|
||||||
|
--mc-progress-bg: #555;
|
||||||
|
--mc-placeholder: #d4d4d4;
|
||||||
|
--mc-btn-outline-color: #e0e0e0;
|
||||||
|
--mc-btn-outline-border: #7a7a7a;
|
||||||
|
--mc-btn-outline-hover-bg: #5c5c5c;
|
||||||
|
--mc-btn-outline-active-bg: #7a7a7a;
|
||||||
|
--mc-btn-light-active-color: #f2f2f7;
|
||||||
|
--mc-btn-light-active-bg: #242424;
|
||||||
|
--mc-btn-light-active-border: #1c1c1e;
|
||||||
|
--mc-btn-dark-bg: #242424;
|
||||||
|
--mc-input-disabled-bg: #6e6e6e;
|
||||||
|
--mc-input-disabled-color: #a8a8a8;
|
||||||
|
--mc-list-header-bg: #444;
|
||||||
|
--mc-list-details-bg: #555;
|
||||||
|
--mc-chip-bg: #444;
|
||||||
|
--mc-table-child-bg: #414141;
|
||||||
|
--mc-dtr-bg: #7a7a7a;
|
||||||
|
--mc-dtr-border: #5c5c5c;
|
||||||
|
--mc-dtr-color: #e0e0e0;
|
||||||
|
--mc-dtr-hover-bg: #7a7a7a;
|
||||||
|
--mc-dtr-active-bg: #949494;
|
||||||
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
background-color: #1c1c1e;
|
background-color: var(--mc-body-bg);
|
||||||
color: #f2f2f7;
|
color: var(--mc-body-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.card {
|
.card {
|
||||||
border: 1px solid #2c2c2e;
|
border: 1px solid var(--mc-surface);
|
||||||
background-color: #2c2c2e;
|
background-color: var(--mc-surface);
|
||||||
}
|
}
|
||||||
|
|
||||||
legend {
|
legend {
|
||||||
color: #f2f2f7;
|
color: var(--mc-body-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-header {
|
.card-header {
|
||||||
color: #8e8e93;
|
color: var(--mc-muted);
|
||||||
background-color: #1c1c1e;
|
background-color: var(--mc-body-bg);
|
||||||
border-color: transparent;
|
border-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-body {
|
.card-body {
|
||||||
--bs-card-color: #bbb;
|
--bs-card-color: var(--mc-text-soft);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-secondary, .paginate_button, .page-link, .btn-light {
|
.btn-secondary, .paginate_button, .page-link, .btn-light {
|
||||||
color: #f2f2f7 !important;
|
color: var(--mc-body-color) !important;
|
||||||
background-color: #5e5e5e !important;
|
background-color: var(--mc-btn-bg) !important;
|
||||||
border-color: #4c4c4e !important;
|
border-color: var(--mc-btn-border) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-dark {
|
.btn-dark {
|
||||||
color: #f2f2f7 !important;
|
color: var(--mc-body-color) !important;
|
||||||
background-color: #242424 !important;
|
background-color: var(--mc-btn-dark-bg) !important;
|
||||||
border-color: #1c1c1e !important;
|
border-color: var(--mc-body-bg) !important;
|
||||||
}
|
|
||||||
|
|
||||||
.btn-secondary:focus, .btn-secondary:hover, .btn-group.open .dropdown-toggle.btn-secondary {
|
|
||||||
background-color: #444444;
|
|
||||||
border-color: #4c4c4e !important;
|
|
||||||
color: #f2f2f7;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-check:checked+.btn-secondary, .btn-check:active+.btn-secondary, .btn-secondary:active, .btn-secondary.active, .show>.btn-secondary.dropdown-toggle {
|
.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;
|
border-color: var(--mc-btn-bg) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.alert-secondary {
|
.alert-secondary {
|
||||||
color: #f2f2f7 !important;
|
color: var(--mc-body-color) !important;
|
||||||
background-color: #5e5e5e !important;
|
background-color: var(--mc-btn-bg) !important;
|
||||||
border-color: #4c4c4e !important;
|
border-color: var(--mc-btn-border) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bg-secondary {
|
.bg-secondary {
|
||||||
color: #f2f2f7 !important;
|
color: var(--mc-body-color) !important;
|
||||||
background-color: #5e5e5e !important;
|
background-color: var(--mc-btn-bg) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.alert-secondary, .alert-secondary a, .alert-secondary .alert-link {
|
.alert-secondary, .alert-secondary a, .alert-secondary .alert-link {
|
||||||
color: #f2f2f7;
|
color: var(--mc-body-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-item.active .page-link {
|
.page-item.active .page-link {
|
||||||
@@ -65,18 +106,18 @@ legend {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.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: #5e5e5e;
|
background-color: var(--mc-btn-bg);
|
||||||
border-color: #4c4c4e !important;
|
border-color: var(--mc-btn-border) !important;
|
||||||
color: #f2f2f7;
|
color: var(--mc-body-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-secondary:disabled, .btn-secondary.disabled {
|
.btn-secondary:disabled, .btn-secondary.disabled {
|
||||||
border-color: #5e5e5e !important;
|
border-color: var(--mc-btn-bg) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-content {
|
.modal-content {
|
||||||
--bs-modal-color: #bbb;
|
--bs-modal-color: var(--mc-text-soft);
|
||||||
background-color: #2c2c2e;
|
background-color: var(--mc-surface);
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-header {
|
.modal-header {
|
||||||
@@ -84,7 +125,7 @@ legend {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.modal-title {
|
.modal-title {
|
||||||
color: #bbb;
|
color: var(--mc-text-soft);
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal .btn-close {
|
.modal .btn-close {
|
||||||
@@ -92,12 +133,12 @@ legend {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.navbar.bg-light {
|
.navbar.bg-light {
|
||||||
background-color: #1c1c1e !important;
|
background-color: var(--mc-body-bg) !important;
|
||||||
border-color: #2c2c2e;
|
border-color: var(--mc-surface);
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-link {
|
.nav-link {
|
||||||
color: #8e8e93 !important;
|
color: var(--mc-muted) !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 {
|
||||||
@@ -105,23 +146,23 @@ legend {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.nav-tabs, .nav-tabs .nav-link {
|
.nav-tabs, .nav-tabs .nav-link {
|
||||||
border-color: #444444 !important;
|
border-color: var(--mc-tab-border) !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: #1c1c1e !important;
|
border-bottom-color: var(--mc-body-bg) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card .nav-tabs .nav-link:not(.disabled):hover, .card .nav-tabs .nav-link:not(.disabled):focus, .card .nav-tabs .nav-link.active {
|
.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;
|
border-bottom-color: var(--mc-surface) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table, .table-striped>tbody>tr:nth-of-type(odd)>*, tbody tr {
|
.table, .table-striped>tbody>tr:nth-of-type(odd)>*, tbody tr {
|
||||||
color: #f2f2f7 !important;
|
color: var(--mc-body-color) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dropdown-menu {
|
.dropdown-menu {
|
||||||
background-color: #424242;
|
background-color: var(--mc-dropdown-bg);
|
||||||
border: 1px solid #282828;
|
border: 1px solid #282828;
|
||||||
}
|
}
|
||||||
.dropdown-menu>li>a:focus, .dropdown-menu>li>a:hover {
|
.dropdown-menu>li>a:focus, .dropdown-menu>li>a:hover {
|
||||||
@@ -132,79 +173,79 @@ legend {
|
|||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
.bootstrap-select>.dropdown-toggle.bs-placeholder, .bootstrap-select>.dropdown-toggle.bs-placeholder.btn-secondary {
|
.bootstrap-select>.dropdown-toggle.bs-placeholder, .bootstrap-select>.dropdown-toggle.bs-placeholder.btn-secondary {
|
||||||
color: #d4d4d4 !important;
|
color: var(--mc-placeholder) !important;
|
||||||
}
|
}
|
||||||
tbody tr {
|
tbody tr {
|
||||||
color: #ccc;
|
color: var(--mc-text);
|
||||||
}
|
}
|
||||||
.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: var(--mc-text);
|
||||||
}
|
}
|
||||||
.navbar-default .navbar-nav>.active>a, .navbar-default .navbar-nav>.active>a:focus, .navbar-default .navbar-nav>.active>a:hover {
|
.navbar-default .navbar-nav>.active>a, .navbar-default .navbar-nav>.active>a:focus, .navbar-default .navbar-nav>.active>a:hover {
|
||||||
color: #ccc;
|
color: var(--mc-text);
|
||||||
}
|
}
|
||||||
.list-group-item {
|
.list-group-item {
|
||||||
background-color: #282828;
|
background-color: var(--mc-surface-alt);
|
||||||
border: 1px solid #555;
|
border: 1px solid var(--mc-border);
|
||||||
}
|
}
|
||||||
.table-striped>tbody>tr:nth-of-type(odd) {
|
.table-striped>tbody>tr:nth-of-type(odd) {
|
||||||
background-color: #424242;
|
background-color: var(--mc-table-stripe-row);
|
||||||
}
|
}
|
||||||
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);
|
||||||
}
|
}
|
||||||
.label.label-last-login {
|
.label.label-last-login {
|
||||||
color: #ccc !important;
|
color: var(--mc-text) !important;
|
||||||
background-color: #555 !important;
|
background-color: #555 !important;
|
||||||
}
|
}
|
||||||
.progress {
|
.progress {
|
||||||
background-color: #555;
|
background-color: var(--mc-progress-bg);
|
||||||
}
|
}
|
||||||
.h1, .h2, .h3, .h4, .h5, .h6, h1, h2, h3, h4, h5, h6 {
|
.h1, .h2, .h3, .h4, .h5, .h6, h1, h2, h3, h4, h5, h6 {
|
||||||
color: #ccc;
|
color: var(--mc-text);
|
||||||
}
|
}
|
||||||
div.numberedtextarea-number {
|
div.numberedtextarea-number {
|
||||||
color: #999;
|
color: #999;
|
||||||
}
|
}
|
||||||
.well {
|
.well {
|
||||||
border: 1px solid #555;
|
border: 1px solid var(--mc-border);
|
||||||
background-color: #282828;
|
background-color: var(--mc-surface-alt);
|
||||||
}
|
}
|
||||||
pre {
|
pre {
|
||||||
color: #ccc;
|
color: var(--mc-text);
|
||||||
background-color: #282828;
|
background-color: var(--mc-surface-alt);
|
||||||
border: 1px solid #555;
|
border: 1px solid var(--mc-border);
|
||||||
}
|
}
|
||||||
.form-control {
|
.form-control {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
input.form-control, textarea.form-control {
|
input.form-control, textarea.form-control {
|
||||||
color: #e2e2e2 !important;
|
color: var(--mc-input-color) !important;
|
||||||
background-color: #424242 !important;
|
background-color: var(--mc-input-bg) !important;
|
||||||
border: 1px solid #999;
|
border: 1px solid var(--mc-input-border);
|
||||||
}
|
}
|
||||||
input.form-control:focus, textarea.form-control {
|
input.form-control:focus, textarea.form-control {
|
||||||
background-color: #424242 !important;
|
background-color: var(--mc-input-bg) !important;
|
||||||
}
|
}
|
||||||
input.form-control:disabled, textarea.form-disabled {
|
input.form-control:disabled, textarea.form-disabled {
|
||||||
color: #a8a8a8 !important;
|
color: var(--mc-input-disabled-color) !important;
|
||||||
background-color: #6e6e6e !important;
|
background-color: var(--mc-input-disabled-bg) !important;
|
||||||
}
|
}
|
||||||
.input-group-addon {
|
.input-group-addon {
|
||||||
color: #ccc;
|
color: var(--mc-text);
|
||||||
background-color: #424242 !important;
|
background-color: var(--mc-input-bg) !important;
|
||||||
border: 1px solid #999;
|
border: 1px solid var(--mc-input-border);
|
||||||
}
|
}
|
||||||
.input-group-text {
|
.input-group-text {
|
||||||
color: #ccc;
|
color: var(--mc-text);
|
||||||
background-color: #1c1c1c;
|
background-color: #1c1c1c;
|
||||||
}
|
}
|
||||||
|
|
||||||
.list-group-item {
|
.list-group-item {
|
||||||
color: #ccc;
|
color: var(--mc-text);
|
||||||
}
|
}
|
||||||
.dropdown-item {
|
.dropdown-item {
|
||||||
color: #ccc;
|
color: var(--mc-text);
|
||||||
}
|
}
|
||||||
.dropdown-item:hover {
|
.dropdown-item:hover {
|
||||||
color: #616161 !important;
|
color: #616161 !important;
|
||||||
@@ -214,9 +255,9 @@ input.form-control:disabled, textarea.form-disabled {
|
|||||||
background-color: #007aff;
|
background-color: #007aff;
|
||||||
}
|
}
|
||||||
.form-select {
|
.form-select {
|
||||||
color: #e2e2e2!important;
|
color: var(--mc-input-color)!important;
|
||||||
background-color: #424242!important;
|
background-color: var(--mc-input-bg)!important;
|
||||||
border: 1px solid #999;
|
border: 1px solid var(--mc-input-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
.responsive-tabs .card-header button[data-bs-toggle="collapse"] {
|
.responsive-tabs .card-header button[data-bs-toggle="collapse"] {
|
||||||
@@ -240,15 +281,15 @@ input.form-control:disabled, textarea.form-disabled {
|
|||||||
|
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: #6fc7e9;
|
color: var(--mc-link);
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
a:hover {
|
a:hover {
|
||||||
color: #3daedb;
|
color: var(--mc-link-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
.breadcrumb-item.active {
|
.breadcrumb-item.active {
|
||||||
color: #ccc;
|
color: var(--mc-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -273,11 +314,11 @@ a:hover {
|
|||||||
background-color: #3a3a3a;
|
background-color: #3a3a3a;
|
||||||
}
|
}
|
||||||
.accordion-button {
|
.accordion-button {
|
||||||
color: #bbb;
|
color: var(--mc-text-soft);
|
||||||
background-color: #2c2c2c;
|
background-color: #2c2c2c;
|
||||||
}
|
}
|
||||||
.accordion-button:not(.collapsed) {
|
.accordion-button:not(.collapsed) {
|
||||||
color: #6fc7e9;
|
color: var(--mc-link);
|
||||||
background-color: #2c2c2c;
|
background-color: #2c2c2c;
|
||||||
}
|
}
|
||||||
.accordion-button::after {
|
.accordion-button::after {
|
||||||
@@ -300,15 +341,15 @@ a:hover {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.tag-box {
|
.tag-box {
|
||||||
background-color: #282828;
|
background-color: var(--mc-surface-alt);
|
||||||
border: 1px solid #555;
|
border: 1px solid var(--mc-border);
|
||||||
}
|
}
|
||||||
.tag-input {
|
.tag-input {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
background-color: #282828;
|
background-color: var(--mc-surface-alt);
|
||||||
}
|
}
|
||||||
.tag-add {
|
.tag-add {
|
||||||
color: #ccc;
|
color: var(--mc-text);
|
||||||
}
|
}
|
||||||
.tag-add:hover {
|
.tag-add:hover {
|
||||||
color: #d1d1d1;
|
color: #d1d1d1;
|
||||||
@@ -318,19 +359,19 @@ a:hover {
|
|||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
.btn-outline-secondary:hover {
|
.btn-outline-secondary:hover {
|
||||||
background-color: #5c5c5c;
|
background-color: var(--mc-btn-outline-hover-bg);
|
||||||
}
|
}
|
||||||
.btn.btn-outline-secondary {
|
.btn.btn-outline-secondary {
|
||||||
color: #e0e0e0 !important;
|
color: var(--mc-btn-outline-color) !important;
|
||||||
border-color: #7a7a7a !important;
|
border-color: var(--mc-btn-outline-border) !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: #7a7a7a !important;
|
background-color: var(--mc-btn-outline-active-bg) !important;
|
||||||
}
|
}
|
||||||
.btn-check:checked+.btn-light, .btn-check:active+.btn-light, .btn-light:active, .btn-light.active, .show>.btn-light.dropdown-toggle {
|
.btn-check:checked+.btn-light, .btn-check:active+.btn-light, .btn-light:active, .btn-light.active, .show>.btn-light.dropdown-toggle {
|
||||||
color: #f2f2f7 !important;
|
color: var(--mc-btn-light-active-color) !important;
|
||||||
background-color: #242424 !important;
|
background-color: var(--mc-btn-light-active-bg) !important;
|
||||||
border-color: #1c1c1e !important;
|
border-color: var(--mc-btn-light-active-border) !important;
|
||||||
}
|
}
|
||||||
.btn-input-missing,
|
.btn-input-missing,
|
||||||
.btn-input-missing:hover,
|
.btn-input-missing:hover,
|
||||||
@@ -348,16 +389,16 @@ a:hover {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.list-group-details {
|
.list-group-details {
|
||||||
background: #555;
|
background: var(--mc-list-details-bg);
|
||||||
}
|
}
|
||||||
.list-group-header {
|
.list-group-header {
|
||||||
background: #444;
|
background: var(--mc-list-header-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
span.mail-address-item {
|
span.mail-address-item {
|
||||||
background-color: #444;
|
background-color: var(--mc-chip-bg);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
border: 1px solid #555;
|
border: 1px solid var(--mc-border);
|
||||||
padding: 2px 7px;
|
padding: 2px 7px;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin: 2px 6px 2px 0;
|
margin: 2px 6px 2px 0;
|
||||||
@@ -365,52 +406,52 @@ span.mail-address-item {
|
|||||||
|
|
||||||
table.dataTable.dtr-inline.collapsed>tbody>tr>td.dtr-control:before:hover,
|
table.dataTable.dtr-inline.collapsed>tbody>tr>td.dtr-control:before:hover,
|
||||||
table.dataTable.dtr-inline.collapsed>tbody>tr>th.dtr-control:before:hover {
|
table.dataTable.dtr-inline.collapsed>tbody>tr>th.dtr-control:before:hover {
|
||||||
background-color: #7a7a7a !important;
|
background-color: var(--mc-dtr-hover-bg) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
table.dataTable.dtr-inline.collapsed>tbody>tr>td.dtr-control:before,
|
table.dataTable.dtr-inline.collapsed>tbody>tr>td.dtr-control:before,
|
||||||
table.dataTable.dtr-inline.collapsed>tbody>tr>th.dtr-control:before {
|
table.dataTable.dtr-inline.collapsed>tbody>tr>th.dtr-control:before {
|
||||||
background-color: #7a7a7a !important;
|
background-color: var(--mc-dtr-bg) !important;
|
||||||
border: 1.5px solid #5c5c5c !important;
|
border: 1.5px solid var(--mc-dtr-border) !important;
|
||||||
color: #e0e0e0 !important;
|
color: var(--mc-dtr-color) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
table.dataTable.dtr-inline.collapsed>tbody>tr.parent>td.dtr-control:before,
|
table.dataTable.dtr-inline.collapsed>tbody>tr.parent>td.dtr-control:before,
|
||||||
table.dataTable.dtr-inline.collapsed>tbody>tr.parent>th.dtr-control:before {
|
table.dataTable.dtr-inline.collapsed>tbody>tr.parent>th.dtr-control:before {
|
||||||
background-color: #949494;
|
background-color: var(--mc-dtr-active-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
table.dataTable.dtr-inline.collapsed>tbody>tr>td.child,
|
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>th.child,
|
||||||
table.dataTable.dtr-inline.collapsed>tbody>tr>td.dataTables_empty {
|
table.dataTable.dtr-inline.collapsed>tbody>tr>td.dataTables_empty {
|
||||||
background-color: #414141;
|
background-color: var(--mc-table-child-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
table.table, .table-striped>tbody>tr:nth-of-type(odd)>*, tbody tr {
|
table.table, .table-striped>tbody>tr:nth-of-type(odd)>*, tbody tr {
|
||||||
color: #ccc !important;
|
color: var(--mc-text) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-secondary {
|
.table-secondary {
|
||||||
--bs-table-bg: #282828;
|
--bs-table-bg: #282828;
|
||||||
--bs-table-striped-bg: #343434;
|
--bs-table-striped-bg: #343434;
|
||||||
--bs-table-striped-color: #f2f2f7;
|
--bs-table-striped-color: var(--mc-body-color);
|
||||||
--bs-table-active-bg: #4c4c4c;
|
--bs-table-active-bg: #4c4c4c;
|
||||||
--bs-table-active-color: #f2f2f7;
|
--bs-table-active-color: var(--mc-body-color);
|
||||||
--bs-table-hover-bg: #3a3a3a;
|
--bs-table-hover-bg: #3a3a3a;
|
||||||
--bs-table-hover-color: #f2f2f7;
|
--bs-table-hover-color: var(--mc-body-color);
|
||||||
color: #ccc;
|
color: var(--mc-text);
|
||||||
border-color: #3a3a3a;
|
border-color: #3a3a3a;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-light {
|
.table-light {
|
||||||
--bs-table-bg: #3a3a3a;
|
--bs-table-bg: #3a3a3a;
|
||||||
--bs-table-striped-bg: #444444;
|
--bs-table-striped-bg: #444444;
|
||||||
--bs-table-striped-color: #f2f2f7;
|
--bs-table-striped-color: var(--mc-body-color);
|
||||||
--bs-table-active-bg: #5c5c5c;
|
--bs-table-active-bg: #5c5c5c;
|
||||||
--bs-table-active-color: #f2f2f7;
|
--bs-table-active-color: var(--mc-body-color);
|
||||||
--bs-table-hover-bg: #4c4c4c;
|
--bs-table-hover-bg: #4c4c4c;
|
||||||
--bs-table-hover-color: #f2f2f7;
|
--bs-table-hover-color: var(--mc-body-color);
|
||||||
color: #ccc;
|
color: var(--mc-text);
|
||||||
border-color: #4c4c4c;
|
border-color: #4c4c4c;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -430,23 +471,23 @@ table.table, .table-striped>tbody>tr:nth-of-type(odd)>*, tbody tr {
|
|||||||
|
|
||||||
.table-striped>tbody>tr:nth-of-type(odd)>td,
|
.table-striped>tbody>tr:nth-of-type(odd)>td,
|
||||||
.table-striped>tbody>tr:nth-of-type(odd)>th {
|
.table-striped>tbody>tr:nth-of-type(odd)>th {
|
||||||
background-color: #282828;
|
background-color: var(--mc-table-stripe);
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-hover>tbody>tr:hover {
|
.table-hover>tbody>tr:hover {
|
||||||
background-color: #343434;
|
background-color: var(--mc-table-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
.table>:not(caption)>*>* {
|
.table>:not(caption)>*>* {
|
||||||
border-color: #5c5c5c;
|
border-color: var(--mc-table-border);
|
||||||
--bs-table-color-state:#bbb;
|
--bs-table-color-state:var(--mc-text-soft);
|
||||||
--bs-table-bg: #3a3a3a;
|
--bs-table-bg: var(--mc-table-bg);
|
||||||
}
|
}
|
||||||
.text-muted {
|
.text-muted {
|
||||||
--bs-secondary-color: #8e8e93;
|
--bs-secondary-color: var(--mc-muted);
|
||||||
}
|
}
|
||||||
input::placeholder {
|
input::placeholder {
|
||||||
color: #8e8e93 !important;
|
color: var(--mc-muted) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-select {
|
.form-select {
|
||||||
|
|||||||
@@ -0,0 +1,212 @@
|
|||||||
|
/* servercow theme, dark mode, bundled after mailcow core dark mode */
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--sc-brown-hover: #806b55;
|
||||||
|
--sc-brown-active: #8a745c;
|
||||||
|
--sc-brown-soft: rgba(201, 180, 154, .18);
|
||||||
|
--sc-brown-tint: rgba(201, 180, 154, .08);
|
||||||
|
--sc-link: #c9b49a;
|
||||||
|
--sc-danger-text: #ff8a8a;
|
||||||
|
--sc-ink: #ece7e1;
|
||||||
|
--sc-muted: #a89f94;
|
||||||
|
--sc-placeholder: #8a8177;
|
||||||
|
--sc-bg: #171513;
|
||||||
|
--sc-surface: #211e1b;
|
||||||
|
--sc-cap: #2a2622;
|
||||||
|
--sc-addon: #2a2622;
|
||||||
|
--sc-line: #463f38;
|
||||||
|
--sc-divider: #34302b;
|
||||||
|
--sc-cap-line: #3d3732;
|
||||||
|
--sc-stripe: rgba(255, 255, 255, .025);
|
||||||
|
--sc-hover: rgba(201, 180, 154, .08);
|
||||||
|
--sc-focus-border: #8a7864;
|
||||||
|
--sc-focus: rgba(201, 180, 154, .25);
|
||||||
|
--sc-shadow: 0 0 10px 0 rgba(0, 0, 0, .5);
|
||||||
|
--bs-body-color: #ece7e1;
|
||||||
|
--bs-body-bg: #171513;
|
||||||
|
/* core dark tokens, see css/themes/mailcow-darkmode.css */
|
||||||
|
--mc-body-bg: var(--sc-bg);
|
||||||
|
--mc-body-color: var(--sc-ink);
|
||||||
|
--mc-surface: var(--sc-surface);
|
||||||
|
--mc-surface-alt: var(--sc-cap);
|
||||||
|
--mc-text: var(--sc-ink);
|
||||||
|
--mc-text-soft: var(--sc-ink);
|
||||||
|
--mc-muted: var(--sc-muted);
|
||||||
|
--mc-border: var(--sc-divider);
|
||||||
|
--mc-tab-border: var(--sc-divider);
|
||||||
|
--mc-link: var(--sc-link);
|
||||||
|
--mc-link-hover: #e0cfba;
|
||||||
|
--mc-btn-bg: var(--sc-cap);
|
||||||
|
--mc-btn-border: var(--sc-line);
|
||||||
|
--mc-input-bg: var(--sc-bg);
|
||||||
|
--mc-input-color: var(--sc-ink);
|
||||||
|
--mc-input-border: var(--sc-line);
|
||||||
|
--mc-dropdown-bg: var(--sc-surface);
|
||||||
|
--mc-table-bg: transparent;
|
||||||
|
--mc-table-stripe: var(--sc-stripe);
|
||||||
|
--mc-table-stripe-row: transparent;
|
||||||
|
--mc-table-hover: var(--sc-hover);
|
||||||
|
--mc-table-border: var(--sc-divider);
|
||||||
|
--mc-progress-bg: var(--sc-cap);
|
||||||
|
--mc-placeholder: var(--sc-placeholder);
|
||||||
|
--mc-hr: var(--sc-divider);
|
||||||
|
--mc-btn-outline-color: var(--sc-ink);
|
||||||
|
--mc-btn-outline-border: var(--sc-line);
|
||||||
|
--mc-btn-outline-hover-bg: var(--sc-brown-tint);
|
||||||
|
--mc-btn-outline-active-bg: var(--sc-brown);
|
||||||
|
--mc-btn-light-active-color: #fff;
|
||||||
|
--mc-btn-light-active-bg: var(--sc-brown);
|
||||||
|
--mc-btn-light-active-border: var(--sc-brown);
|
||||||
|
--mc-btn-dark-bg: var(--sc-brown);
|
||||||
|
--mc-input-disabled-bg: var(--sc-cap);
|
||||||
|
--mc-input-disabled-color: var(--sc-muted);
|
||||||
|
--mc-list-header-bg: var(--sc-cap);
|
||||||
|
--mc-list-details-bg: var(--sc-surface);
|
||||||
|
--mc-chip-bg: var(--sc-cap);
|
||||||
|
--mc-table-child-bg: var(--sc-surface);
|
||||||
|
--mc-dtr-bg: var(--sc-cap);
|
||||||
|
--mc-dtr-border: var(--sc-line);
|
||||||
|
--mc-dtr-color: var(--sc-link);
|
||||||
|
--mc-dtr-hover-bg: var(--sc-hover);
|
||||||
|
--mc-dtr-active-bg: var(--sc-brown-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* navbar */
|
||||||
|
.navbar.bg-light {
|
||||||
|
background-color: var(--sc-surface) !important;
|
||||||
|
border-bottom-color: var(--sc-green);
|
||||||
|
}
|
||||||
|
.navbar-light .navbar-nav .nav-link {
|
||||||
|
color: var(--sc-ink) !important;
|
||||||
|
}
|
||||||
|
.navbar-light .navbar-nav .nav-link:hover,
|
||||||
|
.navbar-light .navbar-nav .nav-link:focus,
|
||||||
|
.navbar-light .navbar-nav .nav-link.active,
|
||||||
|
.navbar-light .navbar-nav .show > .nav-link {
|
||||||
|
color: var(--sc-link) !important;
|
||||||
|
}
|
||||||
|
.dropdown-menu {
|
||||||
|
border-color: var(--sc-cap-line);
|
||||||
|
}
|
||||||
|
.dropdown-item:hover, .dropdown-item:focus {
|
||||||
|
color: var(--sc-link) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* cards and modals */
|
||||||
|
.card-header {
|
||||||
|
color: var(--sc-ink);
|
||||||
|
background-color: var(--sc-cap);
|
||||||
|
border-color: var(--sc-cap-line);
|
||||||
|
}
|
||||||
|
.modal-content {
|
||||||
|
background-color: var(--sc-surface);
|
||||||
|
}
|
||||||
|
.modal-header {
|
||||||
|
border-color: var(--sc-cap-line);
|
||||||
|
}
|
||||||
|
.input-group-text {
|
||||||
|
background-color: var(--sc-addon);
|
||||||
|
}
|
||||||
|
.form-control::file-selector-button {
|
||||||
|
color: var(--sc-ink);
|
||||||
|
background-color: var(--sc-cap);
|
||||||
|
border-color: var(--sc-line);
|
||||||
|
}
|
||||||
|
.form-control:hover:not(:disabled):not([readonly])::file-selector-button {
|
||||||
|
color: var(--sc-link);
|
||||||
|
background-color: var(--sc-line);
|
||||||
|
}
|
||||||
|
.form-check-input:not(:checked) {
|
||||||
|
background-color: var(--sc-bg);
|
||||||
|
border-color: var(--sc-line);
|
||||||
|
}
|
||||||
|
kbd {
|
||||||
|
color: var(--sc-ink);
|
||||||
|
background-color: var(--sc-cap);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* buttons */
|
||||||
|
.btn-secondary:hover, .btn-secondary:focus, .btn-light:hover, .btn-light:focus, .page-link:hover {
|
||||||
|
color: var(--sc-link) !important;
|
||||||
|
background-color: var(--sc-line) !important;
|
||||||
|
}
|
||||||
|
.btn-check:checked + .btn-secondary, .btn-secondary.active, .show > .btn-secondary.dropdown-toggle,
|
||||||
|
.page-item.active .page-link {
|
||||||
|
color: #fff !important;
|
||||||
|
background-color: var(--sc-brown) !important;
|
||||||
|
border-color: var(--sc-brown) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* tabs */
|
||||||
|
.nav-tabs .nav-link {
|
||||||
|
color: var(--sc-ink) !important;
|
||||||
|
border-bottom-color: transparent !important;
|
||||||
|
}
|
||||||
|
.nav-tabs .nav-link:not(.disabled):hover, .nav-tabs .nav-link:not(.disabled):focus,
|
||||||
|
.card .nav-tabs .nav-link:not(.disabled):hover, .card .nav-tabs .nav-link:not(.disabled):focus {
|
||||||
|
color: var(--sc-link) !important;
|
||||||
|
border-bottom-color: transparent !important;
|
||||||
|
}
|
||||||
|
.nav-tabs .nav-link.active, .card .nav-tabs .nav-link.active {
|
||||||
|
color: var(--sc-link) !important;
|
||||||
|
border-bottom-color: var(--sc-link) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* semantic text */
|
||||||
|
.text-success {
|
||||||
|
color: var(--sc-green) !important;
|
||||||
|
}
|
||||||
|
.text-warning {
|
||||||
|
color: var(--sc-warning) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* alerts, opaque dark tints (also used for floating notifications) */
|
||||||
|
.alert-primary {
|
||||||
|
color: #e6d9c9 !important;
|
||||||
|
background-color: #3c342b !important;
|
||||||
|
border-color: #5e4f3e !important;
|
||||||
|
}
|
||||||
|
.alert-info {
|
||||||
|
color: #cfe9f6 !important;
|
||||||
|
background-color: #2f3334 !important;
|
||||||
|
border-color: #44535a !important;
|
||||||
|
}
|
||||||
|
.alert-success {
|
||||||
|
color: #cdeea0;
|
||||||
|
background-color: #2e3419;
|
||||||
|
border-color: #405515;
|
||||||
|
}
|
||||||
|
.alert-danger {
|
||||||
|
color: #f9c4c4;
|
||||||
|
background-color: #402a28;
|
||||||
|
border-color: #693a38;
|
||||||
|
}
|
||||||
|
.alert-warning {
|
||||||
|
color: #f5d496;
|
||||||
|
background-color: #3c2f19;
|
||||||
|
border-color: #654815;
|
||||||
|
}
|
||||||
|
.alert-secondary, .alert-light {
|
||||||
|
color: var(--sc-ink) !important;
|
||||||
|
background-color: var(--sc-cap) !important;
|
||||||
|
border-color: var(--sc-divider) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert .btn-close {
|
||||||
|
filter: invert(1) grayscale(100%) brightness(200%);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* slider */
|
||||||
|
.noUi-target {
|
||||||
|
background: var(--sc-bg);
|
||||||
|
border-color: var(--sc-line);
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
.noUi-handle {
|
||||||
|
background: var(--sc-cap);
|
||||||
|
border-color: var(--sc-line);
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
.noUi-handle::before, .noUi-handle::after {
|
||||||
|
background: var(--sc-line);
|
||||||
|
}
|
||||||
@@ -0,0 +1,202 @@
|
|||||||
|
|
||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright [yyyy] [name of copyright owner]
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
Copyright 2020 The Poppins Project Authors (https://github.com/itfoundry/Poppins)
|
||||||
|
|
||||||
|
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||||
|
This license is copied below, and is also available with a FAQ at:
|
||||||
|
http://scripts.sil.org/OFL
|
||||||
|
|
||||||
|
|
||||||
|
-----------------------------------------------------------
|
||||||
|
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||||
|
-----------------------------------------------------------
|
||||||
|
|
||||||
|
PREAMBLE
|
||||||
|
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||||
|
development of collaborative font projects, to support the font creation
|
||||||
|
efforts of academic and linguistic communities, and to provide a free and
|
||||||
|
open framework in which fonts may be shared and improved in partnership
|
||||||
|
with others.
|
||||||
|
|
||||||
|
The OFL allows the licensed fonts to be used, studied, modified and
|
||||||
|
redistributed freely as long as they are not sold by themselves. The
|
||||||
|
fonts, including any derivative works, can be bundled, embedded,
|
||||||
|
redistributed and/or sold with any software provided that any reserved
|
||||||
|
names are not used by derivative works. The fonts and derivatives,
|
||||||
|
however, cannot be released under any other type of license. The
|
||||||
|
requirement for fonts to remain under this license does not apply
|
||||||
|
to any document created using the fonts or their derivatives.
|
||||||
|
|
||||||
|
DEFINITIONS
|
||||||
|
"Font Software" refers to the set of files released by the Copyright
|
||||||
|
Holder(s) under this license and clearly marked as such. This may
|
||||||
|
include source files, build scripts and documentation.
|
||||||
|
|
||||||
|
"Reserved Font Name" refers to any names specified as such after the
|
||||||
|
copyright statement(s).
|
||||||
|
|
||||||
|
"Original Version" refers to the collection of Font Software components as
|
||||||
|
distributed by the Copyright Holder(s).
|
||||||
|
|
||||||
|
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||||
|
or substituting -- in part or in whole -- any of the components of the
|
||||||
|
Original Version, by changing formats or by porting the Font Software to a
|
||||||
|
new environment.
|
||||||
|
|
||||||
|
"Author" refers to any designer, engineer, programmer, technical
|
||||||
|
writer or other person who contributed to the Font Software.
|
||||||
|
|
||||||
|
PERMISSION & CONDITIONS
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||||
|
redistribute, and sell modified and unmodified copies of the Font
|
||||||
|
Software, subject to the following conditions:
|
||||||
|
|
||||||
|
1) Neither the Font Software nor any of its individual components,
|
||||||
|
in Original or Modified Versions, may be sold by itself.
|
||||||
|
|
||||||
|
2) Original or Modified Versions of the Font Software may be bundled,
|
||||||
|
redistributed and/or sold with any software, provided that each copy
|
||||||
|
contains the above copyright notice and this license. These can be
|
||||||
|
included either as stand-alone text files, human-readable headers or
|
||||||
|
in the appropriate machine-readable metadata fields within text or
|
||||||
|
binary files as long as those fields can be easily viewed by the user.
|
||||||
|
|
||||||
|
3) No Modified Version of the Font Software may use the Reserved Font
|
||||||
|
Name(s) unless explicit written permission is granted by the corresponding
|
||||||
|
Copyright Holder. This restriction only applies to the primary font name as
|
||||||
|
presented to the users.
|
||||||
|
|
||||||
|
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||||
|
Software shall not be used to promote, endorse or advertise any
|
||||||
|
Modified Version, except to acknowledge the contribution(s) of the
|
||||||
|
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||||
|
permission.
|
||||||
|
|
||||||
|
5) The Font Software, modified or unmodified, in part or in whole,
|
||||||
|
must be distributed entirely under this license, and must not be
|
||||||
|
distributed under any other license. The requirement for fonts to
|
||||||
|
remain under this license does not apply to any document created
|
||||||
|
using the Font Software.
|
||||||
|
|
||||||
|
TERMINATION
|
||||||
|
This license becomes null and void if any of the above conditions are
|
||||||
|
not met.
|
||||||
|
|
||||||
|
DISCLAIMER
|
||||||
|
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||||
|
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||||
|
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||||
|
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||||
|
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loaded 100 of 545 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user