mirror of
https://github.com/mailcow/mailcow-dockerized.git
synced 2026-09-01 03:37:15 +00:00
Compare commits
408
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
161f763b6c | ||
|
|
17d20599a7 | ||
|
|
d80b15b06c | ||
|
|
ed4fe41d7a | ||
|
|
6b9d875773 | ||
|
|
4d5b9d1c80 | ||
|
|
8abf453e1c | ||
|
|
8bd9e12a73 | ||
|
|
c665d430de | ||
|
|
2195a674ff | ||
|
|
089c44aee1 | ||
|
|
6644699e65 | ||
|
|
91d3b7246a | ||
|
|
b362c7105d | ||
|
|
555e7ed706 | ||
|
|
59e2a9ac77 | ||
|
|
feed5ad183 | ||
|
|
f1f7a9800b | ||
|
|
8c85044781 | ||
|
|
8fb32e4b59 | ||
|
|
b5fa926bc0 | ||
|
|
d1a2f4e168 | ||
|
|
f7a536d634 | ||
|
|
8b456a33e7 | ||
|
|
54da526d41 | ||
|
|
50be483c39 | ||
|
|
d64c923aca | ||
|
|
2d5f166ba8 | ||
|
|
94b5a623ab | ||
|
|
e406ecd461 | ||
|
|
68a95c3af3 | ||
|
|
bd037d5644 | ||
|
|
7036dbf4f8 | ||
|
|
2f10a4633f | ||
|
|
36c70db86c | ||
|
|
9cd16f0001 | ||
|
|
f4961c4023 | ||
|
|
38de21592c | ||
|
|
f44bd2f36a | ||
|
|
95a77f2dcb | ||
|
|
e856510fb2 | ||
|
|
d51d06d716 | ||
|
|
54170d075a | ||
|
|
b4bb1a625b | ||
|
|
2e5a29bf69 | ||
|
|
e245ac04d9 | ||
|
|
c877fdf0a5 | ||
|
|
fea38c8e1b | ||
|
|
cc9af65852 | ||
|
|
92cc8bec90 | ||
|
|
145745329e | ||
|
|
8e72d22c56 | ||
|
|
ddd76d99cd | ||
|
|
7e63061b30 | ||
|
|
52ca8f95c2 | ||
|
|
6758999cf3 | ||
|
|
5c5f251d35 | ||
|
|
1a10d0748e | ||
|
|
f4f9f72395 | ||
|
|
6f81778e5e | ||
|
|
8c9524a2fe | ||
|
|
2ebd32d2ee | ||
|
|
069b89629e | ||
|
|
496c3a51ac | ||
|
|
06a2a14c55 | ||
|
|
4b62af9d02 | ||
|
|
90ca1bf25a | ||
|
|
14772c3a20 | ||
|
|
e5d0ed8c4c | ||
|
|
c17cc8a792 | ||
|
|
4cba448671 | ||
|
|
7dab6c63d1 | ||
|
|
5408bc809d | ||
|
|
bd5c18b145 | ||
|
|
0f06c8e563 | ||
|
|
fffe1aec3d | ||
|
|
6f7fee49cd | ||
|
|
c1d75cf808 | ||
|
|
473fe2885d | ||
|
|
31e1550668 | ||
|
|
315f55bc65 | ||
|
|
e696ee2f6c | ||
|
|
d65aa11870 | ||
|
|
f6630e5b43 | ||
|
|
98e26c5603 | ||
|
|
3098caebbc | ||
|
|
94e23a3cd3 | ||
|
|
1840074991 | ||
|
|
99768e16f5 | ||
|
|
1f8b4679cc | ||
|
|
9e723e942d | ||
|
|
47e4552b44 | ||
|
|
ddcce811c1 | ||
|
|
7e6c36dce1 | ||
|
|
fa154c71f3 | ||
|
|
83a045ed3e | ||
|
|
064817ac70 | ||
|
|
2bd7a24b8c | ||
|
|
ecc2462e4c | ||
|
|
2d8db72d46 | ||
|
|
277a307fb9 | ||
|
|
d455555621 | ||
|
|
9ea2ff1dff | ||
|
|
24cc369d84 | ||
|
|
5f5367f2f9 | ||
|
|
03c31f825a | ||
|
|
c0050e8836 | ||
|
|
15891960f7 | ||
|
|
cce02e2b15 | ||
|
|
0fafda696b | ||
|
|
843db5854c | ||
|
|
23e4e4f373 | ||
|
|
175878f8f1 | ||
|
|
3fcda21c4e | ||
|
|
eac1bf02fc | ||
|
|
a7f2b2145f | ||
|
|
faac56a611 | ||
|
|
be37bc5a68 | ||
|
|
b227c76156 | ||
|
|
db4e3b4d54 | ||
|
|
ffbc37a00c | ||
|
|
e6fa0c5099 | ||
|
|
ce3ceaf5b8 | ||
|
|
efc2e76159 | ||
|
|
7d17715e2e | ||
|
|
aed440bcb3 | ||
|
|
4f2348631a | ||
|
|
da586682f6 | ||
|
|
22ae1f5363 | ||
|
|
c9cf8d7f38 | ||
|
|
cbd3d8b9bc | ||
|
|
f553f38635 | ||
|
|
07373907e3 | ||
|
|
104af58628 | ||
|
|
4ddcee28e4 | ||
|
|
b0d16bbcee | ||
|
|
9175e5f086 | ||
|
|
ff3d571054 | ||
|
|
a8e945f3da | ||
|
|
33547d1d73 | ||
|
|
539c32d99c | ||
|
|
d42c3823c4 | ||
|
|
8ead057ce9 | ||
|
|
886dbcc419 | ||
|
|
b0873edb6a | ||
|
|
694d751915 | ||
|
|
dc15994d40 | ||
|
|
ec24825280 | ||
|
|
08278a8be9 | ||
|
|
5a00b5124b | ||
|
|
8c039f694f | ||
|
|
95bf46c1e4 | ||
|
|
edde35156d | ||
|
|
84e3c32f13 | ||
|
|
ecb848493b | ||
|
|
8a65b9d1c6 | ||
|
|
ed9264fd2a | ||
|
|
7817dda43f | ||
|
|
018e292854 | ||
|
|
127fb1e8f5 | ||
|
|
09f09cb850 | ||
|
|
d4bf377a96 | ||
|
|
abd6fe8c79 | ||
|
|
5f8382ef44 | ||
|
|
03eccd4e42 | ||
|
|
1da8d1c894 | ||
|
|
d1feebf164 | ||
|
|
293b885a85 | ||
|
|
1e0850193a | ||
|
|
33acf56526 | ||
|
|
bea9ad7e8f | ||
|
|
e7ea3aa608 | ||
|
|
5888e248c3 | ||
|
|
2e176339ba | ||
|
|
8f883f3d37 | ||
|
|
709117fe19 | ||
|
|
82ea418423 | ||
|
|
fd24163c6e | ||
|
|
3caef45a12 | ||
|
|
8760e7e5db | ||
|
|
e848226062 | ||
|
|
efaeb77e13 | ||
|
|
569b4cf985 | ||
|
|
6c857243ec | ||
|
|
905e93627c | ||
|
|
598ea21827 | ||
|
|
6012cf4486 | ||
|
|
fe71f84c82 | ||
|
|
763ecbc93e | ||
|
|
97312c1a9d | ||
|
|
baf0e7e658 | ||
|
|
a4ca5f9363 | ||
|
|
dcbea71e67 | ||
|
|
524f19ff77 | ||
|
|
4e33c7143f | ||
|
|
4c184b25f4 | ||
|
|
ddc2309f1e | ||
|
|
bccfcefd17 | ||
|
|
d40252ccce | ||
|
|
4abb5cbfab | ||
|
|
b695936273 | ||
|
|
d9463c7950 | ||
|
|
579542381a | ||
|
|
ce5659f300 | ||
|
|
1967cb642f | ||
|
|
ba0eb04ebe | ||
|
|
609ce6b0d6 | ||
|
|
af61e2d303 | ||
|
|
c6e3f517e1 | ||
|
|
6eaa8d43e9 | ||
|
|
c033cd6254 | ||
|
|
7562578b74 | ||
|
|
43f570e761 | ||
|
|
5e478a32df | ||
|
|
22c94a4d2f | ||
|
|
99dc0f6616 | ||
|
|
4732f568fa | ||
|
|
96d4802cb2 | ||
|
|
ad5b94af5e | ||
|
|
7fce984cac | ||
|
|
404e2f0190 | ||
|
|
1c52eaa3a4 | ||
|
|
c7e04b4146 | ||
|
|
075959aea9 | ||
|
|
885ba2510e | ||
|
|
87563249f1 | ||
|
|
fb1686065d | ||
|
|
0428f5c9bd | ||
|
|
a70c23065c | ||
|
|
caaa4a414d | ||
|
|
c3d841340c | ||
|
|
2e8897c2cf | ||
|
|
b8cd00111f | ||
|
|
81cda80651 | ||
|
|
c1d4f04c22 | ||
|
|
82276cd1ca | ||
|
|
56ea4302ed | ||
|
|
c06112b26e | ||
|
|
aa5a4f0998 | ||
|
|
bf4f471cfd | ||
|
|
978bff9dbc | ||
|
|
869d9af7dd | ||
|
|
af10499ecb | ||
|
|
a1a4d8ff98 | ||
|
|
95d61e8aa2 | ||
|
|
ec8dd1a54f | ||
|
|
382ee34d0e | ||
|
|
0999c9e9ab | ||
|
|
c485968e7f | ||
|
|
e727620bd3 | ||
|
|
71fa3ecebc | ||
|
|
70101d1187 | ||
|
|
c060c205d3 | ||
|
|
5ca900749c | ||
|
|
b005803fe0 | ||
|
|
ec77406dba | ||
|
|
ee15721550 | ||
|
|
038b2efb75 | ||
|
|
1fe4cd03e9 | ||
|
|
12e02e67ff | ||
|
|
b6f57dfb78 | ||
|
|
3ebf2c2d2d | ||
|
|
1bac6f1ee7 | ||
|
|
67e7acd6bd | ||
|
|
910ce573d6 | ||
|
|
689336b3e1 | ||
|
|
01cf72cdef | ||
|
|
4cdb97c699 | ||
|
|
1bd795a9c6 | ||
|
|
39f29e6c30 | ||
|
|
1ab6af21e3 | ||
|
|
5d95c48e0d | ||
|
|
dbb9e474b0 | ||
|
|
f8eed8c786 | ||
|
|
ef010aa39c | ||
|
|
79171ea6f5 | ||
|
|
4e3294b273 | ||
|
|
32a6ecddb6 | ||
|
|
f3d9833ecf | ||
|
|
930ca76ea7 | ||
|
|
9a2887cf46 | ||
|
|
9950914086 | ||
|
|
470cfb0026 | ||
|
|
6c106b4e4d | ||
|
|
3d6253a2b2 | ||
|
|
b873812588 | ||
|
|
514fefd2ed | ||
|
|
6f9ee2d151 | ||
|
|
9832006141 | ||
|
|
890295bbfc | ||
|
|
0413d26855 | ||
|
|
7b29c1f304 | ||
|
|
ae3ef391ee | ||
|
|
7313f996d3 | ||
|
|
62d16c9e56 | ||
|
|
a52e977b89 | ||
|
|
674b41ce08 | ||
|
|
1b833be760 | ||
|
|
88adb1adf5 | ||
|
|
ec472f13cf | ||
|
|
2e1d98cc7c | ||
|
|
07d7e3dc30 | ||
|
|
b0f5aee628 | ||
|
|
d3065612fd | ||
|
|
9912e41f78 | ||
|
|
04200c99a4 | ||
|
|
45666d2c4e | ||
|
|
9a806e64ce | ||
|
|
95e0608749 | ||
|
|
22a09b9795 | ||
|
|
04d5c43550 | ||
|
|
fbcb8cbeb9 | ||
|
|
0338a36ecf | ||
|
|
23fb5e2fca | ||
|
|
3507ff2773 | ||
|
|
a4970397f1 | ||
|
|
4132f6bd48 | ||
|
|
586b3a2ed1 | ||
|
|
6af2addf3c | ||
|
|
f6eed6c441 | ||
|
|
b85837c803 | ||
|
|
653fc40d4c | ||
|
|
c17d80a6fd | ||
|
|
980bfa3aa0 | ||
|
|
664a954393 | ||
|
|
d5a27c4ccb | ||
|
|
6a8a2e2136 | ||
|
|
b859a52b8e | ||
|
|
10e0c42eff | ||
|
|
f47df263d7 | ||
|
|
2642d9109e | ||
|
|
6708b94ebb | ||
|
|
79cf0abc6e | ||
|
|
7de70322d6 | ||
|
|
417835dea8 | ||
|
|
3dcacc4187 | ||
|
|
69f0552d4f | ||
|
|
c443a9400a | ||
|
|
5c9f387d94 | ||
|
|
e9414d17e4 | ||
|
|
6bfa58611e | ||
|
|
df4d3bb6e0 | ||
|
|
e31b6d9a07 | ||
|
|
455ef084b4 | ||
|
|
c2948735f2 | ||
|
|
24c62b2f09 | ||
|
|
1ef0149076 | ||
|
|
922d173540 | ||
|
|
fd088cb504 | ||
|
|
721ee2394e | ||
|
|
c217be06c6 | ||
|
|
871c422ec1 | ||
|
|
3cc28af607 | ||
|
|
796e131c3a | ||
|
|
dd160cd508 | ||
|
|
732b321962 | ||
|
|
c51a769aec | ||
|
|
45a61755a5 | ||
|
|
769c57c355 | ||
|
|
2e7eb7c0fd | ||
|
|
4c83147d01 | ||
|
|
ca0bec4fc2 | ||
|
|
6f50dd17da | ||
|
|
4a331929d0 | ||
|
|
748bc893b6 | ||
|
|
e462602ddc | ||
|
|
4e0f435d12 | ||
|
|
46f0581936 | ||
|
|
20f04ecf6b | ||
|
|
ff43799763 | ||
|
|
85ca197615 | ||
|
|
d06d23bbaf | ||
|
|
702ed85dfd | ||
|
|
8abe74a562 | ||
|
|
2f8a181281 | ||
|
|
5c5287ca21 | ||
|
|
83ba8d5840 | ||
|
|
ce219668cf | ||
|
|
5b1b49a418 | ||
|
|
8978a9ad79 | ||
|
|
5f4a4fd759 | ||
|
|
171c591da4 | ||
|
|
9133b9899c | ||
|
|
701c9fb1b4 | ||
|
|
eabd22188b | ||
|
|
7028619742 | ||
|
|
c915bf2ee2 | ||
|
|
011edd5ac9 | ||
|
|
7ba3de4ced | ||
|
|
8ead77083f | ||
|
|
b2774fb50b | ||
|
|
4440bd46ad | ||
|
|
28985973eb | ||
|
|
f2c4697ca3 | ||
|
|
383b5affb5 | ||
|
|
ed4dcff63b | ||
|
|
caca32bbba | ||
|
|
d31e74c778 | ||
|
|
6c00e29276 | ||
|
|
9940c503a2 | ||
|
|
4b2862cb3c | ||
|
|
a36485f0f1 | ||
|
|
78168ee80a | ||
|
|
610609378f | ||
|
|
260906e350 | ||
|
|
2891bbf82a | ||
|
|
eb26bcbc94 | ||
|
|
6dc90186f9 |
@@ -11,22 +11,35 @@ body:
|
||||
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)*
|
||||
label: Checklist prior issue creation
|
||||
description: Prior to creating the issue...
|
||||
options:
|
||||
- label: ... I understand that not following the below instructions will result in immediate closure and/or deletion of my issue.
|
||||
- label: I understand that failure to follow below instructions may cause this issue to be closed.
|
||||
required: true
|
||||
- label: ... I have understood that this bug report is dedicated for bugs, and not for support-related inquiries.
|
||||
- label: I understand that vague, incomplete or inaccurate information may cause this issue to be closed.
|
||||
required: true
|
||||
- label: ... I have understood that answers are voluntary and community-driven, and not commercial support.
|
||||
- label: I understand that this form is intended solely for reporting software bugs and not for support-related inquiries.
|
||||
required: true
|
||||
- label: ... I have verified that my issue has not been already answered in the past. I also checked previous [issues](https://github.com/mailcow/mailcow-dockerized/issues).
|
||||
- label: I understand that all responses are voluntary and community-driven, and do not constitute commercial support.
|
||||
required: true
|
||||
- label: I confirm that I have reviewed previous [issues](https://github.com/mailcow/mailcow-dockerized/issues) to ensure this matter has not already been addressed.
|
||||
required: true
|
||||
- label: I confirm that my environment meets all [prerequisite requirements](https://docs.mailcow.email/getstarted/prerequisite-system/) as specified in the official documentation.
|
||||
required: true
|
||||
- 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
|
||||
description: Please provide a brief description of the bug. If applicable, add screenshots to help explain your problem. (Very useful for bugs in mailcow UI.)
|
||||
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
|
||||
@@ -36,45 +49,36 @@ body:
|
||||
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.
|
||||
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`"
|
||||
description: "#### Run: `git rev-parse --abbrev-ref HEAD`"
|
||||
multiple: false
|
||||
options:
|
||||
- master
|
||||
- master (stable)
|
||||
- staging
|
||||
- nightly
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: "Which architecture are you using?"
|
||||
description: "#### `uname -m`"
|
||||
description: "#### Run: `uname -m`"
|
||||
multiple: false
|
||||
options:
|
||||
- x86
|
||||
- x86_64
|
||||
- ARM64 (aarch64)
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
attributes:
|
||||
label: "Operating System:"
|
||||
description: "#### Run: `lsb_release -ds`"
|
||||
placeholder: "e.g. Ubuntu 22.04 LTS"
|
||||
validations:
|
||||
required: true
|
||||
@@ -93,43 +97,44 @@ body:
|
||||
- type: input
|
||||
attributes:
|
||||
label: "Virtualization technology:"
|
||||
placeholder: "KVM, VMware, Xen, etc - **LXC and OpenVZ are not supported**"
|
||||
description: "LXC and OpenVZ are not supported!"
|
||||
placeholder: "KVM, VMware ESXi, Xen, etc"
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
attributes:
|
||||
label: "Docker version:"
|
||||
description: "#### `docker version`"
|
||||
description: "#### Run: `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`"
|
||||
description: "#### Run: `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"
|
||||
description: "#### Run: ```git describe --tags `git rev-list --tags --max-count=1` ```"
|
||||
placeholder: "2022-08x"
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
attributes:
|
||||
label: "Reverse proxy:"
|
||||
placeholder: "e.g. Nginx/Traefik"
|
||||
placeholder: "e.g. nginx/Traefik, or none"
|
||||
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**:"
|
||||
description: "#### Output of `git diff origin/master`, any other changes to the code? Sanitize if needed. If so, **please post them**:"
|
||||
render: plain text
|
||||
validations:
|
||||
required: true
|
||||
required: false
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: "Logs of iptables -L -vn:"
|
||||
|
||||
@@ -14,7 +14,7 @@ jobs:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Mark/Close Stale Issues and Pull Requests 🗑️
|
||||
uses: actions/stale@v10.0.0
|
||||
uses: actions/stale@v11.0.0
|
||||
with:
|
||||
repo-token: ${{ secrets.STALE_ACTION_PAT }}
|
||||
days-before-stale: 60
|
||||
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
- "watchdog-mailcow"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v7
|
||||
- name: Setup Docker
|
||||
run: |
|
||||
curl -sSL https://get.docker.com/ | CHANNEL=stable sudo sh
|
||||
|
||||
@@ -8,11 +8,11 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Run the Action
|
||||
uses: devops-infra/action-pull-request@v0.6.1
|
||||
uses: devops-infra/action-pull-request@v1.4.0
|
||||
with:
|
||||
github_token: ${{ secrets.PRTONIGHTLY_ACTION_PAT }}
|
||||
title: Automatic PR to nightly from ${{ github.event.repository.updated_at}}
|
||||
|
||||
@@ -13,24 +13,24 @@ jobs:
|
||||
packages: write
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
uses: docker/setup-qemu-action@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
- name: Login to GHCR
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
|
||||
@@ -15,14 +15,14 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Generate postscreen_access.cidr
|
||||
run: |
|
||||
bash helper-scripts/update_postscreen_whitelist.sh
|
||||
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
uses: peter-evans/create-pull-request@v8
|
||||
with:
|
||||
token: ${{ secrets.mailcow_action_Update_postscreen_access_cidr_pat }}
|
||||
commit-message: update postscreen_access.cidr
|
||||
|
||||
@@ -51,6 +51,7 @@ data/conf/sogo/cron.creds
|
||||
data/conf/sogo/custom-fulllogo.svg
|
||||
data/conf/sogo/custom-shortlogo.svg
|
||||
data/conf/sogo/custom-fulllogo.png
|
||||
data/conf/acme/dns-01.conf
|
||||
data/gitea/
|
||||
data/gogs/
|
||||
data/hooks/dovecot/*
|
||||
|
||||
+7
-3
@@ -1,11 +1,11 @@
|
||||
# Contribution Guidelines
|
||||
**_Last modified on 15th August 2024_**
|
||||
**_Last modified on 12th November 2025_**
|
||||
|
||||
First of all, thank you for wanting to provide a bugfix or a new feature for the mailcow community, it's because of your help that the project can continue to grow!
|
||||
|
||||
As we want to keep mailcow's development structured we setup these Guidelines which helps you to create your issue/pull request accordingly.
|
||||
|
||||
**PLEASE NOTE, THAT WE MIGHT CLOSE ISSUES/PULL REQUESTS IF THEY DON'T FULLFIL OUR WRITTEN GUIDELINES WRITTEN INSIDE THIS DOCUMENT**. So please check this guidelines before you propose a Issue/Pull Request.
|
||||
**PLEASE NOTE, THAT WE WILL CLOSE ISSUES/PULL REQUESTS IF THEY DON'T FULFILL OUR WRITTEN GUIDELINES WRITTEN INSIDE THIS DOCUMENT**. So please check this guidelines before you propose a Issue/Pull Request.
|
||||
|
||||
## Topics
|
||||
|
||||
@@ -27,14 +27,18 @@ However, please note the following regarding pull requests:
|
||||
6. Please **ALWAYS** create the actual pull request against the staging branch and **NEVER** directly against the master branch. *If you forget to do this, our moobot will remind you to switch the branch to staging.*
|
||||
7. Wait for a merge commit: It may happen that we do not accept your pull request immediately or sometimes not at all for various reasons. Please do not be disappointed if this is the case. We always endeavor to incorporate any meaningful changes from the community into the mailcow project.
|
||||
8. If you are planning larger and therefore more complex pull requests, it would be advisable to first announce this in a separate issue and then start implementing it after the idea has been accepted in order to avoid unnecessary frustration and effort!
|
||||
9. If your PR requires a Docker image rebuild (changes to Dockerfiles or files in data/Dockerfiles/), update the image tag in docker-compose.yml. Use the base-image versioning (e.g. ghcr.io/mailcow/sogo:5.12.4 → :5.12.5 for version bumps; append a letter for patch fixes, e.g. :5.12.4a). Follow this scheme.
|
||||
|
||||
---
|
||||
|
||||
## Issue Reporting
|
||||
**_Last modified on 15th August 2024_**
|
||||
**_Last modified on 12th November 2025_**
|
||||
|
||||
If you plan to report a issue within mailcow please read and understand the following rules:
|
||||
|
||||
### Security disclosures / Security-related fixes
|
||||
- Security vulnerabilities and security fixes must always be reported confidentially first to the contact address specified in SECURITY.md before they are integrated, published, or publicly disclosed in issues/PRs. Please wait for a response from the specified contact to ensure coordinated and responsible disclosure.
|
||||
|
||||
### Issue Reporting Guidelines
|
||||
|
||||
1. **ONLY** use the issue tracker for bug reports or improvement requests and NOT for support questions. For support questions you can either contact the [mailcow community on Telegram](https://docs.mailcow.email/#community-support-and-chat) or the mailcow team directly in exchange for a [support fee](https://docs.mailcow.email/#commercial-support).
|
||||
|
||||
@@ -23,10 +23,15 @@ A big thank you to everyone supporting us on GitHub Sponsors—your contribution
|
||||
<a href="https://www.maehdros.com/" target=_blank><img
|
||||
src="https://avatars.githubusercontent.com/u/173894712" height="58"
|
||||
/></a>
|
||||
<a href="https://garske-systems.de/" target=_blank><img
|
||||
src="https://garske-systems.de/img/logo_min.png" height="58"
|
||||
/></a>
|
||||
|
||||
### 50$/Month Sponsors
|
||||
<a href="https://github.com/vnukhr" target=_blank><img
|
||||
src="https://avatars.githubusercontent.com/u/7805987?s=52&v=4" height="58"
|
||||
/></a><a href="https://github.com/humiico" target=_blank><img
|
||||
src="https://avatars.githubusercontent.com/u/110134705?s=52&v=4" height="58"
|
||||
/></a>
|
||||
|
||||
## Info, documentation and support
|
||||
|
||||
+45
-39
@@ -17,7 +17,13 @@ caller="${BASH_SOURCE[1]##*/}"
|
||||
|
||||
get_installed_tools(){
|
||||
for bin in openssl curl docker git awk sha1sum grep cut jq; do
|
||||
if [[ -z $(command -v ${bin}) ]]; then echo "Cannot find ${bin}, exiting..."; exit 1; fi
|
||||
if [[ -z $(command -v ${bin}) ]]; then
|
||||
echo "Error: Cannot find command '${bin}'. Cannot proceed."
|
||||
echo "Solution: Please review system requirements and install requirements. Then, re-run the script."
|
||||
echo "See System Requirements: https://docs.mailcow.email/getstarted/install/"
|
||||
echo "Exiting..."
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
if grep --help 2>&1 | head -n 1 | grep -q -i "busybox"; then echo -e "${LIGHT_RED}BusyBox grep detected, please install gnu grep, \"apk add --no-cache --upgrade grep\"${NC}"; exit 1; fi
|
||||
@@ -32,45 +38,45 @@ get_docker_version(){
|
||||
}
|
||||
|
||||
get_compose_type(){
|
||||
if docker compose > /dev/null 2>&1; then
|
||||
if docker compose version --short | grep -e "^2." -e "^v2." > /dev/null 2>&1; then
|
||||
COMPOSE_VERSION=native
|
||||
COMPOSE_COMMAND="docker compose"
|
||||
if [[ "$caller" == "update.sh" ]]; then
|
||||
sed -i 's/^DOCKER_COMPOSE_VERSION=.*/DOCKER_COMPOSE_VERSION=native/' "$SCRIPT_DIR/mailcow.conf"
|
||||
fi
|
||||
echo -e "\e[33mFound Docker Compose Plugin (native).\e[0m"
|
||||
echo -e "\e[33mSetting the DOCKER_COMPOSE_VERSION Variable to native\e[0m"
|
||||
sleep 2
|
||||
echo -e "\e[33mNotice: You'll have to update this Compose Version via your Package Manager manually!\e[0m"
|
||||
else
|
||||
echo -e "\e[31mCannot find Docker Compose with a Version Higher than 2.X.X.\e[0m"
|
||||
echo -e "\e[31mPlease update/install it manually regarding to this doc site: https://docs.mailcow.email/install/\e[0m"
|
||||
exit 1
|
||||
fi
|
||||
elif docker-compose > /dev/null 2>&1; then
|
||||
if ! [[ $(alias docker-compose 2> /dev/null) ]] ; then
|
||||
if docker-compose version --short | grep "^2." > /dev/null 2>&1; then
|
||||
COMPOSE_VERSION=standalone
|
||||
COMPOSE_COMMAND="docker-compose"
|
||||
if [[ "$caller" == "update.sh" ]]; then
|
||||
sed -i 's/^DOCKER_COMPOSE_VERSION=.*/DOCKER_COMPOSE_VERSION=standalone/' "$SCRIPT_DIR/mailcow.conf"
|
||||
fi
|
||||
echo -e "\e[33mFound Docker Compose Standalone.\e[0m"
|
||||
echo -e "\e[33mSetting the DOCKER_COMPOSE_VERSION Variable to standalone\e[0m"
|
||||
sleep 2
|
||||
echo -e "\e[33mNotice: For an automatic update of docker-compose please use the update_compose.sh scripts located at the helper-scripts folder.\e[0m"
|
||||
else
|
||||
echo -e "\e[31mCannot find Docker Compose with a Version Higher than 2.X.X.\e[0m"
|
||||
echo -e "\e[31mPlease update/install manually regarding to this doc site: https://docs.mailcow.email/install/\e[0m"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
if docker compose > /dev/null 2>&1; then
|
||||
if docker compose version --short | grep -e "^[2-9]\." -e "^v[2-9]\." -e "^[1-9][0-9]\." -e "^v[1-9][0-9]\." > /dev/null 2>&1; then
|
||||
COMPOSE_VERSION=native
|
||||
COMPOSE_COMMAND="docker compose"
|
||||
if [[ "$caller" == "update.sh" ]]; then
|
||||
sed -i 's/^DOCKER_COMPOSE_VERSION=.*/DOCKER_COMPOSE_VERSION=native/' "$SCRIPT_DIR/mailcow.conf"
|
||||
fi
|
||||
echo -e "\e[33mFound Docker Compose Plugin (native).\e[0m"
|
||||
echo -e "\e[33mSetting the DOCKER_COMPOSE_VERSION Variable to native\e[0m"
|
||||
sleep 2
|
||||
echo -e "\e[33mNotice: You'll have to update this Compose Version via your Package Manager manually!\e[0m"
|
||||
else
|
||||
echo -e "\e[31mCannot find Docker Compose.\e[0m"
|
||||
echo -e "\e[31mPlease install it regarding to this doc site: https://docs.mailcow.email/install/\e[0m"
|
||||
exit 1
|
||||
echo -e "\e[31mCannot find Docker Compose with a Version Higher than 2.X.X.\e[0m"
|
||||
echo -e "\e[31mPlease update/install it manually regarding to this doc site: https://docs.mailcow.email/install/\e[0m"
|
||||
exit 1
|
||||
fi
|
||||
elif docker-compose > /dev/null 2>&1; then
|
||||
if ! [[ $(alias docker-compose 2> /dev/null) ]] ; then
|
||||
if docker-compose version --short | grep -e "^[2-9]\." -e "^[1-9][0-9]\." > /dev/null 2>&1; then
|
||||
COMPOSE_VERSION=standalone
|
||||
COMPOSE_COMMAND="docker-compose"
|
||||
if [[ "$caller" == "update.sh" ]]; then
|
||||
sed -i 's/^DOCKER_COMPOSE_VERSION=.*/DOCKER_COMPOSE_VERSION=standalone/' "$SCRIPT_DIR/mailcow.conf"
|
||||
fi
|
||||
echo -e "\e[33mFound Docker Compose Standalone.\e[0m"
|
||||
echo -e "\e[33mSetting the DOCKER_COMPOSE_VERSION Variable to standalone\e[0m"
|
||||
sleep 2
|
||||
echo -e "\e[33mNotice: For an automatic update of docker-compose please use the update_compose.sh scripts located at the helper-scripts folder.\e[0m"
|
||||
else
|
||||
echo -e "\e[31mCannot find Docker Compose with a Version Higher than 2.X.X.\e[0m"
|
||||
echo -e "\e[31mPlease update/install manually regarding to this doc site: https://docs.mailcow.email/install/\e[0m"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
else
|
||||
echo -e "\e[31mCannot find Docker Compose.\e[0m"
|
||||
echo -e "\e[31mPlease install it regarding to this doc site: https://docs.mailcow.email/install/\e[0m"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
detect_bad_asn() {
|
||||
@@ -221,4 +227,4 @@ detect_major_update() {
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,6 +155,7 @@ docker_daemon_edit(){
|
||||
fi
|
||||
|
||||
if [[ $ans =~ ^[Yy]$ ]]; then
|
||||
mkdir -p "$(dirname "$DOCKER_DAEMON_CONFIG")"
|
||||
if [[ -n "$DOCKER_MAJOR" && "$DOCKER_MAJOR" -lt 27 ]]; then
|
||||
cat > "$DOCKER_DAEMON_CONFIG" <<EOF
|
||||
{
|
||||
|
||||
@@ -43,6 +43,7 @@ adapt_new_options() {
|
||||
"ALLOW_ADMIN_EMAIL_LOGIN"
|
||||
"SKIP_HTTP_VERIFICATION"
|
||||
"SOGO_EXPIRE_SESSION"
|
||||
"SOGO_URL_ENCRYPTION_KEY"
|
||||
"REDIS_PORT"
|
||||
"REDISPASS"
|
||||
"DOVECOT_MASTER_USER"
|
||||
@@ -56,11 +57,14 @@ adapt_new_options() {
|
||||
"DISABLE_NETFILTER_ISOLATION_RULE"
|
||||
"HTTP_REDIRECT"
|
||||
"ENABLE_IPV6"
|
||||
"ACME_DNS_CHALLENGE"
|
||||
"ACME_DNS_PROVIDER"
|
||||
"ACME_ACCOUNT_EMAIL"
|
||||
)
|
||||
|
||||
sed -i --follow-symlinks '$a\' mailcow.conf
|
||||
for option in ${CONFIG_ARRAY[@]}; do
|
||||
if grep -q "${option}" mailcow.conf; then
|
||||
if grep -q "^#\?${option}=" mailcow.conf; then
|
||||
continue
|
||||
fi
|
||||
|
||||
@@ -94,7 +98,6 @@ adapt_new_options() {
|
||||
echo '# Max log lines per service to keep in Redis logs' >> mailcow.conf
|
||||
echo "LOG_LINES=9999" >> mailcow.conf
|
||||
;;
|
||||
|
||||
IPV4_NETWORK)
|
||||
echo '# Internal IPv4 /24 subnet, format n.n.n. (expands to n.n.n.0/24)' >> mailcow.conf
|
||||
echo "IPV4_NETWORK=172.22.1" >> mailcow.conf
|
||||
@@ -276,21 +279,36 @@ adapt_new_options() {
|
||||
echo '# A COMPLETE DOCKER STACK REBUILD (compose down && compose up -d) IS NEEDED TO APPLY THIS.' >> mailcow.conf
|
||||
echo ENABLE_IPV6=${IPV6_BOOL} >> mailcow.conf
|
||||
;;
|
||||
|
||||
SKIP_CLAMD)
|
||||
echo '# Skip ClamAV (clamd-mailcow) anti-virus (Rspamd will auto-detect a missing ClamAV container) - y/n' >> mailcow.conf
|
||||
echo 'SKIP_CLAMD=n' >> mailcow.conf
|
||||
;;
|
||||
|
||||
SKIP_OLEFY)
|
||||
echo '# Skip Olefy (olefy-mailcow) anti-virus for Office documents (Rspamd will auto-detect a missing Olefy container) - y/n' >> mailcow.conf
|
||||
echo 'SKIP_OLEFY=n' >> mailcow.conf
|
||||
;;
|
||||
|
||||
REDISPASS)
|
||||
echo "REDISPASS=$(LC_ALL=C </dev/urandom tr -dc A-Za-z0-9 2>/dev/null | head -c 28)" >> mailcow.conf
|
||||
;;
|
||||
|
||||
SOGO_URL_ENCRYPTION_KEY)
|
||||
echo '# SOGo URL encryption key (exactly 16 characters, limited to A–Z, a–z, 0–9)' >> mailcow.conf
|
||||
echo '# This key is used to encrypt email addresses within SOGo URLs' >> mailcow.conf
|
||||
echo "SOGO_URL_ENCRYPTION_KEY=$(LC_ALL=C </dev/urandom tr -dc A-Za-z0-9 2>/dev/null | head -c 16)" >> mailcow.conf
|
||||
;;
|
||||
ACME_DNS_CHALLENGE)
|
||||
echo '# Enable DNS-01 challenge for ACME (acme-mailcow) - y/n' >> mailcow.conf
|
||||
echo '# This requires you to set ACME_DNS_PROVIDER and ACME_ACCOUNT_EMAIL below' >> mailcow.conf
|
||||
echo 'ACME_DNS_CHALLENGE=n' >> mailcow.conf
|
||||
;;
|
||||
ACME_DNS_PROVIDER)
|
||||
echo '# DNS provider for DNS-01 challenge (e.g. dns_cf, dns_azure, dns_gd, etc.)' >> mailcow.conf
|
||||
echo '# See the dns-01 provider documentation for more information.' >> mailcow.conf
|
||||
echo 'ACME_DNS_PROVIDER=dns_xxx' >> mailcow.conf
|
||||
;;
|
||||
ACME_ACCOUNT_EMAIL)
|
||||
echo '# Account email for ACME DNS-01 challenge registration' >> mailcow.conf
|
||||
echo 'ACME_ACCOUNT_EMAIL=me@example.com' >> mailcow.conf
|
||||
;;
|
||||
*)
|
||||
echo "${option}=" >> mailcow.conf
|
||||
;;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM alpine:3.21
|
||||
FROM alpine:3.23
|
||||
|
||||
LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
|
||||
|
||||
@@ -14,11 +14,22 @@ RUN apk upgrade --no-cache \
|
||||
tini \
|
||||
tzdata \
|
||||
python3 \
|
||||
acme-tiny
|
||||
acme-tiny \
|
||||
git \
|
||||
socat \
|
||||
&& git clone --depth 1 https://github.com/acmesh-official/acme.sh.git /opt/acme.sh \
|
||||
&& chmod +x /opt/acme.sh/acme.sh \
|
||||
&& mkdir -p /var/lib/acme/acme-sh
|
||||
|
||||
ENV ACME_SH_BIN=/opt/acme.sh/acme.sh \
|
||||
ACME_SH_HOME=/opt/acme.sh \
|
||||
ACME_SH_CONFIG_HOME=/var/lib/acme/acme-sh
|
||||
|
||||
COPY acme.sh /srv/acme.sh
|
||||
COPY functions.sh /srv/functions.sh
|
||||
COPY obtain-certificate.sh /srv/obtain-certificate.sh
|
||||
COPY obtain-certificate-dns.sh /srv/obtain-certificate-dns.sh
|
||||
COPY load-dns-config.sh /srv/load-dns-config.sh
|
||||
COPY reload-configurations.sh /srv/reload-configurations.sh
|
||||
COPY expand6.sh /srv/expand6.sh
|
||||
|
||||
|
||||
@@ -14,6 +14,17 @@ until [[ $(${REDIS_CMDLINE} PING) == "PONG" ]]; do
|
||||
sleep 2
|
||||
done
|
||||
|
||||
# Create DNS-01 configuration template if it doesn't exist
|
||||
if [[ ! -f /etc/acme/dns-01.conf ]]; then
|
||||
mkdir -p /etc/acme
|
||||
cat > /etc/acme/dns-01.conf <<'EOF'
|
||||
# Add here your DNS-01 challenge configuration
|
||||
# For more information, visit the acme.sh documentation:
|
||||
# https://github.com/acmesh-official/acme.sh/wiki/dnsapi
|
||||
EOF
|
||||
echo "Created DNS-01 configuration template at /etc/acme/dns-01.conf"
|
||||
fi
|
||||
|
||||
source /srv/functions.sh
|
||||
# Thanks to https://github.com/cvmiller -> https://github.com/cvmiller/expand6
|
||||
source /srv/expand6.sh
|
||||
@@ -42,6 +53,10 @@ if [[ "${ENABLE_SSL_SNI}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
|
||||
ENABLE_SSL_SNI=y
|
||||
fi
|
||||
|
||||
if [[ "${ACME_DNS_CHALLENGE}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
|
||||
ACME_DNS_CHALLENGE=y
|
||||
fi
|
||||
|
||||
if [[ "${SKIP_LETS_ENCRYPT}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
|
||||
log_f "SKIP_LETS_ENCRYPT=y, skipping Let's Encrypt..."
|
||||
sleep 365d
|
||||
@@ -234,18 +249,71 @@ while true; do
|
||||
done <<< "${SQL_DOMAINS}"
|
||||
|
||||
if [[ ${ONLY_MAILCOW_HOSTNAME} != "y" ]]; then
|
||||
# Fetch all domains with an active MTA-STS policy once.
|
||||
unset MTA_STS_ACTIVE_DOMAINS
|
||||
declare -A MTA_STS_ACTIVE_DOMAINS
|
||||
if [[ ${AUTODISCOVER_SAN} == "y" ]]; then
|
||||
SQL_MTA_STS_DOMAINS=$(mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT domain FROM mta_sts WHERE active = 1" -Bs)
|
||||
if [[ $? -eq 0 ]]; then
|
||||
while read mta_sts_domain; do
|
||||
if [[ -z "${mta_sts_domain}" ]]; then
|
||||
# ignore empty lines
|
||||
continue
|
||||
fi
|
||||
MTA_STS_ACTIVE_DOMAINS["${mta_sts_domain}"]=1
|
||||
done <<< "${SQL_MTA_STS_DOMAINS}"
|
||||
fi
|
||||
fi
|
||||
for SQL_DOMAIN in "${SQL_DOMAIN_ARR[@]}"; do
|
||||
unset VALIDATED_CONFIG_DOMAINS_SUBDOMAINS
|
||||
declare -a VALIDATED_CONFIG_DOMAINS_SUBDOMAINS
|
||||
for SUBDOMAIN in "${ADDITIONAL_WC_ARR[@]}"; do
|
||||
if [[ "${SUBDOMAIN}.${SQL_DOMAIN}" != "${MAILCOW_HOSTNAME}" ]]; then
|
||||
if check_domain "${SUBDOMAIN}.${SQL_DOMAIN}"; then
|
||||
VALIDATED_CONFIG_DOMAINS_SUBDOMAINS+=("${SUBDOMAIN}.${SQL_DOMAIN}")
|
||||
fi
|
||||
FULL_SUBDOMAIN="${SUBDOMAIN}.${SQL_DOMAIN}"
|
||||
|
||||
# Skip mta-sts subdomain unless MTA-STS is enabled (active) for this domain
|
||||
if [[ "${SUBDOMAIN}" == "mta-sts" && -z "${MTA_STS_ACTIVE_DOMAINS[${SQL_DOMAIN}]}" ]]; then
|
||||
log_f "MTA-STS is not enabled for ${SQL_DOMAIN} - skipping mta-sts subdomain certificate"
|
||||
continue
|
||||
fi
|
||||
|
||||
# Skip if subdomain matches MAILCOW_HOSTNAME
|
||||
if [[ "${FULL_SUBDOMAIN}" == "${MAILCOW_HOSTNAME}" ]]; then
|
||||
continue
|
||||
fi
|
||||
# Skip if subdomain is covered by a wildcard in ADDITIONAL_SAN
|
||||
if is_covered_by_wildcard "${FULL_SUBDOMAIN}"; then
|
||||
log_f "Subdomain '${FULL_SUBDOMAIN}' is covered by wildcard - skipping explicit subdomain"
|
||||
continue
|
||||
fi
|
||||
# Validate and add subdomain
|
||||
if check_domain "${FULL_SUBDOMAIN}"; then
|
||||
VALIDATED_CONFIG_DOMAINS_SUBDOMAINS+=("${FULL_SUBDOMAIN}")
|
||||
fi
|
||||
done
|
||||
VALIDATED_CONFIG_DOMAINS+=("${VALIDATED_CONFIG_DOMAINS_SUBDOMAINS[*]}")
|
||||
done
|
||||
|
||||
# Fetch alias domains where target domain has MTA-STS enabled
|
||||
if [[ ${AUTODISCOVER_SAN} == "y" ]]; then
|
||||
SQL_ALIAS_DOMAINS=$(mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT ad.alias_domain FROM alias_domain ad INNER JOIN mta_sts m ON ad.target_domain = m.domain WHERE ad.active = 1 AND m.active = 1" -Bs)
|
||||
if [[ $? -eq 0 ]]; then
|
||||
while read alias_domain; do
|
||||
if [[ -z "${alias_domain}" ]]; then
|
||||
# ignore empty lines
|
||||
continue
|
||||
fi
|
||||
# Only add mta-sts subdomain for alias domains
|
||||
if [[ "mta-sts.${alias_domain}" != "${MAILCOW_HOSTNAME}" ]]; then
|
||||
# Skip if mta-sts subdomain is covered by a wildcard
|
||||
if is_covered_by_wildcard "mta-sts.${alias_domain}"; then
|
||||
log_f "Alias domain mta-sts subdomain 'mta-sts.${alias_domain}' is covered by wildcard - skipping"
|
||||
elif check_domain "mta-sts.${alias_domain}"; then
|
||||
VALIDATED_CONFIG_DOMAINS+=("mta-sts.${alias_domain}")
|
||||
fi
|
||||
fi
|
||||
done <<< "${SQL_ALIAS_DOMAINS}"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
if check_domain ${MAILCOW_HOSTNAME}; then
|
||||
@@ -274,13 +342,31 @@ while true; do
|
||||
done
|
||||
fi
|
||||
|
||||
# Check if MAILCOW_HOSTNAME is covered by a wildcard in ADDITIONAL_SAN
|
||||
MAILCOW_HOSTNAME_COVERED=0
|
||||
if [[ ! -z ${VALIDATED_MAILCOW_HOSTNAME} ]]; then
|
||||
if is_covered_by_wildcard "${VALIDATED_MAILCOW_HOSTNAME}"; then
|
||||
MAILCOW_PARENT_DOMAIN=$(echo ${VALIDATED_MAILCOW_HOSTNAME} | cut -d. -f2-)
|
||||
log_f "MAILCOW_HOSTNAME '${VALIDATED_MAILCOW_HOSTNAME}' is covered by wildcard '*.${MAILCOW_PARENT_DOMAIN}' - skipping explicit hostname"
|
||||
MAILCOW_HOSTNAME_COVERED=1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Unique domains for server certificate
|
||||
if [[ ${ENABLE_SSL_SNI} == "y" ]]; then
|
||||
# create certificate for server name and fqdn SANs only
|
||||
SERVER_SAN_VALIDATED=(${VALIDATED_MAILCOW_HOSTNAME} $(echo ${ADDITIONAL_VALIDATED_SAN[*]} | xargs -n1 | sort -u | xargs))
|
||||
if [[ ${MAILCOW_HOSTNAME_COVERED} == "1" ]]; then
|
||||
SERVER_SAN_VALIDATED=($(echo ${ADDITIONAL_VALIDATED_SAN[*]} | xargs -n1 | sort -u | xargs))
|
||||
else
|
||||
SERVER_SAN_VALIDATED=(${VALIDATED_MAILCOW_HOSTNAME} $(echo ${ADDITIONAL_VALIDATED_SAN[*]} | xargs -n1 | sort -u | xargs))
|
||||
fi
|
||||
else
|
||||
# create certificate for all domains, including all subdomains from other domains [*]
|
||||
SERVER_SAN_VALIDATED=(${VALIDATED_MAILCOW_HOSTNAME} $(echo ${VALIDATED_CONFIG_DOMAINS[*]} ${ADDITIONAL_VALIDATED_SAN[*]} | xargs -n1 | sort -u | xargs))
|
||||
if [[ ${MAILCOW_HOSTNAME_COVERED} == "1" ]]; then
|
||||
SERVER_SAN_VALIDATED=($(echo ${VALIDATED_CONFIG_DOMAINS[*]} ${ADDITIONAL_VALIDATED_SAN[*]} | xargs -n1 | sort -u | xargs))
|
||||
else
|
||||
SERVER_SAN_VALIDATED=(${VALIDATED_MAILCOW_HOSTNAME} $(echo ${VALIDATED_CONFIG_DOMAINS[*]} ${ADDITIONAL_VALIDATED_SAN[*]} | xargs -n1 | sort -u | xargs))
|
||||
fi
|
||||
fi
|
||||
if [[ ! -z ${SERVER_SAN_VALIDATED[*]} ]]; then
|
||||
CERT_NAME=${SERVER_SAN_VALIDATED[0]}
|
||||
|
||||
@@ -80,6 +80,11 @@ check_domain(){
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ ${ACME_DNS_CHALLENGE} == "y" ]]; then
|
||||
log_f "ACME_DNS_CHALLENGE=y - skipping IP and HTTP validation for ${DOMAIN}"
|
||||
return 0
|
||||
fi
|
||||
# Check if CNAME without v6 enabled target
|
||||
if [[ ! -z ${AAAA_DOMAIN} ]] && [[ -z $(echo ${AAAA_DOMAIN} | grep "^\([0-9a-fA-F]\{0,4\}:\)\{1,7\}[0-9a-fA-F]\{0,4\}$") ]]; then
|
||||
AAAA_DOMAIN=
|
||||
@@ -130,3 +135,32 @@ verify_challenge_path(){
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Check if a domain is covered by a wildcard (*.example.com) in ADDITIONAL_SAN
|
||||
# Usage: is_covered_by_wildcard "subdomain.example.com"
|
||||
# Returns: 0 if covered, 1 if not covered
|
||||
# Note: Only returns 0 (covered) when DNS-01 challenge is enabled,
|
||||
# as wildcards cannot be validated with HTTP-01 challenge
|
||||
is_covered_by_wildcard() {
|
||||
local DOMAIN=$1
|
||||
|
||||
# Only skip if DNS challenge is enabled (wildcards require DNS-01)
|
||||
if [[ ${ACME_DNS_CHALLENGE} != "y" ]]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Return early if no ADDITIONAL_SAN is set
|
||||
if [[ -z ${ADDITIONAL_SAN} ]]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Extract parent domain (e.g., mail.example.com -> example.com)
|
||||
local PARENT_DOMAIN=$(echo ${DOMAIN} | cut -d. -f2-)
|
||||
|
||||
# Check if ADDITIONAL_SAN contains a wildcard for this parent domain
|
||||
if [[ "${ADDITIONAL_SAN}" == *"*.${PARENT_DOMAIN}"* ]]; then
|
||||
return 0 # Covered by wildcard
|
||||
fi
|
||||
|
||||
return 1 # Not covered
|
||||
}
|
||||
|
||||
Executable
+57
@@ -0,0 +1,57 @@
|
||||
#!/bin/bash
|
||||
|
||||
SCRIPT_SOURCE="${BASH_SOURCE[0]:-${0}}"
|
||||
if [[ "${SCRIPT_SOURCE}" == "${0}" ]]; then
|
||||
__dns_loader_standalone=1
|
||||
else
|
||||
__dns_loader_standalone=0
|
||||
fi
|
||||
|
||||
CONFIG_PATH="${ACME_DNS_CONFIG_FILE:-/etc/acme/dns-01.conf}"
|
||||
|
||||
if [[ ! -f "${CONFIG_PATH}" ]]; then
|
||||
if [[ $__dns_loader_standalone -eq 1 ]]; then
|
||||
exit 0
|
||||
else
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
source /srv/functions.sh
|
||||
|
||||
log_f "Loading DNS-01 configuration from ${CONFIG_PATH}"
|
||||
|
||||
LINE_NO=0
|
||||
while IFS= read -r line || [[ -n "${line}" ]]; do
|
||||
LINE_NO=$((LINE_NO+1))
|
||||
line="${line%$'\r'}"
|
||||
line_trimmed="$(printf '%s' "${line}" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')"
|
||||
[[ -z "${line_trimmed}" ]] && continue
|
||||
[[ "${line_trimmed:0:1}" == "#" ]] && continue
|
||||
if [[ "${line_trimmed}" != *=* ]]; then
|
||||
log_f "Skipping invalid DNS config line ${LINE_NO} (missing key=value)"
|
||||
continue
|
||||
fi
|
||||
KEY="${line_trimmed%%=*}"
|
||||
VALUE="${line_trimmed#*=}"
|
||||
KEY="$(printf '%s' "${KEY}" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')"
|
||||
VALUE="$(printf '%s' "${VALUE}" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')"
|
||||
if [[ -z "${KEY}" ]]; then
|
||||
log_f "Skipping invalid DNS config line ${LINE_NO} (empty key)"
|
||||
continue
|
||||
fi
|
||||
if [[ "${VALUE}" =~ ^\".*\"$ ]]; then
|
||||
VALUE="${VALUE:1:-1}"
|
||||
elif [[ "${VALUE}" =~ ^\'.*\'$ ]]; then
|
||||
VALUE="${VALUE:1:-1}"
|
||||
fi
|
||||
export "${KEY}"="${VALUE}"
|
||||
log_f "Exported DNS config key ${KEY}"
|
||||
|
||||
done < "${CONFIG_PATH}"
|
||||
|
||||
if [[ $__dns_loader_standalone -eq 1 ]]; then
|
||||
exit 0
|
||||
else
|
||||
return 0
|
||||
fi
|
||||
@@ -0,0 +1,177 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Return values / exit codes
|
||||
# 0 = cert created successfully
|
||||
# 1 = cert renewed successfully
|
||||
# 2 = cert not due for renewal
|
||||
# * = errors
|
||||
|
||||
source /srv/functions.sh
|
||||
|
||||
CERT_DOMAINS=(${DOMAINS[@]})
|
||||
CERT_DOMAIN=${CERT_DOMAINS[0]}
|
||||
ACME_BASE=/var/lib/acme
|
||||
|
||||
# Load optional DNS provider secrets from /etc/acme/dns-01.conf
|
||||
if [[ -f /srv/load-dns-config.sh ]]; then
|
||||
source /srv/load-dns-config.sh
|
||||
if declare -F log_f >/dev/null; then
|
||||
log_f "ACME_DNS_CHALLENGE is enabled, DNS provider secrets loaded"
|
||||
fi
|
||||
fi
|
||||
|
||||
TYPE=${1}
|
||||
PREFIX=""
|
||||
# only support rsa certificates for now
|
||||
if [[ "${TYPE}" != "rsa" ]]; then
|
||||
log_f "Unknown certificate type '${TYPE}' requested"
|
||||
exit 5
|
||||
fi
|
||||
|
||||
if [[ -z "${ACME_DNS_PROVIDER}" ]]; then
|
||||
log_f "ACME_DNS_PROVIDER is required when ACME_DNS_CHALLENGE is enabled"
|
||||
exit 6
|
||||
fi
|
||||
|
||||
DOMAINS_FILE=${ACME_BASE}/${CERT_DOMAIN}/domains
|
||||
CERT=${ACME_BASE}/${CERT_DOMAIN}/${PREFIX}cert.pem
|
||||
SHARED_KEY=${ACME_BASE}/acme/${PREFIX}key.pem # must already exist
|
||||
KEY=${ACME_BASE}/${CERT_DOMAIN}/${PREFIX}key.pem
|
||||
CSR=${ACME_BASE}/${CERT_DOMAIN}/${PREFIX}acme.csr
|
||||
|
||||
if [[ -z ${CERT_DOMAINS[*]} ]]; then
|
||||
log_f "Missing CERT_DOMAINS to obtain a certificate"
|
||||
exit 3
|
||||
fi
|
||||
|
||||
if [[ "${LE_STAGING}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
|
||||
if [[ ! -z "${DIRECTORY_URL}" ]]; then
|
||||
log_f "Cannot use DIRECTORY_URL with LE_STAGING=y - ignoring DIRECTORY_URL"
|
||||
fi
|
||||
log_f "Using Let's Encrypt staging servers"
|
||||
ACME_SH_SERVER_ARGS=("--staging")
|
||||
elif [[ ! -z "${DIRECTORY_URL}" ]]; then
|
||||
log_f "Using custom directory URL ${DIRECTORY_URL}"
|
||||
ACME_SH_SERVER_ARGS=("--server" "${DIRECTORY_URL}")
|
||||
else
|
||||
log_f "Using Let's Encrypt production servers"
|
||||
ACME_SH_SERVER_ARGS=("--server" "letsencrypt")
|
||||
fi
|
||||
|
||||
if [[ -f ${DOMAINS_FILE} && "$(cat ${DOMAINS_FILE})" == "${CERT_DOMAINS[*]}" ]]; then
|
||||
if [[ ! -f ${CERT} || ! -f "${KEY}" || -f "${ACME_BASE}/force_renew" ]]; then
|
||||
log_f "Certificate ${CERT} doesn't exist yet or forced renewal - start obtaining"
|
||||
elif ! openssl x509 -checkend 2592000 -noout -in ${CERT} > /dev/null; then
|
||||
log_f "Certificate ${CERT} is due for renewal (< 30 days) - start renewing"
|
||||
else
|
||||
log_f "Certificate ${CERT} validation done, neither changed nor due for renewal."
|
||||
exit 2
|
||||
fi
|
||||
else
|
||||
log_f "Certificate ${CERT} missing or changed domains '${CERT_DOMAINS[*]}' - start obtaining"
|
||||
fi
|
||||
|
||||
# Make backup
|
||||
if [[ -f ${CERT} ]]; then
|
||||
DATE=$(date +%Y-%m-%d_%H_%M_%S)
|
||||
BACKUP_DIR=${ACME_BASE}/backups/${CERT_DOMAIN}/${PREFIX}${DATE}
|
||||
log_f "Creating backups in ${BACKUP_DIR} ..."
|
||||
mkdir -p ${BACKUP_DIR}/
|
||||
[[ -f ${DOMAINS_FILE} ]] && cp ${DOMAINS_FILE} ${BACKUP_DIR}/
|
||||
[[ -f ${CERT} ]] && cp ${CERT} ${BACKUP_DIR}/
|
||||
[[ -f ${KEY} ]] && cp ${KEY} ${BACKUP_DIR}/
|
||||
[[ -f ${CSR} ]] && cp ${CSR} ${BACKUP_DIR}/
|
||||
fi
|
||||
|
||||
mkdir -p ${ACME_BASE}/${CERT_DOMAIN}
|
||||
if [[ ! -f ${KEY} ]]; then
|
||||
log_f "Copying shared private key for this certificate..."
|
||||
cp ${SHARED_KEY} ${KEY}
|
||||
chmod 600 ${KEY}
|
||||
fi
|
||||
|
||||
# Generating CSR to keep layout parity with HTTP challenge flow
|
||||
printf "[SAN]\nsubjectAltName=" > /tmp/_SAN
|
||||
printf "DNS:%s," "${CERT_DOMAINS[@]}" >> /tmp/_SAN
|
||||
sed -i '$s/,$//' /tmp/_SAN
|
||||
openssl req -new -sha256 -key ${KEY} -subj "/" -reqexts SAN -config <(cat "$(openssl version -d | sed 's/.*\"\(.*\)\"/\1/g')/openssl.cnf" /tmp/_SAN) > ${CSR}
|
||||
|
||||
log_f "Checking resolver..."
|
||||
until dig letsencrypt.org +time=3 +tries=1 @unbound > /dev/null; do
|
||||
sleep 2
|
||||
done
|
||||
log_f "Resolver OK"
|
||||
|
||||
ACME_SH_BIN_PATH=${ACME_SH_BIN:-/opt/acme.sh/acme.sh}
|
||||
ACME_SH_WORK_HOME=${ACME_SH_CONFIG_HOME:-/var/lib/acme/acme-sh}
|
||||
mkdir -p ${ACME_SH_WORK_HOME}
|
||||
|
||||
if [[ ! -x ${ACME_SH_BIN_PATH} ]]; then
|
||||
log_f "acme.sh binary not found at ${ACME_SH_BIN_PATH}"
|
||||
exit 7
|
||||
fi
|
||||
|
||||
if [[ ! -f ${ACME_SH_WORK_HOME}/account.conf ]]; then
|
||||
if [[ -z "${ACME_ACCOUNT_EMAIL}" ]]; then
|
||||
log_f "ACME_ACCOUNT_EMAIL is required to register a new acme.sh account"
|
||||
exit 8
|
||||
fi
|
||||
log_f "Registering acme.sh account for ${ACME_ACCOUNT_EMAIL}"
|
||||
REGISTER_CMD=("${ACME_SH_BIN_PATH}" "--home" "${ACME_SH_WORK_HOME}" "--config-home" "${ACME_SH_WORK_HOME}" "--cert-home" "${ACME_SH_WORK_HOME}" "--register-account" "-m" "${ACME_ACCOUNT_EMAIL}")
|
||||
REGISTER_CMD+=("${ACME_SH_SERVER_ARGS[@]}")
|
||||
REGISTER_RESPONSE=$("${REGISTER_CMD[@]}" 2>&1)
|
||||
if [[ $? -ne 0 ]]; then
|
||||
log_f "Failed to register acme.sh account: ${REGISTER_RESPONSE}"
|
||||
exit 9
|
||||
fi
|
||||
fi
|
||||
|
||||
TMP_CERT=$(mktemp /tmp/acme-cert.XXXXXX)
|
||||
TMP_FULLCHAIN=$(mktemp /tmp/acme-fullchain.XXXXXX)
|
||||
|
||||
ACME_CMD=("${ACME_SH_BIN_PATH}" "--home" "${ACME_SH_WORK_HOME}" "--config-home" "${ACME_SH_WORK_HOME}" "--cert-home" "${ACME_SH_WORK_HOME}")
|
||||
ACME_CMD+=("${ACME_SH_SERVER_ARGS[@]}")
|
||||
ACME_CMD+=("--issue" "--dns" "${ACME_DNS_PROVIDER}" "--key-file" "${KEY}" "--cert-file" "${TMP_CERT}" "--fullchain-file" "${TMP_FULLCHAIN}" "--force")
|
||||
for domain in "${CERT_DOMAINS[@]}"; do
|
||||
ACME_CMD+=("-d" "${domain}")
|
||||
done
|
||||
|
||||
log_f "Using command ${ACME_CMD[*]}"
|
||||
if [[ -n "${ACME_DNS_PROVIDER}" ]]; then
|
||||
log_f "DNS provider: ${ACME_DNS_PROVIDER}"
|
||||
fi
|
||||
if compgen -A variable | grep -Eq "^DNS_|^ACME_"; then
|
||||
LOG_KEYS=$(env | grep -E "^(DNS_|ACME_)" | cut -d= -f1 | tr '\n' ' ')
|
||||
log_f "Available DNS/ACME env keys: ${LOG_KEYS}" redis_only
|
||||
fi
|
||||
ACME_RESPONSE=$("${ACME_CMD[@]}" 2>&1 | tee /dev/fd/5; exit ${PIPESTATUS[0]})
|
||||
SUCCESS="$?"
|
||||
ACME_RESPONSE_B64=$(echo "${ACME_RESPONSE}" | openssl enc -e -A -base64)
|
||||
log_f "${ACME_RESPONSE_B64}" redis_only b64
|
||||
|
||||
case "$SUCCESS" in
|
||||
0)
|
||||
log_f "Deploying certificate ${CERT}..."
|
||||
if verify_hash_match ${TMP_FULLCHAIN} ${KEY}; then
|
||||
RETURN=0
|
||||
if [[ -f ${CERT} ]]; then
|
||||
RETURN=1
|
||||
fi
|
||||
mv -f ${TMP_FULLCHAIN} ${CERT}
|
||||
rm -f ${TMP_CERT}
|
||||
echo -n ${CERT_DOMAINS[*]} > ${DOMAINS_FILE}
|
||||
log_f "Certificate successfully obtained via DNS challenge"
|
||||
exit ${RETURN}
|
||||
else
|
||||
log_f "Certificate was requested, but key and certificate hashes do not match"
|
||||
rm -f ${TMP_CERT} ${TMP_FULLCHAIN}
|
||||
exit 4
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
log_f "Failed to obtain certificate ${CERT} for domains '${CERT_DOMAINS[*]}' via DNS challenge"
|
||||
redis-cli -h redis -a ${REDISPASS} --no-auth-warning SET ACME_FAIL_TIME "$(date +%s)"
|
||||
rm -f ${TMP_CERT} ${TMP_FULLCHAIN}
|
||||
exit 100${SUCCESS}
|
||||
;;
|
||||
esac
|
||||
@@ -20,6 +20,10 @@ if [[ "${TYPE}" != "rsa" ]]; then
|
||||
log_f "Unknown certificate type '${TYPE}' requested"
|
||||
exit 5
|
||||
fi
|
||||
|
||||
if [[ "${ACME_DNS_CHALLENGE}" == "y" ]]; then
|
||||
exec /srv/obtain-certificate-dns.sh "$@"
|
||||
fi
|
||||
DOMAINS_FILE=${ACME_BASE}/${CERT_DOMAIN}/domains
|
||||
CERT=${ACME_BASE}/${CERT_DOMAIN}/${PREFIX}cert.pem
|
||||
SHARED_KEY=${ACME_BASE}/acme/${PREFIX}key.pem # must already exist
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
FROM debian:bookworm-slim
|
||||
FROM debian:trixie-slim
|
||||
|
||||
RUN apt update && apt install pigz -y --no-install-recommends
|
||||
RUN apt update && apt install pigz zstd -y --no-install-recommends
|
||||
@@ -1,7 +1,7 @@
|
||||
FROM alpine:3.21 AS builder
|
||||
FROM alpine:3.24 AS builder
|
||||
|
||||
WORKDIR /src
|
||||
ENV CLAMD_VERSION=1.4.2
|
||||
ENV CLAMD_VERSION=1.4.6
|
||||
|
||||
RUN apk upgrade --no-cache \
|
||||
&& apk add --update --no-cache \
|
||||
@@ -68,7 +68,7 @@ RUN wget -P /src https://www.clamav.net/downloads/production/clamav-${CLAMD_VERS
|
||||
"/clamav/etc/clamav/clamav-milter.conf.sample" > "/clamav/etc/clamav/clamav-milter.conf" || exit 1
|
||||
|
||||
|
||||
FROM alpine:3.21
|
||||
FROM alpine:3.24
|
||||
|
||||
LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM alpine:3.21
|
||||
FROM alpine:3.23
|
||||
|
||||
LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
|
||||
|
||||
|
||||
@@ -110,12 +110,12 @@ async def get_container(container_id : str):
|
||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||
|
||||
@app.get("/containers/json")
|
||||
async def get_containers():
|
||||
async def get_containers(all: bool = False):
|
||||
global dockerapi
|
||||
|
||||
containers = {}
|
||||
try:
|
||||
for container in (await dockerapi.async_docker_client.containers.list()):
|
||||
for container in (await dockerapi.async_docker_client.containers.list(all=all)):
|
||||
container_info = await container.show()
|
||||
containers.update({container_info['Id']: container_info})
|
||||
return Response(content=json.dumps(containers, indent=4), media_type="application/json")
|
||||
|
||||
@@ -3,7 +3,7 @@ FROM alpine:3.21
|
||||
LABEL maintainer="The Infrastructure Company GmbH <info@servercow.de>"
|
||||
|
||||
# renovate: datasource=github-releases depName=tianon/gosu versioning=semver-coerced extractVersion=^(?<version>.*)$
|
||||
ARG GOSU_VERSION=1.17
|
||||
ARG GOSU_VERSION=1.19
|
||||
|
||||
ENV LANG=C.UTF-8
|
||||
ENV LC_ALL=C.UTF-8
|
||||
|
||||
@@ -204,16 +204,17 @@ EOF
|
||||
# Create random master Password for SOGo SSO
|
||||
RAND_PASS=$(cat /dev/urandom | tr -dc 'a-z0-9' | fold -w 32 | head -n 1)
|
||||
echo -n ${RAND_PASS} > /etc/phpfpm/sogo-sso.pass
|
||||
# Creating additional creds file for SOGo notify crons (calendars, etc)
|
||||
echo -n ${RAND_USER}@mailcow.local:${RAND_PASS} > /etc/sogo/cron.creds
|
||||
cat <<EOF > /etc/dovecot/sogo-sso.conf
|
||||
# Autogenerated by mailcow
|
||||
passdb {
|
||||
driver = static
|
||||
args = allow_real_nets=${IPV4_NETWORK}.248/32 password={plain}${RAND_PASS}
|
||||
args = allow_nets=${IPV4_NETWORK}.248/32 password={plain}${RAND_PASS}
|
||||
}
|
||||
EOF
|
||||
|
||||
# Creating additional creds file for SOGo notify crons (calendars, etc) (dummy user, sso password)
|
||||
echo -n ${RAND_USER}@mailcow.local:${RAND_PASS} > /etc/sogo/cron.creds
|
||||
|
||||
if [[ "${MASTER}" =~ ^([nN][oO]|[nN])+$ ]]; then
|
||||
# Toggling MASTER will result in a rebuild of containers, so the quota script will be recreated
|
||||
cat <<'EOF' > /usr/local/bin/quota_notify.py
|
||||
|
||||
@@ -5,35 +5,13 @@ use LockFile::Simple qw(lock trylock unlock);
|
||||
use Proc::ProcessTable;
|
||||
use Data::Dumper qw(Dumper);
|
||||
use IPC::Run 'run';
|
||||
use JSON::PP;
|
||||
use File::Temp;
|
||||
use Try::Tiny;
|
||||
use POSIX ();
|
||||
use sigtrap 'handler' => \&sig_handler, qw(INT TERM KILL QUIT);
|
||||
|
||||
sub trim { my $s = shift; $s =~ s/^\s+|\s+$//g; return $s };
|
||||
my $t = Proc::ProcessTable->new;
|
||||
my $imapsync_running = grep { $_->{cmndline} =~ /imapsync\s/i } @{$t->table};
|
||||
if ($imapsync_running ge 1)
|
||||
{
|
||||
print "imapsync is active, exiting...";
|
||||
exit;
|
||||
}
|
||||
|
||||
sub qqw($) {
|
||||
my @params = ();
|
||||
my @values = split(/(?=--)/, $_[0]);
|
||||
foreach my $val (@values) {
|
||||
my @tmpparam = split(/ /, $val, 2);
|
||||
foreach my $tmpval (@tmpparam) {
|
||||
if ($tmpval ne '') {
|
||||
push @params, $tmpval;
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach my $val (@params) {
|
||||
$val=trim($val);
|
||||
}
|
||||
return @params;
|
||||
}
|
||||
|
||||
$run_dir="/tmp";
|
||||
$dsn = 'DBI:mysql:database=' . $ENV{'DBNAME'} . ';mysql_socket=/var/run/mysqld/mysqld.sock';
|
||||
@@ -46,56 +24,92 @@ $dbh = DBI->connect($dsn, $ENV{'DBUSER'}, $ENV{'DBPASS'}, {
|
||||
});
|
||||
$dbh->do("UPDATE imapsync SET is_running = 0");
|
||||
|
||||
# Max concurrent imapsync processes (admin-configurable, stored in MySQL). Fallback 1.
|
||||
my $max_parallel = 1;
|
||||
my ($mp) = $dbh->selectrow_array("SELECT value FROM imapsync_settings WHERE name = 'max_parallel'");
|
||||
$max_parallel = int($mp) if defined($mp) && int($mp) >= 1;
|
||||
|
||||
# Global per-process bandwidth cap in bytes/s (0 = off). Applied to every imapsync run below.
|
||||
my $global_bw = 0;
|
||||
my ($gbw) = $dbh->selectrow_array("SELECT value FROM imapsync_settings WHERE name = 'max_bytes_per_second'");
|
||||
$global_bw = int($gbw) if defined($gbw) && int($gbw) > 0;
|
||||
|
||||
sub sig_handler {
|
||||
# Send die to force exception in "run"
|
||||
die "sig_handler received signal, preparing to exit...\n";
|
||||
};
|
||||
|
||||
open my $file, '<', "/etc/sogo/sieve.creds";
|
||||
my $creds = <$file>;
|
||||
open my $file, '<', "/etc/sogo/sieve.creds";
|
||||
my $creds = <$file>;
|
||||
close $file;
|
||||
my ($master_user, $master_pass) = split /:/, $creds;
|
||||
my $sth = $dbh->prepare("SELECT id,
|
||||
user1,
|
||||
user2,
|
||||
host1,
|
||||
authmech1,
|
||||
password1,
|
||||
exclude,
|
||||
port1,
|
||||
enc1,
|
||||
delete2duplicates,
|
||||
maxage,
|
||||
subfolder2,
|
||||
delete1,
|
||||
delete2,
|
||||
automap,
|
||||
skipcrossduplicates,
|
||||
maxbytespersecond,
|
||||
custom_params,
|
||||
subscribeall,
|
||||
timeout1,
|
||||
timeout2,
|
||||
dry
|
||||
FROM imapsync
|
||||
WHERE active = 1
|
||||
AND is_running = 0
|
||||
# Source-based schema: join imapsync_source to read host/port/encryption/auth-type
|
||||
my $sth = $dbh->prepare("SELECT
|
||||
i.id,
|
||||
i.user1,
|
||||
i.user2,
|
||||
s.host1,
|
||||
s.auth_type,
|
||||
i.password1,
|
||||
i.exclude,
|
||||
s.port1,
|
||||
s.enc1,
|
||||
i.delete2duplicates,
|
||||
i.maxage,
|
||||
i.subfolder2,
|
||||
i.delete1,
|
||||
i.delete2,
|
||||
i.automap,
|
||||
i.skipcrossduplicates,
|
||||
i.maxbytespersecond,
|
||||
i.custom_params,
|
||||
i.subscribeall,
|
||||
i.timeout1,
|
||||
i.timeout2,
|
||||
i.dry,
|
||||
s.oauth_access_token,
|
||||
s.oauth_token_expires,
|
||||
s.oauth_flow,
|
||||
i.source_id
|
||||
FROM imapsync i JOIN imapsync_source s ON i.source_id = s.id
|
||||
WHERE i.active = 1
|
||||
AND s.active = 1
|
||||
AND i.is_running = 0
|
||||
AND (
|
||||
UNIX_TIMESTAMP(NOW()) - UNIX_TIMESTAMP(last_run) > mins_interval * 60
|
||||
UNIX_TIMESTAMP(NOW()) - UNIX_TIMESTAMP(i.last_run) > i.mins_interval * 60
|
||||
OR
|
||||
last_run IS NULL)
|
||||
ORDER BY last_run");
|
||||
i.last_run IS NULL)
|
||||
ORDER BY i.last_run ASC, i.prio DESC, i.id ASC
|
||||
LIMIT $max_parallel");
|
||||
|
||||
$sth->execute();
|
||||
my $row;
|
||||
my @rows = @{$sth->fetchall_arrayref()};
|
||||
$sth->finish();
|
||||
|
||||
while ($row = $sth->fetchrow_arrayref()) {
|
||||
# Only up to $max_parallel due jobs are pulled per run (LIMIT above)
|
||||
my $active = 0;
|
||||
foreach my $row (@rows) {
|
||||
# Throttle: once max children are running, reap one before starting the next
|
||||
if ($active >= $max_parallel) {
|
||||
wait();
|
||||
$active-- if $active > 0;
|
||||
}
|
||||
my $pid = fork();
|
||||
next if (!defined $pid);
|
||||
if ($pid != 0) { $active++; next; } # parent: one more child running, continue
|
||||
|
||||
# ---- CHILD ---- own DB connection so the parent's handle stays intact across forks
|
||||
$dbh->{InactiveDestroy} = 1;
|
||||
$dbh = DBI->connect($dsn, $ENV{'DBUSER'}, $ENV{'DBPASS'}, {
|
||||
mysql_auto_reconnect => 1,
|
||||
mysql_enable_utf8mb4 => 1
|
||||
});
|
||||
|
||||
$id = @$row[0];
|
||||
$user1 = @$row[1];
|
||||
$user2 = @$row[2];
|
||||
$host1 = @$row[3];
|
||||
$authmech1 = @$row[4];
|
||||
$auth_type = @$row[4];
|
||||
$password1 = @$row[5];
|
||||
$exclude = @$row[6];
|
||||
$port1 = @$row[7];
|
||||
@@ -113,21 +127,78 @@ while ($row = $sth->fetchrow_arrayref()) {
|
||||
$timeout1 = @$row[19];
|
||||
$timeout2 = @$row[20];
|
||||
$dry = @$row[21];
|
||||
$oauth_access_token = @$row[22];
|
||||
$oauth_token_expires = @$row[23];
|
||||
$oauth_flow = @$row[24];
|
||||
$source_id = @$row[25];
|
||||
|
||||
if ($enc1 eq "TLS") { $enc1 = "--tls1"; } elsif ($enc1 eq "SSL") { $enc1 = "--ssl1"; } else { undef $enc1; }
|
||||
|
||||
my $template = $run_dir . '/imapsync.XXXXXXX';
|
||||
my $passfile1 = File::Temp->new(TEMPLATE => $template);
|
||||
my $passfile2 = File::Temp->new(TEMPLATE => $template);
|
||||
|
||||
my $tokenfile1;
|
||||
|
||||
binmode( $passfile1, ":utf8" );
|
||||
|
||||
print $passfile1 "$password1\n";
|
||||
|
||||
# Auth-mode-specific argument set
|
||||
my @auth_args;
|
||||
if (defined($auth_type) && $auth_type eq 'XOAUTH2') {
|
||||
# authorization_code sources use a per-user token (keyed by source + user1),
|
||||
# not the shared source-level token used by client_credentials.
|
||||
if (defined($oauth_flow) && $oauth_flow eq 'authorization_code') {
|
||||
my $tsth = $dbh->prepare("SELECT access_token, token_expires FROM imapsync_source_oauth_token WHERE source_id=? AND username=?");
|
||||
$tsth->execute($source_id, $user1);
|
||||
my $trow = $tsth->fetchrow_arrayref();
|
||||
$oauth_access_token = $trow ? @$trow[0] : undef;
|
||||
$oauth_token_expires = $trow ? @$trow[1] : undef;
|
||||
}
|
||||
# Skip sync if cached token is missing or about to expire (<2 min)
|
||||
if (!defined($oauth_access_token) || $oauth_access_token eq ''
|
||||
|| !defined($oauth_token_expires) || $oauth_token_expires - time() < 120) {
|
||||
my $upd = $dbh->prepare("UPDATE imapsync SET returned_text=?, success=0, exit_status='OAUTH_TOKEN_NOT_READY', last_run=NOW(), is_running=0 WHERE id=?");
|
||||
$upd->execute("OAuth access token for the configured source is missing or expires too soon; refresh cron will retry.", $id);
|
||||
$dbh->disconnect();
|
||||
unlink $passfile1->filename if defined $passfile1;
|
||||
unlink $passfile2->filename if defined $passfile2;
|
||||
POSIX::_exit(75); # EX_TEMPFAIL: did not actually sync (token not ready)
|
||||
}
|
||||
$tokenfile1 = File::Temp->new(TEMPLATE => $template);
|
||||
binmode($tokenfile1, ":utf8");
|
||||
print $tokenfile1 "$oauth_access_token\n";
|
||||
@auth_args = ("--authmech1", "XOAUTH2",
|
||||
"--oauthaccesstoken1", $tokenfile1->filename);
|
||||
} else {
|
||||
print $passfile1 "$password1\n";
|
||||
@auth_args = ("--passfile1", $passfile1->filename);
|
||||
# imapsync auto-picks PLAIN; only emit --authmech1 for LOGIN/CRAM-MD5
|
||||
if (defined($auth_type) && $auth_type ne 'PLAIN') {
|
||||
push @auth_args, "--authmech1", $auth_type;
|
||||
}
|
||||
}
|
||||
|
||||
print $passfile2 trim($master_pass) . "\n";
|
||||
|
||||
my @custom_params_a = qqw($custom_params);
|
||||
# Structured custom params: JSON array of {o,v}. Build one argv token per pair as "--opt=value"
|
||||
# (value glued to its option) so a value can never become a separate option; passed via
|
||||
# IPC::Run list form (execvp, no shell). Only allowlisted option names are stored (validated on write).
|
||||
my @custom_params_a;
|
||||
my $cp_pairs = eval { decode_json(defined($custom_params) && $custom_params ne '' ? $custom_params : '[]') };
|
||||
if (ref($cp_pairs) eq 'ARRAY') {
|
||||
for my $p (@$cp_pairs) {
|
||||
next unless ref($p) eq 'HASH' && defined $p->{o} && $p->{o} ne '';
|
||||
(my $ov = defined($p->{v}) ? $p->{v} : '') =~ s/\x00//g;
|
||||
push @custom_params_a, ($ov eq '' ? "--$p->{o}" : "--$p->{o}=$ov");
|
||||
}
|
||||
}
|
||||
my $custom_params_ref = \@custom_params_a;
|
||||
|
||||
# Effective per-process bandwidth cap (bytes/s): per-job value, capped by the global limit.
|
||||
my $eff_bw = int($maxbytespersecond);
|
||||
if ($global_bw > 0) {
|
||||
$eff_bw = ($eff_bw > 0 && $eff_bw < $global_bw) ? $eff_bw : $global_bw;
|
||||
}
|
||||
|
||||
my $generated_cmds = [ "/usr/local/bin/imapsync",
|
||||
"--tmpdir", "/tmp",
|
||||
"--nofoldersizes",
|
||||
@@ -137,7 +208,7 @@ while ($row = $sth->fetchrow_arrayref()) {
|
||||
($exclude eq "" ? () : ("--exclude", $exclude)),
|
||||
($subfolder2 eq "" ? () : ('--subfolder2', $subfolder2)),
|
||||
($maxage eq "0" ? () : ('--maxage', $maxage)),
|
||||
($maxbytespersecond eq "0" ? () : ('--maxbytespersecond', $maxbytespersecond)),
|
||||
($eff_bw <= 0 ? () : ('--maxbytespersecond', $eff_bw)),
|
||||
($delete2duplicates ne "1" ? () : ('--delete2duplicates')),
|
||||
($subscribeall ne "1" ? () : ('--subscribeall')),
|
||||
($delete1 ne "1" ? () : ('--delete')),
|
||||
@@ -147,7 +218,7 @@ while ($row = $sth->fetchrow_arrayref()) {
|
||||
(!defined($enc1) ? () : ($enc1)),
|
||||
"--host1", $host1,
|
||||
"--user1", $user1,
|
||||
"--passfile1", $passfile1->filename,
|
||||
@auth_args,
|
||||
"--port1", $port1,
|
||||
"--host2", "localhost",
|
||||
"--user2", $user2 . '*' . trim($master_user),
|
||||
@@ -187,10 +258,19 @@ while ($row = $sth->fetchrow_arrayref()) {
|
||||
$update->execute();
|
||||
};
|
||||
|
||||
|
||||
$dbh->disconnect();
|
||||
# Remove this child's own temp files, then _exit so END/DESTROY are skipped — this keeps
|
||||
# the parent's LockFile lock and DB handle intact (a plain exit() would run autoclean and
|
||||
# release the shared lock while the parent + siblings are still working).
|
||||
unlink $passfile1->filename if defined $passfile1;
|
||||
unlink $passfile2->filename if defined $passfile2;
|
||||
unlink $tokenfile1->filename if defined $tokenfile1;
|
||||
POSIX::_exit(0); # ---- end CHILD ----
|
||||
}
|
||||
|
||||
$sth->finish();
|
||||
# Parent: wait for all remaining children to finish
|
||||
while (wait() != -1) { }
|
||||
|
||||
$dbh->disconnect();
|
||||
|
||||
$lockmgr->unlock($lock_file);
|
||||
|
||||
@@ -47,7 +47,7 @@ try:
|
||||
if max_score == "":
|
||||
max_score = 9999.0
|
||||
|
||||
def query_mysql(query, headers = True, update = False):
|
||||
def query_mysql(query, params = None, headers = True, update = False):
|
||||
while True:
|
||||
try:
|
||||
cnx = MySQLdb.connect(user=os.environ.get('DBUSER'), password=os.environ.get('DBPASS'), database=os.environ.get('DBNAME'), charset="utf8mb4", collation="utf8mb4_general_ci")
|
||||
@@ -57,7 +57,10 @@ try:
|
||||
else:
|
||||
break
|
||||
cur = cnx.cursor()
|
||||
cur.execute(query)
|
||||
if params:
|
||||
cur.execute(query, params)
|
||||
else:
|
||||
cur.execute(query)
|
||||
if not update:
|
||||
result = []
|
||||
columns = tuple( [d[0] for d in cur.description] )
|
||||
@@ -76,7 +79,7 @@ try:
|
||||
|
||||
def notify_rcpt(rcpt, msg_count, quarantine_acl, category):
|
||||
if category == "add_header": category = "add header"
|
||||
meta_query = query_mysql('SELECT `qhash`, id, subject, score, sender, created, action FROM quarantine WHERE notified = 0 AND rcpt = "%s" AND score < %f AND (action = "%s" OR "all" = "%s")' % (rcpt, max_score, category, category))
|
||||
meta_query = query_mysql('SELECT `qhash`, id, subject, score, sender, created, action FROM quarantine WHERE notified = 0 AND rcpt = %s AND score < %s AND (action = %s OR "all" = %s)', (rcpt, max_score, category, category))
|
||||
print("%s: %d of %d messages qualify for notification" % (rcpt, len(meta_query), msg_count))
|
||||
if len(meta_query) == 0:
|
||||
return
|
||||
@@ -130,7 +133,7 @@ try:
|
||||
server.sendmail(msg['From'], [str(redirect)] + [str(bcc)], text)
|
||||
server.quit()
|
||||
for res in meta_query:
|
||||
query_mysql('UPDATE quarantine SET notified = 1 WHERE id = "%d"' % (res['id']), update = True)
|
||||
query_mysql('UPDATE quarantine SET notified = 1 WHERE id = %s', (res['id'],), update = True)
|
||||
r.hset('Q_LAST_NOTIFIED', record['rcpt'], time_now)
|
||||
break
|
||||
except Exception as ex:
|
||||
@@ -138,7 +141,7 @@ try:
|
||||
print('%s' % (ex))
|
||||
time.sleep(3)
|
||||
|
||||
records = query_mysql('SELECT IFNULL(user_acl.quarantine, 0) AS quarantine_acl, count(id) AS counter, rcpt FROM quarantine LEFT OUTER JOIN user_acl ON user_acl.username = rcpt WHERE notified = 0 AND score < %f AND rcpt in (SELECT username FROM mailbox) GROUP BY rcpt' % (max_score))
|
||||
records = query_mysql('SELECT IFNULL(user_acl.quarantine, 0) AS quarantine_acl, count(id) AS counter, rcpt FROM quarantine LEFT OUTER JOIN user_acl ON user_acl.username = rcpt WHERE notified = 0 AND score < %s AND rcpt in (SELECT username FROM mailbox) GROUP BY rcpt', (max_score,))
|
||||
|
||||
for record in records:
|
||||
attrs = ''
|
||||
@@ -156,7 +159,7 @@ try:
|
||||
except Exception as ex:
|
||||
print('Could not determine last notification for %s, assuming never' % (record['rcpt']))
|
||||
last_notification = 0
|
||||
attrs_json = query_mysql('SELECT attributes FROM mailbox WHERE username = "%s"' % (record['rcpt']))
|
||||
attrs_json = query_mysql('SELECT attributes FROM mailbox WHERE username = %s', (record['rcpt'],))
|
||||
attrs = attrs_json[0]['attributes']
|
||||
if isinstance(attrs, str):
|
||||
# if attr is str then just load it
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM alpine:3.21
|
||||
FROM alpine:3.23
|
||||
|
||||
LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
|
||||
|
||||
@@ -40,4 +40,4 @@ COPY ./docker-entrypoint.sh /app/
|
||||
|
||||
RUN chmod +x /app/docker-entrypoint.sh
|
||||
|
||||
CMD ["/bin/sh", "-c", "/app/docker-entrypoint.sh"]
|
||||
CMD ["/bin/sh", "-c", "/app/docker-entrypoint.sh"]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/bin/sh
|
||||
|
||||
backend=iptables
|
||||
backend=nftables
|
||||
|
||||
nft list table ip filter &>/dev/null
|
||||
nftables_found=$?
|
||||
|
||||
@@ -449,6 +449,11 @@ if __name__ == '__main__':
|
||||
tables = NFTables(chain_name, logger)
|
||||
else:
|
||||
logger.logInfo('Using IPTables backend')
|
||||
logger.logWarn(
|
||||
"DEPRECATION: iptables-legacy is deprecated and will be removed in future releases. "
|
||||
"Please switch to nftables on your host to ensure complete compatibility."
|
||||
)
|
||||
time.sleep(5)
|
||||
tables = IPTables(chain_name, logger)
|
||||
|
||||
clear()
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import time
|
||||
import json
|
||||
import datetime
|
||||
|
||||
class Logger:
|
||||
def __init__(self):
|
||||
@@ -8,17 +9,28 @@ class Logger:
|
||||
def set_redis(self, redis):
|
||||
self.r = redis
|
||||
|
||||
def _format_timestamp(self):
|
||||
# Local time with milliseconds
|
||||
return datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
def log(self, priority, message):
|
||||
tolog = {}
|
||||
tolog['time'] = int(round(time.time()))
|
||||
tolog['priority'] = priority
|
||||
tolog['message'] = message
|
||||
print(message)
|
||||
# build redis-friendly dict
|
||||
tolog = {
|
||||
'time': int(round(time.time())), # keep raw timestamp for Redis
|
||||
'priority': priority,
|
||||
'message': message
|
||||
}
|
||||
|
||||
# print human-readable message with timestamp
|
||||
ts = self._format_timestamp()
|
||||
print(f"{ts} {priority.upper()}: {message}", flush=True)
|
||||
|
||||
# also push JSON to Redis if connected
|
||||
if self.r is not None:
|
||||
try:
|
||||
self.r.lpush('NETFILTER_LOG', json.dumps(tolog, ensure_ascii=False))
|
||||
except Exception as ex:
|
||||
print('Failed logging to redis: %s' % (ex))
|
||||
print(f'{ts} WARN: Failed logging to redis: {ex}', flush=True)
|
||||
|
||||
def logWarn(self, message):
|
||||
self.log('warn', message)
|
||||
@@ -27,4 +39,4 @@ class Logger:
|
||||
self.log('crit', message)
|
||||
|
||||
def logInfo(self, message):
|
||||
self.log('info', message)
|
||||
self.log('info', message)
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM nginx:alpine
|
||||
FROM nginx:1.30.4-alpine
|
||||
LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>"
|
||||
|
||||
ENV PIP_BREAK_SYSTEM_PACKAGES=1
|
||||
|
||||
@@ -10,7 +10,7 @@ def includes_conf(env, template_vars):
|
||||
server_name_config = f"server_name {template_vars['MAILCOW_HOSTNAME']} autodiscover.* autoconfig.* {' '.join(template_vars['ADDITIONAL_SERVER_NAMES'])};"
|
||||
listen_plain_config = f"listen {template_vars['HTTP_PORT']};"
|
||||
listen_ssl_config = f"listen {template_vars['HTTPS_PORT']};"
|
||||
if not template_vars['ENABLE_IPV6']:
|
||||
if template_vars['ENABLE_IPV6']:
|
||||
listen_plain_config += f"\nlisten [::]:{template_vars['HTTP_PORT']};"
|
||||
listen_ssl_config += f"\nlisten [::]:{template_vars['HTTPS_PORT']} ssl;"
|
||||
listen_ssl_config += "\nhttp2 on;"
|
||||
|
||||
@@ -3,17 +3,17 @@ FROM php:8.2-fpm-alpine3.21
|
||||
LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
|
||||
|
||||
# renovate: datasource=github-tags depName=krakjoe/apcu versioning=semver-coerced extractVersion=^v(?<version>.*)$
|
||||
ARG APCU_PECL_VERSION=5.1.26
|
||||
ARG APCU_PECL_VERSION=5.1.28
|
||||
# renovate: datasource=github-tags depName=Imagick/imagick versioning=semver-coerced extractVersion=(?<version>.*)$
|
||||
ARG IMAGICK_PECL_VERSION=3.8.0
|
||||
ARG IMAGICK_PECL_VERSION=3.8.1
|
||||
# renovate: datasource=github-tags depName=php/pecl-mail-mailparse versioning=semver-coerced extractVersion=^v(?<version>.*)$
|
||||
ARG MAILPARSE_PECL_VERSION=3.1.8
|
||||
ARG MAILPARSE_PECL_VERSION=3.2.0
|
||||
# renovate: datasource=github-tags depName=php-memcached-dev/php-memcached versioning=semver-coerced extractVersion=^v(?<version>.*)$
|
||||
ARG MEMCACHED_PECL_VERSION=3.3.0
|
||||
ARG MEMCACHED_PECL_VERSION=3.4.0
|
||||
# renovate: datasource=github-tags depName=phpredis/phpredis versioning=semver-coerced extractVersion=(?<version>.*)$
|
||||
ARG REDIS_PECL_VERSION=6.2.0
|
||||
ARG REDIS_PECL_VERSION=6.3.0
|
||||
# renovate: datasource=github-tags depName=composer/composer versioning=semver-coerced extractVersion=(?<version>.*)$
|
||||
ARG COMPOSER_VERSION=2.8.6
|
||||
ARG COMPOSER_VERSION=2.10.2
|
||||
|
||||
RUN apk add -U --no-cache autoconf \
|
||||
aspell-dev \
|
||||
|
||||
@@ -167,7 +167,7 @@ DELIMITER //
|
||||
CREATE EVENT clean_spamalias
|
||||
ON SCHEDULE EVERY 1 DAY DO
|
||||
BEGIN
|
||||
DELETE FROM spamalias WHERE validity < UNIX_TIMESTAMP();
|
||||
DELETE FROM spamalias WHERE validity < UNIX_TIMESTAMP() AND permanent = 0;
|
||||
END;
|
||||
//
|
||||
DELIMITER ;
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
FROM golang:1.25-bookworm AS builder
|
||||
FROM golang:1.26-trixie AS builder
|
||||
WORKDIR /src
|
||||
|
||||
ENV CGO_ENABLED=0 \
|
||||
GO111MODULE=on \
|
||||
NOOPT=1 \
|
||||
VERSION=1.8.14
|
||||
VERSION=1.11.0
|
||||
|
||||
RUN git clone --branch v${VERSION} https://github.com/Zuplu/postfix-tlspol && \
|
||||
cd /src/postfix-tlspol && \
|
||||
scripts/build.sh build-only
|
||||
|
||||
|
||||
FROM debian:bookworm-slim
|
||||
FROM debian:trixie-slim
|
||||
LABEL maintainer="The Infrastructure Company GmbH <info@servercow.de>"
|
||||
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@version: 3.38
|
||||
@version: 4.8
|
||||
@include "scl.conf"
|
||||
options {
|
||||
chain_hostnames(off);
|
||||
@@ -7,7 +7,7 @@ options {
|
||||
dns_cache(no);
|
||||
use_fqdn(no);
|
||||
owner("root"); group("adm"); perm(0640);
|
||||
stats_freq(0);
|
||||
stats(freq(0));
|
||||
bad_hostname("^gconfd$");
|
||||
};
|
||||
source s_src {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@version: 3.38
|
||||
@version: 4.8
|
||||
@include "scl.conf"
|
||||
options {
|
||||
chain_hostnames(off);
|
||||
@@ -7,7 +7,7 @@ options {
|
||||
dns_cache(no);
|
||||
use_fqdn(no);
|
||||
owner("root"); group("adm"); perm(0640);
|
||||
stats_freq(0);
|
||||
stats(freq(0));
|
||||
bad_hostname("^gconfd$");
|
||||
};
|
||||
source s_src {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM debian:bookworm-slim
|
||||
FROM debian:trixie-slim
|
||||
|
||||
LABEL maintainer="The Infrastructure Company GmbH <info@servercow.de>"
|
||||
|
||||
|
||||
@@ -329,14 +329,17 @@ query = SELECT goto FROM alias
|
||||
SELECT id FROM alias
|
||||
WHERE address='%s'
|
||||
AND (active='1' OR active='2')
|
||||
AND sender_allowed='1'
|
||||
), (
|
||||
SELECT id FROM alias
|
||||
WHERE address='@%d'
|
||||
AND (active='1' OR active='2')
|
||||
AND sender_allowed='1'
|
||||
)
|
||||
)
|
||||
)
|
||||
AND active='1'
|
||||
AND sender_allowed='1'
|
||||
AND (domain IN
|
||||
(SELECT domain FROM domain
|
||||
WHERE domain='%d'
|
||||
@@ -390,7 +393,7 @@ hosts = unix:/var/run/mysqld/mysqld.sock
|
||||
dbname = ${DBNAME}
|
||||
query = SELECT goto FROM spamalias
|
||||
WHERE address='%s'
|
||||
AND validity >= UNIX_TIMESTAMP()
|
||||
AND (validity >= UNIX_TIMESTAMP() OR permanent != 0)
|
||||
EOF
|
||||
|
||||
if [ ! -f /opt/postfix/conf/dns_blocklists.cf ]; then
|
||||
@@ -524,4 +527,4 @@ if [[ $? != 0 ]]; then
|
||||
else
|
||||
postfix -c /opt/postfix/conf start
|
||||
sleep 126144000
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@version: 3.38
|
||||
@version: 4.8
|
||||
@include "scl.conf"
|
||||
options {
|
||||
chain_hostnames(off);
|
||||
@@ -7,7 +7,7 @@ options {
|
||||
dns_cache(no);
|
||||
use_fqdn(no);
|
||||
owner("root"); group("adm"); perm(0640);
|
||||
stats_freq(0);
|
||||
stats(freq(0));
|
||||
bad_hostname("^gconfd$");
|
||||
};
|
||||
source s_src {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@version: 3.38
|
||||
@version: 4.8
|
||||
@include "scl.conf"
|
||||
options {
|
||||
chain_hostnames(off);
|
||||
@@ -7,7 +7,7 @@ options {
|
||||
dns_cache(no);
|
||||
use_fqdn(no);
|
||||
owner("root"); group("adm"); perm(0640);
|
||||
stats_freq(0);
|
||||
stats(freq(0));
|
||||
bad_hostname("^gconfd$");
|
||||
};
|
||||
source s_src {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
FROM debian:bookworm-slim
|
||||
FROM debian:trixie-slim
|
||||
LABEL maintainer="The Infrastructure Company GmbH <info@servercow.de>"
|
||||
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
ARG RSPAMD_VER=rspamd_3.12.1-1~6dbfca2fa
|
||||
ARG CODENAME=bookworm
|
||||
ARG RSPAMD_VER=rspamd_4.1.4-1~beb659b
|
||||
ARG CODENAME=trixie
|
||||
ENV LC_ALL=C
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
@@ -14,8 +14,8 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
dnsutils \
|
||||
netcat-traditional \
|
||||
wget \
|
||||
redis-tools \
|
||||
procps \
|
||||
redis-tools \
|
||||
procps \
|
||||
nano \
|
||||
lua-cjson \
|
||||
&& arch=$(arch | sed s/aarch64/arm64/ | sed s/x86_64/amd64/) \
|
||||
|
||||
@@ -1,47 +1,167 @@
|
||||
FROM debian:bookworm-slim
|
||||
# SOGo built from source to enable security patch application
|
||||
# Repository: https://github.com/Alinto/sogo
|
||||
# Version: SOGo-5.12.9
|
||||
#
|
||||
# Applied security patches:
|
||||
# -
|
||||
#
|
||||
# To add new patches, modify SOGO_SECURITY_PATCHES ARG below with space-separated commit hashes
|
||||
|
||||
FROM debian:bookworm
|
||||
|
||||
LABEL maintainer="The Infrastructure Company GmbH <info@servercow.de>"
|
||||
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
ARG DEBIAN_VERSION=bookworm
|
||||
ARG SOGO_DEBIAN_REPOSITORY=https://packagingv2.sogo.nu/sogo-nightly-debian/
|
||||
ARG SOGO_VERSION=SOGo-5.12.10
|
||||
ARG SOPE_VERSION=SOPE-5.12.10
|
||||
# Security patches to apply (space-separated commit hashes)
|
||||
ARG SOGO_SECURITY_PATCHES=""
|
||||
# renovate: datasource=github-releases depName=tianon/gosu versioning=semver-coerced extractVersion=^(?<version>.*)$
|
||||
ARG GOSU_VERSION=1.17
|
||||
ARG GOSU_VERSION=1.19
|
||||
ENV LC_ALL=C
|
||||
|
||||
# Prerequisites
|
||||
RUN echo "Building from repository $SOGO_DEBIAN_REPOSITORY" \
|
||||
&& apt-get update && apt-get install -y --no-install-recommends \
|
||||
apt-transport-https \
|
||||
ca-certificates \
|
||||
gettext \
|
||||
gnupg \
|
||||
mariadb-client \
|
||||
rsync \
|
||||
supervisor \
|
||||
syslog-ng \
|
||||
syslog-ng-core \
|
||||
syslog-ng-mod-redis \
|
||||
dirmngr \
|
||||
netcat-traditional \
|
||||
psmisc \
|
||||
wget \
|
||||
patch \
|
||||
# Install dependencies, build SOPE and SOGo, then clean up (all in one layer to minimize image size)
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
# Build dependencies
|
||||
git \
|
||||
build-essential \
|
||||
gobjc \
|
||||
pkg-config \
|
||||
gnustep-make \
|
||||
gnustep-base-runtime \
|
||||
libgnustep-base-dev \
|
||||
libxml2-dev \
|
||||
libldap2-dev \
|
||||
libssl-dev \
|
||||
zlib1g-dev \
|
||||
libpq-dev \
|
||||
libmariadb-dev-compat \
|
||||
libmemcached-dev \
|
||||
libsodium-dev \
|
||||
libcurl4-openssl-dev \
|
||||
libzip-dev \
|
||||
libytnef0-dev \
|
||||
libwbxml2-dev \
|
||||
curl \
|
||||
ca-certificates \
|
||||
# Runtime dependencies
|
||||
apt-transport-https \
|
||||
gettext \
|
||||
gnupg \
|
||||
mariadb-client \
|
||||
rsync \
|
||||
supervisor \
|
||||
syslog-ng \
|
||||
syslog-ng-core \
|
||||
syslog-ng-mod-redis \
|
||||
dirmngr \
|
||||
netcat-traditional \
|
||||
psmisc \
|
||||
wget \
|
||||
patch \
|
||||
libobjc4 \
|
||||
libxml2 \
|
||||
libldap-2.5-0 \
|
||||
libssl3 \
|
||||
zlib1g \
|
||||
libmariadb3 \
|
||||
libmemcached11 \
|
||||
libsodium23 \
|
||||
libcurl4 \
|
||||
libzip4 \
|
||||
libytnef0 \
|
||||
libwbxml2-1 \
|
||||
# Download gosu
|
||||
&& dpkgArch="$(dpkg --print-architecture | awk -F- '{ print $NF }')" \
|
||||
&& wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$dpkgArch" \
|
||||
&& chmod +x /usr/local/bin/gosu \
|
||||
&& gosu nobody true \
|
||||
&& mkdir /usr/share/doc/sogo \
|
||||
&& touch /usr/share/doc/sogo/empty.sh \
|
||||
&& wget -O- https://keys.openpgp.org/vks/v1/by-fingerprint/74FFC6D72B925A34B5D356BDF8A27B36A6E2EAE9 | gpg --dearmor | apt-key add - \
|
||||
&& echo "deb [trusted=yes] ${SOGO_DEBIAN_REPOSITORY} ${DEBIAN_VERSION} main" > /etc/apt/sources.list.d/sogo.list \
|
||||
&& apt-get update && apt-get install -y --no-install-recommends \
|
||||
sogo \
|
||||
sogo-activesync \
|
||||
&& apt-get autoclean \
|
||||
# Build SOPE
|
||||
&& git clone --depth 1 --branch ${SOPE_VERSION} https://github.com/Alinto/sope.git /tmp/sope \
|
||||
&& cd /tmp/sope \
|
||||
&& rm -rf .git \
|
||||
&& . /usr/share/GNUstep/Makefiles/GNUstep.sh \
|
||||
&& ./configure --prefix=/usr --disable-debug --disable-strip \
|
||||
&& make -j$(nproc) \
|
||||
&& make install \
|
||||
&& cd / \
|
||||
&& rm -rf /tmp/sope \
|
||||
# Build SOGo with security patches
|
||||
&& git clone --depth 1 --branch ${SOGO_VERSION} https://github.com/Alinto/sogo.git /tmp/sogo \
|
||||
&& cd /tmp/sogo \
|
||||
&& git config user.email "builder@mailcow.local" \
|
||||
&& git config user.name "SOGo Builder" \
|
||||
&& for patch in ${SOGO_SECURITY_PATCHES}; do \
|
||||
echo "Applying security patch: ${patch}"; \
|
||||
git fetch origin ${patch} && git cherry-pick ${patch}; \
|
||||
done \
|
||||
&& rm -rf .git \
|
||||
&& . /usr/share/GNUstep/Makefiles/GNUstep.sh \
|
||||
&& ./configure --disable-debug --disable-strip \
|
||||
&& make -j$(nproc) \
|
||||
&& make install \
|
||||
&& cd /tmp/sogo/ActiveSync \
|
||||
&& . /usr/share/GNUstep/Makefiles/GNUstep.sh \
|
||||
&& make -j$(nproc) install \
|
||||
&& cd / \
|
||||
&& rm -rf /tmp/sogo \
|
||||
# Strip binaries
|
||||
&& strip --strip-unneeded /usr/local/sbin/sogod 2>/dev/null || true \
|
||||
&& strip --strip-unneeded /usr/local/sbin/sogo-tool 2>/dev/null || true \
|
||||
&& strip --strip-unneeded /usr/local/sbin/sogo-ealarms-notify 2>/dev/null || true \
|
||||
&& strip --strip-unneeded /usr/local/sbin/sogo-slapd-sockd 2>/dev/null || true \
|
||||
# Remove build dependencies and clean up
|
||||
&& apt-get purge -y --auto-remove \
|
||||
git \
|
||||
build-essential \
|
||||
gobjc \
|
||||
gnustep-make \
|
||||
libgnustep-base-dev \
|
||||
libxml2-dev \
|
||||
libldap2-dev \
|
||||
libssl-dev \
|
||||
zlib1g-dev \
|
||||
libpq-dev \
|
||||
libmariadb-dev-compat \
|
||||
libmemcached-dev \
|
||||
libsodium-dev \
|
||||
libcurl4-openssl-dev \
|
||||
libzip-dev \
|
||||
libytnef0-dev \
|
||||
curl \
|
||||
&& apt-get autoremove -y \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& rm -rf /usr/share/doc/* \
|
||||
&& rm -rf /usr/share/man/* \
|
||||
&& rm -rf /var/cache/debconf/* \
|
||||
&& rm -rf /tmp/* \
|
||||
&& rm -rf /root/.cache \
|
||||
&& find /usr/local/lib -name '*.a' -delete \
|
||||
&& find /usr/lib -name '*.a' -delete \
|
||||
&& mkdir -p /usr/share/doc/sogo \
|
||||
&& touch /usr/share/doc/sogo/empty.sh \
|
||||
&& touch /etc/default/locale
|
||||
|
||||
# Configure library paths
|
||||
RUN echo "/usr/lib64" > /etc/ld.so.conf.d/sogo.conf \
|
||||
&& echo "/usr/local/lib/sogo" >> /etc/ld.so.conf.d/sogo.conf \
|
||||
&& echo "/usr/local/lib/GNUstep/Frameworks/SOGo.framework/Versions/5/sogo" >> /etc/ld.so.conf.d/sogo.conf \
|
||||
&& ldconfig
|
||||
|
||||
# Create sogo user and group
|
||||
RUN groupadd -r -g 999 sogo \
|
||||
&& useradd -r -u 999 -g sogo -d /var/lib/sogo -s /bin/bash -c "SOGo Daemon" sogo \
|
||||
&& mkdir -p /var/lib/sogo /var/run/sogo /var/log/sogo /var/spool/sogo \
|
||||
&& chown -R sogo:sogo /var/lib/sogo /var/run/sogo /var/log/sogo /var/spool/sogo
|
||||
|
||||
# Create symlinks for SOGo binaries
|
||||
RUN ln -s /usr/local/sbin/sogod /usr/sbin/sogod \
|
||||
&& ln -s /usr/local/sbin/sogo-tool /usr/sbin/sogo-tool \
|
||||
&& ln -s /usr/local/sbin/sogo-ealarms-notify /usr/sbin/sogo-ealarms-notify \
|
||||
&& ln -s /usr/local/sbin/sogo-slapd-sockd /usr/sbin/sogo-slapd-sockd
|
||||
|
||||
# Copy configuration files and scripts
|
||||
COPY ./bootstrap-sogo.sh /bootstrap-sogo.sh
|
||||
COPY syslog-ng.conf /etc/syslog-ng/syslog-ng.conf
|
||||
COPY syslog-ng-redis_slave.conf /etc/syslog-ng/syslog-ng-redis_slave.conf
|
||||
@@ -56,4 +176,4 @@ RUN chmod +x /bootstrap-sogo.sh \
|
||||
|
||||
ENTRYPOINT ["/docker-entrypoint.sh"]
|
||||
|
||||
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/supervisord.conf"]
|
||||
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/supervisord.conf"]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
--- /usr/lib/GNUstep/SOGo/Templates/UIxAclEditor.wox 2018-08-17 18:29:57.987504204 +0200
|
||||
+++ /usr/lib/GNUstep/SOGo/Templates/UIxAclEditor.wox 2018-08-17 18:29:35.918291298 +0200
|
||||
--- /usr/local/lib/GNUstep/SOGo/Templates/UIxAclEditor.wox 2018-08-17 18:29:57.987504204 +0200
|
||||
+++ /usr/local/lib/GNUstep/SOGo/Templates/UIxAclEditor.wox 2018-08-17 18:29:35.918291298 +0200
|
||||
@@ -46,7 +46,7 @@
|
||||
</md-item-template>
|
||||
</md-autocomplete>
|
||||
|
||||
@@ -130,18 +130,22 @@ chmod 600 /var/lib/sogo/GNUstep/Defaults/sogod.plist
|
||||
# Patch ACLs
|
||||
#if [[ ${ACL_ANYONE} == 'allow' ]]; then
|
||||
# #enable any or authenticated targets for ACL
|
||||
# if patch -R -sfN --dry-run /usr/lib/GNUstep/SOGo/Templates/UIxAclEditor.wox < /acl.diff > /dev/null; then
|
||||
# patch -R /usr/lib/GNUstep/SOGo/Templates/UIxAclEditor.wox < /acl.diff;
|
||||
# if patch -R -sfN --dry-run /usr/local/lib/GNUstep/SOGo/Templates/UIxAclEditor.wox < /acl.diff > /dev/null; then
|
||||
# patch -R /usr/local/lib/GNUstep/SOGo/Templates/UIxAclEditor.wox < /acl.diff;
|
||||
# fi
|
||||
#else
|
||||
# #disable any or authenticated targets for ACL
|
||||
# if patch -sfN --dry-run /usr/lib/GNUstep/SOGo/Templates/UIxAclEditor.wox < /acl.diff > /dev/null; then
|
||||
# patch /usr/lib/GNUstep/SOGo/Templates/UIxAclEditor.wox < /acl.diff;
|
||||
# if patch -sfN --dry-run /usr/local/lib/GNUstep/SOGo/Templates/UIxAclEditor.wox < /acl.diff > /dev/null; then
|
||||
# patch /usr/local/lib/GNUstep/SOGo/Templates/UIxAclEditor.wox < /acl.diff;
|
||||
# fi
|
||||
#fi
|
||||
|
||||
if patch -R -sfN --dry-run /usr/lib/GNUstep/SOGo/Templates/UIxTopnavToolbar.wox < /navMailcowBtns.diff > /dev/null; then
|
||||
patch -R /usr/lib/GNUstep/SOGo/Templates/UIxTopnavToolbar.wox < /navMailcowBtns.diff;
|
||||
# Apply custom UI patch (reverse patch to ADD buttons)
|
||||
if patch -R -sfN --dry-run /usr/local/lib/GNUstep/SOGo/Templates/UIxTopnavToolbar.wox < /navMailcowBtns.diff > /dev/null; then
|
||||
echo "Applying navMailcowBtns patch (reverse to add buttons)..."
|
||||
patch -R /usr/local/lib/GNUstep/SOGo/Templates/UIxTopnavToolbar.wox < /navMailcowBtns.diff;
|
||||
else
|
||||
echo "navMailcowBtns patch already applied or cannot be applied"
|
||||
fi
|
||||
|
||||
# Rename custom logo, if any
|
||||
@@ -149,7 +153,7 @@ fi
|
||||
|
||||
# Rsync web content
|
||||
echo "Syncing web content with named volume"
|
||||
rsync -a /usr/lib/GNUstep/SOGo/. /sogo_web/
|
||||
rsync -a /usr/local/lib/GNUstep/SOGo/. /sogo_web/
|
||||
|
||||
# Chown backup path
|
||||
chown -R sogo:sogo /sogo_backup
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
FROM alpine:3.21
|
||||
FROM alpine:3.23
|
||||
|
||||
LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
|
||||
|
||||
# install unbound from alpine:edge to get security patches
|
||||
RUN apk add --no-cache --repository=https://dl-cdn.alpinelinux.org/alpine/edge/main unbound
|
||||
|
||||
# install other packages from regular alpine stable repo
|
||||
RUN apk add --update --no-cache \
|
||||
curl \
|
||||
bind-tools \
|
||||
coreutils \
|
||||
unbound \
|
||||
bash \
|
||||
openssl \
|
||||
drill \
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM alpine:3.21
|
||||
FROM alpine:3.23
|
||||
|
||||
LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
|
||||
|
||||
@@ -37,5 +37,6 @@ RUN apk add --update \
|
||||
COPY watchdog.sh /watchdog.sh
|
||||
COPY check_mysql_slavestatus.sh /usr/lib/nagios/plugins/check_mysql_slavestatus.sh
|
||||
COPY check_dns.sh /usr/lib/mailcow/check_dns.sh
|
||||
COPY client.cnf /etc/my.cnf.d/client.cnf
|
||||
|
||||
CMD ["/watchdog.sh"]
|
||||
|
||||
@@ -19,19 +19,19 @@ if [ -z "$HOST" ]; then
|
||||
fi
|
||||
|
||||
# run dig and measure the time it takes to run
|
||||
START_TIME=$(date +%s%3N)
|
||||
START_TIME=$(perl -MTime::HiRes -e 'print Time::HiRes::time')
|
||||
dig_output=$(dig +short +timeout=2 +tries=1 "$HOST" @"$SERVER" 2>/dev/null)
|
||||
dig_rc=$?
|
||||
END_TIME=$(perl -MTime::HiRes -e 'print Time::HiRes::time')
|
||||
dig_output_ips=$(echo "$dig_output" | grep -E '^[0-9.]+$' | sort | paste -sd ',' -)
|
||||
END_TIME=$(date +%s%3N)
|
||||
ELAPSED_TIME=$((END_TIME - START_TIME))
|
||||
ELAPSED_TIME=$(perl -e "printf('%.3f', $END_TIME - $START_TIME)")
|
||||
|
||||
# validate and perform nagios like output and exit codes
|
||||
if [ $dig_rc -ne 0 ] || [ -z "$dig_output" ]; then
|
||||
echo "Domain $HOST was not found by the server"
|
||||
exit 2
|
||||
elif [ $dig_rc -eq 0 ]; then
|
||||
echo "DNS OK: $ELAPSED_TIME ms response time. $HOST returns $dig_output_ips"
|
||||
echo "DNS OK: $ELAPSED_TIME seconds response time. $HOST returns $dig_output_ips"
|
||||
exit 0
|
||||
else
|
||||
echo "Unknown error"
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
[client]
|
||||
ssl = false
|
||||
ssl-verify-server-cert = false
|
||||
@@ -38,7 +38,7 @@ if [[ ! -p /tmp/com_pipe ]]; then
|
||||
fi
|
||||
|
||||
# Wait for containers
|
||||
while ! mariadb-admin status --ssl=false --socket=/var/run/mysqld/mysqld.sock -u${DBUSER} -p${DBPASS} --silent; do
|
||||
while ! mariadb-admin status --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u${DBUSER} -p${DBPASS} --silent; do
|
||||
echo "Waiting for SQL..."
|
||||
sleep 2
|
||||
done
|
||||
@@ -359,8 +359,8 @@ mysql_checks() {
|
||||
while [ ${err_count} -lt ${THRESHOLD} ]; do
|
||||
touch /tmp/mysql-mailcow; echo "$(tail -50 /tmp/mysql-mailcow)" > /tmp/mysql-mailcow
|
||||
err_c_cur=${err_count}
|
||||
/usr/lib/nagios/plugins/check_mysql -s /var/run/mysqld/mysqld.sock -u ${DBUSER} -p ${DBPASS} -d ${DBNAME} 2>> /tmp/mysql-mailcow 1>&2; err_count=$(( ${err_count} + $? ))
|
||||
/usr/lib/nagios/plugins/check_mysql_query -s /var/run/mysqld/mysqld.sock -u ${DBUSER} -p ${DBPASS} -d ${DBNAME} -q "SELECT COUNT(*) FROM information_schema.tables" 2>> /tmp/mysql-mailcow 1>&2; err_count=$(( ${err_count} + $? ))
|
||||
/usr/lib/nagios/plugins/check_mysql -f /etc/my.cnf.d/client.cnf -s /var/run/mysqld/mysqld.sock -u ${DBUSER} -p ${DBPASS} -d ${DBNAME} 2>> /tmp/mysql-mailcow 1>&2; err_count=$(( ${err_count} + $? ))
|
||||
/usr/lib/nagios/plugins/check_mysql_query -f /etc/my.cnf.d/client.cnf -s /var/run/mysqld/mysqld.sock -u ${DBUSER} -p ${DBPASS} -d ${DBNAME} -q "SELECT COUNT(*) FROM information_schema.tables" 2>> /tmp/mysql-mailcow 1>&2; err_count=$(( ${err_count} + $? ))
|
||||
[ ${err_c_cur} -eq ${err_count} ] && [ ! $((${err_count} - 1)) -lt 0 ] && err_count=$((${err_count} - 1)) diff_c=1
|
||||
[ ${err_c_cur} -ne ${err_count} ] && diff_c=$(( ${err_c_cur} - ${err_count} ))
|
||||
progress "MySQL/MariaDB" ${THRESHOLD} $(( ${THRESHOLD} - ${err_count} )) ${diff_c}
|
||||
@@ -384,7 +384,7 @@ mysql_repl_checks() {
|
||||
while [ ${err_count} -lt ${THRESHOLD} ]; do
|
||||
touch /tmp/mysql_repl_checks; echo "$(tail -50 /tmp/mysql_repl_checks)" > /tmp/mysql_repl_checks
|
||||
err_c_cur=${err_count}
|
||||
/usr/lib/nagios/plugins/check_mysql_slavestatus.sh -S /var/run/mysqld/mysqld.sock -u root -p ${DBROOT} 2>> /tmp/mysql_repl_checks 1>&2; err_count=$(( ${err_count} + $? ))
|
||||
/usr/lib/nagios/plugins/check_mysql_slavestatus.sh -o /etc/my.cnf.d/client.cnf -S /var/run/mysqld/mysqld.sock -u root -p ${DBROOT} 2>> /tmp/mysql_repl_checks 1>&2; err_count=$(( ${err_count} + $? ))
|
||||
[ ${err_c_cur} -eq ${err_count} ] && [ ! $((${err_count} - 1)) -lt 0 ] && err_count=$((${err_count} - 1)) diff_c=1
|
||||
[ ${err_c_cur} -ne ${err_count} ] && diff_c=$(( ${err_c_cur} - ${err_count} ))
|
||||
progress "MySQL/MariaDB replication" ${THRESHOLD} $(( ${THRESHOLD} - ${err_count} )) ${diff_c}
|
||||
|
||||
@@ -80,14 +80,21 @@ if ($isSOGoRequest) {
|
||||
}
|
||||
if ($result === false){
|
||||
// If it's a SOGo Request, don't check for protocol access
|
||||
$service = ($isSOGoRequest) ? false : array($post['service'] => true);
|
||||
$result = apppass_login($post['username'], $post['password'], $service, array(
|
||||
if ($isSOGoRequest) {
|
||||
$service = 'SOGO';
|
||||
$post['service'] = 'NONE';
|
||||
} else {
|
||||
$service = $post['service'];
|
||||
}
|
||||
|
||||
$result = apppass_login($post['username'], $post['password'], array(
|
||||
'service' => $post['service'],
|
||||
'is_internal' => true,
|
||||
'remote_addr' => $post['real_rip']
|
||||
));
|
||||
if ($result) {
|
||||
error_log('MAILCOWAUTH: App auth for user ' . $post['username'] . " with service " . $post['service'] . " from IP " . $post['real_rip']);
|
||||
set_sasl_log($post['username'], $post['real_rip'], $post['service']);
|
||||
error_log('MAILCOWAUTH: App auth for user ' . $post['username'] . " with service " . $service . " from IP " . $post['real_rip']);
|
||||
set_sasl_log($post['username'], $post['real_rip'], $service);
|
||||
}
|
||||
}
|
||||
if ($result === false){
|
||||
|
||||
@@ -32,8 +32,17 @@ function auth_password_verify(request, password)
|
||||
-- Returning PASSDB_RESULT_PASSWORD_MISMATCH will reset the user's auth cache entry.
|
||||
-- Returning PASSDB_RESULT_INTERNAL_FAILURE keeps the existing cache entry,
|
||||
-- even if the TTL has expired. Useful to avoid cache eviction during backend issues.
|
||||
|
||||
-- On a network-level failure (nginx unreachable, DNS failure, timeout) https.request
|
||||
-- returns nil plus an error string, so c is not a numeric HTTP status code. Treat this
|
||||
-- as a backend outage and keep the cache entry, rather than wiping it as a mismatch.
|
||||
if type(c) ~= "number" then
|
||||
dovecot.i_info("HTTP request to auth backend failed with " .. tostring(c) .. " for user " .. request.user)
|
||||
return dovecot.auth.PASSDB_RESULT_INTERNAL_FAILURE, "Upstream unreachable"
|
||||
end
|
||||
|
||||
if c ~= 200 and c ~= 401 then
|
||||
dovecot.i_info("HTTP request failed with " .. c .. " for user " .. request.user)
|
||||
dovecot.i_info("HTTP request failed with " .. tostring(c) .. " for user " .. request.user)
|
||||
return dovecot.auth.PASSDB_RESULT_PASSWORD_MISMATCH, "Upstream error"
|
||||
end
|
||||
|
||||
|
||||
@@ -1,13 +1,3 @@
|
||||
# global_sieve_before script
|
||||
# global_sieve_before -> user sieve_before (mailcow UI) -> user sieve_after (mailcow UI) -> global_sieve_after
|
||||
|
||||
require ["mailbox", "fileinto"];
|
||||
|
||||
if header :contains ["Chat-Version"] [""] {
|
||||
if mailboxexists "DeltaChat" {
|
||||
fileinto "DeltaChat";
|
||||
} else {
|
||||
fileinto :create "DeltaChat";
|
||||
}
|
||||
stop;
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ events {
|
||||
http {
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
server_tokens off;
|
||||
|
||||
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||||
'$status $body_bytes_sent "$http_referer" '
|
||||
@@ -41,12 +42,19 @@ http {
|
||||
https https;
|
||||
}
|
||||
|
||||
map $server_port $sogo_auth_internal {
|
||||
65510 "1";
|
||||
default "";
|
||||
}
|
||||
|
||||
{% if HTTP_REDIRECT %}
|
||||
# HTTP to HTTPS redirect
|
||||
server {
|
||||
root /web;
|
||||
listen {{ HTTP_PORT }} default_server;
|
||||
{% if ENABLE_IPV6 %}
|
||||
listen [::]:{{ HTTP_PORT }} default_server;
|
||||
{%endif%}
|
||||
|
||||
server_name {{ MAILCOW_HOSTNAME }} autodiscover.* autoconfig.* mta-sts.* {{ ADDITIONAL_SERVER_NAMES | join(' ') }};
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@ ssl_session_tickets off;
|
||||
|
||||
add_header Strict-Transport-Security "max-age=15768000;";
|
||||
add_header X-Content-Type-Options nosniff;
|
||||
add_header X-XSS-Protection "1; mode=block";
|
||||
add_header X-Robots-Tag none;
|
||||
add_header X-Download-Options noopen;
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
@@ -117,6 +116,9 @@ location ~ \.php$ {
|
||||
include /etc/nginx/fastcgi_params;
|
||||
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||
fastcgi_param PATH_INFO $fastcgi_path_info;
|
||||
fastcgi_param HTTP_X_REAL_IP $remote_addr;
|
||||
# trusted internal-auth marker; empty for external clients (see nginx.conf map)
|
||||
fastcgi_param SOGO_AUTH_INTERNAL $sogo_auth_internal;
|
||||
fastcgi_read_timeout 3600;
|
||||
fastcgi_send_timeout 3600;
|
||||
}
|
||||
@@ -262,19 +264,19 @@ location ~* /sogo$ {
|
||||
}
|
||||
|
||||
location /SOGo.woa/WebServerResources/ {
|
||||
alias /usr/lib/GNUstep/SOGo/WebServerResources/;
|
||||
alias /usr/local/lib/GNUstep/SOGo/WebServerResources/;
|
||||
}
|
||||
|
||||
location /.woa/WebServerResources/ {
|
||||
alias /usr/lib/GNUstep/SOGo/WebServerResources/;
|
||||
alias /usr/local/lib/GNUstep/SOGo/WebServerResources/;
|
||||
}
|
||||
|
||||
location /SOGo/WebServerResources/ {
|
||||
alias /usr/lib/GNUstep/SOGo/WebServerResources/;
|
||||
alias /usr/local/lib/GNUstep/SOGo/WebServerResources/;
|
||||
}
|
||||
|
||||
location (^/SOGo/so/ControlPanel/Products/[^/]*UI/Resources/.*\.(jpg|png|gif|css|js)$) {
|
||||
alias /usr/lib/GNUstep/SOGo/$1.SOGo/Resources/$2;
|
||||
alias /usr/local/lib/GNUstep/SOGo/$1.SOGo/Resources/$2;
|
||||
}
|
||||
{% endif %}
|
||||
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
<?php
|
||||
|
||||
require_once(__DIR__ . '/../web/inc/vars.inc.php');
|
||||
if (file_exists(__DIR__ . '/../web/inc/vars.local.inc.php')) {
|
||||
include_once(__DIR__ . '/../web/inc/vars.local.inc.php');
|
||||
}
|
||||
require_once __DIR__ . '/../web/inc/lib/vendor/autoload.php';
|
||||
|
||||
$dsn = $database_type . ":unix_socket=" . $database_sock . ";dbname=" . $database_name;
|
||||
$opt = [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
PDO::ATTR_EMULATE_PREPARES => false,
|
||||
];
|
||||
try {
|
||||
$pdo = new PDO($dsn, $database_user, $database_pass, $opt);
|
||||
} catch (PDOException $e) {
|
||||
fwrite(STDERR, "DB connect failed: " . $e->getMessage() . "\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$redis = new Redis();
|
||||
try {
|
||||
if (!empty(getenv('REDIS_SLAVEOF_IP'))) {
|
||||
$redis->connect(getenv('REDIS_SLAVEOF_IP'), getenv('REDIS_SLAVEOF_PORT'));
|
||||
} else {
|
||||
$redis->connect('redis-mailcow', 6379);
|
||||
}
|
||||
$redis->auth(getenv("REDISPASS"));
|
||||
} catch (Exception $e) {
|
||||
fwrite(STDERR, "Redis connect failed: " . $e->getMessage() . "\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
function logMsg($priority, $message, $task = "imapsync OAuth refresh") {
|
||||
global $redis;
|
||||
$redis->lPush('CRON_LOG', json_encode(array(
|
||||
"time" => time(),
|
||||
"priority" => $priority,
|
||||
"task" => $task,
|
||||
"message" => $message,
|
||||
)));
|
||||
}
|
||||
|
||||
require_once __DIR__ . '/../web/inc/functions.inc.php';
|
||||
require_once __DIR__ . '/../web/inc/functions.auth.inc.php';
|
||||
require_once __DIR__ . '/../web/inc/sessions.inc.php';
|
||||
require_once __DIR__ . '/../web/inc/functions.mailbox.inc.php';
|
||||
require_once __DIR__ . '/../web/inc/functions.syncjob.inc.php';
|
||||
|
||||
// File-lock to prevent concurrent runs
|
||||
$lock_file = '/tmp/imapsync-oauth-refresh.lock';
|
||||
if (file_exists($lock_file)) {
|
||||
$pid = (int)trim(@file_get_contents($lock_file));
|
||||
if ($pid > 0 && posix_kill($pid, 0)) {
|
||||
logMsg("info", "Previous refresh still running (pid=$pid), exiting");
|
||||
exit(0);
|
||||
}
|
||||
@unlink($lock_file);
|
||||
}
|
||||
file_put_contents($lock_file, getmypid());
|
||||
|
||||
try {
|
||||
// 1) client_credentials sources: refresh the shared source-level token (expires < 5 min or none)
|
||||
$stmt = $pdo->prepare("SELECT `id`, `name` FROM `imapsync_source`
|
||||
WHERE `active` = 1 AND `auth_type` = 'XOAUTH2' AND `oauth_flow` = 'client_credentials'
|
||||
AND (`oauth_token_expires` IS NULL OR `oauth_token_expires` < :soon)");
|
||||
$stmt->execute(array(':soon' => time() + 300));
|
||||
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $src) {
|
||||
$ok = imapsync_source_refresh_token_internal($src['id']);
|
||||
if ($ok) {
|
||||
logMsg("info", "Refreshed token for source '" . $src['name'] . "' (id=" . $src['id'] . ")");
|
||||
} else {
|
||||
$e = $pdo->prepare("SELECT `oauth_last_refresh_error` FROM `imapsync_source` WHERE `id` = :id");
|
||||
$e->execute(array(':id' => $src['id']));
|
||||
logMsg("warning", "Refresh failed for source '" . $src['name'] . "' (id=" . $src['id'] . "): " . $e->fetchColumn());
|
||||
}
|
||||
}
|
||||
|
||||
// 2) authorization_code sources: refresh each per-user token via its stored refresh_token
|
||||
$stmt = $pdo->prepare("SELECT t.`source_id`, t.`username`, s.`name`
|
||||
FROM `imapsync_source_oauth_token` t
|
||||
JOIN `imapsync_source` s ON s.`id` = t.`source_id`
|
||||
WHERE s.`active` = 1 AND s.`auth_type` = 'XOAUTH2' AND s.`oauth_flow` = 'authorization_code'
|
||||
AND t.`refresh_token` <> ''
|
||||
AND (t.`token_expires` IS NULL OR t.`token_expires` < :soon)");
|
||||
$stmt->execute(array(':soon' => time() + 300));
|
||||
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $tok) {
|
||||
$ok = imapsync_source_refresh_user_token_internal($tok['source_id'], $tok['username']);
|
||||
if ($ok) {
|
||||
logMsg("info", "Refreshed user token '" . $tok['username'] . "' on source '" . $tok['name'] . "' (id=" . $tok['source_id'] . ")");
|
||||
} else {
|
||||
$e = $pdo->prepare("SELECT `last_refresh_error` FROM `imapsync_source_oauth_token` WHERE `source_id` = :sid AND `username` = :user");
|
||||
$e->execute(array(':sid' => $tok['source_id'], ':user' => $tok['username']));
|
||||
logMsg("warning", "User-token refresh failed for '" . $tok['username'] . "' on source '" . $tok['name'] . "' (id=" . $tok['source_id'] . "): " . $e->fetchColumn());
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
@unlink($lock_file);
|
||||
}
|
||||
@@ -1,7 +1,16 @@
|
||||
; NOTE: Restart phpfpm on ANY manual changes to PHP files!
|
||||
|
||||
; opcache
|
||||
opcache.enable=1
|
||||
opcache.enable_cli=1
|
||||
opcache.interned_strings_buffer=16
|
||||
opcache.max_accelerated_files=10000
|
||||
opcache.memory_consumption=128
|
||||
opcache.save_comments=1
|
||||
opcache.revalidate_freq=1
|
||||
opcache.validate_timestamps=0
|
||||
|
||||
; JIT
|
||||
; Disabled for now due to some PHP segmentation faults observed
|
||||
; in certain environments. Possibly some PHP or PHP extension bug.
|
||||
opcache.jit=disable
|
||||
opcache.jit_buffer_size=0
|
||||
|
||||
@@ -37,7 +37,7 @@ postscreen_access_list = permit_mynetworks,
|
||||
cidr:/opt/postfix/conf/postscreen_access.cidr,
|
||||
tcp:127.0.0.1:10027
|
||||
postscreen_bare_newline_enable = no
|
||||
postscreen_blacklist_action = drop
|
||||
postscreen_denylist_action = drop
|
||||
postscreen_cache_cleanup_interval = 24h
|
||||
postscreen_cache_map = proxy:btree:$data_directory/postscreen_cache
|
||||
postscreen_dnsbl_action = enforce
|
||||
@@ -107,8 +107,6 @@ smtpd_sender_restrictions = reject_authenticated_sender_login_mismatch,
|
||||
reject_unknown_sender_domain
|
||||
smtpd_soft_error_limit = 3
|
||||
smtpd_tls_auth_only = yes
|
||||
smtpd_tls_dh1024_param_file = /etc/ssl/mail/dhparams.pem
|
||||
smtpd_tls_eecdh_grade = auto
|
||||
smtpd_tls_exclude_ciphers = ECDHE-RSA-RC4-SHA, RC4, aNULL, DES-CBC3-SHA, ECDHE-RSA-DES-CBC3-SHA, EDH-RSA-DES-CBC3-SHA
|
||||
smtpd_tls_loglevel = 1
|
||||
|
||||
@@ -152,7 +150,7 @@ smtp_sasl_auth_enable = yes
|
||||
smtp_sasl_password_maps = proxy:mysql:/opt/postfix/conf/sql/mysql_sasl_passwd_maps_sender_dependent.cf
|
||||
smtp_sasl_security_options =
|
||||
smtp_sasl_mechanism_filter = plain, login
|
||||
smtp_tls_policy_maps = proxy:mysql:/opt/postfix/conf/sql/mysql_tls_policy_override_maps.cf socketmap:inet:postfix-tlspol:8642:QUERY
|
||||
smtp_tls_policy_maps = proxy:mysql:/opt/postfix/conf/sql/mysql_tls_policy_override_maps.cf socketmap:inet:postfix-tlspol:8642:QUERYwithTLSRPT
|
||||
smtp_header_checks = pcre:/opt/postfix/conf/anonymize_headers.pcre
|
||||
mail_name = Postcow
|
||||
# local_transport map catches local destinations and prevents routing local dests when the next map would route "*"
|
||||
|
||||
@@ -29,7 +29,6 @@ smtps inet n - n - - smtpd
|
||||
# TLS protocol can be modified by setting submission_smtpd_tls_mandatory_protocols in extra.cf
|
||||
submission inet n - n - - smtpd
|
||||
-o smtpd_client_restrictions=permit_mynetworks,permit_sasl_authenticated,reject
|
||||
-o smtpd_enforce_tls=yes
|
||||
-o smtpd_tls_security_level=encrypt
|
||||
-o smtpd_tls_mandatory_protocols=$submission_smtpd_tls_mandatory_protocols
|
||||
-o tls_preempt_cipherlist=yes
|
||||
@@ -38,7 +37,6 @@ submission inet n - n - - smtpd
|
||||
10587 inet n - n - - smtpd
|
||||
-o smtpd_upstream_proxy_protocol=haproxy
|
||||
-o smtpd_client_restrictions=permit_mynetworks,permit_sasl_authenticated,reject
|
||||
-o smtpd_enforce_tls=yes
|
||||
-o smtpd_tls_security_level=encrypt
|
||||
-o smtpd_tls_mandatory_protocols=$submission_smtpd_tls_mandatory_protocols
|
||||
-o tls_preempt_cipherlist=yes
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
# Whitelist generated by Postwhite v3.4 on Mon Sep 1 00:23:07 UTC 2025
|
||||
# Whitelist generated by Postwhite v3.4 on Sat Aug 1 00:35:20 UTC 2026
|
||||
# https://github.com/stevejenkins/postwhite/
|
||||
# 2165 total rules
|
||||
# 2256 total rules
|
||||
2a00:1450:4000::/36 permit
|
||||
2a00:1450:4864::/56 permit
|
||||
2a01:111:f400::/48 permit
|
||||
2a01:111:f403::/49 permit
|
||||
2a01:111:f403:8000::/50 permit
|
||||
2a01:111:f403:2800::/53 permit
|
||||
2a01:111:f403:8000::/51 permit
|
||||
2a01:111:f403::/49 permit
|
||||
2a01:111:f403:c000::/51 permit
|
||||
2a01:111:f403:d000::/53 permit
|
||||
2a01:111:f403:f000::/52 permit
|
||||
2a01:238:20a:202:5370::1 permit
|
||||
2a01:238:20a:202:5372::1 permit
|
||||
@@ -27,9 +29,15 @@
|
||||
2a01:b747:3004:200::/56 permit
|
||||
2a01:b747:3005:200::/56 permit
|
||||
2a01:b747:3006:200::/56 permit
|
||||
2a01:b747:3007::/56 permit
|
||||
2a02:a60:0:5::/64 permit
|
||||
2a0f:f640::/56 permit
|
||||
2c0f:fb50:4000::/36 permit
|
||||
2c0f:fb50:4864::/56 permit
|
||||
2.207.151.32/27 permit
|
||||
2.207.151.53 permit
|
||||
2.207.217.30 permit
|
||||
2.207.223.160/27 permit
|
||||
3.64.237.68 permit
|
||||
3.65.3.180 permit
|
||||
3.70.123.177 permit
|
||||
@@ -50,12 +58,15 @@
|
||||
8.25.196.0/23 permit
|
||||
8.36.116.0/24 permit
|
||||
8.39.54.0/23 permit
|
||||
8.39.54.240/29 permit
|
||||
8.39.54.250/31 permit
|
||||
8.39.144.0/24 permit
|
||||
8.40.222.0/23 permit
|
||||
8.40.222.240/29 permit
|
||||
8.40.222.250/31 permit
|
||||
8.47.10.9 permit
|
||||
12.130.86.238 permit
|
||||
13.107.246.40 permit
|
||||
13.108.16.0/20 permit
|
||||
13.110.208.0/21 permit
|
||||
13.110.209.0/24 permit
|
||||
13.110.216.0/22 permit
|
||||
@@ -64,6 +75,7 @@
|
||||
13.111.191.0/24 permit
|
||||
13.216.7.111 permit
|
||||
13.216.54.180 permit
|
||||
13.247.164.219 permit
|
||||
15.200.21.50 permit
|
||||
15.200.44.248 permit
|
||||
15.200.201.185 permit
|
||||
@@ -174,6 +186,7 @@
|
||||
35.161.32.253 permit
|
||||
35.162.73.231 permit
|
||||
35.167.93.243 permit
|
||||
35.174.145.124 permit
|
||||
35.176.132.251 permit
|
||||
35.205.92.9 permit
|
||||
35.228.216.85 permit
|
||||
@@ -183,7 +196,6 @@
|
||||
37.218.249.47 permit
|
||||
37.218.251.62 permit
|
||||
39.156.163.64/29 permit
|
||||
40.90.65.81 permit
|
||||
40.92.0.0/15 permit
|
||||
40.92.0.0/16 permit
|
||||
40.107.0.0/16 permit
|
||||
@@ -271,9 +283,7 @@
|
||||
50.56.130.221 permit
|
||||
50.56.130.222 permit
|
||||
50.112.246.219 permit
|
||||
51.77.79.158 permit
|
||||
51.83.17.38 permit
|
||||
51.89.119.103 permit
|
||||
52.1.14.157 permit
|
||||
52.5.230.59 permit
|
||||
52.6.74.205 permit
|
||||
@@ -300,15 +310,6 @@
|
||||
52.94.124.0/28 permit
|
||||
52.95.48.152/29 permit
|
||||
52.95.49.88/29 permit
|
||||
52.96.91.34 permit
|
||||
52.96.111.82 permit
|
||||
52.96.172.98 permit
|
||||
52.96.214.50 permit
|
||||
52.96.222.194 permit
|
||||
52.96.222.226 permit
|
||||
52.96.223.2 permit
|
||||
52.96.228.130 permit
|
||||
52.96.229.242 permit
|
||||
52.100.0.0/15 permit
|
||||
52.102.0.0/16 permit
|
||||
52.103.0.0/17 permit
|
||||
@@ -324,8 +325,6 @@
|
||||
52.234.172.96/28 permit
|
||||
52.235.253.128 permit
|
||||
52.236.28.240/28 permit
|
||||
54.36.149.183 permit
|
||||
54.38.221.122 permit
|
||||
54.90.148.255 permit
|
||||
54.165.19.38 permit
|
||||
54.174.52.0/24 permit
|
||||
@@ -376,6 +375,7 @@
|
||||
64.132.88.0/23 permit
|
||||
64.132.92.0/24 permit
|
||||
64.181.194.190 permit
|
||||
64.181.213.254 permit
|
||||
64.207.219.7 permit
|
||||
64.207.219.8 permit
|
||||
64.207.219.9 permit
|
||||
@@ -405,27 +405,8 @@
|
||||
64.207.219.143 permit
|
||||
64.233.160.0/19 permit
|
||||
65.52.80.137 permit
|
||||
65.54.51.64/26 permit
|
||||
65.54.61.64/26 permit
|
||||
65.54.121.120/29 permit
|
||||
65.54.190.0/24 permit
|
||||
65.54.241.0/24 permit
|
||||
65.55.29.77 permit
|
||||
65.55.33.64/28 permit
|
||||
65.55.34.0/24 permit
|
||||
65.55.42.224/28 permit
|
||||
65.55.52.224/27 permit
|
||||
65.55.78.128/25 permit
|
||||
65.55.81.48/28 permit
|
||||
65.55.90.0/24 permit
|
||||
65.55.94.0/25 permit
|
||||
65.55.111.0/24 permit
|
||||
65.55.113.64/26 permit
|
||||
65.55.116.0/25 permit
|
||||
65.55.126.0/25 permit
|
||||
65.55.174.0/25 permit
|
||||
65.55.178.128/27 permit
|
||||
65.55.234.192/26 permit
|
||||
65.110.161.77 permit
|
||||
65.123.29.213 permit
|
||||
65.123.29.220 permit
|
||||
@@ -480,7 +461,11 @@
|
||||
66.218.75.252/31 permit
|
||||
66.218.75.254 permit
|
||||
66.220.144.128/25 permit
|
||||
66.220.144.178 permit
|
||||
66.220.144.179 permit
|
||||
66.220.155.0/24 permit
|
||||
66.220.155.178 permit
|
||||
66.220.155.179 permit
|
||||
66.220.157.0/25 permit
|
||||
66.231.80.0/20 permit
|
||||
66.240.227.0/24 permit
|
||||
@@ -545,8 +530,9 @@
|
||||
69.162.98.0/24 permit
|
||||
69.169.224.0/20 permit
|
||||
69.171.232.0/24 permit
|
||||
69.171.232.180 permit
|
||||
69.171.232.181 permit
|
||||
69.171.244.0/23 permit
|
||||
70.37.151.128/25 permit
|
||||
70.42.149.35 permit
|
||||
72.3.185.0/24 permit
|
||||
72.14.192.0/18 permit
|
||||
@@ -643,7 +629,6 @@
|
||||
74.208.4.220 permit
|
||||
74.208.4.221 permit
|
||||
74.209.250.0/24 permit
|
||||
75.2.70.75 permit
|
||||
76.223.128.0/19 permit
|
||||
76.223.176.0/20 permit
|
||||
77.238.176.0/24 permit
|
||||
@@ -668,6 +653,7 @@
|
||||
77.238.189.148/30 permit
|
||||
79.135.106.0/24 permit
|
||||
79.135.107.0/24 permit
|
||||
80.225.160.128/25 permit
|
||||
81.169.146.243 permit
|
||||
81.169.146.245 permit
|
||||
81.169.146.246 permit
|
||||
@@ -686,6 +672,11 @@
|
||||
82.165.159.45 permit
|
||||
82.165.159.130 permit
|
||||
82.165.159.131 permit
|
||||
84.8.68.0/25 permit
|
||||
84.8.192.128/25 permit
|
||||
84.8.224.128/25 permit
|
||||
85.9.206.169 permit
|
||||
85.9.210.45 permit
|
||||
85.158.136.0/21 permit
|
||||
85.215.255.39 permit
|
||||
85.215.255.40 permit
|
||||
@@ -734,11 +725,11 @@
|
||||
87.248.117.205 permit
|
||||
87.253.232.0/21 permit
|
||||
89.22.108.0/24 permit
|
||||
91.198.2.0/24 permit
|
||||
91.198.2.177 permit
|
||||
91.198.2.217 permit
|
||||
91.198.2.222 permit
|
||||
91.211.240.0/22 permit
|
||||
94.236.119.0/26 permit
|
||||
94.245.112.0/27 permit
|
||||
94.245.112.10/31 permit
|
||||
95.131.104.0/21 permit
|
||||
95.217.114.154 permit
|
||||
96.43.144.0/20 permit
|
||||
@@ -1231,19 +1222,17 @@
|
||||
98.139.245.208/30 permit
|
||||
98.139.245.212/31 permit
|
||||
99.78.197.208/28 permit
|
||||
99.83.190.102 permit
|
||||
103.9.96.0/22 permit
|
||||
103.28.42.0/24 permit
|
||||
103.122.78.238 permit
|
||||
103.84.217.15 permit
|
||||
103.84.217.238 permit
|
||||
103.89.75.238 permit
|
||||
103.151.192.0/23 permit
|
||||
103.168.172.128/27 permit
|
||||
103.237.104.0/22 permit
|
||||
104.43.243.237 permit
|
||||
104.44.112.128/25 permit
|
||||
104.47.0.0/17 permit
|
||||
104.47.20.0/23 permit
|
||||
104.47.75.0/24 permit
|
||||
104.47.108.0/23 permit
|
||||
104.130.96.0/28 permit
|
||||
104.130.122.0/23 permit
|
||||
106.10.144.64/27 permit
|
||||
@@ -1378,15 +1367,9 @@
|
||||
108.174.6.215 permit
|
||||
108.175.18.45 permit
|
||||
108.175.30.45 permit
|
||||
108.177.96.0/20 permit
|
||||
108.179.144.0/20 permit
|
||||
109.224.244.0/24 permit
|
||||
109.237.142.0/24 permit
|
||||
111.221.23.128/25 permit
|
||||
111.221.26.0/27 permit
|
||||
111.221.66.0/25 permit
|
||||
111.221.69.128/25 permit
|
||||
111.221.112.0/21 permit
|
||||
112.19.199.64/29 permit
|
||||
112.19.242.64/29 permit
|
||||
116.214.12.47 permit
|
||||
@@ -1404,6 +1387,7 @@
|
||||
117.120.16.0/21 permit
|
||||
119.42.242.52/31 permit
|
||||
119.42.242.156 permit
|
||||
121.244.91.31 permit
|
||||
121.244.91.48 permit
|
||||
121.244.91.52 permit
|
||||
122.15.156.182 permit
|
||||
@@ -1434,6 +1418,7 @@
|
||||
128.245.248.0/21 permit
|
||||
129.41.77.70 permit
|
||||
129.41.169.249 permit
|
||||
129.77.16.0/20 permit
|
||||
129.80.5.164 permit
|
||||
129.80.64.36 permit
|
||||
129.80.67.121 permit
|
||||
@@ -1442,6 +1427,20 @@
|
||||
129.146.88.28 permit
|
||||
129.146.147.105 permit
|
||||
129.146.236.58 permit
|
||||
129.148.135.0/25 permit
|
||||
129.148.148.0/25 permit
|
||||
129.148.164.0/25 permit
|
||||
129.148.180.0/25 permit
|
||||
129.148.215.0/25 permit
|
||||
129.149.6.0/25 permit
|
||||
129.149.22.0/25 permit
|
||||
129.149.38.0/25 permit
|
||||
129.149.52.0/25 permit
|
||||
129.149.68.0/25 permit
|
||||
129.149.84.0/25 permit
|
||||
129.149.100.0/25 permit
|
||||
129.149.118.0/25 permit
|
||||
129.149.126.0/25 permit
|
||||
129.151.67.221 permit
|
||||
129.153.62.216 permit
|
||||
129.153.104.71 permit
|
||||
@@ -1450,19 +1449,23 @@
|
||||
129.153.194.228 permit
|
||||
129.154.255.129 permit
|
||||
129.158.56.255 permit
|
||||
129.158.62.153 permit
|
||||
129.159.22.159 permit
|
||||
129.159.87.137 permit
|
||||
129.213.195.191 permit
|
||||
130.35.116.0/25 permit
|
||||
130.61.9.72 permit
|
||||
130.162.39.83 permit
|
||||
130.248.172.0/24 permit
|
||||
130.248.173.0/24 permit
|
||||
131.186.12.0/25 permit
|
||||
131.253.30.0/24 permit
|
||||
131.253.121.0/26 permit
|
||||
132.145.13.209 permit
|
||||
132.226.26.225 permit
|
||||
132.226.49.32 permit
|
||||
132.226.56.24 permit
|
||||
134.98.248.128/25 permit
|
||||
134.128.64.0/19 permit
|
||||
134.128.96.0/19 permit
|
||||
134.170.27.8 permit
|
||||
@@ -1485,6 +1488,7 @@
|
||||
136.143.184.0/24 permit
|
||||
136.143.188.0/24 permit
|
||||
136.143.190.0/23 permit
|
||||
136.146.128.0/20 permit
|
||||
136.147.128.0/20 permit
|
||||
136.147.135.0/24 permit
|
||||
136.147.176.0/20 permit
|
||||
@@ -1492,6 +1496,10 @@
|
||||
136.147.182.0/24 permit
|
||||
136.147.224.0/20 permit
|
||||
136.179.50.206 permit
|
||||
136.248.224.128/25 permit
|
||||
136.248.232.128/25 permit
|
||||
138.1.108.0/25 permit
|
||||
138.1.170.0/24 permit
|
||||
139.60.152.0/22 permit
|
||||
139.138.35.44 permit
|
||||
139.138.46.121 permit
|
||||
@@ -1500,8 +1508,11 @@
|
||||
139.138.57.55 permit
|
||||
139.138.58.119 permit
|
||||
139.167.79.86 permit
|
||||
139.177.108.0/25 permit
|
||||
139.180.17.0/24 permit
|
||||
140.238.148.191 permit
|
||||
141.148.55.217 permit
|
||||
141.148.91.244 permit
|
||||
141.148.159.229 permit
|
||||
141.193.32.0/23 permit
|
||||
141.193.184.32/27 permit
|
||||
@@ -1536,6 +1547,9 @@
|
||||
146.88.28.0/24 permit
|
||||
146.148.116.76 permit
|
||||
147.154.32.0/25 permit
|
||||
147.154.63.0/24 permit
|
||||
147.154.126.0/24 permit
|
||||
147.154.191.0/24 permit
|
||||
147.243.1.47 permit
|
||||
147.243.1.48 permit
|
||||
147.243.1.153 permit
|
||||
@@ -1543,39 +1557,38 @@
|
||||
147.243.128.26 permit
|
||||
148.105.0.0/16 permit
|
||||
148.105.8.0/21 permit
|
||||
148.116.32.128/25 permit
|
||||
149.72.0.0/16 permit
|
||||
149.72.234.184 permit
|
||||
149.72.248.236 permit
|
||||
149.97.173.180 permit
|
||||
149.118.160.128/25 permit
|
||||
150.136.21.199 permit
|
||||
150.171.109.72 permit
|
||||
150.230.98.160 permit
|
||||
151.145.38.14 permit
|
||||
152.67.105.195 permit
|
||||
152.69.200.236 permit
|
||||
152.70.155.126 permit
|
||||
155.248.135.128/25 permit
|
||||
155.248.140.0/25 permit
|
||||
155.248.148.0/25 permit
|
||||
155.248.208.51 permit
|
||||
155.248.220.138 permit
|
||||
155.248.234.149 permit
|
||||
155.248.237.141 permit
|
||||
157.55.0.192/26 permit
|
||||
157.55.1.128/26 permit
|
||||
157.55.2.0/25 permit
|
||||
157.55.9.128/25 permit
|
||||
157.55.11.0/25 permit
|
||||
157.55.49.0/25 permit
|
||||
157.55.61.0/24 permit
|
||||
157.55.157.128/25 permit
|
||||
157.55.225.0/25 permit
|
||||
157.56.24.0/25 permit
|
||||
157.56.120.128/26 permit
|
||||
157.56.232.0/21 permit
|
||||
157.56.240.0/20 permit
|
||||
157.56.248.0/21 permit
|
||||
157.58.30.128/25 permit
|
||||
157.58.196.96/29 permit
|
||||
157.58.249.3 permit
|
||||
157.137.32.128/25 permit
|
||||
157.137.96.128/25 permit
|
||||
157.151.208.65 permit
|
||||
157.255.1.64/29 permit
|
||||
158.101.211.207 permit
|
||||
158.247.16.0/20 permit
|
||||
158.247.100.0/25 permit
|
||||
159.13.4.0/25 permit
|
||||
159.92.154.0/24 permit
|
||||
159.92.155.0/24 permit
|
||||
159.92.157.0/24 permit
|
||||
@@ -1597,8 +1610,11 @@
|
||||
159.135.224.0/20 permit
|
||||
159.135.228.10 permit
|
||||
159.183.0.0/16 permit
|
||||
159.183.14.233 permit
|
||||
159.183.68.71 permit
|
||||
159.183.79.38 permit
|
||||
159.183.121.182 permit
|
||||
159.183.129.172 permit
|
||||
160.1.62.192 permit
|
||||
161.38.192.0/20 permit
|
||||
161.38.204.0/22 permit
|
||||
@@ -1606,6 +1622,7 @@
|
||||
161.71.64.0/20 permit
|
||||
162.88.4.0/23 permit
|
||||
162.88.8.0/24 permit
|
||||
162.88.24.0/23 permit
|
||||
162.88.24.0/24 permit
|
||||
162.88.25.0/24 permit
|
||||
162.88.36.0/24 permit
|
||||
@@ -1616,12 +1633,20 @@
|
||||
163.114.134.16 permit
|
||||
163.114.135.16 permit
|
||||
163.116.128.0/17 permit
|
||||
163.192.116.87 permit
|
||||
163.192.125.176 permit
|
||||
163.192.196.146 permit
|
||||
163.192.204.161 permit
|
||||
164.152.23.32 permit
|
||||
164.152.25.241 permit
|
||||
164.177.132.168/30 permit
|
||||
165.1.100.0/25 permit
|
||||
165.173.128.0/24 permit
|
||||
165.173.180.0/24 permit
|
||||
165.173.180.250/31 permit
|
||||
165.173.182.0/24 permit
|
||||
165.173.182.250/31 permit
|
||||
165.173.189.205 permit
|
||||
166.78.68.0/22 permit
|
||||
166.78.68.221 permit
|
||||
166.78.69.169 permit
|
||||
@@ -1640,9 +1665,12 @@
|
||||
167.89.75.126 permit
|
||||
167.89.75.136 permit
|
||||
167.89.75.164 permit
|
||||
167.89.101.2 permit
|
||||
167.89.101.192/28 permit
|
||||
167.220.67.232/29 permit
|
||||
168.107.248.128/25 permit
|
||||
168.110.160.128/25 permit
|
||||
168.110.248.128/25 permit
|
||||
168.129.184.128/25 permit
|
||||
168.129.248.128/25 permit
|
||||
168.138.5.36 permit
|
||||
168.138.73.51 permit
|
||||
168.138.77.31 permit
|
||||
@@ -1655,24 +1683,21 @@
|
||||
169.148.131.0/24 permit
|
||||
169.148.138.0/24 permit
|
||||
169.148.142.10 permit
|
||||
169.148.142.33 permit
|
||||
169.148.144.0/25 permit
|
||||
169.148.144.10 permit
|
||||
169.148.146.0/23 permit
|
||||
169.148.174.10 permit
|
||||
169.148.175.3 permit
|
||||
169.148.179.3 permit
|
||||
169.148.188.0/24 permit
|
||||
169.148.188.182 permit
|
||||
170.9.232.254 permit
|
||||
170.10.128.0/24 permit
|
||||
170.10.129.0/24 permit
|
||||
170.10.132.56/29 permit
|
||||
170.10.132.64/29 permit
|
||||
170.10.133.0/24 permit
|
||||
172.217.0.0/20 permit
|
||||
172.217.32.0/20 permit
|
||||
172.217.128.0/19 permit
|
||||
172.217.160.0/20 permit
|
||||
172.217.192.0/19 permit
|
||||
172.253.56.0/21 permit
|
||||
172.253.112.0/20 permit
|
||||
173.0.84.0/29 permit
|
||||
173.0.84.224/27 permit
|
||||
173.0.94.244/30 permit
|
||||
@@ -1681,6 +1706,7 @@
|
||||
173.203.81.39 permit
|
||||
173.224.161.128/25 permit
|
||||
173.224.165.0/26 permit
|
||||
173.224.166.48/28 permit
|
||||
174.36.84.8/29 permit
|
||||
174.36.84.16/29 permit
|
||||
174.36.84.32/29 permit
|
||||
@@ -1700,8 +1726,7 @@
|
||||
182.50.78.64/28 permit
|
||||
183.240.219.64/29 permit
|
||||
185.4.120.0/22 permit
|
||||
185.11.253.128/27 permit
|
||||
185.11.255.0/24 permit
|
||||
185.11.255.144 permit
|
||||
185.12.80.0/22 permit
|
||||
185.28.196.0/22 permit
|
||||
185.58.84.93 permit
|
||||
@@ -1715,8 +1740,16 @@
|
||||
185.138.56.128/25 permit
|
||||
185.189.236.0/22 permit
|
||||
185.211.120.0/22 permit
|
||||
185.233.188.0/23 permit
|
||||
185.233.190.0/23 permit
|
||||
185.233.188.68 permit
|
||||
185.233.188.75 permit
|
||||
185.233.188.84 permit
|
||||
185.233.188.160 permit
|
||||
185.233.188.176 permit
|
||||
185.233.188.247 permit
|
||||
185.233.189.44 permit
|
||||
185.233.189.98 permit
|
||||
185.233.189.122 permit
|
||||
185.233.189.228 permit
|
||||
185.250.236.0/22 permit
|
||||
185.250.239.148 permit
|
||||
185.250.239.168 permit
|
||||
@@ -1775,8 +1808,24 @@
|
||||
192.18.139.154 permit
|
||||
192.18.145.36 permit
|
||||
192.18.152.58 permit
|
||||
192.22.32.128/25 permit
|
||||
192.22.96.128/25 permit
|
||||
192.22.160.128/25 permit
|
||||
192.22.224.128/25 permit
|
||||
192.28.128.0/18 permit
|
||||
192.29.24.0/25 permit
|
||||
192.29.44.0/25 permit
|
||||
192.29.72.0/25 permit
|
||||
192.29.88.0/25 permit
|
||||
192.29.103.128/25 permit
|
||||
192.29.134.0/25 permit
|
||||
192.29.151.128/25 permit
|
||||
192.29.172.0/25 permit
|
||||
192.29.178.0/25 permit
|
||||
192.29.200.0/25 permit
|
||||
192.29.216.0/25 permit
|
||||
192.29.232.0/25 permit
|
||||
192.29.248.0/25 permit
|
||||
192.30.252.0/22 permit
|
||||
192.161.144.0/20 permit
|
||||
192.162.87.0/24 permit
|
||||
@@ -1784,15 +1833,12 @@
|
||||
192.237.159.42 permit
|
||||
192.237.159.43 permit
|
||||
192.254.112.0/20 permit
|
||||
192.254.112.60 permit
|
||||
192.254.112.98/31 permit
|
||||
192.254.113.10 permit
|
||||
192.254.113.101 permit
|
||||
192.254.114.176 permit
|
||||
193.109.254.0/23 permit
|
||||
193.122.128.100 permit
|
||||
193.123.56.63 permit
|
||||
193.142.157.0/24 permit
|
||||
193.142.157.15 permit
|
||||
193.142.157.125 permit
|
||||
193.142.157.158 permit
|
||||
193.142.157.191 permit
|
||||
193.142.157.198 permit
|
||||
194.19.134.0/25 permit
|
||||
@@ -1816,6 +1862,7 @@
|
||||
194.97.212.12 permit
|
||||
194.106.220.0/23 permit
|
||||
194.113.24.0/22 permit
|
||||
194.113.42.0/26 permit
|
||||
194.154.193.192/27 permit
|
||||
195.4.92.0/23 permit
|
||||
195.54.172.0/23 permit
|
||||
@@ -1829,6 +1876,7 @@
|
||||
198.61.254.21 permit
|
||||
198.61.254.231 permit
|
||||
198.178.234.57 permit
|
||||
198.202.211.1 permit
|
||||
198.244.48.0/20 permit
|
||||
198.244.56.107 permit
|
||||
198.244.56.108 permit
|
||||
@@ -1912,7 +1960,6 @@
|
||||
204.14.232.64/28 permit
|
||||
204.14.234.64/28 permit
|
||||
204.75.142.0/24 permit
|
||||
204.79.197.212 permit
|
||||
204.92.114.187 permit
|
||||
204.92.114.203 permit
|
||||
204.92.114.204/31 permit
|
||||
@@ -1940,24 +1987,13 @@
|
||||
206.165.246.80/29 permit
|
||||
206.191.224.0/19 permit
|
||||
206.246.157.1 permit
|
||||
207.46.4.128/25 permit
|
||||
207.46.22.35 permit
|
||||
207.46.50.72 permit
|
||||
207.46.50.82 permit
|
||||
207.46.50.192/26 permit
|
||||
207.46.50.224 permit
|
||||
207.46.52.71 permit
|
||||
207.46.52.79 permit
|
||||
207.46.58.128/25 permit
|
||||
207.46.116.128/29 permit
|
||||
207.46.117.0/24 permit
|
||||
207.46.132.128/27 permit
|
||||
207.46.198.0/25 permit
|
||||
207.46.200.0/27 permit
|
||||
207.67.38.0/24 permit
|
||||
207.67.98.192/27 permit
|
||||
207.68.176.0/26 permit
|
||||
207.68.176.96/27 permit
|
||||
207.97.204.96/29 permit
|
||||
207.126.144.0/20 permit
|
||||
207.171.160.0/19 permit
|
||||
@@ -1965,6 +2001,7 @@
|
||||
207.211.30.128/25 permit
|
||||
207.211.31.0/25 permit
|
||||
207.211.41.113 permit
|
||||
207.211.132.0/25 permit
|
||||
207.218.90.0/24 permit
|
||||
207.218.90.122 permit
|
||||
207.250.68.0/24 permit
|
||||
@@ -1972,6 +2009,8 @@
|
||||
208.43.21.28/30 permit
|
||||
208.43.21.64/29 permit
|
||||
208.43.21.72/30 permit
|
||||
208.56.9.224 permit
|
||||
208.56.13.196 permit
|
||||
208.64.132.0/22 permit
|
||||
208.71.40.63 permit
|
||||
208.71.40.64/31 permit
|
||||
@@ -1998,6 +2037,7 @@
|
||||
208.71.42.214 permit
|
||||
208.72.249.240/29 permit
|
||||
208.75.120.0/22 permit
|
||||
208.76.62.0/23 permit
|
||||
208.76.62.0/24 permit
|
||||
208.76.63.0/24 permit
|
||||
208.82.237.96/29 permit
|
||||
@@ -2106,14 +2146,10 @@
|
||||
212.227.126.225 permit
|
||||
212.227.126.226 permit
|
||||
212.227.126.227 permit
|
||||
213.95.19.64/27 permit
|
||||
213.95.135.4 permit
|
||||
213.199.128.139 permit
|
||||
213.199.128.145 permit
|
||||
213.199.138.181 permit
|
||||
213.199.138.191 permit
|
||||
213.199.161.128/27 permit
|
||||
213.199.177.0/26 permit
|
||||
216.17.150.242 permit
|
||||
216.17.150.251 permit
|
||||
216.24.224.0/20 permit
|
||||
@@ -2141,7 +2177,6 @@
|
||||
216.39.62.60/31 permit
|
||||
216.39.62.136/29 permit
|
||||
216.39.62.144/31 permit
|
||||
216.58.192.0/19 permit
|
||||
216.66.217.240/29 permit
|
||||
216.71.138.33 permit
|
||||
216.71.152.207 permit
|
||||
@@ -2163,6 +2198,7 @@
|
||||
216.136.168.80/28 permit
|
||||
216.139.64.0/19 permit
|
||||
216.145.221.0/24 permit
|
||||
216.146.32.0/23 permit
|
||||
216.146.32.0/24 permit
|
||||
216.146.33.0/24 permit
|
||||
216.198.0.0/18 permit
|
||||
@@ -2183,6 +2219,7 @@
|
||||
223.165.120.0/23 permit
|
||||
2001:0868:0100:0600::/64 permit
|
||||
2001:4860:4000::/36 permit
|
||||
2001:4860:4864::/56 permit
|
||||
2001:748:100:40::2:0/112 permit
|
||||
2001:748:400:1300::3 permit
|
||||
2001:748:400:1300::4 permit
|
||||
@@ -2200,26 +2237,25 @@
|
||||
2001:748:400:3301::3 permit
|
||||
2001:748:400:3301::4 permit
|
||||
2404:6800:4000::/36 permit
|
||||
2603:1010:3:3::5b permit
|
||||
2603:1020:201:10::10f permit
|
||||
2603:1030:20e:3::23c permit
|
||||
2603:1030:b:3::152 permit
|
||||
2603:1030:c02:8::14 permit
|
||||
2404:6800:4864::/56 permit
|
||||
2603:1061:14:75::1 permit
|
||||
2607:13c0:0001:0000:0000:0000:0000:7000/116 permit
|
||||
2607:13c0:0002:0000:0000:0000:0000:1000/116 permit
|
||||
2607:13c0:0004:0000:0000:0000:0000:0000/116 permit
|
||||
2607:f8b0:4000::/36 permit
|
||||
2620:109:c003:104::215 permit
|
||||
2607:f8b0:4864::/56 permit
|
||||
2620:109:c003:104::/64 permit
|
||||
2620:109:c006:104::215 permit
|
||||
2620:109:c003:104::215 permit
|
||||
2620:109:c006:104::/64 permit
|
||||
2620:109:c006:104::215 permit
|
||||
2620:109:c00d:104::/64 permit
|
||||
2620:10d:c090:400::8:1 permit
|
||||
2620:10d:c091:400::8:1 permit
|
||||
2620:10d:c09b:400::8:1 permit
|
||||
2620:10d:c09c:400::8:1 permit
|
||||
2620:119:50c0:207::215 permit
|
||||
2620:119:50c0:207::/64 permit
|
||||
2620:119:50c0:207::215 permit
|
||||
2800:3f0:4000::/36 permit
|
||||
2800:3f0:4864::/56 permit
|
||||
49.12.4.251 permit # checks.mailcow.email
|
||||
2a01:4f8:c17:7906::10 permit # checks.mailcow.email
|
||||
|
||||
@@ -133,7 +133,7 @@ try {
|
||||
error_log("ALIAS EXPANDER: http pipe: goto address " . $goto . " is an alias branch for " . $goto_branch . PHP_EOL);
|
||||
$goto_branch_array = explode(',', $goto_branch);
|
||||
} else {
|
||||
$stmt = $pdo->prepare("SELECT `target_domain` FROM `alias_domain` WHERE `alias_domain` = :domain AND `active` AND '1'");
|
||||
$stmt = $pdo->prepare("SELECT `target_domain` FROM `alias_domain` WHERE `alias_domain` = :domain AND `active` = '1'");
|
||||
$stmt->execute(array(':domain' => $parsed_goto['domain']));
|
||||
$goto_branch = $stmt->fetch(PDO::FETCH_ASSOC)['target_domain'];
|
||||
if ($goto_branch) {
|
||||
|
||||
@@ -3,8 +3,7 @@ rules {
|
||||
backend = "http";
|
||||
url = "http://nginx:9081/pipe.php";
|
||||
selector = "reject_no_global_bl";
|
||||
formatter = "default";
|
||||
meta_headers = true;
|
||||
formatter = "multipart";
|
||||
}
|
||||
RLINFO {
|
||||
backend = "http";
|
||||
@@ -16,8 +15,7 @@ rules {
|
||||
backend = "http";
|
||||
url = "http://nginx:9081/pushover.php";
|
||||
selector = "mailcow_rcpt";
|
||||
formatter = "json";
|
||||
meta_headers = true;
|
||||
formatter = "multipart";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -146,8 +146,171 @@ rspamd_config:register_symbol({
|
||||
return false
|
||||
end
|
||||
|
||||
-- Helper function to parse IPv6 into 8 segments
|
||||
local function ipv6_to_segments(ip_str)
|
||||
-- Remove zone identifier if present (e.g., %eth0)
|
||||
ip_str = ip_str:gsub("%%.*$", "")
|
||||
|
||||
local segments = {}
|
||||
|
||||
-- Handle :: compression
|
||||
if ip_str:find('::') then
|
||||
local before, after = ip_str:match('^(.*)::(.*)$')
|
||||
before = before or ''
|
||||
after = after or ''
|
||||
|
||||
local before_parts = {}
|
||||
local after_parts = {}
|
||||
|
||||
if before ~= '' then
|
||||
for seg in before:gmatch('[^:]+') do
|
||||
table.insert(before_parts, tonumber(seg, 16) or 0)
|
||||
end
|
||||
end
|
||||
|
||||
if after ~= '' then
|
||||
for seg in after:gmatch('[^:]+') do
|
||||
table.insert(after_parts, tonumber(seg, 16) or 0)
|
||||
end
|
||||
end
|
||||
|
||||
-- Add before segments
|
||||
for _, seg in ipairs(before_parts) do
|
||||
table.insert(segments, seg)
|
||||
end
|
||||
|
||||
-- Add compressed zeros
|
||||
local zeros_needed = 8 - #before_parts - #after_parts
|
||||
for i = 1, zeros_needed do
|
||||
table.insert(segments, 0)
|
||||
end
|
||||
|
||||
-- Add after segments
|
||||
for _, seg in ipairs(after_parts) do
|
||||
table.insert(segments, seg)
|
||||
end
|
||||
else
|
||||
-- No compression
|
||||
for seg in ip_str:gmatch('[^:]+') do
|
||||
table.insert(segments, tonumber(seg, 16) or 0)
|
||||
end
|
||||
end
|
||||
|
||||
-- Ensure we have exactly 8 segments
|
||||
while #segments < 8 do
|
||||
table.insert(segments, 0)
|
||||
end
|
||||
|
||||
return segments
|
||||
end
|
||||
|
||||
-- Generate all common IPv6 notations
|
||||
local function get_ipv6_variants(ip_str)
|
||||
local variants = {}
|
||||
local seen = {}
|
||||
|
||||
local function add_variant(v)
|
||||
if v and not seen[v] then
|
||||
table.insert(variants, v)
|
||||
seen[v] = true
|
||||
end
|
||||
end
|
||||
|
||||
-- For IPv4, just return the original
|
||||
if not ip_str:find(':') then
|
||||
add_variant(ip_str)
|
||||
return variants
|
||||
end
|
||||
|
||||
local segments = ipv6_to_segments(ip_str)
|
||||
|
||||
-- 1. Fully expanded form (all zeros shown as 0000)
|
||||
local expanded_parts = {}
|
||||
for _, seg in ipairs(segments) do
|
||||
table.insert(expanded_parts, string.format('%04x', seg))
|
||||
end
|
||||
add_variant(table.concat(expanded_parts, ':'))
|
||||
|
||||
-- 2. Standard form (no leading zeros, but all segments present)
|
||||
local standard_parts = {}
|
||||
for _, seg in ipairs(segments) do
|
||||
table.insert(standard_parts, string.format('%x', seg))
|
||||
end
|
||||
add_variant(table.concat(standard_parts, ':'))
|
||||
|
||||
-- 3. Find all possible :: compressions
|
||||
-- RFC 5952: compress the longest run of consecutive zeros
|
||||
-- But we need to check all possibilities since Redis might have any form
|
||||
|
||||
-- Find all zero runs
|
||||
local zero_runs = {}
|
||||
local in_run = false
|
||||
local run_start = 0
|
||||
local run_length = 0
|
||||
|
||||
for i = 1, 8 do
|
||||
if segments[i] == 0 then
|
||||
if not in_run then
|
||||
in_run = true
|
||||
run_start = i
|
||||
run_length = 1
|
||||
else
|
||||
run_length = run_length + 1
|
||||
end
|
||||
else
|
||||
if in_run then
|
||||
if run_length >= 1 then -- Allow single zero compression too
|
||||
table.insert(zero_runs, {start = run_start, length = run_length})
|
||||
end
|
||||
in_run = false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Don't forget the last run
|
||||
if in_run and run_length >= 1 then
|
||||
table.insert(zero_runs, {start = run_start, length = run_length})
|
||||
end
|
||||
|
||||
-- Generate variant for each zero run compression
|
||||
for _, run in ipairs(zero_runs) do
|
||||
local parts = {}
|
||||
|
||||
-- Before compression
|
||||
for i = 1, run.start - 1 do
|
||||
table.insert(parts, string.format('%x', segments[i]))
|
||||
end
|
||||
|
||||
-- The compression
|
||||
if run.start == 1 then
|
||||
table.insert(parts, '')
|
||||
table.insert(parts, '')
|
||||
elseif run.start + run.length - 1 == 8 then
|
||||
table.insert(parts, '')
|
||||
table.insert(parts, '')
|
||||
else
|
||||
table.insert(parts, '')
|
||||
end
|
||||
|
||||
-- After compression
|
||||
for i = run.start + run.length, 8 do
|
||||
table.insert(parts, string.format('%x', segments[i]))
|
||||
end
|
||||
|
||||
local compressed = table.concat(parts, ':'):gsub('::+', '::')
|
||||
add_variant(compressed)
|
||||
end
|
||||
|
||||
return variants
|
||||
end
|
||||
|
||||
local from_ip_string = tostring(ip)
|
||||
ip_check_table = {from_ip_string}
|
||||
local ip_check_table = {}
|
||||
|
||||
-- Add all variants of the exact IP
|
||||
for _, variant in ipairs(get_ipv6_variants(from_ip_string)) do
|
||||
table.insert(ip_check_table, variant)
|
||||
end
|
||||
|
||||
local maxbits = 128
|
||||
local minbits = 32
|
||||
@@ -155,10 +318,18 @@ rspamd_config:register_symbol({
|
||||
maxbits = 32
|
||||
minbits = 8
|
||||
end
|
||||
|
||||
-- Add all CIDR notations with variants
|
||||
for i=maxbits,minbits,-1 do
|
||||
local nip = ip:apply_mask(i):to_string() .. "/" .. i
|
||||
table.insert(ip_check_table, nip)
|
||||
local masked_ip = ip:apply_mask(i)
|
||||
local cidr_base = masked_ip:to_string()
|
||||
|
||||
for _, variant in ipairs(get_ipv6_variants(cidr_base)) do
|
||||
local cidr = variant .. "/" .. i
|
||||
table.insert(ip_check_table, cidr)
|
||||
end
|
||||
end
|
||||
|
||||
local function keep_spam_cb(err, data)
|
||||
if err then
|
||||
rspamd_logger.infox(rspamd_config, "keep_spam query request for ip %s returned invalid or empty data (\"%s\") or error (\"%s\")", ip, data, err)
|
||||
@@ -166,12 +337,15 @@ rspamd_config:register_symbol({
|
||||
else
|
||||
for k,v in pairs(data) do
|
||||
if (v and v ~= userdata and v == '1') then
|
||||
rspamd_logger.infox(rspamd_config, "found ip in keep_spam map, setting pre-result")
|
||||
rspamd_logger.infox(rspamd_config, "found ip %s (checked as: %s) in keep_spam map, setting pre-result accept", from_ip_string, ip_check_table[k])
|
||||
task:set_pre_result('accept', 'ip matched with forward hosts', 'keep_spam')
|
||||
task:set_flag('no_stat')
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
table.insert(ip_check_table, 1, 'KEEP_SPAM')
|
||||
local redis_ret_user = rspamd_redis_make_request(task,
|
||||
redis_params, -- connect params
|
||||
@@ -210,6 +384,7 @@ rspamd_config:register_symbol({
|
||||
rspamd_config:register_symbol({
|
||||
name = 'TAG_MOO',
|
||||
type = 'postfilter',
|
||||
flags = 'ignore_passthrough',
|
||||
callback = function(task)
|
||||
local util = require("rspamd_util")
|
||||
local rspamd_logger = require "rspamd_logger"
|
||||
@@ -217,9 +392,7 @@ rspamd_config:register_symbol({
|
||||
local rspamd_http = require "rspamd_http"
|
||||
local rcpts = task:get_recipients('smtp')
|
||||
local lua_util = require "lua_util"
|
||||
|
||||
local tagged_rcpt = task:get_symbol("TAGGED_RCPT")
|
||||
local mailcow_domain = task:get_symbol("RCPT_MAILCOW_DOMAIN")
|
||||
|
||||
local function remove_moo_tag()
|
||||
local moo_tag_header = task:get_header('X-Moo-Tag', false)
|
||||
@@ -231,101 +404,147 @@ rspamd_config:register_symbol({
|
||||
return true
|
||||
end
|
||||
|
||||
if tagged_rcpt and tagged_rcpt[1].options and mailcow_domain then
|
||||
local tag = tagged_rcpt[1].options[1]
|
||||
rspamd_logger.infox("found tag: %s", tag)
|
||||
local action = task:get_metric_action('default')
|
||||
rspamd_logger.infox("metric action now: %s", action)
|
||||
-- Check if we have exactly one recipient
|
||||
if not (rcpts and #rcpts == 1) then
|
||||
rspamd_logger.infox("TAG_MOO: not exactly one rcpt (%s), removing moo tag", rcpts and #rcpts or 0)
|
||||
remove_moo_tag()
|
||||
return
|
||||
end
|
||||
|
||||
if action ~= 'no action' and action ~= 'greylist' then
|
||||
rspamd_logger.infox("skipping tag handler for action: %s", action)
|
||||
remove_moo_tag()
|
||||
return true
|
||||
local rcpt_addr = rcpts[1]['addr']
|
||||
local rcpt_user = rcpts[1]['user']
|
||||
local rcpt_domain = rcpts[1]['domain']
|
||||
|
||||
-- Check if recipient has a tag (contains '+')
|
||||
local tag = nil
|
||||
if tagged_rcpt ~= nil then
|
||||
tag = tagged_rcpt
|
||||
rspamd_logger.infox("TAG_MOO: found tag in recipient: %s (base: %s, tag: %s)", rcpt_addr, base_user, tag)
|
||||
end
|
||||
|
||||
if not tag then
|
||||
rspamd_logger.infox("TAG_MOO: no tag found in recipient %s, removing moo tag", rcpt_addr)
|
||||
remove_moo_tag()
|
||||
return
|
||||
end
|
||||
|
||||
-- Optional: Check if domain is a mailcow domain
|
||||
-- When KEEP_SPAM is active, RCPT_MAILCOW_DOMAIN might not be set
|
||||
-- If the mail is being delivered, we can assume it's valid
|
||||
local mailcow_domain = task:get_symbol("RCPT_MAILCOW_DOMAIN")
|
||||
if not mailcow_domain then
|
||||
rspamd_logger.infox("TAG_MOO: RCPT_MAILCOW_DOMAIN not set (possibly due to pre-result), proceeding anyway for domain %s", rcpt_domain)
|
||||
end
|
||||
|
||||
local action = task:get_metric_action('default')
|
||||
rspamd_logger.infox("TAG_MOO: metric action: %s", action)
|
||||
|
||||
-- Check if we have a pre-result (e.g., from KEEP_SPAM or POSTMASTER_HANDLER)
|
||||
local allow_processing = false
|
||||
|
||||
if task.has_pre_result then
|
||||
local has_pre, pre_action = task:has_pre_result()
|
||||
if has_pre then
|
||||
rspamd_logger.infox("TAG_MOO: pre-result detected: %s", tostring(pre_action))
|
||||
if pre_action == 'accept' then
|
||||
allow_processing = true
|
||||
rspamd_logger.infox("TAG_MOO: pre-result is accept, will process")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function http_callback(err_message, code, body, headers)
|
||||
if body ~= nil and body ~= "" then
|
||||
rspamd_logger.infox(rspamd_config, "expanding rcpt to \"%s\"", body)
|
||||
-- Allow processing for mild actions or when we have pre-result accept
|
||||
if not allow_processing and action ~= 'no action' and action ~= 'greylist' then
|
||||
rspamd_logger.infox("TAG_MOO: skipping tag handler for action: %s", action)
|
||||
remove_moo_tag()
|
||||
return true
|
||||
end
|
||||
|
||||
local function tag_callback_subject(err, data)
|
||||
if err or type(data) ~= 'string' then
|
||||
rspamd_logger.infox(rspamd_config, "subject tag handler rcpt %s returned invalid or empty data (\"%s\") or error (\"%s\") - trying subfolder tag handler...", body, data, err)
|
||||
rspamd_logger.infox("TAG_MOO: processing allowed")
|
||||
|
||||
local function tag_callback_subfolder(err, data)
|
||||
if err or type(data) ~= 'string' then
|
||||
rspamd_logger.infox(rspamd_config, "subfolder tag handler for rcpt %s returned invalid or empty data (\"%s\") or error (\"%s\")", body, data, err)
|
||||
remove_moo_tag()
|
||||
else
|
||||
rspamd_logger.infox("Add X-Moo-Tag header")
|
||||
task:set_milter_reply({
|
||||
add_headers = {['X-Moo-Tag'] = 'YES'}
|
||||
})
|
||||
end
|
||||
end
|
||||
local function http_callback(err_message, code, body, headers)
|
||||
if body ~= nil and body ~= "" then
|
||||
rspamd_logger.infox(rspamd_config, "TAG_MOO: expanding rcpt to \"%s\"", body)
|
||||
|
||||
local redis_ret_subfolder = rspamd_redis_make_request(task,
|
||||
redis_params, -- connect params
|
||||
body, -- hash key
|
||||
false, -- is write
|
||||
tag_callback_subfolder, --callback
|
||||
'HGET', -- command
|
||||
{'RCPT_WANTS_SUBFOLDER_TAG', body} -- arguments
|
||||
)
|
||||
if not redis_ret_subfolder then
|
||||
rspamd_logger.infox(rspamd_config, "cannot make request to load tag handler for rcpt")
|
||||
local function tag_callback_subject(err, data)
|
||||
if err or type(data) ~= 'string' or data == '' then
|
||||
rspamd_logger.infox(rspamd_config, "TAG_MOO: subject tag handler rcpt %s returned invalid or empty data (\"%s\") or error (\"%s\") - trying subfolder tag handler...", body, data, err)
|
||||
|
||||
local function tag_callback_subfolder(err, data)
|
||||
if err or type(data) ~= 'string' or data == '' then
|
||||
rspamd_logger.infox(rspamd_config, "TAG_MOO: subfolder tag handler for rcpt %s returned invalid or empty data (\"%s\") or error (\"%s\")", body, data, err)
|
||||
remove_moo_tag()
|
||||
else
|
||||
rspamd_logger.infox("TAG_MOO: User wants subfolder tag, adding X-Moo-Tag header")
|
||||
task:set_milter_reply({
|
||||
add_headers = {['X-Moo-Tag'] = 'YES'}
|
||||
})
|
||||
end
|
||||
|
||||
else
|
||||
rspamd_logger.infox("user wants subject modified for tagged mail")
|
||||
local sbj = task:get_header('Subject')
|
||||
new_sbj = '=?UTF-8?B?' .. tostring(util.encode_base64('[' .. tag .. '] ' .. sbj)) .. '?='
|
||||
task:set_milter_reply({
|
||||
remove_headers = {
|
||||
['Subject'] = 1,
|
||||
['X-Moo-Tag'] = 0
|
||||
},
|
||||
add_headers = {['Subject'] = new_sbj}
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
local redis_ret_subject = rspamd_redis_make_request(task,
|
||||
redis_params, -- connect params
|
||||
body, -- hash key
|
||||
false, -- is write
|
||||
tag_callback_subject, --callback
|
||||
'HGET', -- command
|
||||
{'RCPT_WANTS_SUBJECT_TAG', body} -- arguments
|
||||
)
|
||||
if not redis_ret_subject then
|
||||
rspamd_logger.infox(rspamd_config, "cannot make request to load tag handler for rcpt")
|
||||
remove_moo_tag()
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
if rcpts and #rcpts == 1 then
|
||||
for _,rcpt in ipairs(rcpts) do
|
||||
local rcpt_split = rspamd_str_split(rcpt['addr'], '@')
|
||||
if #rcpt_split == 2 then
|
||||
if rcpt_split[1] == 'postmaster' then
|
||||
rspamd_logger.infox(rspamd_config, "not expanding postmaster alias")
|
||||
local redis_ret_subfolder = rspamd_redis_make_request(task,
|
||||
redis_params, -- connect params
|
||||
body, -- hash key
|
||||
false, -- is write
|
||||
tag_callback_subfolder, --callback
|
||||
'HGET', -- command
|
||||
{'RCPT_WANTS_SUBFOLDER_TAG', body} -- arguments
|
||||
)
|
||||
if not redis_ret_subfolder then
|
||||
rspamd_logger.infox(rspamd_config, "TAG_MOO: cannot make request to load tag handler for rcpt")
|
||||
remove_moo_tag()
|
||||
else
|
||||
rspamd_http.request({
|
||||
task=task,
|
||||
url='http://nginx:8081/aliasexp.php',
|
||||
body='',
|
||||
callback=http_callback,
|
||||
headers={Rcpt=rcpt['addr']},
|
||||
})
|
||||
end
|
||||
|
||||
else
|
||||
rspamd_logger.infox("TAG_MOO: user wants subject modified for tagged mail")
|
||||
local sbj = task:get_header('Subject') or ''
|
||||
local tag_value = tag[1] and tag[1].options and tag[1].options[1] or ''
|
||||
new_sbj = '=?UTF-8?B?' .. tostring(util.encode_base64('[' .. tag_value .. '] ' .. sbj)) .. '?='
|
||||
task:set_milter_reply({
|
||||
remove_headers = {
|
||||
['Subject'] = 1,
|
||||
['X-Moo-Tag'] = 0
|
||||
},
|
||||
add_headers = {['Subject'] = new_sbj}
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
local redis_ret_subject = rspamd_redis_make_request(task,
|
||||
redis_params, -- connect params
|
||||
body, -- hash key
|
||||
false, -- is write
|
||||
tag_callback_subject, --callback
|
||||
'HGET', -- command
|
||||
{'RCPT_WANTS_SUBJECT_TAG', body} -- arguments
|
||||
)
|
||||
if not redis_ret_subject then
|
||||
rspamd_logger.infox(rspamd_config, "TAG_MOO: cannot make request to load tag handler for rcpt")
|
||||
remove_moo_tag()
|
||||
end
|
||||
else
|
||||
rspamd_logger.infox("TAG_MOO: alias expansion returned empty body")
|
||||
remove_moo_tag()
|
||||
end
|
||||
end
|
||||
|
||||
local rcpt_split = rspamd_str_split(rcpt_addr, '@')
|
||||
if #rcpt_split == 2 then
|
||||
if rcpt_split[1]:match('^postmaster') then
|
||||
rspamd_logger.infox(rspamd_config, "TAG_MOO: not expanding postmaster alias")
|
||||
remove_moo_tag()
|
||||
else
|
||||
rspamd_logger.infox("TAG_MOO: requesting alias expansion for %s", rcpt_addr)
|
||||
rspamd_http.request({
|
||||
task=task,
|
||||
url='http://nginx:8081/aliasexp.php',
|
||||
body='',
|
||||
callback=http_callback,
|
||||
headers={Rcpt=rcpt_addr},
|
||||
})
|
||||
end
|
||||
else
|
||||
rspamd_logger.infox("TAG_MOO: invalid rcpt format")
|
||||
remove_moo_tag()
|
||||
end
|
||||
end,
|
||||
@@ -335,6 +554,7 @@ rspamd_config:register_symbol({
|
||||
rspamd_config:register_symbol({
|
||||
name = 'BCC',
|
||||
type = 'postfilter',
|
||||
flags = 'ignore_passthrough',
|
||||
callback = function(task)
|
||||
local util = require("rspamd_util")
|
||||
local rspamd_http = require "rspamd_http"
|
||||
@@ -363,11 +583,13 @@ rspamd_config:register_symbol({
|
||||
local email_content = tostring(task:get_content())
|
||||
email_content = string.gsub(email_content, "\r\n%.", "\r\n..")
|
||||
-- send mail
|
||||
local from_smtp = task:get_from('smtp')
|
||||
local from_addr = (from_smtp and from_smtp[1] and from_smtp[1].addr) or 'mailer-daemon@localhost'
|
||||
lua_smtp.sendmail({
|
||||
task = task,
|
||||
host = os.getenv("IPV4_NETWORK") .. '.253',
|
||||
port = 591,
|
||||
from = task:get_from(stp)[1].addr,
|
||||
from = from_addr,
|
||||
recipients = bcc_dest,
|
||||
helo = 'bcc',
|
||||
timeout = 20,
|
||||
@@ -397,27 +619,41 @@ rspamd_config:register_symbol({
|
||||
end
|
||||
|
||||
local action = task:get_metric_action('default')
|
||||
rspamd_logger.infox("metric action now: %s", action)
|
||||
rspamd_logger.infox("BCC: metric action: %s", action)
|
||||
|
||||
-- Check for pre-result accept (e.g., from KEEP_SPAM)
|
||||
local allow_bcc = false
|
||||
if task.has_pre_result then
|
||||
local has_pre, pre_action = task:has_pre_result()
|
||||
if has_pre and pre_action == 'accept' then
|
||||
allow_bcc = true
|
||||
rspamd_logger.infox("BCC: pre-result accept detected, will send BCC")
|
||||
end
|
||||
end
|
||||
|
||||
-- Allow BCC for mild actions or when we have pre-result accept
|
||||
if not allow_bcc and action ~= 'no action' and action ~= 'add header' and action ~= 'rewrite subject' then
|
||||
rspamd_logger.infox("BCC: skipping for action: %s", action)
|
||||
return
|
||||
end
|
||||
|
||||
local function rcpt_callback(err_message, code, body, headers)
|
||||
if err_message == nil and code == 201 and body ~= nil then
|
||||
if action == 'no action' or action == 'add header' or action == 'rewrite subject' then
|
||||
send_mail(task, body)
|
||||
end
|
||||
rspamd_logger.infox("BCC: sending BCC to %s for rcpt match", body)
|
||||
send_mail(task, body)
|
||||
end
|
||||
end
|
||||
|
||||
local function from_callback(err_message, code, body, headers)
|
||||
if err_message == nil and code == 201 and body ~= nil then
|
||||
if action == 'no action' or action == 'add header' or action == 'rewrite subject' then
|
||||
send_mail(task, body)
|
||||
end
|
||||
rspamd_logger.infox("BCC: sending BCC to %s for from match", body)
|
||||
send_mail(task, body)
|
||||
end
|
||||
end
|
||||
|
||||
if rcpt_table then
|
||||
for _,e in ipairs(rcpt_table) do
|
||||
rspamd_logger.infox(rspamd_config, "checking bcc for rcpt address %s", e)
|
||||
rspamd_logger.infox(rspamd_config, "BCC: checking bcc for rcpt address %s", e)
|
||||
rspamd_http.request({
|
||||
task=task,
|
||||
url='http://nginx:8081/bcc.php',
|
||||
@@ -430,7 +666,7 @@ rspamd_config:register_symbol({
|
||||
|
||||
if from_table then
|
||||
for _,e in ipairs(from_table) do
|
||||
rspamd_logger.infox(rspamd_config, "checking bcc for from address %s", e)
|
||||
rspamd_logger.infox(rspamd_config, "BCC: checking bcc for from address %s", e)
|
||||
rspamd_http.request({
|
||||
task=task,
|
||||
url='http://nginx:8081/bcc.php',
|
||||
@@ -441,7 +677,7 @@ rspamd_config:register_symbol({
|
||||
end
|
||||
end
|
||||
|
||||
return true
|
||||
-- Don't return true to avoid symbol being logged
|
||||
end,
|
||||
priority = 20
|
||||
})
|
||||
|
||||
@@ -32,47 +32,42 @@ function parse_email($email) {
|
||||
$a = strrpos($email, '@');
|
||||
return array('local' => substr($email, 0, $a), 'domain' => substr(substr($email, $a), 1));
|
||||
}
|
||||
if (!function_exists('getallheaders')) {
|
||||
function getallheaders() {
|
||||
if (!is_array($_SERVER)) {
|
||||
return array();
|
||||
}
|
||||
$headers = array();
|
||||
foreach ($_SERVER as $name => $value) {
|
||||
if (substr($name, 0, 5) == 'HTTP_') {
|
||||
$headers[str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($name, 5)))))] = $value;
|
||||
}
|
||||
}
|
||||
return $headers;
|
||||
}
|
||||
// rspamd metadata_exporter (multipart formatter):
|
||||
// - $_POST['metadata'] JSON with the rspamd metadata
|
||||
// - $_FILES['message'] raw RFC822 message
|
||||
if (empty($_POST['metadata']) || !isset($_FILES['message']) || $_FILES['message']['error'] !== UPLOAD_ERR_OK) {
|
||||
error_log("QUARANTINE: missing multipart parts from rspamd" . PHP_EOL);
|
||||
http_response_code(400);
|
||||
exit;
|
||||
}
|
||||
|
||||
$raw_data_content = file_get_contents('php://input');
|
||||
$meta = json_decode($_POST['metadata'], true);
|
||||
if (!is_array($meta)) {
|
||||
error_log("QUARANTINE: cannot decode metadata JSON" . PHP_EOL);
|
||||
http_response_code(400);
|
||||
exit;
|
||||
}
|
||||
|
||||
$raw_data_content = file_get_contents($_FILES['message']['tmp_name']);
|
||||
$raw_data = mb_convert_encoding($raw_data_content, 'HTML-ENTITIES', "UTF-8");
|
||||
$headers = getallheaders();
|
||||
$raw_size = (int)$_FILES['message']['size'];
|
||||
|
||||
$qid = $headers['X-Rspamd-Qid'];
|
||||
$fuzzy = $headers['X-Rspamd-Fuzzy'];
|
||||
$subject = iconv_mime_decode($headers['X-Rspamd-Subject']);
|
||||
$score = $headers['X-Rspamd-Score'];
|
||||
$rcpts = $headers['X-Rspamd-Rcpt'];
|
||||
$user = $headers['X-Rspamd-User'];
|
||||
$ip = $headers['X-Rspamd-Ip'];
|
||||
$action = $headers['X-Rspamd-Action'];
|
||||
$sender = $headers['X-Rspamd-From'];
|
||||
$symbols = $headers['X-Rspamd-Symbols'];
|
||||
|
||||
$raw_size = (int)$_SERVER['CONTENT_LENGTH'];
|
||||
$qid = $meta['qid'] ?? 'unknown';
|
||||
$subject = $meta['subject'] ?? '';
|
||||
$score = $meta['score'] ?? 0;
|
||||
$rcpts = $meta['rcpt'] ?? array();
|
||||
$user = $meta['user'] ?? 'unknown';
|
||||
$ip = $meta['ip'] ?? 'unknown';
|
||||
$action = $meta['action'] ?? 'no action';
|
||||
$sender = $meta['from'] ?? '';
|
||||
$symbols = json_encode($meta['symbols'] ?? array());
|
||||
$fuzzy = json_encode(is_array($meta['fuzzy'] ?? null) ? $meta['fuzzy'] : array());
|
||||
|
||||
if (empty($sender)) {
|
||||
error_log("QUARANTINE: Unknown sender, assuming empty-env-from@localhost" . PHP_EOL);
|
||||
$sender = 'empty-env-from@localhost';
|
||||
}
|
||||
|
||||
if ($fuzzy == 'unknown') {
|
||||
$fuzzy = '[]';
|
||||
}
|
||||
|
||||
try {
|
||||
$max_size = (int)$redis->Get('Q_MAX_SIZE');
|
||||
if (($max_size * 1048576) < $raw_size) {
|
||||
@@ -94,7 +89,7 @@ catch (RedisException $e) {
|
||||
$rcpt_final_mailboxes = array();
|
||||
|
||||
// Loop through all rcpts
|
||||
foreach (json_decode($rcpts, true) as $rcpt) {
|
||||
foreach ($rcpts as $rcpt) {
|
||||
// Remove tag
|
||||
$rcpt = preg_replace('/^(.*?)\+.*(@.*)$/', '$1$2', $rcpt);
|
||||
|
||||
@@ -182,7 +177,7 @@ foreach (json_decode($rcpts, true) as $rcpt) {
|
||||
error_log("RCPT RESOVLER: http pipe: goto address " . $goto . " is an alias branch for " . $goto_branch . PHP_EOL);
|
||||
$goto_branch_array = explode(',', $goto_branch);
|
||||
} else {
|
||||
$stmt = $pdo->prepare("SELECT `target_domain` FROM `alias_domain` WHERE `alias_domain` = :domain AND `active` AND '1'");
|
||||
$stmt = $pdo->prepare("SELECT `target_domain` FROM `alias_domain` WHERE `alias_domain` = :domain AND `active` = '1'");
|
||||
$stmt->execute(array(':domain' => $parsed_goto['domain']));
|
||||
$goto_branch = $stmt->fetch(PDO::FETCH_ASSOC)['target_domain'];
|
||||
if ($goto_branch) {
|
||||
|
||||
@@ -32,50 +32,46 @@ function parse_email($email) {
|
||||
$a = strrpos($email, '@');
|
||||
return array('local' => substr($email, 0, $a), 'domain' => substr(substr($email, $a), 1));
|
||||
}
|
||||
if (!function_exists('getallheaders')) {
|
||||
function getallheaders() {
|
||||
if (!is_array($_SERVER)) {
|
||||
return array();
|
||||
}
|
||||
$headers = array();
|
||||
foreach ($_SERVER as $name => $value) {
|
||||
if (substr($name, 0, 5) == 'HTTP_') {
|
||||
$headers[str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($name, 5)))))] = $value;
|
||||
}
|
||||
}
|
||||
return $headers;
|
||||
}
|
||||
// rspamd metadata_exporter (multipart formatter): metadata JSON arrives as $_POST['metadata'].
|
||||
if (empty($_POST['metadata'])) {
|
||||
error_log("NOTIFY: missing metadata part from rspamd" . PHP_EOL);
|
||||
http_response_code(400);
|
||||
exit;
|
||||
}
|
||||
|
||||
$headers = getallheaders();
|
||||
$json_body = json_decode(file_get_contents('php://input'));
|
||||
$meta = json_decode($_POST['metadata'], true);
|
||||
if (!is_array($meta)) {
|
||||
error_log("NOTIFY: cannot decode metadata JSON" . PHP_EOL);
|
||||
http_response_code(400);
|
||||
exit;
|
||||
}
|
||||
|
||||
$qid = $headers['X-Rspamd-Qid'];
|
||||
$rcpts = $headers['X-Rspamd-Rcpt'];
|
||||
$sender = $headers['X-Rspamd-From'];
|
||||
$ip = $headers['X-Rspamd-Ip'];
|
||||
$subject = iconv_mime_decode($headers['X-Rspamd-Subject']);
|
||||
$messageid= $json_body->message_id;
|
||||
$qid = $meta['qid'] ?? 'unknown';
|
||||
$rcpts = $meta['rcpt'] ?? array();
|
||||
$sender = $meta['from'] ?? '';
|
||||
$ip = $meta['ip'] ?? 'unknown';
|
||||
$subject = $meta['subject'] ?? '';
|
||||
$messageid= $meta['message_id'] ?? '';
|
||||
$priority = 0;
|
||||
|
||||
$symbols_array = json_decode($headers['X-Rspamd-Symbols'], true);
|
||||
$symbols_array = $meta['symbols'] ?? array();
|
||||
if (is_array($symbols_array)) {
|
||||
foreach ($symbols_array as $symbol) {
|
||||
if ($symbol['name'] == 'HAS_X_PRIO_ONE') {
|
||||
if (($symbol['name'] ?? null) == 'HAS_X_PRIO_ONE') {
|
||||
$priority = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$sender_address = $json_body->header_from[0];
|
||||
$sender_address = $meta['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_address = $meta['header_to'][0] ?? '';
|
||||
$to_name = '-';
|
||||
if (preg_match('/(?<name>.*?)<(?<address>.*?)>/i', $to_address, $matches)) {
|
||||
$to_address = $matches['address'];
|
||||
@@ -85,7 +81,7 @@ if (preg_match('/(?<name>.*?)<(?<address>.*?)>/i', $to_address, $matches)) {
|
||||
$rcpt_final_mailboxes = array();
|
||||
|
||||
// Loop through all rcpts
|
||||
foreach (json_decode($rcpts, true) as $rcpt) {
|
||||
foreach ($rcpts as $rcpt) {
|
||||
// Remove tag
|
||||
$rcpt = preg_replace('/^(.*?)\+.*(@.*)$/', '$1$2', $rcpt);
|
||||
|
||||
@@ -167,7 +163,7 @@ foreach (json_decode($rcpts, true) as $rcpt) {
|
||||
error_log("RCPT RESOVLER: http pipe: goto address " . $goto . " is an alias branch for " . $goto_branch . PHP_EOL);
|
||||
$goto_branch_array = explode(',', $goto_branch);
|
||||
} else {
|
||||
$stmt = $pdo->prepare("SELECT `target_domain` FROM `alias_domain` WHERE `alias_domain` = :domain AND `active` AND '1'");
|
||||
$stmt = $pdo->prepare("SELECT `target_domain` FROM `alias_domain` WHERE `alias_domain` = :domain AND `active` = '1'");
|
||||
$stmt->execute(array(':domain' => $parsed_goto['domain']));
|
||||
$goto_branch = $stmt->fetch(PDO::FETCH_ASSOC)['target_domain'];
|
||||
if ($goto_branch) {
|
||||
|
||||
@@ -86,6 +86,12 @@
|
||||
SOGoMaximumFailedLoginInterval = 900;
|
||||
SOGoFailedLoginBlockInterval = 900;
|
||||
|
||||
// Enable SOGo URL Description for GDPR compliance, this may cause some issues with calendars and contacts. Also uncomment the encryption key below to use it.
|
||||
//SOGoURLEncryptionEnabled = NO;
|
||||
|
||||
// Set a 16 character encryption key for SOGo URL Description, change this to your own value
|
||||
//SOGoURLPathEncryptionKey = "SOGoSuperSecret0";
|
||||
|
||||
GCSChannelCollectionTimer = 60;
|
||||
GCSChannelExpireAge = 60;
|
||||
|
||||
|
||||
@@ -2,18 +2,7 @@
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.admin.inc.php';
|
||||
|
||||
if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'domainadmin') {
|
||||
header('Location: /domainadmin/mailbox');
|
||||
exit();
|
||||
}
|
||||
elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user') {
|
||||
header('Location: /user');
|
||||
exit();
|
||||
}
|
||||
elseif (!isset($_SESSION['mailcow_cc_role']) || $_SESSION['mailcow_cc_role'] != "admin") {
|
||||
header('Location: /admin');
|
||||
exit();
|
||||
}
|
||||
protect_route(['admin']);
|
||||
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php';
|
||||
$_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
|
||||
|
||||
@@ -3,8 +3,11 @@ require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.admin.inc.php';
|
||||
|
||||
if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'admin') {
|
||||
header('Location: /admin/dashboard');
|
||||
exit();
|
||||
// Only redirect to dashboard if NO pending actions
|
||||
if (empty($_SESSION['pending_tfa_setup']) && empty($_SESSION['pending_pw_update'])) {
|
||||
header('Location: /admin/dashboard');
|
||||
exit();
|
||||
}
|
||||
}
|
||||
elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'domainadmin') {
|
||||
header('Location: /domainadmin/mailbox');
|
||||
|
||||
@@ -2,18 +2,7 @@
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.admin.inc.php';
|
||||
|
||||
if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'domainadmin') {
|
||||
header('Location: /domainadmin/mailbox');
|
||||
exit();
|
||||
}
|
||||
elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user') {
|
||||
header('Location: /user');
|
||||
exit();
|
||||
}
|
||||
elseif (!isset($_SESSION['mailcow_cc_role']) || $_SESSION['mailcow_cc_role'] != "admin") {
|
||||
header('Location: /admin');
|
||||
exit();
|
||||
}
|
||||
protect_route(['admin']);
|
||||
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php';
|
||||
$_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
|
||||
|
||||
@@ -2,19 +2,7 @@
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.admin.inc.php';
|
||||
|
||||
if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'domainadmin') {
|
||||
header('Location: /domainadmin/mailbox');
|
||||
exit();
|
||||
}
|
||||
elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user') {
|
||||
header('Location: /user');
|
||||
exit();
|
||||
}
|
||||
elseif (!isset($_SESSION['mailcow_cc_role']) || $_SESSION['mailcow_cc_role'] != "admin") {
|
||||
header('Location: /admin');
|
||||
exit();
|
||||
}
|
||||
|
||||
protect_route(['admin']);
|
||||
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php';
|
||||
$js_minifier->add('/web/js/site/queue.js');
|
||||
|
||||
@@ -2,18 +2,7 @@
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.admin.inc.php';
|
||||
|
||||
if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'domainadmin') {
|
||||
header('Location: /domainadmin/mailbox');
|
||||
exit();
|
||||
}
|
||||
elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user') {
|
||||
header('Location: /user');
|
||||
exit();
|
||||
}
|
||||
elseif (!isset($_SESSION['mailcow_cc_role']) || $_SESSION['mailcow_cc_role'] != "admin") {
|
||||
header('Location: /admin');
|
||||
exit();
|
||||
}
|
||||
protect_route(['admin']);
|
||||
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php';
|
||||
$_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
|
||||
@@ -127,6 +116,7 @@ $template_data = [
|
||||
'ip_check' => customize('get', 'ip_check'),
|
||||
'custom_login' => customize('get', 'custom_login'),
|
||||
'password_complexity' => password_complexity('get'),
|
||||
'imapsync_settings' => imapsync_get_settings(),
|
||||
'show_rspamd_global_filters' => @$_SESSION['show_rspamd_global_filters'],
|
||||
'cors_settings' => $cors_settings,
|
||||
'is_https' => isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on',
|
||||
|
||||
+700
-7
@@ -185,6 +185,9 @@ paths:
|
||||
example:
|
||||
username: info@domain.tld
|
||||
domain: domain.tld
|
||||
description: my time limited alias
|
||||
validity: 8760
|
||||
permanent: false
|
||||
properties:
|
||||
username:
|
||||
description: 'the mailbox an alias should be created for'
|
||||
@@ -192,6 +195,15 @@ paths:
|
||||
domain:
|
||||
description: "the domain"
|
||||
type: string
|
||||
description:
|
||||
description: "a description for the alias, defaults to an empty string"
|
||||
type: string
|
||||
validity:
|
||||
description: "how many hours the alias stays valid, 1 to 87600, defaults to 8760 (one year)"
|
||||
type: integer
|
||||
permanent:
|
||||
description: "keep the alias after it expired, defaults to false"
|
||||
type: boolean
|
||||
type: object
|
||||
summary: Create time limited alias
|
||||
/api/v1/add/app-passwd:
|
||||
@@ -1072,6 +1084,7 @@ paths:
|
||||
password2: "*"
|
||||
quota: "3072"
|
||||
force_pw_update: "1"
|
||||
force_tfa: "1"
|
||||
tls_enforce_in: "1"
|
||||
tls_enforce_out: "1"
|
||||
tags: ["tag1", "tag2"]
|
||||
@@ -1118,6 +1131,7 @@ paths:
|
||||
password2: atedismonsin
|
||||
quota: "3072"
|
||||
force_pw_update: "1"
|
||||
force_tfa: "1"
|
||||
tls_enforce_in: "1"
|
||||
tls_enforce_out: "1"
|
||||
tags: ["tag1", "tag2"]
|
||||
@@ -1151,6 +1165,9 @@ paths:
|
||||
force_pw_update:
|
||||
description: forces the user to update its password on first login
|
||||
type: boolean
|
||||
force_tfa:
|
||||
description: force 2FA enrollment at login
|
||||
type: boolean
|
||||
tls_enforce_in:
|
||||
description: force inbound email tls encryption
|
||||
type: boolean
|
||||
@@ -1520,7 +1537,9 @@ paths:
|
||||
timeout1: "600"
|
||||
timeout2: "600"
|
||||
exclude: "(?i)spam|(?i)junk"
|
||||
custom_params: "--dry"
|
||||
custom_params:
|
||||
- {o: dry, v: ""}
|
||||
- {o: folder, v: INBOX}
|
||||
delete2duplicates: "1"
|
||||
delete1: "1"
|
||||
delete2: "0"
|
||||
@@ -1569,8 +1588,19 @@ paths:
|
||||
description: exclude objects (regex)
|
||||
type: string
|
||||
custom_params:
|
||||
description: custom parameters
|
||||
type: string
|
||||
description: >-
|
||||
Allowlisted imapsync options as option/value pairs. `o` is the
|
||||
option name (must be one from `GET /get/syncjob_options`); `v`
|
||||
is the value (empty string for pure flags). Values may contain
|
||||
any character and are passed to imapsync as a single argument.
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
o:
|
||||
type: string
|
||||
v:
|
||||
type: string
|
||||
delete2duplicates:
|
||||
description: delete duplicates on destination (--delete2duplicates)
|
||||
type: boolean
|
||||
@@ -1593,6 +1623,151 @@ paths:
|
||||
description: enables or disables the sync job
|
||||
type: boolean
|
||||
type: object
|
||||
/api/v1/add/syncjob_source:
|
||||
post:
|
||||
responses:
|
||||
"401":
|
||||
$ref: "#/components/responses/Unauthorized"
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
examples:
|
||||
response:
|
||||
value:
|
||||
- log:
|
||||
- syncjob
|
||||
- add
|
||||
- source
|
||||
- name: Example import
|
||||
description: ""
|
||||
host1: imap.example.org
|
||||
port1: 993
|
||||
enc1: SSL
|
||||
auth_type: PLAIN
|
||||
active: 1
|
||||
scope: all
|
||||
msg:
|
||||
- imapsync_source_added
|
||||
- Example import
|
||||
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:
|
||||
- Sync jobs
|
||||
description: >-
|
||||
Create a reusable IMAP sync source. A sync job binds to a source by id;
|
||||
the source carries host/port/encryption and either static credentials
|
||||
(PLAIN/LOGIN/CRAM-MD5 — supplied per-job) or an OAuth2 token endpoint
|
||||
(XOAUTH2 — token cached on the source row).
|
||||
operationId: Create sync job source
|
||||
summary: Create sync job source
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
example:
|
||||
name: Example import
|
||||
description: ""
|
||||
host1: imap.example.org
|
||||
port1: "993"
|
||||
enc1: SSL
|
||||
auth_type: PLAIN
|
||||
active: "1"
|
||||
scope: all
|
||||
properties:
|
||||
name:
|
||||
description: source name, unique per owner
|
||||
type: string
|
||||
description:
|
||||
description: free text shown in the UI
|
||||
type: string
|
||||
host1:
|
||||
description: remote IMAP host
|
||||
type: string
|
||||
port1:
|
||||
description: remote IMAP port (1-65535)
|
||||
type: integer
|
||||
enc1:
|
||||
description: TLS / SSL / PLAIN
|
||||
type: string
|
||||
enum:
|
||||
- TLS
|
||||
- SSL
|
||||
- PLAIN
|
||||
auth_type:
|
||||
description: authentication mechanism
|
||||
type: string
|
||||
enum:
|
||||
- PLAIN
|
||||
- LOGIN
|
||||
- CRAM-MD5
|
||||
- XOAUTH2
|
||||
oauth_flow:
|
||||
description: XOAUTH2 grant type (default client_credentials)
|
||||
type: string
|
||||
enum:
|
||||
- client_credentials
|
||||
- authorization_code
|
||||
oauth_token_endpoint:
|
||||
description: required when auth_type is XOAUTH2 (token URL)
|
||||
type: string
|
||||
oauth_authorize_endpoint:
|
||||
description: required when oauth_flow is authorization_code (authorize URL)
|
||||
type: string
|
||||
oauth_userinfo_endpoint:
|
||||
description: optional identity endpoint for authorization_code
|
||||
type: string
|
||||
oauth_client_id:
|
||||
description: required when auth_type is XOAUTH2
|
||||
type: string
|
||||
oauth_client_secret:
|
||||
description: required when auth_type is XOAUTH2
|
||||
type: string
|
||||
oauth_scope:
|
||||
description: required when auth_type is XOAUTH2
|
||||
type: string
|
||||
oauth_extra_params:
|
||||
description: optional JSON object merged into the token request body
|
||||
type: string
|
||||
active:
|
||||
description: enables or disables the source
|
||||
type: boolean
|
||||
scope:
|
||||
description: >-
|
||||
admin/domainadmin — source visibility. client_credentials
|
||||
XOAUTH2 sources are always forced to a private scope.
|
||||
type: string
|
||||
enum:
|
||||
- all
|
||||
- domain
|
||||
- user
|
||||
domains:
|
||||
description: target domains when scope is domain
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
users:
|
||||
description: target users when scope is user
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
type: object
|
||||
/api/v1/add/tls-policy-map:
|
||||
post:
|
||||
responses:
|
||||
@@ -2454,6 +2629,90 @@ paths:
|
||||
type: object
|
||||
type: object
|
||||
summary: Delete mails in Quarantine
|
||||
/api/v1/edit/qitem:
|
||||
post:
|
||||
responses:
|
||||
"401":
|
||||
$ref: "#/components/responses/Unauthorized"
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
examples:
|
||||
release:
|
||||
value:
|
||||
- log:
|
||||
- quarantine
|
||||
- edit
|
||||
- id:
|
||||
- "33"
|
||||
action: release
|
||||
msg:
|
||||
- item_released
|
||||
- "33"
|
||||
type: success
|
||||
learnham:
|
||||
value:
|
||||
- log:
|
||||
- quarantine
|
||||
- edit
|
||||
- id:
|
||||
- "34"
|
||||
action: learnham
|
||||
msg:
|
||||
- item_learned
|
||||
- "34"
|
||||
type: success
|
||||
schema:
|
||||
properties:
|
||||
log:
|
||||
description: contains request object
|
||||
items: {}
|
||||
type: array
|
||||
msg:
|
||||
items: {}
|
||||
type: array
|
||||
type:
|
||||
enum:
|
||||
- success
|
||||
- danger
|
||||
- error
|
||||
type: string
|
||||
type: object
|
||||
description: OK
|
||||
headers: {}
|
||||
tags:
|
||||
- Quarantine
|
||||
description: >-
|
||||
Using this endpoint you can perform actions on quarantine items. It is possible to release
|
||||
emails from quarantine into to the inbox, or learn them as ham to improve Rspamd filtering.
|
||||
You must provide the quarantine item IDs. You can get the IDs using the GET method.
|
||||
operationId: Edit mails in Quarantine
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
example:
|
||||
items:
|
||||
- "33"
|
||||
- "34"
|
||||
attr:
|
||||
action: release
|
||||
properties:
|
||||
items:
|
||||
description: contains list of quarantine item IDs to release or learn as ham
|
||||
type: object
|
||||
attr:
|
||||
description: attributes for the action
|
||||
type: object
|
||||
properties:
|
||||
action:
|
||||
type: string
|
||||
enum:
|
||||
- release
|
||||
- learnham
|
||||
description: "release - return email to inbox; learnham - learn as ham to improve filtering"
|
||||
type: object
|
||||
summary: Edit mails in Quarantine
|
||||
/api/v1/delete/recipient_map:
|
||||
post:
|
||||
responses:
|
||||
@@ -2681,6 +2940,62 @@ paths:
|
||||
type: object
|
||||
type: object
|
||||
summary: Delete sync job
|
||||
/api/v1/delete/syncjob_source:
|
||||
post:
|
||||
responses:
|
||||
"401":
|
||||
$ref: "#/components/responses/Unauthorized"
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
examples:
|
||||
response:
|
||||
value:
|
||||
- log:
|
||||
- syncjob
|
||||
- delete
|
||||
- source
|
||||
- id:
|
||||
- "3"
|
||||
msg:
|
||||
- imapsync_source_deleted
|
||||
- Example import
|
||||
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:
|
||||
- Sync jobs
|
||||
description: >-
|
||||
Delete one or more sync job sources by id. Sources still referenced by
|
||||
a sync job cannot be deleted (FK ON DELETE RESTRICT) — delete or
|
||||
repoint the dependent jobs first.
|
||||
operationId: Delete sync job source
|
||||
summary: Delete sync job source
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
example:
|
||||
- "3"
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
/api/v1/delete/tls-policy-map:
|
||||
post:
|
||||
responses:
|
||||
@@ -3330,6 +3645,7 @@ paths:
|
||||
- mailbox
|
||||
- active: "1"
|
||||
force_pw_update: "0"
|
||||
force_tfa: "0"
|
||||
name: Full name
|
||||
password: "*"
|
||||
password2: "*"
|
||||
@@ -3380,6 +3696,7 @@ paths:
|
||||
attr:
|
||||
active: "1"
|
||||
force_pw_update: "0"
|
||||
force_tfa: "0"
|
||||
name: Full name
|
||||
authsource: mailcow
|
||||
password: ""
|
||||
@@ -3403,6 +3720,9 @@ paths:
|
||||
force_pw_update:
|
||||
description: force user to change password on next login
|
||||
type: boolean
|
||||
force_tfa:
|
||||
description: force 2FA enrollment at login
|
||||
type: boolean
|
||||
name:
|
||||
description: Full name of the mailbox user
|
||||
type: string
|
||||
@@ -3778,8 +4098,25 @@ paths:
|
||||
etc.)
|
||||
type: boolean
|
||||
custom_params:
|
||||
description: Custom parameters passed to imapsync command
|
||||
type: string
|
||||
description: >-
|
||||
Allowlisted imapsync options as option/value pairs. `o` is
|
||||
the option name (must be one from `GET /get/syncjob_options`);
|
||||
`v` is the value (empty string for flags). Values are passed to imapsync as a single
|
||||
argument.
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
o:
|
||||
type: string
|
||||
v:
|
||||
type: string
|
||||
prio:
|
||||
description: >-
|
||||
Priority (admin only; ignored otherwise). The runner processes due
|
||||
jobs least-recently-run first, priority breaks ties (0 = normal,
|
||||
higher wins).
|
||||
type: number
|
||||
delete1:
|
||||
description: Delete from source when completed
|
||||
type: boolean
|
||||
@@ -3847,6 +4184,269 @@ paths:
|
||||
type: object
|
||||
type: object
|
||||
summary: Update sync job
|
||||
/api/v1/edit/syncjob_source:
|
||||
post:
|
||||
responses:
|
||||
"401":
|
||||
$ref: "#/components/responses/Unauthorized"
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
examples:
|
||||
response:
|
||||
value:
|
||||
- log:
|
||||
- syncjob
|
||||
- edit
|
||||
- source
|
||||
- id:
|
||||
- "3"
|
||||
name: Example import
|
||||
host1: imap.example.org
|
||||
port1: 993
|
||||
msg:
|
||||
- imapsync_source_modified
|
||||
- Example import
|
||||
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:
|
||||
- Sync jobs
|
||||
description: >-
|
||||
Update one or more sync job sources. Fields not provided are left
|
||||
unchanged. For XOAUTH2 sources, an empty `oauth_client_secret`
|
||||
preserves the stored value.
|
||||
operationId: Update sync job source
|
||||
summary: Update sync job source
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
example:
|
||||
items:
|
||||
- "3"
|
||||
attr:
|
||||
name: Example import
|
||||
description: ""
|
||||
host1: imap.example.org
|
||||
port1: "993"
|
||||
enc1: SSL
|
||||
auth_type: PLAIN
|
||||
active: "1"
|
||||
properties:
|
||||
attr:
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
description:
|
||||
type: string
|
||||
host1:
|
||||
type: string
|
||||
port1:
|
||||
type: integer
|
||||
enc1:
|
||||
type: string
|
||||
enum:
|
||||
- TLS
|
||||
- SSL
|
||||
- PLAIN
|
||||
auth_type:
|
||||
type: string
|
||||
enum:
|
||||
- PLAIN
|
||||
- LOGIN
|
||||
- CRAM-MD5
|
||||
- XOAUTH2
|
||||
oauth_flow:
|
||||
description: XOAUTH2 grant type
|
||||
type: string
|
||||
enum:
|
||||
- client_credentials
|
||||
- authorization_code
|
||||
oauth_token_endpoint:
|
||||
type: string
|
||||
oauth_authorize_endpoint:
|
||||
description: required when oauth_flow is authorization_code
|
||||
type: string
|
||||
oauth_userinfo_endpoint:
|
||||
description: optional identity endpoint for authorization_code
|
||||
type: string
|
||||
oauth_client_id:
|
||||
type: string
|
||||
oauth_client_secret:
|
||||
description: empty keeps the previously stored secret
|
||||
type: string
|
||||
oauth_scope:
|
||||
type: string
|
||||
oauth_extra_params:
|
||||
type: string
|
||||
active:
|
||||
type: boolean
|
||||
scope:
|
||||
description: source visibility (admin/domainadmin)
|
||||
type: string
|
||||
enum:
|
||||
- all
|
||||
- domain
|
||||
- user
|
||||
domains:
|
||||
description: target domains when scope is domain
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
users:
|
||||
description: target users when scope is user
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
type: object
|
||||
items:
|
||||
description: list of source ids to update
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
type: object
|
||||
/api/v1/edit/syncjob_source/refresh_token:
|
||||
post:
|
||||
responses:
|
||||
"401":
|
||||
$ref: "#/components/responses/Unauthorized"
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
examples:
|
||||
response:
|
||||
value:
|
||||
- log:
|
||||
- syncjob
|
||||
- edit
|
||||
- refresh_token
|
||||
- id:
|
||||
- "3"
|
||||
msg:
|
||||
- imapsync_source_token_refreshed
|
||||
- Example import
|
||||
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:
|
||||
- Sync jobs
|
||||
description: >-
|
||||
Force an OAuth2 access-token refresh for a XOAUTH2 source. Performs a
|
||||
Client-Credentials grant against the source's `oauth_token_endpoint`
|
||||
and caches the new access token + expiry on the source row. On failure,
|
||||
the error is recorded in `oauth_last_refresh_error`.
|
||||
operationId: Refresh sync job source token
|
||||
summary: Refresh sync job source token
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
example:
|
||||
items:
|
||||
- "3"
|
||||
properties:
|
||||
items:
|
||||
description: list of source ids to refresh
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
type: object
|
||||
/api/v1/edit/imapsync_settings:
|
||||
post:
|
||||
responses:
|
||||
"401":
|
||||
$ref: "#/components/responses/Unauthorized"
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
examples:
|
||||
response:
|
||||
value:
|
||||
- log:
|
||||
- imapsync_settings
|
||||
- edit
|
||||
msg:
|
||||
- max_parallel_saved
|
||||
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:
|
||||
- Sync jobs
|
||||
description: >-
|
||||
Update global sync job settings (admin only). `max_parallel` is the
|
||||
number of imapsync processes that may run concurrently; `max_kb_per_second`
|
||||
is a per-process bandwidth cap in KB/s (0 = unlimited), stored internally
|
||||
in bytes/s.
|
||||
operationId: Update sync job settings
|
||||
summary: Update sync job settings
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
example:
|
||||
attr:
|
||||
max_parallel: "4"
|
||||
max_kb_per_second: "2000"
|
||||
properties:
|
||||
attr:
|
||||
properties:
|
||||
max_parallel:
|
||||
description: Max. concurrent sync processes (>= 1)
|
||||
type: number
|
||||
max_kb_per_second:
|
||||
description: Bandwidth limit per process in KB/s (0 = unlimited)
|
||||
type: number
|
||||
type: object
|
||||
type: object
|
||||
/api/v1/edit/user-acl:
|
||||
post:
|
||||
responses:
|
||||
@@ -4797,6 +5397,7 @@ paths:
|
||||
- active: "1"
|
||||
attributes:
|
||||
force_pw_update: "0"
|
||||
force_tfa: "0"
|
||||
mailbox_format: "maildir:"
|
||||
quarantine_notification: never
|
||||
sogo_access: "1"
|
||||
@@ -4813,6 +5414,7 @@ paths:
|
||||
quota: 3221225472
|
||||
quota_used: 0
|
||||
rl: false
|
||||
sender_acl: ["otherbox@doman3.tld", "@aliasdomain.tld"]
|
||||
spam_aliases: 0
|
||||
username: info@doman3.tld
|
||||
tags: ["tag1", "tag2"]
|
||||
@@ -5545,6 +6147,95 @@ paths:
|
||||
description: You can list all syn jobs existing in system.
|
||||
operationId: Get sync jobs
|
||||
summary: Get sync jobs
|
||||
"/api/v1/get/syncjob_source/{id}":
|
||||
get:
|
||||
parameters:
|
||||
- description: source id, or `all` to list every source visible to the session
|
||||
example: all
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- description: e.g. api-key-string
|
||||
example: api-key-string
|
||||
in: header
|
||||
name: X-API-Key
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"401":
|
||||
$ref: "#/components/responses/Unauthorized"
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
examples:
|
||||
response:
|
||||
value:
|
||||
- id: 3
|
||||
name: Example import
|
||||
description: ""
|
||||
created_by: ""
|
||||
scope: all
|
||||
host1: imap.example.org
|
||||
port1: 993
|
||||
enc1: SSL
|
||||
auth_type: PLAIN
|
||||
oauth_token_endpoint: null
|
||||
oauth_client_id: null
|
||||
oauth_client_secret: ""
|
||||
oauth_scope: null
|
||||
oauth_extra_params: null
|
||||
oauth_access_token: ""
|
||||
oauth_token_expires: null
|
||||
oauth_last_refresh_error: null
|
||||
active: 1
|
||||
created: "2026-06-11 09:00:00"
|
||||
modified: "2026-06-11 09:00:00"
|
||||
description: OK
|
||||
headers: {}
|
||||
tags:
|
||||
- Sync jobs
|
||||
description: >-
|
||||
Returns a single sync job source by id, or all sources visible to the
|
||||
caller when `id` is `all`. Secrets (`oauth_client_secret`,
|
||||
`oauth_access_token`) are blanked unless requested with internal
|
||||
flags — they are never returned over the JSON API.
|
||||
operationId: Get sync job sources
|
||||
summary: Get sync job sources
|
||||
/api/v1/get/syncjob_options:
|
||||
get:
|
||||
parameters:
|
||||
- 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:
|
||||
- dry
|
||||
- folder
|
||||
- delete2folders
|
||||
- maxsize
|
||||
description: OK
|
||||
headers: {}
|
||||
tags:
|
||||
- Sync jobs
|
||||
description: >-
|
||||
Returns the allowlist of imapsync option names accepted in a sync job's
|
||||
`custom_params`. Used by the UI to populate the option autocompletion.
|
||||
operationId: Get sync job options
|
||||
summary: Get sync job options
|
||||
"/api/v1/get/tls-policy-map/{id}":
|
||||
get:
|
||||
parameters:
|
||||
@@ -5721,6 +6412,7 @@ paths:
|
||||
- active: "1"
|
||||
attributes:
|
||||
force_pw_update: "0"
|
||||
force_tfa: "0"
|
||||
mailbox_format: "maildir:"
|
||||
quarantine_notification: never
|
||||
sogo_access: "1"
|
||||
@@ -5738,6 +6430,7 @@ paths:
|
||||
quota: 3221225472
|
||||
quota_used: 0
|
||||
rl: false
|
||||
sender_acl: ["otherbox@domain3.tld", "@aliasdomain.tld"]
|
||||
spam_aliases: 0
|
||||
username: info@domain3.tld
|
||||
tags: ["tag1", "tag2"]
|
||||
@@ -5760,7 +6453,7 @@ paths:
|
||||
response:
|
||||
value:
|
||||
- type: "success"
|
||||
log: ["cors", "edit", {"allowed_origins": ["*", "mail.mailcow.tld"], "allowed_methods": ["POST", "GET", "DELETE", "PUT"]}]
|
||||
log: ["cors", "edit", {"allowed_origins": ["*", "https://mail.mailcow.tld"], "allowed_methods": ["POST", "GET", "DELETE", "PUT"]}]
|
||||
msg: "cors_headers_edited"
|
||||
description: OK
|
||||
headers: { }
|
||||
@@ -5777,7 +6470,7 @@ paths:
|
||||
schema:
|
||||
example:
|
||||
attr:
|
||||
allowed_origins: ["*", "mail.mailcow.tld"]
|
||||
allowed_origins: ["*", "https://mail.mailcow.tld"]
|
||||
allowed_methods: ["POST", "GET", "DELETE", "PUT"]
|
||||
properties:
|
||||
attr:
|
||||
|
||||
@@ -29,8 +29,8 @@ header('Content-Type: application/xml');
|
||||
<clientConfig version="1.1">
|
||||
<emailProvider id="<?=$mailcow_hostname; ?>">
|
||||
<domain>%EMAILDOMAIN%</domain>
|
||||
<displayName>A mailcow mail server</displayName>
|
||||
<displayShortName>mail server</displayShortName>
|
||||
<displayName><?=$autodiscover_config['displayName']; ?></displayName>
|
||||
<displayShortName><?=$autodiscover_config['displayShortName']; ?></displayShortName>
|
||||
|
||||
<incomingServer type="imap">
|
||||
<hostname><?=$autodiscover_config['imap']['server']; ?></hostname>
|
||||
|
||||
+150
-85
@@ -7,6 +7,8 @@ if(file_exists('inc/vars.local.inc.php')) {
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.inc.php';
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.auth.inc.php';
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/sessions.inc.php';
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.mailbox.inc.php';
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.ratelimit.inc.php';
|
||||
$default_autodiscover_config = $autodiscover_config;
|
||||
$autodiscover_config = array_merge($default_autodiscover_config, $autodiscover_config);
|
||||
|
||||
@@ -58,101 +60,31 @@ $pdo = new PDO($dsn, $database_user, $database_pass, $opt);
|
||||
$iam_provider = identity_provider('init');
|
||||
$iam_settings = identity_provider('get');
|
||||
|
||||
$login_user = strtolower(trim($_SERVER['PHP_AUTH_USER']));
|
||||
$login_pass = trim(htmlspecialchars_decode($_SERVER['PHP_AUTH_PW']));
|
||||
// Passwordless autodiscover - no authentication required
|
||||
// Email will be extracted from the request body
|
||||
$login_user = null;
|
||||
$login_role = null;
|
||||
|
||||
if (empty($_SERVER['PHP_AUTH_USER']) || empty($_SERVER['PHP_AUTH_PW'])) {
|
||||
$json = json_encode(
|
||||
array(
|
||||
"time" => time(),
|
||||
"ua" => $_SERVER['HTTP_USER_AGENT'],
|
||||
"user" => "none",
|
||||
"ip" => $_SERVER['REMOTE_ADDR'],
|
||||
"service" => "Error: must be authenticated"
|
||||
)
|
||||
);
|
||||
$redis->lPush('AUTODISCOVER_LOG', $json);
|
||||
header('WWW-Authenticate: Basic realm="' . $_SERVER['HTTP_HOST'] . '"');
|
||||
header('HTTP/1.0 401 Unauthorized');
|
||||
exit(0);
|
||||
}
|
||||
|
||||
$login_role = check_login($login_user, $login_pass, array('eas' => TRUE));
|
||||
|
||||
if ($login_role === "user") {
|
||||
header("Content-Type: application/xml");
|
||||
echo '<?xml version="1.0" encoding="utf-8" ?>' . PHP_EOL;
|
||||
header("Content-Type: application/xml");
|
||||
echo '<?xml version="1.0" encoding="utf-8" ?>' . PHP_EOL;
|
||||
?>
|
||||
<Autodiscover xmlns="http://schemas.microsoft.com/exchange/autodiscover/responseschema/2006">
|
||||
<?php
|
||||
if(!$data) {
|
||||
try {
|
||||
$json = json_encode(
|
||||
array(
|
||||
"time" => time(),
|
||||
"ua" => $_SERVER['HTTP_USER_AGENT'],
|
||||
"user" => $_SERVER['PHP_AUTH_USER'],
|
||||
"ip" => $_SERVER['REMOTE_ADDR'],
|
||||
"service" => "Error: invalid or missing request data"
|
||||
)
|
||||
);
|
||||
$redis->lPush('AUTODISCOVER_LOG', $json);
|
||||
$redis->lTrim('AUTODISCOVER_LOG', 0, 100);
|
||||
}
|
||||
catch (RedisException $e) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'msg' => 'Redis: '.$e
|
||||
);
|
||||
return false;
|
||||
}
|
||||
list($usec, $sec) = explode(' ', microtime());
|
||||
?>
|
||||
<Response>
|
||||
<Error Time="<?=date('H:i:s', $sec) . substr($usec, 0, strlen($usec) - 2);?>" Id="2477272013">
|
||||
<ErrorCode>600</ErrorCode>
|
||||
<Message>Invalid Request</Message>
|
||||
<DebugData />
|
||||
</Error>
|
||||
</Response>
|
||||
</Autodiscover>
|
||||
<?php
|
||||
exit(0);
|
||||
}
|
||||
try {
|
||||
$discover = new SimpleXMLElement($data);
|
||||
$email = $discover->Request->EMailAddress;
|
||||
} catch (Exception $e) {
|
||||
$email = $_SERVER['PHP_AUTH_USER'];
|
||||
}
|
||||
|
||||
$username = trim($email);
|
||||
try {
|
||||
$stmt = $pdo->prepare("SELECT `name` FROM `mailbox` WHERE `username`= :username");
|
||||
$stmt->execute(array(':username' => $username));
|
||||
$MailboxData = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
}
|
||||
catch(PDOException $e) {
|
||||
die("Failed to determine name from SQL");
|
||||
}
|
||||
if (!empty($MailboxData['name'])) {
|
||||
$displayname = $MailboxData['name'];
|
||||
}
|
||||
else {
|
||||
$displayname = $email;
|
||||
}
|
||||
if(!$data) {
|
||||
try {
|
||||
$json = json_encode(
|
||||
array(
|
||||
"time" => time(),
|
||||
"ua" => $_SERVER['HTTP_USER_AGENT'],
|
||||
"user" => $_SERVER['PHP_AUTH_USER'],
|
||||
"user" => "none",
|
||||
"ip" => $_SERVER['REMOTE_ADDR'],
|
||||
"service" => $autodiscover_config['autodiscoverType']
|
||||
"service" => "Error: invalid or missing request data"
|
||||
)
|
||||
);
|
||||
$redis->lPush('AUTODISCOVER_LOG', $json);
|
||||
$redis->lTrim('AUTODISCOVER_LOG', 0, 100);
|
||||
$redis->publish("F2B_CHANNEL", "Autodiscover: Invalid request by " . $_SERVER['REMOTE_ADDR']);
|
||||
error_log("Autodiscover: Invalid request by " . $_SERVER['REMOTE_ADDR']);
|
||||
}
|
||||
catch (RedisException $e) {
|
||||
$_SESSION['return'][] = array(
|
||||
@@ -161,7 +93,143 @@ if ($login_role === "user") {
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if ($autodiscover_config['autodiscoverType'] == 'imap') {
|
||||
list($usec, $sec) = explode(' ', microtime());
|
||||
?>
|
||||
<Response>
|
||||
<Error Time="<?=date('H:i:s', $sec) . substr($usec, 0, strlen($usec) - 2);?>" Id="<?=rand(1000000000, 9999999999);?>">
|
||||
<ErrorCode>600</ErrorCode>
|
||||
<Message>Invalid Request</Message>
|
||||
<DebugData />
|
||||
</Error>
|
||||
</Response>
|
||||
</Autodiscover>
|
||||
<?php
|
||||
exit(0);
|
||||
}
|
||||
try {
|
||||
$discover = new SimpleXMLElement($data);
|
||||
$email = $discover->Request->EMailAddress;
|
||||
} catch (Exception $e) {
|
||||
// If parsing fails, return error
|
||||
try {
|
||||
$json = json_encode(
|
||||
array(
|
||||
"time" => time(),
|
||||
"ua" => $_SERVER['HTTP_USER_AGENT'],
|
||||
"user" => "none",
|
||||
"ip" => $_SERVER['REMOTE_ADDR'],
|
||||
"service" => "Error: could not parse email from request"
|
||||
)
|
||||
);
|
||||
$redis->lPush('AUTODISCOVER_LOG', $json);
|
||||
$redis->lTrim('AUTODISCOVER_LOG', 0, 100);
|
||||
$redis->publish("F2B_CHANNEL", "Autodiscover: Malformed XML by " . $_SERVER['REMOTE_ADDR']);
|
||||
error_log("Autodiscover: Malformed XML by " . $_SERVER['REMOTE_ADDR']);
|
||||
}
|
||||
catch (RedisException $e) {
|
||||
// Silently fail
|
||||
}
|
||||
list($usec, $sec) = explode(' ', microtime());
|
||||
?>
|
||||
<Response>
|
||||
<Error Time="<?=date('H:i:s', $sec) . substr($usec, 0, strlen($usec) - 2);?>" Id="<?=rand(1000000000, 9999999999);?>">
|
||||
<ErrorCode>600</ErrorCode>
|
||||
<Message>Invalid Request</Message>
|
||||
<DebugData />
|
||||
</Error>
|
||||
</Response>
|
||||
</Autodiscover>
|
||||
<?php
|
||||
exit(0);
|
||||
}
|
||||
|
||||
$username = trim((string)$email);
|
||||
try {
|
||||
$stmt = $pdo->prepare("SELECT `mailbox`.`name`, `mailbox`.`active` FROM `mailbox`
|
||||
INNER JOIN `domain` ON `mailbox`.`domain` = `domain`.`domain`
|
||||
WHERE `mailbox`.`username` = :username
|
||||
AND `mailbox`.`active` = '1'
|
||||
AND `domain`.`active` = '1'");
|
||||
$stmt->execute(array(':username' => $username));
|
||||
$MailboxData = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
}
|
||||
catch(PDOException $e) {
|
||||
// Database error - return error response with complete XML
|
||||
list($usec, $sec) = explode(' ', microtime());
|
||||
?>
|
||||
<Response>
|
||||
<Error Time="<?=date('H:i:s', $sec) . substr($usec, 0, strlen($usec) - 2);?>" Id="<?=rand(1000000000, 9999999999);?>">
|
||||
<ErrorCode>500</ErrorCode>
|
||||
<Message>Database Error</Message>
|
||||
<DebugData />
|
||||
</Error>
|
||||
</Response>
|
||||
</Autodiscover>
|
||||
<?php
|
||||
exit(0);
|
||||
}
|
||||
|
||||
// Mailbox not found or not active - return generic error to prevent user enumeration
|
||||
if (empty($MailboxData)) {
|
||||
try {
|
||||
$json = json_encode(
|
||||
array(
|
||||
"time" => time(),
|
||||
"ua" => $_SERVER['HTTP_USER_AGENT'],
|
||||
"user" => $email,
|
||||
"ip" => $_SERVER['REMOTE_ADDR'],
|
||||
"service" => "Error: mailbox not found or inactive"
|
||||
)
|
||||
);
|
||||
$redis->lPush('AUTODISCOVER_LOG', $json);
|
||||
$redis->lTrim('AUTODISCOVER_LOG', 0, 100);
|
||||
$redis->publish("F2B_CHANNEL", "Autodiscover: Invalid mailbox attempt by " . $_SERVER['REMOTE_ADDR']);
|
||||
error_log("Autodiscover: Invalid mailbox attempt by " . $_SERVER['REMOTE_ADDR']);
|
||||
}
|
||||
catch (RedisException $e) {
|
||||
// Silently fail
|
||||
}
|
||||
list($usec, $sec) = explode(' ', microtime());
|
||||
?>
|
||||
<Response>
|
||||
<Error Time="<?=date('H:i:s', $sec) . substr($usec, 0, strlen($usec) - 2);?>" Id="<?=rand(1000000000, 9999999999);?>">
|
||||
<ErrorCode>600</ErrorCode>
|
||||
<Message>Invalid Request</Message>
|
||||
<DebugData />
|
||||
</Error>
|
||||
</Response>
|
||||
</Autodiscover>
|
||||
<?php
|
||||
exit(0);
|
||||
}
|
||||
|
||||
if (!empty($MailboxData['name'])) {
|
||||
$displayname = $MailboxData['name'];
|
||||
}
|
||||
else {
|
||||
$displayname = $email;
|
||||
}
|
||||
try {
|
||||
$json = json_encode(
|
||||
array(
|
||||
"time" => time(),
|
||||
"ua" => $_SERVER['HTTP_USER_AGENT'],
|
||||
"user" => $email,
|
||||
"ip" => $_SERVER['REMOTE_ADDR'],
|
||||
"service" => $autodiscover_config['autodiscoverType']
|
||||
)
|
||||
);
|
||||
$redis->lPush('AUTODISCOVER_LOG', $json);
|
||||
$redis->lTrim('AUTODISCOVER_LOG', 0, 100);
|
||||
}
|
||||
catch (RedisException $e) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'msg' => 'Redis: '.$e
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if ($autodiscover_config['autodiscoverType'] == 'imap') {
|
||||
?>
|
||||
<Response xmlns="http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a">
|
||||
<User>
|
||||
@@ -236,6 +304,3 @@ if ($login_role === "user") {
|
||||
}
|
||||
?>
|
||||
</Autodiscover>
|
||||
<?php
|
||||
}
|
||||
?>
|
||||
|
||||
@@ -3,8 +3,11 @@ require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.domainadmin.inc.php';
|
||||
|
||||
if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'domainadmin') {
|
||||
header('Location: /domainadmin/mailbox');
|
||||
exit();
|
||||
// Only redirect to mailbox if NO pending actions
|
||||
if (empty($_SESSION['pending_tfa_setup']) && empty($_SESSION['pending_pw_update'])) {
|
||||
header('Location: /domainadmin/mailbox');
|
||||
exit();
|
||||
}
|
||||
}
|
||||
elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'admin') {
|
||||
header('Location: /admin/dashboard');
|
||||
|
||||
@@ -2,18 +2,7 @@
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.domainadmin.inc.php';
|
||||
|
||||
if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'admin') {
|
||||
header('Location: /admin/dashboard');
|
||||
exit();
|
||||
}
|
||||
elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user') {
|
||||
header('Location: /user');
|
||||
exit();
|
||||
}
|
||||
elseif (!isset($_SESSION['mailcow_cc_role']) || $_SESSION['mailcow_cc_role'] != "domainadmin") {
|
||||
header('Location: /domainadmin');
|
||||
exit();
|
||||
}
|
||||
protect_route(['domainadmin']);
|
||||
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php';
|
||||
$_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
|
||||
|
||||
@@ -2,41 +2,28 @@
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.domainadmin.inc.php';
|
||||
|
||||
if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'domainadmin') {
|
||||
/*
|
||||
/ DOMAIN ADMIN
|
||||
*/
|
||||
|
||||
/*
|
||||
/ DOMAIN ADMIN
|
||||
*/
|
||||
protect_route(['domainadmin']);
|
||||
|
||||
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"));
|
||||
$username = $_SESSION['mailcow_cc_username'];
|
||||
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"));
|
||||
$username = $_SESSION['mailcow_cc_username'];
|
||||
|
||||
$template = 'domainadmin.twig';
|
||||
$template_data = [
|
||||
'acl' => $_SESSION['acl'],
|
||||
'acl_json' => json_encode($_SESSION['acl']),
|
||||
'user_spam_score' => mailbox('get', 'spam_score', $username),
|
||||
'tfa_data' => $tfa_data,
|
||||
'fido2_data' => $fido2_data,
|
||||
'lang_user' => json_encode($lang['user']),
|
||||
'lang_datatables' => json_encode($lang['datatables']),
|
||||
];
|
||||
}
|
||||
elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'admin') {
|
||||
header('Location: /admin/dashboard');
|
||||
exit();
|
||||
}
|
||||
elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user') {
|
||||
header('Location: /user');
|
||||
exit();
|
||||
}
|
||||
else {
|
||||
header('Location: /domainadmin');
|
||||
exit();
|
||||
}
|
||||
$template = 'domainadmin.twig';
|
||||
$template_data = [
|
||||
'acl' => $_SESSION['acl'],
|
||||
'acl_json' => json_encode($_SESSION['acl']),
|
||||
'user_spam_score' => mailbox('get', 'spam_score', $username),
|
||||
'tfa_data' => $tfa_data,
|
||||
'fido2_data' => $fido2_data,
|
||||
'lang_user' => json_encode($lang['user']),
|
||||
'lang_datatables' => json_encode($lang['datatables']),
|
||||
];
|
||||
|
||||
$js_minifier->add('/web/js/site/user.js');
|
||||
$js_minifier->add('/web/js/site/pwgen.js');
|
||||
|
||||
+10
-6
@@ -1,10 +1,8 @@
|
||||
<?php
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
|
||||
$AuthUsers = array("admin", "domainadmin", "user");
|
||||
if (!isset($_SESSION['mailcow_cc_role']) OR !in_array($_SESSION['mailcow_cc_role'], $AuthUsers)) {
|
||||
header('Location: /');
|
||||
exit();
|
||||
}
|
||||
|
||||
protect_route();
|
||||
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php';
|
||||
|
||||
$template = 'edit.twig';
|
||||
@@ -206,9 +204,15 @@ if (isset($_SESSION['mailcow_cc_role'])) {
|
||||
if (isset($_GET['syncjob']) &&
|
||||
is_numeric($_GET['syncjob'])) {
|
||||
$id = $_GET["syncjob"];
|
||||
$result = mailbox('get', 'syncjob_details', $id);
|
||||
$result = syncjob('get', 'job', $id);
|
||||
$template = 'edit/syncjob.twig';
|
||||
}
|
||||
elseif (isset($_GET['syncjob_source']) &&
|
||||
is_numeric($_GET['syncjob_source'])) {
|
||||
$id = $_GET["syncjob_source"];
|
||||
$result = syncjob('get', 'source', array('id' => $id, 'with_secret' => true));
|
||||
$template = 'edit/imapsync_source.twig';
|
||||
}
|
||||
elseif (isset($_GET['filter']) &&
|
||||
is_numeric($_GET['filter'])) {
|
||||
$id = $_GET["filter"];
|
||||
|
||||
@@ -129,7 +129,16 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm
|
||||
);
|
||||
}
|
||||
|
||||
$mta_sts = mailbox('get', 'mta_sts', $domain);
|
||||
// Check if domain is an alias domain and get target domain's MTA-STS
|
||||
$alias_domain_details = mailbox('get', 'alias_domain_details', $domain);
|
||||
$mta_sts_domain = $domain;
|
||||
|
||||
if ($alias_domain_details !== false && !empty($alias_domain_details['target_domain'])) {
|
||||
// This is an alias domain, check target domain for MTA-STS
|
||||
$mta_sts_domain = $alias_domain_details['target_domain'];
|
||||
}
|
||||
|
||||
$mta_sts = mailbox('get', 'mta_sts', $mta_sts_domain);
|
||||
if (count($mta_sts) > 0 && $mta_sts['active'] == 1) {
|
||||
if (!in_array($domain, $alias_domains)) {
|
||||
$records[] = array(
|
||||
@@ -371,13 +380,13 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm
|
||||
else {
|
||||
$state = state_nomatch;
|
||||
}
|
||||
$state .= '<br />' . $current[$data_field[$current['type']]];
|
||||
$state .= '<br />' . htmlspecialchars($current[$data_field[$current['type']]]);
|
||||
}
|
||||
if ($current['type'] == 'TXT' &&
|
||||
stripos($current['txt'], 'v=dmarc') === 0 &&
|
||||
$record[2] == $dmarc_link) {
|
||||
$current['txt'] = str_replace(' ', '', $current['txt']);
|
||||
$state = $current[$data_field[$current['type']]] . state_optional;
|
||||
$state = htmlspecialchars($current[$data_field[$current['type']]]) . state_optional;
|
||||
}
|
||||
elseif ($current['type'] == 'TXT' &&
|
||||
stripos($current['txt'], 'v=spf') === 0 &&
|
||||
@@ -387,7 +396,7 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm
|
||||
if (in_array($ip, $rslt) && in_array(expand_ipv6($ip6), $rslt)) {
|
||||
$state = state_good;
|
||||
}
|
||||
$state .= '<br />' . $current[$data_field[$current['type']]] . state_optional;
|
||||
$state .= '<br />' . htmlspecialchars($current[$data_field[$current['type']]]) . state_optional;
|
||||
}
|
||||
elseif ($current['type'] == 'TXT' &&
|
||||
stripos($current['txt'], 'v=dkim') === 0 &&
|
||||
@@ -417,7 +426,7 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm
|
||||
if ($state == state_nomatch) {
|
||||
$state = array();
|
||||
foreach ($currents as $current) {
|
||||
$state[] = $current[$data_field[$current['type']]];
|
||||
$state[] = htmlspecialchars($current[$data_field[$current['type']]]);
|
||||
}
|
||||
$state = implode('<br />', $state);
|
||||
}
|
||||
@@ -427,12 +436,22 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm
|
||||
<td>%s</td>
|
||||
<td class="dns-found">%s</td>
|
||||
<td class="dns-recommended">%s</td>
|
||||
</tr>', $record[0], $record[1], $record[2], $state);
|
||||
</tr>', htmlspecialchars($record[0]), htmlspecialchars($record[1]), $record[2], $state);
|
||||
$record[3] = explode('<br />', $state);
|
||||
}
|
||||
|
||||
unset($record);
|
||||
|
||||
// Make a hostname RHS absolute so it is not read relative to $ORIGIN.
|
||||
// Already-absolute names, the SRV root target "." and IP addresses are left alone.
|
||||
$absolutize = function($host) {
|
||||
$host = trim($host);
|
||||
if ($host === '' || $host === '.' || substr($host, -1) === '.' || filter_var($host, FILTER_VALIDATE_IP)) {
|
||||
return $host;
|
||||
}
|
||||
return $host . '.';
|
||||
};
|
||||
|
||||
$dns_data = sprintf("\$ORIGIN %s.\n", $domain);
|
||||
foreach ($records as $record) {
|
||||
if ($domain == substr($record[0], -strlen($domain))) {
|
||||
@@ -453,22 +472,32 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm
|
||||
$val = str_replace(state_optional, '', $val);
|
||||
$val = str_replace(state_good, '', $val);
|
||||
if (strlen($val) > 0) {
|
||||
// these are all TXT values, their RHS is a character string, not a name
|
||||
$vals[] = sprintf("%s\tIN\t%s\t%s\n", $label, $record[1], $val);
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
if ($record[1] == 'MX' || $record[1] == 'CNAME') {
|
||||
$val = $absolutize($val);
|
||||
}
|
||||
elseif ($record[1] == 'SRV') {
|
||||
// format here is "target port"; only the target is a name
|
||||
$parts = explode(' ', $val, 2);
|
||||
$parts[0] = $absolutize($parts[0]);
|
||||
$val = implode(' ', $parts);
|
||||
}
|
||||
$vals[] = sprintf("%s\tIN\t%s\t%s\n", $label, $record[1], $val);
|
||||
}
|
||||
|
||||
foreach ($vals as $val) {
|
||||
$dns_data .= str_replace($domain, $domain . '.', $val);
|
||||
$dns_data .= $val;
|
||||
}
|
||||
}
|
||||
}
|
||||
?>
|
||||
</table>
|
||||
<a id='download-zonefile' class="btn btn-sm btn-secondary visible-xs-block visible-sm-inline visible-md-inline visible-lg-inline mb-4" style="margin-top:10px" data-zonefile="<?=base64_encode($dns_data);?>" download='<?=$_GET['domain'];?>.txt' type='text/csv'>Download</a>
|
||||
<a id='download-zonefile' class="btn btn-sm btn-secondary visible-xs-block visible-sm-inline visible-md-inline visible-lg-inline mb-4" style="margin-top:10px" data-zonefile="<?=base64_encode($dns_data);?>" download='<?=htmlspecialchars($_GET['domain']);?>.txt' type='text/csv'>Download</a>
|
||||
<script>
|
||||
var zonefile_dl_link = document.getElementById('download-zonefile');
|
||||
var zonefile = atob(zonefile_dl_link.getAttribute('data-zonefile'));
|
||||
|
||||
@@ -6,7 +6,7 @@ if (!isset($_SESSION['mailcow_cc_role'])) {
|
||||
}
|
||||
|
||||
if (isset($_GET['id']) && is_numeric($_GET['id'])) {
|
||||
if ($details = mailbox('get', 'syncjob_details', intval($_GET['id']))) {
|
||||
if ($details = syncjob('get', 'job', intval($_GET['id']))) {
|
||||
echo (empty($details['log'])) ? '-' : $details['log'];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
// Admins only
|
||||
if (!isset($_SESSION['mailcow_cc_role']) || $_SESSION['mailcow_cc_role'] !== 'admin') {
|
||||
http_response_code(403);
|
||||
echo json_encode(array('status' => 'error', 'message' => 'access denied'));
|
||||
exit;
|
||||
}
|
||||
|
||||
$owner = $GLOBALS['MAILCOW_GIT_OWNER'];
|
||||
$repo = $GLOBALS['MAILCOW_GIT_REPO'];
|
||||
$current = $GLOBALS['MAILCOW_GIT_VERSION'];
|
||||
|
||||
// Cache key is bound to the running version, so an update invalidates it naturally
|
||||
$cache_key = 'MAILCOW_UPDATE_CHECK/' . $current;
|
||||
|
||||
// Serve cached result if present (one GitHub call per TTL, not one per page load)
|
||||
$cached = $redis->get($cache_key);
|
||||
if ($cached !== false) {
|
||||
echo $cached;
|
||||
exit;
|
||||
}
|
||||
|
||||
// GitHub requires a User-Agent and rate-limits unauthenticated requests to 60/h per IP
|
||||
function github_get($url) {
|
||||
$ch = curl_init($url);
|
||||
curl_setopt_array($ch, array(
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_USERAGENT => 'mailcow-update-check',
|
||||
CURLOPT_TIMEOUT => 10,
|
||||
CURLOPT_HTTPHEADER => array('Accept: application/vnd.github+json'),
|
||||
));
|
||||
$body = curl_exec($ch);
|
||||
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
if ($body === false || $code !== 200) return false;
|
||||
$json = json_decode($body, true);
|
||||
return is_array($json) ? $json : false;
|
||||
}
|
||||
|
||||
$latest = github_get("https://api.github.com/repos/{$owner}/{$repo}/releases/latest");
|
||||
$release = github_get("https://api.github.com/repos/{$owner}/{$repo}/releases/tags/{$current}");
|
||||
|
||||
// Any failure (rate limit, offline, unexpected payload) -> honest error, cached briefly
|
||||
if ($latest === false || $release === false ||
|
||||
empty($latest['tag_name']) || empty($latest['created_at']) || empty($release['created_at'])) {
|
||||
$result = json_encode(array('status' => 'error', 'message' => 'could not reach GitHub API'));
|
||||
$redis->setex($cache_key, 300, $result);
|
||||
echo $result;
|
||||
exit;
|
||||
}
|
||||
|
||||
$date_latest = strtotime($latest['created_at']);
|
||||
$date_current = strtotime($release['created_at']);
|
||||
|
||||
if ($date_latest <= $date_current) {
|
||||
$result = json_encode(array('status' => 'no_update'));
|
||||
} else {
|
||||
$result = json_encode(array('status' => 'update_available', 'latest_tag' => $latest['tag_name']));
|
||||
}
|
||||
|
||||
// Cache the positive result for an hour
|
||||
$redis->setex($cache_key, 3600, $result);
|
||||
echo $result;
|
||||
@@ -33,11 +33,11 @@ if (array_key_exists('pending_tfa_methods', $_SESSION)) {
|
||||
if (isset($pending_tfa_authmechs['webauthn'])) {
|
||||
$pending_tfa_authmechs['webauthn'] = true;
|
||||
}
|
||||
if (!isset($pending_tfa_authmechs['webauthn'])
|
||||
if (!isset($pending_tfa_authmechs['webauthn'])
|
||||
&& isset($pending_tfa_authmechs['yubi_otp'])) {
|
||||
$pending_tfa_authmechs['yubi_otp'] = true;
|
||||
}
|
||||
if (!isset($pending_tfa_authmechs['webauthn'])
|
||||
if (!isset($pending_tfa_authmechs['webauthn'])
|
||||
&& !isset($pending_tfa_authmechs['yubi_otp'])
|
||||
&& isset($pending_tfa_authmechs['totp'])) {
|
||||
$pending_tfa_authmechs['totp'] = true;
|
||||
@@ -64,12 +64,15 @@ $globalVariables = [
|
||||
'pending_tfa_methods' => @$_SESSION['pending_tfa_methods'],
|
||||
'pending_tfa_authmechs' => $pending_tfa_authmechs,
|
||||
'pending_mailcow_cc_username' => @$_SESSION['pending_mailcow_cc_username'],
|
||||
'pending_tfa_setup' => !empty($_SESSION['pending_tfa_setup']),
|
||||
'pending_pw_update_modal' => !empty($_SESSION['pending_pw_update']),
|
||||
'lang_footer' => json_encode($lang['footer']),
|
||||
'lang_acl' => json_encode($lang['acl']),
|
||||
'lang_tfa' => json_encode($lang['tfa']),
|
||||
'lang_fido2' => json_encode($lang['fido2']),
|
||||
'lang_success' => json_encode($lang['success']),
|
||||
'lang_danger' => json_encode($lang['danger']),
|
||||
'lang_syncjobs' => json_encode($lang['syncjobs']),
|
||||
'docker_timeout' => $DOCKER_TIMEOUT,
|
||||
'session_lifetime' => (int)$SESSION_LIFETIME,
|
||||
'csrf_token' => $_SESSION['CSRF']['TOKEN'],
|
||||
|
||||
@@ -121,34 +121,56 @@ function admin($_action, $_data = null) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (!empty($password)) {
|
||||
if (password_check($password, $password2) !== true) {
|
||||
return false;
|
||||
// Check if this is a self password change via forced update
|
||||
if ($username == $_SESSION['mailcow_cc_username'] && !empty($_SESSION['pending_pw_update'])) {
|
||||
// Forced password update: only change password and clear force_pw_update flag
|
||||
if (!empty($password)) {
|
||||
if (password_check($password, $_data['password2']) !== true) {
|
||||
return false;
|
||||
}
|
||||
$password_hashed = hash_password($password);
|
||||
$stmt = $pdo->prepare("UPDATE `admin` SET `password` = :password_hashed,
|
||||
`attributes` = JSON_SET(COALESCE(`attributes`, '{}'), '$.force_pw_update', '0')
|
||||
WHERE `username` = :username");
|
||||
$stmt->execute(array(
|
||||
':password_hashed' => $password_hashed,
|
||||
':username' => $username
|
||||
));
|
||||
unset($_SESSION['pending_pw_update']);
|
||||
}
|
||||
$password_hashed = hash_password($password);
|
||||
$stmt = $pdo->prepare("UPDATE `admin` SET `username` = :username_new, `active` = :active, `password` = :password_hashed WHERE `username` = :username");
|
||||
$stmt->execute(array(
|
||||
':password_hashed' => $password_hashed,
|
||||
':username_new' => $username_new,
|
||||
':username' => $username,
|
||||
':active' => $active
|
||||
));
|
||||
if (isset($_data['disable_tfa'])) {
|
||||
$stmt = $pdo->prepare("UPDATE `tfa` SET `active` = '0' WHERE `username` = :username");
|
||||
$stmt->execute(array(':username' => $username));
|
||||
} else {
|
||||
// Normal admin edit: update all attributes
|
||||
$force_tfa = intval($_data['force_tfa'] ?? 0) ? 1 : 0;
|
||||
$force_pw_update = intval($_data['force_pw_update'] ?? 0) ? 1 : 0;
|
||||
if (!empty($password)) {
|
||||
if (password_check($password, $password2) !== true) {
|
||||
return false;
|
||||
}
|
||||
$password_hashed = hash_password($password);
|
||||
$stmt = $pdo->prepare("UPDATE `admin` SET `username` = :username_new, `active` = :active, `password` = :password_hashed,
|
||||
`attributes` = JSON_SET(COALESCE(`attributes`, '{}'), '$.force_tfa', :force_tfa, '$.force_pw_update', :force_pw_update)
|
||||
WHERE `username` = :username");
|
||||
$stmt->execute(array(
|
||||
':password_hashed' => $password_hashed,
|
||||
':username_new' => $username_new,
|
||||
':username' => $username,
|
||||
':active' => $active,
|
||||
':force_tfa' => strval($force_tfa),
|
||||
':force_pw_update' => strval($force_pw_update)
|
||||
));
|
||||
}
|
||||
else {
|
||||
$stmt = $pdo->prepare("UPDATE `tfa` SET `username` = :username_new WHERE `username` = :username");
|
||||
$stmt->execute(array(':username_new' => $username_new, ':username' => $username));
|
||||
$stmt = $pdo->prepare("UPDATE `admin` SET `username` = :username_new, `active` = :active,
|
||||
`attributes` = JSON_SET(COALESCE(`attributes`, '{}'), '$.force_tfa', :force_tfa, '$.force_pw_update', :force_pw_update)
|
||||
WHERE `username` = :username");
|
||||
$stmt->execute(array(
|
||||
':username_new' => $username_new,
|
||||
':username' => $username,
|
||||
':active' => $active,
|
||||
':force_tfa' => strval($force_tfa),
|
||||
':force_pw_update' => strval($force_pw_update)
|
||||
));
|
||||
}
|
||||
}
|
||||
else {
|
||||
$stmt = $pdo->prepare("UPDATE `admin` SET `username` = :username_new, `active` = :active WHERE `username` = :username");
|
||||
$stmt->execute(array(
|
||||
':username_new' => $username_new,
|
||||
':username' => $username,
|
||||
':active' => $active
|
||||
));
|
||||
if (isset($_data['disable_tfa'])) {
|
||||
$stmt = $pdo->prepare("UPDATE `tfa` SET `active` = '0' WHERE `username` = :username");
|
||||
$stmt->execute(array(':username' => $username));
|
||||
@@ -223,7 +245,8 @@ function admin($_action, $_data = null) {
|
||||
`tfa`.`active` AS `tfa_active`,
|
||||
`admin`.`username`,
|
||||
`admin`.`created`,
|
||||
`admin`.`active` AS `active`
|
||||
`admin`.`active` AS `active`,
|
||||
`admin`.`attributes` AS `attributes`
|
||||
FROM `admin`
|
||||
LEFT OUTER JOIN `tfa` ON `tfa`.`username`=`admin`.`username`
|
||||
WHERE `admin`.`username`= :admin AND `superadmin` = '1'");
|
||||
@@ -240,6 +263,7 @@ function admin($_action, $_data = null) {
|
||||
$admindata['active'] = $row['active'];
|
||||
$admindata['active_int'] = $row['active'];
|
||||
$admindata['created'] = $row['created'];
|
||||
$admindata['attributes'] = json_decode($row['attributes'], true) ?? array('force_tfa' => '0', 'force_pw_update' => '0');
|
||||
return $admindata;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?php
|
||||
function app_passwd($_action, $_data = null) {
|
||||
global $pdo;
|
||||
global $lang;
|
||||
global $pdo;
|
||||
global $lang;
|
||||
$_data_log = $_data;
|
||||
!isset($_data_log['app_passwd']) ?: $_data_log['app_passwd'] = '*';
|
||||
!isset($_data_log['app_passwd2']) ?: $_data_log['app_passwd2'] = '*';
|
||||
@@ -43,20 +43,7 @@ function app_passwd($_action, $_data = null) {
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if (!preg_match('/' . $GLOBALS['PASSWD_REGEP'] . '/', $password)) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => 'password_complexity'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if ($password != $password2) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => 'password_mismatch'
|
||||
);
|
||||
if (password_check($password, $password2) !== true) {
|
||||
return false;
|
||||
}
|
||||
$password_hashed = hash_password($password);
|
||||
@@ -88,15 +75,15 @@ function app_passwd($_action, $_data = null) {
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => 'app_passwd_added'
|
||||
);
|
||||
break;
|
||||
break;
|
||||
case 'edit':
|
||||
$ids = (array)$_data['id'];
|
||||
foreach ($ids as $id) {
|
||||
$is_now = app_passwd('details', $id);
|
||||
if (!empty($is_now)) {
|
||||
$app_name = (!empty($_data['app_name'])) ? $_data['app_name'] : $is_now['name'];
|
||||
$password = (!empty($_data['password'])) ? $_data['password'] : null;
|
||||
$password2 = (!empty($_data['password2'])) ? $_data['password2'] : null;
|
||||
$password = (!empty($_data['app_passwd'])) ? $_data['app_passwd'] : null;
|
||||
$password2 = (!empty($_data['app_passwd2'])) ? $_data['app_passwd2'] : null;
|
||||
if (isset($_data['protocols'])) {
|
||||
$protocols = (array)$_data['protocols'];
|
||||
$imap_access = (in_array('imap_access', $protocols)) ? 1 : 0;
|
||||
@@ -126,20 +113,7 @@ function app_passwd($_action, $_data = null) {
|
||||
}
|
||||
$app_name = htmlspecialchars(trim($app_name));
|
||||
if (!empty($password) && !empty($password2)) {
|
||||
if (!preg_match('/' . $GLOBALS['PASSWD_REGEP'] . '/', $password)) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
||||
'msg' => 'password_complexity'
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if ($password != $password2) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
||||
'msg' => 'password_mismatch'
|
||||
);
|
||||
if (password_check($password, $password2) !== true) {
|
||||
continue;
|
||||
}
|
||||
$password_hashed = hash_password($password);
|
||||
@@ -182,7 +156,7 @@ function app_passwd($_action, $_data = null) {
|
||||
'msg' => array('object_modified', htmlspecialchars(implode(', ', $ids)))
|
||||
);
|
||||
}
|
||||
break;
|
||||
break;
|
||||
case 'delete':
|
||||
$ids = (array)$_data['id'];
|
||||
foreach ($ids as $id) {
|
||||
@@ -213,19 +187,17 @@ function app_passwd($_action, $_data = null) {
|
||||
'msg' => array('app_passwd_removed', htmlspecialchars($id))
|
||||
);
|
||||
}
|
||||
break;
|
||||
break;
|
||||
case 'get':
|
||||
$app_passwds = array();
|
||||
$stmt = $pdo->prepare("SELECT `id`, `name` FROM `app_passwd` WHERE `mailbox` = :username");
|
||||
$stmt->execute(array(':username' => $username));
|
||||
$app_passwds = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
return $app_passwds;
|
||||
break;
|
||||
break;
|
||||
case 'details':
|
||||
$app_passwd_data = array();
|
||||
$stmt = $pdo->prepare("SELECT *
|
||||
FROM `app_passwd`
|
||||
WHERE `id` = :id");
|
||||
$stmt = $pdo->prepare("SELECT * FROM `app_passwd` WHERE `id` = :id");
|
||||
$stmt->execute(array(':id' => $_data));
|
||||
$app_passwd_data = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
if (empty($app_passwd_data)) {
|
||||
@@ -237,6 +209,6 @@ function app_passwd($_action, $_data = null) {
|
||||
}
|
||||
$app_passwd_data['name'] = htmlspecialchars(trim($app_passwd_data['name']));
|
||||
return $app_passwd_data;
|
||||
break;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
<?php
|
||||
function check_login($user, $pass, $app_passwd_data = false, $extra = null) {
|
||||
function check_login($user, $pass, $extra = null) {
|
||||
global $pdo;
|
||||
global $redis;
|
||||
|
||||
$is_internal = $extra['is_internal'];
|
||||
$role = $extra['role'];
|
||||
$extra['service'] = !isset($extra['service']) ? 'NONE' : $extra['service'];
|
||||
|
||||
// Try validate admin
|
||||
if (!isset($role) || $role == "admin") {
|
||||
@@ -25,34 +26,20 @@ function check_login($user, $pass, $app_passwd_data = false, $extra = null) {
|
||||
|
||||
// Try validate app password
|
||||
if (!isset($role) || $role == "app") {
|
||||
$result = apppass_login($user, $pass, $app_passwd_data);
|
||||
$result = apppass_login($user, $pass, $extra);
|
||||
if ($result !== false) {
|
||||
if ($app_passwd_data['eas'] === true) {
|
||||
$service = 'EAS';
|
||||
} elseif ($app_passwd_data['dav'] === true) {
|
||||
$service = 'DAV';
|
||||
} else {
|
||||
$service = 'NONE';
|
||||
}
|
||||
$real_rip = ($_SERVER['HTTP_X_REAL_IP'] ?? $_SERVER['REMOTE_ADDR']);
|
||||
set_sasl_log($user, $real_rip, $service, $pass);
|
||||
set_sasl_log($user, $real_rip, $extra['service'], $pass);
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
// Try validate user
|
||||
if (!isset($role) || $role == "user") {
|
||||
$result = user_login($user, $pass);
|
||||
$result = user_login($user, $pass, $extra);
|
||||
if ($result !== false) {
|
||||
if ($app_passwd_data['eas'] === true) {
|
||||
$service = 'EAS';
|
||||
} elseif ($app_passwd_data['dav'] === true) {
|
||||
$service = 'DAV';
|
||||
} else {
|
||||
$service = 'MAILCOWUI';
|
||||
}
|
||||
$real_rip = ($_SERVER['HTTP_X_REAL_IP'] ?? $_SERVER['REMOTE_ADDR']);
|
||||
set_sasl_log($user, $real_rip, $service);
|
||||
set_sasl_log($user, $real_rip, $extra['service']);
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
@@ -95,7 +82,7 @@ function admin_login($user, $pass){
|
||||
}
|
||||
|
||||
$user = strtolower(trim($user));
|
||||
$stmt = $pdo->prepare("SELECT `password` FROM `admin`
|
||||
$stmt = $pdo->prepare("SELECT `password`, `attributes` FROM `admin`
|
||||
WHERE `superadmin` = '1'
|
||||
AND `active` = '1'
|
||||
AND `username` = :user");
|
||||
@@ -104,6 +91,13 @@ function admin_login($user, $pass){
|
||||
|
||||
// verify password
|
||||
if (verify_hash($row['password'], $pass)) {
|
||||
$admin_attrs = json_decode($row['attributes'], true) ?? [];
|
||||
|
||||
// Check force_pw_update
|
||||
if (intval($admin_attrs['force_pw_update'] ?? 0) == 1) {
|
||||
$_SESSION['pending_pw_update'] = true;
|
||||
}
|
||||
|
||||
// check for tfa authenticators
|
||||
$authenticators = get_tfa($user);
|
||||
if (isset($authenticators['additional']) && is_array($authenticators['additional']) && count($authenticators['additional']) > 0) {
|
||||
@@ -123,6 +117,10 @@ function admin_login($user, $pass){
|
||||
// Reactivate TFA if it was set to "deactivate TFA for next login"
|
||||
$stmt = $pdo->prepare("UPDATE `tfa` SET `active`='1' WHERE `username` = :user");
|
||||
$stmt->execute(array(':user' => $user));
|
||||
// Check force_tfa: only force setup if NO TFA exists at all
|
||||
if (intval($admin_attrs['force_tfa'] ?? 0) == 1 && !tfa_exists($user)) {
|
||||
$_SESSION['pending_tfa_setup'] = true;
|
||||
}
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $user, '*'),
|
||||
@@ -148,7 +146,7 @@ function domainadmin_login($user, $pass){
|
||||
return false;
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare("SELECT `password` FROM `admin`
|
||||
$stmt = $pdo->prepare("SELECT `password`, `attributes` FROM `admin`
|
||||
WHERE `superadmin` = '0'
|
||||
AND `active`='1'
|
||||
AND `username` = :user");
|
||||
@@ -157,6 +155,13 @@ function domainadmin_login($user, $pass){
|
||||
|
||||
// verify password
|
||||
if (verify_hash($row['password'], $pass) !== false) {
|
||||
$admin_attrs = json_decode($row['attributes'], true) ?? [];
|
||||
|
||||
// Check force_pw_update
|
||||
if (intval($admin_attrs['force_pw_update'] ?? 0) == 1) {
|
||||
$_SESSION['pending_pw_update'] = true;
|
||||
}
|
||||
|
||||
// check for tfa authenticators
|
||||
$authenticators = get_tfa($user);
|
||||
if (isset($authenticators['additional']) && is_array($authenticators['additional']) && count($authenticators['additional']) > 0) {
|
||||
@@ -176,6 +181,10 @@ function domainadmin_login($user, $pass){
|
||||
// Reactivate TFA if it was set to "deactivate TFA for next login"
|
||||
$stmt = $pdo->prepare("UPDATE `tfa` SET `active`='1' WHERE `username` = :user");
|
||||
$stmt->execute(array(':user' => $user));
|
||||
// Check force_tfa: only force setup if NO TFA exists at all
|
||||
if (intval($admin_attrs['force_tfa'] ?? 0) == 1 && !tfa_exists($user)) {
|
||||
$_SESSION['pending_tfa_setup'] = true;
|
||||
}
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $user, '*'),
|
||||
@@ -193,7 +202,7 @@ function user_login($user, $pass, $extra = null){
|
||||
global $iam_settings;
|
||||
|
||||
$is_internal = $extra['is_internal'];
|
||||
$service = $extra['service'];
|
||||
$extra['service'] = !isset($extra['service']) ? 'NONE' : $extra['service'];
|
||||
|
||||
if (!filter_var($user, FILTER_VALIDATE_EMAIL) && !ctype_alnum(str_replace(array('_', '.', '-'), '', $user))) {
|
||||
if (!$is_internal){
|
||||
@@ -236,10 +245,10 @@ function user_login($user, $pass, $extra = null){
|
||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!empty($row)) {
|
||||
// check if user has access to service (imap, smtp, pop3, sieve) if service is set
|
||||
// check if user has access to service (imap, smtp, pop3, sieve, dav, eas) if service is set
|
||||
$row['attributes'] = json_decode($row['attributes'], true);
|
||||
if (isset($service)) {
|
||||
$key = strtolower($service) . "_access";
|
||||
if ($extra['service'] != 'NONE') {
|
||||
$key = strtolower($extra['service']) . "_access";
|
||||
if (isset($row['attributes'][$key]) && $row['attributes'][$key] != '1') {
|
||||
return false;
|
||||
}
|
||||
@@ -253,8 +262,8 @@ function user_login($user, $pass, $extra = null){
|
||||
|
||||
// check if user has access to service (imap, smtp, pop3, sieve) if service is set
|
||||
$row['attributes'] = json_decode($row['attributes'], true);
|
||||
if (isset($service)) {
|
||||
$key = strtolower($service) . "_access";
|
||||
if ($extra['service'] != 'NONE') {
|
||||
$key = strtolower($extra['service']) . "_access";
|
||||
if (isset($row['attributes'][$key]) && $row['attributes'][$key] != '1') {
|
||||
return false;
|
||||
}
|
||||
@@ -278,6 +287,8 @@ function user_login($user, $pass, $extra = null){
|
||||
return false;
|
||||
}
|
||||
|
||||
$row['attributes'] = json_decode($row['attributes'], true);
|
||||
|
||||
// check for tfa authenticators
|
||||
$authenticators = get_tfa($user);
|
||||
if (isset($authenticators['additional']) && is_array($authenticators['additional']) && count($authenticators['additional']) > 0 && !$is_internal) {
|
||||
@@ -299,6 +310,10 @@ function user_login($user, $pass, $extra = null){
|
||||
// Reactivate TFA if it was set to "deactivate TFA for next login"
|
||||
$stmt = $pdo->prepare("UPDATE `tfa` SET `active`='1' WHERE `username` = :user");
|
||||
$stmt->execute(array(':user' => $user));
|
||||
// Check force_tfa: only force setup if NO TFA exists at all
|
||||
if (intval($row['attributes']['force_tfa']) == 1 && !tfa_exists($user)) {
|
||||
$_SESSION['pending_tfa_setup'] = true;
|
||||
}
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $user, '*', 'Provider: Keycloak'),
|
||||
@@ -330,6 +345,8 @@ function user_login($user, $pass, $extra = null){
|
||||
return false;
|
||||
}
|
||||
|
||||
$row['attributes'] = json_decode($row['attributes'], true);
|
||||
|
||||
// check for tfa authenticators
|
||||
$authenticators = get_tfa($user);
|
||||
if (isset($authenticators['additional']) && is_array($authenticators['additional']) && count($authenticators['additional']) > 0 && !$is_internal) {
|
||||
@@ -351,6 +368,10 @@ function user_login($user, $pass, $extra = null){
|
||||
// Reactivate TFA if it was set to "deactivate TFA for next login"
|
||||
$stmt = $pdo->prepare("UPDATE `tfa` SET `active`='1' WHERE `username` = :user");
|
||||
$stmt->execute(array(':user' => $user));
|
||||
// Check force_tfa: only force setup if NO TFA exists at all
|
||||
if (intval($row['attributes']['force_tfa']) == 1 && !tfa_exists($user)) {
|
||||
$_SESSION['pending_tfa_setup'] = true;
|
||||
}
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $user, '*', 'Provider: LDAP'),
|
||||
@@ -394,6 +415,10 @@ function user_login($user, $pass, $extra = null){
|
||||
// Reactivate TFA if it was set to "deactivate TFA for next login"
|
||||
$stmt = $pdo->prepare("UPDATE `tfa` SET `active`='1' WHERE `username` = :user");
|
||||
$stmt->execute(array(':user' => $user));
|
||||
// Check force_tfa: only force setup if NO TFA exists at all
|
||||
if (intval($row['attributes']['force_tfa']) == 1 && !tfa_exists($user)) {
|
||||
$_SESSION['pending_tfa_setup'] = true;
|
||||
}
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $user, '*', 'Provider: mailcow'),
|
||||
@@ -408,7 +433,7 @@ function user_login($user, $pass, $extra = null){
|
||||
|
||||
return false;
|
||||
}
|
||||
function apppass_login($user, $pass, $app_passwd_data, $extra = null){
|
||||
function apppass_login($user, $pass, $extra = null){
|
||||
global $pdo;
|
||||
|
||||
$is_internal = $extra['is_internal'];
|
||||
@@ -424,20 +449,8 @@ function apppass_login($user, $pass, $app_passwd_data, $extra = null){
|
||||
return false;
|
||||
}
|
||||
|
||||
$protocol = false;
|
||||
if ($app_passwd_data['eas']){
|
||||
$protocol = 'eas';
|
||||
} else if ($app_passwd_data['dav']){
|
||||
$protocol = 'dav';
|
||||
} else if ($app_passwd_data['smtp']){
|
||||
$protocol = 'smtp';
|
||||
} else if ($app_passwd_data['imap']){
|
||||
$protocol = 'imap';
|
||||
} else if ($app_passwd_data['sieve']){
|
||||
$protocol = 'sieve';
|
||||
} else if ($app_passwd_data['pop3']){
|
||||
$protocol = 'pop3';
|
||||
} else if (!$is_internal) {
|
||||
$extra['service'] = !isset($extra['service']) ? 'NONE' : $extra['service'];
|
||||
if (!$is_internal && $extra['service'] == 'NONE') {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -458,7 +471,7 @@ function apppass_login($user, $pass, $app_passwd_data, $extra = null){
|
||||
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
foreach ($rows as $row) {
|
||||
if ($protocol && $row[$protocol . '_access'] != '1'){
|
||||
if ($extra['service'] != 'NONE' && $row[strtolower($extra['service']) . '_access'] != '1'){
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -325,8 +325,10 @@ function customize($_action, $_item, $_data = null) {
|
||||
break;
|
||||
case 'ui_texts':
|
||||
try {
|
||||
$data['title_name'] = ($title_name = $redis->get('TITLE_NAME')) ? $title_name : 'mailcow UI';
|
||||
$data['main_name'] = ($main_name = $redis->get('MAIN_NAME')) ? $main_name : 'mailcow UI';
|
||||
$mailcow_hostname = strtolower(getenv("MAILCOW_HOSTNAME"));
|
||||
|
||||
$data['title_name'] = ($title_name = $redis->get('TITLE_NAME')) ? $title_name : "$mailcow_hostname - mail UI";
|
||||
$data['main_name'] = ($main_name = $redis->get('MAIN_NAME')) ? $main_name : "$mailcow_hostname - mail UI";
|
||||
$data['apps_name'] = ($apps_name = $redis->get('APPS_NAME')) ? $apps_name : $lang['header']['apps'];
|
||||
$data['help_text'] = ($help_text = $redis->get('HELP_TEXT')) ? $help_text : false;
|
||||
if (!empty($redis->get('UI_IMPRESS'))) {
|
||||
|
||||
@@ -63,7 +63,7 @@ function docker($action, $service_name = null, $attr1 = null, $attr2 = null, $ex
|
||||
break;
|
||||
case 'info':
|
||||
if (empty($service_name)) {
|
||||
curl_setopt($curl, CURLOPT_URL, 'https://dockerapi:443/containers/json');
|
||||
curl_setopt($curl, CURLOPT_URL, 'https://dockerapi:443/containers/json?all=true');
|
||||
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
|
||||
curl_setopt($curl, CURLOPT_POST, 0);
|
||||
curl_setopt($curl, CURLOPT_TIMEOUT, $DOCKER_TIMEOUT);
|
||||
|
||||
@@ -195,17 +195,23 @@ function domain_admin($_action, $_data = null) {
|
||||
));
|
||||
}
|
||||
}
|
||||
$force_tfa = intval($_data['force_tfa'] ?? 0) ? 1 : 0;
|
||||
$force_pw_update = intval($_data['force_pw_update'] ?? 0) ? 1 : 0;
|
||||
if (!empty($password)) {
|
||||
if (password_check($password, $password2) !== true) {
|
||||
return false;
|
||||
}
|
||||
$password_hashed = hash_password($password);
|
||||
$stmt = $pdo->prepare("UPDATE `admin` SET `username` = :username_new, `active` = :active, `password` = :password_hashed WHERE `username` = :username");
|
||||
$stmt = $pdo->prepare("UPDATE `admin` SET `username` = :username_new, `active` = :active, `password` = :password_hashed,
|
||||
`attributes` = JSON_SET(COALESCE(`attributes`, '{}'), '$.force_tfa', :force_tfa, '$.force_pw_update', :force_pw_update)
|
||||
WHERE `username` = :username");
|
||||
$stmt->execute(array(
|
||||
':password_hashed' => $password_hashed,
|
||||
':username_new' => $username_new,
|
||||
':username' => $username,
|
||||
':active' => $active
|
||||
':active' => $active,
|
||||
':force_tfa' => strval($force_tfa),
|
||||
':force_pw_update' => strval($force_pw_update)
|
||||
));
|
||||
if (isset($_data['disable_tfa'])) {
|
||||
$stmt = $pdo->prepare("UPDATE `tfa` SET `active` = '0' WHERE `username` = :username");
|
||||
@@ -217,11 +223,15 @@ function domain_admin($_action, $_data = null) {
|
||||
}
|
||||
}
|
||||
else {
|
||||
$stmt = $pdo->prepare("UPDATE `admin` SET `username` = :username_new, `active` = :active WHERE `username` = :username");
|
||||
$stmt = $pdo->prepare("UPDATE `admin` SET `username` = :username_new, `active` = :active,
|
||||
`attributes` = JSON_SET(COALESCE(`attributes`, '{}'), '$.force_tfa', :force_tfa, '$.force_pw_update', :force_pw_update)
|
||||
WHERE `username` = :username");
|
||||
$stmt->execute(array(
|
||||
':username_new' => $username_new,
|
||||
':username' => $username,
|
||||
':active' => $active
|
||||
':active' => $active,
|
||||
':force_tfa' => strval($force_tfa),
|
||||
':force_pw_update' => strval($force_pw_update)
|
||||
));
|
||||
if (isset($_data['disable_tfa'])) {
|
||||
$stmt = $pdo->prepare("UPDATE `tfa` SET `active` = '0' WHERE `username` = :username");
|
||||
@@ -244,31 +254,37 @@ function domain_admin($_action, $_data = null) {
|
||||
// Can only edit itself
|
||||
elseif ($_SESSION['mailcow_cc_role'] == "domainadmin") {
|
||||
$username = $_SESSION['mailcow_cc_username'];
|
||||
$password_old = $_data['user_old_pass'];
|
||||
$password_old = $_data['user_old_pass'] ?? '';
|
||||
$password_new = $_data['user_new_pass'];
|
||||
$password_new2 = $_data['user_new_pass2'];
|
||||
|
||||
$stmt = $pdo->prepare("SELECT `password` FROM `admin`
|
||||
WHERE `username` = :user");
|
||||
$stmt->execute(array(':user' => $username));
|
||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
if (!verify_hash($row['password'], $password_old)) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
return false;
|
||||
// Only verify old password if this is NOT a forced password update
|
||||
if (empty($_SESSION['pending_pw_update'])) {
|
||||
$stmt = $pdo->prepare("SELECT `password` FROM `admin`
|
||||
WHERE `username` = :user");
|
||||
$stmt->execute(array(':user' => $username));
|
||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
if (!verify_hash($row['password'], $password_old)) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (password_check($password_new, $password_new2) !== true) {
|
||||
return false;
|
||||
}
|
||||
$password_hashed = hash_password($password_new);
|
||||
$stmt = $pdo->prepare("UPDATE `admin` SET `password` = :password_hashed WHERE `username` = :username");
|
||||
$stmt = $pdo->prepare("UPDATE `admin` SET `password` = :password_hashed,
|
||||
`attributes` = JSON_SET(COALESCE(`attributes`, '{}'), '$.force_pw_update', '0')
|
||||
WHERE `username` = :username");
|
||||
$stmt->execute(array(
|
||||
':password_hashed' => $password_hashed,
|
||||
':username' => $username
|
||||
));
|
||||
unset($_SESSION['pending_pw_update']);
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
@@ -360,9 +376,11 @@ function domain_admin($_action, $_data = null) {
|
||||
`tfa`.`active` AS `tfa_active`,
|
||||
`domain_admins`.`username`,
|
||||
`domain_admins`.`created`,
|
||||
`domain_admins`.`active` AS `active`
|
||||
`domain_admins`.`active` AS `active`,
|
||||
`admin`.`attributes` AS `attributes`
|
||||
FROM `domain_admins`
|
||||
LEFT OUTER JOIN `tfa` ON `tfa`.`username`=`domain_admins`.`username`
|
||||
LEFT OUTER JOIN `admin` ON `admin`.`username`=`domain_admins`.`username`
|
||||
WHERE `domain_admins`.`username`= :domain_admin");
|
||||
$stmt->execute(array(
|
||||
':domain_admin' => $_data
|
||||
@@ -377,6 +395,7 @@ function domain_admin($_action, $_data = null) {
|
||||
$domainadmindata['active'] = $row['active'];
|
||||
$domainadmindata['active_int'] = $row['active'];
|
||||
$domainadmindata['created'] = $row['created'];
|
||||
$domainadmindata['attributes'] = json_decode($row['attributes'], true) ?? array('force_tfa' => '0', 'force_pw_update' => '0');
|
||||
// GET SELECTED
|
||||
$stmt = $pdo->prepare("SELECT `domain` FROM `domain`
|
||||
WHERE `domain` IN (
|
||||
@@ -410,14 +429,25 @@ function domain_admin_sso($_action, $_data) {
|
||||
|
||||
switch ($_action) {
|
||||
case 'check':
|
||||
$token = $_data;
|
||||
$token = preg_replace('/[^a-zA-Z0-9-]/', '', $_data);
|
||||
|
||||
$stmt = $pdo->prepare("SELECT `t1`.`username` FROM `da_sso` AS `t1` JOIN `admin` AS `t2` ON `t1`.`username` = `t2`.`username` WHERE `t1`.`token` = :token AND `t1`.`created` > DATE_SUB(NOW(), INTERVAL '30' SECOND) AND `t2`.`active` = 1 AND `t2`.`superadmin` = 0;");
|
||||
$stmt->execute(array(
|
||||
':token' => preg_replace('/[^a-zA-Z0-9-]/', '', $token)
|
||||
':token' => $token
|
||||
));
|
||||
$return = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
return empty($return['username']) ? false : $return['username'];
|
||||
if (empty($return['username'])) {
|
||||
return false;
|
||||
}
|
||||
// single-use: consume the token; if a concurrent request already used it, deny
|
||||
$del = $pdo->prepare("DELETE FROM `da_sso` WHERE `token` = :token");
|
||||
$del->execute(array(
|
||||
':token' => $token
|
||||
));
|
||||
if ($del->rowCount() < 1) {
|
||||
return false;
|
||||
}
|
||||
return $return['username'];
|
||||
case 'issue':
|
||||
if ($_SESSION['mailcow_cc_role'] != "admin") {
|
||||
$_SESSION['return'][] = array(
|
||||
|
||||
@@ -108,6 +108,14 @@ function fwdhost($_action, $_data = null) {
|
||||
}
|
||||
break;
|
||||
case 'delete':
|
||||
if ($_SESSION['mailcow_cc_role'] != "admin") {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
$hosts = (array)$_data['forwardinghost'];
|
||||
foreach ($hosts as $host) {
|
||||
try {
|
||||
|
||||
+319
-63
@@ -53,6 +53,39 @@ function valid_network($network) {
|
||||
function valid_hostname($hostname) {
|
||||
return filter_var($hostname, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME);
|
||||
}
|
||||
// Validates a browser-style Origin: scheme://host[:port], no path/query/fragment/credentials
|
||||
function valid_origin($origin) {
|
||||
$parts = parse_url($origin);
|
||||
if ($parts === false || !isset($parts['scheme']) || !isset($parts['host'])) {
|
||||
return false;
|
||||
}
|
||||
if (!in_array(strtolower($parts['scheme']), array('http', 'https'), true)) {
|
||||
return false;
|
||||
}
|
||||
if (isset($parts['user']) || isset($parts['pass']) || isset($parts['path']) || isset($parts['query']) || isset($parts['fragment'])) {
|
||||
return false;
|
||||
}
|
||||
$host = $parts['host'];
|
||||
$host_without_brackets = trim($host, '[]');
|
||||
if (!valid_hostname($host_without_brackets) && !filter_var($host_without_brackets, FILTER_VALIDATE_IP)) {
|
||||
return false;
|
||||
}
|
||||
// Reject anything that doesn't round-trip to the exact same origin (e.g. stray trailing slash)
|
||||
$port = isset($parts['port']) ? ':' . $parts['port'] : '';
|
||||
$rebuilt = strtolower($parts['scheme']) . '://' . strtolower($host) . $port;
|
||||
return strtolower($origin) === $rebuilt;
|
||||
}
|
||||
// Bring an origin into the exact form a browser sends it (scheme://host[:port], lowercase).
|
||||
function normalize_cors_origin($origin) {
|
||||
$origin = trim($origin);
|
||||
if ($origin === '*') {
|
||||
return '*';
|
||||
}
|
||||
if ($origin !== '' && !preg_match('~^[a-z][a-z0-9+.-]*://~i', $origin)) {
|
||||
$origin = 'https://' . $origin;
|
||||
}
|
||||
return valid_origin($origin) ? strtolower($origin) : false;
|
||||
}
|
||||
// Thanks to https://stackoverflow.com/a/49373789
|
||||
// Validates exact ip matches and ip-in-cidr, ipv4 and ipv6
|
||||
function ip_acl($ip, $networks) {
|
||||
@@ -205,6 +238,42 @@ function password_complexity($_action, $_data = null) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function password_generate(){
|
||||
$password_complexity = password_complexity('get');
|
||||
$min_length = max(16, intval($password_complexity['length']));
|
||||
|
||||
$lowercase = range('a', 'z');
|
||||
$uppercase = range('A', 'Z');
|
||||
$digits = range(0, 9);
|
||||
$special_chars = str_split('!@#$%^&*()?=');
|
||||
|
||||
$password = [
|
||||
$lowercase[random_int(0, count($lowercase) - 1)],
|
||||
$uppercase[random_int(0, count($uppercase) - 1)],
|
||||
$digits[random_int(0, count($digits) - 1)],
|
||||
$special_chars[random_int(0, count($special_chars) - 1)],
|
||||
];
|
||||
|
||||
$all = array_merge($lowercase, $uppercase, $digits, $special_chars);
|
||||
|
||||
while (count($password) < $min_length) {
|
||||
$password[] = $all[random_int(0, count($all) - 1)];
|
||||
}
|
||||
|
||||
// Cryptographically secure shuffle using Fisher-Yates algorithm
|
||||
$count = count($password);
|
||||
for ($i = $count - 1; $i > 0; $i--) {
|
||||
$j = random_int(0, $i);
|
||||
$temp = $password[$i];
|
||||
$password[$i] = $password[$j];
|
||||
$password[$j] = $temp;
|
||||
}
|
||||
|
||||
return implode('', $password);
|
||||
|
||||
}
|
||||
|
||||
function password_check($password1, $password2) {
|
||||
$password_complexity = password_complexity('get');
|
||||
|
||||
@@ -522,7 +591,7 @@ function logger($_data = false) {
|
||||
$type = $return['type'];
|
||||
$msg = null;
|
||||
if (isset($return['msg'])) {
|
||||
$msg = json_encode($return['msg'], JSON_UNESCAPED_UNICODE);
|
||||
$msg = json_encode($return['msg'], JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT);
|
||||
}
|
||||
$call = null;
|
||||
if (isset($return['log'])) {
|
||||
@@ -564,6 +633,18 @@ function logger($_data = false) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
function is_local_mailcow_domain($domain) {
|
||||
// True if domain is a locally managed, active primary or alias domain
|
||||
global $pdo;
|
||||
$domain = idn_to_ascii($domain, 0, INTL_IDNA_VARIANT_UTS46);
|
||||
if (empty($domain)) {
|
||||
return false;
|
||||
}
|
||||
$stmt = $pdo->prepare("SELECT 1 FROM `domain` WHERE `domain` = :d AND `active` = 1
|
||||
UNION SELECT 1 FROM `alias_domain` WHERE `alias_domain` = :d2 AND `active` = 1 LIMIT 1");
|
||||
$stmt->execute(array(':d' => $domain, ':d2' => $domain));
|
||||
return (bool)$stmt->fetchColumn();
|
||||
}
|
||||
function hasDomainAccess($username, $role, $domain) {
|
||||
global $pdo;
|
||||
if (empty($domain) || !is_valid_domain_name($domain)) {
|
||||
@@ -814,6 +895,32 @@ function verify_hash($hash, $password) {
|
||||
$hash = $components[4];
|
||||
return hash_equals(hash_pbkdf2('sha1', $password, $salt, $rounds), $hash);
|
||||
|
||||
case "PBKDF2-SHA512":
|
||||
// Handle FreeIPA-style hash: {PBKDF2-SHA512}10000$<base64_salt>$<base64_hash>
|
||||
$components = explode('$', $hash);
|
||||
if (count($components) !== 3) return false;
|
||||
|
||||
// 1st part: iteration count (integer)
|
||||
$iterations = intval($components[0]);
|
||||
if ($iterations <= 0) return false;
|
||||
|
||||
// 2nd part: salt (base64-encoded)
|
||||
$salt = $components[1];
|
||||
// 3rd part: hash (base64-encoded)
|
||||
$stored_hash_b64 = $components[2];
|
||||
|
||||
// Decode salt and hash from base64
|
||||
$salt_bin = base64_decode($salt, true);
|
||||
$hash_bin = base64_decode($stored_hash_b64, true);
|
||||
if ($salt_bin === false || $hash_bin === false) return false;
|
||||
// Get length of hash in bytes
|
||||
$hash_len = strlen($hash_bin);
|
||||
if ($hash_len === 0) return false;
|
||||
|
||||
// Calculate PBKDF2-SHA512 hash for provided password
|
||||
$test_hash = hash_pbkdf2('sha512', $password, $salt_bin, $iterations, $hash_len, true);
|
||||
return hash_equals($hash_bin, $test_hash);
|
||||
|
||||
case "PLAIN-MD4":
|
||||
return hash_equals(hash('md4', $password), $hash);
|
||||
|
||||
@@ -971,20 +1078,24 @@ function edit_user_account($_data) {
|
||||
}
|
||||
|
||||
// edit password
|
||||
if (!empty($password_old) && !empty($_data['user_new_pass']) && !empty($_data['user_new_pass2'])) {
|
||||
$stmt = $pdo->prepare("SELECT `password` FROM `mailbox`
|
||||
WHERE `kind` NOT REGEXP 'location|thing|group'
|
||||
AND `username` = :user AND authsource = 'mailcow'");
|
||||
$stmt->execute(array(':user' => $username));
|
||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
$is_forced_pw_update = !empty($_SESSION['pending_pw_update']);
|
||||
if (((!empty($password_old) || $is_forced_pw_update) && !empty($_data['user_new_pass']) && !empty($_data['user_new_pass2']))) {
|
||||
// Only verify old password if this is NOT a forced password update
|
||||
if (!$is_forced_pw_update) {
|
||||
$stmt = $pdo->prepare("SELECT `password` FROM `mailbox`
|
||||
WHERE `kind` NOT REGEXP 'location|thing|group'
|
||||
AND `username` = :user AND authsource = 'mailcow'");
|
||||
$stmt->execute(array(':user' => $username));
|
||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!verify_hash($row['password'], $password_old)) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_data_log),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
return false;
|
||||
if (!verify_hash($row['password'], $password_old)) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_data_log),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
$password_new = $_data['user_new_pass'];
|
||||
@@ -1006,7 +1117,7 @@ function edit_user_account($_data) {
|
||||
update_sogo_static_view();
|
||||
}
|
||||
// edit password recovery email
|
||||
elseif (isset($pw_recovery_email)) {
|
||||
elseif (!empty($password_old) && isset($pw_recovery_email)) {
|
||||
if (!isset($_SESSION['acl']['pw_reset']) || $_SESSION['acl']['pw_reset'] != "1" ) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
@@ -1016,6 +1127,21 @@ function edit_user_account($_data) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare("SELECT `password` FROM `mailbox`
|
||||
WHERE `kind` NOT REGEXP 'location|thing|group'
|
||||
AND `username` = :user AND authsource = 'mailcow'");
|
||||
$stmt->execute(array(':user' => $username));
|
||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!verify_hash($row['password'], $password_old)) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_data_log),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
$pw_recovery_email = (!filter_var($pw_recovery_email, FILTER_VALIDATE_EMAIL)) ? '' : $pw_recovery_email;
|
||||
$stmt = $pdo->prepare("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.recovery_email', :recovery_email)
|
||||
WHERE `username` = :username AND authsource = 'mailcow'");
|
||||
@@ -1107,11 +1233,21 @@ function user_get_alias_details($username) {
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
function is_valid_domain_name($domain_name) {
|
||||
function is_valid_domain_name($domain_name, $options = array()) {
|
||||
if (empty($domain_name)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Convert domain name to ASCII for validation
|
||||
$domain_name = idn_to_ascii($domain_name, 0, INTL_IDNA_VARIANT_UTS46);
|
||||
|
||||
if (isset($options['allow_wildcard']) && $options['allow_wildcard'] == true) {
|
||||
// Remove '*.' if wildcard subdomains are allowed
|
||||
if (strpos($domain_name, '*.') === 0) {
|
||||
$domain_name = substr($domain_name, 2);
|
||||
}
|
||||
}
|
||||
|
||||
return (preg_match("/^([a-z\d](-*[a-z\d])*)(\.([a-z\d](-*[a-z\d])*))*$/i", $domain_name)
|
||||
&& preg_match("/^.{1,253}$/", $domain_name)
|
||||
&& preg_match("/^[^\.]{1,63}(\.[^\.]{1,63})*$/", $domain_name));
|
||||
@@ -1123,50 +1259,52 @@ function set_tfa($_data) {
|
||||
global $iam_settings;
|
||||
|
||||
$_data_log = $_data;
|
||||
$access_denied = null;
|
||||
!isset($_data_log['confirm_password']) ?: $_data_log['confirm_password'] = '*';
|
||||
$username = $_SESSION['mailcow_cc_username'];
|
||||
|
||||
// check for empty user and role
|
||||
if (!isset($_SESSION['mailcow_cc_role']) || empty($username)) $access_denied = true;
|
||||
|
||||
// check admin confirm password
|
||||
if ($access_denied === null) {
|
||||
$stmt = $pdo->prepare("SELECT `password` FROM `admin`
|
||||
WHERE `username` = :username");
|
||||
$stmt->execute(array(':username' => $username));
|
||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
if ($row) {
|
||||
if (!verify_hash($row['password'], $_data["confirm_password"])) $access_denied = true;
|
||||
else $access_denied = false;
|
||||
// skip password check if this is a forced TFA enrollment after login
|
||||
if (!empty($_SESSION['pending_tfa_setup'])) {
|
||||
$username = $_SESSION['mailcow_cc_username'];
|
||||
if (empty($username) || !isset($_SESSION['mailcow_cc_role'])) {
|
||||
$_SESSION['return'][] = array('type' => 'danger', 'log' => array(__FUNCTION__, $_data_log), 'msg' => 'access_denied');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$username = $_SESSION['mailcow_cc_username'];
|
||||
$access_denied = null;
|
||||
|
||||
// check mailbox confirm password
|
||||
if ($access_denied === null) {
|
||||
$stmt = $pdo->prepare("SELECT `password`, `authsource` FROM `mailbox`
|
||||
WHERE `username` = :username");
|
||||
$stmt->execute(array(':username' => $username));
|
||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
if ($row) {
|
||||
if ($row['authsource'] == 'ldap'){
|
||||
if (!ldap_mbox_login($username, $_data["confirm_password"], $iam_settings)) $access_denied = true;
|
||||
else $access_denied = false;
|
||||
} else {
|
||||
if (!isset($_SESSION['mailcow_cc_role']) || empty($username)) $access_denied = true;
|
||||
|
||||
// check admin password
|
||||
if ($access_denied === null) {
|
||||
$stmt = $pdo->prepare("SELECT `password` FROM `admin` WHERE `username` = :username");
|
||||
$stmt->execute(array(':username' => $username));
|
||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
if ($row) {
|
||||
if (!verify_hash($row['password'], $_data["confirm_password"])) $access_denied = true;
|
||||
else $access_denied = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// set access_denied error
|
||||
if ($access_denied){
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_data_log),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
return false;
|
||||
// check mailbox password
|
||||
if ($access_denied === null) {
|
||||
$stmt = $pdo->prepare("SELECT `password`, `authsource` FROM `mailbox` WHERE `username` = :username");
|
||||
$stmt->execute(array(':username' => $username));
|
||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
if ($row) {
|
||||
if ($row['authsource'] == 'ldap'){
|
||||
if (!ldap_mbox_login($username, $_data["confirm_password"], $iam_settings)) $access_denied = true;
|
||||
else $access_denied = false;
|
||||
} else {
|
||||
if (!verify_hash($row['password'], $_data["confirm_password"])) $access_denied = true;
|
||||
else $access_denied = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($access_denied) {
|
||||
$_SESSION['return'][] = array('type' => 'danger', 'log' => array(__FUNCTION__, $_data_log), 'msg' => 'access_denied');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
switch ($_data["tfa_method"]) {
|
||||
@@ -1219,6 +1357,7 @@ function set_tfa($_data) {
|
||||
);
|
||||
return false;
|
||||
}
|
||||
unset($_SESSION['pending_tfa_setup']);
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $_data_log),
|
||||
@@ -1232,6 +1371,7 @@ function set_tfa($_data) {
|
||||
//$stmt->execute(array(':username' => $username));
|
||||
$stmt = $pdo->prepare("INSERT INTO `tfa` (`username`, `key_id`, `authmech`, `secret`, `active`) VALUES (?, ?, 'totp', ?, '1')");
|
||||
$stmt->execute(array($username, $key_id, $_POST['totp_secret']));
|
||||
unset($_SESSION['pending_tfa_setup']);
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $_data_log),
|
||||
@@ -1260,6 +1400,7 @@ function set_tfa($_data) {
|
||||
0
|
||||
));
|
||||
|
||||
unset($_SESSION['pending_tfa_setup']);
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $_data_log),
|
||||
@@ -1267,6 +1408,25 @@ function set_tfa($_data) {
|
||||
);
|
||||
break;
|
||||
case "none":
|
||||
// Block TFA removal if force_tfa policy is active
|
||||
$is_forced_tfa = false;
|
||||
if ($_SESSION['mailcow_cc_role'] === 'user') {
|
||||
$stmt_check = $pdo->prepare("SELECT JSON_EXTRACT(`attributes`, '$.force_tfa') FROM `mailbox` WHERE `username` = ?");
|
||||
$stmt_check->execute(array($username));
|
||||
$is_forced_tfa = ($stmt_check->fetchColumn() == '1');
|
||||
} else {
|
||||
$stmt_check = $pdo->prepare("SELECT JSON_EXTRACT(`attributes`, '$.force_tfa') FROM `admin` WHERE `username` = ?");
|
||||
$stmt_check->execute(array($username));
|
||||
$is_forced_tfa = ($stmt_check->fetchColumn() == '1');
|
||||
}
|
||||
if ($is_forced_tfa) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_data_log),
|
||||
'msg' => 'tfa_removal_blocked'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
$stmt = $pdo->prepare("DELETE FROM `tfa` WHERE `username` = :username");
|
||||
$stmt->execute(array(':username' => $username));
|
||||
$_SESSION['return'][] = array(
|
||||
@@ -1519,6 +1679,26 @@ function unset_tfa_key($_data) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Block key removal if force_tfa policy is active
|
||||
$is_forced_tfa = false;
|
||||
if ($_SESSION['mailcow_cc_role'] === 'user') {
|
||||
$stmt_check = $pdo->prepare("SELECT JSON_EXTRACT(`attributes`, '$.force_tfa') FROM `mailbox` WHERE `username` = ?");
|
||||
$stmt_check->execute(array($username));
|
||||
$is_forced_tfa = ($stmt_check->fetchColumn() == '1');
|
||||
} else {
|
||||
$stmt_check = $pdo->prepare("SELECT JSON_EXTRACT(`attributes`, '$.force_tfa') FROM `admin` WHERE `username` = ?");
|
||||
$stmt_check->execute(array($username));
|
||||
$is_forced_tfa = ($stmt_check->fetchColumn() == '1');
|
||||
}
|
||||
if ($is_forced_tfa) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_data_log),
|
||||
'msg' => 'tfa_removal_blocked'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
// check if it's last key
|
||||
$stmt = $pdo->prepare("SELECT COUNT(*) AS `keys` FROM `tfa`
|
||||
WHERE `username` = :username AND `active` = '1'");
|
||||
@@ -1551,6 +1731,15 @@ function unset_tfa_key($_data) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
function tfa_exists($username) {
|
||||
global $pdo;
|
||||
if (empty($username)) {
|
||||
return false;
|
||||
}
|
||||
$stmt = $pdo->prepare("SELECT COUNT(*) as count FROM `tfa` WHERE `username` = :username");
|
||||
$stmt->execute(array(':username' => $username));
|
||||
return $stmt->fetch(PDO::FETCH_ASSOC)['count'] > 0;
|
||||
}
|
||||
function get_tfa($username = null, $id = null) {
|
||||
global $pdo;
|
||||
if (empty($username) && isset($_SESSION['mailcow_cc_username'])) {
|
||||
@@ -2139,10 +2328,13 @@ function cors($action, $data = null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$allowed_origins = isset($data['allowed_origins']) ? $data['allowed_origins'] : array($_SERVER['SERVER_NAME']);
|
||||
$allowed_origins = isset($data['allowed_origins']) ? $data['allowed_origins'] : array(getBaseURL());
|
||||
$allowed_origins = !is_array($allowed_origins) ? array_filter(array_map('trim', explode("\n", $allowed_origins))) : $allowed_origins;
|
||||
foreach ($allowed_origins as $origin) {
|
||||
if (!filter_var($origin, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME) && $origin != '*') {
|
||||
foreach ($allowed_origins as &$origin) {
|
||||
if ($origin === '*') {
|
||||
continue;
|
||||
}
|
||||
if (!valid_origin($origin)) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $action, $data),
|
||||
@@ -2150,7 +2342,10 @@ function cors($action, $data = null) {
|
||||
);
|
||||
return false;
|
||||
}
|
||||
// browsers always send a lowercase scheme/host in the Origin header, so normalize to match
|
||||
$origin = strtolower($origin);
|
||||
}
|
||||
unset($origin);
|
||||
|
||||
$allowed_methods = isset($data['allowed_methods']) ? $data['allowed_methods'] : array('GET', 'POST', 'PUT', 'DELETE');
|
||||
$allowed_methods = !is_array($allowed_methods) ? array_map('trim', preg_split( "/( |,|;|\n)/", $allowed_methods)) : $allowed_methods;
|
||||
@@ -2198,26 +2393,35 @@ function cors($action, $data = null) {
|
||||
);
|
||||
}
|
||||
|
||||
$cors_settings = !$cors_settings ? array('allowed_origins' => $_SERVER['SERVER_NAME'], 'allowed_methods' => 'GET, POST, PUT, DELETE') : $cors_settings;
|
||||
$cors_settings['allowed_origins'] = empty($cors_settings['allowed_origins']) ? $_SERVER['SERVER_NAME'] : $cors_settings['allowed_origins'];
|
||||
$cors_settings = !$cors_settings ? array('allowed_origins' => getBaseURL(), 'allowed_methods' => 'GET, POST, PUT, DELETE') : $cors_settings;
|
||||
$cors_settings['allowed_origins'] = empty($cors_settings['allowed_origins']) ? getBaseURL() : $cors_settings['allowed_origins'];
|
||||
$cors_settings['allowed_methods'] = empty($cors_settings['allowed_methods']) ? 'GET, POST, PUT, DELETE, OPTION' : $cors_settings['allowed_methods'];
|
||||
|
||||
return $cors_settings;
|
||||
break;
|
||||
case "set_headers":
|
||||
$cors_settings = cors('get');
|
||||
// normalize the stored list; it may still hold bare hostnames written before origins were validated as origins
|
||||
$allowed_origins = array_filter(array_map('normalize_cors_origin', explode(',', $cors_settings['allowed_origins'])));
|
||||
$origin = isset($_SERVER['HTTP_ORIGIN']) ? normalize_cors_origin($_SERVER['HTTP_ORIGIN']) : false;
|
||||
// check if requested origin is in allowed origins
|
||||
$allowed_origins = explode(', ', $cors_settings['allowed_origins']);
|
||||
$cors_settings['allowed_origins'] = $allowed_origins[0];
|
||||
if (in_array('*', $allowed_origins)){
|
||||
$cors_settings['allowed_origins'] = '*';
|
||||
} else if (array_key_exists('HTTP_ORIGIN', $_SERVER) && in_array($_SERVER['HTTP_ORIGIN'], $allowed_origins)) {
|
||||
$cors_settings['allowed_origins'] = $_SERVER['HTTP_ORIGIN'];
|
||||
$allow_origin = null;
|
||||
if (in_array('*', $allowed_origins, true)) {
|
||||
$allow_origin = '*';
|
||||
} else if ($origin !== false && in_array($origin, $allowed_origins, true)) {
|
||||
$allow_origin = $origin;
|
||||
}
|
||||
// always allow OPTIONS for preflight request
|
||||
$cors_settings["allowed_methods"] = empty($cors_settings["allowed_methods"]) ? 'OPTIONS' : $cors_settings["allowed_methods"] . ', ' . 'OPTIONS';
|
||||
|
||||
header('Access-Control-Allow-Origin: ' . $cors_settings['allowed_origins']);
|
||||
// a disallowed origin gets no Access-Control-Allow-Origin at all; echoing a different origin than the requesting one tells a browser nothing
|
||||
if ($allow_origin !== null) {
|
||||
header('Access-Control-Allow-Origin: ' . $allow_origin);
|
||||
}
|
||||
if ($allow_origin !== '*') {
|
||||
// the response depends on the request origin, keep caches from mixing them up
|
||||
header('Vary: Origin', false);
|
||||
}
|
||||
header('Access-Control-Allow-Methods: '. $cors_settings['allowed_methods']);
|
||||
header('Access-Control-Allow-Headers: Accept, Content-Type, X-Api-Key, Origin');
|
||||
|
||||
@@ -3353,6 +3557,58 @@ function set_user_loggedin_session($user) {
|
||||
unset($_SESSION['pending_mailcow_cc_role']);
|
||||
unset($_SESSION['pending_tfa_methods']);
|
||||
}
|
||||
function protect_route($allowed_roles = ['admin', 'domainadmin', 'user'], $redirects = []) {
|
||||
// Check if user is authenticated
|
||||
if (!isset($_SESSION['mailcow_cc_role'])) {
|
||||
if (isset($redirects['unauthenticated'])) {
|
||||
header('Location: ' . $redirects['unauthenticated']);
|
||||
} else {
|
||||
// Send a deep link to the login page for its area instead of the user login at /,
|
||||
// e.g. /admin/dashboard -> /admin rather than /
|
||||
$request_uri = isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '/';
|
||||
if (strpos($request_uri, '/admin/') === 0) {
|
||||
header('Location: /admin');
|
||||
} elseif (strpos($request_uri, '/domainadmin/') === 0) {
|
||||
header('Location: /domainadmin');
|
||||
} else {
|
||||
header('Location: /');
|
||||
}
|
||||
}
|
||||
exit();
|
||||
}
|
||||
|
||||
// Check for pending actions (2FA setup, password update)
|
||||
if (!empty($_SESSION['pending_tfa_setup']) || !empty($_SESSION['pending_pw_update'])) {
|
||||
$pending_redirect = '/';
|
||||
if ($_SESSION['mailcow_cc_role'] === 'admin') {
|
||||
$pending_redirect = '/admin';
|
||||
} elseif ($_SESSION['mailcow_cc_role'] === 'domainadmin') {
|
||||
$pending_redirect = '/domainadmin';
|
||||
}
|
||||
header('Location: ' . $pending_redirect);
|
||||
exit();
|
||||
}
|
||||
|
||||
// Check if user's role is in the allowed roles for the route
|
||||
if (!in_array($_SESSION['mailcow_cc_role'], $allowed_roles)) {
|
||||
if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'admin') {
|
||||
header('Location: /admin/dashboard');
|
||||
exit();
|
||||
}
|
||||
elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'domainadmin') {
|
||||
header('Location: /domainadmin/mailbox');
|
||||
exit();
|
||||
}
|
||||
elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user') {
|
||||
header('Location: /user');
|
||||
exit();
|
||||
}
|
||||
else {
|
||||
header('Location: /');
|
||||
exit();
|
||||
}
|
||||
}
|
||||
}
|
||||
function get_logs($application, $lines = false) {
|
||||
if ($lines === false) {
|
||||
$lines = $GLOBALS['LOG_LINES'] - 1;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -22,7 +22,7 @@ function quarantine($_action, $_data = null) {
|
||||
return false;
|
||||
}
|
||||
$stmt = $pdo->prepare('SELECT `id` FROM `quarantine` LEFT OUTER JOIN `user_acl` ON `user_acl`.`username` = `rcpt`
|
||||
WHERE `qhash` = :hash
|
||||
WHERE `qhash` = :hash
|
||||
AND user_acl.quarantine = 1
|
||||
AND rcpt IN (SELECT username FROM mailbox)');
|
||||
$stmt->execute(array(':hash' => $hash));
|
||||
@@ -65,7 +65,7 @@ function quarantine($_action, $_data = null) {
|
||||
return false;
|
||||
}
|
||||
$stmt = $pdo->prepare('SELECT `id` FROM `quarantine` LEFT OUTER JOIN `user_acl` ON `user_acl`.`username` = `rcpt`
|
||||
WHERE `qhash` = :hash
|
||||
WHERE `qhash` = :hash
|
||||
AND `user_acl`.`quarantine` = 1
|
||||
AND `username` IN (SELECT `username` FROM `mailbox`)');
|
||||
$stmt->execute(array(':hash' => $hash));
|
||||
@@ -170,6 +170,8 @@ function quarantine($_action, $_data = null) {
|
||||
}
|
||||
elseif ($release_format == 'raw') {
|
||||
$detail_row['msg'] = preg_replace('/^X-Spam-Flag: (.*)/m', 'X-Pre-Release-Spam-Flag: $1', $detail_row['msg']);
|
||||
// dot-stuffing per RFC5321 4.5.2: escape leading dots
|
||||
$detail_row['msg'] = preg_replace('~^\.~m', '..', $detail_row['msg']);
|
||||
$postfix_talk = array(
|
||||
array('220', 'HELO quarantine' . chr(10)),
|
||||
array('250', 'MAIL FROM: ' . $sender . chr(10)),
|
||||
@@ -180,7 +182,7 @@ function quarantine($_action, $_data = null) {
|
||||
array('221', '')
|
||||
);
|
||||
// Thanks to https://stackoverflow.com/questions/6632399/given-an-email-as-raw-text-how-can-i-send-it-using-php
|
||||
$smtp_connection = fsockopen($postfix, 590, $errno, $errstr, 1);
|
||||
$smtp_connection = fsockopen($postfix, 590, $errno, $errstr, 1);
|
||||
if (!$smtp_connection) {
|
||||
logger(array('return' => array(
|
||||
array(
|
||||
@@ -192,7 +194,7 @@ function quarantine($_action, $_data = null) {
|
||||
return false;
|
||||
}
|
||||
for ($i=0; $i < count($postfix_talk); $i++) {
|
||||
$smtp_resource = fgets($smtp_connection, 256);
|
||||
$smtp_resource = fgets($smtp_connection, 256);
|
||||
if (substr($smtp_resource, 0, 3) !== $postfix_talk[$i][0]) {
|
||||
$ret = substr($smtp_resource, 0, 3);
|
||||
$ret = (empty($ret)) ? '-' : $ret;
|
||||
@@ -465,17 +467,19 @@ function quarantine($_action, $_data = null) {
|
||||
}
|
||||
elseif ($release_format == 'raw') {
|
||||
$row['msg'] = preg_replace('/^X-Spam-Flag: (.*)/m', 'X-Pre-Release-Spam-Flag: $1', $row['msg']);
|
||||
// dot-stuffing per RFC5321 4.5.2: escape leading dots
|
||||
$row['msg'] = preg_replace('~^\.~m', '..', $row['msg']);
|
||||
$postfix_talk = array(
|
||||
array('220', 'HELO quarantine' . chr(10)),
|
||||
array('250', 'MAIL FROM: ' . $sender . chr(10)),
|
||||
array('250', 'RCPT TO: ' . $row['rcpt'] . chr(10)),
|
||||
array('250', 'DATA' . chr(10)),
|
||||
array('354', str_replace("\n.", '', $row['msg']) . chr(10) . '.' . chr(10)),
|
||||
array('354', $row['msg'] . chr(10) . '.' . chr(10)),
|
||||
array('250', 'QUIT' . chr(10)),
|
||||
array('221', '')
|
||||
);
|
||||
// Thanks to https://stackoverflow.com/questions/6632399/given-an-email-as-raw-text-how-can-i-send-it-using-php
|
||||
$smtp_connection = fsockopen($postfix, 590, $errno, $errstr, 1);
|
||||
$smtp_connection = fsockopen($postfix, 590, $errno, $errstr, 1);
|
||||
if (!$smtp_connection) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'warning',
|
||||
@@ -485,7 +489,7 @@ function quarantine($_action, $_data = null) {
|
||||
return false;
|
||||
}
|
||||
for ($i=0; $i < count($postfix_talk); $i++) {
|
||||
$smtp_resource = fgets($smtp_connection, 256);
|
||||
$smtp_resource = fgets($smtp_connection, 256);
|
||||
if (substr($smtp_resource, 0, 3) !== $postfix_talk[$i][0]) {
|
||||
$ret = substr($smtp_resource, 0, 3);
|
||||
$ret = (empty($ret)) ? '-' : $ret;
|
||||
@@ -833,7 +837,7 @@ function quarantine($_action, $_data = null) {
|
||||
)));
|
||||
return false;
|
||||
}
|
||||
$stmt = $pdo->prepare('SELECT * FROM `quarantine` WHERE `qhash` = :hash');
|
||||
$stmt = $pdo->prepare('SELECT * FROM `quarantine` WHERE `qhash` = :hash');
|
||||
$stmt->execute(array(':hash' => $hash));
|
||||
return $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
break;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -62,7 +62,11 @@ if ($app_links_processed){
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Workaround to get text with <br> straight to twig.
|
||||
// Using "nl2br" doesn't work with Twig as it would escape everything by default.
|
||||
if (isset($UI_TEXTS["ui_footer"])) {
|
||||
$UI_TEXTS["ui_footer"] = nl2br($UI_TEXTS["ui_footer"]);
|
||||
}
|
||||
|
||||
$globalVariables = [
|
||||
'mailcow_hostname' => getenv('MAILCOW_HOSTNAME'),
|
||||
@@ -85,7 +89,7 @@ $globalVariables = [
|
||||
'app_links' => $app_links,
|
||||
'app_links_processed' => $app_links_processed,
|
||||
'is_root_uri' => (parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH) == '/'),
|
||||
'uri' => $_SERVER['REQUEST_URI'],
|
||||
'uri' => parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH) ?: '/',
|
||||
];
|
||||
|
||||
foreach ($globalVariables as $globalVariableName => $globalVariableValue) {
|
||||
|
||||
+219
-20
@@ -4,7 +4,7 @@ function init_db_schema()
|
||||
try {
|
||||
global $pdo;
|
||||
|
||||
$db_version = "19082025_1436";
|
||||
$db_version = "19082026_0900";
|
||||
|
||||
$stmt = $pdo->query("SHOW TABLES LIKE 'versions'");
|
||||
$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
|
||||
@@ -76,7 +76,8 @@ function init_db_schema()
|
||||
"superadmin" => "TINYINT(1) NOT NULL DEFAULT '0'",
|
||||
"created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)",
|
||||
"modified" => "DATETIME ON UPDATE NOW(0)",
|
||||
"active" => "TINYINT(1) NOT NULL DEFAULT '1'"
|
||||
"active" => "TINYINT(1) NOT NULL DEFAULT '1'",
|
||||
"attributes" => "JSON"
|
||||
),
|
||||
"keys" => array(
|
||||
"primary" => array(
|
||||
@@ -185,6 +186,7 @@ function init_db_schema()
|
||||
"public_comment" => "TEXT",
|
||||
"sogo_visible" => "TINYINT(1) NOT NULL DEFAULT '1'",
|
||||
"internal" => "TINYINT(1) NOT NULL DEFAULT '0'",
|
||||
"sender_allowed" => "TINYINT(1) NOT NULL DEFAULT '1'",
|
||||
"active" => "TINYINT(1) NOT NULL DEFAULT '1'"
|
||||
),
|
||||
"keys" => array(
|
||||
@@ -554,7 +556,8 @@ function init_db_schema()
|
||||
"description" => "TEXT NOT NULL",
|
||||
"created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)",
|
||||
"modified" => "DATETIME ON UPDATE CURRENT_TIMESTAMP",
|
||||
"validity" => "INT(11)"
|
||||
"validity" => "INT(11)",
|
||||
"permanent" => "TINYINT(1) NOT NULL DEFAULT '0'"
|
||||
),
|
||||
"keys" => array(
|
||||
"primary" => array(
|
||||
@@ -716,7 +719,8 @@ function init_db_schema()
|
||||
"alias_domains" => "TINYINT(1) NOT NULL DEFAULT '0'",
|
||||
"mailbox_relayhost" => "TINYINT(1) NOT NULL DEFAULT '1'",
|
||||
"domain_relayhost" => "TINYINT(1) NOT NULL DEFAULT '1'",
|
||||
"domain_desc" => "TINYINT(1) NOT NULL DEFAULT '0'"
|
||||
"domain_desc" => "TINYINT(1) NOT NULL DEFAULT '0'",
|
||||
"alias_external_goto" => "TINYINT(1) NOT NULL DEFAULT '1'"
|
||||
),
|
||||
"keys" => array(
|
||||
"primary" => array(
|
||||
@@ -751,35 +755,145 @@ function init_db_schema()
|
||||
),
|
||||
"attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC"
|
||||
),
|
||||
"imapsync_source" => array(
|
||||
"cols" => array(
|
||||
"id" => "INT NOT NULL AUTO_INCREMENT",
|
||||
"name" => "VARCHAR(100) NOT NULL",
|
||||
"description" => "VARCHAR(255) NOT NULL DEFAULT ''",
|
||||
"created_by" => "VARCHAR(255) NOT NULL DEFAULT ''",
|
||||
"scope" => "ENUM('all','domain','user') NOT NULL DEFAULT 'user'",
|
||||
"host1" => "VARCHAR(255) NOT NULL",
|
||||
"port1" => "SMALLINT UNSIGNED NOT NULL",
|
||||
"enc1" => "ENUM('TLS','SSL','PLAIN') DEFAULT 'TLS'",
|
||||
"auth_type" => "ENUM('PLAIN','LOGIN','CRAM-MD5','XOAUTH2') DEFAULT 'PLAIN'",
|
||||
"oauth_flow" => "ENUM('client_credentials','authorization_code') NOT NULL DEFAULT 'client_credentials'",
|
||||
"oauth_token_endpoint" => "VARCHAR(500) DEFAULT NULL",
|
||||
"oauth_authorize_endpoint" => "VARCHAR(500) DEFAULT NULL",
|
||||
"oauth_userinfo_endpoint" => "VARCHAR(500) DEFAULT NULL",
|
||||
"oauth_client_id" => "VARCHAR(500) DEFAULT NULL",
|
||||
"oauth_client_secret" => "VARCHAR(500) DEFAULT NULL",
|
||||
"oauth_scope" => "VARCHAR(500) DEFAULT NULL",
|
||||
"oauth_extra_params" => "TEXT DEFAULT NULL",
|
||||
"oauth_access_token" => "TEXT DEFAULT NULL",
|
||||
"oauth_token_expires" => "BIGINT UNSIGNED DEFAULT NULL",
|
||||
"oauth_last_refresh_error" => "TEXT DEFAULT NULL",
|
||||
"created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)",
|
||||
"modified" => "DATETIME ON UPDATE CURRENT_TIMESTAMP",
|
||||
"active" => "TINYINT(1) NOT NULL DEFAULT '1'"
|
||||
),
|
||||
"keys" => array(
|
||||
"primary" => array(
|
||||
"" => array("id")
|
||||
),
|
||||
"unique" => array(
|
||||
"uniq_creator_name" => array("created_by", "name")
|
||||
),
|
||||
"key" => array(
|
||||
"idx_created_by" => array("created_by"),
|
||||
"idx_scope" => array("scope")
|
||||
)
|
||||
),
|
||||
"attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC"
|
||||
),
|
||||
"imapsync_source_domain" => array(
|
||||
"cols" => array(
|
||||
"source_id" => "INT NOT NULL",
|
||||
"domain" => "VARCHAR(255) NOT NULL"
|
||||
),
|
||||
"keys" => array(
|
||||
"primary" => array(
|
||||
"" => array("source_id", "domain")
|
||||
),
|
||||
"key" => array(
|
||||
"idx_domain" => array("domain")
|
||||
),
|
||||
"fkey" => array(
|
||||
"fk_imapsync_source_domain" => array(
|
||||
"col" => "source_id",
|
||||
"ref" => "imapsync_source.id",
|
||||
"delete" => "CASCADE",
|
||||
"update" => "NO ACTION"
|
||||
)
|
||||
)
|
||||
),
|
||||
"attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC"
|
||||
),
|
||||
"imapsync_source_user" => array(
|
||||
"cols" => array(
|
||||
"source_id" => "INT NOT NULL",
|
||||
"username" => "VARCHAR(255) NOT NULL"
|
||||
),
|
||||
"keys" => array(
|
||||
"primary" => array(
|
||||
"" => array("source_id", "username")
|
||||
),
|
||||
"key" => array(
|
||||
"idx_username" => array("username")
|
||||
),
|
||||
"fkey" => array(
|
||||
"fk_imapsync_source_user" => array(
|
||||
"col" => "source_id",
|
||||
"ref" => "imapsync_source.id",
|
||||
"delete" => "CASCADE",
|
||||
"update" => "NO ACTION"
|
||||
)
|
||||
)
|
||||
),
|
||||
"attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC"
|
||||
),
|
||||
"imapsync_source_oauth_token" => array(
|
||||
"cols" => array(
|
||||
"source_id" => "INT NOT NULL",
|
||||
"username" => "VARCHAR(255) NOT NULL",
|
||||
"access_token" => "TEXT DEFAULT NULL",
|
||||
"refresh_token" => "TEXT DEFAULT NULL",
|
||||
"token_expires" => "BIGINT UNSIGNED DEFAULT NULL",
|
||||
"last_refresh_error" => "TEXT DEFAULT NULL",
|
||||
"created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)",
|
||||
"modified" => "DATETIME ON UPDATE CURRENT_TIMESTAMP"
|
||||
),
|
||||
"keys" => array(
|
||||
"primary" => array(
|
||||
"" => array("source_id", "username")
|
||||
),
|
||||
"fkey" => array(
|
||||
"fk_imapsync_source_oauth_token" => array(
|
||||
"col" => "source_id",
|
||||
"ref" => "imapsync_source.id",
|
||||
"delete" => "CASCADE",
|
||||
"update" => "NO ACTION"
|
||||
)
|
||||
)
|
||||
),
|
||||
"attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC"
|
||||
),
|
||||
"imapsync" => array(
|
||||
"cols" => array(
|
||||
"id" => "INT NOT NULL AUTO_INCREMENT",
|
||||
"user2" => "VARCHAR(255) NOT NULL",
|
||||
"host1" => "VARCHAR(255) NOT NULL",
|
||||
"authmech1" => "ENUM('PLAIN','LOGIN','CRAM-MD5') DEFAULT 'PLAIN'",
|
||||
"source_id" => "INT NOT NULL",
|
||||
"regextrans2" => "VARCHAR(255) DEFAULT ''",
|
||||
"authmd51" => "TINYINT(1) NOT NULL DEFAULT 0",
|
||||
"domain2" => "VARCHAR(255) NOT NULL DEFAULT ''",
|
||||
"subfolder2" => "VARCHAR(255) NOT NULL DEFAULT ''",
|
||||
"user1" => "VARCHAR(255) NOT NULL",
|
||||
"password1" => "VARCHAR(255) NOT NULL",
|
||||
"password1" => "VARCHAR(255) NOT NULL DEFAULT ''",
|
||||
"exclude" => "VARCHAR(500) NOT NULL DEFAULT ''",
|
||||
"maxage" => "SMALLINT NOT NULL DEFAULT '0'",
|
||||
"mins_interval" => "SMALLINT UNSIGNED NOT NULL DEFAULT '0'",
|
||||
"maxbytespersecond" => "VARCHAR(50) NOT NULL DEFAULT '0'",
|
||||
"port1" => "SMALLINT UNSIGNED NOT NULL",
|
||||
"enc1" => "ENUM('TLS','SSL','PLAIN') DEFAULT 'TLS'",
|
||||
"delete2duplicates" => "TINYINT(1) NOT NULL DEFAULT '1'",
|
||||
"delete1" => "TINYINT(1) NOT NULL DEFAULT '0'",
|
||||
"delete2" => "TINYINT(1) NOT NULL DEFAULT '0'",
|
||||
"automap" => "TINYINT(1) NOT NULL DEFAULT '0'",
|
||||
"skipcrossduplicates" => "TINYINT(1) NOT NULL DEFAULT '0'",
|
||||
"custom_params" => "VARCHAR(512) NOT NULL DEFAULT ''",
|
||||
"custom_params" => "TEXT",
|
||||
"timeout1" => "SMALLINT NOT NULL DEFAULT '600'",
|
||||
"timeout2" => "SMALLINT NOT NULL DEFAULT '600'",
|
||||
"subscribeall" => "TINYINT(1) NOT NULL DEFAULT '1'",
|
||||
"dry" => "TINYINT(1) NOT NULL DEFAULT '0'",
|
||||
"is_running" => "TINYINT(1) NOT NULL DEFAULT '0'",
|
||||
"prio" => "INT NOT NULL DEFAULT 0",
|
||||
"returned_text" => "LONGTEXT",
|
||||
"last_run" => "TIMESTAMP NULL DEFAULT NULL",
|
||||
"success" => "TINYINT(1) UNSIGNED DEFAULT NULL",
|
||||
@@ -791,6 +905,32 @@ function init_db_schema()
|
||||
"keys" => array(
|
||||
"primary" => array(
|
||||
"" => array("id")
|
||||
),
|
||||
"key" => array(
|
||||
"idx_source_id" => array("source_id"),
|
||||
"idx_prio" => array("prio")
|
||||
),
|
||||
"fkey" => array(
|
||||
"fk_imapsync_source" => array(
|
||||
"col" => "source_id",
|
||||
"ref" => "imapsync_source.id",
|
||||
"delete" => "RESTRICT",
|
||||
"update" => "NO ACTION"
|
||||
)
|
||||
)
|
||||
),
|
||||
"attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC"
|
||||
),
|
||||
"imapsync_settings" => array(
|
||||
"cols" => array(
|
||||
"name" => "VARCHAR(64) NOT NULL",
|
||||
"value" => "VARCHAR(255) NOT NULL",
|
||||
"created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)",
|
||||
"modified" => "DATETIME ON UPDATE CURRENT_TIMESTAMP"
|
||||
),
|
||||
"keys" => array(
|
||||
"primary" => array(
|
||||
"" => array("name")
|
||||
)
|
||||
),
|
||||
"attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC"
|
||||
@@ -1180,6 +1320,33 @@ function init_db_schema()
|
||||
}
|
||||
}
|
||||
|
||||
// Migrate imapsync to source-based schema
|
||||
if ($table == 'imapsync') {
|
||||
$stmt = $pdo->query("SHOW TABLES LIKE 'imapsync'");
|
||||
if (count($stmt->fetchAll(PDO::FETCH_ASSOC)) != 0) {
|
||||
$stmt = $pdo->query("SHOW COLUMNS FROM `imapsync` LIKE 'host1'");
|
||||
$has_legacy_cols = (count($stmt->fetchAll(PDO::FETCH_ASSOC)) != 0);
|
||||
$stmt = $pdo->query("SHOW COLUMNS FROM `imapsync` LIKE 'source_id'");
|
||||
$has_source_id = (count($stmt->fetchAll(PDO::FETCH_ASSOC)) != 0);
|
||||
|
||||
if ($has_legacy_cols && !$has_source_id) {
|
||||
$pdo->exec("INSERT IGNORE INTO `imapsync_source` (`name`, `created_by`, `scope`, `host1`, `port1`, `enc1`, `auth_type`, `active`)
|
||||
SELECT CONCAT('legacy-', `host1`, '-', `port1`, '-', `enc1`, '-', `authmech1`) AS name,
|
||||
'' AS created_by, 'all' AS scope, `host1`, `port1`, `enc1`, `authmech1`, 1
|
||||
FROM `imapsync`
|
||||
GROUP BY `host1`, `port1`, `enc1`, `authmech1`");
|
||||
$pdo->exec("ALTER TABLE `imapsync` ADD COLUMN `source_id` INT DEFAULT NULL");
|
||||
$pdo->exec("UPDATE `imapsync` i
|
||||
JOIN `imapsync_source` s
|
||||
ON s.`host1` = i.`host1` AND s.`port1` = i.`port1`
|
||||
AND s.`enc1` = i.`enc1` AND s.`auth_type` = i.`authmech1`
|
||||
AND s.`created_by` = '' AND s.`scope` = 'all'
|
||||
SET i.`source_id` = s.`id`");
|
||||
$pdo->exec("ALTER TABLE `imapsync` MODIFY COLUMN `source_id` INT NOT NULL");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$stmt = $pdo->query("SHOW TABLES LIKE '" . $table . "'");
|
||||
$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
|
||||
if ($num_results != 0) {
|
||||
@@ -1337,20 +1504,42 @@ function init_db_schema()
|
||||
$pdo->query($create);
|
||||
}
|
||||
|
||||
// Mitigate imapsync argument injection issue
|
||||
$pdo->query("UPDATE `imapsync` SET `custom_params` = ''
|
||||
WHERE `custom_params` LIKE '%pipemess%'
|
||||
OR custom_params LIKE '%skipmess%'
|
||||
OR custom_params LIKE '%delete2foldersonly%'
|
||||
OR custom_params LIKE '%delete2foldersbutnot%'
|
||||
OR custom_params LIKE '%regexflag%'
|
||||
OR custom_params LIKE '%pipemess%'
|
||||
OR custom_params LIKE '%regextrans2%'
|
||||
OR custom_params LIKE '%maxlinelengthcmd%';");
|
||||
// Clear old app_passwd log entries
|
||||
$pdo->exec("DELETE FROM logs
|
||||
WHERE role != 'unauthenticated'
|
||||
AND JSON_EXTRACT(`call`, '$[0]') = 'app_passwd'
|
||||
AND JSON_EXTRACT(`call`, '$[1]') = 'edit'
|
||||
AND (JSON_CONTAINS_PATH(`call`, 'one', '$[2].password')
|
||||
OR JSON_CONTAINS_PATH(`call`, 'one', '$[2].password2'));");
|
||||
|
||||
// Migrate webauthn tfa
|
||||
$stmt = $pdo->query("ALTER TABLE `tfa` MODIFY COLUMN `authmech` ENUM('yubi_otp', 'u2f', 'hotp', 'totp', 'webauthn')");
|
||||
|
||||
// Syncjobs: seed global settings
|
||||
$pdo->query("INSERT IGNORE INTO `imapsync_settings` (`name`, `value`) VALUES ('max_parallel', '1')");
|
||||
|
||||
// Syncjobs: migrate custom_params from raw imapsync string to structured JSON pairs.
|
||||
// Old values could not contain spaces (they were rejected), so a whitespace split is safe.
|
||||
$allow = $GLOBALS['IMAPSYNC_OPTIONS'];
|
||||
$cp_upd = $pdo->prepare("UPDATE `imapsync` SET `custom_params` = :cp WHERE `id` = :id");
|
||||
foreach ($pdo->query("SELECT `id`, `custom_params` FROM `imapsync`")->fetchAll(PDO::FETCH_ASSOC) as $r) {
|
||||
$cp = trim((string)$r['custom_params']);
|
||||
if ($cp !== '' && $cp[0] === '[') continue; // already migrated (JSON)
|
||||
$pairs = array();
|
||||
if ($cp !== '') {
|
||||
$tokens = preg_split('/\s+/', $cp, -1, PREG_SPLIT_NO_EMPTY);
|
||||
for ($i = 0; $i < count($tokens); $i++) {
|
||||
if (strpos($tokens[$i], '--') !== 0) continue;
|
||||
$opt = ltrim($tokens[$i], '-'); $val = '';
|
||||
if (strpos($opt, '=') !== false) { list($opt, $val) = explode('=', $opt, 2); }
|
||||
elseif ($i + 1 < count($tokens) && strpos($tokens[$i + 1], '--') !== 0) { $val = $tokens[++$i]; }
|
||||
if (!array_key_exists(strtolower($opt), $allow)) continue; // drop options no longer allowed
|
||||
$pairs[] = array('o' => strtolower($opt), 'v' => $val);
|
||||
}
|
||||
}
|
||||
$cp_upd->execute(array(':cp' => json_encode($pairs), ':id' => $r['id']));
|
||||
}
|
||||
|
||||
// Inject admin if not exists
|
||||
$stmt = $pdo->query("SELECT NULL FROM `admin`");
|
||||
$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
|
||||
@@ -1380,11 +1569,18 @@ function init_db_schema()
|
||||
$pdo->query("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.passwd_update', \"0\") WHERE JSON_VALUE(`attributes`, '$.passwd_update') IS NULL;");
|
||||
$pdo->query("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.relayhost', \"0\") WHERE JSON_VALUE(`attributes`, '$.relayhost') IS NULL;");
|
||||
$pdo->query("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.force_pw_update', \"0\") WHERE JSON_VALUE(`attributes`, '$.force_pw_update') IS NULL;");
|
||||
$pdo->query("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.force_tfa', \"0\") WHERE JSON_VALUE(`attributes`, '$.force_tfa') IS NULL;");
|
||||
// admin attributes
|
||||
$pdo->query("UPDATE `admin` SET `attributes` = '{}' WHERE `attributes` = '' OR `attributes` IS NULL;");
|
||||
$pdo->query("UPDATE `admin` SET `attributes` = JSON_SET(`attributes`, '$.force_tfa', \"0\") WHERE JSON_VALUE(`attributes`, '$.force_tfa') IS NULL;");
|
||||
$pdo->query("UPDATE `admin` SET `attributes` = JSON_SET(`attributes`, '$.force_pw_update', \"0\") WHERE JSON_VALUE(`attributes`, '$.force_pw_update') IS NULL;");
|
||||
$pdo->query("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.sieve_access', \"1\") WHERE JSON_VALUE(`attributes`, '$.sieve_access') IS NULL;");
|
||||
$pdo->query("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.sogo_access', \"1\") WHERE JSON_VALUE(`attributes`, '$.sogo_access') IS NULL;");
|
||||
$pdo->query("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.imap_access', \"1\") WHERE JSON_VALUE(`attributes`, '$.imap_access') IS NULL;");
|
||||
$pdo->query("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.pop3_access', \"1\") WHERE JSON_VALUE(`attributes`, '$.pop3_access') IS NULL;");
|
||||
$pdo->query("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.smtp_access', \"1\") WHERE JSON_VALUE(`attributes`, '$.smtp_access') IS NULL;");
|
||||
$pdo->query("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.eas_access', \"1\") WHERE JSON_VALUE(`attributes`, '$.eas_access') IS NULL;");
|
||||
$pdo->query("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.dav_access', \"1\") WHERE JSON_VALUE(`attributes`, '$.dav_access') IS NULL;");
|
||||
$pdo->query("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.mailbox_format', \"maildir:\") WHERE JSON_VALUE(`attributes`, '$.mailbox_format') IS NULL;");
|
||||
$pdo->query("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.quarantine_notification', \"never\") WHERE JSON_VALUE(`attributes`, '$.quarantine_notification') IS NULL;");
|
||||
$pdo->query("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.quarantine_category', \"reject\") WHERE JSON_VALUE(`attributes`, '$.quarantine_category') IS NULL;");
|
||||
@@ -1437,6 +1633,7 @@ function init_db_schema()
|
||||
"rl_frame" => "s",
|
||||
"rl_value" => "",
|
||||
"force_pw_update" => intval($GLOBALS['MAILBOX_DEFAULT_ATTRIBUTES']['force_pw_update']),
|
||||
"force_tfa" => intval($GLOBALS['MAILBOX_DEFAULT_ATTRIBUTES']['force_tfa']),
|
||||
"sogo_access" => intval($GLOBALS['MAILBOX_DEFAULT_ATTRIBUTES']['sogo_access']),
|
||||
"active" => 1,
|
||||
"tls_enforce_in" => intval($GLOBALS['MAILBOX_DEFAULT_ATTRIBUTES']['tls_enforce_in']),
|
||||
@@ -1445,6 +1642,8 @@ function init_db_schema()
|
||||
"pop3_access" => intval($GLOBALS['MAILBOX_DEFAULT_ATTRIBUTES']['pop3_access']),
|
||||
"smtp_access" => intval($GLOBALS['MAILBOX_DEFAULT_ATTRIBUTES']['smtp_access']),
|
||||
"sieve_access" => intval($GLOBALS['MAILBOX_DEFAULT_ATTRIBUTES']['sieve_access']),
|
||||
"eas_access" => intval($GLOBALS['MAILBOX_DEFAULT_ATTRIBUTES']['eas_access']),
|
||||
"dav_access" => intval($GLOBALS['MAILBOX_DEFAULT_ATTRIBUTES']['dav_access']),
|
||||
"acl_spam_alias" => 1,
|
||||
"acl_tls_policy" => 1,
|
||||
"acl_spam_score" => 1,
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"directorytree/ldaprecord": "^3.3",
|
||||
"twig/twig": "^3.0",
|
||||
"stevenmaguire/oauth2-keycloak": "^4.0",
|
||||
"league/oauth2-client": "^2.7"
|
||||
"league/oauth2-client": "^2.7",
|
||||
"bacon/bacon-qr-code": "^2.0"
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+269
-155
@@ -4,8 +4,62 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "8f5a147cdb147b935a158b86f47a4747",
|
||||
"content-hash": "50fb4a320500820e36f30eabc45222a0",
|
||||
"packages": [
|
||||
{
|
||||
"name": "bacon/bacon-qr-code",
|
||||
"version": "2.0.8",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/Bacon/BaconQrCode.git",
|
||||
"reference": "8674e51bb65af933a5ffaf1c308a660387c35c22"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/Bacon/BaconQrCode/zipball/8674e51bb65af933a5ffaf1c308a660387c35c22",
|
||||
"reference": "8674e51bb65af933a5ffaf1c308a660387c35c22",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"dasprid/enum": "^1.0.3",
|
||||
"ext-iconv": "*",
|
||||
"php": "^7.1 || ^8.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"phly/keep-a-changelog": "^2.1",
|
||||
"phpunit/phpunit": "^7 | ^8 | ^9",
|
||||
"spatie/phpunit-snapshot-assertions": "^4.2.9",
|
||||
"squizlabs/php_codesniffer": "^3.4"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-imagick": "to generate QR code images"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"BaconQrCode\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"BSD-2-Clause"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Ben Scholzen 'DASPRiD'",
|
||||
"email": "mail@dasprids.de",
|
||||
"homepage": "https://dasprids.de/",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"description": "BaconQrCode is a QR code generator for PHP.",
|
||||
"homepage": "https://github.com/Bacon/BaconQrCode",
|
||||
"support": {
|
||||
"issues": "https://github.com/Bacon/BaconQrCode/issues",
|
||||
"source": "https://github.com/Bacon/BaconQrCode/tree/2.0.8"
|
||||
},
|
||||
"time": "2022-12-07T17:46:57+00:00"
|
||||
},
|
||||
{
|
||||
"name": "bshaffer/oauth2-server-php",
|
||||
"version": "v1.11.1",
|
||||
@@ -137,6 +191,56 @@
|
||||
],
|
||||
"time": "2024-02-09T16:56:22+00:00"
|
||||
},
|
||||
{
|
||||
"name": "dasprid/enum",
|
||||
"version": "1.0.7",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/DASPRiD/Enum.git",
|
||||
"reference": "b5874fa9ed0043116c72162ec7f4fb50e02e7cce"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/DASPRiD/Enum/zipball/b5874fa9ed0043116c72162ec7f4fb50e02e7cce",
|
||||
"reference": "b5874fa9ed0043116c72162ec7f4fb50e02e7cce",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=7.1 <9.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^7 || ^8 || ^9 || ^10 || ^11",
|
||||
"squizlabs/php_codesniffer": "*"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"DASPRiD\\Enum\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"BSD-2-Clause"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Ben Scholzen 'DASPRiD'",
|
||||
"email": "mail@dasprids.de",
|
||||
"homepage": "https://dasprids.de/",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"description": "PHP 7.1 enum implementation",
|
||||
"keywords": [
|
||||
"enum",
|
||||
"map"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/DASPRiD/Enum/issues",
|
||||
"source": "https://github.com/DASPRiD/Enum/tree/1.0.7"
|
||||
},
|
||||
"time": "2025-09-16T12:23:56+00:00"
|
||||
},
|
||||
{
|
||||
"name": "ddeboer/imap",
|
||||
"version": "1.13.1",
|
||||
@@ -214,30 +318,32 @@
|
||||
},
|
||||
{
|
||||
"name": "directorytree/ldaprecord",
|
||||
"version": "v2.20.5",
|
||||
"version": "v3.8.5",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/DirectoryTree/LdapRecord.git",
|
||||
"reference": "5bd0a5a9d257cf1049ae83055dbba4c3479ddf16"
|
||||
"reference": "00e5f088f8c4028d5f398783cccc2e8119a27a65"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/DirectoryTree/LdapRecord/zipball/5bd0a5a9d257cf1049ae83055dbba4c3479ddf16",
|
||||
"reference": "5bd0a5a9d257cf1049ae83055dbba4c3479ddf16",
|
||||
"url": "https://api.github.com/repos/DirectoryTree/LdapRecord/zipball/00e5f088f8c4028d5f398783cccc2e8119a27a65",
|
||||
"reference": "00e5f088f8c4028d5f398783cccc2e8119a27a65",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-iconv": "*",
|
||||
"ext-json": "*",
|
||||
"ext-ldap": "*",
|
||||
"illuminate/contracts": "^5.0|^6.0|^7.0|^8.0|^9.0|^10.0",
|
||||
"nesbot/carbon": "^1.0|^2.0",
|
||||
"php": ">=7.3",
|
||||
"psr/log": "^1.0|^2.0|^3.0",
|
||||
"psr/simple-cache": "^1.0|^2.0",
|
||||
"symfony/polyfill-php80": "^1.25",
|
||||
"tightenco/collect": "^5.6|^6.0|^7.0|^8.0|^9.0"
|
||||
"illuminate/collections": "^8.0|^9.0|^10.0|^11.0|^12.0",
|
||||
"illuminate/contracts": "^8.0|^9.0|^10.0|^11.0|^12.0",
|
||||
"nesbot/carbon": "*",
|
||||
"php": ">=8.1",
|
||||
"psr/log": "*",
|
||||
"psr/simple-cache": "^1.0|^2.0|^3.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"fakerphp/faker": "^1.21",
|
||||
"laravel/pint": "^1.6",
|
||||
"mockery/mockery": "^1.0",
|
||||
"phpunit/phpunit": "^9.0",
|
||||
"spatie/ray": "^1.24"
|
||||
@@ -284,7 +390,7 @@
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2023-10-11T16:34:34+00:00"
|
||||
"time": "2025-10-06T02:22:34+00:00"
|
||||
},
|
||||
{
|
||||
"name": "firebase/php-jwt",
|
||||
@@ -677,6 +783,107 @@
|
||||
],
|
||||
"time": "2023-04-17T16:00:45+00:00"
|
||||
},
|
||||
{
|
||||
"name": "illuminate/collections",
|
||||
"version": "v10.49.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/illuminate/collections.git",
|
||||
"reference": "6ae9c74fa92d4e1824d1b346cd435e8eacdc3232"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/illuminate/collections/zipball/6ae9c74fa92d4e1824d1b346cd435e8eacdc3232",
|
||||
"reference": "6ae9c74fa92d4e1824d1b346cd435e8eacdc3232",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"illuminate/conditionable": "^10.0",
|
||||
"illuminate/contracts": "^10.0",
|
||||
"illuminate/macroable": "^10.0",
|
||||
"php": "^8.1"
|
||||
},
|
||||
"suggest": {
|
||||
"symfony/var-dumper": "Required to use the dump method (^6.2)."
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "10.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"files": [
|
||||
"helpers.php"
|
||||
],
|
||||
"psr-4": {
|
||||
"Illuminate\\Support\\": ""
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Taylor Otwell",
|
||||
"email": "taylor@laravel.com"
|
||||
}
|
||||
],
|
||||
"description": "The Illuminate Collections package.",
|
||||
"homepage": "https://laravel.com",
|
||||
"support": {
|
||||
"issues": "https://github.com/laravel/framework/issues",
|
||||
"source": "https://github.com/laravel/framework"
|
||||
},
|
||||
"time": "2025-09-08T19:05:53+00:00"
|
||||
},
|
||||
{
|
||||
"name": "illuminate/conditionable",
|
||||
"version": "v10.49.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/illuminate/conditionable.git",
|
||||
"reference": "47c700320b7a419f0d188d111f3bbed978fcbd3f"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/illuminate/conditionable/zipball/47c700320b7a419f0d188d111f3bbed978fcbd3f",
|
||||
"reference": "47c700320b7a419f0d188d111f3bbed978fcbd3f",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^8.0.2"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "10.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Illuminate\\Support\\": ""
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Taylor Otwell",
|
||||
"email": "taylor@laravel.com"
|
||||
}
|
||||
],
|
||||
"description": "The Illuminate Conditionable package.",
|
||||
"homepage": "https://laravel.com",
|
||||
"support": {
|
||||
"issues": "https://github.com/laravel/framework/issues",
|
||||
"source": "https://github.com/laravel/framework"
|
||||
},
|
||||
"time": "2025-03-24T11:47:24+00:00"
|
||||
},
|
||||
{
|
||||
"name": "illuminate/contracts",
|
||||
"version": "v10.44.0",
|
||||
@@ -725,6 +932,52 @@
|
||||
},
|
||||
"time": "2024-01-15T18:52:32+00:00"
|
||||
},
|
||||
{
|
||||
"name": "illuminate/macroable",
|
||||
"version": "v10.49.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/illuminate/macroable.git",
|
||||
"reference": "dff667a46ac37b634dcf68909d9d41e94dc97c27"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/illuminate/macroable/zipball/dff667a46ac37b634dcf68909d9d41e94dc97c27",
|
||||
"reference": "dff667a46ac37b634dcf68909d9d41e94dc97c27",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^8.1"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "10.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Illuminate\\Support\\": ""
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Taylor Otwell",
|
||||
"email": "taylor@laravel.com"
|
||||
}
|
||||
],
|
||||
"description": "The Illuminate Macroable package.",
|
||||
"homepage": "https://laravel.com",
|
||||
"support": {
|
||||
"issues": "https://github.com/laravel/framework/issues",
|
||||
"source": "https://github.com/laravel/framework"
|
||||
},
|
||||
"time": "2023-06-05T12:46:42+00:00"
|
||||
},
|
||||
{
|
||||
"name": "league/oauth2-client",
|
||||
"version": "2.7.0",
|
||||
@@ -2452,145 +2705,6 @@
|
||||
],
|
||||
"time": "2023-12-26T14:02:43+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/var-dumper",
|
||||
"version": "v6.4.3",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/var-dumper.git",
|
||||
"reference": "0435a08f69125535336177c29d56af3abc1f69da"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/var-dumper/zipball/0435a08f69125535336177c29d56af3abc1f69da",
|
||||
"reference": "0435a08f69125535336177c29d56af3abc1f69da",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.1",
|
||||
"symfony/deprecation-contracts": "^2.5|^3",
|
||||
"symfony/polyfill-mbstring": "~1.0"
|
||||
},
|
||||
"conflict": {
|
||||
"symfony/console": "<5.4"
|
||||
},
|
||||
"require-dev": {
|
||||
"ext-iconv": "*",
|
||||
"symfony/console": "^5.4|^6.0|^7.0",
|
||||
"symfony/error-handler": "^6.3|^7.0",
|
||||
"symfony/http-kernel": "^5.4|^6.0|^7.0",
|
||||
"symfony/process": "^5.4|^6.0|^7.0",
|
||||
"symfony/uid": "^5.4|^6.0|^7.0",
|
||||
"twig/twig": "^2.13|^3.0.4"
|
||||
},
|
||||
"bin": [
|
||||
"Resources/bin/var-dump-server"
|
||||
],
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"files": [
|
||||
"Resources/functions/dump.php"
|
||||
],
|
||||
"psr-4": {
|
||||
"Symfony\\Component\\VarDumper\\": ""
|
||||
},
|
||||
"exclude-from-classmap": [
|
||||
"/Tests/"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Nicolas Grekas",
|
||||
"email": "p@tchwork.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"description": "Provides mechanisms for walking through any arbitrary PHP variable",
|
||||
"homepage": "https://symfony.com",
|
||||
"keywords": [
|
||||
"debug",
|
||||
"dump"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/var-dumper/tree/v6.4.3"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://symfony.com/sponsor",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-01-23T14:53:30+00:00"
|
||||
},
|
||||
{
|
||||
"name": "tightenco/collect",
|
||||
"version": "v9.52.7",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/tighten/collect.git",
|
||||
"reference": "b15143cd11fe01a700fcc449df61adc64452fa6d"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/tighten/collect/zipball/b15143cd11fe01a700fcc449df61adc64452fa6d",
|
||||
"reference": "b15143cd11fe01a700fcc449df61adc64452fa6d",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^8.0",
|
||||
"symfony/var-dumper": "^3.4 || ^4.0 || ^5.0 || ^6.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"mockery/mockery": "^1.0",
|
||||
"nesbot/carbon": "^2.23.0",
|
||||
"phpunit/phpunit": "^8.3"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"files": [
|
||||
"src/Collect/Support/helpers.php",
|
||||
"src/Collect/Support/alias.php"
|
||||
],
|
||||
"psr-4": {
|
||||
"Tightenco\\Collect\\": "src/Collect"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Taylor Otwell",
|
||||
"email": "taylorotwell@gmail.com"
|
||||
}
|
||||
],
|
||||
"description": "Collect - Illuminate Collections as a separate package.",
|
||||
"keywords": [
|
||||
"collection",
|
||||
"laravel"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/tighten/collect/issues",
|
||||
"source": "https://github.com/tighten/collect/tree/v9.52.7"
|
||||
},
|
||||
"time": "2023-04-14T21:51:36+00:00"
|
||||
},
|
||||
{
|
||||
"name": "twig/twig",
|
||||
"version": "v3.14.0",
|
||||
@@ -2674,10 +2788,10 @@
|
||||
"packages-dev": [],
|
||||
"aliases": [],
|
||||
"minimum-stability": "stable",
|
||||
"stability-flags": [],
|
||||
"stability-flags": {},
|
||||
"prefer-stable": false,
|
||||
"prefer-lowest": false,
|
||||
"platform": [],
|
||||
"platform-dev": [],
|
||||
"platform": {},
|
||||
"platform-dev": {},
|
||||
"plugin-api-version": "2.6.0"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
Copyright (c) 2017, Ben Scholzen 'DASPRiD'
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
|
||||
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
@@ -0,0 +1,39 @@
|
||||
# QR Code generator
|
||||
|
||||
[](https://github.com/Bacon/BaconQrCode/actions/workflows/ci.yml)
|
||||
[](https://codecov.io/gh/Bacon/BaconQrCode)
|
||||
[](https://packagist.org/packages/bacon/bacon-qr-code)
|
||||
[](https://packagist.org/packages/bacon/bacon-qr-code)
|
||||
[](https://packagist.org/packages/bacon/bacon-qr-code)
|
||||
|
||||
|
||||
## Introduction
|
||||
BaconQrCode is a port of QR code portion of the ZXing library. It currently
|
||||
only features the encoder part, but could later receive the decoder part as
|
||||
well.
|
||||
|
||||
As the Reed Solomon codec implementation of the ZXing library performs quite
|
||||
slow in PHP, it was exchanged with the implementation by Phil Karn.
|
||||
|
||||
|
||||
## Example usage
|
||||
```php
|
||||
use BaconQrCode\Renderer\ImageRenderer;
|
||||
use BaconQrCode\Renderer\Image\ImagickImageBackEnd;
|
||||
use BaconQrCode\Renderer\RendererStyle\RendererStyle;
|
||||
use BaconQrCode\Writer;
|
||||
|
||||
$renderer = new ImageRenderer(
|
||||
new RendererStyle(400),
|
||||
new ImagickImageBackEnd()
|
||||
);
|
||||
$writer = new Writer($renderer);
|
||||
$writer->writeFile('Hello World!', 'qrcode.png');
|
||||
```
|
||||
|
||||
## Available image renderer back ends
|
||||
BaconQrCode comes with multiple back ends for rendering images. Currently included are the following:
|
||||
|
||||
- `ImagickImageBackEnd`: renders raster images using the Imagick library
|
||||
- `SvgImageBackEnd`: renders SVG files using XMLWriter
|
||||
- `EpsImageBackEnd`: renders EPS files
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user