Compare commits
971 Commits
Author | SHA1 | Date |
---|---|---|
|
d6c3c58f42 | |
|
b050cb9864 | |
|
e176724775 | |
|
8f9ed9e0df | |
|
003eecf131 | |
|
180b9fc8d2 | |
|
5d3491c801 | |
|
c45684b986 | |
|
5c886d2f4e | |
|
9f39af46aa | |
|
7cda9f063f | |
|
5e7583c5e6 | |
|
a1fb962215 | |
|
57d849a51b | |
|
3000da6b88 | |
|
db75cbbcb0 | |
|
22acbb6b57 | |
|
31cb0f7db1 | |
|
6d17b9f504 | |
|
0f337971ff | |
|
6cf2775e7e | |
|
dabf9104ed | |
|
952ddb18fd | |
|
34d990a800 | |
|
020cb21b35 | |
|
525364ba65 | |
|
731fabef58 | |
|
c10be77a1b | |
|
a8bc4e3f37 | |
|
815572f200 | |
|
23fc54f2cf | |
|
11407973b1 | |
|
b9867e3fe0 | |
|
3814c3294f | |
|
9c44b5e546 | |
|
cd635ec813 | |
|
03831149f8 | |
|
521120a448 | |
|
ec8d298c36 | |
|
03580cbf39 | |
|
2b009c71c1 | |
|
b903cf3888 | |
|
cf239dd6b2 | |
|
a0723f60d2 | |
|
da8e496430 | |
|
722134e474 | |
|
cb1a11e551 | |
|
8984509f58 | |
|
0f0d43b253 | |
|
0f6956572e | |
|
29892dc694 | |
|
14265f3de8 | |
|
0863bffdd2 | |
|
3b748a30cc | |
|
5619175108 | |
|
6e9c024b3c | |
|
8cd4ae1e34 | |
|
689856b186 | |
|
7b645303d6 | |
|
408381bddb | |
|
380cdab6fc | |
|
03b7a8d639 | |
|
bf6a61fa2d | |
|
1de47072f8 | |
|
c0c46b7cf5 | |
|
42a91af7ac | |
|
6e1ee638ff | |
|
61c8afa088 | |
|
c873a14127 | |
|
06cce79806 | |
|
0927c5df57 | |
|
e691d2c782 | |
|
67510adb9e | |
|
490d553dfc | |
|
70aab7568e | |
|
f82aba3e26 | |
|
f80940efdc | |
|
6f875398c0 | |
|
7a582afbdc | |
|
38cd376228 | |
|
74bcec45f1 | |
|
9700b3251f | |
|
88b8d50cd5 | |
|
55b0191050 | |
|
33c97fb318 | |
|
23d33ad5a8 | |
|
bd6c98047a | |
|
73d6a29ae1 | |
|
173e39c859 | |
|
c0745c5cde | |
|
1a6f93327e | |
|
3c68a53170 | |
|
e38c27ed67 | |
|
8eaf8bbbde | |
|
e015c7dbca | |
|
58452abcdf | |
|
2cbf0da137 | |
|
97a492b891 | |
|
aabcd10539 | |
|
ee607dc3cc | |
|
1265302a8e | |
|
b5acf56e20 | |
|
9752313d24 | |
|
fe4a418af4 | |
|
e5f03e8526 | |
|
fb60c4a150 | |
|
6b82284a41 | |
|
192f67cd41 | |
|
fd203abd47 | |
|
6b65f0fc74 | |
|
856b3b62f2 | |
|
0372a2150d | |
|
f3322c0577 | |
|
c2bcc4e086 | |
|
6e79c48640 | |
|
d7dfa95e1b | |
|
cf1cc24e33 | |
|
6824a5650f | |
|
73570cc8b5 | |
|
959dcb9980 | |
|
8f28666916 | |
|
3eaa5a626c | |
|
8c79056a94 | |
|
ed076dc23e | |
|
be2286c11c | |
|
0e24c3d300 | |
|
e1d8df6580 | |
|
04a08a7d69 | |
|
3c0c8aa01f | |
|
026b278357 | |
|
4121509ceb | |
|
00ac61f0a4 | |
|
4bb0dbb2f7 | |
|
13b6df74af | |
|
5c025bf865 | |
|
20fc9eaf84 | |
|
22a0479fab | |
|
3510d5617d | |
|
236d627fbd | |
|
99739eada0 | |
|
7bfef57894 | |
|
d9dfe15253 | |
|
3fe8aaa719 | |
|
78a8fac6af | |
|
6986e7758f | |
|
b4a9df76b8 | |
|
d9d958356a | |
|
96f954a4e2 | |
|
44585e1c15 | |
|
c737ff4180 | |
|
025279009d | |
|
a9dc13d567 | |
|
c3ed01c9b5 | |
|
bd0b4a521e | |
|
800a0ace71 | |
|
db97869472 | |
|
f681fcf154 | |
|
db1b5956fc | |
|
bdb07061ed | |
|
428b917579 | |
|
469f959e96 | |
|
b68e189d97 | |
|
028ef22878 | |
|
80dacc015a | |
|
0194c39bd5 | |
|
f53ca24bb0 | |
|
ae46a877d3 | |
|
400939faf6 | |
|
fd0205aafd | |
|
e367a8ce24 | |
|
096e2a41e9 | |
|
e010f08143 | |
|
3d2483ca37 | |
|
535dd23509 | |
|
4336a99c6a | |
|
4cd5f93cdf | |
|
67955779b0 | |
|
26c34b484a | |
|
4021613059 | |
|
e891bf8411 | |
|
f7798d1aac | |
|
d11f00261b | |
|
22cd12f37b | |
|
db2fb12837 | |
|
e808e595eb | |
|
ce6742c676 | |
|
cf3dc584d0 | |
|
62f3603588 | |
|
9fd4aa93e9 | |
|
5bc3d93545 | |
|
c28a6b89f0 | |
|
1233613bea | |
|
0206e0886c | |
|
f6d135fbad | |
|
f7da314dcf | |
|
e6ce5e88f7 | |
|
e5e6418be8 | |
|
6507b53bbb | |
|
0f59d4952b | |
|
7225bd2f55 | |
|
deb2b80352 | |
|
ad9dee92be | |
|
f36bc16ca7 | |
|
bda5f0ed4a | |
|
cbe1c97a82 | |
|
81fcbdd104 | |
|
1a9294b58f | |
|
310c01aac2 | |
|
229303c1f8 | |
|
fc075bc6b7 | |
|
d04f0257c2 | |
|
d11d356803 | |
|
c54750ef8b | |
|
510ef5196b | |
|
04e46f9f5b | |
|
6c0a5028c0 | |
|
791bbeeb39 | |
|
a5b8f1b7f7 | |
|
af267ff706 | |
|
46cc022590 | |
|
f77c65411d | |
|
1052e13af8 | |
|
11e1502b12 | |
|
02afc45a15 | |
|
3e1cfe0d08 | |
|
d20df7d73e | |
|
a8c61daeaf | |
|
1a4f11209a | |
|
04403aaf70 | |
|
7f0dd7d0d7 | |
|
cd29ad883e | |
|
e1cd719a17 | |
|
15bb331a7d | |
|
6f3179bb8d | |
|
29e5b87207 | |
|
4403bc2d18 | |
|
63e92e0897 | |
|
aa4d8b1f47 | |
|
9054ca18be | |
|
38291d123f | |
|
ca64ff2c0b | |
|
dc85f49961 | |
|
5dca4dac81 | |
|
df8775d4c9 | |
|
2bc663dcd5 | |
|
1071bb8230 | |
|
e437810eca | |
|
e8fd34d31f | |
|
6aebb8352e | |
|
d684e0efc0 | |
|
64ac6a8891 | |
|
72e8180c6b | |
|
d62c275004 | |
|
aa7f562761 | |
|
a1f033e4c1 | |
|
58ddc31db6 | |
|
5bf62481d5 | |
|
6ff3f3f044 | |
|
640f535e99 | |
|
05d1a974eb | |
|
99e38d81b1 | |
|
ed7b384e24 | |
|
5439ea1010 | |
|
b719982504 | |
|
8281d3fa55 | |
|
9ba65a572e | |
|
afddcf7f3b | |
|
294569f5c9 | |
|
ef6452cf55 | |
|
9af40eba10 | |
|
1b3a13ca19 | |
|
71cc607de6 | |
|
2ebd8345df | |
|
f5baeb31c1 | |
|
5abda44bc6 | |
|
520d070081 | |
|
86beba6f5a | |
|
f0d9948aee | |
|
8e3d2f7010 | |
|
fc1c5a505d | |
|
18cb06fbc7 | |
|
1af785a94f | |
|
7626becb38 | |
|
5d5e959729 | |
|
49bbdd064e | |
|
9279ee2e76 | |
|
a76e6b32f7 | |
|
4c6f8c4f60 | |
|
826d32413b | |
|
b6799d9fcb | |
|
8782304e8d | |
|
9c55d46bc6 | |
|
099db33e44 | |
|
5c57df4669 | |
|
152431a7d7 | |
|
36fa5dc633 | |
|
814f4aed15 | |
|
e990856629 | |
|
c97afbfa0b | |
|
93b3e0302a | |
|
27c87de4ed | |
|
028ad4ceb9 | |
|
e501642b8e | |
|
7966f010a2 | |
|
b22f74cb59 | |
|
c928948b15 | |
|
606eaad8f7 | |
|
c44281f62d | |
|
1e98784eee | |
|
dd9296ffc2 | |
|
fc0e6b6efb | |
|
68f5fbf65c | |
|
9727e4084f | |
|
5c2f48e94c | |
|
cb098df743 | |
|
b3c54ed07a | |
|
c601eca25d | |
|
48a13255f3 | |
|
08f93c7d58 | |
|
e5c9752681 | |
|
afa1ed1eff | |
|
072cbe62de | |
|
9fe8bfadf3 | |
|
75e4953070 | |
|
de30650dc7 | |
|
690c34bc1d | |
|
4d2e32ee40 | |
|
02b2988beb | |
|
3f1a5af88b | |
|
850fd85d4d | |
|
24acd42589 | |
|
eaa0dea63b | |
|
dd50bbca9b | |
|
f3f5471ef7 | |
|
516c8ea66c | |
|
48310034e5 | |
|
be35a88f8c | |
|
e67b512499 | |
|
0cf59159cd | |
|
e7a929a947 | |
|
dabf4d4383 | |
|
13bdd4ad0b | |
|
3281b97ea9 | |
|
8070db96e9 | |
|
82c80a9682 | |
|
136cc2e3ff | |
|
eefce62f01 | |
|
240b2c63f6 | |
|
355da03fba | |
|
55d57c552d | |
|
a56e5eb2fe | |
|
e7817fab78 | |
|
714b8417f4 | |
|
ffb68c8848 | |
|
7a5be0ccbf | |
|
c2927af554 | |
|
24cea0cf22 | |
|
f4351c119f | |
|
f40b6b5b65 | |
|
bff96eb1ae | |
|
de9564c4c9 | |
|
614aa1e49e | |
|
b368c299d9 | |
|
01a61d4e62 | |
|
174fcd7167 | |
|
dca85f2ffb | |
|
8ad5acb020 | |
|
287118c3a7 | |
|
78621a1f50 | |
|
116859e0ba | |
|
c4827e908c | |
|
41d56a867a | |
|
9f4dd1d172 | |
|
948d23f56d | |
|
50e9a3ec8a | |
|
0dbd6be010 | |
|
2b4189b1a4 | |
|
4bf81975dc | |
|
ea040f4412 | |
|
c246648949 | |
|
125aaa5b7d | |
|
aa7888c37d | |
|
f1e1232849 | |
|
bb7c7bcff6 | |
|
e5cf35aff8 | |
|
65585e286d | |
|
99bcfb8c4b | |
|
d98fd74968 | |
|
7875185e1f | |
|
a8d50955ee | |
|
bfd5329363 | |
|
ea1eb48596 | |
|
f1bb23ba2a | |
|
5160eff294 | |
|
118984dfff | |
|
87214fef70 | |
|
f1f9626b5b | |
|
3a13c93022 | |
|
83bd66db98 | |
|
13175b4e6c | |
|
ecefbf2166 | |
|
a763dda068 | |
|
698b2bf988 | |
|
a71cc759f6 | |
|
802d304579 | |
|
faf8da1365 | |
|
ce546e8a90 | |
|
f4731eecdb | |
|
6704377402 | |
|
827cb00837 | |
|
299a342a62 | |
|
8614d63ace | |
|
77f04d10c7 | |
|
6d8c978d17 | |
|
d55994b66a | |
|
ff4f2ae0b6 | |
|
0b00f15811 | |
|
bed5218550 | |
|
86b67a9a7b | |
|
73370de1f9 | |
|
524aba0964 | |
|
64528d8e0e | |
|
31b5faa729 | |
|
17977a2fff | |
|
3e007eeaae | |
|
a96b209e1b | |
|
05b897f43e | |
|
738dcac60d | |
|
b3bbeee5e2 | |
|
782eae4d4c | |
|
f2f5e212f5 | |
|
ff7102468e | |
|
118cb1017a | |
|
360bb6f306 | |
|
d8e314db1a | |
|
fd14c51f85 | |
|
57a5a9baeb | |
|
65c74c75c7 | |
|
e82f3b3975 | |
|
d7323213b8 | |
|
fbc33da734 | |
|
210815d4cf | |
|
1e672ae349 | |
|
19fabd0e64 | |
|
ef392ef6ba | |
|
046e658984 | |
|
a46db9e0df | |
|
17f3cc3ad8 | |
|
3236a10cf5 | |
|
a4eb6d5f1b | |
|
a09661fc83 | |
|
f52ab69a5b | |
|
9d1b620dcf | |
|
3ebd801b3d | |
|
05181f1888 | |
|
6875baf64c | |
|
0cdb1e638d | |
|
da415e5c6b | |
|
c46a1c1e2f | |
|
b79a1530fb | |
|
a6a7ab45f8 | |
|
c8f69ffe77 | |
|
79982e0e8d | |
|
bc937ed2db | |
|
8ca028eb2e | |
|
df17e6b75e | |
|
f880e1834d | |
|
4dd1b97e38 | |
|
074e3fcd6e | |
|
aeb433cc39 | |
|
eb9d360c0a | |
|
abfad4e025 | |
|
3f40fada1b | |
|
a9871d05b2 | |
|
39e46d2e0b | |
|
e9091cbb8c | |
|
cb340d78e1 | |
|
996b2db514 | |
|
548d7b9833 | |
|
96dbbf4db6 | |
|
4f14462af7 | |
|
1e08b4ece6 | |
|
177ebe26de | |
|
6fd9efc30a | |
|
da72184fda | |
|
6f212a41d8 | |
|
52314d1a35 | |
|
3028a18a37 | |
|
a2b31cb28d | |
|
26a5fcf989 | |
|
509086ef54 | |
|
963510ed22 | |
|
2c0f8cda50 | |
|
50d2671d75 | |
|
b73d879f3c | |
|
725a5fe5b9 | |
|
65ca42ca42 | |
|
b22ff59f7b | |
|
58527857d9 | |
|
6306c4555c | |
|
922603f906 | |
|
f8d45de749 | |
|
b6760e19b7 | |
|
6ce25f38e1 | |
|
5cb7f726bc | |
|
a334f33b35 | |
|
cb1602c2de | |
|
b503271aba | |
|
008e5651f8 | |
|
5e3aab12a7 | |
|
8026b6c874 | |
|
51b80f6fa1 | |
|
75fdeb2843 | |
|
2b1d927de4 | |
|
e5d788497a | |
|
b173e2ef86 | |
|
042676fff7 | |
|
44d53146af | |
|
3d48c2427a | |
|
a9046d8f35 | |
|
b4a1b81aec | |
|
90eb0ea27a | |
|
4f01b9fd25 | |
|
174b5c8f7f | |
|
3912fcb238 | |
|
ef70457a48 | |
|
8c4dbaec4f | |
|
645e8f426c | |
|
bae1d1c047 | |
|
ce4fb069d5 | |
|
9444000d46 | |
|
eacd9ac240 | |
|
cf38d6ca69 | |
|
905993d66e | |
|
b8656763ec | |
|
0d7fe2e347 | |
|
33bd871a63 | |
|
772122b255 | |
|
9fb346751c | |
|
10e560c5b2 | |
|
cb058e91a3 | |
|
ba9f2bc376 | |
|
fb7e234120 | |
|
27e7407407 | |
|
623397d20a | |
|
7d46de33d8 | |
|
8c80cecdfb | |
|
5470b51cc7 | |
|
8e0b1d8aee | |
|
e53f431273 | |
|
8e0ee67108 | |
|
3d82d9af1b | |
|
8a0bd23985 | |
|
2834459b22 | |
|
000894dabd | |
|
494620cdea | |
|
a502eb239d | |
|
caf775093e | |
|
4387e4764f | |
|
3b8e17c21f | |
|
f28e18e676 | |
|
b4bab1d5b9 | |
|
c4d5072e5c | |
|
852bf750ca | |
|
47359c4113 | |
|
15f2c4c769 | |
|
e74af0db89 | |
|
a0174c61e8 | |
|
5b30dce609 | |
|
8f6099e3e4 | |
|
7c44375223 | |
|
72e204f8fd | |
|
b5f5b53e37 | |
|
1c15133a52 | |
|
7c9c2c35f8 | |
|
5ff62d8f22 | |
|
9806e568c0 | |
|
b4bb4e2938 | |
|
4427173a6c | |
|
db66fe33fa | |
|
de7b809229 | |
|
cf5fa96a93 | |
|
a40df1ff87 | |
|
a161aa2c92 | |
|
cad0f25345 | |
|
2ed453a400 | |
|
452d8a686f | |
|
9e76b6ee70 | |
|
45e97b3753 | |
|
ecc16c69e6 | |
|
90f77f6d5c | |
|
0c11cf747a | |
|
6d36475ed3 | |
|
fee6ff43bf | |
|
57cd5ec818 | |
|
02512e0f4f | |
|
555f4a8a6d | |
|
3633766544 | |
|
e98a984417 | |
|
bc9141753f | |
|
1f9f4157a6 | |
|
778a3ed551 | |
|
5ea4305185 | |
|
ef311f22bf | |
|
e202530afb | |
|
85deeaf806 | |
|
825c8a6abe | |
|
cdc8f63b4b | |
|
9db9818ede | |
|
4f7ee669d3 | |
|
77f9947613 | |
|
a8eb3b6ac5 | |
|
575eab1cf0 | |
|
7a23e4fd4e | |
|
77e6124b00 | |
|
b16b276f36 | |
|
a3ddb58566 | |
|
be7252f620 | |
|
4f380debb5 | |
|
f34d3620b1 | |
|
70e99447f9 | |
|
db8af3d1e0 | |
|
7f70b0f703 | |
|
2e10cc8e79 | |
|
047d9143c0 | |
|
047c4aa3a0 | |
|
925b220905 | |
|
6708059227 | |
|
1f3d9d4e1c | |
|
0dcfac8f15 | |
|
ad8b7f0894 | |
|
55f810b23f | |
|
65eddee63e | |
|
a7a0eef125 | |
|
5d35af9d69 | |
|
ea21bca7df | |
|
a3c0737ba8 | |
|
7b57b3392c | |
|
492451bfee | |
|
9747995510 | |
|
ad9112010f | |
|
a4ec2d1d86 | |
|
b7f07951f6 | |
|
0e3363e61c | |
|
4322c98f73 | |
|
edcf789126 | |
|
b985ba4f0e | |
|
67c0405274 | |
|
9b32151ab5 | |
|
a1e8077f45 | |
|
956e4e2aa7 | |
|
b51a659515 | |
|
44a6f09a09 | |
|
4c10525078 | |
|
c9ab8b2eff | |
|
4bf38bf00f | |
|
7c7c67948e | |
|
263cb96786 | |
|
b6e3e7a658 | |
|
ceaf1423f4 | |
|
c2e0a275e1 | |
|
c8620a066d | |
|
9598b503ec | |
|
1ca566f670 | |
|
94f4ec8b96 | |
|
7aab2c55ff | |
|
6abb4d34c1 | |
|
c8ccf080f3 | |
|
0342ae926c | |
|
be08742653 | |
|
528f7da5ef | |
|
7d72ae3449 | |
|
753cde0b85 | |
|
223ba44b61 | |
|
cd02483b19 | |
|
f724662874 | |
|
bee762737e | |
|
83efd3e506 | |
|
2278a6cc73 | |
|
586b60b276 | |
|
f07b9ea304 | |
|
09dca5d76c | |
|
65bb808441 | |
|
83b79edb42 | |
|
b8ec244d92 | |
|
5b924614aa | |
|
43103add47 | |
|
124d5d6bb2 | |
|
58fde558f7 | |
|
8b314acfcf | |
|
1c0eab9893 | |
|
514079fe96 | |
|
c62daa0c59 | |
|
1a05101f50 | |
|
47fb46c837 | |
|
d29580aa02 | |
|
d0fc62ef13 | |
|
b14c0e4c11 | |
|
e26d5b8ba5 | |
|
8987ebca36 | |
|
d3cd21956a | |
|
43ec12f4f0 | |
|
40cf2c85e6 | |
|
6195b7c334 | |
|
b149da28c8 | |
|
e5cb2dd00e | |
|
c9b883dff5 | |
|
ad43253a90 | |
|
979de67c2b | |
|
8416caf798 | |
|
80d9dfe420 | |
|
385570c1e8 | |
|
d82cfc6c62 | |
|
27e9210d52 | |
|
fdf52dcb17 | |
|
1ff220ccf8 | |
|
8a49b50f33 | |
|
1b6e5b7116 | |
|
536ab34955 | |
|
f7369f0611 | |
|
14bc105d43 | |
|
2efb4365bf | |
|
c1b86fc782 | |
|
52e92cc0db | |
|
3af2f636a5 | |
|
6fb967cf79 | |
|
0dab215e27 | |
|
03c49ea1f8 | |
|
6ec136e63f | |
|
bba5671eaf | |
|
88d7593d89 | |
|
bd23b80d45 | |
|
f96e0c4071 | |
|
cbd8e40f14 | |
|
2d2c033ba7 | |
|
496c68d2af | |
|
c505943e8b | |
|
2841c09c1f | |
|
11700d7ecb | |
|
18444bd284 | |
|
9d3a89d362 | |
|
33eb2c8801 | |
|
2d0ab4a1b8 | |
|
2fec7ccd58 | |
|
a835419168 | |
|
052959f435 | |
|
4ce16d1ea4 | |
|
c1c7167ace | |
|
3d538d4f14 | |
|
7969e7116d | |
|
4f58f2caee | |
|
263baa81c0 | |
|
092890b6ab | |
|
db7d7ea288 | |
|
452daf5d5e | |
|
d373164e13 | |
|
cd7715fa0e | |
|
af9c3a8565 | |
|
dd6b8c44a4 | |
|
499273dbb7 | |
|
6612b892b7 | |
|
89cea31475 | |
|
872fa07213 | |
|
36e4ee7738 | |
|
a139eb9bce | |
|
7166696aa2 | |
|
537a7908f1 | |
|
560df58bb4 | |
|
5629d47cb6 | |
|
3fe776ee69 | |
|
37b4ff811d | |
|
7384aab2f4 | |
|
581be02e53 | |
|
71db83efce | |
|
7ae7f25580 | |
|
4ce05d4d4d | |
|
fdb56de0a8 | |
|
df56d73cec | |
|
4ba0e155f3 | |
|
304655f7ff | |
|
6210f06bc0 | |
|
09ae37410e | |
|
351c803623 | |
|
6a027b70e7 | |
|
5d14baa43a | |
|
141b397c82 | |
|
fd853cfc6f | |
|
63f718178e | |
|
cdd2adbc73 | |
|
74baf20feb | |
|
958112af6b | |
|
08d0f9448e | |
|
7bcc8bd3a2 | |
|
0eb2545773 | |
|
714511b0a8 | |
|
cb6a5d4069 | |
|
c9700773f4 | |
|
2229f87d9b | |
|
d360503443 | |
|
838182a8b4 | |
|
967cfedbb3 | |
|
a36645a282 | |
|
3368a70f88 | |
|
cd1715ba52 | |
|
0bc2a16093 | |
|
a21b3cd606 | |
|
1c479684fc | |
|
c9dbc7c7b7 | |
|
c41dc9d8c0 | |
|
1db5841424 | |
|
e53b068902 | |
|
2bd436dfd8 | |
|
d13be25f45 | |
|
6efd9dc5f9 | |
|
f13530d8a1 | |
|
1edd4012e4 | |
|
4390c9855a | |
|
4d53216c05 | |
|
8a86fa491e | |
|
040206859f | |
|
d06119a21d | |
|
c27ad97287 | |
|
b1658c0f83 | |
|
3e6a241c69 | |
|
05b8609073 | |
|
552f09f48a | |
|
97df5c3b9c | |
|
8d9102aa08 | |
|
160dceff3e | |
|
33e5ad2b5c | |
|
998cb642a9 | |
|
07ac195fea | |
|
7d5990bf0f | |
|
4ec982163e | |
|
3c9502f241 | |
|
63cecb2fd8 | |
|
3029a2d33d | |
|
fa0d2a959d | |
|
0ece065cb0 | |
|
f79cac3292 | |
|
7a20a9941e | |
|
24cc960379 | |
|
353df6413f | |
|
fb7e00c158 | |
|
a0567beee5 | |
|
b68eae16e5 | |
|
9a812edee4 | |
|
43d2a6e135 | |
|
5839e22796 | |
|
ee844c81d2 | |
|
b6cb3b026c | |
|
df33ebb2a0 | |
|
d2a6838958 | |
|
96b8054e6b | |
|
dfdd2dadb4 | |
|
d0528b7883 | |
|
f40e682800 | |
|
f4dc01d1ec | |
|
187ddedf96 | |
|
5613134fed | |
|
e454ed4e39 | |
|
1e2125653e | |
|
835a726d2a | |
|
0539cc6d8c | |
|
549ff7d100 | |
|
456b528785 | |
|
003a6342a5 | |
|
fb10764167 | |
|
9e1554f5c7 | |
|
42c82be8f5 | |
|
76ec0e888b | |
|
892c99fa23 | |
|
28da482ef2 | |
|
936f07336c | |
|
224a59ab4b | |
|
96c8e01a3b | |
|
88bd7bff1e | |
|
7075b9f0c0 | |
|
b19666f7e0 | |
|
e663f3db72 | |
|
fd1ffdba80 | |
|
a12538b3a8 | |
|
cdff1ba37b | |
|
f6a51f6b6f | |
|
6c5ab7800e | |
|
7e26a2ab98 | |
|
4e6c398c8c | |
|
d4e829465b | |
|
1ade37312e | |
|
372e381a85 | |
|
374cc64601 | |
|
1cf25572a3 | |
|
ba45f70a30 | |
|
5e56566de6 | |
|
a2ccf7ef03 | |
|
654dbf8198 | |
|
53a5254897 | |
|
45dd0611d9 | |
|
e62069d3db | |
|
5088636d5f | |
|
003d70990e | |
|
c433daf024 | |
|
051d08b499 | |
|
b980e7af29 | |
|
4d0799dead | |
|
d3dca1ddc2 | |
|
9b8039440c | |
|
eea5c9df2f | |
|
aa9aff800c | |
|
0b3b5e230b | |
|
db0d91beb1 | |
|
4758033445 | |
|
1e48fb8cda | |
|
1d8da117d6 | |
|
635fa795d2 | |
|
c1792df819 | |
|
36944f8073 | |
|
4d59cb0351 | |
|
fd7269d455 | |
|
b375e6a250 | |
|
48589d20e2 | |
|
be9cbcf5ac | |
|
b04faddac4 | |
|
e925187dda | |
|
06f380a17a | |
|
67882414e1 | |
|
2b149fb8ea | |
|
3166bd5df5 | |
|
e911452d0c | |
|
deac5ad2fe | |
|
f097267bcd | |
|
161130c116 | |
|
a03b8f28ae | |
|
6d3798ad08 | |
|
70921b8d15 | |
|
b185f83fc3 | |
|
bb9ae02ccc | |
|
880a68d563 | |
|
d52323df2d | |
|
769e08843b | |
|
60af295c0a | |
|
e7fe52a625 | |
|
49c506eed9 | |
|
21fadf6df2 | |
|
5fcccbc97d | |
|
3ef2b6cfa2 | |
|
84b4269c75 | |
|
7692685122 | |
|
b820096656 | |
|
a0c09af67e | |
|
a2d57d43d1 | |
|
eddaf7a975 | |
|
bdd8b4a5ab | |
|
98bc947d00 | |
|
24275ffdbf | |
|
5541f84c3c | |
|
4907f702c8 | |
|
412e1188b0 | |
|
6d6b673cf2 | |
|
a1ffaae3d5 | |
|
f3f6fb8908 | |
|
a1a96bfabb | |
|
df33f1a130 | |
|
4c6a2055c2 | |
|
f09a3df870 | |
|
ea1a412749 | |
|
db82327d9a | |
|
ea1a02bd7d | |
|
1606658cb1 | |
|
54ba66733e | |
|
f6847e6f8c |
120
.drone.yml
120
.drone.yml
|
@ -1,120 +0,0 @@
|
|||
---
|
||||
kind: pipeline
|
||||
name: integration-testing
|
||||
|
||||
platform:
|
||||
os: linux
|
||||
arch: amd64
|
||||
|
||||
clone:
|
||||
disable: true
|
||||
|
||||
steps:
|
||||
- name: prepare-tests
|
||||
pull: default
|
||||
image: timovibritannia/ansible
|
||||
commands:
|
||||
- git clone https://github.com/mailcow/mailcow-integration-tests.git --branch $(curl -sL https://api.github.com/repos/mailcow/mailcow-integration-tests/releases/latest | jq -r '.tag_name') --single-branch .
|
||||
- chmod +x ci.sh
|
||||
- chmod +x ci-ssh.sh
|
||||
- chmod +x ci-piprequierments.sh
|
||||
- ./ci.sh
|
||||
- wget -O group_vars/all/secrets.yml $SECRETS_DOWNLOAD_URL --quiet
|
||||
environment:
|
||||
SECRETS_DOWNLOAD_URL:
|
||||
from_secret: SECRETS_DOWNLOAD_URL
|
||||
VAULT_PW:
|
||||
from_secret: VAULT_PW
|
||||
when:
|
||||
branch:
|
||||
- master
|
||||
- staging
|
||||
event:
|
||||
- push
|
||||
|
||||
- name: lint
|
||||
pull: default
|
||||
image: timovibritannia/ansible
|
||||
commands:
|
||||
- ansible-lint ./
|
||||
when:
|
||||
branch:
|
||||
- master
|
||||
- staging
|
||||
event:
|
||||
- push
|
||||
|
||||
- name: create-server
|
||||
pull: default
|
||||
image: timovibritannia/ansible
|
||||
commands:
|
||||
- ./ci-piprequierments.sh
|
||||
- ansible-playbook mailcow-start-server.yml --diff
|
||||
- ./ci-ssh.sh
|
||||
environment:
|
||||
ANSIBLE_HOST_KEY_CHECKING: false
|
||||
ANSIBLE_FORCE_COLOR: true
|
||||
when:
|
||||
branch:
|
||||
- master
|
||||
- staging
|
||||
event:
|
||||
- push
|
||||
|
||||
- name: setup-server
|
||||
pull: default
|
||||
image: timovibritannia/ansible
|
||||
commands:
|
||||
- sleep 120
|
||||
- ./ci-piprequierments.sh
|
||||
- ansible-playbook mailcow-setup-server.yml --private-key /drone/src/id_ssh_rsa --diff
|
||||
environment:
|
||||
ANSIBLE_HOST_KEY_CHECKING: false
|
||||
ANSIBLE_FORCE_COLOR: true
|
||||
when:
|
||||
branch:
|
||||
- master
|
||||
- staging
|
||||
event:
|
||||
- push
|
||||
|
||||
- name: run-tests
|
||||
pull: default
|
||||
image: timovibritannia/ansible
|
||||
commands:
|
||||
- ./ci-piprequierments.sh
|
||||
- ansible-playbook mailcow-integration-tests.yml --private-key /drone/src/id_ssh_rsa --diff
|
||||
environment:
|
||||
ANSIBLE_HOST_KEY_CHECKING: false
|
||||
ANSIBLE_FORCE_COLOR: true
|
||||
when:
|
||||
branch:
|
||||
- master
|
||||
- staging
|
||||
event:
|
||||
- push
|
||||
|
||||
- name: delete-server
|
||||
pull: default
|
||||
image: timovibritannia/ansible
|
||||
commands:
|
||||
- ./ci-piprequierments.sh
|
||||
- ansible-playbook mailcow-delete-server.yml --diff
|
||||
environment:
|
||||
ANSIBLE_HOST_KEY_CHECKING: false
|
||||
ANSIBLE_FORCE_COLOR: true
|
||||
when:
|
||||
branch:
|
||||
- master
|
||||
- staging
|
||||
event:
|
||||
- push
|
||||
status:
|
||||
- failure
|
||||
- success
|
||||
|
||||
---
|
||||
kind: signature
|
||||
hmac: f6619243fe2a27563291c9f2a46d93ffbc3b6dced9a05f23e64b555ce03a31e5
|
||||
|
||||
...
|
|
@ -1 +1 @@
|
|||
custom: https://mailcow.github.io/mailcow-dockerized-docs/#help-mailcow
|
||||
custom: ["https://www.servercow.de/mailcow?lang=en#sal"]
|
||||
|
|
|
@ -1,94 +0,0 @@
|
|||
name: 🐞 Bug Report
|
||||
description: Report a reproducible bug for mailcow. (NOT to be used for support questions.)
|
||||
labels: ["bug"]
|
||||
body:
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Contribution guidelines
|
||||
description: Please read the contribution guidelines before proceeding.
|
||||
options:
|
||||
- label: I've read the [contribution guidelines](https://github.com/mailcow/mailcow-dockerized/blob/master/CONTRIBUTING.md) and wholeheartedly agree
|
||||
required: true
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: I've found a bug and checked that ...
|
||||
description: Prior to placing the issue, please check following:** *(fill out each checkbox with an `X` once done)*
|
||||
options:
|
||||
- label: ... I understand that not following the below instructions will result in immediate closure and/or deletion of my issue.
|
||||
required: true
|
||||
- label: ... I have understood that this bug report is dedicated for bugs, and not for support-related inquiries.
|
||||
required: true
|
||||
- label: ... I have understood that answers are voluntary and community-driven, and not commercial support.
|
||||
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).
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
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.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Logs
|
||||
description: Please take a look at the [official documentation](https://mailcow.github.io/mailcow-dockerized-docs/debug-logs/) and post the last few lines of logs, when the error occurs. For example, docker container logs of affected containers. This will be automatically formatted into code, so no need for backticks.
|
||||
render: bash
|
||||
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:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: System information
|
||||
description: In this stage we would kindly ask you to attach general system information about your setup.
|
||||
value: |-
|
||||
| Question | Answer |
|
||||
| --- | --- |
|
||||
| My operating system | I_DO_REPLY_HERE |
|
||||
| Is Apparmor, SELinux or similar active? | I_DO_REPLY_HERE |
|
||||
| Virtualization technlogy (KVM, VMware, Xen, etc - **LXC and OpenVZ are not supported** | I_DO_REPLY_HERE |
|
||||
| Server/VM specifications (Memory, CPU Cores) | I_DO_REPLY_HERE |
|
||||
| Docker Version (`docker version`) | I_DO_REPLY_HERE |
|
||||
| Docker-Compose Version (`docker-compose version`) | I_DO_REPLY_HERE |
|
||||
| Reverse proxy (custom solution) | I_DO_REPLY_HERE |
|
||||
|
||||
Output of `git diff origin/master`, any other changes to the code? If so, **please post them**:
|
||||
```
|
||||
YOUR OUTPUT GOES HERE
|
||||
```
|
||||
|
||||
All third-party firewalls and custom iptables rules are unsupported. **Please check the Docker docs about how to use Docker with your own ruleset**. Nevertheless, iptabels output can help us to help you:
|
||||
iptables -L -vn:
|
||||
```
|
||||
YOUR OUTPUT GOES HERE
|
||||
```
|
||||
|
||||
ip6tables -L -vn:
|
||||
```
|
||||
YOUR OUTPUT GOES HERE
|
||||
```
|
||||
|
||||
iptables -L -vn -t nat:
|
||||
```
|
||||
YOUR OUTPUT GOES HERE
|
||||
```
|
||||
|
||||
ip6tables -L -vn -t nat:
|
||||
```
|
||||
YOUR OUTPUT GOES HERE
|
||||
```
|
||||
|
||||
DNS problems? Please run `docker exec -it $(docker ps -qf name=acme-mailcow) dig +short stackoverflow.com @172.22.1.254` (set the IP accordingly, if you changed the internal mailcow network) and post the output:
|
||||
```
|
||||
YOUR OUTPUT GOES HERE
|
||||
```
|
||||
validations:
|
||||
required: true
|
|
@ -0,0 +1,157 @@
|
|||
name: 🐞 Bug Report
|
||||
description: Report a reproducible bug for mailcow. (NOT to be used for support questions.)
|
||||
labels: ["bug"]
|
||||
body:
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Contribution guidelines
|
||||
description: Please read the contribution guidelines before proceeding.
|
||||
options:
|
||||
- label: I've read the [contribution guidelines](https://github.com/mailcow/mailcow-dockerized/blob/master/CONTRIBUTING.md) and wholeheartedly agree
|
||||
required: true
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: I've found a bug and checked that ...
|
||||
description: Prior to placing the issue, please check following:** *(fill out each checkbox with an `X` once done)*
|
||||
options:
|
||||
- label: ... I understand that not following the below instructions will result in immediate closure and/or deletion of my issue.
|
||||
required: true
|
||||
- label: ... I have understood that this bug report is dedicated for bugs, and not for support-related inquiries.
|
||||
required: true
|
||||
- label: ... I have understood that answers are voluntary and community-driven, and not commercial support.
|
||||
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).
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
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.
|
||||
render: plain text
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: "Logs:"
|
||||
description: "Please take a look at the [official documentation](https://docs.mailcow.email/troubleshooting/debug-logs/) and post the last few lines of logs, when the error occurs. For example, docker container logs of affected containers. This will be automatically formatted into code, so no need for backticks."
|
||||
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."
|
||||
render: plain text
|
||||
placeholder: |-
|
||||
1. ...
|
||||
2. ...
|
||||
3. ...
|
||||
validations:
|
||||
required: true
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
## System information
|
||||
### In this stage we would kindly ask you to attach general system information about your setup.
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: "Which branch are you using?"
|
||||
description: "#### `git rev-parse --abbrev-ref HEAD`"
|
||||
multiple: false
|
||||
options:
|
||||
- master
|
||||
- nightly
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
attributes:
|
||||
label: "Operating System:"
|
||||
placeholder: "e.g. Ubuntu 22.04 LTS"
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
attributes:
|
||||
label: "Server/VM specifications:"
|
||||
placeholder: "Memory, CPU Cores"
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
attributes:
|
||||
label: "Is Apparmor, SELinux or similar active?"
|
||||
placeholder: "yes/no"
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
attributes:
|
||||
label: "Virtualization technology:"
|
||||
placeholder: "KVM, VMware, Xen, etc - **LXC and OpenVZ are not supported**"
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
attributes:
|
||||
label: "Docker version:"
|
||||
description: "#### `docker version`"
|
||||
placeholder: "20.10.21"
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
attributes:
|
||||
label: "docker-compose version or docker compose version:"
|
||||
description: "#### `docker-compose version` or `docker compose version`"
|
||||
placeholder: "v2.12.2"
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
attributes:
|
||||
label: "mailcow version:"
|
||||
description: "#### ```git describe --tags `git rev-list --tags --max-count=1` ```"
|
||||
placeholder: "2022-08"
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
attributes:
|
||||
label: "Reverse proxy:"
|
||||
placeholder: "e.g. Nginx/Traefik"
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: "Logs of git diff:"
|
||||
description: "#### Output of `git diff origin/master`, any other changes to the code? If so, **please post them**:"
|
||||
render: plain text
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: "Logs of iptables -L -vn:"
|
||||
description: "#### Output of `iptables -L -vn`"
|
||||
render: plain text
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: "Logs of ip6tables -L -vn:"
|
||||
description: "#### Output of `ip6tables -L -vn`"
|
||||
render: plain text
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: "Logs of iptables -L -vn -t nat:"
|
||||
description: "#### Output of `iptables -L -vn -t nat`"
|
||||
render: plain text
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: "Logs of ip6tables -L -vn -t nat:"
|
||||
description: "#### Output of `ip6tables -L -vn -t nat`"
|
||||
render: plain text
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: "DNS check:"
|
||||
description: "#### Output of `docker exec -it $(docker ps -qf name=acme-mailcow) dig +short stackoverflow.com @172.22.1.254` (set the IP accordingly, if you changed the internal mailcow network)"
|
||||
render: plain text
|
||||
validations:
|
||||
required: true
|
|
@ -0,0 +1,13 @@
|
|||
## :memo: Brief description
|
||||
<!-- Diff summary - START -->
|
||||
<!-- Diff summary - END -->
|
||||
|
||||
|
||||
## :computer: Commits
|
||||
<!-- Diff commits - START -->
|
||||
<!-- Diff commits - END -->
|
||||
|
||||
|
||||
## :file_folder: Modified files
|
||||
<!-- Diff files - START -->
|
||||
<!-- Diff files - END -->
|
|
@ -0,0 +1,31 @@
|
|||
{
|
||||
"enabled": true,
|
||||
"timezone": "Europe/Berlin",
|
||||
"dependencyDashboard": true,
|
||||
"dependencyDashboardTitle": "Renovate Dashboard",
|
||||
"commitBody": "Signed-off-by: milkmaker <milkmaker@mailcow.de>",
|
||||
"rebaseWhen": "auto",
|
||||
"labels": ["renovate"],
|
||||
"assignees": [
|
||||
"@magiccc"
|
||||
],
|
||||
"baseBranches": ["staging"],
|
||||
"enabledManagers": ["github-actions", "regex", "docker-compose"],
|
||||
"ignorePaths": [
|
||||
"data\/web\/inc\/lib\/vendor\/matthiasmullie\/minify\/**"
|
||||
],
|
||||
"regexManagers": [
|
||||
{
|
||||
"fileMatch": ["^helper-scripts\/nextcloud.sh$"],
|
||||
"matchStrings": [
|
||||
"#\\srenovate:\\sdatasource=(?<datasource>.*?) depName=(?<depName>.*?)( versioning=(?<versioning>.*?))?( extractVersion=(?<extractVersion>.*?))?\\s.*?_VERSION=(?<currentValue>.*)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"fileMatch": ["(^|/)Dockerfile[^/]*$"],
|
||||
"matchStrings": [
|
||||
"#\\srenovate:\\sdatasource=(?<datasource>.*?) depName=(?<depName>.*?)( versioning=(?<versioning>.*?))?\\s(ENV|ARG) .*?_VERSION=(?<currentValue>.*)\\s"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 71 KiB |
|
@ -0,0 +1,33 @@
|
|||
name: Check PRs if on staging
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, edited]
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
is_not_staging:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.pull_request.base.ref != 'staging' #check if the target branch is not staging
|
||||
steps:
|
||||
- name: Send message
|
||||
uses: thollander/actions-comment-pull-request@v2.4.0
|
||||
with:
|
||||
GITHUB_TOKEN: ${{ secrets.CHECKIFPRISSTAGING_ACTION_PAT }}
|
||||
message: |
|
||||
Thanks for contributing!
|
||||
|
||||
I noticed that you didn't select `staging` as your base branch. Please change the base branch to `staging`.
|
||||
See the attached picture on how to change the base branch to `staging`:
|
||||
|
||||

|
||||
|
||||
- name: Fail #we want to see failed checks in the PR
|
||||
if: ${{ success() }} #set exit code to 1 even if commenting somehow failed
|
||||
run: exit 1
|
||||
|
||||
is_staging:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.pull_request.base.ref == 'staging' #check if the target branch is staging
|
||||
steps:
|
||||
- name: Success
|
||||
run: exit 0
|
|
@ -14,7 +14,7 @@ jobs:
|
|||
pull-requests: write
|
||||
steps:
|
||||
- name: Mark/Close Stale Issues and Pull Requests 🗑️
|
||||
uses: actions/stale@v4
|
||||
uses: actions/stale@v8.0.0
|
||||
with:
|
||||
repo-token: ${{ secrets.STALE_ACTION_PAT }}
|
||||
days-before-stale: 60
|
||||
|
@ -30,6 +30,7 @@ jobs:
|
|||
stale-issue-label: "stale"
|
||||
stale-pr-label: "stale"
|
||||
exempt-draft-pr: "true"
|
||||
close-issue-reason: "not_planned"
|
||||
operations-per-run: "250"
|
||||
ascending: "true"
|
||||
#DRY-RUN
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
name: Build mailcow Docker Images
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "master", "staging" ]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read # to fetch code (actions/checkout)
|
||||
|
||||
jobs:
|
||||
docker_image_builds:
|
||||
strategy:
|
||||
matrix:
|
||||
images:
|
||||
- "acme-mailcow"
|
||||
- "clamd-mailcow"
|
||||
- "dockerapi-mailcow"
|
||||
- "dovecot-mailcow"
|
||||
- "netfilter-mailcow"
|
||||
- "olefy-mailcow"
|
||||
- "php-fpm-mailcow"
|
||||
- "postfix-mailcow"
|
||||
- "rspamd-mailcow"
|
||||
- "sogo-mailcow"
|
||||
- "solr-mailcow"
|
||||
- "unbound-mailcow"
|
||||
- "watchdog-mailcow"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Setup Docker
|
||||
run: |
|
||||
curl -sSL https://get.docker.com/ | CHANNEL=stable sudo sh
|
||||
sudo service docker start
|
||||
- name: Prepair Image Builds
|
||||
run: |
|
||||
cp helper-scripts/docker-compose.override.yml.d/BUILD_FLAGS/docker-compose.override.yml docker-compose.override.yml
|
||||
- name: Build Docker Images
|
||||
run: |
|
||||
docker compose build ${image}
|
||||
env:
|
||||
image: ${{ matrix.images }}
|
|
@ -0,0 +1,25 @@
|
|||
name: Create PR to merge to nightly from staging
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- staging
|
||||
jobs:
|
||||
action-pull-request:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Run the Action
|
||||
uses: devops-infra/action-pull-request@v0.5.5
|
||||
with:
|
||||
github_token: ${{ secrets.PRTONIGHTLY_ACTION_PAT }}
|
||||
title: Automatic PR to nightly from ${{ github.event.repository.updated_at}}
|
||||
assignee: DerLinkman
|
||||
source_branch: staging
|
||||
target_branch: nightly
|
||||
reviewer: DerLinkman
|
||||
label: upstream
|
||||
template: .github/ISSUE_TEMPLATE/pr_to_nighty_template.yml
|
||||
get_diff: true
|
|
@ -0,0 +1,34 @@
|
|||
name: Build mailcow backup image
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# At 00:00 on Sunday
|
||||
- cron: "0 0 * * 0"
|
||||
workflow_dispatch: # Allow to run workflow manually
|
||||
|
||||
jobs:
|
||||
docker_image_build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.BACKUPIMAGEBUILD_ACTION_DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.BACKUPIMAGEBUILD_ACTION_DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
file: data/Dockerfiles/backup/Dockerfile
|
||||
push: true
|
||||
tags: mailcow/backup:latest
|
|
@ -0,0 +1,39 @@
|
|||
name: Update postscreen_access.cidr
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Monthly
|
||||
- cron: "0 0 1 * *"
|
||||
workflow_dispatch: # Allow to run workflow manually
|
||||
|
||||
permissions:
|
||||
contents: read # to fetch code (actions/checkout)
|
||||
|
||||
|
||||
jobs:
|
||||
Update-postscreen_access_cidr:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Generate postscreen_access.cidr
|
||||
run: |
|
||||
bash helper-scripts/update_postscreen_whitelist.sh
|
||||
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v5
|
||||
with:
|
||||
token: ${{ secrets.mailcow_action_Update_postscreen_access_cidr_pat }}
|
||||
commit-message: update postscreen_access.cidr
|
||||
committer: milkmaker <milkmaker@mailcow.de>
|
||||
author: milkmaker <milkmaker@mailcow.de>
|
||||
signoff: false
|
||||
branch: update/postscreen_access.cidr
|
||||
base: staging
|
||||
delete-branch: true
|
||||
add-paths: |
|
||||
data/conf/postfix/postscreen_access.cidr
|
||||
title: '[Postfix] update postscreen_access.cidr'
|
||||
body: |
|
||||
This PR updates the postscreen_access.cidr using GitHub Actions and [helper-scripts/update_postscreen_whitelist.sh](https://github.com/mailcow/mailcow-dockerized/blob/master/helper-scripts/update_postscreen_whitelist.sh)
|
|
@ -36,6 +36,8 @@ data/conf/postfix/extra.cf
|
|||
data/conf/postfix/sni.map
|
||||
data/conf/postfix/sni.map.db
|
||||
data/conf/postfix/sql
|
||||
data/conf/postfix/dns_blocklists.cf
|
||||
data/conf/postfix/dnsbl_reply.map
|
||||
data/conf/rspamd/custom/*
|
||||
data/conf/rspamd/local.d/*
|
||||
data/conf/rspamd/override.d/*
|
||||
|
|
16
.travis.yml
16
.travis.yml
|
@ -1,16 +0,0 @@
|
|||
sudo: required
|
||||
services:
|
||||
- docker
|
||||
script:
|
||||
- echo 'Europe/Berlin' | MAILCOW_HOSTNAME=build.mailcow ./generate_config.sh
|
||||
- docker-compose pull --ignore-pull-failures --parallel
|
||||
- docker-compose build
|
||||
- docker login --username=$DOCKER_HUB_USERNAME --password=$DOCKER_HUB_PASSWORD
|
||||
- docker-compose push
|
||||
branches:
|
||||
only:
|
||||
- master_disabled
|
||||
env:
|
||||
global:
|
||||
- secure: MpxpTwD7f0CNEVLitSpVmocK7O9r+BwFE1deEHK4AlQo/oc9cOlhGe1EL3mx9zbglPmjlDg/8kMUGv6vSirIabfBo9Szjps76bHckFr9lr2Ykkg0e29oC8pgPpSXD1eY/1ZIN/FvIkxpUFLETo1okS/j9q/A0DCGFmti0n3EoMORsgRz9CpNAiEh0zpSd6+euPAGHuczuCrDuO84my9bIOCjA/+aPunHNeXiuM8yIM2SxCSyGtIKT0+jvquIvLF58VxivysXBlRfhDn8fhB09nXA2Ru/derYQACfcmNSn9Pd4bDpebPJW5B9H/XA8xjb58uKinUlncbAMB/QnxoT75j9YRWJZRSQ+34XNYP6ZgK9soZ2TC6djQyEKTUu45Kp/1s+poSn42m9jytJJTmmK0KxsZTRcC8JD5nrjIMZWPUNNTwC5L4+I7ZRWg2WooK3LNyq1Ng8Hn6W77wSgsvAJw2HD3Lx58AprGUhHuBeaIZRuSN9aKwZrl9vKQJLqPnOp/nF2EC6kot5HYYtcotGtETXPUDih21gWD5ZM2BqVqYfQQnJnNMgeYmMdj6QQuTFqhuNJf7hXRIRkTnD3j1gDOLKQZazW0+N2JE8XWDFwi6fKScDsxT85lJti9HmzHa7+k4RVHmUYuDgRoPuzUgjWHvPsiz3/Z8WQ9JYpH84S8w=
|
||||
- secure: fWzZisT6nGDNL4lf6tXB07eFG2drgBakHxzdF/NFVvzuP861RFR6omuL+ED0PgXrEHDJBxaBLv52je8irmUXrAH1CNr7T8DWiZo/h5h609Uzr+38T1NnIu4krL0Wo6/CDwlLKnzqTq9yBIZLQSHVJmo8AOpo1JPIi2ajodqj9ZfmAxDQTQl+G6zvQjtqIkYHsHY7A44Rto0f14ykn7w2S82Jn6Ry89VNI5V1WEO3sMpM/XekNP/HokNcRIuntL/0+kuLvTJ5akGoTjBQxSnSW95opzPeGky74HRU2obExJYqKvF0VfVJRNAqejwjIiFIbbjqV0Sk5391kFuhuBErQQDM1bOHGdxZ41HsJH29qNWIl7C33Yl10qERoqecgsJ1N/bS2ZEmWqm/zQh5GClCXPvYmzEqMYsMGM3vjbKdjDlc1Wh2w/eFclsXN9LSXh1mc35rtj46frcT6e5Kof87AIfC9hTgDvk9kAsyjaHMkSHSZthbZXCIcsD8qriNm5UqfFBYD79mPIP1S2YMQ2jscCsjHOZgYVrcm0kzDF21J1w6H0Lo7d1jw37LYlegBdtLQ9gYgqY2D5m+nxWuVoD5FZmpR+5JGtK+ootyLFF8aiFoHXd4op1JCxRLjgkmnZKXzw3kTQSpE7oa7CgzchtQmK2nqcqla1b5Qk7ilVcjooo=
|
10
README.md
10
README.md
|
@ -1,8 +1,6 @@
|
|||
# mailcow: dockerized - 🐮 + 🐋 = 💕
|
||||
|
||||
## We stand with 🇺🇦
|
||||
|
||||
[](https://drone.mailcow.email/mailcow/mailcow-dockerized) [](https://drone.mailcow.email/mailcow/mailcow-dockerized) [](https://translate.mailcow.email/engage/mailcow-dockerized/)
|
||||
[](https://translate.mailcow.email/engage/mailcow-dockerized/)
|
||||
[](https://twitter.com/mailcow_email)
|
||||
|
||||
## Want to support mailcow?
|
||||
|
@ -35,3 +33,9 @@ Telegram desktop clients are available for [multiple platforms](https://desktop.
|
|||
|
||||
**Important**: mailcow makes use of various open-source software. Please assure you agree with their license before using mailcow.
|
||||
Any part of mailcow itself is released under **GNU General Public License, Version 3**.
|
||||
|
||||
mailcow is a registered word mark of The Infrastructure Company GmbH, Parkstr. 42, 47877 Willich, Germany.
|
||||
|
||||
The project is managed and maintained by The Infrastructure Company GmbH.
|
||||
|
||||
Originated from @andryyy (André)
|
|
@ -0,0 +1,42 @@
|
|||
# Security Policies and Procedures
|
||||
|
||||
This document outlines security procedures and general policies for the _mailcow: dockerized_ project as found on [mailcow-dockerized](https://github.com/mailcow/mailcow-dockerized).
|
||||
|
||||
* [Reporting a Vulnerability](#reporting-a-vulnerability)
|
||||
* [Disclosure Policy](#disclosure-policy)
|
||||
* [Comments on this Policy](#comments-on-this-policy)
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
The mailcow team and community take all security vulnerabilities
|
||||
seriously. Thank you for improving the security of our open source
|
||||
software. We appreciate your efforts and responsible disclosure and will
|
||||
make every effort to acknowledge your contributions.
|
||||
|
||||
Report security vulnerabilities by emailing the mailcow team at:
|
||||
|
||||
info at servercow.de
|
||||
|
||||
mailcow team will acknowledge your email as soon as possible, and will
|
||||
send a more detailed response afterwards indicating the next steps in
|
||||
handling your report. After the initial reply to your report, the mailcow
|
||||
team will endeavor to keep you informed of the progress towards a fix and
|
||||
full announcement, and may ask for additional information or guidance.
|
||||
|
||||
Report security vulnerabilities in third-party modules to the person or
|
||||
team maintaining the module.
|
||||
|
||||
## Disclosure Policy
|
||||
|
||||
When the mailcow team receives a security bug report, they will assign it
|
||||
to a primary handler. This person will coordinate the fix and release
|
||||
process, involving the following steps:
|
||||
|
||||
* Confirm the problem and determine the affected versions.
|
||||
* Audit code to find any potential similar problems.
|
||||
* Prepare fixes for all releases still under maintenance.
|
||||
|
||||
## Comments on this Policy
|
||||
|
||||
If you have suggestions on how this process could be improved please submit a
|
||||
pull request.
|
|
@ -1,6 +1,6 @@
|
|||
FROM alpine:3.15
|
||||
FROM alpine:3.17
|
||||
|
||||
LABEL maintainer "Andre Peters <andre.peters@servercow.de>"
|
||||
LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>"
|
||||
|
||||
RUN apk upgrade --no-cache \
|
||||
&& apk add --update --no-cache \
|
||||
|
|
|
@ -213,11 +213,13 @@ while true; do
|
|||
done
|
||||
ADDITIONAL_WC_ARR+=('autodiscover' 'autoconfig')
|
||||
|
||||
if [[ ${SKIP_IP_CHECK} != "y" ]]; then
|
||||
# Start IP detection
|
||||
log_f "Detecting IP addresses..."
|
||||
IPV4=$(get_ipv4)
|
||||
IPV6=$(get_ipv6)
|
||||
log_f "OK: ${IPV4}, ${IPV6:-"0000:0000:0000:0000:0000:0000:0000:0000"}"
|
||||
fi
|
||||
|
||||
#########################################
|
||||
# IP and webroot challenge verification #
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
FROM debian:bullseye-slim
|
||||
|
||||
RUN apt update && apt install pigz
|
|
@ -1,76 +1,21 @@
|
|||
FROM debian:bullseye-slim
|
||||
FROM clamav/clamav:1.0.1-1_base
|
||||
|
||||
LABEL maintainer "André Peters <andre.peters@servercow.de>"
|
||||
|
||||
ARG CLAMAV=0.104.2
|
||||
ARG TINI_VERSION=v0.19.0
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
ca-certificates \
|
||||
build-essential \
|
||||
pkg-config \
|
||||
python3 \
|
||||
python3-pip \
|
||||
valgrind \
|
||||
check \
|
||||
libbz2-dev \
|
||||
libcurl4-openssl-dev \
|
||||
libjson-c-dev \
|
||||
libmilter-dev \
|
||||
libncurses5-dev \
|
||||
libpcre2-dev \
|
||||
libssl-dev \
|
||||
libxml2-dev \
|
||||
zlib1g-dev \
|
||||
curl \
|
||||
bash \
|
||||
wget \
|
||||
tzdata \
|
||||
dnsutils \
|
||||
RUN apk upgrade --no-cache \
|
||||
&& apk add --update --no-cache \
|
||||
rsync \
|
||||
dos2unix \
|
||||
netcat \
|
||||
&& python3 -m pip install cmake \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& wget -O - https://www.clamav.net/downloads/production/clamav-${CLAMAV}.tar.gz | tar xfvz - \
|
||||
&& cd clamav-${CLAMAV} \
|
||||
&& cmake . \
|
||||
-D CMAKE_INSTALL_PREFIX=/usr \
|
||||
-D CMAKE_INSTALL_LIBDIR=/usr/lib \
|
||||
-D APP_CONFIG_DIRECTORY=/etc/clamav \
|
||||
-D CMAKE_INSTALL_MANDIR=/usr/share/man \
|
||||
-D CMAKE_INSTALL_INFODIR=/usr/share/info \
|
||||
-D CLAMAV_USER=clamav \
|
||||
-D CLAMAV_GROUP=clamav \
|
||||
-D DATABASE_DIRECTORY=/var/lib/clamav \
|
||||
-D ENABLE_APP=ON \
|
||||
-D ENABLE_JSON_SHARED=OFF \
|
||||
-D CMAKE_BUILD_TYPE=MinSizeRel \
|
||||
&& cmake --build . -j4 \
|
||||
&& cmake --build . --target install \
|
||||
&& cd .. && rm -rf clamav-${CLAMAV} \
|
||||
&& apt-get -y --auto-remove purge build-essential \
|
||||
&& apt-get -y purge pkg-config \
|
||||
python3 \
|
||||
python3-pip \
|
||||
valgrind \
|
||||
check \
|
||||
libbz2-dev \
|
||||
libcurl4-openssl-dev \
|
||||
libjson-c-dev \
|
||||
libmilter-dev \
|
||||
libncurses5-dev \
|
||||
libpcre2-dev \
|
||||
libssl-dev \
|
||||
libxml2-dev \
|
||||
zlib1g-dev \
|
||||
bind-tools \
|
||||
bash
|
||||
|
||||
&& addgroup --system --gid 700 clamav \
|
||||
&& adduser --system --no-create-home --home /var/lib/clamav --uid 700 --gid 700 --disabled-login clamav \
|
||||
&& rm -rf /tmp/* /var/tmp/*
|
||||
|
||||
COPY clamd.sh ./
|
||||
ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini /sbin/tini
|
||||
# init
|
||||
COPY clamd.sh /clamd.sh
|
||||
RUN chmod +x /sbin/tini
|
||||
|
||||
CMD ["/sbin/tini", "-g", "--", "/clamd.sh"]
|
||||
# healthcheck
|
||||
COPY healthcheck.sh /healthcheck.sh
|
||||
RUN chmod +x /healthcheck.sh
|
||||
HEALTHCHECK --start-period=6m CMD "/healthcheck.sh"
|
||||
|
||||
ENTRYPOINT []
|
||||
CMD ["/sbin/tini", "-g", "--", "/clamd.sh"]
|
|
@ -0,0 +1,9 @@
|
|||
#!/bin/bash
|
||||
|
||||
if [[ "${SKIP_CLAMD}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
|
||||
echo "SKIP_CLAMD=y, skipping ClamAV..."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# run clamd healthcheck
|
||||
/usr/local/bin/clamdcheck.sh
|
|
@ -1,6 +1,6 @@
|
|||
FROM alpine:3.15
|
||||
FROM alpine:3.17
|
||||
|
||||
LABEL maintainer "Andre Peters <andre.peters@servercow.de>"
|
||||
LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>"
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
|
@ -8,11 +8,18 @@ RUN apk add --update --no-cache python3 \
|
|||
py3-pip \
|
||||
openssl \
|
||||
tzdata \
|
||||
py3-psutil \
|
||||
&& pip3 install --upgrade pip \
|
||||
fastapi \
|
||||
uvicorn \
|
||||
aiodocker \
|
||||
docker \
|
||||
flask \
|
||||
flask-restful
|
||||
aioredis
|
||||
RUN mkdir /app/modules
|
||||
|
||||
COPY dockerapi.py /app/
|
||||
COPY docker-entrypoint.sh /app/
|
||||
COPY main.py /app/main.py
|
||||
COPY modules/ /app/modules/
|
||||
|
||||
CMD ["python3", "-u", "/app/dockerapi.py"]
|
||||
ENTRYPOINT ["/bin/sh", "/app/docker-entrypoint.sh"]
|
||||
CMD exec python main.py
|
|
@ -0,0 +1,9 @@
|
|||
#!/bin/bash
|
||||
|
||||
`openssl req -x509 -newkey rsa:4096 -sha256 -days 3650 -nodes \
|
||||
-keyout /app/dockerapi_key.pem \
|
||||
-out /app/dockerapi_cert.pem \
|
||||
-subj /CN=dockerapi/O=mailcow \
|
||||
-addext subjectAltName=DNS:dockerapi`
|
||||
|
||||
exec "$@"
|
|
@ -1,419 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
from flask import Flask
|
||||
from flask_restful import Resource, Api
|
||||
from flask import jsonify
|
||||
from flask import Response
|
||||
from flask import request
|
||||
from threading import Thread
|
||||
import docker
|
||||
import uuid
|
||||
import signal
|
||||
import time
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import ssl
|
||||
import socket
|
||||
import subprocess
|
||||
import traceback
|
||||
|
||||
docker_client = docker.DockerClient(base_url='unix://var/run/docker.sock', version='auto')
|
||||
app = Flask(__name__)
|
||||
api = Api(app)
|
||||
|
||||
class containers_get(Resource):
|
||||
def get(self):
|
||||
containers = {}
|
||||
try:
|
||||
for container in docker_client.containers.list(all=True):
|
||||
containers.update({container.attrs['Id']: container.attrs})
|
||||
return containers
|
||||
except Exception as e:
|
||||
return jsonify(type='danger', msg=str(e))
|
||||
|
||||
class container_get(Resource):
|
||||
def get(self, container_id):
|
||||
if container_id and container_id.isalnum():
|
||||
try:
|
||||
for container in docker_client.containers.list(all=True, filters={"id": container_id}):
|
||||
return container.attrs
|
||||
except Exception as e:
|
||||
return jsonify(type='danger', msg=str(e))
|
||||
else:
|
||||
return jsonify(type='danger', msg='no or invalid id defined')
|
||||
|
||||
class container_post(Resource):
|
||||
def post(self, container_id, post_action):
|
||||
if container_id and container_id.isalnum() and post_action:
|
||||
try:
|
||||
"""Dispatch container_post api call"""
|
||||
if post_action == 'exec':
|
||||
if not request.json or not 'cmd' in request.json:
|
||||
return jsonify(type='danger', msg='cmd is missing')
|
||||
if not request.json or not 'task' in request.json:
|
||||
return jsonify(type='danger', msg='task is missing')
|
||||
|
||||
api_call_method_name = '__'.join(['container_post', str(post_action), str(request.json['cmd']), str(request.json['task']) ])
|
||||
else:
|
||||
api_call_method_name = '__'.join(['container_post', str(post_action) ])
|
||||
|
||||
api_call_method = getattr(self, api_call_method_name, lambda container_id: jsonify(type='danger', msg='container_post - unknown api call'))
|
||||
|
||||
|
||||
print("api call: %s, container_id: %s" % (api_call_method_name, container_id))
|
||||
return api_call_method(container_id)
|
||||
except Exception as e:
|
||||
print("error - container_post: %s" % str(e))
|
||||
return jsonify(type='danger', msg=str(e))
|
||||
|
||||
else:
|
||||
return jsonify(type='danger', msg='invalid container id or missing action')
|
||||
|
||||
|
||||
# api call: container_post - post_action: stop
|
||||
def container_post__stop(self, container_id):
|
||||
for container in docker_client.containers.list(all=True, filters={"id": container_id}):
|
||||
container.stop()
|
||||
return jsonify(type='success', msg='command completed successfully')
|
||||
|
||||
|
||||
# api call: container_post - post_action: start
|
||||
def container_post__start(self, container_id):
|
||||
for container in docker_client.containers.list(all=True, filters={"id": container_id}):
|
||||
container.start()
|
||||
return jsonify(type='success', msg='command completed successfully')
|
||||
|
||||
|
||||
# api call: container_post - post_action: restart
|
||||
def container_post__restart(self, container_id):
|
||||
for container in docker_client.containers.list(all=True, filters={"id": container_id}):
|
||||
container.restart()
|
||||
return jsonify(type='success', msg='command completed successfully')
|
||||
|
||||
|
||||
# api call: container_post - post_action: top
|
||||
def container_post__top(self, container_id):
|
||||
for container in docker_client.containers.list(all=True, filters={"id": container_id}):
|
||||
return jsonify(type='success', msg=container.top())
|
||||
|
||||
|
||||
# api call: container_post - post_action: stats
|
||||
def container_post__stats(self, container_id):
|
||||
for container in docker_client.containers.list(all=True, filters={"id": container_id}):
|
||||
for stat in container.stats(decode=True, stream=True):
|
||||
return jsonify(type='success', msg=stat )
|
||||
|
||||
|
||||
# api call: container_post - post_action: exec - cmd: mailq - task: delete
|
||||
def container_post__exec__mailq__delete(self, container_id):
|
||||
if 'items' in request.json:
|
||||
r = re.compile("^[0-9a-fA-F]+$")
|
||||
filtered_qids = filter(r.match, request.json['items'])
|
||||
if filtered_qids:
|
||||
flagged_qids = ['-d %s' % i for i in filtered_qids]
|
||||
sanitized_string = str(' '.join(flagged_qids));
|
||||
|
||||
for container in docker_client.containers.list(filters={"id": container_id}):
|
||||
postsuper_r = container.exec_run(["/bin/bash", "-c", "/usr/sbin/postsuper " + sanitized_string])
|
||||
return exec_run_handler('generic', postsuper_r)
|
||||
|
||||
# api call: container_post - post_action: exec - cmd: mailq - task: hold
|
||||
def container_post__exec__mailq__hold(self, container_id):
|
||||
if 'items' in request.json:
|
||||
r = re.compile("^[0-9a-fA-F]+$")
|
||||
filtered_qids = filter(r.match, request.json['items'])
|
||||
if filtered_qids:
|
||||
flagged_qids = ['-h %s' % i for i in filtered_qids]
|
||||
sanitized_string = str(' '.join(flagged_qids));
|
||||
|
||||
for container in docker_client.containers.list(filters={"id": container_id}):
|
||||
postsuper_r = container.exec_run(["/bin/bash", "-c", "/usr/sbin/postsuper " + sanitized_string])
|
||||
return exec_run_handler('generic', postsuper_r)
|
||||
|
||||
# api call: container_post - post_action: exec - cmd: mailq - task: cat
|
||||
def container_post__exec__mailq__cat(self, container_id):
|
||||
if 'items' in request.json:
|
||||
r = re.compile("^[0-9a-fA-F]+$")
|
||||
filtered_qids = filter(r.match, request.json['items'])
|
||||
if filtered_qids:
|
||||
sanitized_string = str(' '.join(filtered_qids));
|
||||
|
||||
for container in docker_client.containers.list(filters={"id": container_id}):
|
||||
postcat_return = container.exec_run(["/bin/bash", "-c", "/usr/sbin/postcat -q " + sanitized_string], user='postfix')
|
||||
if not postcat_return:
|
||||
postcat_return = 'err: invalid'
|
||||
return exec_run_handler('utf8_text_only', postcat_return)
|
||||
|
||||
# api call: container_post - post_action: exec - cmd: mailq - task: unhold
|
||||
def container_post__exec__mailq__unhold(self, container_id):
|
||||
if 'items' in request.json:
|
||||
r = re.compile("^[0-9a-fA-F]+$")
|
||||
filtered_qids = filter(r.match, request.json['items'])
|
||||
if filtered_qids:
|
||||
flagged_qids = ['-H %s' % i for i in filtered_qids]
|
||||
sanitized_string = str(' '.join(flagged_qids));
|
||||
|
||||
for container in docker_client.containers.list(filters={"id": container_id}):
|
||||
postsuper_r = container.exec_run(["/bin/bash", "-c", "/usr/sbin/postsuper " + sanitized_string])
|
||||
return exec_run_handler('generic', postsuper_r)
|
||||
|
||||
|
||||
# api call: container_post - post_action: exec - cmd: mailq - task: deliver
|
||||
def container_post__exec__mailq__deliver(self, container_id):
|
||||
if 'items' in request.json:
|
||||
r = re.compile("^[0-9a-fA-F]+$")
|
||||
filtered_qids = filter(r.match, request.json['items'])
|
||||
if filtered_qids:
|
||||
flagged_qids = ['-i %s' % i for i in filtered_qids]
|
||||
|
||||
for container in docker_client.containers.list(filters={"id": container_id}):
|
||||
for i in flagged_qids:
|
||||
postqueue_r = container.exec_run(["/bin/bash", "-c", "/usr/sbin/postqueue " + i], user='postfix')
|
||||
# todo: check each exit code
|
||||
return jsonify(type='success', msg=str("Scheduled immediate delivery"))
|
||||
|
||||
|
||||
# api call: container_post - post_action: exec - cmd: mailq - task: list
|
||||
def container_post__exec__mailq__list(self, container_id):
|
||||
for container in docker_client.containers.list(filters={"id": container_id}):
|
||||
mailq_return = container.exec_run(["/usr/sbin/postqueue", "-j"], user='postfix')
|
||||
return exec_run_handler('utf8_text_only', mailq_return)
|
||||
|
||||
|
||||
# api call: container_post - post_action: exec - cmd: mailq - task: flush
|
||||
def container_post__exec__mailq__flush(self, container_id):
|
||||
for container in docker_client.containers.list(filters={"id": container_id}):
|
||||
postqueue_r = container.exec_run(["/usr/sbin/postqueue", "-f"], user='postfix')
|
||||
return exec_run_handler('generic', postqueue_r)
|
||||
|
||||
|
||||
# api call: container_post - post_action: exec - cmd: mailq - task: super_delete
|
||||
def container_post__exec__mailq__super_delete(self, container_id):
|
||||
for container in docker_client.containers.list(filters={"id": container_id}):
|
||||
postsuper_r = container.exec_run(["/usr/sbin/postsuper", "-d", "ALL"])
|
||||
return exec_run_handler('generic', postsuper_r)
|
||||
|
||||
|
||||
# api call: container_post - post_action: exec - cmd: system - task: fts_rescan
|
||||
def container_post__exec__system__fts_rescan(self, container_id):
|
||||
if 'username' in request.json:
|
||||
for container in docker_client.containers.list(filters={"id": container_id}):
|
||||
rescan_return = container.exec_run(["/bin/bash", "-c", "/usr/bin/doveadm fts rescan -u '" + request.json['username'].replace("'", "'\\''") + "'"], user='vmail')
|
||||
if rescan_return.exit_code == 0:
|
||||
return jsonify(type='success', msg='fts_rescan: rescan triggered')
|
||||
else:
|
||||
return jsonify(type='warning', msg='fts_rescan error')
|
||||
|
||||
if 'all' in request.json:
|
||||
for container in docker_client.containers.list(filters={"id": container_id}):
|
||||
rescan_return = container.exec_run(["/bin/bash", "-c", "/usr/bin/doveadm fts rescan -A"], user='vmail')
|
||||
if rescan_return.exit_code == 0:
|
||||
return jsonify(type='success', msg='fts_rescan: rescan triggered')
|
||||
else:
|
||||
return jsonify(type='warning', msg='fts_rescan error')
|
||||
|
||||
|
||||
# api call: container_post - post_action: exec - cmd: system - task: df
|
||||
def container_post__exec__system__df(self, container_id):
|
||||
if 'dir' in request.json:
|
||||
for container in docker_client.containers.list(filters={"id": container_id}):
|
||||
df_return = container.exec_run(["/bin/bash", "-c", "/bin/df -H '" + request.json['dir'].replace("'", "'\\''") + "' | /usr/bin/tail -n1 | /usr/bin/tr -s [:blank:] | /usr/bin/tr ' ' ','"], user='nobody')
|
||||
if df_return.exit_code == 0:
|
||||
return df_return.output.decode('utf-8').rstrip()
|
||||
else:
|
||||
return "0,0,0,0,0,0"
|
||||
|
||||
|
||||
# api call: container_post - post_action: exec - cmd: system - task: mysql_upgrade
|
||||
def container_post__exec__system__mysql_upgrade(self, container_id):
|
||||
for container in docker_client.containers.list(filters={"id": container_id}):
|
||||
sql_return = container.exec_run(["/bin/bash", "-c", "/usr/bin/mysql_upgrade -uroot -p'" + os.environ['DBROOT'].replace("'", "'\\''") + "'\n"], user='mysql')
|
||||
if sql_return.exit_code == 0:
|
||||
matched = False
|
||||
for line in sql_return.output.decode('utf-8').split("\n"):
|
||||
if 'is already upgraded to' in line:
|
||||
matched = True
|
||||
if matched:
|
||||
return jsonify(type='success', msg='mysql_upgrade: already upgraded', text=sql_return.output.decode('utf-8'))
|
||||
else:
|
||||
container.restart()
|
||||
return jsonify(type='warning', msg='mysql_upgrade: upgrade was applied', text=sql_return.output.decode('utf-8'))
|
||||
else:
|
||||
return jsonify(type='error', msg='mysql_upgrade: error running command', text=sql_return.output.decode('utf-8'))
|
||||
|
||||
# api call: container_post - post_action: exec - cmd: system - task: mysql_tzinfo_to_sql
|
||||
def container_post__exec__system__mysql_tzinfo_to_sql(self, container_id):
|
||||
for container in docker_client.containers.list(filters={"id": container_id}):
|
||||
sql_return = container.exec_run(["/bin/bash", "-c", "/usr/bin/mysql_tzinfo_to_sql /usr/share/zoneinfo | /bin/sed 's/Local time zone must be set--see zic manual page/FCTY/' | /usr/bin/mysql -uroot -p'" + os.environ['DBROOT'].replace("'", "'\\''") + "' mysql \n"], user='mysql')
|
||||
if sql_return.exit_code == 0:
|
||||
return jsonify(type='info', msg='mysql_tzinfo_to_sql: command completed successfully', text=sql_return.output.decode('utf-8'))
|
||||
else:
|
||||
return jsonify(type='error', msg='mysql_tzinfo_to_sql: error running command', text=sql_return.output.decode('utf-8'))
|
||||
|
||||
# api call: container_post - post_action: exec - cmd: reload - task: dovecot
|
||||
def container_post__exec__reload__dovecot(self, container_id):
|
||||
for container in docker_client.containers.list(filters={"id": container_id}):
|
||||
reload_return = container.exec_run(["/bin/bash", "-c", "/usr/sbin/dovecot reload"])
|
||||
return exec_run_handler('generic', reload_return)
|
||||
|
||||
|
||||
# api call: container_post - post_action: exec - cmd: reload - task: postfix
|
||||
def container_post__exec__reload__postfix(self, container_id):
|
||||
for container in docker_client.containers.list(filters={"id": container_id}):
|
||||
reload_return = container.exec_run(["/bin/bash", "-c", "/usr/sbin/postfix reload"])
|
||||
return exec_run_handler('generic', reload_return)
|
||||
|
||||
|
||||
# api call: container_post - post_action: exec - cmd: reload - task: nginx
|
||||
def container_post__exec__reload__nginx(self, container_id):
|
||||
for container in docker_client.containers.list(filters={"id": container_id}):
|
||||
reload_return = container.exec_run(["/bin/sh", "-c", "/usr/sbin/nginx -s reload"])
|
||||
return exec_run_handler('generic', reload_return)
|
||||
|
||||
|
||||
# api call: container_post - post_action: exec - cmd: sieve - task: list
|
||||
def container_post__exec__sieve__list(self, container_id):
|
||||
if 'username' in request.json:
|
||||
for container in docker_client.containers.list(filters={"id": container_id}):
|
||||
sieve_return = container.exec_run(["/bin/bash", "-c", "/usr/bin/doveadm sieve list -u '" + request.json['username'].replace("'", "'\\''") + "'"])
|
||||
return exec_run_handler('utf8_text_only', sieve_return)
|
||||
|
||||
|
||||
# api call: container_post - post_action: exec - cmd: sieve - task: print
|
||||
def container_post__exec__sieve__print(self, container_id):
|
||||
if 'username' in request.json and 'script_name' in request.json:
|
||||
for container in docker_client.containers.list(filters={"id": container_id}):
|
||||
cmd = ["/bin/bash", "-c", "/usr/bin/doveadm sieve get -u '" + request.json['username'].replace("'", "'\\''") + "' '" + request.json['script_name'].replace("'", "'\\''") + "'"]
|
||||
sieve_return = container.exec_run(cmd)
|
||||
return exec_run_handler('utf8_text_only', sieve_return)
|
||||
|
||||
|
||||
# api call: container_post - post_action: exec - cmd: maildir - task: cleanup
|
||||
def container_post__exec__maildir__cleanup(self, container_id):
|
||||
if 'maildir' in request.json:
|
||||
for container in docker_client.containers.list(filters={"id": container_id}):
|
||||
sane_name = re.sub(r'\W+', '', request.json['maildir'])
|
||||
cmd = ["/bin/bash", "-c", "if [[ -d '/var/vmail/" + request.json['maildir'].replace("'", "'\\''") + "' ]]; then /bin/mv '/var/vmail/" + request.json['maildir'].replace("'", "'\\''") + "' '/var/vmail/_garbage/" + str(int(time.time())) + "_" + sane_name + "'; fi"]
|
||||
maildir_cleanup = container.exec_run(cmd, user='vmail')
|
||||
return exec_run_handler('generic', maildir_cleanup)
|
||||
|
||||
|
||||
|
||||
# api call: container_post - post_action: exec - cmd: rspamd - task: worker_password
|
||||
def container_post__exec__rspamd__worker_password(self, container_id):
|
||||
if 'raw' in request.json:
|
||||
for container in docker_client.containers.list(filters={"id": container_id}):
|
||||
cmd = "/usr/bin/rspamadm pw -e -p '" + request.json['raw'].replace("'", "'\\''") + "' 2> /dev/null"
|
||||
cmd_response = exec_cmd_container(container, cmd, user="_rspamd")
|
||||
matched = False
|
||||
for line in cmd_response.split("\n"):
|
||||
if '$2$' in line:
|
||||
hash = line.strip()
|
||||
hash_out = re.search('\$2\$.+$', hash).group(0)
|
||||
rspamd_passphrase_hash = re.sub('[^0-9a-zA-Z\$]+', '', hash_out.rstrip())
|
||||
|
||||
rspamd_password_filename = "/etc/rspamd/override.d/worker-controller-password.inc"
|
||||
cmd = '''/bin/echo 'enable_password = "%s";' > %s && cat %s''' % (rspamd_passphrase_hash, rspamd_password_filename, rspamd_password_filename)
|
||||
cmd_response = exec_cmd_container(container, cmd, user="_rspamd")
|
||||
|
||||
if rspamd_passphrase_hash.startswith("$2$") and rspamd_passphrase_hash in cmd_response:
|
||||
container.restart()
|
||||
matched = True
|
||||
|
||||
if matched:
|
||||
return jsonify(type='success', msg='command completed successfully')
|
||||
else:
|
||||
return jsonify(type='danger', msg='command did not complete')
|
||||
|
||||
def exec_cmd_container(container, cmd, user, timeout=2, shell_cmd="/bin/bash"):
|
||||
|
||||
def recv_socket_data(c_socket, timeout):
|
||||
c_socket.setblocking(0)
|
||||
total_data=[];
|
||||
data='';
|
||||
begin=time.time()
|
||||
while True:
|
||||
if total_data and time.time()-begin > timeout:
|
||||
break
|
||||
elif time.time()-begin > timeout*2:
|
||||
break
|
||||
try:
|
||||
data = c_socket.recv(8192)
|
||||
if data:
|
||||
total_data.append(data.decode('utf-8'))
|
||||
#change the beginning time for measurement
|
||||
begin=time.time()
|
||||
else:
|
||||
#sleep for sometime to indicate a gap
|
||||
time.sleep(0.1)
|
||||
break
|
||||
except:
|
||||
pass
|
||||
return ''.join(total_data)
|
||||
|
||||
try :
|
||||
socket = container.exec_run([shell_cmd], stdin=True, socket=True, user=user).output._sock
|
||||
if not cmd.endswith("\n"):
|
||||
cmd = cmd + "\n"
|
||||
socket.send(cmd.encode('utf-8'))
|
||||
data = recv_socket_data(socket, timeout)
|
||||
socket.close()
|
||||
return data
|
||||
|
||||
except Exception as e:
|
||||
print("error - exec_cmd_container: %s" % str(e))
|
||||
traceback.print_exc(file=sys.stdout)
|
||||
|
||||
def exec_run_handler(type, output):
|
||||
if type == 'generic':
|
||||
if output.exit_code == 0:
|
||||
return jsonify(type='success', msg='command completed successfully')
|
||||
else:
|
||||
return jsonify(type='danger', msg='command failed: ' + output.output.decode('utf-8'))
|
||||
if type == 'utf8_text_only':
|
||||
r = Response(response=output.output.decode('utf-8'), status=200, mimetype="text/plain")
|
||||
r.headers["Content-Type"] = "text/plain; charset=utf-8"
|
||||
return r
|
||||
|
||||
class GracefulKiller:
|
||||
kill_now = False
|
||||
def __init__(self):
|
||||
signal.signal(signal.SIGINT, self.exit_gracefully)
|
||||
signal.signal(signal.SIGTERM, self.exit_gracefully)
|
||||
|
||||
def exit_gracefully(self, signum, frame):
|
||||
self.kill_now = True
|
||||
|
||||
def create_self_signed_cert():
|
||||
process = subprocess.Popen(
|
||||
"openssl req -x509 -newkey rsa:4096 -sha256 -days 3650 -nodes -keyout /app/dockerapi_key.pem -out /app/dockerapi_cert.pem -subj /CN=dockerapi/O=mailcow -addext subjectAltName=DNS:dockerapi".split(),
|
||||
stdout = subprocess.PIPE, stderr = subprocess.PIPE, shell=False
|
||||
)
|
||||
process.wait()
|
||||
|
||||
def startFlaskAPI():
|
||||
create_self_signed_cert()
|
||||
try:
|
||||
ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
|
||||
ctx.check_hostname = False
|
||||
ctx.load_cert_chain(certfile='/app/dockerapi_cert.pem', keyfile='/app/dockerapi_key.pem')
|
||||
except:
|
||||
print ("Cannot initialize TLS, retrying in 5s...")
|
||||
time.sleep(5)
|
||||
app.run(debug=False, host='0.0.0.0', port=443, threaded=True, ssl_context=ctx)
|
||||
|
||||
api.add_resource(containers_get, '/containers/json')
|
||||
api.add_resource(container_get, '/containers/<string:container_id>/json')
|
||||
api.add_resource(container_post, '/containers/<string:container_id>/<string:post_action>')
|
||||
|
||||
if __name__ == '__main__':
|
||||
api_thread = Thread(target=startFlaskAPI)
|
||||
api_thread.daemon = True
|
||||
api_thread.start()
|
||||
killer = GracefulKiller()
|
||||
while True:
|
||||
time.sleep(1)
|
||||
if killer.kill_now:
|
||||
break
|
||||
print ("Stopping dockerapi-mailcow")
|
|
@ -0,0 +1,260 @@
|
|||
import os
|
||||
import sys
|
||||
import uvicorn
|
||||
import json
|
||||
import uuid
|
||||
import async_timeout
|
||||
import asyncio
|
||||
import aioredis
|
||||
import aiodocker
|
||||
import docker
|
||||
import logging
|
||||
from logging.config import dictConfig
|
||||
from fastapi import FastAPI, Response, Request
|
||||
from modules.DockerApi import DockerApi
|
||||
|
||||
dockerapi = None
|
||||
app = FastAPI()
|
||||
|
||||
# Define Routes
|
||||
@app.get("/host/stats")
|
||||
async def get_host_update_stats():
|
||||
global dockerapi
|
||||
|
||||
if dockerapi.host_stats_isUpdating == False:
|
||||
asyncio.create_task(dockerapi.get_host_stats())
|
||||
dockerapi.host_stats_isUpdating = True
|
||||
|
||||
while True:
|
||||
if await dockerapi.redis_client.exists('host_stats'):
|
||||
break
|
||||
await asyncio.sleep(1.5)
|
||||
|
||||
stats = json.loads(await dockerapi.redis_client.get('host_stats'))
|
||||
return Response(content=json.dumps(stats, indent=4), media_type="application/json")
|
||||
|
||||
@app.get("/containers/{container_id}/json")
|
||||
async def get_container(container_id : str):
|
||||
global dockerapi
|
||||
|
||||
if container_id and container_id.isalnum():
|
||||
try:
|
||||
for container in (await dockerapi.async_docker_client.containers.list()):
|
||||
if container._id == container_id:
|
||||
container_info = await container.show()
|
||||
return Response(content=json.dumps(container_info, indent=4), media_type="application/json")
|
||||
|
||||
res = {
|
||||
"type": "danger",
|
||||
"msg": "no container found"
|
||||
}
|
||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||
except Exception as e:
|
||||
res = {
|
||||
"type": "danger",
|
||||
"msg": str(e)
|
||||
}
|
||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||
else:
|
||||
res = {
|
||||
"type": "danger",
|
||||
"msg": "no or invalid id defined"
|
||||
}
|
||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||
|
||||
@app.get("/containers/json")
|
||||
async def get_containers():
|
||||
global dockerapi
|
||||
|
||||
containers = {}
|
||||
try:
|
||||
for container in (await dockerapi.async_docker_client.containers.list()):
|
||||
container_info = await container.show()
|
||||
containers.update({container_info['Id']: container_info})
|
||||
return Response(content=json.dumps(containers, indent=4), media_type="application/json")
|
||||
except Exception as e:
|
||||
res = {
|
||||
"type": "danger",
|
||||
"msg": str(e)
|
||||
}
|
||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||
|
||||
@app.post("/containers/{container_id}/{post_action}")
|
||||
async def post_containers(container_id : str, post_action : str, request: Request):
|
||||
global dockerapi
|
||||
|
||||
try :
|
||||
request_json = await request.json()
|
||||
except Exception as err:
|
||||
request_json = {}
|
||||
|
||||
if container_id and container_id.isalnum() and post_action:
|
||||
try:
|
||||
"""Dispatch container_post api call"""
|
||||
if post_action == 'exec':
|
||||
if not request_json or not 'cmd' in request_json:
|
||||
res = {
|
||||
"type": "danger",
|
||||
"msg": "cmd is missing"
|
||||
}
|
||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||
if not request_json or not 'task' in request_json:
|
||||
res = {
|
||||
"type": "danger",
|
||||
"msg": "task is missing"
|
||||
}
|
||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||
|
||||
api_call_method_name = '__'.join(['container_post', str(post_action), str(request_json['cmd']), str(request_json['task']) ])
|
||||
else:
|
||||
api_call_method_name = '__'.join(['container_post', str(post_action) ])
|
||||
|
||||
api_call_method = getattr(dockerapi, api_call_method_name, lambda container_id: Response(content=json.dumps({'type': 'danger', 'msg':'container_post - unknown api call' }, indent=4), media_type="application/json"))
|
||||
|
||||
dockerapi.logger.info("api call: %s, container_id: %s" % (api_call_method_name, container_id))
|
||||
return api_call_method(request_json, container_id=container_id)
|
||||
except Exception as e:
|
||||
dockerapi.logger.error("error - container_post: %s" % str(e))
|
||||
res = {
|
||||
"type": "danger",
|
||||
"msg": str(e)
|
||||
}
|
||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||
|
||||
else:
|
||||
res = {
|
||||
"type": "danger",
|
||||
"msg": "invalid container id or missing action"
|
||||
}
|
||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||
|
||||
@app.post("/container/{container_id}/stats/update")
|
||||
async def post_container_update_stats(container_id : str):
|
||||
global dockerapi
|
||||
|
||||
# start update task for container if no task is running
|
||||
if container_id not in dockerapi.containerIds_to_update:
|
||||
asyncio.create_task(dockerapi.get_container_stats(container_id))
|
||||
dockerapi.containerIds_to_update.append(container_id)
|
||||
|
||||
while True:
|
||||
if await dockerapi.redis_client.exists(container_id + '_stats'):
|
||||
break
|
||||
await asyncio.sleep(1.5)
|
||||
|
||||
stats = json.loads(await dockerapi.redis_client.get(container_id + '_stats'))
|
||||
return Response(content=json.dumps(stats, indent=4), media_type="application/json")
|
||||
|
||||
# Events
|
||||
@app.on_event("startup")
|
||||
async def startup_event():
|
||||
global dockerapi
|
||||
|
||||
# Initialize a custom logger
|
||||
logger = logging.getLogger("dockerapi")
|
||||
logger.setLevel(logging.INFO)
|
||||
# Configure the logger to output logs to the terminal
|
||||
handler = logging.StreamHandler()
|
||||
handler.setLevel(logging.INFO)
|
||||
formatter = logging.Formatter("%(levelname)s: %(message)s")
|
||||
handler.setFormatter(formatter)
|
||||
logger.addHandler(handler)
|
||||
|
||||
logger.info("Init APP")
|
||||
|
||||
# Init redis client
|
||||
if os.environ['REDIS_SLAVEOF_IP'] != "":
|
||||
redis_client = redis = await aioredis.from_url(f"redis://{os.environ['REDIS_SLAVEOF_IP']}:{os.environ['REDIS_SLAVEOF_PORT']}/0")
|
||||
else:
|
||||
redis_client = redis = await aioredis.from_url("redis://redis-mailcow:6379/0")
|
||||
|
||||
# Init docker clients
|
||||
sync_docker_client = docker.DockerClient(base_url='unix://var/run/docker.sock', version='auto')
|
||||
async_docker_client = aiodocker.Docker(url='unix:///var/run/docker.sock')
|
||||
|
||||
dockerapi = DockerApi(redis_client, sync_docker_client, async_docker_client, logger)
|
||||
|
||||
logger.info("Subscribe to redis channel")
|
||||
# Subscribe to redis channel
|
||||
dockerapi.pubsub = redis.pubsub()
|
||||
await dockerapi.pubsub.subscribe("MC_CHANNEL")
|
||||
asyncio.create_task(handle_pubsub_messages(dockerapi.pubsub))
|
||||
|
||||
@app.on_event("shutdown")
|
||||
async def shutdown_event():
|
||||
global dockerapi
|
||||
|
||||
# Close docker connections
|
||||
dockerapi.sync_docker_client.close()
|
||||
await dockerapi.async_docker_client.close()
|
||||
|
||||
# Close redis
|
||||
await dockerapi.pubsub.unsubscribe("MC_CHANNEL")
|
||||
await dockerapi.redis_client.close()
|
||||
|
||||
# PubSub Handler
|
||||
async def handle_pubsub_messages(channel: aioredis.client.PubSub):
|
||||
global dockerapi
|
||||
|
||||
while True:
|
||||
try:
|
||||
async with async_timeout.timeout(1):
|
||||
message = await channel.get_message(ignore_subscribe_messages=True)
|
||||
if message is not None:
|
||||
# Parse message
|
||||
data_json = json.loads(message['data'].decode('utf-8'))
|
||||
dockerapi.logger.info(f"PubSub Received - {json.dumps(data_json)}")
|
||||
|
||||
# Handle api_call
|
||||
if 'api_call' in data_json:
|
||||
# api_call: container_post
|
||||
if data_json['api_call'] == "container_post":
|
||||
if 'post_action' in data_json and 'container_name' in data_json:
|
||||
try:
|
||||
"""Dispatch container_post api call"""
|
||||
request_json = {}
|
||||
if data_json['post_action'] == 'exec':
|
||||
if 'request' in data_json:
|
||||
request_json = data_json['request']
|
||||
if 'cmd' in request_json:
|
||||
if 'task' in request_json:
|
||||
api_call_method_name = '__'.join(['container_post', str(data_json['post_action']), str(request_json['cmd']), str(request_json['task']) ])
|
||||
else:
|
||||
dockerapi.logger.error("api call: task missing")
|
||||
else:
|
||||
dockerapi.logger.error("api call: cmd missing")
|
||||
else:
|
||||
dockerapi.logger.error("api call: request missing")
|
||||
else:
|
||||
api_call_method_name = '__'.join(['container_post', str(data_json['post_action'])])
|
||||
|
||||
if api_call_method_name:
|
||||
api_call_method = getattr(dockerapi, api_call_method_name)
|
||||
if api_call_method:
|
||||
dockerapi.logger.info("api call: %s, container_name: %s" % (api_call_method_name, data_json['container_name']))
|
||||
api_call_method(request_json, container_name=data_json['container_name'])
|
||||
else:
|
||||
dockerapi.logger.error("api call not found: %s, container_name: %s" % (api_call_method_name, data_json['container_name']))
|
||||
except Exception as e:
|
||||
dockerapi.logger.error("container_post: %s" % str(e))
|
||||
else:
|
||||
dockerapi.logger.error("api call: missing container_name, post_action or request")
|
||||
else:
|
||||
dockerapi.logger.error("Unknwon PubSub recieved - %s" % json.dumps(data_json))
|
||||
else:
|
||||
dockerapi.logger.error("Unknwon PubSub recieved - %s" % json.dumps(data_json))
|
||||
|
||||
await asyncio.sleep(0.01)
|
||||
except asyncio.TimeoutError:
|
||||
pass
|
||||
|
||||
if __name__ == '__main__':
|
||||
uvicorn.run(
|
||||
app,
|
||||
host="0.0.0.0",
|
||||
port=443,
|
||||
ssl_certfile="/app/dockerapi_cert.pem",
|
||||
ssl_keyfile="/app/dockerapi_key.pem",
|
||||
log_level="info",
|
||||
loop="none"
|
||||
)
|
|
@ -0,0 +1,487 @@
|
|||
import psutil
|
||||
import sys
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
import json
|
||||
import asyncio
|
||||
import platform
|
||||
from datetime import datetime
|
||||
from fastapi import FastAPI, Response, Request
|
||||
|
||||
class DockerApi:
|
||||
def __init__(self, redis_client, sync_docker_client, async_docker_client, logger):
|
||||
self.redis_client = redis_client
|
||||
self.sync_docker_client = sync_docker_client
|
||||
self.async_docker_client = async_docker_client
|
||||
self.logger = logger
|
||||
|
||||
self.host_stats_isUpdating = False
|
||||
self.containerIds_to_update = []
|
||||
|
||||
# api call: container_post - post_action: stop
|
||||
def container_post__stop(self, request_json, **kwargs):
|
||||
if 'container_id' in kwargs:
|
||||
filters = {"id": kwargs['container_id']}
|
||||
elif 'container_name' in kwargs:
|
||||
filters = {"name": kwargs['container_name']}
|
||||
|
||||
for container in self.sync_docker_client.containers.list(all=True, filters=filters):
|
||||
container.stop()
|
||||
|
||||
res = { 'type': 'success', 'msg': 'command completed successfully'}
|
||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||
# api call: container_post - post_action: start
|
||||
def container_post__start(self, request_json, **kwargs):
|
||||
if 'container_id' in kwargs:
|
||||
filters = {"id": kwargs['container_id']}
|
||||
elif 'container_name' in kwargs:
|
||||
filters = {"name": kwargs['container_name']}
|
||||
|
||||
for container in self.sync_docker_client.containers.list(all=True, filters=filters):
|
||||
container.start()
|
||||
|
||||
res = { 'type': 'success', 'msg': 'command completed successfully'}
|
||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||
# api call: container_post - post_action: restart
|
||||
def container_post__restart(self, request_json, **kwargs):
|
||||
if 'container_id' in kwargs:
|
||||
filters = {"id": kwargs['container_id']}
|
||||
elif 'container_name' in kwargs:
|
||||
filters = {"name": kwargs['container_name']}
|
||||
|
||||
for container in self.sync_docker_client.containers.list(all=True, filters=filters):
|
||||
container.restart()
|
||||
|
||||
res = { 'type': 'success', 'msg': 'command completed successfully'}
|
||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||
# api call: container_post - post_action: top
|
||||
def container_post__top(self, request_json, **kwargs):
|
||||
if 'container_id' in kwargs:
|
||||
filters = {"id": kwargs['container_id']}
|
||||
elif 'container_name' in kwargs:
|
||||
filters = {"name": kwargs['container_name']}
|
||||
|
||||
for container in self.sync_docker_client.containers.list(all=True, filters=filters):
|
||||
res = { 'type': 'success', 'msg': container.top()}
|
||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||
# api call: container_post - post_action: stats
|
||||
def container_post__stats(self, request_json, **kwargs):
|
||||
if 'container_id' in kwargs:
|
||||
filters = {"id": kwargs['container_id']}
|
||||
elif 'container_name' in kwargs:
|
||||
filters = {"name": kwargs['container_name']}
|
||||
|
||||
for container in self.sync_docker_client.containers.list(all=True, filters=filters):
|
||||
for stat in container.stats(decode=True, stream=True):
|
||||
res = { 'type': 'success', 'msg': stat}
|
||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||
# api call: container_post - post_action: exec - cmd: mailq - task: delete
|
||||
def container_post__exec__mailq__delete(self, request_json, **kwargs):
|
||||
if 'container_id' in kwargs:
|
||||
filters = {"id": kwargs['container_id']}
|
||||
elif 'container_name' in kwargs:
|
||||
filters = {"name": kwargs['container_name']}
|
||||
|
||||
if 'items' in request_json:
|
||||
r = re.compile("^[0-9a-fA-F]+$")
|
||||
filtered_qids = filter(r.match, request_json['items'])
|
||||
if filtered_qids:
|
||||
flagged_qids = ['-d %s' % i for i in filtered_qids]
|
||||
sanitized_string = str(' '.join(flagged_qids))
|
||||
for container in self.sync_docker_client.containers.list(filters=filters):
|
||||
postsuper_r = container.exec_run(["/bin/bash", "-c", "/usr/sbin/postsuper " + sanitized_string])
|
||||
return self.exec_run_handler('generic', postsuper_r)
|
||||
# api call: container_post - post_action: exec - cmd: mailq - task: hold
|
||||
def container_post__exec__mailq__hold(self, request_json, **kwargs):
|
||||
if 'container_id' in kwargs:
|
||||
filters = {"id": kwargs['container_id']}
|
||||
elif 'container_name' in kwargs:
|
||||
filters = {"name": kwargs['container_name']}
|
||||
|
||||
if 'items' in request_json:
|
||||
r = re.compile("^[0-9a-fA-F]+$")
|
||||
filtered_qids = filter(r.match, request_json['items'])
|
||||
if filtered_qids:
|
||||
flagged_qids = ['-h %s' % i for i in filtered_qids]
|
||||
sanitized_string = str(' '.join(flagged_qids))
|
||||
for container in self.sync_docker_client.containers.list(filters=filters):
|
||||
postsuper_r = container.exec_run(["/bin/bash", "-c", "/usr/sbin/postsuper " + sanitized_string])
|
||||
return self.exec_run_handler('generic', postsuper_r)
|
||||
# api call: container_post - post_action: exec - cmd: mailq - task: cat
|
||||
def container_post__exec__mailq__cat(self, request_json, **kwargs):
|
||||
if 'container_id' in kwargs:
|
||||
filters = {"id": kwargs['container_id']}
|
||||
elif 'container_name' in kwargs:
|
||||
filters = {"name": kwargs['container_name']}
|
||||
|
||||
if 'items' in request_json:
|
||||
r = re.compile("^[0-9a-fA-F]+$")
|
||||
filtered_qids = filter(r.match, request_json['items'])
|
||||
if filtered_qids:
|
||||
sanitized_string = str(' '.join(filtered_qids))
|
||||
|
||||
for container in self.sync_docker_client.containers.list(filters=filters):
|
||||
postcat_return = container.exec_run(["/bin/bash", "-c", "/usr/sbin/postcat -q " + sanitized_string], user='postfix')
|
||||
if not postcat_return:
|
||||
postcat_return = 'err: invalid'
|
||||
return self.exec_run_handler('utf8_text_only', postcat_return)
|
||||
# api call: container_post - post_action: exec - cmd: mailq - task: unhold
|
||||
def container_post__exec__mailq__unhold(self, request_json, **kwargs):
|
||||
if 'container_id' in kwargs:
|
||||
filters = {"id": kwargs['container_id']}
|
||||
elif 'container_name' in kwargs:
|
||||
filters = {"name": kwargs['container_name']}
|
||||
|
||||
if 'items' in request_json:
|
||||
r = re.compile("^[0-9a-fA-F]+$")
|
||||
filtered_qids = filter(r.match, request_json['items'])
|
||||
if filtered_qids:
|
||||
flagged_qids = ['-H %s' % i for i in filtered_qids]
|
||||
sanitized_string = str(' '.join(flagged_qids))
|
||||
for container in self.sync_docker_client.containers.list(filters=filters):
|
||||
postsuper_r = container.exec_run(["/bin/bash", "-c", "/usr/sbin/postsuper " + sanitized_string])
|
||||
return self.exec_run_handler('generic', postsuper_r)
|
||||
# api call: container_post - post_action: exec - cmd: mailq - task: deliver
|
||||
def container_post__exec__mailq__deliver(self, request_json, **kwargs):
|
||||
if 'container_id' in kwargs:
|
||||
filters = {"id": kwargs['container_id']}
|
||||
elif 'container_name' in kwargs:
|
||||
filters = {"name": kwargs['container_name']}
|
||||
|
||||
if 'items' in request_json:
|
||||
r = re.compile("^[0-9a-fA-F]+$")
|
||||
filtered_qids = filter(r.match, request_json['items'])
|
||||
if filtered_qids:
|
||||
flagged_qids = ['-i %s' % i for i in filtered_qids]
|
||||
for container in self.sync_docker_client.containers.list(filters=filters):
|
||||
for i in flagged_qids:
|
||||
postqueue_r = container.exec_run(["/bin/bash", "-c", "/usr/sbin/postqueue " + i], user='postfix')
|
||||
# todo: check each exit code
|
||||
res = { 'type': 'success', 'msg': 'Scheduled immediate delivery'}
|
||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||
# api call: container_post - post_action: exec - cmd: mailq - task: list
|
||||
def container_post__exec__mailq__list(self, request_json, **kwargs):
|
||||
if 'container_id' in kwargs:
|
||||
filters = {"id": kwargs['container_id']}
|
||||
elif 'container_name' in kwargs:
|
||||
filters = {"name": kwargs['container_name']}
|
||||
|
||||
for container in self.sync_docker_client.containers.list(filters=filters):
|
||||
mailq_return = container.exec_run(["/usr/sbin/postqueue", "-j"], user='postfix')
|
||||
return self.exec_run_handler('utf8_text_only', mailq_return)
|
||||
# api call: container_post - post_action: exec - cmd: mailq - task: flush
|
||||
def container_post__exec__mailq__flush(self, request_json, **kwargs):
|
||||
if 'container_id' in kwargs:
|
||||
filters = {"id": kwargs['container_id']}
|
||||
elif 'container_name' in kwargs:
|
||||
filters = {"name": kwargs['container_name']}
|
||||
|
||||
for container in self.sync_docker_client.containers.list(filters=filters):
|
||||
postqueue_r = container.exec_run(["/usr/sbin/postqueue", "-f"], user='postfix')
|
||||
return self.exec_run_handler('generic', postqueue_r)
|
||||
# api call: container_post - post_action: exec - cmd: mailq - task: super_delete
|
||||
def container_post__exec__mailq__super_delete(self, request_json, **kwargs):
|
||||
if 'container_id' in kwargs:
|
||||
filters = {"id": kwargs['container_id']}
|
||||
elif 'container_name' in kwargs:
|
||||
filters = {"name": kwargs['container_name']}
|
||||
|
||||
for container in self.sync_docker_client.containers.list(filters=filters):
|
||||
postsuper_r = container.exec_run(["/usr/sbin/postsuper", "-d", "ALL"])
|
||||
return self.exec_run_handler('generic', postsuper_r)
|
||||
# api call: container_post - post_action: exec - cmd: system - task: fts_rescan
|
||||
def container_post__exec__system__fts_rescan(self, request_json, **kwargs):
|
||||
if 'container_id' in kwargs:
|
||||
filters = {"id": kwargs['container_id']}
|
||||
elif 'container_name' in kwargs:
|
||||
filters = {"name": kwargs['container_name']}
|
||||
|
||||
if 'username' in request_json:
|
||||
for container in self.sync_docker_client.containers.list(filters=filters):
|
||||
rescan_return = container.exec_run(["/bin/bash", "-c", "/usr/bin/doveadm fts rescan -u '" + request_json['username'].replace("'", "'\\''") + "'"], user='vmail')
|
||||
if rescan_return.exit_code == 0:
|
||||
res = { 'type': 'success', 'msg': 'fts_rescan: rescan triggered'}
|
||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||
else:
|
||||
res = { 'type': 'warning', 'msg': 'fts_rescan error'}
|
||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||
if 'all' in request_json:
|
||||
for container in self.sync_docker_client.containers.list(filters=filters):
|
||||
rescan_return = container.exec_run(["/bin/bash", "-c", "/usr/bin/doveadm fts rescan -A"], user='vmail')
|
||||
if rescan_return.exit_code == 0:
|
||||
res = { 'type': 'success', 'msg': 'fts_rescan: rescan triggered'}
|
||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||
else:
|
||||
res = { 'type': 'warning', 'msg': 'fts_rescan error'}
|
||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||
# api call: container_post - post_action: exec - cmd: system - task: df
|
||||
def container_post__exec__system__df(self, request_json, **kwargs):
|
||||
if 'container_id' in kwargs:
|
||||
filters = {"id": kwargs['container_id']}
|
||||
elif 'container_name' in kwargs:
|
||||
filters = {"name": kwargs['container_name']}
|
||||
|
||||
if 'dir' in request_json:
|
||||
for container in self.sync_docker_client.containers.list(filters=filters):
|
||||
df_return = container.exec_run(["/bin/bash", "-c", "/bin/df -H '" + request_json['dir'].replace("'", "'\\''") + "' | /usr/bin/tail -n1 | /usr/bin/tr -s [:blank:] | /usr/bin/tr ' ' ','"], user='nobody')
|
||||
if df_return.exit_code == 0:
|
||||
return df_return.output.decode('utf-8').rstrip()
|
||||
else:
|
||||
return "0,0,0,0,0,0"
|
||||
# api call: container_post - post_action: exec - cmd: system - task: mysql_upgrade
|
||||
def container_post__exec__system__mysql_upgrade(self, request_json, **kwargs):
|
||||
if 'container_id' in kwargs:
|
||||
filters = {"id": kwargs['container_id']}
|
||||
elif 'container_name' in kwargs:
|
||||
filters = {"name": kwargs['container_name']}
|
||||
|
||||
for container in self.sync_docker_client.containers.list(filters=filters):
|
||||
sql_return = container.exec_run(["/bin/bash", "-c", "/usr/bin/mysql_upgrade -uroot -p'" + os.environ['DBROOT'].replace("'", "'\\''") + "'\n"], user='mysql')
|
||||
if sql_return.exit_code == 0:
|
||||
matched = False
|
||||
for line in sql_return.output.decode('utf-8').split("\n"):
|
||||
if 'is already upgraded to' in line:
|
||||
matched = True
|
||||
if matched:
|
||||
res = { 'type': 'success', 'msg':'mysql_upgrade: already upgraded', 'text': sql_return.output.decode('utf-8')}
|
||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||
else:
|
||||
container.restart()
|
||||
res = { 'type': 'warning', 'msg':'mysql_upgrade: upgrade was applied', 'text': sql_return.output.decode('utf-8')}
|
||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||
else:
|
||||
res = { 'type': 'error', 'msg': 'mysql_upgrade: error running command', 'text': sql_return.output.decode('utf-8')}
|
||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||
# api call: container_post - post_action: exec - cmd: system - task: mysql_tzinfo_to_sql
|
||||
def container_post__exec__system__mysql_tzinfo_to_sql(self, request_json, **kwargs):
|
||||
if 'container_id' in kwargs:
|
||||
filters = {"id": kwargs['container_id']}
|
||||
elif 'container_name' in kwargs:
|
||||
filters = {"name": kwargs['container_name']}
|
||||
|
||||
for container in self.sync_docker_client.containers.list(filters=filters):
|
||||
sql_return = container.exec_run(["/bin/bash", "-c", "/usr/bin/mysql_tzinfo_to_sql /usr/share/zoneinfo | /bin/sed 's/Local time zone must be set--see zic manual page/FCTY/' | /usr/bin/mysql -uroot -p'" + os.environ['DBROOT'].replace("'", "'\\''") + "' mysql \n"], user='mysql')
|
||||
if sql_return.exit_code == 0:
|
||||
res = { 'type': 'info', 'msg': 'mysql_tzinfo_to_sql: command completed successfully', 'text': sql_return.output.decode('utf-8')}
|
||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||
else:
|
||||
res = { 'type': 'error', 'msg': 'mysql_tzinfo_to_sql: error running command', 'text': sql_return.output.decode('utf-8')}
|
||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||
# api call: container_post - post_action: exec - cmd: reload - task: dovecot
|
||||
def container_post__exec__reload__dovecot(self, request_json, **kwargs):
|
||||
if 'container_id' in kwargs:
|
||||
filters = {"id": kwargs['container_id']}
|
||||
elif 'container_name' in kwargs:
|
||||
filters = {"name": kwargs['container_name']}
|
||||
|
||||
for container in self.sync_docker_client.containers.list(filters=filters):
|
||||
reload_return = container.exec_run(["/bin/bash", "-c", "/usr/sbin/dovecot reload"])
|
||||
return self.exec_run_handler('generic', reload_return)
|
||||
# api call: container_post - post_action: exec - cmd: reload - task: postfix
|
||||
def container_post__exec__reload__postfix(self, request_json, **kwargs):
|
||||
if 'container_id' in kwargs:
|
||||
filters = {"id": kwargs['container_id']}
|
||||
elif 'container_name' in kwargs:
|
||||
filters = {"name": kwargs['container_name']}
|
||||
|
||||
for container in self.sync_docker_client.containers.list(filters=filters):
|
||||
reload_return = container.exec_run(["/bin/bash", "-c", "/usr/sbin/postfix reload"])
|
||||
return self.exec_run_handler('generic', reload_return)
|
||||
# api call: container_post - post_action: exec - cmd: reload - task: nginx
|
||||
def container_post__exec__reload__nginx(self, request_json, **kwargs):
|
||||
if 'container_id' in kwargs:
|
||||
filters = {"id": kwargs['container_id']}
|
||||
elif 'container_name' in kwargs:
|
||||
filters = {"name": kwargs['container_name']}
|
||||
|
||||
for container in self.sync_docker_client.containers.list(filters=filters):
|
||||
reload_return = container.exec_run(["/bin/sh", "-c", "/usr/sbin/nginx -s reload"])
|
||||
return self.exec_run_handler('generic', reload_return)
|
||||
# api call: container_post - post_action: exec - cmd: sieve - task: list
|
||||
def container_post__exec__sieve__list(self, request_json, **kwargs):
|
||||
if 'container_id' in kwargs:
|
||||
filters = {"id": kwargs['container_id']}
|
||||
elif 'container_name' in kwargs:
|
||||
filters = {"name": kwargs['container_name']}
|
||||
|
||||
if 'username' in request_json:
|
||||
for container in self.sync_docker_client.containers.list(filters=filters):
|
||||
sieve_return = container.exec_run(["/bin/bash", "-c", "/usr/bin/doveadm sieve list -u '" + request_json['username'].replace("'", "'\\''") + "'"])
|
||||
return self.exec_run_handler('utf8_text_only', sieve_return)
|
||||
# api call: container_post - post_action: exec - cmd: sieve - task: print
|
||||
def container_post__exec__sieve__print(self, request_json, **kwargs):
|
||||
if 'container_id' in kwargs:
|
||||
filters = {"id": kwargs['container_id']}
|
||||
elif 'container_name' in kwargs:
|
||||
filters = {"name": kwargs['container_name']}
|
||||
|
||||
if 'username' in request_json and 'script_name' in request_json:
|
||||
for container in self.sync_docker_client.containers.list(filters=filters):
|
||||
cmd = ["/bin/bash", "-c", "/usr/bin/doveadm sieve get -u '" + request_json['username'].replace("'", "'\\''") + "' '" + request_json['script_name'].replace("'", "'\\''") + "'"]
|
||||
sieve_return = container.exec_run(cmd)
|
||||
return self.exec_run_handler('utf8_text_only', sieve_return)
|
||||
# api call: container_post - post_action: exec - cmd: maildir - task: cleanup
|
||||
def container_post__exec__maildir__cleanup(self, request_json, **kwargs):
|
||||
if 'container_id' in kwargs:
|
||||
filters = {"id": kwargs['container_id']}
|
||||
elif 'container_name' in kwargs:
|
||||
filters = {"name": kwargs['container_name']}
|
||||
|
||||
if 'maildir' in request_json:
|
||||
for container in self.sync_docker_client.containers.list(filters=filters):
|
||||
sane_name = re.sub(r'\W+', '', request_json['maildir'])
|
||||
vmail_name = request_json['maildir'].replace("'", "'\\''")
|
||||
cmd_vmail = "if [[ -d '/var/vmail/" + vmail_name + "' ]]; then /bin/mv '/var/vmail/" + vmail_name + "' '/var/vmail/_garbage/" + str(int(time.time())) + "_" + sane_name + "'; fi"
|
||||
index_name = request_json['maildir'].split("/")
|
||||
if len(index_name) > 1:
|
||||
index_name = index_name[1].replace("'", "'\\''") + "@" + index_name[0].replace("'", "'\\''")
|
||||
cmd_vmail_index = "if [[ -d '/var/vmail_index/" + index_name + "' ]]; then /bin/mv '/var/vmail_index/" + index_name + "' '/var/vmail/_garbage/" + str(int(time.time())) + "_" + sane_name + "_index'; fi"
|
||||
cmd = ["/bin/bash", "-c", cmd_vmail + " && " + cmd_vmail_index]
|
||||
else:
|
||||
cmd = ["/bin/bash", "-c", cmd_vmail]
|
||||
maildir_cleanup = container.exec_run(cmd, user='vmail')
|
||||
return self.exec_run_handler('generic', maildir_cleanup)
|
||||
# api call: container_post - post_action: exec - cmd: rspamd - task: worker_password
|
||||
def container_post__exec__rspamd__worker_password(self, request_json, **kwargs):
|
||||
if 'container_id' in kwargs:
|
||||
filters = {"id": kwargs['container_id']}
|
||||
elif 'container_name' in kwargs:
|
||||
filters = {"name": kwargs['container_name']}
|
||||
|
||||
if 'raw' in request_json:
|
||||
for container in self.sync_docker_client.containers.list(filters=filters):
|
||||
cmd = "/usr/bin/rspamadm pw -e -p '" + request_json['raw'].replace("'", "'\\''") + "' 2> /dev/null"
|
||||
cmd_response = self.exec_cmd_container(container, cmd, user="_rspamd")
|
||||
|
||||
matched = False
|
||||
for line in cmd_response.split("\n"):
|
||||
if '$2$' in line:
|
||||
hash = line.strip()
|
||||
hash_out = re.search('\$2\$.+$', hash).group(0)
|
||||
rspamd_passphrase_hash = re.sub('[^0-9a-zA-Z\$]+', '', hash_out.rstrip())
|
||||
rspamd_password_filename = "/etc/rspamd/override.d/worker-controller-password.inc"
|
||||
cmd = '''/bin/echo 'enable_password = "%s";' > %s && cat %s''' % (rspamd_passphrase_hash, rspamd_password_filename, rspamd_password_filename)
|
||||
cmd_response = self.exec_cmd_container(container, cmd, user="_rspamd")
|
||||
if rspamd_passphrase_hash.startswith("$2$") and rspamd_passphrase_hash in cmd_response:
|
||||
container.restart()
|
||||
matched = True
|
||||
if matched:
|
||||
res = { 'type': 'success', 'msg': 'command completed successfully' }
|
||||
self.logger.info('success changing Rspamd password')
|
||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||
else:
|
||||
self.logger.error('failed changing Rspamd password')
|
||||
res = { 'type': 'danger', 'msg': 'command did not complete' }
|
||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||
|
||||
# Collect host stats
|
||||
async def get_host_stats(self, wait=5):
|
||||
try:
|
||||
system_time = datetime.now()
|
||||
host_stats = {
|
||||
"cpu": {
|
||||
"cores": psutil.cpu_count(),
|
||||
"usage": psutil.cpu_percent()
|
||||
},
|
||||
"memory": {
|
||||
"total": psutil.virtual_memory().total,
|
||||
"usage": psutil.virtual_memory().percent,
|
||||
"swap": psutil.swap_memory()
|
||||
},
|
||||
"uptime": time.time() - psutil.boot_time(),
|
||||
"system_time": system_time.strftime("%d.%m.%Y %H:%M:%S"),
|
||||
"architecture": platform.machine()
|
||||
}
|
||||
|
||||
await self.redis_client.set('host_stats', json.dumps(host_stats), ex=10)
|
||||
except Exception as e:
|
||||
res = {
|
||||
"type": "danger",
|
||||
"msg": str(e)
|
||||
}
|
||||
|
||||
await asyncio.sleep(wait)
|
||||
self.host_stats_isUpdating = False
|
||||
# Collect container stats
|
||||
async def get_container_stats(self, container_id, wait=5, stop=False):
|
||||
if container_id and container_id.isalnum():
|
||||
try:
|
||||
for container in (await self.async_docker_client.containers.list()):
|
||||
if container._id == container_id:
|
||||
res = await container.stats(stream=False)
|
||||
|
||||
if await self.redis_client.exists(container_id + '_stats'):
|
||||
stats = json.loads(await self.redis_client.get(container_id + '_stats'))
|
||||
else:
|
||||
stats = []
|
||||
stats.append(res[0])
|
||||
if len(stats) > 3:
|
||||
del stats[0]
|
||||
await self.redis_client.set(container_id + '_stats', json.dumps(stats), ex=60)
|
||||
except Exception as e:
|
||||
res = {
|
||||
"type": "danger",
|
||||
"msg": str(e)
|
||||
}
|
||||
else:
|
||||
res = {
|
||||
"type": "danger",
|
||||
"msg": "no or invalid id defined"
|
||||
}
|
||||
|
||||
await asyncio.sleep(wait)
|
||||
if stop == True:
|
||||
# update task was called second time, stop
|
||||
self.containerIds_to_update.remove(container_id)
|
||||
else:
|
||||
# call update task a second time
|
||||
await self.get_container_stats(container_id, wait=0, stop=True)
|
||||
|
||||
def exec_cmd_container(self, container, cmd, user, timeout=2, shell_cmd="/bin/bash"):
|
||||
def recv_socket_data(c_socket, timeout):
|
||||
c_socket.setblocking(0)
|
||||
total_data=[]
|
||||
data=''
|
||||
begin=time.time()
|
||||
while True:
|
||||
if total_data and time.time()-begin > timeout:
|
||||
break
|
||||
elif time.time()-begin > timeout*2:
|
||||
break
|
||||
try:
|
||||
data = c_socket.recv(8192)
|
||||
if data:
|
||||
total_data.append(data.decode('utf-8'))
|
||||
#change the beginning time for measurement
|
||||
begin=time.time()
|
||||
else:
|
||||
#sleep for sometime to indicate a gap
|
||||
time.sleep(0.1)
|
||||
break
|
||||
except:
|
||||
pass
|
||||
return ''.join(total_data)
|
||||
|
||||
try :
|
||||
socket = container.exec_run([shell_cmd], stdin=True, socket=True, user=user).output._sock
|
||||
if not cmd.endswith("\n"):
|
||||
cmd = cmd + "\n"
|
||||
socket.send(cmd.encode('utf-8'))
|
||||
data = recv_socket_data(socket, timeout)
|
||||
socket.close()
|
||||
return data
|
||||
except Exception as e:
|
||||
self.logger.error("error - exec_cmd_container: %s" % str(e))
|
||||
traceback.print_exc(file=sys.stdout)
|
||||
|
||||
def exec_run_handler(self, type, output):
|
||||
if type == 'generic':
|
||||
if output.exit_code == 0:
|
||||
res = { 'type': 'success', 'msg': 'command completed successfully' }
|
||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||
else:
|
||||
res = { 'type': 'danger', 'msg': 'command failed: ' + output.output.decode('utf-8') }
|
||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||
if type == 'utf8_text_only':
|
||||
return Response(content=output.output.decode('utf-8'), media_type="text/plain")
|
|
@ -1,10 +1,13 @@
|
|||
FROM debian:bullseye-slim
|
||||
LABEL maintainer "Andre Peters <andre.peters@servercow.de>"
|
||||
LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>"
|
||||
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
ARG DOVECOT=2.3.18
|
||||
# renovate: datasource=github-tags depName=dovecot/core versioning=semver-coerced
|
||||
ARG DOVECOT=2.3.20
|
||||
# renovate: datasource=github-releases depName=tianon/gosu versioning=semver-coerced
|
||||
ARG GOSU_VERSION=1.16
|
||||
ENV LC_ALL C
|
||||
ENV GOSU_VERSION 1.14
|
||||
|
||||
|
||||
# Add groups and users before installing Dovecot to not break compatibility
|
||||
RUN groupadd -g 5000 vmail \
|
||||
|
@ -18,6 +21,7 @@ RUN groupadd -g 5000 vmail \
|
|||
&& touch /etc/default/locale \
|
||||
&& apt-get update \
|
||||
&& apt-get -y --no-install-recommends install \
|
||||
build-essential \
|
||||
apt-transport-https \
|
||||
ca-certificates \
|
||||
cpanminus \
|
||||
|
@ -58,6 +62,7 @@ RUN groupadd -g 5000 vmail \
|
|||
libproc-processtable-perl \
|
||||
libreadonly-perl \
|
||||
libregexp-common-perl \
|
||||
libssl-dev \
|
||||
libsys-meminfo-perl \
|
||||
libterm-readkey-perl \
|
||||
libtest-deep-perl \
|
||||
|
@ -107,6 +112,8 @@ RUN groupadd -g 5000 vmail \
|
|||
&& apt-get autoclean \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& rm -rf /tmp/* /var/tmp/* /root/.cache/
|
||||
# imapsync dependencies
|
||||
RUN cpan Crypt::OpenSSL::PKCS12
|
||||
|
||||
COPY trim_logs.sh /usr/local/bin/trim_logs.sh
|
||||
COPY clean_q_aged.sh /usr/local/bin/clean_q_aged.sh
|
||||
|
|
|
@ -159,7 +159,7 @@ function auth_password_verify(req, pass)
|
|||
VALUES ("%s", 0, "%s", "%s")]], con:escape(req.service), con:escape(req.user), con:escape(req.real_rip)))
|
||||
cur:close()
|
||||
con:close()
|
||||
return dovecot.auth.PASSDB_RESULT_OK, "password=" .. pass
|
||||
return dovecot.auth.PASSDB_RESULT_OK, ""
|
||||
end
|
||||
row = cur:fetch (row, "a")
|
||||
end
|
||||
|
@ -180,13 +180,13 @@ function auth_password_verify(req, pass)
|
|||
if tostring(req.real_rip) == "__IPV4_SOGO__" then
|
||||
cur:close()
|
||||
con:close()
|
||||
return dovecot.auth.PASSDB_RESULT_OK, "password=" .. pass
|
||||
return dovecot.auth.PASSDB_RESULT_OK, ""
|
||||
elseif row.has_prot_access == "1" then
|
||||
con:execute(string.format([[REPLACE INTO sasl_log (service, app_password, username, real_rip)
|
||||
VALUES ("%s", %d, "%s", "%s")]], con:escape(req.service), row.id, con:escape(req.user), con:escape(req.real_rip)))
|
||||
cur:close()
|
||||
con:close()
|
||||
return dovecot.auth.PASSDB_RESULT_OK, "password=" .. pass
|
||||
return dovecot.auth.PASSDB_RESULT_OK, ""
|
||||
end
|
||||
end
|
||||
row = cur:fetch (row, "a")
|
||||
|
@ -307,6 +307,7 @@ namespace {
|
|||
}
|
||||
EOF
|
||||
|
||||
|
||||
cat <<EOF > /etc/dovecot/sogo_trusted_ip.conf
|
||||
# Autogenerated by mailcow
|
||||
remote ${IPV4_NETWORK}.248 {
|
||||
|
@ -349,6 +350,14 @@ sievec /var/vmail/sieve/global_sieve_after.sieve
|
|||
sievec /usr/lib/dovecot/sieve/report-spam.sieve
|
||||
sievec /usr/lib/dovecot/sieve/report-ham.sieve
|
||||
|
||||
for file in /var/vmail/*/*/sieve/*.sieve ; do
|
||||
if [[ "$file" == "/var/vmail/*/*/sieve/*.sieve" ]]; then
|
||||
continue
|
||||
fi
|
||||
sievec "$file" "$(dirname "$file")/../.dovecot.svbin"
|
||||
chown vmail:vmail "$(dirname "$file")/../.dovecot.svbin"
|
||||
done
|
||||
|
||||
# Fix permissions
|
||||
chown root:root /etc/dovecot/sql/*.conf
|
||||
chown root:dovecot /etc/dovecot/sql/dovecot-dict-sql-sieve* /etc/dovecot/sql/dovecot-dict-sql-quota* /etc/dovecot/lua/passwd-verify.lua
|
||||
|
|
|
@ -8492,6 +8492,7 @@ sub xoauth2
|
|||
require HTML::Entities ;
|
||||
require JSON ;
|
||||
require JSON::WebToken::Crypt::RSA ;
|
||||
require Crypt::OpenSSL::PKCS12;
|
||||
require Crypt::OpenSSL::RSA ;
|
||||
require Encode::Byte ;
|
||||
require IO::Socket::SSL ;
|
||||
|
@ -8532,8 +8533,9 @@ sub xoauth2
|
|||
|
||||
$sync->{ debug } and myprint( "Service account: $iss\nKey file: $keyfile\nKey password: $keypass\n");
|
||||
|
||||
# Get private key from p12 file (would be better in perl...)
|
||||
$key = `openssl pkcs12 -in "$keyfile" -nodes -nocerts -passin pass:$keypass -nomacver`;
|
||||
# Get private key from p12 file
|
||||
my $pkcs12 = Crypt::OpenSSL::PKCS12->new_from_file($keyfile);
|
||||
$key = $pkcs12->private_key($keypass);
|
||||
|
||||
$sync->{ debug } and myprint( "Private key:\n$key\n");
|
||||
}
|
||||
|
|
|
@ -50,7 +50,7 @@ try:
|
|||
def query_mysql(query, headers = True, update = False):
|
||||
while True:
|
||||
try:
|
||||
cnx = mysql.connector.connect(unix_socket = '/var/run/mysqld/mysqld.sock', user=os.environ.get('DBUSER'), passwd=os.environ.get('DBPASS'), database=os.environ.get('DBNAME'), charset="utf8")
|
||||
cnx = mysql.connector.connect(unix_socket = '/var/run/mysqld/mysqld.sock', user=os.environ.get('DBUSER'), passwd=os.environ.get('DBPASS'), database=os.environ.get('DBNAME'), charset="utf8mb4", collation="utf8mb4_general_ci")
|
||||
except Exception as ex:
|
||||
print('%s - trying again...' % (ex))
|
||||
time.sleep(3)
|
||||
|
@ -166,4 +166,4 @@ try:
|
|||
notify_rcpt(record['rcpt'], record['counter'], record['quarantine_acl'], attrs['quarantine_category'])
|
||||
|
||||
finally:
|
||||
os.unlink(pidfile)
|
||||
os.unlink(pidfile)
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
FROM alpine:3.15
|
||||
LABEL maintainer "Andre Peters <andre.peters@servercow.de>"
|
||||
FROM alpine:3.17
|
||||
LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>"
|
||||
|
||||
ENV XTABLES_LIBDIR /usr/lib/xtables
|
||||
ENV PYTHON_IPTABLES_XTABLES_VERSION 12
|
||||
|
|
|
@ -64,28 +64,40 @@ def refreshF2boptions():
|
|||
global f2boptions
|
||||
global quit_now
|
||||
global exit_code
|
||||
|
||||
f2boptions = {}
|
||||
|
||||
if not r.get('F2B_OPTIONS'):
|
||||
f2boptions = {}
|
||||
f2boptions['ban_time'] = int
|
||||
f2boptions['max_attempts'] = int
|
||||
f2boptions['retry_window'] = int
|
||||
f2boptions['netban_ipv4'] = int
|
||||
f2boptions['netban_ipv6'] = int
|
||||
f2boptions['ban_time'] = r.get('F2B_BAN_TIME') or 1800
|
||||
f2boptions['max_attempts'] = r.get('F2B_MAX_ATTEMPTS') or 10
|
||||
f2boptions['retry_window'] = r.get('F2B_RETRY_WINDOW') or 600
|
||||
f2boptions['netban_ipv4'] = r.get('F2B_NETBAN_IPV4') or 32
|
||||
f2boptions['netban_ipv6'] = r.get('F2B_NETBAN_IPV6') or 128
|
||||
r.set('F2B_OPTIONS', json.dumps(f2boptions, ensure_ascii=False))
|
||||
f2boptions['ban_time'] = r.get('F2B_BAN_TIME')
|
||||
f2boptions['max_ban_time'] = r.get('F2B_MAX_BAN_TIME')
|
||||
f2boptions['ban_time_increment'] = r.get('F2B_BAN_TIME_INCREMENT')
|
||||
f2boptions['max_attempts'] = r.get('F2B_MAX_ATTEMPTS')
|
||||
f2boptions['retry_window'] = r.get('F2B_RETRY_WINDOW')
|
||||
f2boptions['netban_ipv4'] = r.get('F2B_NETBAN_IPV4')
|
||||
f2boptions['netban_ipv6'] = r.get('F2B_NETBAN_IPV6')
|
||||
else:
|
||||
try:
|
||||
f2boptions = {}
|
||||
f2boptions = json.loads(r.get('F2B_OPTIONS'))
|
||||
except ValueError:
|
||||
print('Error loading F2B options: F2B_OPTIONS is not json')
|
||||
quit_now = True
|
||||
exit_code = 2
|
||||
|
||||
verifyF2boptions(f2boptions)
|
||||
r.set('F2B_OPTIONS', json.dumps(f2boptions, ensure_ascii=False))
|
||||
|
||||
def verifyF2boptions(f2boptions):
|
||||
verifyF2boption(f2boptions,'ban_time', 1800)
|
||||
verifyF2boption(f2boptions,'max_ban_time', 10000)
|
||||
verifyF2boption(f2boptions,'ban_time_increment', True)
|
||||
verifyF2boption(f2boptions,'max_attempts', 10)
|
||||
verifyF2boption(f2boptions,'retry_window', 600)
|
||||
verifyF2boption(f2boptions,'netban_ipv4', 32)
|
||||
verifyF2boption(f2boptions,'netban_ipv6', 128)
|
||||
|
||||
def verifyF2boption(f2boptions, f2boption, f2bdefault):
|
||||
f2boptions[f2boption] = f2boptions[f2boption] if f2boption in f2boptions and f2boptions[f2boption] is not None else f2bdefault
|
||||
|
||||
def refreshF2bregex():
|
||||
global f2bregex
|
||||
global quit_now
|
||||
|
@ -94,12 +106,12 @@ def refreshF2bregex():
|
|||
f2bregex = {}
|
||||
f2bregex[1] = 'mailcow UI: Invalid password for .+ by ([0-9a-f\.:]+)'
|
||||
f2bregex[2] = 'Rspamd UI: Invalid password by ([0-9a-f\.:]+)'
|
||||
f2bregex[3] = 'warning: .*\[([0-9a-f\.:]+)\]: SASL .+ authentication failed'
|
||||
f2bregex[3] = 'warning: .*\[([0-9a-f\.:]+)\]: SASL .+ authentication failed: (?!.*Connection lost to authentication server).+'
|
||||
f2bregex[4] = 'warning: non-SMTP command from .*\[([0-9a-f\.:]+)]:.+'
|
||||
f2bregex[5] = 'NOQUEUE: reject: RCPT from \[([0-9a-f\.:]+)].+Protocol error.+'
|
||||
f2bregex[6] = '-login: Disconnected \(auth failed, .+\): user=.*, method=.+, rip=([0-9a-f\.:]+),'
|
||||
f2bregex[7] = '-login: Aborted login \(auth failed .+\): user=.+, rip=([0-9a-f\.:]+), lip.+'
|
||||
f2bregex[8] = '-login: Aborted login \(tried to use disallowed .+\): user=.+, rip=([0-9a-f\.:]+), lip.+'
|
||||
f2bregex[6] = '-login: Disconnected.+ \(auth failed, .+\): user=.*, method=.+, rip=([0-9a-f\.:]+),'
|
||||
f2bregex[7] = '-login: Aborted login.+ \(auth failed .+\): user=.+, rip=([0-9a-f\.:]+), lip.+'
|
||||
f2bregex[8] = '-login: Aborted login.+ \(tried to use disallowed .+\): user=.+, rip=([0-9a-f\.:]+), lip.+'
|
||||
f2bregex[9] = 'SOGo.+ Login from \'([0-9a-f\.:]+)\' for user .+ might not have worked'
|
||||
f2bregex[10] = '([0-9a-f\.:]+) \"GET \/SOGo\/.* HTTP.+\" 403 .+'
|
||||
r.set('F2B_REGEX', json.dumps(f2bregex, ensure_ascii=False))
|
||||
|
@ -147,6 +159,7 @@ def ban(address):
|
|||
global lock
|
||||
refreshF2boptions()
|
||||
BAN_TIME = int(f2boptions['ban_time'])
|
||||
BAN_TIME_INCREMENT = bool(f2boptions['ban_time_increment'])
|
||||
MAX_ATTEMPTS = int(f2boptions['max_attempts'])
|
||||
RETRY_WINDOW = int(f2boptions['retry_window'])
|
||||
NETBAN_IPV4 = '/' + str(f2boptions['netban_ipv4'])
|
||||
|
@ -174,20 +187,16 @@ def ban(address):
|
|||
net = ipaddress.ip_network((address + (NETBAN_IPV4 if type(ip) is ipaddress.IPv4Address else NETBAN_IPV6)), strict=False)
|
||||
net = str(net)
|
||||
|
||||
if not net in bans or time.time() - bans[net]['last_attempt'] > RETRY_WINDOW:
|
||||
bans[net] = { 'attempts': 0 }
|
||||
active_window = RETRY_WINDOW
|
||||
else:
|
||||
active_window = time.time() - bans[net]['last_attempt']
|
||||
if not net in bans:
|
||||
bans[net] = {'attempts': 0, 'last_attempt': 0, 'ban_counter': 0}
|
||||
|
||||
bans[net]['attempts'] += 1
|
||||
bans[net]['last_attempt'] = time.time()
|
||||
|
||||
active_window = time.time() - bans[net]['last_attempt']
|
||||
|
||||
if bans[net]['attempts'] >= MAX_ATTEMPTS:
|
||||
cur_time = int(round(time.time()))
|
||||
logCrit('Banning %s for %d minutes' % (net, BAN_TIME / 60))
|
||||
NET_BAN_TIME = BAN_TIME if not BAN_TIME_INCREMENT else BAN_TIME * 2 ** bans[net]['ban_counter']
|
||||
logCrit('Banning %s for %d minutes' % (net, NET_BAN_TIME / 60 ))
|
||||
if type(ip) is ipaddress.IPv4Address:
|
||||
with lock:
|
||||
chain = iptc.Chain(iptc.Table(iptc.Table.FILTER), 'MAILCOW')
|
||||
|
@ -206,7 +215,7 @@ def ban(address):
|
|||
rule.target = target
|
||||
if rule not in chain.rules:
|
||||
chain.insert_rule(rule)
|
||||
r.hset('F2B_ACTIVE_BANS', '%s' % net, cur_time + BAN_TIME)
|
||||
r.hset('F2B_ACTIVE_BANS', '%s' % net, cur_time + NET_BAN_TIME)
|
||||
else:
|
||||
logWarn('%d more attempts in the next %d seconds until %s is banned' % (MAX_ATTEMPTS - bans[net]['attempts'], RETRY_WINDOW, net))
|
||||
|
||||
|
@ -238,7 +247,8 @@ def unban(net):
|
|||
r.hdel('F2B_ACTIVE_BANS', '%s' % net)
|
||||
r.hdel('F2B_QUEUE_UNBAN', '%s' % net)
|
||||
if net in bans:
|
||||
del bans[net]
|
||||
bans[net]['attempts'] = 0
|
||||
bans[net]['ban_counter'] += 1
|
||||
|
||||
def permBan(net, unban=False):
|
||||
global lock
|
||||
|
@ -252,7 +262,7 @@ def permBan(net, unban=False):
|
|||
if rule not in chain.rules and not unban:
|
||||
logCrit('Add host/network %s to blacklist' % net)
|
||||
chain.insert_rule(rule)
|
||||
r.hset('F2B_PERM_BANS', '%s' % net, int(round(time.time())))
|
||||
r.hset('F2B_PERM_BANS', '%s' % net, int(round(time.time())))
|
||||
elif rule in chain.rules and unban:
|
||||
logCrit('Remove host/network %s from blacklist' % net)
|
||||
chain.delete_rule(rule)
|
||||
|
@ -267,7 +277,7 @@ def permBan(net, unban=False):
|
|||
if rule not in chain.rules and not unban:
|
||||
logCrit('Add host/network %s to blacklist' % net)
|
||||
chain.insert_rule(rule)
|
||||
r.hset('F2B_PERM_BANS', '%s' % net, int(round(time.time())))
|
||||
r.hset('F2B_PERM_BANS', '%s' % net, int(round(time.time())))
|
||||
elif rule in chain.rules and unban:
|
||||
logCrit('Remove host/network %s from blacklist' % net)
|
||||
chain.delete_rule(rule)
|
||||
|
@ -332,7 +342,7 @@ def watch():
|
|||
logWarn('%s matched rule id %s (%s)' % (addr, rule_id, item['data']))
|
||||
ban(addr)
|
||||
except Exception as ex:
|
||||
logWarn('Error reading log line from pubsub')
|
||||
logWarn('Error reading log line from pubsub: %s' % ex)
|
||||
quit_now = True
|
||||
exit_code = 2
|
||||
|
||||
|
@ -346,6 +356,8 @@ def snat4(snat_target):
|
|||
rule.dst = '!' + rule.src
|
||||
target = rule.create_target("SNAT")
|
||||
target.to_source = snat_target
|
||||
match = rule.create_match("comment")
|
||||
match.comment = f'{int(round(time.time()))}'
|
||||
return rule
|
||||
|
||||
while not quit_now:
|
||||
|
@ -356,19 +368,35 @@ def snat4(snat_target):
|
|||
table.refresh()
|
||||
chain = iptc.Chain(table, 'POSTROUTING')
|
||||
table.autocommit = False
|
||||
if get_snat4_rule() not in chain.rules:
|
||||
logCrit('Added POSTROUTING rule for source network %s to SNAT target %s' % (get_snat4_rule().src, snat_target))
|
||||
chain.insert_rule(get_snat4_rule())
|
||||
table.commit()
|
||||
new_rule = get_snat4_rule()
|
||||
|
||||
if not chain.rules:
|
||||
# if there are no rules in the chain, insert the new rule directly
|
||||
logInfo(f'Added POSTROUTING rule for source network {new_rule.src} to SNAT target {snat_target}')
|
||||
chain.insert_rule(new_rule)
|
||||
else:
|
||||
for position, item in enumerate(chain.rules):
|
||||
if item == get_snat4_rule():
|
||||
if position != 0:
|
||||
chain.delete_rule(get_snat4_rule())
|
||||
table.commit()
|
||||
for position, rule in enumerate(chain.rules):
|
||||
if not hasattr(rule.target, 'parameter'):
|
||||
continue
|
||||
match = all((
|
||||
new_rule.get_src() == rule.get_src(),
|
||||
new_rule.get_dst() == rule.get_dst(),
|
||||
new_rule.target.parameters == rule.target.parameters,
|
||||
new_rule.target.name == rule.target.name
|
||||
))
|
||||
if position == 0:
|
||||
if not match:
|
||||
logInfo(f'Added POSTROUTING rule for source network {new_rule.src} to SNAT target {snat_target}')
|
||||
chain.insert_rule(new_rule)
|
||||
else:
|
||||
if match:
|
||||
logInfo(f'Remove rule for source network {new_rule.src} to SNAT target {snat_target} from POSTROUTING chain at position {position}')
|
||||
chain.delete_rule(rule)
|
||||
|
||||
table.commit()
|
||||
table.autocommit = True
|
||||
except:
|
||||
print('Error running SNAT4, retrying...')
|
||||
print('Error running SNAT4, retrying...')
|
||||
|
||||
def snat6(snat_target):
|
||||
global lock
|
||||
|
@ -402,13 +430,15 @@ def snat6(snat_target):
|
|||
table.commit()
|
||||
table.autocommit = True
|
||||
except:
|
||||
print('Error running SNAT6, retrying...')
|
||||
print('Error running SNAT6, retrying...')
|
||||
|
||||
def autopurge():
|
||||
while not quit_now:
|
||||
time.sleep(10)
|
||||
refreshF2boptions()
|
||||
BAN_TIME = int(f2boptions['ban_time'])
|
||||
MAX_BAN_TIME = int(f2boptions['max_ban_time'])
|
||||
BAN_TIME_INCREMENT = bool(f2boptions['ban_time_increment'])
|
||||
MAX_ATTEMPTS = int(f2boptions['max_attempts'])
|
||||
QUEUE_UNBAN = r.hgetall('F2B_QUEUE_UNBAN')
|
||||
if QUEUE_UNBAN:
|
||||
|
@ -416,7 +446,9 @@ def autopurge():
|
|||
unban(str(net))
|
||||
for net in bans.copy():
|
||||
if bans[net]['attempts'] >= MAX_ATTEMPTS:
|
||||
if time.time() - bans[net]['last_attempt'] > BAN_TIME:
|
||||
NET_BAN_TIME = BAN_TIME if not BAN_TIME_INCREMENT else BAN_TIME * 2 ** bans[net]['ban_counter']
|
||||
TIME_SINCE_LAST_ATTEMPT = time.time() - bans[net]['last_attempt']
|
||||
if TIME_SINCE_LAST_ATTEMPT > NET_BAN_TIME or TIME_SINCE_LAST_ATTEMPT > MAX_BAN_TIME:
|
||||
unban(net)
|
||||
|
||||
def isIpNetwork(address):
|
||||
|
@ -468,7 +500,7 @@ def whitelistUpdate():
|
|||
if Counter(new_whitelist) != Counter(WHITELIST):
|
||||
WHITELIST = new_whitelist
|
||||
logInfo('Whitelist was changed, it has %s entries' % len(WHITELIST))
|
||||
time.sleep(60.0 - ((time.time() - start_time) % 60.0))
|
||||
time.sleep(60.0 - ((time.time() - start_time) % 60.0))
|
||||
|
||||
def blacklistUpdate():
|
||||
global quit_now
|
||||
|
@ -479,7 +511,7 @@ def blacklistUpdate():
|
|||
new_blacklist = []
|
||||
if list:
|
||||
new_blacklist = genNetworkList(list)
|
||||
if Counter(new_blacklist) != Counter(BLACKLIST):
|
||||
if Counter(new_blacklist) != Counter(BLACKLIST):
|
||||
addban = set(new_blacklist).difference(BLACKLIST)
|
||||
delban = set(BLACKLIST).difference(new_blacklist)
|
||||
BLACKLIST = new_blacklist
|
||||
|
@ -490,7 +522,7 @@ def blacklistUpdate():
|
|||
if delban:
|
||||
for net in delban:
|
||||
permBan(net=net, unban=True)
|
||||
time.sleep(60.0 - ((time.time() - start_time) % 60.0))
|
||||
time.sleep(60.0 - ((time.time() - start_time) % 60.0))
|
||||
|
||||
def initChain():
|
||||
# Is called before threads start, no locking
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
FROM alpine:3.15
|
||||
LABEL maintainer "Andre Peters <andre.peters@servercow.de>"
|
||||
FROM alpine:3.17
|
||||
LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>"
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
|
|
|
@ -1,12 +1,18 @@
|
|||
FROM php:8.0-fpm-alpine3.14
|
||||
LABEL maintainer "Andre Peters <andre.peters@servercow.de>"
|
||||
FROM php:8.2-fpm-alpine3.17
|
||||
LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>"
|
||||
|
||||
ENV APCU_PECL 5.1.20
|
||||
ENV IMAGICK_PECL 3.5.1
|
||||
# Mailparse is pulled from master branch
|
||||
#ENV MAILPARSE_PECL 3.0.2
|
||||
ENV MEMCACHED_PECL 3.1.5
|
||||
ENV REDIS_PECL 5.3.4
|
||||
# renovate: datasource=github-tags depName=krakjoe/apcu versioning=semver-coerced
|
||||
ARG APCU_PECL_VERSION=5.1.22
|
||||
# renovate: datasource=github-tags depName=Imagick/imagick versioning=semver-coerced
|
||||
ARG IMAGICK_PECL_VERSION=3.7.0
|
||||
# renovate: datasource=github-tags depName=php/pecl-mail-mailparse versioning=semver-coerced
|
||||
ARG MAILPARSE_PECL_VERSION=3.1.4
|
||||
# renovate: datasource=github-tags depName=php-memcached-dev/php-memcached versioning=semver-coerced
|
||||
ARG MEMCACHED_PECL_VERSION=3.2.0
|
||||
# renovate: datasource=github-tags depName=phpredis/phpredis versioning=semver-coerced
|
||||
ARG REDIS_PECL_VERSION=5.3.7
|
||||
# renovate: datasource=github-tags depName=composer/composer versioning=semver-coerced
|
||||
ARG COMPOSER_VERSION=2.5.5
|
||||
|
||||
RUN apk add -U --no-cache autoconf \
|
||||
aspell-dev \
|
||||
|
@ -18,6 +24,7 @@ RUN apk add -U --no-cache autoconf \
|
|||
freetype-dev \
|
||||
g++ \
|
||||
git \
|
||||
gettext \
|
||||
gettext-dev \
|
||||
gmp-dev \
|
||||
gnupg \
|
||||
|
@ -27,8 +34,11 @@ RUN apk add -U --no-cache autoconf \
|
|||
imagemagick-dev \
|
||||
imap-dev \
|
||||
jq \
|
||||
libavif \
|
||||
libavif-dev \
|
||||
libjpeg-turbo \
|
||||
libjpeg-turbo-dev \
|
||||
libmemcached \
|
||||
libmemcached-dev \
|
||||
libpng \
|
||||
libpng-dev \
|
||||
|
@ -38,8 +48,11 @@ RUN apk add -U --no-cache autoconf \
|
|||
libtool \
|
||||
libwebp-dev \
|
||||
libxml2-dev \
|
||||
libxpm \
|
||||
libxpm-dev \
|
||||
libzip \
|
||||
libzip-dev \
|
||||
linux-headers \
|
||||
make \
|
||||
mysql-client \
|
||||
openldap-dev \
|
||||
|
@ -49,22 +62,24 @@ RUN apk add -U --no-cache autoconf \
|
|||
samba-client \
|
||||
zlib-dev \
|
||||
tzdata \
|
||||
&& git clone https://github.com/php/pecl-mail-mailparse \
|
||||
&& cd pecl-mail-mailparse \
|
||||
&& pecl install package.xml \
|
||||
&& cd .. \
|
||||
&& rm -r pecl-mail-mailparse \
|
||||
&& pecl install redis-${REDIS_PECL} memcached-${MEMCACHED_PECL} APCu-${APCU_PECL} imagick-${IMAGICK_PECL} \
|
||||
&& pecl install APCu-${APCU_PECL_VERSION} \
|
||||
&& pecl install imagick-${IMAGICK_PECL_VERSION} \
|
||||
&& pecl install mailparse-${MAILPARSE_PECL_VERSION} \
|
||||
&& pecl install memcached-${MEMCACHED_PECL_VERSION} \
|
||||
&& pecl install redis-${REDIS_PECL_VERSION} \
|
||||
&& docker-php-ext-enable apcu imagick memcached mailparse redis \
|
||||
&& pecl clear-cache \
|
||||
&& docker-php-ext-configure intl \
|
||||
&& docker-php-ext-configure exif \
|
||||
&& docker-php-ext-configure gd --with-freetype=/usr/include/ \
|
||||
--with-jpeg=/usr/include/ \
|
||||
&& docker-php-ext-install -j 4 exif gd gettext intl ldap opcache pcntl pdo pdo_mysql pspell soap sockets zip bcmath gmp \
|
||||
--with-webp \
|
||||
--with-xpm \
|
||||
--with-avif \
|
||||
&& docker-php-ext-install -j 4 exif gd gettext intl ldap opcache pcntl pdo pdo_mysql pspell soap sockets sysvsem zip bcmath gmp \
|
||||
&& docker-php-ext-configure imap --with-imap --with-imap-ssl \
|
||||
&& docker-php-ext-install -j 4 imap \
|
||||
&& curl --silent --show-error https://getcomposer.org/installer | php \
|
||||
&& curl --silent --show-error https://getcomposer.org/installer | php -- --version=${COMPOSER_VERSION} \
|
||||
&& mv composer.phar /usr/local/bin/composer \
|
||||
&& chmod +x /usr/local/bin/composer \
|
||||
&& apk del --purge autoconf \
|
||||
|
@ -72,15 +87,22 @@ RUN apk add -U --no-cache autoconf \
|
|||
cyrus-sasl-dev \
|
||||
freetype-dev \
|
||||
g++ \
|
||||
gettext-dev \
|
||||
icu-dev \
|
||||
imagemagick-dev \
|
||||
imap-dev \
|
||||
libavif-dev \
|
||||
libjpeg-turbo-dev \
|
||||
libmemcached-dev \
|
||||
libpng-dev \
|
||||
libressl-dev \
|
||||
libwebp-dev \
|
||||
libxml2-dev \
|
||||
libxpm-dev \
|
||||
libzip-dev \
|
||||
linux-headers \
|
||||
make \
|
||||
openldap-dev \
|
||||
pcre-dev \
|
||||
zlib-dev
|
||||
|
||||
|
@ -88,4 +110,4 @@ COPY ./docker-entrypoint.sh /
|
|||
|
||||
ENTRYPOINT ["/docker-entrypoint.sh"]
|
||||
|
||||
CMD ["php-fpm"]
|
||||
CMD ["php-fpm"]
|
|
@ -172,6 +172,24 @@ BEGIN
|
|||
END;
|
||||
//
|
||||
DELIMITER ;
|
||||
DROP EVENT IF EXISTS clean_sasl_log;
|
||||
DELIMITER //
|
||||
CREATE EVENT clean_sasl_log
|
||||
ON SCHEDULE EVERY 1 DAY DO
|
||||
BEGIN
|
||||
DELETE sasl_log.* FROM sasl_log
|
||||
LEFT JOIN (
|
||||
SELECT username, service, MAX(datetime) AS lastdate
|
||||
FROM sasl_log
|
||||
GROUP BY username, service
|
||||
) AS last ON sasl_log.username = last.username AND sasl_log.service = last.service
|
||||
WHERE datetime < DATE_SUB(NOW(), INTERVAL 31 DAY) AND datetime < lastdate;
|
||||
DELETE FROM sasl_log
|
||||
WHERE username NOT IN (SELECT username FROM mailbox) AND
|
||||
datetime < DATE_SUB(NOW(), INTERVAL 31 DAY);
|
||||
END;
|
||||
//
|
||||
DELIMITER ;
|
||||
EOF
|
||||
fi
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
FROM debian:buster-slim
|
||||
LABEL maintainer "Andre Peters <andre.peters@servercow.de>"
|
||||
FROM debian:bullseye-slim
|
||||
LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>"
|
||||
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
ENV LC_ALL C
|
||||
|
@ -17,10 +17,10 @@ RUN groupadd -g 102 postfix \
|
|||
ca-certificates \
|
||||
curl \
|
||||
dirmngr \
|
||||
dnsutils \
|
||||
dnsutils \
|
||||
gnupg \
|
||||
libsasl2-modules \
|
||||
mariadb-client \
|
||||
mariadb-client \
|
||||
perl \
|
||||
postfix \
|
||||
postfix-mysql \
|
||||
|
@ -32,7 +32,7 @@ RUN groupadd -g 102 postfix \
|
|||
syslog-ng \
|
||||
syslog-ng-core \
|
||||
syslog-ng-mod-redis \
|
||||
tzdata \
|
||||
tzdata \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& touch /etc/default/locale \
|
||||
&& printf '#!/bin/bash\n/usr/sbin/postconf -c /opt/postfix/conf "$@"' > /usr/local/sbin/postconf \
|
||||
|
|
|
@ -323,7 +323,19 @@ hosts = unix:/var/run/mysqld/mysqld.sock
|
|||
dbname = ${DBNAME}
|
||||
# First select queries domain and alias_domain to determine if domains are active.
|
||||
query = SELECT goto FROM alias
|
||||
WHERE address='%s'
|
||||
WHERE id IN (
|
||||
SELECT COALESCE (
|
||||
(
|
||||
SELECT id FROM alias
|
||||
WHERE address='%s'
|
||||
AND (active='1' OR active='2')
|
||||
), (
|
||||
SELECT id FROM alias
|
||||
WHERE address='@%d'
|
||||
AND (active='1' OR active='2')
|
||||
)
|
||||
)
|
||||
)
|
||||
AND active='1'
|
||||
AND (domain IN
|
||||
(SELECT domain FROM domain
|
||||
|
@ -354,7 +366,7 @@ query = SELECT goto FROM alias
|
|||
WHERE alias_domain.alias_domain = '%d'
|
||||
AND mailbox.username = CONCAT('%u','@',alias_domain.target_domain)
|
||||
AND (mailbox.active = '1' OR mailbox.active ='2')
|
||||
AND alias_domain.active='1'
|
||||
AND alias_domain.active='1';
|
||||
EOF
|
||||
|
||||
# MX based routing
|
||||
|
@ -381,12 +393,101 @@ query = SELECT goto FROM spamalias
|
|||
AND validity >= UNIX_TIMESTAMP()
|
||||
EOF
|
||||
|
||||
sed -i '/User overrides/q' /opt/postfix/conf/main.cf
|
||||
if [ ! -f /opt/postfix/conf/dns_blocklists.cf ]; then
|
||||
cat <<EOF > /opt/postfix/conf/dns_blocklists.cf
|
||||
# This file can be edited.
|
||||
# Delete this file and restart postfix container to revert any changes.
|
||||
postscreen_dnsbl_sites = wl.mailspike.net=127.0.0.[18;19;20]*-2
|
||||
hostkarma.junkemailfilter.com=127.0.0.1*-2
|
||||
list.dnswl.org=127.0.[0..255].0*-2
|
||||
list.dnswl.org=127.0.[0..255].1*-4
|
||||
list.dnswl.org=127.0.[0..255].2*-6
|
||||
list.dnswl.org=127.0.[0..255].3*-8
|
||||
ix.dnsbl.manitu.net*2
|
||||
bl.spamcop.net*2
|
||||
bl.suomispam.net*2
|
||||
hostkarma.junkemailfilter.com=127.0.0.2*3
|
||||
hostkarma.junkemailfilter.com=127.0.0.4*2
|
||||
hostkarma.junkemailfilter.com=127.0.1.2*1
|
||||
backscatter.spameatingmonkey.net*2
|
||||
bl.ipv6.spameatingmonkey.net*2
|
||||
bl.spameatingmonkey.net*2
|
||||
b.barracudacentral.org=127.0.0.2*7
|
||||
bl.mailspike.net=127.0.0.2*5
|
||||
bl.mailspike.net=127.0.0.[10;11;12]*4
|
||||
dnsbl.sorbs.net=127.0.0.10*8
|
||||
dnsbl.sorbs.net=127.0.0.5*6
|
||||
dnsbl.sorbs.net=127.0.0.7*3
|
||||
dnsbl.sorbs.net=127.0.0.8*2
|
||||
dnsbl.sorbs.net=127.0.0.6*2
|
||||
dnsbl.sorbs.net=127.0.0.9*2
|
||||
EOF
|
||||
fi
|
||||
DNSBL_CONFIG=$(grep -v '^#' /opt/postfix/conf/dns_blocklists.cf | grep '\S')
|
||||
|
||||
if [ ! -z "$DNSBL_CONFIG" ]; then
|
||||
echo -e "\e[33mChecking if ASN for your IP is listed for Spamhaus Bad ASN List...\e[0m"
|
||||
if [ -n "$SPAMHAUS_DQS_KEY" ]; then
|
||||
echo -e "\e[32mDetected SPAMHAUS_DQS_KEY variable from mailcow.conf...\e[0m"
|
||||
echo -e "\e[33mUsing DQS Blocklists from Spamhaus!\e[0m"
|
||||
SPAMHAUS_DNSBL_CONFIG=$(cat <<EOF
|
||||
${SPAMHAUS_DQS_KEY}.zen.dq.spamhaus.net=127.0.0.[4..7]*6
|
||||
${SPAMHAUS_DQS_KEY}.zen.dq.spamhaus.net=127.0.0.[10;11]*8
|
||||
${SPAMHAUS_DQS_KEY}.zen.dq.spamhaus.net=127.0.0.3*4
|
||||
${SPAMHAUS_DQS_KEY}.zen.dq.spamhaus.net=127.0.0.2*3
|
||||
postscreen_dnsbl_reply_map = texthash:/opt/postfix/conf/dnsbl_reply.map
|
||||
EOF
|
||||
|
||||
cat <<EOF > /opt/postfix/conf/dnsbl_reply.map
|
||||
# Autogenerated by mailcow, using Spamhaus DQS reply domains
|
||||
${SPAMHAUS_DQS_KEY}.sbl.dq.spamhaus.net sbl.spamhaus.org
|
||||
${SPAMHAUS_DQS_KEY}.xbl.dq.spamhaus.net xbl.spamhaus.org
|
||||
${SPAMHAUS_DQS_KEY}.pbl.dq.spamhaus.net pbl.spamhaus.org
|
||||
${SPAMHAUS_DQS_KEY}.zen.dq.spamhaus.net zen.spamhaus.org
|
||||
${SPAMHAUS_DQS_KEY}.dbl.dq.spamhaus.net dbl.spamhaus.org
|
||||
${SPAMHAUS_DQS_KEY}.zrd.dq.spamhaus.net zrd.spamhaus.org
|
||||
EOF
|
||||
)
|
||||
else
|
||||
if [ -f "/opt/postfix/conf/dnsbl_reply.map" ]; then
|
||||
rm /opt/postfix/conf/dnsbl_reply.map
|
||||
fi
|
||||
response=$(curl --connect-timeout 15 --max-time 30 -s -o /dev/null -w "%{http_code}" "https://asn-check.mailcow.email")
|
||||
if [ "$response" -eq 503 ]; then
|
||||
echo -e "\e[31mThe AS of your IP is listed as a banned AS from Spamhaus!\e[0m"
|
||||
echo -e "\e[33mNo SPAMHAUS_DQS_KEY found... Skipping Spamhaus blocklists entirely!\e[0m"
|
||||
SPAMHAUS_DNSBL_CONFIG=""
|
||||
elif [ "$response" -eq 200 ]; then
|
||||
echo -e "\e[32mThe AS of your IP is NOT listed as a banned AS from Spamhaus!\e[0m"
|
||||
echo -e "\e[33mUsing the open Spamhaus blocklists.\e[0m"
|
||||
SPAMHAUS_DNSBL_CONFIG=$(cat <<EOF
|
||||
zen.spamhaus.org=127.0.0.[10;11]*8
|
||||
zen.spamhaus.org=127.0.0.[4..7]*6
|
||||
zen.spamhaus.org=127.0.0.3*4
|
||||
zen.spamhaus.org=127.0.0.2*3
|
||||
EOF
|
||||
)
|
||||
|
||||
else
|
||||
echo -e "\e[31mWe couldn't determine your AS... (maybe DNS/Network issue?) Response Code: $response\e[0m"
|
||||
echo -e "\e[33mDeactivating Spamhaus DNS Blocklists to be on the safe site!\e[0m"
|
||||
SPAMHAUS_DNSBL_CONFIG=""
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# Reset main.cf
|
||||
sed -i '/Overrides/q' /opt/postfix/conf/main.cf
|
||||
echo >> /opt/postfix/conf/main.cf
|
||||
# Append postscreen dnsbl sites to main.cf
|
||||
if [ ! -z "$DNSBL_CONFIG" ]; then
|
||||
echo -e "${DNSBL_CONFIG}\n${SPAMHAUS_DNSBL_CONFIG}" >> /opt/postfix/conf/main.cf
|
||||
fi
|
||||
# Append user overrides
|
||||
echo -e "\n# User Overrides" >> /opt/postfix/conf/main.cf
|
||||
touch /opt/postfix/conf/extra.cf
|
||||
sed -i '/myhostname/d' /opt/postfix/conf/extra.cf
|
||||
echo -e "myhostname = ${MAILCOW_HOSTNAME}\n$(cat /opt/postfix/conf/extra.cf)" > /opt/postfix/conf/extra.cf
|
||||
|
||||
cat /opt/postfix/conf/extra.cf >> /opt/postfix/conf/main.cf
|
||||
|
||||
if [ ! -f /opt/postfix/conf/custom_transport.pcre ]; then
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
@version: 3.19
|
||||
@version: 3.28
|
||||
@include "scl.conf"
|
||||
options {
|
||||
chain_hostnames(off);
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
@version: 3.19
|
||||
@version: 3.28
|
||||
@include "scl.conf"
|
||||
options {
|
||||
chain_hostnames(off);
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
FROM debian:bullseye-slim
|
||||
LABEL maintainer "Andre Peters <andre.peters@tinc.gmbh>"
|
||||
LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>"
|
||||
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
ARG CODENAME=bullseye
|
||||
|
@ -26,6 +26,7 @@ RUN apt-get update && apt-get install -y \
|
|||
|
||||
COPY settings.conf /etc/rspamd/settings.conf
|
||||
COPY metadata_exporter.lua /usr/share/rspamd/plugins/metadata_exporter.lua
|
||||
COPY set_worker_password.sh /set_worker_password.sh
|
||||
COPY docker-entrypoint.sh /docker-entrypoint.sh
|
||||
|
||||
ENTRYPOINT ["/docker-entrypoint.sh"]
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
#!/bin/bash
|
||||
|
||||
password_file='/etc/rspamd/override.d/worker-controller-password.inc'
|
||||
password_hash=`/usr/bin/rspamadm pw -e -p $1`
|
||||
|
||||
echo 'enable_password = "'$password_hash'";' > $password_file
|
||||
|
||||
if grep -q "$password_hash" "$password_file"; then
|
||||
echo "OK"
|
||||
else
|
||||
echo "ERROR"
|
||||
fi
|
|
@ -1,10 +1,11 @@
|
|||
FROM debian:bullseye-slim
|
||||
LABEL maintainer "Andre Peters <andre.peters@servercow.de>"
|
||||
LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>"
|
||||
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
ARG SOGO_DEBIAN_REPOSITORY=http://packages.inverse.ca/SOGo/nightly/5/debian/
|
||||
ARG SOGO_DEBIAN_REPOSITORY=http://packages.sogo.nu/nightly/5/debian/
|
||||
# renovate: datasource=github-releases depName=tianon/gosu versioning=semver-coerced
|
||||
ARG GOSU_VERSION=1.16
|
||||
ENV LC_ALL C
|
||||
ENV GOSU_VERSION 1.14
|
||||
|
||||
# Prerequisites
|
||||
RUN echo "Building from repository $SOGO_DEBIAN_REPOSITORY" \
|
||||
|
@ -30,7 +31,7 @@ RUN echo "Building from repository $SOGO_DEBIAN_REPOSITORY" \
|
|||
&& gosu nobody true \
|
||||
&& mkdir /usr/share/doc/sogo \
|
||||
&& touch /usr/share/doc/sogo/empty.sh \
|
||||
&& apt-key adv --keyserver keyserver.ubuntu.com --recv-key 0x810273C4 \
|
||||
&& apt-key adv --keyserver keys.openpgp.org --recv-key 74FFC6D72B925A34B5D356BDF8A27B36A6E2EAE9 \
|
||||
&& echo "deb ${SOGO_DEBIAN_REPOSITORY} bullseye bullseye" > /etc/apt/sources.list.d/sogo.list \
|
||||
&& apt-get update && apt-get install -y --no-install-recommends \
|
||||
sogo \
|
||||
|
@ -52,4 +53,4 @@ RUN chmod +x /bootstrap-sogo.sh \
|
|||
|
||||
ENTRYPOINT ["/docker-entrypoint.sh"]
|
||||
|
||||
CMD exec /usr/bin/supervisord -c /etc/supervisor/supervisord.conf
|
||||
CMD exec /usr/bin/supervisord -c /etc/supervisor/supervisord.conf
|
|
@ -142,6 +142,10 @@ cat <<EOF > /var/lib/sogo/GNUstep/Defaults/sogod.plist
|
|||
<string>mysql://${DBUSER}:${DBPASS}@%2Fvar%2Frun%2Fmysqld%2Fmysqld.sock/${DBNAME}/sogo_acl</string>
|
||||
<key>SOGoIMAPServer</key>
|
||||
<string>imap://${IPV4_NETWORK}.250:143/?TLS=YES&tlsVerifyMode=none</string>
|
||||
<key>SOGoSieveServer</key>
|
||||
<string>sieve://${IPV4_NETWORK}.250:4190/?TLS=YES&tlsVerifyMode=none</string>
|
||||
<key>SOGoSMTPServer</key>
|
||||
<string>smtp://${IPV4_NETWORK}.253:588/?TLS=YES&tlsVerifyMode=none</string>
|
||||
<key>SOGoTrustProxyAuthentication</key>
|
||||
<string>YES</string>
|
||||
<key>SOGoEncryptionKey</key>
|
||||
|
|
|
@ -2,7 +2,8 @@ FROM solr:7.7-slim
|
|||
|
||||
USER root
|
||||
|
||||
ENV GOSU_VERSION 1.11
|
||||
# renovate: datasource=github-releases depName=tianon/gosu versioning=semver-coerced
|
||||
ARG GOSU_VERSION=1.16
|
||||
|
||||
COPY solr.sh /
|
||||
COPY solr-config-7.7.0.xml /
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
FROM alpine:3.15
|
||||
FROM alpine:3.17
|
||||
|
||||
LABEL maintainer "Andre Peters <andre.peters@servercow.de>"
|
||||
LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>"
|
||||
|
||||
RUN apk add --update --no-cache \
|
||||
curl \
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
FROM alpine:3.15
|
||||
FROM alpine:3.17
|
||||
LABEL maintainer "André Peters <andre.peters@servercow.de>"
|
||||
|
||||
# Installation
|
||||
|
|
|
@ -24,7 +24,7 @@ server {
|
|||
add_header X-Download-Options "noopen" always;
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
add_header X-Permitted-Cross-Domain-Policies "none" always;
|
||||
add_header X-Robots-Tag "none" always;
|
||||
add_header X-Robots-Tag "noindex, nofollow" always;
|
||||
add_header X-XSS-Protection "1; mode=block" always;
|
||||
|
||||
fastcgi_hide_header X-Powered-By;
|
||||
|
|
|
@ -24,6 +24,11 @@ mail_plugins = </etc/dovecot/mail_plugins
|
|||
mail_attachment_fs = crypt:set_prefix=mail_crypt_global:posix:
|
||||
mail_attachment_dir = /var/attachments
|
||||
mail_attachment_min_size = 128k
|
||||
# Significantly speeds up very large mailboxes, but is only safe to enable if
|
||||
# you do not manually modify the files in the `cur` directories in
|
||||
# mailcowdockerized_vmail-vol-1.
|
||||
# https://docs.mailcow.email/manual-guides/Dovecot/u_e-dovecot-performance/
|
||||
maildir_very_dirty_syncs = yes
|
||||
|
||||
# Dovecot 2.2
|
||||
#ssl_protocols = !SSLv3
|
||||
|
|
|
@ -65,7 +65,7 @@
|
|||
}
|
||||
|
||||
location ~ ^/api/v1/(.*)$ {
|
||||
try_files $uri $uri/ /json_api.php?query=$1;
|
||||
try_files $uri $uri/ /json_api.php?query=$1&$args;
|
||||
}
|
||||
|
||||
location ^~ /.well-known/acme-challenge/ {
|
||||
|
@ -114,7 +114,7 @@
|
|||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_redirect off;
|
||||
error_page 403 /_rspamderror.php;
|
||||
error_page 401 /_rspamderror.php;
|
||||
}
|
||||
proxy_pass http://rspamd:11334/;
|
||||
proxy_set_header Host $http_host;
|
||||
|
@ -163,7 +163,9 @@
|
|||
proxy_connect_timeout 75;
|
||||
proxy_send_timeout 3600;
|
||||
proxy_read_timeout 3600;
|
||||
proxy_buffers 64 256k;
|
||||
proxy_buffer_size 128k;
|
||||
proxy_buffers 64 512k;
|
||||
proxy_busy_buffers_size 512k;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header Host $http_host;
|
||||
|
@ -197,6 +199,9 @@
|
|||
proxy_set_header x-webobjects-server-name $server_name;
|
||||
proxy_set_header x-webobjects-server-url $client_req_scheme://$http_host;
|
||||
proxy_set_header x-webobjects-server-port $server_port;
|
||||
proxy_buffer_size 128k;
|
||||
proxy_buffers 64 512k;
|
||||
proxy_busy_buffers_size 512k;
|
||||
proxy_send_timeout 3600;
|
||||
proxy_read_timeout 3600;
|
||||
client_body_buffer_size 128k;
|
||||
|
|
|
@ -40,34 +40,6 @@ postscreen_blacklist_action = drop
|
|||
postscreen_cache_cleanup_interval = 24h
|
||||
postscreen_cache_map = proxy:btree:$data_directory/postscreen_cache
|
||||
postscreen_dnsbl_action = enforce
|
||||
postscreen_dnsbl_sites = wl.mailspike.net=127.0.0.[18;19;20]*-2
|
||||
hostkarma.junkemailfilter.com=127.0.0.1*-2
|
||||
list.dnswl.org=127.0.[0..255].0*-2
|
||||
list.dnswl.org=127.0.[0..255].1*-4
|
||||
list.dnswl.org=127.0.[0..255].2*-6
|
||||
list.dnswl.org=127.0.[0..255].3*-8
|
||||
ix.dnsbl.manitu.net*2
|
||||
bl.spamcop.net*2
|
||||
bl.suomispam.net*2
|
||||
hostkarma.junkemailfilter.com=127.0.0.2*3
|
||||
hostkarma.junkemailfilter.com=127.0.0.4*2
|
||||
hostkarma.junkemailfilter.com=127.0.1.2*1
|
||||
backscatter.spameatingmonkey.net*2
|
||||
bl.ipv6.spameatingmonkey.net*2
|
||||
bl.spameatingmonkey.net*2
|
||||
b.barracudacentral.org=127.0.0.2*7
|
||||
bl.mailspike.net=127.0.0.2*5
|
||||
bl.mailspike.net=127.0.0.[10;11;12]*4
|
||||
dnsbl.sorbs.net=127.0.0.10*8
|
||||
dnsbl.sorbs.net=127.0.0.5*6
|
||||
dnsbl.sorbs.net=127.0.0.7*3
|
||||
dnsbl.sorbs.net=127.0.0.8*2
|
||||
dnsbl.sorbs.net=127.0.0.6*2
|
||||
dnsbl.sorbs.net=127.0.0.9*2
|
||||
zen.spamhaus.org=127.0.0.[10;11]*8
|
||||
zen.spamhaus.org=127.0.0.[4..7]*6
|
||||
zen.spamhaus.org=127.0.0.3*4
|
||||
zen.spamhaus.org=127.0.0.2*3
|
||||
postscreen_dnsbl_threshold = 6
|
||||
postscreen_dnsbl_ttl = 5m
|
||||
postscreen_greet_action = enforce
|
||||
|
@ -197,4 +169,4 @@ smtps_smtpd_tls_mandatory_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1
|
|||
parent_domain_matches_subdomains = debug_peer_list,fast_flush_domains,mynetworks,qmqpd_authorized_clients
|
||||
|
||||
# DO NOT EDIT ANYTHING BELOW #
|
||||
# User overrides #
|
||||
# Overrides #
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -27,4 +27,5 @@
|
|||
#197518 2 #Rackmarkt SL, Spain
|
||||
#197695 2 #Domain names registrar REG.RU Ltd, Russia
|
||||
#198068 2 #P.A.G.M. OU, Estonia
|
||||
#201942 5 #Soltia Consulting SL, Spain
|
||||
#201942 5 #Soltia Consulting SL, Spain
|
||||
#213373 4 #IP Connect Inc
|
|
@ -3,7 +3,6 @@
|
|||
/.*episerver.*/i
|
||||
/.*supergewinne.*/i
|
||||
/List-Unsubscribe.*nbps\.eu/i
|
||||
/X-Mailer: AWeber.*/i
|
||||
/.*regiofinder.*/i
|
||||
/.*EmailSocket.*/i
|
||||
/List-Unsubscribe:.*respread.*/i
|
||||
|
|
|
@ -8,7 +8,7 @@ VIRUS_FOUND {
|
|||
}
|
||||
# Bad policy from free mail providers
|
||||
FREEMAIL_POLICY_FAILURE {
|
||||
expression = "-g+:policies & !DMARC_POLICY_ALLOW & !MAILLIST & ( FREEMAIL_ENVFROM | FREEMAIL_FROM ) & !WHITELISTED_FWD_HOST";
|
||||
expression = "FREEMAIL_FROM & !DMARC_POLICY_ALLOW & !MAILLIST& !WHITELISTED_FWD_HOST & -g+:policies";
|
||||
score = 16.0;
|
||||
}
|
||||
# Applies to freemail with undisclosed recipients
|
||||
|
@ -68,3 +68,39 @@ WL_FWD_HOST {
|
|||
ENCRYPTED_CHAT {
|
||||
expression = "CHAT_VERSION_HEADER & ENCRYPTED_PGP";
|
||||
}
|
||||
|
||||
CLAMD_SPAM_FOUND {
|
||||
expression = "CLAM_SECI_SPAM & !MAILCOW_WHITE";
|
||||
description = "Probably Spam, Securite Spam Flag set through ClamAV";
|
||||
score = 5;
|
||||
}
|
||||
|
||||
CLAMD_BAD_PDF {
|
||||
expression = "CLAM_SECI_PDF & !MAILCOW_WHITE";
|
||||
description = "Bad PDF Found, Securite bad PDF Flag set through ClamAV";
|
||||
score = 8;
|
||||
}
|
||||
|
||||
CLAMD_BAD_JPG {
|
||||
expression = "CLAM_SECI_JPG & !MAILCOW_WHITE";
|
||||
description = "Bad JPG Found, Securite bad JPG Flag set through ClamAV";
|
||||
score = 8;
|
||||
}
|
||||
|
||||
CLAMD_ASCII_MALWARE {
|
||||
expression = "CLAM_SECI_ASCII & !MAILCOW_WHITE";
|
||||
description = "ASCII malware found, Securite ASCII malware Flag set through ClamAV";
|
||||
score = 8;
|
||||
}
|
||||
|
||||
CLAMD_HTML_MALWARE {
|
||||
expression = "CLAM_SECI_HTML & !MAILCOW_WHITE";
|
||||
description = "HTML malware found, Securite HTML malware Flag set through ClamAV";
|
||||
score = 8;
|
||||
}
|
||||
|
||||
CLAMD_JS_MALWARE {
|
||||
expression = "CLAM_SECI_JS & !MAILCOW_WHITE";
|
||||
description = "JS malware found, Securite JS malware Flag set through ClamAV";
|
||||
score = 8;
|
||||
}
|
|
@ -18,6 +18,9 @@ symbols {
|
|||
"ENCRYPTED_CHAT" {
|
||||
score = -20.0;
|
||||
}
|
||||
"SOGO_CONTACT" {
|
||||
score = -99.0;
|
||||
}
|
||||
}
|
||||
|
||||
group "MX" {
|
||||
|
|
|
@ -16,8 +16,7 @@ rules {
|
|||
backend = "http";
|
||||
url = "http://nginx:9081/pushover.php";
|
||||
selector = "mailcow_rcpt";
|
||||
# Only return msgid, do not parse the full message
|
||||
formatter = "msgid";
|
||||
formatter = "json";
|
||||
meta_headers = true;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -159,8 +159,8 @@ BAZAAR_ABUSE_CH {
|
|||
}
|
||||
|
||||
URLHAUS_ABUSE_CH {
|
||||
type = "url";
|
||||
filter = "full";
|
||||
type = "selector";
|
||||
selector = "urls";
|
||||
map = "https://urlhaus.abuse.ch/downloads/text_online/";
|
||||
score = 10.0;
|
||||
}
|
||||
|
@ -175,7 +175,7 @@ BAD_SUBJECT_00 {
|
|||
type = "header";
|
||||
header = "subject";
|
||||
regexp = true;
|
||||
map = "http://nullnull.org/bad-subject-regex.txt";
|
||||
map = "http://fuzzy.mailcow.email/bad-subject-regex.txt";
|
||||
score = 6.0;
|
||||
symbols_set = ["BAD_SUBJECT_00"];
|
||||
}
|
||||
|
|
|
@ -1,24 +0,0 @@
|
|||
rules {
|
||||
"LONG" {
|
||||
train {
|
||||
max_trains = 200;
|
||||
max_usages = 20;
|
||||
max_iterations = 25;
|
||||
learning_rate = 0.01,
|
||||
}
|
||||
symbol_spam = "NEURAL_SPAM_LONG";
|
||||
symbol_ham = "NEURAL_HAM_LONG";
|
||||
ann_expire = 45d;
|
||||
}
|
||||
"SHORT" {
|
||||
train {
|
||||
max_trains = 100;
|
||||
max_usages = 10;
|
||||
max_iterations = 15;
|
||||
learning_rate = 0.01,
|
||||
}
|
||||
symbol_spam = "NEURAL_SPAM_SHORT";
|
||||
symbol_ham = "NEURAL_HAM_SHORT";
|
||||
ann_expire = 7d;
|
||||
}
|
||||
}
|
|
@ -1,18 +0,0 @@
|
|||
symbols = {
|
||||
"NEURAL_SPAM_LONG" {
|
||||
weight = 3.7; # sample weight
|
||||
description = "Neural network spam (long)";
|
||||
}
|
||||
"NEURAL_HAM_LONG" {
|
||||
weight = -4.0; # sample weight
|
||||
description = "Neural network ham (long)";
|
||||
}
|
||||
"NEURAL_SPAM_SHORT" {
|
||||
weight = 2.5; # sample weight
|
||||
description = "Neural network spam (short)";
|
||||
}
|
||||
"NEURAL_HAM_SHORT" {
|
||||
weight = -2.0; # sample weight
|
||||
description = "Neural network ham (short)";
|
||||
}
|
||||
}
|
|
@ -1,61 +0,0 @@
|
|||
-- Thanks to https://raw.githubusercontent.com/fatalbanana
|
||||
|
||||
local lua_maps = require 'lua_maps'
|
||||
local rspamd_regexp = require 'rspamd_regexp'
|
||||
local rspamd_util = require 'rspamd_util'
|
||||
|
||||
local ivm_sendgrid_ids = lua_maps.map_add_from_ucl(
|
||||
'https://www.invaluement.com/spdata/sendgrid-id-dnsbl.txt',
|
||||
'set',
|
||||
'Invaluement Service Provider DNSBL: Sendgrid IDs'
|
||||
)
|
||||
|
||||
local ivm_sendgrid_envfromdomains = lua_maps.map_add_from_ucl(
|
||||
'https://www.invaluement.com/spdata/sendgrid-envelopefromdomain-dnsbl.txt',
|
||||
'set',
|
||||
'Invaluement Service Provider DNSBL: Sendgrid envelope domains'
|
||||
)
|
||||
|
||||
local cb_id = rspamd_config:register_symbol({
|
||||
name = 'IVM_SENDGRID',
|
||||
callback = function(task)
|
||||
-- Is it Sendgrid?
|
||||
local sg_hdr = task:get_header('X-SG-EID')
|
||||
if not sg_hdr then return end
|
||||
|
||||
-- Get original envelope from
|
||||
local env_from = task:get_from{'smtp', 'orig'}
|
||||
if not env_from then return end
|
||||
|
||||
-- Check normalised domain in domains list
|
||||
if ivm_sendgrid_envfromdomains and ivm_sendgrid_envfromdomains:get_key(rspamd_util.get_tld(env_from[1].domain)) then
|
||||
task:insert_result('IVM_SENDGRID_DOMAIN', 1.0)
|
||||
end
|
||||
|
||||
-- Check ID in ID list
|
||||
local lp_re = rspamd_regexp.create_cached([[^bounces\+(\d+)-]])
|
||||
local res = lp_re:search(env_from[1].user, true, true)
|
||||
if not res then return end
|
||||
if ivm_sendgrid_ids and ivm_sendgrid_ids:get_key(res[1][2]) then
|
||||
task:insert_result('IVM_SENDGRID_ID', 1.0)
|
||||
end
|
||||
end,
|
||||
description = 'Invaluement Service Provider DNSBL: Sendgrid',
|
||||
type = 'callback',
|
||||
})
|
||||
|
||||
rspamd_config:register_symbol({
|
||||
name = 'IVM_SENDGRID_DOMAIN',
|
||||
parent = cb_id,
|
||||
group = 'ivmspdnsbl',
|
||||
score = 8.0,
|
||||
type = 'virtual',
|
||||
})
|
||||
|
||||
rspamd_config:register_symbol({
|
||||
name = 'IVM_SENDGRID_ID',
|
||||
parent = cb_id,
|
||||
group = 'ivmspdnsbl',
|
||||
score = 8.0,
|
||||
type = 'virtual',
|
||||
})
|
|
@ -340,6 +340,10 @@ rspamd_config:register_symbol({
|
|||
if not bcc_dest then
|
||||
return -- stop
|
||||
end
|
||||
-- dot stuff content before sending
|
||||
local email_content = tostring(task:get_content())
|
||||
email_content = string.gsub(email_content, "\r\n%.", "\r\n..")
|
||||
-- send mail
|
||||
lua_smtp.sendmail({
|
||||
task = task,
|
||||
host = os.getenv("IPV4_NETWORK") .. '.253',
|
||||
|
@ -347,8 +351,8 @@ rspamd_config:register_symbol({
|
|||
from = task:get_from(stp)[1].addr,
|
||||
recipients = bcc_dest,
|
||||
helo = 'bcc',
|
||||
timeout = 10,
|
||||
}, task:get_content(), sendmail_cb)
|
||||
timeout = 20,
|
||||
}, email_content, sendmail_cb)
|
||||
end
|
||||
|
||||
-- determine from
|
||||
|
|
|
@ -47,12 +47,14 @@ if (!function_exists('getallheaders')) {
|
|||
}
|
||||
|
||||
$headers = getallheaders();
|
||||
$json_body = json_decode(file_get_contents('php://input'));
|
||||
|
||||
$qid = $headers['X-Rspamd-Qid'];
|
||||
$rcpts = $headers['X-Rspamd-Rcpt'];
|
||||
$sender = $headers['X-Rspamd-From'];
|
||||
$ip = $headers['X-Rspamd-Ip'];
|
||||
$subject = $headers['X-Rspamd-Subject'];
|
||||
$messageid= $json_body->message_id;
|
||||
$priority = 0;
|
||||
|
||||
$symbols_array = json_decode($headers['X-Rspamd-Symbols'], true);
|
||||
|
@ -65,6 +67,20 @@ if (is_array($symbols_array)) {
|
|||
}
|
||||
}
|
||||
|
||||
$sender_address = $json_body->header_from[0];
|
||||
$sender_name = '-';
|
||||
if (preg_match('/(?<name>.*?)<(?<address>.*?)>/i', $sender_address, $matches)) {
|
||||
$sender_address = $matches['address'];
|
||||
$sender_name = trim($matches['name'], '"\' ');
|
||||
}
|
||||
|
||||
$to_address = $json_body->header_to[0];
|
||||
$to_name = '-';
|
||||
if (preg_match('/(?<name>.*?)<(?<address>.*?)>/i', $to_address, $matches)) {
|
||||
$to_address = $matches['address'];
|
||||
$to_name = trim($matches['name'], '"\' ');
|
||||
}
|
||||
|
||||
$rcpt_final_mailboxes = array();
|
||||
|
||||
// Loop through all rcpts
|
||||
|
@ -229,9 +245,16 @@ foreach ($rcpt_final_mailboxes as $rcpt_final) {
|
|||
$post_fields = array(
|
||||
"token" => $api_data['token'],
|
||||
"user" => $api_data['key'],
|
||||
"title" => sprintf("%s", str_replace(array('{SUBJECT}', '{SENDER}'), array($subject, $sender), $title)),
|
||||
"title" => sprintf("%s", str_replace(
|
||||
array('{SUBJECT}', '{SENDER}', '{SENDER_NAME}', '{SENDER_ADDRESS}', '{TO_NAME}', '{TO_ADDRESS}', '{MSG_ID}'),
|
||||
array($subject, $sender, $sender_name, $sender_address, $to_name, $to_address, $messageid), $title)
|
||||
),
|
||||
"priority" => $priority,
|
||||
"message" => sprintf("%s", str_replace(array('{SUBJECT}', '{SENDER}'), array($subject, $sender), $text))
|
||||
"message" => sprintf("%s", str_replace(
|
||||
array('{SUBJECT}', '{SENDER}', '{SENDER_NAME}', '{SENDER_ADDRESS}', '{TO_NAME}', '{TO_ADDRESS}', '{MSG_ID}', '\n'),
|
||||
array($subject, $sender, $sender_name, $sender_address, $to_name, $to_address, $messageid, PHP_EOL), $text)
|
||||
),
|
||||
"sound" => $attributes['sound'] ?? "pushover"
|
||||
);
|
||||
if ($attributes['evaluate_x_prio'] == "1" && $priority == 1) {
|
||||
$post_fields['expire'] = 600;
|
||||
|
|
|
@ -32,8 +32,6 @@
|
|||
// );
|
||||
|
||||
// self-signed is not trusted anymore
|
||||
SOGoSieveServer = "sieve://dovecot:4190/?TLS=YES&tlsVerifyMode=none";
|
||||
SOGoSMTPServer = "smtp://postfix:588/?TLS=YES&tlsVerifyMode=none";
|
||||
WOPort = "0.0.0.0:20000";
|
||||
SOGoMemcachedHost = "memcached";
|
||||
|
||||
|
@ -64,7 +62,7 @@
|
|||
SOGoFirstDayOfWeek = "1";
|
||||
|
||||
SOGoSieveFolderEncoding = "UTF-8";
|
||||
SOGoPasswordChangeEnabled = YES;
|
||||
SOGoPasswordChangeEnabled = NO;
|
||||
SOGoSentFolderName = "Sent";
|
||||
SOGoMailShowSubscribedFoldersOnly = NO;
|
||||
NGImap4ConnectionStringSeparator = "/";
|
||||
|
|
|
@ -13,12 +13,12 @@
|
|||
Please check the logs or contact support if the error persists.</p>
|
||||
<h2>Quick debugging</h2>
|
||||
<p>Check Nginx and PHP logs:</p>
|
||||
<pre>docker-compose logs --tail=200 php-fpm-mailcow nginx-mailcow</pre>
|
||||
<pre>docker compose logs --tail=200 php-fpm-mailcow nginx-mailcow</pre>
|
||||
<p>Make sure your SQL credentials in mailcow.conf (a link to .env) do fit your initialized SQL volume. If you see an access denied, you might have the wrong mailcow.conf:</p>
|
||||
<pre>source mailcow.conf ; docker-compose exec mysql-mailcow mysql -u${DBUSER} -p${DBPASS} ${DBNAME}</pre>
|
||||
<pre>source mailcow.conf ; docker compose exec mysql-mailcow mysql -u${DBUSER} -p${DBPASS} ${DBNAME}</pre>
|
||||
<p>In case of a previous failed installation, create a backup of your existing data, followed by removing all volumes and starting over (<b>NEVER</b> do this with a production system, it will remove <b>ALL</b> data):</p>
|
||||
<pre>BACKUP_LOCATION=/tmp/ ./helper-scripts/backup_and_restore.sh backup all</pre>
|
||||
<pre>docker-compose down --volumes ; docker-compose up -d</pre>
|
||||
<pre>docker compose down --volumes ; docker compose up -d</pre>
|
||||
<p>Make sure your timezone is correct. Use "America/New_York" for example, do not use spaces. Check <a href="https://en.wikipedia.org/wiki/List_of_tz_database_time_zones">here</a> for a list.</p>
|
||||
<br>Click to learn more about <a style="color:red;text-decoration:none;" href="https://mailcow.github.io/mailcow-dockerized-docs/#get-support" target="_blank">getting support.</a>
|
||||
</body>
|
||||
|
|
|
@ -10,9 +10,6 @@ require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php';
|
|||
$_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
|
||||
$tfa_data = get_tfa();
|
||||
$fido2_data = fido2(array("action" => "get_friendly_names"));
|
||||
if (!isset($_SESSION['gal']) && $license_cache = $redis->Get('LICENSE_STATUS_CACHE')) {
|
||||
$_SESSION['gal'] = json_decode($license_cache, true);
|
||||
}
|
||||
|
||||
$js_minifier->add('/web/js/site/admin.js');
|
||||
$js_minifier->add('/web/js/presets/rspamd.js');
|
||||
|
@ -83,6 +80,10 @@ foreach ($RSPAMD_MAPS['regex'] as $rspamd_regex_desc => $rspamd_regex_map) {
|
|||
];
|
||||
}
|
||||
|
||||
// cors settings
|
||||
$cors_settings = cors('get');
|
||||
$cors_settings['allowed_origins'] = str_replace(", ", "\n", $cors_settings['allowed_origins']);
|
||||
$cors_settings['allowed_methods'] = explode(", ", $cors_settings['allowed_methods']);
|
||||
|
||||
$template = 'admin.twig';
|
||||
$template_data = [
|
||||
|
@ -90,8 +91,6 @@ $template_data = [
|
|||
'tfa_id' => @$_SESSION['tfa_id'],
|
||||
'fido2_cid' => @$_SESSION['fido2_cid'],
|
||||
'fido2_data' => $fido2_data,
|
||||
'gal' => @$_SESSION['gal'],
|
||||
'license_guid' => license('guid'),
|
||||
'api' => [
|
||||
'ro' => admin_api('ro', 'get'),
|
||||
'rw' => admin_api('rw', 'get'),
|
||||
|
@ -109,9 +108,12 @@ $template_data = [
|
|||
'rsettings' => $rsettings,
|
||||
'rspamd_regex_maps' => $rspamd_regex_maps,
|
||||
'logo_specs' => customize('get', 'main_logo_specs'),
|
||||
'ip_check' => customize('get', 'ip_check'),
|
||||
'password_complexity' => password_complexity('get'),
|
||||
'show_rspamd_global_filters' => @$_SESSION['show_rspamd_global_filters'],
|
||||
'cors_settings' => $cors_settings,
|
||||
'lang_admin' => json_encode($lang['admin']),
|
||||
'lang_datatables' => json_encode($lang['datatables'])
|
||||
];
|
||||
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/footer.inc.php';
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
html {
|
||||
box-sizing: border-box;
|
||||
overflow: -moz-scrollbars-vertical;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
*,
|
||||
*:before,
|
||||
*:after {
|
||||
box-sizing: inherit;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
background: #fafafa;
|
||||
}
|
|
@ -5,56 +5,15 @@
|
|||
<meta charset="UTF-8">
|
||||
<title>Swagger UI</title>
|
||||
<link rel="stylesheet" type="text/css" href="./swagger-ui.css" />
|
||||
<link rel="stylesheet" type="text/css" href="index.css" />
|
||||
<link rel="icon" type="image/png" href="./favicon-32x32.png" sizes="32x32" />
|
||||
<link rel="icon" type="image/png" href="./favicon-16x16.png" sizes="16x16" />
|
||||
<style>
|
||||
html
|
||||
{
|
||||
box-sizing: border-box;
|
||||
overflow: -moz-scrollbars-vertical;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
*,
|
||||
*:before,
|
||||
*:after
|
||||
{
|
||||
box-sizing: inherit;
|
||||
}
|
||||
|
||||
body
|
||||
{
|
||||
margin:0;
|
||||
background: #fafafa;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="swagger-ui"></div>
|
||||
|
||||
<script src="./swagger-ui-bundle.js" charset="UTF-8"> </script>
|
||||
<script src="./swagger-ui-standalone-preset.js" charset="UTF-8"> </script>
|
||||
<script>
|
||||
window.onload = function() {
|
||||
// Begin Swagger UI call region
|
||||
const ui = SwaggerUIBundle({
|
||||
url: "/api/openapi.yaml",
|
||||
dom_id: '#swagger-ui',
|
||||
deepLinking: true,
|
||||
presets: [
|
||||
SwaggerUIBundle.presets.apis,
|
||||
SwaggerUIStandalonePreset
|
||||
],
|
||||
plugins: [
|
||||
SwaggerUIBundle.plugins.DownloadUrl
|
||||
],
|
||||
layout: "StandaloneLayout"
|
||||
});
|
||||
// End Swagger UI call region
|
||||
|
||||
window.ui = ui;
|
||||
};
|
||||
</script>
|
||||
<script src="./swagger-initializer.js" charset="UTF-8"> </script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
var isValid, qp, arr;
|
||||
|
||||
if (/code|token|error/.test(window.location.hash)) {
|
||||
qp = window.location.hash.substring(1);
|
||||
qp = window.location.hash.substring(1).replace('?', '&');
|
||||
} else {
|
||||
qp = location.search.substring(1);
|
||||
}
|
||||
|
@ -38,7 +38,7 @@
|
|||
authId: oauth2.auth.name,
|
||||
source: "auth",
|
||||
level: "warning",
|
||||
message: "Authorization may be unsafe, passed state was changed in server Passed state wasn't returned from auth server"
|
||||
message: "Authorization may be unsafe, passed state was changed in server. The passed state wasn't returned from auth server."
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -58,7 +58,7 @@
|
|||
authId: oauth2.auth.name,
|
||||
source: "auth",
|
||||
level: "error",
|
||||
message: oauthErrorMsg || "[Authorization failed]: no accessCode received from the server"
|
||||
message: oauthErrorMsg || "[Authorization failed]: no accessCode received from the server."
|
||||
});
|
||||
}
|
||||
} else {
|
||||
|
@ -67,9 +67,13 @@
|
|||
window.close();
|
||||
}
|
||||
|
||||
window.addEventListener('DOMContentLoaded', function () {
|
||||
run();
|
||||
});
|
||||
if (document.readyState !== 'loading') {
|
||||
run();
|
||||
} else {
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
run();
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
openapi: 3.0.0
|
||||
openapi: 3.1.0
|
||||
info:
|
||||
description: >-
|
||||
mailcow is complete e-mailing solution with advanced antispam, antivirus,
|
||||
|
@ -209,10 +209,17 @@ paths:
|
|||
- app_passwd
|
||||
- add
|
||||
- active: "1"
|
||||
app_name: emclient
|
||||
username: info@domain.tld
|
||||
app_name: wordpress
|
||||
app_passwd: keyleudecticidechothistishownsan31
|
||||
app_passwd2: keyleudecticidechothistishownsan31
|
||||
username: hello@mailcow.email
|
||||
protocols:
|
||||
- imap_access
|
||||
- dav_access
|
||||
- smtp_access
|
||||
- eas_access
|
||||
- pop3_access
|
||||
- sieve_access
|
||||
msg: app_passwd_added
|
||||
type: success
|
||||
schema:
|
||||
|
@ -249,6 +256,13 @@ paths:
|
|||
app_name: wordpress
|
||||
app_passwd: keyleudecticidechothistishownsan31
|
||||
app_passwd2: keyleudecticidechothistishownsan31
|
||||
protocols:
|
||||
- imap_access
|
||||
- dav_access
|
||||
- smtp_access
|
||||
- eas_access
|
||||
- pop3_access
|
||||
- sieve_access
|
||||
properties:
|
||||
active:
|
||||
description: is alias active or not
|
||||
|
@ -497,27 +511,30 @@ paths:
|
|||
relay_all_recipients: "0"
|
||||
rl_frame: s
|
||||
rl_value: "10"
|
||||
tags: ["tag1", "tag2"]
|
||||
- null
|
||||
msg:
|
||||
- domain_added
|
||||
- domain.tld
|
||||
type: success
|
||||
schema:
|
||||
properties:
|
||||
log:
|
||||
description: contains request object
|
||||
items: {}
|
||||
type: array
|
||||
msg:
|
||||
items: {}
|
||||
type: array
|
||||
type:
|
||||
enum:
|
||||
- success
|
||||
- danger
|
||||
- error
|
||||
type: string
|
||||
type: object
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
log:
|
||||
description: contains request object
|
||||
items: {}
|
||||
type: array
|
||||
msg:
|
||||
items: {}
|
||||
type: array
|
||||
type:
|
||||
enum:
|
||||
- success
|
||||
- danger
|
||||
- error
|
||||
type: string
|
||||
description: OK
|
||||
headers: {}
|
||||
tags:
|
||||
|
@ -544,6 +561,7 @@ paths:
|
|||
rl_frame: s
|
||||
rl_value: "10"
|
||||
restart_sogo: "10"
|
||||
tags: ["tag1", "tag2"]
|
||||
properties:
|
||||
active:
|
||||
description: is domain active or not
|
||||
|
@ -563,6 +581,11 @@ paths:
|
|||
domain:
|
||||
description: Fully qualified domain name
|
||||
type: string
|
||||
gal:
|
||||
description: >-
|
||||
is domain global address list active or not, it enables
|
||||
shared contacts accross domain in SOGo webmail
|
||||
type: boolean
|
||||
mailboxes:
|
||||
description: limit count of mailboxes associated with this domain
|
||||
type: number
|
||||
|
@ -580,6 +603,9 @@ paths:
|
|||
if not, them you have to create "dummy" mailbox for each
|
||||
address to relay
|
||||
type: boolean
|
||||
relay_unknown_only:
|
||||
description: Relay non-existing mailboxes only. Existing mailboxes will be delivered locally.
|
||||
type: boolean
|
||||
rl_frame:
|
||||
enum:
|
||||
- s
|
||||
|
@ -590,6 +616,11 @@ paths:
|
|||
rl_value:
|
||||
description: rate limit value
|
||||
type: number
|
||||
tags:
|
||||
description: tags for this Domain
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
type: object
|
||||
summary: Create domain
|
||||
/api/v1/add/domain-admin:
|
||||
|
@ -668,6 +699,38 @@ paths:
|
|||
type: string
|
||||
type: object
|
||||
summary: Create Domain Admin user
|
||||
/api/v1/add/sso/domain-admin:
|
||||
post:
|
||||
responses:
|
||||
"401":
|
||||
$ref: "#/components/responses/Unauthorized"
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
examples:
|
||||
response:
|
||||
value:
|
||||
token: "591F6D-5C3DD2-7455CD-DAF1C1-AA4FCC"
|
||||
description: OK
|
||||
headers: { }
|
||||
tags:
|
||||
- Single Sign-On
|
||||
description: >-
|
||||
Using this endpoint you can issue a token for Domain Admin user. This token can be used for
|
||||
autologin Domain Admin user by using query_string var sso_token={token}. Token expiration time is 30s
|
||||
operationId: Issue Domain Admin SSO token
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
example:
|
||||
username: testadmin
|
||||
properties:
|
||||
username:
|
||||
description: the username for the admin user
|
||||
type: object
|
||||
type: object
|
||||
summary: Issue Domain Admin SSO token
|
||||
/api/v1/edit/da-acl:
|
||||
post:
|
||||
responses:
|
||||
|
@ -1010,6 +1073,7 @@ paths:
|
|||
force_pw_update: "1"
|
||||
tls_enforce_in: "1"
|
||||
tls_enforce_out: "1"
|
||||
tags: ["tag1", "tag2"]
|
||||
- null
|
||||
msg:
|
||||
- mailbox_added
|
||||
|
@ -1054,6 +1118,7 @@ paths:
|
|||
force_pw_update: "1"
|
||||
tls_enforce_in: "1"
|
||||
tls_enforce_out: "1"
|
||||
tags: ["tag1", "tag2"]
|
||||
properties:
|
||||
active:
|
||||
description: is mailbox active or not
|
||||
|
@ -1934,21 +1999,23 @@ paths:
|
|||
- domain2.tld
|
||||
type: success
|
||||
schema:
|
||||
properties:
|
||||
log:
|
||||
description: contains request object
|
||||
items: {}
|
||||
type: array
|
||||
msg:
|
||||
items: {}
|
||||
type: array
|
||||
type:
|
||||
enum:
|
||||
- success
|
||||
- danger
|
||||
- error
|
||||
type: string
|
||||
type: object
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
log:
|
||||
description: contains request object
|
||||
items: {}
|
||||
type: array
|
||||
msg:
|
||||
items: {}
|
||||
type: array
|
||||
type:
|
||||
enum:
|
||||
- success
|
||||
- danger
|
||||
- error
|
||||
type: string
|
||||
description: OK
|
||||
headers: {}
|
||||
tags:
|
||||
|
@ -1959,14 +2026,15 @@ paths:
|
|||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
example:
|
||||
- domain.tld
|
||||
- domain2.tld
|
||||
properties:
|
||||
items:
|
||||
description: contains list of domains you want to delete
|
||||
type: object
|
||||
type: object
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
summary: Delete domain
|
||||
/api/v1/delete/domain-admin:
|
||||
post:
|
||||
|
@ -2716,6 +2784,140 @@ paths:
|
|||
type: object
|
||||
type: object
|
||||
summary: Delete Transport Maps
|
||||
"/api/v1/delete/mailbox/tag/{mailbox}":
|
||||
post:
|
||||
parameters:
|
||||
- description: name of mailbox
|
||||
in: path
|
||||
name: mailbox
|
||||
example: info@domain.tld
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"401":
|
||||
$ref: "#/components/responses/Unauthorized"
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
examples:
|
||||
response:
|
||||
value:
|
||||
- log:
|
||||
- mailbox
|
||||
- delete
|
||||
- tags_mailbox
|
||||
- tags:
|
||||
- tag1
|
||||
- tag2
|
||||
mailbox: info@domain.tld
|
||||
- null
|
||||
msg:
|
||||
- mailbox_modified
|
||||
- info@domain.tld
|
||||
type: success
|
||||
schema:
|
||||
properties:
|
||||
log:
|
||||
description: contains request object
|
||||
items: {}
|
||||
type: array
|
||||
msg:
|
||||
items: {}
|
||||
type: array
|
||||
type:
|
||||
enum:
|
||||
- success
|
||||
- danger
|
||||
- error
|
||||
type: string
|
||||
type: object
|
||||
description: OK
|
||||
headers: {}
|
||||
tags:
|
||||
- Mailboxes
|
||||
description: You can delete one or more mailbox tags.
|
||||
operationId: Delete mailbox tags
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
example:
|
||||
- tag1
|
||||
- tag2
|
||||
properties:
|
||||
items:
|
||||
description: contains list of mailboxes you want to delete
|
||||
type: object
|
||||
type: object
|
||||
summary: Delete mailbox tags
|
||||
"/api/v1/delete/domain/tag/{domain}":
|
||||
post:
|
||||
parameters:
|
||||
- description: name of domain
|
||||
in: path
|
||||
name: domain
|
||||
example: domain.tld
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"401":
|
||||
$ref: "#/components/responses/Unauthorized"
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
examples:
|
||||
response:
|
||||
value:
|
||||
- log:
|
||||
- mailbox
|
||||
- delete
|
||||
- tags_domain
|
||||
- tags:
|
||||
- tag1
|
||||
- tag2
|
||||
domain: domain.tld
|
||||
- null
|
||||
msg:
|
||||
- domain_modified
|
||||
- domain.tld
|
||||
type: success
|
||||
schema:
|
||||
properties:
|
||||
log:
|
||||
description: contains request object
|
||||
items: {}
|
||||
type: array
|
||||
msg:
|
||||
items: {}
|
||||
type: array
|
||||
type:
|
||||
enum:
|
||||
- success
|
||||
- danger
|
||||
- error
|
||||
type: string
|
||||
type: object
|
||||
description: OK
|
||||
headers: {}
|
||||
tags:
|
||||
- Domains
|
||||
description: You can delete one or more domain tags.
|
||||
operationId: Delete domain tags
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
example:
|
||||
- tag1
|
||||
- tag2
|
||||
properties:
|
||||
items:
|
||||
description: contains list of domains you want to delete
|
||||
type: object
|
||||
type: object
|
||||
summary: Delete domain tags
|
||||
/api/v1/edit/alias:
|
||||
post:
|
||||
responses:
|
||||
|
@ -2820,23 +3022,25 @@ paths:
|
|||
$ref: "#/components/responses/Unauthorized"
|
||||
"200":
|
||||
content:
|
||||
"*/*":
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
log:
|
||||
description: contains request object
|
||||
items: {}
|
||||
type: array
|
||||
msg:
|
||||
items: {}
|
||||
type: array
|
||||
type:
|
||||
enum:
|
||||
- success
|
||||
- danger
|
||||
- error
|
||||
type: string
|
||||
type: object
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
log:
|
||||
type: array
|
||||
description: contains request object
|
||||
items: {}
|
||||
msg:
|
||||
type: array
|
||||
items: {}
|
||||
type:
|
||||
enum:
|
||||
- success
|
||||
- danger
|
||||
- error
|
||||
type: string
|
||||
description: OK
|
||||
headers: {}
|
||||
tags:
|
||||
|
@ -2865,6 +3069,7 @@ paths:
|
|||
quota: "10240"
|
||||
relay_all_recipients: "0"
|
||||
relayhost: "2"
|
||||
tags: ["tag3", "tag4"]
|
||||
items: domain.tld
|
||||
properties:
|
||||
attr:
|
||||
|
@ -2903,13 +3108,33 @@ paths:
|
|||
if not, them you have to create "dummy" mailbox for each
|
||||
address to relay
|
||||
type: boolean
|
||||
relay_unknown_only:
|
||||
description: Relay non-existing mailboxes only. Existing mailboxes will be delivered locally.
|
||||
type: boolean
|
||||
relayhost:
|
||||
description: id of relayhost
|
||||
type: number
|
||||
rl_frame:
|
||||
enum:
|
||||
- s
|
||||
- m
|
||||
- h
|
||||
- d
|
||||
type: string
|
||||
rl_value:
|
||||
description: rate limit value
|
||||
type: number
|
||||
tags:
|
||||
description: tags for this Domain
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
type: object
|
||||
items:
|
||||
description: contains list of domain names you want update
|
||||
type: object
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
type: object
|
||||
summary: Update domain
|
||||
/api/v1/edit/fail2ban:
|
||||
|
@ -2951,8 +3176,10 @@ paths:
|
|||
example:
|
||||
attr:
|
||||
ban_time: "86400"
|
||||
ban_time_increment: "1"
|
||||
blacklist: "10.100.6.5/32,10.100.8.4/32"
|
||||
max_attempts: "5"
|
||||
max_ban_time: "86400"
|
||||
netban_ipv4: "24"
|
||||
netban_ipv6: "64"
|
||||
retry_window: "600"
|
||||
|
@ -2966,11 +3193,17 @@ paths:
|
|||
description: the backlisted ips or hostnames separated by comma
|
||||
type: string
|
||||
ban_time:
|
||||
description: the time a ip should be banned
|
||||
description: the time an ip should be banned
|
||||
type: number
|
||||
ban_time_increment:
|
||||
description: if the time of the ban should increase each time
|
||||
type: boolean
|
||||
max_attempts:
|
||||
description: the maximum numbe of wrong logins before a ip is banned
|
||||
type: number
|
||||
max_ban_time:
|
||||
description: the maximum time an ip should be banned
|
||||
type: number
|
||||
netban_ipv4:
|
||||
description: the networks mask to ban for ipv4
|
||||
type: number
|
||||
|
@ -3019,6 +3252,7 @@ paths:
|
|||
sogo_access: "1"
|
||||
username:
|
||||
- info@domain.tld
|
||||
tags: ["tag3", "tag4"]
|
||||
- null
|
||||
msg:
|
||||
- mailbox_modified
|
||||
|
@ -3066,6 +3300,7 @@ paths:
|
|||
- domain3.tld
|
||||
- "*"
|
||||
sogo_access: "1"
|
||||
tags: ["tag3", "tag4"]
|
||||
items:
|
||||
- info@domain.tld
|
||||
properties:
|
||||
|
@ -3154,6 +3389,7 @@ paths:
|
|||
evaluate_x_prio: "0"
|
||||
key: 21e8918e1jksdjcpis712
|
||||
only_x_prio: "0"
|
||||
sound: "pushover"
|
||||
senders: ""
|
||||
senders_regex: ""
|
||||
text: ""
|
||||
|
@ -3197,6 +3433,7 @@ paths:
|
|||
evaluate_x_prio: "0"
|
||||
key: 21e8918e1jksdjcpis712
|
||||
only_x_prio: "0"
|
||||
sound: "pushover"
|
||||
senders: ""
|
||||
senders_regex: ""
|
||||
text: ""
|
||||
|
@ -3218,6 +3455,9 @@ paths:
|
|||
only_x_prio:
|
||||
description: Only send push for prio mails
|
||||
type: number
|
||||
sound:
|
||||
description: Set notification sound
|
||||
type: string
|
||||
senders:
|
||||
description: Only send push for emails from these senders
|
||||
type: string
|
||||
|
@ -3793,6 +4033,13 @@ paths:
|
|||
- all
|
||||
- mailcow.tld
|
||||
type: string
|
||||
- description: comma seperated list of tags to filter by
|
||||
example: "tag1,tag2"
|
||||
in: query
|
||||
name: tags
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
- description: e.g. api-key-string
|
||||
example: api-key-string
|
||||
in: header
|
||||
|
@ -3831,6 +4078,7 @@ paths:
|
|||
relay_all_recipients: "0"
|
||||
relayhost: "0"
|
||||
rl: false
|
||||
tags: ["tag1", "tag2"]
|
||||
- active: "1"
|
||||
aliases_in_domain: 0
|
||||
aliases_left: 400
|
||||
|
@ -3853,6 +4101,7 @@ paths:
|
|||
relay_all_recipients: "0"
|
||||
relayhost: "0"
|
||||
rl: false
|
||||
tags: ["tag3", "tag4"]
|
||||
description: OK
|
||||
headers: {}
|
||||
tags:
|
||||
|
@ -3872,10 +4121,12 @@ paths:
|
|||
response:
|
||||
value:
|
||||
ban_time: 604800
|
||||
ban_time_increment: 1
|
||||
blacklist: |-
|
||||
45.82.153.37/32
|
||||
92.118.38.52/32
|
||||
max_attempts: 1
|
||||
max_ban_time: 604800
|
||||
netban_ipv4: 32
|
||||
netban_ipv6: 128
|
||||
perm_bans:
|
||||
|
@ -4345,6 +4596,13 @@ paths:
|
|||
- all
|
||||
- user@domain.tld
|
||||
type: string
|
||||
- description: comma seperated list of tags to filter by
|
||||
example: "tag1,tag2"
|
||||
in: query
|
||||
name: tags
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
- description: e.g. api-key-string
|
||||
example: api-key-string
|
||||
in: header
|
||||
|
@ -4382,6 +4640,7 @@ paths:
|
|||
rl: false
|
||||
spam_aliases: 0
|
||||
username: info@doman3.tld
|
||||
tags: ["tag1", "tag2"]
|
||||
description: OK
|
||||
headers: {}
|
||||
tags:
|
||||
|
@ -5072,6 +5331,27 @@ paths:
|
|||
of used storage.
|
||||
operationId: Get vmail status
|
||||
summary: Get vmail status
|
||||
/api/v1/get/status/version:
|
||||
get:
|
||||
responses:
|
||||
"401":
|
||||
$ref: "#/components/responses/Unauthorized"
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
examples:
|
||||
response:
|
||||
value:
|
||||
version: "2022-04"
|
||||
description: OK
|
||||
headers: {}
|
||||
tags:
|
||||
- Status
|
||||
description: >-
|
||||
Using this endpoint you can get the current running release of this
|
||||
instance.
|
||||
operationId: Get version status
|
||||
summary: Get version status
|
||||
/api/v1/get/syncjobs/all/no_log:
|
||||
get:
|
||||
responses:
|
||||
|
@ -5268,6 +5548,104 @@ paths:
|
|||
attr:
|
||||
spam_score: "8,15"
|
||||
summary: Edit mailbox spam filter score
|
||||
"/api/v1/get/mailbox/all/{domain}":
|
||||
get:
|
||||
parameters:
|
||||
- description: name of domain
|
||||
in: path
|
||||
name: domain
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
- description: e.g. api-key-string
|
||||
example: api-key-string
|
||||
in: header
|
||||
name: X-API-Key
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"401":
|
||||
$ref: "#/components/responses/Unauthorized"
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
examples:
|
||||
response:
|
||||
value:
|
||||
- active: "1"
|
||||
attributes:
|
||||
force_pw_update: "0"
|
||||
mailbox_format: "maildir:"
|
||||
quarantine_notification: never
|
||||
sogo_access: "1"
|
||||
tls_enforce_in: "0"
|
||||
tls_enforce_out: "0"
|
||||
domain: domain3.tld
|
||||
is_relayed: 0
|
||||
local_part: info
|
||||
max_new_quota: 10737418240
|
||||
messages: 0
|
||||
name: Full name
|
||||
percent_class: success
|
||||
percent_in_use: 0
|
||||
quota: 3221225472
|
||||
quota_used: 0
|
||||
rl: false
|
||||
spam_aliases: 0
|
||||
username: info@domain3.tld
|
||||
tags: ["tag1", "tag2"]
|
||||
description: OK
|
||||
headers: {}
|
||||
tags:
|
||||
- Mailboxes
|
||||
description: You can list all mailboxes existing in system for a specific domain.
|
||||
operationId: Get mailboxes of a domain
|
||||
summary: Get mailboxes of a domain
|
||||
/api/v1/edit/cors:
|
||||
post:
|
||||
responses:
|
||||
"401":
|
||||
$ref: "#/components/responses/Unauthorized"
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
examples:
|
||||
response:
|
||||
value:
|
||||
- type: "success"
|
||||
log: ["cors", "edit", {"allowed_origins": ["*", "mail.mailcow.tld"], "allowed_methods": ["POST", "GET", "DELETE", "PUT"]}]
|
||||
msg: "cors_headers_edited"
|
||||
description: OK
|
||||
headers: { }
|
||||
tags:
|
||||
- Cross-Origin Resource Sharing (CORS)
|
||||
description: >-
|
||||
This endpoint allows you to manage Cross-Origin Resource Sharing (CORS) settings for the API.
|
||||
CORS is a security feature implemented by web browsers to prevent unauthorized cross-origin requests.
|
||||
By editing the CORS settings, you can specify which domains and which methods are permitted to access the API resources from outside the mailcow domain.
|
||||
operationId: Edit Cross-Origin Resource Sharing (CORS) settings
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
example:
|
||||
attr:
|
||||
allowed_origins: ["*", "mail.mailcow.tld"]
|
||||
allowed_methods: ["POST", "GET", "DELETE", "PUT"]
|
||||
properties:
|
||||
attr:
|
||||
type: object
|
||||
properties:
|
||||
allowed_origins:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
allowed_methods:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
summary: Edit Cross-Origin Resource Sharing (CORS) settings
|
||||
|
||||
tags:
|
||||
- name: Domains
|
||||
|
@ -5294,6 +5672,8 @@ tags:
|
|||
description: Manage DKIM keys
|
||||
- name: Domain admin
|
||||
description: Create or udpdate domain admin users
|
||||
- name: Single Sign-On
|
||||
description: Issue tokens for users
|
||||
- name: Address Rewriting
|
||||
description: Create BCC maps or recipient maps
|
||||
- name: Outgoing TLS Policy Map Overrides
|
||||
|
@ -5310,3 +5690,5 @@ tags:
|
|||
description: Get the status of your cow
|
||||
- name: Ratelimits
|
||||
description: Edit domain ratelimits
|
||||
- name: Cross-Origin Resource Sharing (CORS)
|
||||
description: Manage Cross-Origin Resource Sharing (CORS) settings
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
window.onload = function() {
|
||||
// Begin Swagger UI call region
|
||||
window.ui = SwaggerUIBundle({
|
||||
urls: [{url: "/api/openapi.yaml", name: "mailcow API"}],
|
||||
dom_id: '#swagger-ui',
|
||||
deepLinking: true,
|
||||
presets: [
|
||||
SwaggerUIBundle.presets.apis,
|
||||
SwaggerUIStandalonePreset
|
||||
],
|
||||
plugins: [
|
||||
SwaggerUIBundle.plugins.DownloadUrl
|
||||
],
|
||||
layout: "StandaloneLayout"
|
||||
});
|
||||
// End Swagger UI call region
|
||||
|
||||
};
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -68,7 +68,7 @@ if (empty($_SERVER['PHP_AUTH_USER']) || empty($_SERVER['PHP_AUTH_PW'])) {
|
|||
exit(0);
|
||||
}
|
||||
|
||||
$login_role = check_login($login_user, $login_pass, true);
|
||||
$login_role = check_login($login_user, $login_pass, array('eas' => TRUE));
|
||||
|
||||
if ($login_role === "user") {
|
||||
header("Content-Type: application/xml");
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -1 +1 @@
|
|||
@media (max-width:1050px){.navbar-header{float:none}.navbar-left,.navbar-nav,.navbar-right{float:none!important}.navbar-toggle{display:block}.navbar-collapse{border-top:1px solid transparent;box-shadow:inset 0 1px 0 rgba(255,255,255,.1)}.navbar-fixed-top{top:0;border-width:0 0 1px}.navbar-collapse.collapse{display:none!important}.navbar-nav{margin-top:7.5px}.navbar-nav>li{float:none}.navbar-nav>li>a{padding-top:10px;padding-bottom:10px}.collapse.in{display:block!important}.navbar-nav .open .dropdown-menu{position:static;float:none;width:auto;margin-top:0;background-color:transparent;border:0;-webkit-box-shadow:none;box-shadow:none}}
|
||||
@media (max-width:1050px){.navbar-header{float:none}.navbar-left,.navbar-nav,.navbar-right{float:none!important}.navbar-toggle{display:block}.navbar-collapse{border-top:1px solid transparent;box-shadow:inset 0 1px 0 rgba(255,255,255,.1)}.navbar-fixed-top{top:0;border-width:0 0 1px}.navbar-nav{margin-top:7.5px}.navbar-nav>li{float:none}.navbar-nav>li>a{padding-top:10px;padding-bottom:10px}.collapse.in{display:block!important}.navbar-nav .open .dropdown-menu{position:static;float:none;width:auto;margin-top:0;background-color:transparent;border:0;-webkit-box-shadow:none;box-shadow:none}}
|
|
@ -0,0 +1,487 @@
|
|||
/*!
|
||||
* Bootstrap-select v1.14.0-beta2 (https://developer.snapappointments.com/bootstrap-select)
|
||||
*
|
||||
* Copyright 2012-2021 SnapAppointments, LLC
|
||||
* Licensed under MIT (https://github.com/snapappointments/bootstrap-select/blob/master/LICENSE)
|
||||
*/
|
||||
|
||||
@-webkit-keyframes bs-notify-fadeOut {
|
||||
0% {
|
||||
opacity: 0.9;
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
@-o-keyframes bs-notify-fadeOut {
|
||||
0% {
|
||||
opacity: 0.9;
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
@keyframes bs-notify-fadeOut {
|
||||
0% {
|
||||
opacity: 0.9;
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
select.bs-select-hidden,
|
||||
.bootstrap-select > select.bs-select-hidden,
|
||||
select.selectpicker {
|
||||
display: none !important;
|
||||
}
|
||||
.bootstrap-select {
|
||||
width: 220px \0;
|
||||
/*IE9 and below*/
|
||||
vertical-align: middle;
|
||||
}
|
||||
.bootstrap-select > .dropdown-toggle {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
text-align: right;
|
||||
white-space: nowrap;
|
||||
display: -webkit-inline-box;
|
||||
display: -webkit-inline-flex;
|
||||
display: -ms-inline-flexbox;
|
||||
display: inline-flex;
|
||||
-webkit-box-align: center;
|
||||
-webkit-align-items: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
-webkit-box-pack: justify;
|
||||
-webkit-justify-content: space-between;
|
||||
-ms-flex-pack: justify;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.bootstrap-select > .dropdown-toggle:after {
|
||||
margin-top: -1px;
|
||||
}
|
||||
.bootstrap-select > .dropdown-toggle.bs-placeholder,
|
||||
.bootstrap-select > .dropdown-toggle.bs-placeholder:hover,
|
||||
.bootstrap-select > .dropdown-toggle.bs-placeholder:focus,
|
||||
.bootstrap-select > .dropdown-toggle.bs-placeholder:active {
|
||||
color: #999;
|
||||
}
|
||||
.bootstrap-select > .dropdown-toggle.bs-placeholder.btn-primary,
|
||||
.bootstrap-select > .dropdown-toggle.bs-placeholder.btn-secondary,
|
||||
.bootstrap-select > .dropdown-toggle.bs-placeholder.btn-success,
|
||||
.bootstrap-select > .dropdown-toggle.bs-placeholder.btn-danger,
|
||||
.bootstrap-select > .dropdown-toggle.bs-placeholder.btn-info,
|
||||
.bootstrap-select > .dropdown-toggle.bs-placeholder.btn-dark,
|
||||
.bootstrap-select > .dropdown-toggle.bs-placeholder.btn-primary:hover,
|
||||
.bootstrap-select > .dropdown-toggle.bs-placeholder.btn-secondary:hover,
|
||||
.bootstrap-select > .dropdown-toggle.bs-placeholder.btn-success:hover,
|
||||
.bootstrap-select > .dropdown-toggle.bs-placeholder.btn-danger:hover,
|
||||
.bootstrap-select > .dropdown-toggle.bs-placeholder.btn-info:hover,
|
||||
.bootstrap-select > .dropdown-toggle.bs-placeholder.btn-dark:hover,
|
||||
.bootstrap-select > .dropdown-toggle.bs-placeholder.btn-primary:focus,
|
||||
.bootstrap-select > .dropdown-toggle.bs-placeholder.btn-secondary:focus,
|
||||
.bootstrap-select > .dropdown-toggle.bs-placeholder.btn-success:focus,
|
||||
.bootstrap-select > .dropdown-toggle.bs-placeholder.btn-danger:focus,
|
||||
.bootstrap-select > .dropdown-toggle.bs-placeholder.btn-info:focus,
|
||||
.bootstrap-select > .dropdown-toggle.bs-placeholder.btn-dark:focus,
|
||||
.bootstrap-select > .dropdown-toggle.bs-placeholder.btn-primary:active,
|
||||
.bootstrap-select > .dropdown-toggle.bs-placeholder.btn-secondary:active,
|
||||
.bootstrap-select > .dropdown-toggle.bs-placeholder.btn-success:active,
|
||||
.bootstrap-select > .dropdown-toggle.bs-placeholder.btn-danger:active,
|
||||
.bootstrap-select > .dropdown-toggle.bs-placeholder.btn-info:active,
|
||||
.bootstrap-select > .dropdown-toggle.bs-placeholder.btn-dark:active {
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
.bootstrap-select > select {
|
||||
position: absolute !important;
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
display: block !important;
|
||||
width: 0.5px !important;
|
||||
height: 100% !important;
|
||||
padding: 0 !important;
|
||||
opacity: 0 !important;
|
||||
border: none;
|
||||
z-index: 0 !important;
|
||||
}
|
||||
.bootstrap-select > select.mobile-device {
|
||||
top: 0;
|
||||
left: 0;
|
||||
display: block !important;
|
||||
width: 100% !important;
|
||||
z-index: 2 !important;
|
||||
}
|
||||
.has-error .bootstrap-select .dropdown-toggle,
|
||||
.error .bootstrap-select .dropdown-toggle,
|
||||
.bootstrap-select.is-invalid .dropdown-toggle,
|
||||
.was-validated .bootstrap-select select:invalid + .dropdown-toggle {
|
||||
border-color: #b94a48;
|
||||
}
|
||||
.bootstrap-select.is-valid .dropdown-toggle,
|
||||
.was-validated .bootstrap-select select:valid + .dropdown-toggle {
|
||||
border-color: #28a745;
|
||||
}
|
||||
.bootstrap-select.fit-width {
|
||||
width: auto !important;
|
||||
}
|
||||
.bootstrap-select:not([class*="col-"]):not([class*="form-control"]):not(.input-group-btn) {
|
||||
width: 220px;
|
||||
}
|
||||
.bootstrap-select > select.mobile-device:focus + .dropdown-toggle,
|
||||
.bootstrap-select .dropdown-toggle:focus {
|
||||
outline: thin dotted #333333 !important;
|
||||
outline: 5px auto -webkit-focus-ring-color !important;
|
||||
outline-offset: -2px;
|
||||
}
|
||||
.bootstrap-select.form-control {
|
||||
margin-bottom: 0;
|
||||
padding: 0;
|
||||
border: none;
|
||||
height: auto;
|
||||
}
|
||||
:not(.input-group) > .bootstrap-select.form-control:not([class*="col-"]) {
|
||||
width: 100%;
|
||||
}
|
||||
.bootstrap-select.form-control.input-group-btn {
|
||||
float: none;
|
||||
z-index: auto;
|
||||
}
|
||||
.form-inline .bootstrap-select,
|
||||
.form-inline .bootstrap-select.form-control:not([class*="col-"]) {
|
||||
width: auto;
|
||||
}
|
||||
.bootstrap-select:not(.input-group-btn),
|
||||
.bootstrap-select[class*="col-"] {
|
||||
float: none;
|
||||
display: inline-block;
|
||||
margin-left: 0;
|
||||
}
|
||||
.bootstrap-select.dropdown-menu-right,
|
||||
.bootstrap-select[class*="col-"].dropdown-menu-right,
|
||||
.row .bootstrap-select[class*="col-"].dropdown-menu-right {
|
||||
float: right;
|
||||
}
|
||||
.form-inline .bootstrap-select,
|
||||
.form-horizontal .bootstrap-select,
|
||||
.form-group .bootstrap-select {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.form-group-lg .bootstrap-select.form-control,
|
||||
.form-group-sm .bootstrap-select.form-control {
|
||||
padding: 0;
|
||||
}
|
||||
.form-group-lg .bootstrap-select.form-control .dropdown-toggle,
|
||||
.form-group-sm .bootstrap-select.form-control .dropdown-toggle {
|
||||
height: 100%;
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
border-radius: inherit;
|
||||
}
|
||||
.bootstrap-select.form-control-sm .dropdown-toggle,
|
||||
.bootstrap-select.form-control-lg .dropdown-toggle {
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
border-radius: inherit;
|
||||
}
|
||||
.bootstrap-select.form-control-sm .dropdown-toggle {
|
||||
padding: 0.25rem 0.5rem;
|
||||
}
|
||||
.bootstrap-select.form-control-lg .dropdown-toggle {
|
||||
padding: 0.5rem 1rem;
|
||||
}
|
||||
.form-inline .bootstrap-select .form-control {
|
||||
width: 100%;
|
||||
}
|
||||
.bootstrap-select.disabled,
|
||||
.bootstrap-select > .disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.bootstrap-select.disabled:focus,
|
||||
.bootstrap-select > .disabled:focus {
|
||||
outline: none !important;
|
||||
}
|
||||
.bootstrap-select.bs-container {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 0 !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
.bootstrap-select.bs-container .dropdown-menu {
|
||||
z-index: 1060;
|
||||
}
|
||||
.bootstrap-select .dropdown-toggle .filter-option {
|
||||
position: static;
|
||||
top: 0;
|
||||
left: 0;
|
||||
float: left;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
overflow: hidden;
|
||||
-webkit-box-flex: 0;
|
||||
-webkit-flex: 0 1 auto;
|
||||
-ms-flex: 0 1 auto;
|
||||
flex: 0 1 auto;
|
||||
}
|
||||
.bs3.bootstrap-select .dropdown-toggle .filter-option {
|
||||
padding-right: inherit;
|
||||
}
|
||||
.input-group .bs3-has-addon.bootstrap-select .dropdown-toggle .filter-option {
|
||||
position: absolute;
|
||||
padding-top: inherit;
|
||||
padding-bottom: inherit;
|
||||
padding-left: inherit;
|
||||
float: none;
|
||||
}
|
||||
.input-group .bs3-has-addon.bootstrap-select .dropdown-toggle .filter-option .filter-option-inner {
|
||||
padding-right: inherit;
|
||||
}
|
||||
.bootstrap-select .dropdown-toggle .filter-option-inner-inner {
|
||||
overflow: hidden;
|
||||
}
|
||||
.bootstrap-select .dropdown-toggle .filter-expand {
|
||||
width: 0 !important;
|
||||
float: left;
|
||||
opacity: 0 !important;
|
||||
overflow: hidden;
|
||||
}
|
||||
.bootstrap-select .dropdown-toggle .caret {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 12px;
|
||||
margin-top: -2px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.bootstrap-select .dropdown-toggle .bs-select-clear-selected {
|
||||
position: relative;
|
||||
display: block;
|
||||
margin-right: 5px;
|
||||
text-align: center;
|
||||
}
|
||||
.bs3.bootstrap-select .dropdown-toggle .bs-select-clear-selected {
|
||||
padding-right: inherit;
|
||||
}
|
||||
.bootstrap-select .dropdown-toggle .bs-select-clear-selected span {
|
||||
position: relative;
|
||||
top: -webkit-calc(((-1em / 1.5) + 1ex) / 2);
|
||||
top: calc(((-1em / 1.5) + 1ex) / 2);
|
||||
pointer-events: none;
|
||||
}
|
||||
.bs3.bootstrap-select .dropdown-toggle .bs-select-clear-selected span {
|
||||
top: auto;
|
||||
}
|
||||
.bootstrap-select .dropdown-toggle.bs-placeholder .bs-select-clear-selected {
|
||||
display: none;
|
||||
}
|
||||
.input-group .bootstrap-select.form-control .dropdown-toggle {
|
||||
border-radius: inherit;
|
||||
}
|
||||
.bootstrap-select[class*="col-"] .dropdown-toggle {
|
||||
width: 100%;
|
||||
}
|
||||
.bootstrap-select .dropdown-menu {
|
||||
min-width: 100%;
|
||||
-webkit-box-sizing: border-box;
|
||||
-moz-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.bootstrap-select .dropdown-menu > .inner:focus {
|
||||
outline: none !important;
|
||||
}
|
||||
.bootstrap-select .dropdown-menu.inner {
|
||||
position: static;
|
||||
float: none;
|
||||
border: 0;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border-radius: 0;
|
||||
-webkit-box-shadow: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
.bootstrap-select .dropdown-menu li {
|
||||
position: relative;
|
||||
}
|
||||
.bootstrap-select .dropdown-menu li.active small {
|
||||
color: rgba(255, 255, 255, 0.5) !important;
|
||||
}
|
||||
.bootstrap-select .dropdown-menu li.disabled a {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.bootstrap-select .dropdown-menu li a {
|
||||
cursor: pointer;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
.bootstrap-select .dropdown-menu li a.opt {
|
||||
position: relative;
|
||||
padding-left: 2.25em;
|
||||
}
|
||||
.bootstrap-select .dropdown-menu li a span.check-mark {
|
||||
display: none;
|
||||
}
|
||||
.bootstrap-select .dropdown-menu li a span.text {
|
||||
display: inline-block;
|
||||
}
|
||||
.bootstrap-select .dropdown-menu li small {
|
||||
padding-left: 0.5em;
|
||||
}
|
||||
.bootstrap-select .dropdown-menu .notify {
|
||||
position: absolute;
|
||||
bottom: 5px;
|
||||
width: 96%;
|
||||
margin: 0 2%;
|
||||
min-height: 26px;
|
||||
padding: 3px 5px;
|
||||
background: #f5f5f5;
|
||||
border: 1px solid #e3e3e3;
|
||||
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05);
|
||||
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05);
|
||||
pointer-events: none;
|
||||
opacity: 0.9;
|
||||
-webkit-box-sizing: border-box;
|
||||
-moz-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.bootstrap-select .dropdown-menu .notify.fadeOut {
|
||||
-webkit-animation: 300ms linear 750ms forwards bs-notify-fadeOut;
|
||||
-o-animation: 300ms linear 750ms forwards bs-notify-fadeOut;
|
||||
animation: 300ms linear 750ms forwards bs-notify-fadeOut;
|
||||
}
|
||||
.bootstrap-select .no-results {
|
||||
padding: 3px;
|
||||
background: #f5f5f5;
|
||||
margin: 0 5px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.bootstrap-select.fit-width .dropdown-toggle .filter-option {
|
||||
position: static;
|
||||
display: inline;
|
||||
padding: 0;
|
||||
}
|
||||
.bootstrap-select.fit-width .dropdown-toggle .filter-option-inner,
|
||||
.bootstrap-select.fit-width .dropdown-toggle .filter-option-inner-inner {
|
||||
display: inline;
|
||||
}
|
||||
.bootstrap-select.fit-width .dropdown-toggle .bs-caret:before {
|
||||
content: '\00a0';
|
||||
}
|
||||
.bootstrap-select.fit-width .dropdown-toggle .caret {
|
||||
position: static;
|
||||
top: auto;
|
||||
margin-top: -1px;
|
||||
}
|
||||
.bootstrap-select.show-tick .dropdown-menu .selected span.check-mark {
|
||||
position: absolute;
|
||||
display: inline-block;
|
||||
right: 15px;
|
||||
top: 5px;
|
||||
}
|
||||
.bootstrap-select.show-tick .dropdown-menu li a span.text {
|
||||
margin-right: 34px;
|
||||
}
|
||||
.bootstrap-select .bs-ok-default:after {
|
||||
content: '';
|
||||
display: block;
|
||||
width: 0.5em;
|
||||
height: 1em;
|
||||
border-style: solid;
|
||||
border-width: 0 0.26em 0.26em 0;
|
||||
-webkit-transform-style: preserve-3d;
|
||||
transform-style: preserve-3d;
|
||||
-webkit-transform: rotate(45deg);
|
||||
-ms-transform: rotate(45deg);
|
||||
-o-transform: rotate(45deg);
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
.bootstrap-select.show-menu-arrow.open > .dropdown-toggle,
|
||||
.bootstrap-select.show-menu-arrow.show > .dropdown-toggle {
|
||||
z-index: 1061;
|
||||
}
|
||||
.bootstrap-select.show-menu-arrow .dropdown-toggle .filter-option:before {
|
||||
content: '';
|
||||
border-left: 7px solid transparent;
|
||||
border-right: 7px solid transparent;
|
||||
border-bottom: 7px solid rgba(204, 204, 204, 0.2);
|
||||
position: absolute;
|
||||
bottom: -4px;
|
||||
left: 9px;
|
||||
display: none;
|
||||
}
|
||||
.bootstrap-select.show-menu-arrow .dropdown-toggle .filter-option:after {
|
||||
content: '';
|
||||
border-left: 6px solid transparent;
|
||||
border-right: 6px solid transparent;
|
||||
border-bottom: 6px solid white;
|
||||
position: absolute;
|
||||
bottom: -4px;
|
||||
left: 10px;
|
||||
display: none;
|
||||
}
|
||||
.bootstrap-select.show-menu-arrow.dropup .dropdown-toggle .filter-option:before {
|
||||
bottom: auto;
|
||||
top: -4px;
|
||||
border-top: 7px solid rgba(204, 204, 204, 0.2);
|
||||
border-bottom: 0;
|
||||
}
|
||||
.bootstrap-select.show-menu-arrow.dropup .dropdown-toggle .filter-option:after {
|
||||
bottom: auto;
|
||||
top: -4px;
|
||||
border-top: 6px solid white;
|
||||
border-bottom: 0;
|
||||
}
|
||||
.bootstrap-select.show-menu-arrow.pull-right .dropdown-toggle .filter-option:before {
|
||||
right: 12px;
|
||||
left: auto;
|
||||
}
|
||||
.bootstrap-select.show-menu-arrow.pull-right .dropdown-toggle .filter-option:after {
|
||||
right: 13px;
|
||||
left: auto;
|
||||
}
|
||||
.bootstrap-select.show-menu-arrow.open > .dropdown-toggle .filter-option:before,
|
||||
.bootstrap-select.show-menu-arrow.show > .dropdown-toggle .filter-option:before,
|
||||
.bootstrap-select.show-menu-arrow.open > .dropdown-toggle .filter-option:after,
|
||||
.bootstrap-select.show-menu-arrow.show > .dropdown-toggle .filter-option:after {
|
||||
display: block;
|
||||
}
|
||||
.bs-searchbox,
|
||||
.bs-actionsbox,
|
||||
.bs-donebutton {
|
||||
padding: 4px 8px;
|
||||
}
|
||||
.bs-actionsbox {
|
||||
width: 100%;
|
||||
-webkit-box-sizing: border-box;
|
||||
-moz-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.bs-actionsbox .btn-group {
|
||||
display: block;
|
||||
}
|
||||
.bs-actionsbox .btn-group button {
|
||||
width: 50%;
|
||||
}
|
||||
.bs-donebutton {
|
||||
float: left;
|
||||
width: 100%;
|
||||
-webkit-box-sizing: border-box;
|
||||
-moz-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.bs-donebutton .btn-group {
|
||||
display: block;
|
||||
}
|
||||
.bs-donebutton .btn-group button {
|
||||
width: 100%;
|
||||
}
|
||||
.bs-searchbox + .bs-actionsbox {
|
||||
padding: 0 8px 4px;
|
||||
}
|
||||
.bs-searchbox .form-control {
|
||||
margin-bottom: 0;
|
||||
width: 100%;
|
||||
float: none;
|
||||
}
|
||||
/*# sourceMappingURL=bootstrap-select.css.map */
|
File diff suppressed because one or more lines are too long
|
@ -1,323 +0,0 @@
|
|||
table.footable-details,
|
||||
table.footable > thead > tr.footable-filtering > th div.form-group {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
table.footable,
|
||||
table.footable-details {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
border-spacing: 0;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
table.footable-hide-fouc {
|
||||
display: none;
|
||||
}
|
||||
table > tbody > tr > td > span.footable-toggle {
|
||||
margin-right: 8px;
|
||||
opacity: 0.3;
|
||||
}
|
||||
table > tbody > tr > td > span.footable-toggle.last-column {
|
||||
margin-left: 8px;
|
||||
float: right;
|
||||
}
|
||||
table.table-condensed > tbody > tr > td > span.footable-toggle {
|
||||
margin-right: 5px;
|
||||
}
|
||||
table.footable-details > tbody > tr > th:nth-child(1) {
|
||||
min-width: 40px;
|
||||
width: 120px;
|
||||
}
|
||||
table.footable-details > tbody > tr > td:nth-child(2) {
|
||||
word-break: break-all;
|
||||
}
|
||||
table.footable-details > tbody > tr:first-child > td,
|
||||
table.footable-details > tbody > tr:first-child > th,
|
||||
table.footable-details > tfoot > tr:first-child > td,
|
||||
table.footable-details > tfoot > tr:first-child > th,
|
||||
table.footable-details > thead > tr:first-child > td,
|
||||
table.footable-details > thead > tr:first-child > th {
|
||||
border-top-width: 0;
|
||||
}
|
||||
table.footable-details.table-bordered > tbody > tr:first-child > td,
|
||||
table.footable-details.table-bordered > tbody > tr:first-child > th,
|
||||
table.footable-details.table-bordered > tfoot > tr:first-child > td,
|
||||
table.footable-details.table-bordered > tfoot > tr:first-child > th,
|
||||
table.footable-details.table-bordered > thead > tr:first-child > td,
|
||||
table.footable-details.table-bordered > thead > tr:first-child > th {
|
||||
border-top-width: 1px;
|
||||
}
|
||||
div.footable-loader {
|
||||
vertical-align: middle;
|
||||
text-align: center;
|
||||
height: 300px;
|
||||
position: relative;
|
||||
}
|
||||
div.footable-loader > span.fooicon {
|
||||
display: inline-block;
|
||||
opacity: 0.3;
|
||||
font-size: 30px;
|
||||
line-height: 32px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
margin-top: -16px;
|
||||
margin-left: -16px;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
-webkit-animation: fooicon-spin-r 2s infinite linear;
|
||||
animation: fooicon-spin-r 2s infinite linear;
|
||||
}
|
||||
table.footable > tbody > tr.footable-empty > td {
|
||||
vertical-align: middle;
|
||||
text-align: center;
|
||||
font-size: 30px;
|
||||
}
|
||||
table.footable > tbody > tr > td,
|
||||
table.footable > tbody > tr > th {
|
||||
display: none;
|
||||
}
|
||||
table.footable > tbody > tr.footable-detail-row > td,
|
||||
table.footable > tbody > tr.footable-detail-row > th,
|
||||
table.footable > tbody > tr.footable-empty > td,
|
||||
table.footable > tbody > tr.footable-empty > th {
|
||||
display: table-cell;
|
||||
}
|
||||
@-webkit-keyframes fooicon-spin-r {
|
||||
0% {
|
||||
-webkit-transform: rotate(0);
|
||||
transform: rotate(0);
|
||||
}
|
||||
100% {
|
||||
-webkit-transform: rotate(359deg);
|
||||
transform: rotate(359deg);
|
||||
}
|
||||
}
|
||||
@keyframes fooicon-spin-r {
|
||||
0% {
|
||||
-webkit-transform: rotate(0);
|
||||
transform: rotate(0);
|
||||
}
|
||||
100% {
|
||||
-webkit-transform: rotate(359deg);
|
||||
transform: rotate(359deg);
|
||||
}
|
||||
}
|
||||
.fooicon {
|
||||
position: relative;
|
||||
top: 0px;
|
||||
display: inline-block;
|
||||
font-family: "bootstrap-icons" !important;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 1;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
@-moz-document url-prefix() {
|
||||
.fooicon {
|
||||
top: 2px;
|
||||
}
|
||||
}
|
||||
.fooicon:after,
|
||||
.fooicon:before {
|
||||
-webkit-box-sizing: border-box;
|
||||
-moz-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.fooicon-loader:before {
|
||||
content: "\f130";
|
||||
}
|
||||
.fooicon-plus:before {
|
||||
content: "\f4fc";
|
||||
}
|
||||
.fooicon-minus:before {
|
||||
content: "\f2e8";
|
||||
}
|
||||
.fooicon-search:before {
|
||||
content: "\f52a";
|
||||
}
|
||||
.fooicon-remove:before {
|
||||
content: "\f62a";
|
||||
}
|
||||
.fooicon-sort:before {
|
||||
content: "\f3c6";
|
||||
}
|
||||
.fooicon-sort-asc:before {
|
||||
content: "\f575";
|
||||
}
|
||||
.fooicon-sort-desc:before {
|
||||
content: "\f57b";
|
||||
}
|
||||
.fooicon-pencil:before {
|
||||
content: "\f4c9";
|
||||
}
|
||||
.fooicon-trash:before {
|
||||
content: "\f62a";
|
||||
}
|
||||
.fooicon-eye-close:before {
|
||||
content: "\f33f";
|
||||
}
|
||||
.fooicon-flash:before {
|
||||
content: "\f46e";
|
||||
}
|
||||
.fooicon-cog:before {
|
||||
content: "\f3e2";
|
||||
}
|
||||
.fooicon-stats:before {
|
||||
content: "\f359";
|
||||
}
|
||||
table.footable > thead > tr.footable-filtering > th {
|
||||
border-bottom-width: 1px;
|
||||
font-weight: 400;
|
||||
}
|
||||
.footable-filtering-external.footable-filtering-right,
|
||||
table.footable.footable-filtering-right > thead > tr.footable-filtering > th,
|
||||
table.footable > thead > tr.footable-filtering > th {
|
||||
text-align: right;
|
||||
}
|
||||
.footable-filtering-external.footable-filtering-left,
|
||||
table.footable.footable-filtering-left > thead > tr.footable-filtering > th {
|
||||
text-align: left;
|
||||
}
|
||||
.footable-filtering-external.footable-filtering-center,
|
||||
.footable-paging-external.footable-paging-center,
|
||||
table.footable-paging-center > tfoot > tr.footable-paging > td,
|
||||
table.footable.footable-filtering-center > thead > tr.footable-filtering > th,
|
||||
table.footable > tfoot > tr.footable-paging > td {
|
||||
text-align: center;
|
||||
}
|
||||
table.footable > thead > tr.footable-filtering > th div.form-group + div.form-group {
|
||||
margin-top: 5px;
|
||||
}
|
||||
table.footable > thead > tr.footable-filtering > th div.input-group {
|
||||
width: 100%;
|
||||
}
|
||||
.footable-filtering-external ul.dropdown-menu > li > a.checkbox,
|
||||
table.footable > thead > tr.footable-filtering > th ul.dropdown-menu > li > a.checkbox {
|
||||
margin: 0;
|
||||
display: block;
|
||||
position: relative;
|
||||
}
|
||||
.footable-filtering-external ul.dropdown-menu > li > a.checkbox > label,
|
||||
table.footable > thead > tr.footable-filtering > th ul.dropdown-menu > li > a.checkbox > label {
|
||||
display: block;
|
||||
padding-left: 20px;
|
||||
}
|
||||
.footable-filtering-external ul.dropdown-menu > li > a.checkbox input[type="checkbox"],
|
||||
table.footable > thead > tr.footable-filtering > th ul.dropdown-menu > li > a.checkbox input[type="checkbox"] {
|
||||
position: absolute;
|
||||
margin-left: -20px;
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
table.footable > thead > tr.footable-filtering > th div.input-group {
|
||||
width: auto;
|
||||
}
|
||||
table.footable > thead > tr.footable-filtering > th div.form-group {
|
||||
margin-left: 2px;
|
||||
margin-right: 2px;
|
||||
}
|
||||
table.footable > thead > tr.footable-filtering > th div.form-group + div.form-group {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
table.footable > tbody > tr > td.footable-sortable,
|
||||
table.footable > tbody > tr > th.footable-sortable,
|
||||
table.footable > tfoot > tr > td.footable-sortable,
|
||||
table.footable > tfoot > tr > th.footable-sortable,
|
||||
table.footable > thead > tr > td.footable-sortable,
|
||||
table.footable > thead > tr > th.footable-sortable {
|
||||
position: relative;
|
||||
padding-right: 30px;
|
||||
cursor: pointer;
|
||||
}
|
||||
td.footable-sortable > span.fooicon,
|
||||
th.footable-sortable > span.fooicon {
|
||||
position: absolute;
|
||||
right: 6px;
|
||||
top: 50%;
|
||||
margin-top: -7px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease-in;
|
||||
}
|
||||
td.footable-sortable.footable-asc > span.fooicon,
|
||||
td.footable-sortable.footable-desc > span.fooicon,
|
||||
td.footable-sortable:hover > span.fooicon,
|
||||
th.footable-sortable.footable-asc > span.fooicon,
|
||||
th.footable-sortable.footable-desc > span.fooicon,
|
||||
th.footable-sortable:hover > span.fooicon {
|
||||
opacity: 1;
|
||||
}
|
||||
table.footable-sorting-disabled td.footable-sortable.footable-asc > span.fooicon,
|
||||
table.footable-sorting-disabled td.footable-sortable.footable-desc > span.fooicon,
|
||||
table.footable-sorting-disabled td.footable-sortable:hover > span.fooicon,
|
||||
table.footable-sorting-disabled th.footable-sortable.footable-asc > span.fooicon,
|
||||
table.footable-sorting-disabled th.footable-sortable.footable-desc > span.fooicon,
|
||||
table.footable-sorting-disabled th.footable-sortable:hover > span.fooicon {
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
.footable-paging-external ul.pagination,
|
||||
table.footable > tfoot > tr.footable-paging > td > ul.pagination {
|
||||
margin: 10px 0 0;
|
||||
}
|
||||
.footable-paging-external span.label,
|
||||
table.footable > tfoot > tr.footable-paging > td > span.label {
|
||||
display: inline-block;
|
||||
margin: 0 0 10px;
|
||||
padding: 4px 10px;
|
||||
}
|
||||
.footable-paging-external.footable-paging-left,
|
||||
table.footable-paging-left > tfoot > tr.footable-paging > td {
|
||||
text-align: left;
|
||||
}
|
||||
.footable-paging-external.footable-paging-right,
|
||||
table.footable-editing-right td.footable-editing,
|
||||
table.footable-editing-right tr.footable-editing,
|
||||
table.footable-paging-right > tfoot > tr.footable-paging > td {
|
||||
text-align: right;
|
||||
}
|
||||
ul.pagination > li.footable-page {
|
||||
display: none;
|
||||
}
|
||||
ul.pagination > li.footable-page.visible {
|
||||
display: inline;
|
||||
}
|
||||
td.footable-editing {
|
||||
width: 90px;
|
||||
max-width: 90px;
|
||||
}
|
||||
table.footable-editing-no-delete td.footable-editing,
|
||||
table.footable-editing-no-edit td.footable-editing,
|
||||
table.footable-editing-no-view td.footable-editing {
|
||||
width: 70px;
|
||||
max-width: 70px;
|
||||
}
|
||||
table.footable-editing-no-delete.footable-editing-no-view td.footable-editing,
|
||||
table.footable-editing-no-edit.footable-editing-no-delete td.footable-editing,
|
||||
table.footable-editing-no-edit.footable-editing-no-view td.footable-editing {
|
||||
width: 50px;
|
||||
max-width: 50px;
|
||||
}
|
||||
table.footable-editing-no-edit.footable-editing-no-delete.footable-editing-no-view td.footable-editing,
|
||||
table.footable-editing-no-edit.footable-editing-no-delete.footable-editing-no-view th.footable-editing {
|
||||
width: 0;
|
||||
max-width: 0;
|
||||
display: none !important;
|
||||
}
|
||||
table.footable-editing-left td.footable-editing,
|
||||
table.footable-editing-left tr.footable-editing {
|
||||
text-align: left;
|
||||
}
|
||||
table.footable-editing button.footable-add,
|
||||
table.footable-editing button.footable-hide,
|
||||
table.footable-editing-show button.footable-show,
|
||||
table.footable-editing.footable-editing-always-show button.footable-hide,
|
||||
table.footable-editing.footable-editing-always-show button.footable-show,
|
||||
table.footable-editing.footable-editing-always-show.footable-editing-no-add tr.footable-editing {
|
||||
display: none;
|
||||
}
|
||||
table.footable-editing.footable-editing-always-show button.footable-add,
|
||||
table.footable-editing.footable-editing-show button.footable-add,
|
||||
table.footable-editing.footable-editing-show button.footable-hide {
|
||||
display: inline-block;
|
||||
}
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,691 @@
|
|||
/*
|
||||
* This combined file was created by the DataTables downloader builder:
|
||||
* https://datatables.net/download
|
||||
*
|
||||
* To rebuild or modify this file with the latest versions of the included
|
||||
* software please visit:
|
||||
* https://datatables.net/download/#bs5/dt-1.13.1/r-2.4.0/sl-1.5.0
|
||||
*
|
||||
* Included libraries:
|
||||
* DataTables 1.13.1, Responsive 2.4.0, Select 1.5.0
|
||||
*/
|
||||
|
||||
@charset "UTF-8";
|
||||
table.dataTable td.dt-control {
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
table.dataTable td.dt-control:before {
|
||||
height: 1em;
|
||||
width: 1em;
|
||||
margin-top: -9px;
|
||||
display: inline-block;
|
||||
color: white;
|
||||
border: 0.15em solid white;
|
||||
border-radius: 1em;
|
||||
box-shadow: 0 0 0.2em #444;
|
||||
box-sizing: content-box;
|
||||
text-align: center;
|
||||
text-indent: 0 !important;
|
||||
font-family: "Courier New", Courier, monospace;
|
||||
line-height: 1em;
|
||||
content: "+";
|
||||
background-color: #31b131;
|
||||
}
|
||||
table.dataTable tr.dt-hasChild td.dt-control:before {
|
||||
content: "-";
|
||||
background-color: #d33333;
|
||||
}
|
||||
|
||||
table.dataTable thead > tr > th.sorting, table.dataTable thead > tr > th.sorting_asc, table.dataTable thead > tr > th.sorting_desc, table.dataTable thead > tr > th.sorting_asc_disabled, table.dataTable thead > tr > th.sorting_desc_disabled,
|
||||
table.dataTable thead > tr > td.sorting,
|
||||
table.dataTable thead > tr > td.sorting_asc,
|
||||
table.dataTable thead > tr > td.sorting_desc,
|
||||
table.dataTable thead > tr > td.sorting_asc_disabled,
|
||||
table.dataTable thead > tr > td.sorting_desc_disabled {
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
padding-right: 26px;
|
||||
}
|
||||
table.dataTable thead > tr > th.sorting:before, table.dataTable thead > tr > th.sorting:after, table.dataTable thead > tr > th.sorting_asc:before, table.dataTable thead > tr > th.sorting_asc:after, table.dataTable thead > tr > th.sorting_desc:before, table.dataTable thead > tr > th.sorting_desc:after, table.dataTable thead > tr > th.sorting_asc_disabled:before, table.dataTable thead > tr > th.sorting_asc_disabled:after, table.dataTable thead > tr > th.sorting_desc_disabled:before, table.dataTable thead > tr > th.sorting_desc_disabled:after,
|
||||
table.dataTable thead > tr > td.sorting:before,
|
||||
table.dataTable thead > tr > td.sorting:after,
|
||||
table.dataTable thead > tr > td.sorting_asc:before,
|
||||
table.dataTable thead > tr > td.sorting_asc:after,
|
||||
table.dataTable thead > tr > td.sorting_desc:before,
|
||||
table.dataTable thead > tr > td.sorting_desc:after,
|
||||
table.dataTable thead > tr > td.sorting_asc_disabled:before,
|
||||
table.dataTable thead > tr > td.sorting_asc_disabled:after,
|
||||
table.dataTable thead > tr > td.sorting_desc_disabled:before,
|
||||
table.dataTable thead > tr > td.sorting_desc_disabled:after {
|
||||
position: absolute;
|
||||
display: block;
|
||||
opacity: 0.125;
|
||||
right: 10px;
|
||||
line-height: 9px;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
table.dataTable thead > tr > th.sorting:before, table.dataTable thead > tr > th.sorting_asc:before, table.dataTable thead > tr > th.sorting_desc:before, table.dataTable thead > tr > th.sorting_asc_disabled:before, table.dataTable thead > tr > th.sorting_desc_disabled:before,
|
||||
table.dataTable thead > tr > td.sorting:before,
|
||||
table.dataTable thead > tr > td.sorting_asc:before,
|
||||
table.dataTable thead > tr > td.sorting_desc:before,
|
||||
table.dataTable thead > tr > td.sorting_asc_disabled:before,
|
||||
table.dataTable thead > tr > td.sorting_desc_disabled:before {
|
||||
bottom: 50%;
|
||||
content: "▲";
|
||||
}
|
||||
table.dataTable thead > tr > th.sorting:after, table.dataTable thead > tr > th.sorting_asc:after, table.dataTable thead > tr > th.sorting_desc:after, table.dataTable thead > tr > th.sorting_asc_disabled:after, table.dataTable thead > tr > th.sorting_desc_disabled:after,
|
||||
table.dataTable thead > tr > td.sorting:after,
|
||||
table.dataTable thead > tr > td.sorting_asc:after,
|
||||
table.dataTable thead > tr > td.sorting_desc:after,
|
||||
table.dataTable thead > tr > td.sorting_asc_disabled:after,
|
||||
table.dataTable thead > tr > td.sorting_desc_disabled:after {
|
||||
top: 50%;
|
||||
content: "▼";
|
||||
}
|
||||
table.dataTable thead > tr > th.sorting_asc:before, table.dataTable thead > tr > th.sorting_desc:after,
|
||||
table.dataTable thead > tr > td.sorting_asc:before,
|
||||
table.dataTable thead > tr > td.sorting_desc:after {
|
||||
opacity: 0.6;
|
||||
}
|
||||
table.dataTable thead > tr > th.sorting_desc_disabled:after, table.dataTable thead > tr > th.sorting_asc_disabled:before,
|
||||
table.dataTable thead > tr > td.sorting_desc_disabled:after,
|
||||
table.dataTable thead > tr > td.sorting_asc_disabled:before {
|
||||
display: none;
|
||||
}
|
||||
table.dataTable thead > tr > th:active,
|
||||
table.dataTable thead > tr > td:active {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
div.dataTables_scrollBody table.dataTable thead > tr > th:before, div.dataTables_scrollBody table.dataTable thead > tr > th:after,
|
||||
div.dataTables_scrollBody table.dataTable thead > tr > td:before,
|
||||
div.dataTables_scrollBody table.dataTable thead > tr > td:after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
div.dataTables_processing {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 200px;
|
||||
margin-left: -100px;
|
||||
margin-top: -26px;
|
||||
text-align: center;
|
||||
padding: 2px;
|
||||
}
|
||||
div.dataTables_processing > div:last-child {
|
||||
position: relative;
|
||||
width: 80px;
|
||||
height: 15px;
|
||||
margin: 1em auto;
|
||||
}
|
||||
div.dataTables_processing > div:last-child > div {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 13px;
|
||||
height: 13px;
|
||||
border-radius: 50%;
|
||||
background: rgba(13, 110, 253, 0.9);
|
||||
animation-timing-function: cubic-bezier(0, 1, 1, 0);
|
||||
}
|
||||
div.dataTables_processing > div:last-child > div:nth-child(1) {
|
||||
left: 8px;
|
||||
animation: datatables-loader-1 0.6s infinite;
|
||||
}
|
||||
div.dataTables_processing > div:last-child > div:nth-child(2) {
|
||||
left: 8px;
|
||||
animation: datatables-loader-2 0.6s infinite;
|
||||
}
|
||||
div.dataTables_processing > div:last-child > div:nth-child(3) {
|
||||
left: 32px;
|
||||
animation: datatables-loader-2 0.6s infinite;
|
||||
}
|
||||
div.dataTables_processing > div:last-child > div:nth-child(4) {
|
||||
left: 56px;
|
||||
animation: datatables-loader-3 0.6s infinite;
|
||||
}
|
||||
|
||||
@keyframes datatables-loader-1 {
|
||||
0% {
|
||||
transform: scale(0);
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
@keyframes datatables-loader-3 {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
}
|
||||
100% {
|
||||
transform: scale(0);
|
||||
}
|
||||
}
|
||||
@keyframes datatables-loader-2 {
|
||||
0% {
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
100% {
|
||||
transform: translate(24px, 0);
|
||||
}
|
||||
}
|
||||
table.dataTable.nowrap th, table.dataTable.nowrap td {
|
||||
white-space: nowrap;
|
||||
}
|
||||
table.dataTable th.dt-left,
|
||||
table.dataTable td.dt-left {
|
||||
text-align: left;
|
||||
}
|
||||
table.dataTable th.dt-center,
|
||||
table.dataTable td.dt-center,
|
||||
table.dataTable td.dataTables_empty {
|
||||
text-align: center;
|
||||
}
|
||||
table.dataTable th.dt-right,
|
||||
table.dataTable td.dt-right {
|
||||
text-align: right;
|
||||
}
|
||||
table.dataTable th.dt-justify,
|
||||
table.dataTable td.dt-justify {
|
||||
text-align: justify;
|
||||
}
|
||||
table.dataTable th.dt-nowrap,
|
||||
table.dataTable td.dt-nowrap {
|
||||
white-space: nowrap;
|
||||
}
|
||||
table.dataTable thead th,
|
||||
table.dataTable thead td,
|
||||
table.dataTable tfoot th,
|
||||
table.dataTable tfoot td {
|
||||
text-align: left;
|
||||
}
|
||||
table.dataTable thead th.dt-head-left,
|
||||
table.dataTable thead td.dt-head-left,
|
||||
table.dataTable tfoot th.dt-head-left,
|
||||
table.dataTable tfoot td.dt-head-left {
|
||||
text-align: left;
|
||||
}
|
||||
table.dataTable thead th.dt-head-center,
|
||||
table.dataTable thead td.dt-head-center,
|
||||
table.dataTable tfoot th.dt-head-center,
|
||||
table.dataTable tfoot td.dt-head-center {
|
||||
text-align: center;
|
||||
}
|
||||
table.dataTable thead th.dt-head-right,
|
||||
table.dataTable thead td.dt-head-right,
|
||||
table.dataTable tfoot th.dt-head-right,
|
||||
table.dataTable tfoot td.dt-head-right {
|
||||
text-align: right;
|
||||
}
|
||||
table.dataTable thead th.dt-head-justify,
|
||||
table.dataTable thead td.dt-head-justify,
|
||||
table.dataTable tfoot th.dt-head-justify,
|
||||
table.dataTable tfoot td.dt-head-justify {
|
||||
text-align: justify;
|
||||
}
|
||||
table.dataTable thead th.dt-head-nowrap,
|
||||
table.dataTable thead td.dt-head-nowrap,
|
||||
table.dataTable tfoot th.dt-head-nowrap,
|
||||
table.dataTable tfoot td.dt-head-nowrap {
|
||||
white-space: nowrap;
|
||||
}
|
||||
table.dataTable tbody th.dt-body-left,
|
||||
table.dataTable tbody td.dt-body-left {
|
||||
text-align: left;
|
||||
}
|
||||
table.dataTable tbody th.dt-body-center,
|
||||
table.dataTable tbody td.dt-body-center {
|
||||
text-align: center;
|
||||
}
|
||||
table.dataTable tbody th.dt-body-right,
|
||||
table.dataTable tbody td.dt-body-right {
|
||||
text-align: right;
|
||||
}
|
||||
table.dataTable tbody th.dt-body-justify,
|
||||
table.dataTable tbody td.dt-body-justify {
|
||||
text-align: justify;
|
||||
}
|
||||
table.dataTable tbody th.dt-body-nowrap,
|
||||
table.dataTable tbody td.dt-body-nowrap {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/*! Bootstrap 5 integration for DataTables
|
||||
*
|
||||
* ©2020 SpryMedia Ltd, all rights reserved.
|
||||
* License: MIT datatables.net/license/mit
|
||||
*/
|
||||
table.dataTable {
|
||||
clear: both;
|
||||
margin-top: 6px !important;
|
||||
margin-bottom: 6px !important;
|
||||
max-width: none !important;
|
||||
border-collapse: separate !important;
|
||||
border-spacing: 0;
|
||||
}
|
||||
table.dataTable td,
|
||||
table.dataTable th {
|
||||
-webkit-box-sizing: content-box;
|
||||
box-sizing: content-box;
|
||||
}
|
||||
table.dataTable td.dataTables_empty,
|
||||
table.dataTable th.dataTables_empty {
|
||||
text-align: center;
|
||||
}
|
||||
table.dataTable.nowrap th,
|
||||
table.dataTable.nowrap td {
|
||||
white-space: nowrap;
|
||||
}
|
||||
table.dataTable.table-striped > tbody > tr:nth-of-type(2n+1) > * {
|
||||
box-shadow: none;
|
||||
}
|
||||
table.dataTable > tbody > tr {
|
||||
background-color: transparent;
|
||||
}
|
||||
table.dataTable > tbody > tr.selected > * {
|
||||
box-shadow: inset 0 0 0 9999px rgba(13, 110, 253, 0.9);
|
||||
color: white;
|
||||
}
|
||||
table.dataTable > tbody > tr.selected a {
|
||||
color: #090a0b;
|
||||
}
|
||||
table.dataTable.table-striped > tbody > tr.odd > * {
|
||||
box-shadow: inset 0 0 0 9999px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
table.dataTable.table-striped > tbody > tr.odd.selected > * {
|
||||
box-shadow: inset 0 0 0 9999px rgba(13, 110, 253, 0.95);
|
||||
}
|
||||
table.dataTable.table-hover > tbody > tr:hover > * {
|
||||
box-shadow: inset 0 0 0 9999px rgba(0, 0, 0, 0.075);
|
||||
}
|
||||
table.dataTable.table-hover > tbody > tr.selected:hover > * {
|
||||
box-shadow: inset 0 0 0 9999px rgba(13, 110, 253, 0.975);
|
||||
}
|
||||
|
||||
div.dataTables_wrapper div.dataTables_length label {
|
||||
font-weight: normal;
|
||||
text-align: left;
|
||||
white-space: nowrap;
|
||||
}
|
||||
div.dataTables_wrapper div.dataTables_length select {
|
||||
width: auto;
|
||||
display: inline-block;
|
||||
}
|
||||
div.dataTables_wrapper div.dataTables_filter {
|
||||
text-align: right;
|
||||
}
|
||||
div.dataTables_wrapper div.dataTables_filter label {
|
||||
font-weight: normal;
|
||||
white-space: nowrap;
|
||||
text-align: left;
|
||||
}
|
||||
div.dataTables_wrapper div.dataTables_filter input {
|
||||
margin-left: 0.5em;
|
||||
display: inline-block;
|
||||
width: auto;
|
||||
}
|
||||
div.dataTables_wrapper div.dataTables_info {
|
||||
padding-top: 0.85em;
|
||||
}
|
||||
div.dataTables_wrapper div.dataTables_paginate {
|
||||
margin: 0;
|
||||
white-space: nowrap;
|
||||
text-align: right;
|
||||
}
|
||||
div.dataTables_wrapper div.dataTables_paginate ul.pagination {
|
||||
margin: 2px 0;
|
||||
white-space: nowrap;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
div.dataTables_wrapper div.dt-row {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
div.dataTables_wrapper span.sorting-value {
|
||||
display: none;
|
||||
}
|
||||
|
||||
div.dataTables_scrollHead table.dataTable {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
div.dataTables_scrollBody > table {
|
||||
border-top: none;
|
||||
margin-top: 0 !important;
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
div.dataTables_scrollBody > table > thead .sorting:before,
|
||||
div.dataTables_scrollBody > table > thead .sorting_asc:before,
|
||||
div.dataTables_scrollBody > table > thead .sorting_desc:before,
|
||||
div.dataTables_scrollBody > table > thead .sorting:after,
|
||||
div.dataTables_scrollBody > table > thead .sorting_asc:after,
|
||||
div.dataTables_scrollBody > table > thead .sorting_desc:after {
|
||||
display: none;
|
||||
}
|
||||
div.dataTables_scrollBody > table > tbody tr:first-child th,
|
||||
div.dataTables_scrollBody > table > tbody tr:first-child td {
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
div.dataTables_scrollFoot > .dataTables_scrollFootInner {
|
||||
box-sizing: content-box;
|
||||
}
|
||||
div.dataTables_scrollFoot > .dataTables_scrollFootInner > table {
|
||||
margin-top: 0 !important;
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 767px) {
|
||||
div.dataTables_wrapper div.dataTables_length,
|
||||
div.dataTables_wrapper div.dataTables_filter,
|
||||
div.dataTables_wrapper div.dataTables_info,
|
||||
div.dataTables_wrapper div.dataTables_paginate {
|
||||
text-align: center;
|
||||
}
|
||||
div.dataTables_wrapper div.dataTables_paginate ul.pagination {
|
||||
justify-content: center !important;
|
||||
}
|
||||
}
|
||||
table.dataTable.table-sm > thead > tr > th:not(.sorting_disabled) {
|
||||
padding-right: 20px;
|
||||
}
|
||||
|
||||
table.table-bordered.dataTable {
|
||||
border-right-width: 0;
|
||||
}
|
||||
table.table-bordered.dataTable thead tr:first-child th,
|
||||
table.table-bordered.dataTable thead tr:first-child td {
|
||||
border-top-width: 1px;
|
||||
}
|
||||
table.table-bordered.dataTable th,
|
||||
table.table-bordered.dataTable td {
|
||||
border-left-width: 0;
|
||||
}
|
||||
table.table-bordered.dataTable th:first-child, table.table-bordered.dataTable th:first-child,
|
||||
table.table-bordered.dataTable td:first-child,
|
||||
table.table-bordered.dataTable td:first-child {
|
||||
border-left-width: 1px;
|
||||
}
|
||||
table.table-bordered.dataTable th:last-child, table.table-bordered.dataTable th:last-child,
|
||||
table.table-bordered.dataTable td:last-child,
|
||||
table.table-bordered.dataTable td:last-child {
|
||||
border-right-width: 1px;
|
||||
}
|
||||
table.table-bordered.dataTable th,
|
||||
table.table-bordered.dataTable td {
|
||||
border-bottom-width: 1px;
|
||||
}
|
||||
|
||||
div.dataTables_scrollHead table.table-bordered {
|
||||
border-bottom-width: 0;
|
||||
}
|
||||
|
||||
div.table-responsive > div.dataTables_wrapper > div.row {
|
||||
margin: 0;
|
||||
}
|
||||
div.table-responsive > div.dataTables_wrapper > div.row > div[class^=col-]:first-child {
|
||||
padding-left: 0;
|
||||
}
|
||||
div.table-responsive > div.dataTables_wrapper > div.row > div[class^=col-]:last-child {
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
|
||||
table.dataTable.dtr-inline.collapsed > tbody > tr > td.child,
|
||||
table.dataTable.dtr-inline.collapsed > tbody > tr > th.child,
|
||||
table.dataTable.dtr-inline.collapsed > tbody > tr > td.dataTables_empty {
|
||||
cursor: default !important;
|
||||
}
|
||||
table.dataTable.dtr-inline.collapsed > tbody > tr > td.child:before,
|
||||
table.dataTable.dtr-inline.collapsed > tbody > tr > th.child:before,
|
||||
table.dataTable.dtr-inline.collapsed > tbody > tr > td.dataTables_empty:before {
|
||||
display: none !important;
|
||||
}
|
||||
table.dataTable.dtr-inline.collapsed > tbody > tr > td.dtr-control,
|
||||
table.dataTable.dtr-inline.collapsed > tbody > tr > th.dtr-control {
|
||||
position: relative;
|
||||
padding-left: 30px;
|
||||
cursor: pointer;
|
||||
}
|
||||
table.dataTable.dtr-inline.collapsed > tbody > tr > td.dtr-control:before,
|
||||
table.dataTable.dtr-inline.collapsed > tbody > tr > th.dtr-control:before {
|
||||
top: 50%;
|
||||
left: 5px;
|
||||
height: 1em;
|
||||
width: 1em;
|
||||
margin-top: -9px;
|
||||
display: block;
|
||||
position: absolute;
|
||||
color: white;
|
||||
border: 0.15em solid white;
|
||||
border-radius: 1em;
|
||||
box-shadow: 0 0 0.2em #444;
|
||||
box-sizing: content-box;
|
||||
text-align: center;
|
||||
text-indent: 0 !important;
|
||||
font-family: "Courier New", Courier, monospace;
|
||||
line-height: 1em;
|
||||
content: "+";
|
||||
background-color: #0d6efd;
|
||||
}
|
||||
table.dataTable.dtr-inline.collapsed > tbody > tr.parent > td.dtr-control:before,
|
||||
table.dataTable.dtr-inline.collapsed > tbody > tr.parent > th.dtr-control:before {
|
||||
content: "-";
|
||||
background-color: #d33333;
|
||||
}
|
||||
table.dataTable.dtr-inline.collapsed.compact > tbody > tr > td.dtr-control,
|
||||
table.dataTable.dtr-inline.collapsed.compact > tbody > tr > th.dtr-control {
|
||||
padding-left: 27px;
|
||||
}
|
||||
table.dataTable.dtr-inline.collapsed.compact > tbody > tr > td.dtr-control:before,
|
||||
table.dataTable.dtr-inline.collapsed.compact > tbody > tr > th.dtr-control:before {
|
||||
left: 4px;
|
||||
height: 14px;
|
||||
width: 14px;
|
||||
border-radius: 14px;
|
||||
line-height: 14px;
|
||||
text-indent: 3px;
|
||||
}
|
||||
table.dataTable.dtr-column > tbody > tr > td.dtr-control,
|
||||
table.dataTable.dtr-column > tbody > tr > th.dtr-control,
|
||||
table.dataTable.dtr-column > tbody > tr > td.control,
|
||||
table.dataTable.dtr-column > tbody > tr > th.control {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
}
|
||||
table.dataTable.dtr-column > tbody > tr > td.dtr-control:before,
|
||||
table.dataTable.dtr-column > tbody > tr > th.dtr-control:before,
|
||||
table.dataTable.dtr-column > tbody > tr > td.control:before,
|
||||
table.dataTable.dtr-column > tbody > tr > th.control:before {
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
height: 0.8em;
|
||||
width: 0.8em;
|
||||
margin-top: -0.5em;
|
||||
margin-left: -0.5em;
|
||||
display: block;
|
||||
position: absolute;
|
||||
color: white;
|
||||
border: 0.15em solid white;
|
||||
border-radius: 1em;
|
||||
box-shadow: 0 0 0.2em #444;
|
||||
box-sizing: content-box;
|
||||
text-align: center;
|
||||
text-indent: 0 !important;
|
||||
font-family: "Courier New", Courier, monospace;
|
||||
line-height: 1em;
|
||||
content: "+";
|
||||
background-color: #0d6efd;
|
||||
}
|
||||
table.dataTable.dtr-column > tbody > tr.parent td.dtr-control:before,
|
||||
table.dataTable.dtr-column > tbody > tr.parent th.dtr-control:before,
|
||||
table.dataTable.dtr-column > tbody > tr.parent td.control:before,
|
||||
table.dataTable.dtr-column > tbody > tr.parent th.control:before {
|
||||
content: "-";
|
||||
background-color: #d33333;
|
||||
}
|
||||
table.dataTable > tbody > tr.child {
|
||||
padding: 0.5em 1em;
|
||||
}
|
||||
table.dataTable > tbody > tr.child:hover {
|
||||
background: transparent !important;
|
||||
}
|
||||
table.dataTable > tbody > tr.child ul.dtr-details {
|
||||
display: inline-block;
|
||||
list-style-type: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
table.dataTable > tbody > tr.child ul.dtr-details > li {
|
||||
border-bottom: 1px solid #efefef;
|
||||
padding: 0.5em 0;
|
||||
}
|
||||
table.dataTable > tbody > tr.child ul.dtr-details > li:first-child {
|
||||
padding-top: 0;
|
||||
}
|
||||
table.dataTable > tbody > tr.child ul.dtr-details > li:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
table.dataTable > tbody > tr.child span.dtr-title {
|
||||
display: inline-block;
|
||||
min-width: 75px;
|
||||
font-weight: bold;
|
||||
}
|
||||
div.dtr-modal {
|
||||
position: fixed;
|
||||
box-sizing: border-box;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
z-index: 100;
|
||||
padding: 10em 1em;
|
||||
}
|
||||
div.dtr-modal div.dtr-modal-display {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
width: 50%;
|
||||
height: 50%;
|
||||
overflow: auto;
|
||||
margin: auto;
|
||||
z-index: 102;
|
||||
overflow: auto;
|
||||
background-color: #f5f5f7;
|
||||
border: 1px solid black;
|
||||
border-radius: 0.5em;
|
||||
box-shadow: 0 12px 30px rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
div.dtr-modal div.dtr-modal-content {
|
||||
position: relative;
|
||||
padding: 1em;
|
||||
}
|
||||
div.dtr-modal div.dtr-modal-close {
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
right: 6px;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border: 1px solid #eaeaea;
|
||||
background-color: #f9f9f9;
|
||||
text-align: center;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
z-index: 12;
|
||||
}
|
||||
div.dtr-modal div.dtr-modal-close:hover {
|
||||
background-color: #eaeaea;
|
||||
}
|
||||
div.dtr-modal div.dtr-modal-background {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 101;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
|
||||
@media screen and (max-width: 767px) {
|
||||
div.dtr-modal div.dtr-modal-display {
|
||||
width: 95%;
|
||||
}
|
||||
}
|
||||
div.dtr-bs-modal table.table tr:first-child td {
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
table.dataTable.table-bordered th.dtr-control.dtr-hidden + *,
|
||||
table.dataTable.table-bordered td.dtr-control.dtr-hidden + * {
|
||||
border-left-width: 1px;
|
||||
}
|
||||
|
||||
|
||||
table.dataTable > tbody > tr > .selected {
|
||||
background-color: rgba(13, 110, 253, 0.9);
|
||||
color: white;
|
||||
}
|
||||
table.dataTable > tbody > tr > td.select-checkbox,
|
||||
table.dataTable > tbody > tr > th.select-checkbox {
|
||||
position: relative;
|
||||
}
|
||||
table.dataTable > tbody > tr > td.select-checkbox:before, table.dataTable > tbody > tr > td.select-checkbox:after,
|
||||
table.dataTable > tbody > tr > th.select-checkbox:before,
|
||||
table.dataTable > tbody > tr > th.select-checkbox:after {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 1.2em;
|
||||
left: 50%;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
table.dataTable > tbody > tr > td.select-checkbox:before,
|
||||
table.dataTable > tbody > tr > th.select-checkbox:before {
|
||||
content: " ";
|
||||
margin-top: -5px;
|
||||
margin-left: -6px;
|
||||
border: 1px solid black;
|
||||
border-radius: 3px;
|
||||
}
|
||||
table.dataTable > tbody > tr.selected > td.select-checkbox:before,
|
||||
table.dataTable > tbody > tr.selected > th.select-checkbox:before {
|
||||
border: 1px solid white;
|
||||
}
|
||||
table.dataTable > tbody > tr.selected > td.select-checkbox:after,
|
||||
table.dataTable > tbody > tr.selected > th.select-checkbox:after {
|
||||
content: "✓";
|
||||
font-size: 20px;
|
||||
margin-top: -19px;
|
||||
margin-left: -6px;
|
||||
text-align: center;
|
||||
text-shadow: 1px 1px #B0BED9, -1px -1px #B0BED9, 1px -1px #B0BED9, -1px 1px #B0BED9;
|
||||
}
|
||||
table.dataTable.compact > tbody > tr > td.select-checkbox:before,
|
||||
table.dataTable.compact > tbody > tr > th.select-checkbox:before {
|
||||
margin-top: -12px;
|
||||
}
|
||||
table.dataTable.compact > tbody > tr.selected > td.select-checkbox:after,
|
||||
table.dataTable.compact > tbody > tr.selected > th.select-checkbox:after {
|
||||
margin-top: -16px;
|
||||
}
|
||||
|
||||
div.dataTables_wrapper span.select-info,
|
||||
div.dataTables_wrapper span.select-item {
|
||||
margin-left: 0.5em;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 640px) {
|
||||
div.dataTables_wrapper span.select-info,
|
||||
div.dataTables_wrapper span.select-item {
|
||||
margin-left: 0;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
table.dataTable.table-sm tbody td.select-checkbox::before {
|
||||
margin-top: -9px;
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
@keyframes chartjs-render-animation{from{opacity:.99}to{opacity:1}}.chartjs-render-monitor{animation:chartjs-render-animation 1ms}.chartjs-size-monitor,.chartjs-size-monitor-expand,.chartjs-size-monitor-shrink{position:absolute;direction:ltr;left:0;top:0;right:0;bottom:0;overflow:hidden;pointer-events:none;visibility:hidden;z-index:-1}.chartjs-size-monitor-expand>div{position:absolute;width:1000000px;height:1000000px;left:0;top:0}.chartjs-size-monitor-shrink>div{position:absolute;width:200%;height:200%;left:0;top:0}
|
|
@ -1,7 +1,7 @@
|
|||
@font-face {
|
||||
font-family: "bootstrap-icons";
|
||||
src: url("/fonts/bootstrap-icons.woff2?45695e8b569b2b0178db2713ca47065c") format("woff2"),
|
||||
url("/fonts/bootstrap-icons.woff?45695e8b569b2b0178db2713ca47065c") format("woff");
|
||||
src: url("/fonts/bootstrap-icons.woff2?524846017b983fc8ded9325d94ed40f3") format("woff2"),
|
||||
url("/fonts/bootstrap-icons.woff?524846017b983fc8ded9325d94ed40f3") format("woff");
|
||||
}
|
||||
|
||||
.bi::before,
|
||||
|
@ -19,6 +19,7 @@ url("/fonts/bootstrap-icons.woff?45695e8b569b2b0178db2713ca47065c") format("woff
|
|||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.bi-123::before { content: "\f67f"; }
|
||||
.bi-alarm-fill::before { content: "\f101"; }
|
||||
.bi-alarm::before { content: "\f102"; }
|
||||
.bi-align-bottom::before { content: "\f103"; }
|
||||
|
@ -1425,3 +1426,279 @@ url("/fonts/bootstrap-icons.woff?45695e8b569b2b0178db2713ca47065c") format("woff
|
|||
.bi-webcam-fill::before { content: "\f67c"; }
|
||||
.bi-webcam::before { content: "\f67d"; }
|
||||
.bi-yin-yang::before { content: "\f67e"; }
|
||||
.bi-bandaid-fill::before { content: "\f680"; }
|
||||
.bi-bandaid::before { content: "\f681"; }
|
||||
.bi-bluetooth::before { content: "\f682"; }
|
||||
.bi-body-text::before { content: "\f683"; }
|
||||
.bi-boombox::before { content: "\f684"; }
|
||||
.bi-boxes::before { content: "\f685"; }
|
||||
.bi-dpad-fill::before { content: "\f686"; }
|
||||
.bi-dpad::before { content: "\f687"; }
|
||||
.bi-ear-fill::before { content: "\f688"; }
|
||||
.bi-ear::before { content: "\f689"; }
|
||||
.bi-envelope-check-1::before { content: "\f68a"; }
|
||||
.bi-envelope-check-fill::before { content: "\f68b"; }
|
||||
.bi-envelope-check::before { content: "\f68c"; }
|
||||
.bi-envelope-dash-1::before { content: "\f68d"; }
|
||||
.bi-envelope-dash-fill::before { content: "\f68e"; }
|
||||
.bi-envelope-dash::before { content: "\f68f"; }
|
||||
.bi-envelope-exclamation-1::before { content: "\f690"; }
|
||||
.bi-envelope-exclamation-fill::before { content: "\f691"; }
|
||||
.bi-envelope-exclamation::before { content: "\f692"; }
|
||||
.bi-envelope-plus-fill::before { content: "\f693"; }
|
||||
.bi-envelope-plus::before { content: "\f694"; }
|
||||
.bi-envelope-slash-1::before { content: "\f695"; }
|
||||
.bi-envelope-slash-fill::before { content: "\f696"; }
|
||||
.bi-envelope-slash::before { content: "\f697"; }
|
||||
.bi-envelope-x-1::before { content: "\f698"; }
|
||||
.bi-envelope-x-fill::before { content: "\f699"; }
|
||||
.bi-envelope-x::before { content: "\f69a"; }
|
||||
.bi-explicit-fill::before { content: "\f69b"; }
|
||||
.bi-explicit::before { content: "\f69c"; }
|
||||
.bi-git::before { content: "\f69d"; }
|
||||
.bi-infinity::before { content: "\f69e"; }
|
||||
.bi-list-columns-reverse::before { content: "\f69f"; }
|
||||
.bi-list-columns::before { content: "\f6a0"; }
|
||||
.bi-meta::before { content: "\f6a1"; }
|
||||
.bi-mortorboard-fill::before { content: "\f6a2"; }
|
||||
.bi-mortorboard::before { content: "\f6a3"; }
|
||||
.bi-nintendo-switch::before { content: "\f6a4"; }
|
||||
.bi-pc-display-horizontal::before { content: "\f6a5"; }
|
||||
.bi-pc-display::before { content: "\f6a6"; }
|
||||
.bi-pc-horizontal::before { content: "\f6a7"; }
|
||||
.bi-pc::before { content: "\f6a8"; }
|
||||
.bi-playstation::before { content: "\f6a9"; }
|
||||
.bi-plus-slash-minus::before { content: "\f6aa"; }
|
||||
.bi-projector-fill::before { content: "\f6ab"; }
|
||||
.bi-projector::before { content: "\f6ac"; }
|
||||
.bi-qr-code-scan::before { content: "\f6ad"; }
|
||||
.bi-qr-code::before { content: "\f6ae"; }
|
||||
.bi-quora::before { content: "\f6af"; }
|
||||
.bi-quote::before { content: "\f6b0"; }
|
||||
.bi-robot::before { content: "\f6b1"; }
|
||||
.bi-send-check-fill::before { content: "\f6b2"; }
|
||||
.bi-send-check::before { content: "\f6b3"; }
|
||||
.bi-send-dash-fill::before { content: "\f6b4"; }
|
||||
.bi-send-dash::before { content: "\f6b5"; }
|
||||
.bi-send-exclamation-1::before { content: "\f6b6"; }
|
||||
.bi-send-exclamation-fill::before { content: "\f6b7"; }
|
||||
.bi-send-exclamation::before { content: "\f6b8"; }
|
||||
.bi-send-fill::before { content: "\f6b9"; }
|
||||
.bi-send-plus-fill::before { content: "\f6ba"; }
|
||||
.bi-send-plus::before { content: "\f6bb"; }
|
||||
.bi-send-slash-fill::before { content: "\f6bc"; }
|
||||
.bi-send-slash::before { content: "\f6bd"; }
|
||||
.bi-send-x-fill::before { content: "\f6be"; }
|
||||
.bi-send-x::before { content: "\f6bf"; }
|
||||
.bi-send::before { content: "\f6c0"; }
|
||||
.bi-steam::before { content: "\f6c1"; }
|
||||
.bi-terminal-dash-1::before { content: "\f6c2"; }
|
||||
.bi-terminal-dash::before { content: "\f6c3"; }
|
||||
.bi-terminal-plus::before { content: "\f6c4"; }
|
||||
.bi-terminal-split::before { content: "\f6c5"; }
|
||||
.bi-ticket-detailed-fill::before { content: "\f6c6"; }
|
||||
.bi-ticket-detailed::before { content: "\f6c7"; }
|
||||
.bi-ticket-fill::before { content: "\f6c8"; }
|
||||
.bi-ticket-perforated-fill::before { content: "\f6c9"; }
|
||||
.bi-ticket-perforated::before { content: "\f6ca"; }
|
||||
.bi-ticket::before { content: "\f6cb"; }
|
||||
.bi-tiktok::before { content: "\f6cc"; }
|
||||
.bi-window-dash::before { content: "\f6cd"; }
|
||||
.bi-window-desktop::before { content: "\f6ce"; }
|
||||
.bi-window-fullscreen::before { content: "\f6cf"; }
|
||||
.bi-window-plus::before { content: "\f6d0"; }
|
||||
.bi-window-split::before { content: "\f6d1"; }
|
||||
.bi-window-stack::before { content: "\f6d2"; }
|
||||
.bi-window-x::before { content: "\f6d3"; }
|
||||
.bi-xbox::before { content: "\f6d4"; }
|
||||
.bi-ethernet::before { content: "\f6d5"; }
|
||||
.bi-hdmi-fill::before { content: "\f6d6"; }
|
||||
.bi-hdmi::before { content: "\f6d7"; }
|
||||
.bi-usb-c-fill::before { content: "\f6d8"; }
|
||||
.bi-usb-c::before { content: "\f6d9"; }
|
||||
.bi-usb-fill::before { content: "\f6da"; }
|
||||
.bi-usb-plug-fill::before { content: "\f6db"; }
|
||||
.bi-usb-plug::before { content: "\f6dc"; }
|
||||
.bi-usb-symbol::before { content: "\f6dd"; }
|
||||
.bi-usb::before { content: "\f6de"; }
|
||||
.bi-boombox-fill::before { content: "\f6df"; }
|
||||
.bi-displayport-1::before { content: "\f6e0"; }
|
||||
.bi-displayport::before { content: "\f6e1"; }
|
||||
.bi-gpu-card::before { content: "\f6e2"; }
|
||||
.bi-memory::before { content: "\f6e3"; }
|
||||
.bi-modem-fill::before { content: "\f6e4"; }
|
||||
.bi-modem::before { content: "\f6e5"; }
|
||||
.bi-motherboard-fill::before { content: "\f6e6"; }
|
||||
.bi-motherboard::before { content: "\f6e7"; }
|
||||
.bi-optical-audio-fill::before { content: "\f6e8"; }
|
||||
.bi-optical-audio::before { content: "\f6e9"; }
|
||||
.bi-pci-card::before { content: "\f6ea"; }
|
||||
.bi-router-fill::before { content: "\f6eb"; }
|
||||
.bi-router::before { content: "\f6ec"; }
|
||||
.bi-ssd-fill::before { content: "\f6ed"; }
|
||||
.bi-ssd::before { content: "\f6ee"; }
|
||||
.bi-thunderbolt-fill::before { content: "\f6ef"; }
|
||||
.bi-thunderbolt::before { content: "\f6f0"; }
|
||||
.bi-usb-drive-fill::before { content: "\f6f1"; }
|
||||
.bi-usb-drive::before { content: "\f6f2"; }
|
||||
.bi-usb-micro-fill::before { content: "\f6f3"; }
|
||||
.bi-usb-micro::before { content: "\f6f4"; }
|
||||
.bi-usb-mini-fill::before { content: "\f6f5"; }
|
||||
.bi-usb-mini::before { content: "\f6f6"; }
|
||||
.bi-cloud-haze2::before { content: "\f6f7"; }
|
||||
.bi-device-hdd-fill::before { content: "\f6f8"; }
|
||||
.bi-device-hdd::before { content: "\f6f9"; }
|
||||
.bi-device-ssd-fill::before { content: "\f6fa"; }
|
||||
.bi-device-ssd::before { content: "\f6fb"; }
|
||||
.bi-displayport-fill::before { content: "\f6fc"; }
|
||||
.bi-mortarboard-fill::before { content: "\f6fd"; }
|
||||
.bi-mortarboard::before { content: "\f6fe"; }
|
||||
.bi-terminal-x::before { content: "\f6ff"; }
|
||||
.bi-arrow-through-heart-fill::before { content: "\f700"; }
|
||||
.bi-arrow-through-heart::before { content: "\f701"; }
|
||||
.bi-badge-sd-fill::before { content: "\f702"; }
|
||||
.bi-badge-sd::before { content: "\f703"; }
|
||||
.bi-bag-heart-fill::before { content: "\f704"; }
|
||||
.bi-bag-heart::before { content: "\f705"; }
|
||||
.bi-balloon-fill::before { content: "\f706"; }
|
||||
.bi-balloon-heart-fill::before { content: "\f707"; }
|
||||
.bi-balloon-heart::before { content: "\f708"; }
|
||||
.bi-balloon::before { content: "\f709"; }
|
||||
.bi-box2-fill::before { content: "\f70a"; }
|
||||
.bi-box2-heart-fill::before { content: "\f70b"; }
|
||||
.bi-box2-heart::before { content: "\f70c"; }
|
||||
.bi-box2::before { content: "\f70d"; }
|
||||
.bi-braces-asterisk::before { content: "\f70e"; }
|
||||
.bi-calendar-heart-fill::before { content: "\f70f"; }
|
||||
.bi-calendar-heart::before { content: "\f710"; }
|
||||
.bi-calendar2-heart-fill::before { content: "\f711"; }
|
||||
.bi-calendar2-heart::before { content: "\f712"; }
|
||||
.bi-chat-heart-fill::before { content: "\f713"; }
|
||||
.bi-chat-heart::before { content: "\f714"; }
|
||||
.bi-chat-left-heart-fill::before { content: "\f715"; }
|
||||
.bi-chat-left-heart::before { content: "\f716"; }
|
||||
.bi-chat-right-heart-fill::before { content: "\f717"; }
|
||||
.bi-chat-right-heart::before { content: "\f718"; }
|
||||
.bi-chat-square-heart-fill::before { content: "\f719"; }
|
||||
.bi-chat-square-heart::before { content: "\f71a"; }
|
||||
.bi-clipboard-check-fill::before { content: "\f71b"; }
|
||||
.bi-clipboard-data-fill::before { content: "\f71c"; }
|
||||
.bi-clipboard-fill::before { content: "\f71d"; }
|
||||
.bi-clipboard-heart-fill::before { content: "\f71e"; }
|
||||
.bi-clipboard-heart::before { content: "\f71f"; }
|
||||
.bi-clipboard-minus-fill::before { content: "\f720"; }
|
||||
.bi-clipboard-plus-fill::before { content: "\f721"; }
|
||||
.bi-clipboard-pulse::before { content: "\f722"; }
|
||||
.bi-clipboard-x-fill::before { content: "\f723"; }
|
||||
.bi-clipboard2-check-fill::before { content: "\f724"; }
|
||||
.bi-clipboard2-check::before { content: "\f725"; }
|
||||
.bi-clipboard2-data-fill::before { content: "\f726"; }
|
||||
.bi-clipboard2-data::before { content: "\f727"; }
|
||||
.bi-clipboard2-fill::before { content: "\f728"; }
|
||||
.bi-clipboard2-heart-fill::before { content: "\f729"; }
|
||||
.bi-clipboard2-heart::before { content: "\f72a"; }
|
||||
.bi-clipboard2-minus-fill::before { content: "\f72b"; }
|
||||
.bi-clipboard2-minus::before { content: "\f72c"; }
|
||||
.bi-clipboard2-plus-fill::before { content: "\f72d"; }
|
||||
.bi-clipboard2-plus::before { content: "\f72e"; }
|
||||
.bi-clipboard2-pulse-fill::before { content: "\f72f"; }
|
||||
.bi-clipboard2-pulse::before { content: "\f730"; }
|
||||
.bi-clipboard2-x-fill::before { content: "\f731"; }
|
||||
.bi-clipboard2-x::before { content: "\f732"; }
|
||||
.bi-clipboard2::before { content: "\f733"; }
|
||||
.bi-emoji-kiss-fill::before { content: "\f734"; }
|
||||
.bi-emoji-kiss::before { content: "\f735"; }
|
||||
.bi-envelope-heart-fill::before { content: "\f736"; }
|
||||
.bi-envelope-heart::before { content: "\f737"; }
|
||||
.bi-envelope-open-heart-fill::before { content: "\f738"; }
|
||||
.bi-envelope-open-heart::before { content: "\f739"; }
|
||||
.bi-envelope-paper-fill::before { content: "\f73a"; }
|
||||
.bi-envelope-paper-heart-fill::before { content: "\f73b"; }
|
||||
.bi-envelope-paper-heart::before { content: "\f73c"; }
|
||||
.bi-envelope-paper::before { content: "\f73d"; }
|
||||
.bi-filetype-aac::before { content: "\f73e"; }
|
||||
.bi-filetype-ai::before { content: "\f73f"; }
|
||||
.bi-filetype-bmp::before { content: "\f740"; }
|
||||
.bi-filetype-cs::before { content: "\f741"; }
|
||||
.bi-filetype-css::before { content: "\f742"; }
|
||||
.bi-filetype-csv::before { content: "\f743"; }
|
||||
.bi-filetype-doc::before { content: "\f744"; }
|
||||
.bi-filetype-docx::before { content: "\f745"; }
|
||||
.bi-filetype-exe::before { content: "\f746"; }
|
||||
.bi-filetype-gif::before { content: "\f747"; }
|
||||
.bi-filetype-heic::before { content: "\f748"; }
|
||||
.bi-filetype-html::before { content: "\f749"; }
|
||||
.bi-filetype-java::before { content: "\f74a"; }
|
||||
.bi-filetype-jpg::before { content: "\f74b"; }
|
||||
.bi-filetype-js::before { content: "\f74c"; }
|
||||
.bi-filetype-jsx::before { content: "\f74d"; }
|
||||
.bi-filetype-key::before { content: "\f74e"; }
|
||||
.bi-filetype-m4p::before { content: "\f74f"; }
|
||||
.bi-filetype-md::before { content: "\f750"; }
|
||||
.bi-filetype-mdx::before { content: "\f751"; }
|
||||
.bi-filetype-mov::before { content: "\f752"; }
|
||||
.bi-filetype-mp3::before { content: "\f753"; }
|
||||
.bi-filetype-mp4::before { content: "\f754"; }
|
||||
.bi-filetype-otf::before { content: "\f755"; }
|
||||
.bi-filetype-pdf::before { content: "\f756"; }
|
||||
.bi-filetype-php::before { content: "\f757"; }
|
||||
.bi-filetype-png::before { content: "\f758"; }
|
||||
.bi-filetype-ppt-1::before { content: "\f759"; }
|
||||
.bi-filetype-ppt::before { content: "\f75a"; }
|
||||
.bi-filetype-psd::before { content: "\f75b"; }
|
||||
.bi-filetype-py::before { content: "\f75c"; }
|
||||
.bi-filetype-raw::before { content: "\f75d"; }
|
||||
.bi-filetype-rb::before { content: "\f75e"; }
|
||||
.bi-filetype-sass::before { content: "\f75f"; }
|
||||
.bi-filetype-scss::before { content: "\f760"; }
|
||||
.bi-filetype-sh::before { content: "\f761"; }
|
||||
.bi-filetype-svg::before { content: "\f762"; }
|
||||
.bi-filetype-tiff::before { content: "\f763"; }
|
||||
.bi-filetype-tsx::before { content: "\f764"; }
|
||||
.bi-filetype-ttf::before { content: "\f765"; }
|
||||
.bi-filetype-txt::before { content: "\f766"; }
|
||||
.bi-filetype-wav::before { content: "\f767"; }
|
||||
.bi-filetype-woff::before { content: "\f768"; }
|
||||
.bi-filetype-xls-1::before { content: "\f769"; }
|
||||
.bi-filetype-xls::before { content: "\f76a"; }
|
||||
.bi-filetype-xml::before { content: "\f76b"; }
|
||||
.bi-filetype-yml::before { content: "\f76c"; }
|
||||
.bi-heart-arrow::before { content: "\f76d"; }
|
||||
.bi-heart-pulse-fill::before { content: "\f76e"; }
|
||||
.bi-heart-pulse::before { content: "\f76f"; }
|
||||
.bi-heartbreak-fill::before { content: "\f770"; }
|
||||
.bi-heartbreak::before { content: "\f771"; }
|
||||
.bi-hearts::before { content: "\f772"; }
|
||||
.bi-hospital-fill::before { content: "\f773"; }
|
||||
.bi-hospital::before { content: "\f774"; }
|
||||
.bi-house-heart-fill::before { content: "\f775"; }
|
||||
.bi-house-heart::before { content: "\f776"; }
|
||||
.bi-incognito::before { content: "\f777"; }
|
||||
.bi-magnet-fill::before { content: "\f778"; }
|
||||
.bi-magnet::before { content: "\f779"; }
|
||||
.bi-person-heart::before { content: "\f77a"; }
|
||||
.bi-person-hearts::before { content: "\f77b"; }
|
||||
.bi-phone-flip::before { content: "\f77c"; }
|
||||
.bi-plugin::before { content: "\f77d"; }
|
||||
.bi-postage-fill::before { content: "\f77e"; }
|
||||
.bi-postage-heart-fill::before { content: "\f77f"; }
|
||||
.bi-postage-heart::before { content: "\f780"; }
|
||||
.bi-postage::before { content: "\f781"; }
|
||||
.bi-postcard-fill::before { content: "\f782"; }
|
||||
.bi-postcard-heart-fill::before { content: "\f783"; }
|
||||
.bi-postcard-heart::before { content: "\f784"; }
|
||||
.bi-postcard::before { content: "\f785"; }
|
||||
.bi-search-heart-fill::before { content: "\f786"; }
|
||||
.bi-search-heart::before { content: "\f787"; }
|
||||
.bi-sliders2-vertical::before { content: "\f788"; }
|
||||
.bi-sliders2::before { content: "\f789"; }
|
||||
.bi-trash3-fill::before { content: "\f78a"; }
|
||||
.bi-trash3::before { content: "\f78b"; }
|
||||
.bi-valentine::before { content: "\f78c"; }
|
||||
.bi-valentine2::before { content: "\f78d"; }
|
||||
.bi-wrench-adjustable-circle-fill::before { content: "\f78e"; }
|
||||
.bi-wrench-adjustable-circle::before { content: "\f78f"; }
|
||||
.bi-wrench-adjustable::before { content: "\f790"; }
|
||||
.bi-filetype-json::before { content: "\f791"; }
|
||||
.bi-filetype-pptx::before { content: "\f792"; }
|
||||
.bi-filetype-xlsx::before { content: "\f793"; }
|
|
@ -0,0 +1,98 @@
|
|||
.dataTables_info {
|
||||
margin: 15px 0 !important;
|
||||
padding: 0px !important;
|
||||
}
|
||||
.dataTables_paginate, .dataTables_length, .dataTables_filter {
|
||||
margin: 15px 0 !important;
|
||||
}
|
||||
.dtr-details {
|
||||
width: 100%;
|
||||
}
|
||||
.table-striped>tbody>tr:nth-of-type(odd) {
|
||||
background-color: #F2F2F2;
|
||||
}
|
||||
td.child>ul>li {
|
||||
display: flex;
|
||||
}
|
||||
table.dataTable>tbody>tr.child ul.dtr-details>li {
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.129);
|
||||
padding: 0.5em 0;
|
||||
}
|
||||
table.dataTable.dtr-inline.collapsed>tbody>tr>td.dtr-control:before:hover,
|
||||
table.dataTable.dtr-inline.collapsed>tbody>tr>th.dtr-control:before:hover {
|
||||
background-color: #5e5e5e;
|
||||
}
|
||||
table.dataTable.dtr-inline.collapsed>tbody>tr>td.dtr-control:before,
|
||||
table.dataTable.dtr-inline.collapsed>tbody>tr>th.dtr-control:before,
|
||||
table.dataTable td.dt-control:before {
|
||||
background-color: #979797 !important;
|
||||
border: 1.5px solid #616161 !important;
|
||||
border-radius: 2px !important;
|
||||
color: #fff;
|
||||
height: 1em;
|
||||
width: 1em;
|
||||
line-height: 1.25em;
|
||||
border-radius: 0px;
|
||||
box-shadow: none;
|
||||
font-size: 14px;
|
||||
transition: 0.5s all;
|
||||
}
|
||||
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 td.dt-control:before {
|
||||
background-color: #979797 !important;
|
||||
}
|
||||
table.dataTable.dtr-inline.collapsed>tbody>tr>td.child,
|
||||
table.dataTable.dtr-inline.collapsed>tbody>tr>th.child,
|
||||
table.dataTable.dtr-inline.collapsed>tbody>tr>td.dataTables_empty {
|
||||
background-color: #fbfbfb;
|
||||
}
|
||||
table.dataTable.table-striped>tbody>tr>td {
|
||||
vertical-align: middle;
|
||||
}
|
||||
table.dataTable.table-striped>tbody>tr>td>input[type="checkbox"] {
|
||||
margin-top: 7px;
|
||||
}
|
||||
td.dtr-col-lg {
|
||||
min-width: 350px;
|
||||
word-break: break-word;
|
||||
}
|
||||
td.dtr-col-md {
|
||||
min-width: 250px;
|
||||
word-break: break-word;
|
||||
}
|
||||
td.dtr-col-sm {
|
||||
min-width: 125px;
|
||||
word-break: break-word;
|
||||
}
|
||||
.dt-data-w100 .dtr-data {
|
||||
width: 100%;
|
||||
}
|
||||
li .dtr-data {
|
||||
word-break: break-all;
|
||||
flex: 1;
|
||||
padding-left: 5px;
|
||||
padding-right: 5px;
|
||||
}
|
||||
table.dataTable>tbody>tr.child span.dtr-title {
|
||||
width: 30%;
|
||||
max-width: 250px;
|
||||
}
|
||||
|
||||
|
||||
div.dataTables_wrapper div.dataTables_filter {
|
||||
text-align: left;
|
||||
}
|
||||
div.dataTables_wrapper div.dataTables_length {
|
||||
text-align: right;
|
||||
}
|
||||
.dataTables_paginate, .dataTables_length, .dataTables_filter {
|
||||
margin: 10px 0!important;
|
||||
}
|
||||
|
||||
td.dt-text-right {
|
||||
text-align: end !important;
|
||||
}
|
||||
th.dt-text-right {
|
||||
text-align: end !important;
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue