mirror of
https://github.com/mailcow/mailcow-dockerized.git
synced 2026-09-01 11:47:15 +00:00
Compare commits
347
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 | ||
|
|
dd160cd508 | ||
|
|
732b321962 | ||
|
|
2f8a181281 | ||
|
|
83ba8d5840 | ||
|
|
6dc90186f9 |
@@ -14,7 +14,7 @@ jobs:
|
|||||||
pull-requests: write
|
pull-requests: write
|
||||||
steps:
|
steps:
|
||||||
- name: Mark/Close Stale Issues and Pull Requests 🗑️
|
- name: Mark/Close Stale Issues and Pull Requests 🗑️
|
||||||
uses: actions/stale@v10.1.0
|
uses: actions/stale@v11.0.0
|
||||||
with:
|
with:
|
||||||
repo-token: ${{ secrets.STALE_ACTION_PAT }}
|
repo-token: ${{ secrets.STALE_ACTION_PAT }}
|
||||||
days-before-stale: 60
|
days-before-stale: 60
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ jobs:
|
|||||||
- "watchdog-mailcow"
|
- "watchdog-mailcow"
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v7
|
||||||
- name: Setup Docker
|
- name: Setup Docker
|
||||||
run: |
|
run: |
|
||||||
curl -sSL https://get.docker.com/ | CHANNEL=stable sudo sh
|
curl -sSL https://get.docker.com/ | CHANNEL=stable sudo sh
|
||||||
|
|||||||
@@ -8,11 +8,11 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v7
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- name: Run the Action
|
- name: Run the Action
|
||||||
uses: devops-infra/action-pull-request@v0.6.1
|
uses: devops-infra/action-pull-request@v1.4.0
|
||||||
with:
|
with:
|
||||||
github_token: ${{ secrets.PRTONIGHTLY_ACTION_PAT }}
|
github_token: ${{ secrets.PRTONIGHTLY_ACTION_PAT }}
|
||||||
title: Automatic PR to nightly from ${{ github.event.repository.updated_at}}
|
title: Automatic PR to nightly from ${{ github.event.repository.updated_at}}
|
||||||
|
|||||||
@@ -13,24 +13,24 @@ jobs:
|
|||||||
packages: write
|
packages: write
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v7
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v3
|
uses: docker/setup-qemu-action@v4
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v4
|
||||||
|
|
||||||
- name: Login to GHCR
|
- name: Login to GHCR
|
||||||
if: github.event_name != 'pull_request'
|
if: github.event_name != 'pull_request'
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v4
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Build and push
|
- name: Build and push
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@v7
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
|
|||||||
@@ -15,14 +15,14 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v7
|
||||||
|
|
||||||
- name: Generate postscreen_access.cidr
|
- name: Generate postscreen_access.cidr
|
||||||
run: |
|
run: |
|
||||||
bash helper-scripts/update_postscreen_whitelist.sh
|
bash helper-scripts/update_postscreen_whitelist.sh
|
||||||
|
|
||||||
- name: Create Pull Request
|
- name: Create Pull Request
|
||||||
uses: peter-evans/create-pull-request@v7
|
uses: peter-evans/create-pull-request@v8
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.mailcow_action_Update_postscreen_access_cidr_pat }}
|
token: ${{ secrets.mailcow_action_Update_postscreen_access_cidr_pat }}
|
||||||
commit-message: update postscreen_access.cidr
|
commit-message: update postscreen_access.cidr
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ data/conf/sogo/cron.creds
|
|||||||
data/conf/sogo/custom-fulllogo.svg
|
data/conf/sogo/custom-fulllogo.svg
|
||||||
data/conf/sogo/custom-shortlogo.svg
|
data/conf/sogo/custom-shortlogo.svg
|
||||||
data/conf/sogo/custom-fulllogo.png
|
data/conf/sogo/custom-fulllogo.png
|
||||||
|
data/conf/acme/dns-01.conf
|
||||||
data/gitea/
|
data/gitea/
|
||||||
data/gogs/
|
data/gogs/
|
||||||
data/hooks/dovecot/*
|
data/hooks/dovecot/*
|
||||||
|
|||||||
+7
-3
@@ -1,11 +1,11 @@
|
|||||||
# Contribution Guidelines
|
# Contribution Guidelines
|
||||||
**_Last modified on 15th August 2024_**
|
**_Last modified on 12th November 2025_**
|
||||||
|
|
||||||
First of all, thank you for wanting to provide a bugfix or a new feature for the mailcow community, it's because of your help that the project can continue to grow!
|
First of all, thank you for wanting to provide a bugfix or a new feature for the mailcow community, it's because of your help that the project can continue to grow!
|
||||||
|
|
||||||
As we want to keep mailcow's development structured we setup these Guidelines which helps you to create your issue/pull request accordingly.
|
As we want to keep mailcow's development structured we setup these Guidelines which helps you to create your issue/pull request accordingly.
|
||||||
|
|
||||||
**PLEASE NOTE, THAT WE MIGHT CLOSE ISSUES/PULL REQUESTS IF THEY DON'T FULLFIL OUR WRITTEN GUIDELINES WRITTEN INSIDE THIS DOCUMENT**. So please check this guidelines before you propose a Issue/Pull Request.
|
**PLEASE NOTE, THAT WE WILL CLOSE ISSUES/PULL REQUESTS IF THEY DON'T FULFILL OUR WRITTEN GUIDELINES WRITTEN INSIDE THIS DOCUMENT**. So please check this guidelines before you propose a Issue/Pull Request.
|
||||||
|
|
||||||
## Topics
|
## Topics
|
||||||
|
|
||||||
@@ -27,14 +27,18 @@ However, please note the following regarding pull requests:
|
|||||||
6. Please **ALWAYS** create the actual pull request against the staging branch and **NEVER** directly against the master branch. *If you forget to do this, our moobot will remind you to switch the branch to staging.*
|
6. Please **ALWAYS** create the actual pull request against the staging branch and **NEVER** directly against the master branch. *If you forget to do this, our moobot will remind you to switch the branch to staging.*
|
||||||
7. Wait for a merge commit: It may happen that we do not accept your pull request immediately or sometimes not at all for various reasons. Please do not be disappointed if this is the case. We always endeavor to incorporate any meaningful changes from the community into the mailcow project.
|
7. Wait for a merge commit: It may happen that we do not accept your pull request immediately or sometimes not at all for various reasons. Please do not be disappointed if this is the case. We always endeavor to incorporate any meaningful changes from the community into the mailcow project.
|
||||||
8. If you are planning larger and therefore more complex pull requests, it would be advisable to first announce this in a separate issue and then start implementing it after the idea has been accepted in order to avoid unnecessary frustration and effort!
|
8. If you are planning larger and therefore more complex pull requests, it would be advisable to first announce this in a separate issue and then start implementing it after the idea has been accepted in order to avoid unnecessary frustration and effort!
|
||||||
|
9. If your PR requires a Docker image rebuild (changes to Dockerfiles or files in data/Dockerfiles/), update the image tag in docker-compose.yml. Use the base-image versioning (e.g. ghcr.io/mailcow/sogo:5.12.4 → :5.12.5 for version bumps; append a letter for patch fixes, e.g. :5.12.4a). Follow this scheme.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Issue Reporting
|
## Issue Reporting
|
||||||
**_Last modified on 15th August 2024_**
|
**_Last modified on 12th November 2025_**
|
||||||
|
|
||||||
If you plan to report a issue within mailcow please read and understand the following rules:
|
If you plan to report a issue within mailcow please read and understand the following rules:
|
||||||
|
|
||||||
|
### Security disclosures / Security-related fixes
|
||||||
|
- Security vulnerabilities and security fixes must always be reported confidentially first to the contact address specified in SECURITY.md before they are integrated, published, or publicly disclosed in issues/PRs. Please wait for a response from the specified contact to ensure coordinated and responsible disclosure.
|
||||||
|
|
||||||
### Issue Reporting Guidelines
|
### Issue Reporting Guidelines
|
||||||
|
|
||||||
1. **ONLY** use the issue tracker for bug reports or improvement requests and NOT for support questions. For support questions you can either contact the [mailcow community on Telegram](https://docs.mailcow.email/#community-support-and-chat) or the mailcow team directly in exchange for a [support fee](https://docs.mailcow.email/#commercial-support).
|
1. **ONLY** use the issue tracker for bug reports or improvement requests and NOT for support questions. For support questions you can either contact the [mailcow community on Telegram](https://docs.mailcow.email/#community-support-and-chat) or the mailcow team directly in exchange for a [support fee](https://docs.mailcow.email/#commercial-support).
|
||||||
|
|||||||
@@ -23,10 +23,15 @@ A big thank you to everyone supporting us on GitHub Sponsors—your contribution
|
|||||||
<a href="https://www.maehdros.com/" target=_blank><img
|
<a href="https://www.maehdros.com/" target=_blank><img
|
||||||
src="https://avatars.githubusercontent.com/u/173894712" height="58"
|
src="https://avatars.githubusercontent.com/u/173894712" height="58"
|
||||||
/></a>
|
/></a>
|
||||||
|
<a href="https://garske-systems.de/" target=_blank><img
|
||||||
|
src="https://garske-systems.de/img/logo_min.png" height="58"
|
||||||
|
/></a>
|
||||||
|
|
||||||
### 50$/Month Sponsors
|
### 50$/Month Sponsors
|
||||||
<a href="https://github.com/vnukhr" target=_blank><img
|
<a href="https://github.com/vnukhr" target=_blank><img
|
||||||
src="https://avatars.githubusercontent.com/u/7805987?s=52&v=4" height="58"
|
src="https://avatars.githubusercontent.com/u/7805987?s=52&v=4" height="58"
|
||||||
|
/></a><a href="https://github.com/humiico" target=_blank><img
|
||||||
|
src="https://avatars.githubusercontent.com/u/110134705?s=52&v=4" height="58"
|
||||||
/></a>
|
/></a>
|
||||||
|
|
||||||
## Info, documentation and support
|
## Info, documentation and support
|
||||||
|
|||||||
+37
-37
@@ -38,45 +38,45 @@ get_docker_version(){
|
|||||||
}
|
}
|
||||||
|
|
||||||
get_compose_type(){
|
get_compose_type(){
|
||||||
if docker compose > /dev/null 2>&1; then
|
if docker compose > /dev/null 2>&1; then
|
||||||
if docker compose version --short | grep -e "^2." -e "^v2." > /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_VERSION=native
|
||||||
COMPOSE_COMMAND="docker compose"
|
COMPOSE_COMMAND="docker compose"
|
||||||
if [[ "$caller" == "update.sh" ]]; then
|
if [[ "$caller" == "update.sh" ]]; then
|
||||||
sed -i 's/^DOCKER_COMPOSE_VERSION=.*/DOCKER_COMPOSE_VERSION=native/' "$SCRIPT_DIR/mailcow.conf"
|
sed -i 's/^DOCKER_COMPOSE_VERSION=.*/DOCKER_COMPOSE_VERSION=native/' "$SCRIPT_DIR/mailcow.conf"
|
||||||
fi
|
fi
|
||||||
echo -e "\e[33mFound Docker Compose Plugin (native).\e[0m"
|
echo -e "\e[33mFound Docker Compose Plugin (native).\e[0m"
|
||||||
echo -e "\e[33mSetting the DOCKER_COMPOSE_VERSION Variable to native\e[0m"
|
echo -e "\e[33mSetting the DOCKER_COMPOSE_VERSION Variable to native\e[0m"
|
||||||
sleep 2
|
sleep 2
|
||||||
echo -e "\e[33mNotice: You'll have to update this Compose Version via your Package Manager manually!\e[0m"
|
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
|
|
||||||
else
|
else
|
||||||
echo -e "\e[31mCannot find Docker Compose.\e[0m"
|
echo -e "\e[31mCannot find Docker Compose with a Version Higher than 2.X.X.\e[0m"
|
||||||
echo -e "\e[31mPlease install it regarding to this doc site: https://docs.mailcow.email/install/\e[0m"
|
echo -e "\e[31mPlease update/install it manually regarding to this doc site: https://docs.mailcow.email/install/\e[0m"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
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() {
|
detect_bad_asn() {
|
||||||
|
|||||||
@@ -57,11 +57,14 @@ adapt_new_options() {
|
|||||||
"DISABLE_NETFILTER_ISOLATION_RULE"
|
"DISABLE_NETFILTER_ISOLATION_RULE"
|
||||||
"HTTP_REDIRECT"
|
"HTTP_REDIRECT"
|
||||||
"ENABLE_IPV6"
|
"ENABLE_IPV6"
|
||||||
|
"ACME_DNS_CHALLENGE"
|
||||||
|
"ACME_DNS_PROVIDER"
|
||||||
|
"ACME_ACCOUNT_EMAIL"
|
||||||
)
|
)
|
||||||
|
|
||||||
sed -i --follow-symlinks '$a\' mailcow.conf
|
sed -i --follow-symlinks '$a\' mailcow.conf
|
||||||
for option in ${CONFIG_ARRAY[@]}; do
|
for option in ${CONFIG_ARRAY[@]}; do
|
||||||
if grep -q "${option}" mailcow.conf; then
|
if grep -q "^#\?${option}=" mailcow.conf; then
|
||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -292,6 +295,20 @@ adapt_new_options() {
|
|||||||
echo '# This key is used to encrypt email addresses within SOGo URLs' >> 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
|
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
|
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>"
|
LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
|
||||||
|
|
||||||
@@ -14,11 +14,22 @@ RUN apk upgrade --no-cache \
|
|||||||
tini \
|
tini \
|
||||||
tzdata \
|
tzdata \
|
||||||
python3 \
|
python3 \
|
||||||
acme-tiny
|
acme-tiny \
|
||||||
|
git \
|
||||||
|
socat \
|
||||||
|
&& git clone --depth 1 https://github.com/acmesh-official/acme.sh.git /opt/acme.sh \
|
||||||
|
&& chmod +x /opt/acme.sh/acme.sh \
|
||||||
|
&& mkdir -p /var/lib/acme/acme-sh
|
||||||
|
|
||||||
|
ENV ACME_SH_BIN=/opt/acme.sh/acme.sh \
|
||||||
|
ACME_SH_HOME=/opt/acme.sh \
|
||||||
|
ACME_SH_CONFIG_HOME=/var/lib/acme/acme-sh
|
||||||
|
|
||||||
COPY acme.sh /srv/acme.sh
|
COPY acme.sh /srv/acme.sh
|
||||||
COPY functions.sh /srv/functions.sh
|
COPY functions.sh /srv/functions.sh
|
||||||
COPY obtain-certificate.sh /srv/obtain-certificate.sh
|
COPY obtain-certificate.sh /srv/obtain-certificate.sh
|
||||||
|
COPY obtain-certificate-dns.sh /srv/obtain-certificate-dns.sh
|
||||||
|
COPY load-dns-config.sh /srv/load-dns-config.sh
|
||||||
COPY reload-configurations.sh /srv/reload-configurations.sh
|
COPY reload-configurations.sh /srv/reload-configurations.sh
|
||||||
COPY expand6.sh /srv/expand6.sh
|
COPY expand6.sh /srv/expand6.sh
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,17 @@ until [[ $(${REDIS_CMDLINE} PING) == "PONG" ]]; do
|
|||||||
sleep 2
|
sleep 2
|
||||||
done
|
done
|
||||||
|
|
||||||
|
# Create DNS-01 configuration template if it doesn't exist
|
||||||
|
if [[ ! -f /etc/acme/dns-01.conf ]]; then
|
||||||
|
mkdir -p /etc/acme
|
||||||
|
cat > /etc/acme/dns-01.conf <<'EOF'
|
||||||
|
# Add here your DNS-01 challenge configuration
|
||||||
|
# For more information, visit the acme.sh documentation:
|
||||||
|
# https://github.com/acmesh-official/acme.sh/wiki/dnsapi
|
||||||
|
EOF
|
||||||
|
echo "Created DNS-01 configuration template at /etc/acme/dns-01.conf"
|
||||||
|
fi
|
||||||
|
|
||||||
source /srv/functions.sh
|
source /srv/functions.sh
|
||||||
# Thanks to https://github.com/cvmiller -> https://github.com/cvmiller/expand6
|
# Thanks to https://github.com/cvmiller -> https://github.com/cvmiller/expand6
|
||||||
source /srv/expand6.sh
|
source /srv/expand6.sh
|
||||||
@@ -42,6 +53,10 @@ if [[ "${ENABLE_SSL_SNI}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
|
|||||||
ENABLE_SSL_SNI=y
|
ENABLE_SSL_SNI=y
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [[ "${ACME_DNS_CHALLENGE}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
|
||||||
|
ACME_DNS_CHALLENGE=y
|
||||||
|
fi
|
||||||
|
|
||||||
if [[ "${SKIP_LETS_ENCRYPT}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
|
if [[ "${SKIP_LETS_ENCRYPT}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
|
||||||
log_f "SKIP_LETS_ENCRYPT=y, skipping Let's Encrypt..."
|
log_f "SKIP_LETS_ENCRYPT=y, skipping Let's Encrypt..."
|
||||||
sleep 365d
|
sleep 365d
|
||||||
@@ -234,18 +249,71 @@ while true; do
|
|||||||
done <<< "${SQL_DOMAINS}"
|
done <<< "${SQL_DOMAINS}"
|
||||||
|
|
||||||
if [[ ${ONLY_MAILCOW_HOSTNAME} != "y" ]]; then
|
if [[ ${ONLY_MAILCOW_HOSTNAME} != "y" ]]; then
|
||||||
|
# Fetch all domains with an active MTA-STS policy once.
|
||||||
|
unset MTA_STS_ACTIVE_DOMAINS
|
||||||
|
declare -A MTA_STS_ACTIVE_DOMAINS
|
||||||
|
if [[ ${AUTODISCOVER_SAN} == "y" ]]; then
|
||||||
|
SQL_MTA_STS_DOMAINS=$(mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT domain FROM mta_sts WHERE active = 1" -Bs)
|
||||||
|
if [[ $? -eq 0 ]]; then
|
||||||
|
while read mta_sts_domain; do
|
||||||
|
if [[ -z "${mta_sts_domain}" ]]; then
|
||||||
|
# ignore empty lines
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
MTA_STS_ACTIVE_DOMAINS["${mta_sts_domain}"]=1
|
||||||
|
done <<< "${SQL_MTA_STS_DOMAINS}"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
for SQL_DOMAIN in "${SQL_DOMAIN_ARR[@]}"; do
|
for SQL_DOMAIN in "${SQL_DOMAIN_ARR[@]}"; do
|
||||||
unset VALIDATED_CONFIG_DOMAINS_SUBDOMAINS
|
unset VALIDATED_CONFIG_DOMAINS_SUBDOMAINS
|
||||||
declare -a VALIDATED_CONFIG_DOMAINS_SUBDOMAINS
|
declare -a VALIDATED_CONFIG_DOMAINS_SUBDOMAINS
|
||||||
for SUBDOMAIN in "${ADDITIONAL_WC_ARR[@]}"; do
|
for SUBDOMAIN in "${ADDITIONAL_WC_ARR[@]}"; do
|
||||||
if [[ "${SUBDOMAIN}.${SQL_DOMAIN}" != "${MAILCOW_HOSTNAME}" ]]; then
|
FULL_SUBDOMAIN="${SUBDOMAIN}.${SQL_DOMAIN}"
|
||||||
if check_domain "${SUBDOMAIN}.${SQL_DOMAIN}"; then
|
|
||||||
VALIDATED_CONFIG_DOMAINS_SUBDOMAINS+=("${SUBDOMAIN}.${SQL_DOMAIN}")
|
# Skip mta-sts subdomain unless MTA-STS is enabled (active) for this domain
|
||||||
fi
|
if [[ "${SUBDOMAIN}" == "mta-sts" && -z "${MTA_STS_ACTIVE_DOMAINS[${SQL_DOMAIN}]}" ]]; then
|
||||||
|
log_f "MTA-STS is not enabled for ${SQL_DOMAIN} - skipping mta-sts subdomain certificate"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Skip if subdomain matches MAILCOW_HOSTNAME
|
||||||
|
if [[ "${FULL_SUBDOMAIN}" == "${MAILCOW_HOSTNAME}" ]]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
# Skip if subdomain is covered by a wildcard in ADDITIONAL_SAN
|
||||||
|
if is_covered_by_wildcard "${FULL_SUBDOMAIN}"; then
|
||||||
|
log_f "Subdomain '${FULL_SUBDOMAIN}' is covered by wildcard - skipping explicit subdomain"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
# Validate and add subdomain
|
||||||
|
if check_domain "${FULL_SUBDOMAIN}"; then
|
||||||
|
VALIDATED_CONFIG_DOMAINS_SUBDOMAINS+=("${FULL_SUBDOMAIN}")
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
VALIDATED_CONFIG_DOMAINS+=("${VALIDATED_CONFIG_DOMAINS_SUBDOMAINS[*]}")
|
VALIDATED_CONFIG_DOMAINS+=("${VALIDATED_CONFIG_DOMAINS_SUBDOMAINS[*]}")
|
||||||
done
|
done
|
||||||
|
|
||||||
|
# Fetch alias domains where target domain has MTA-STS enabled
|
||||||
|
if [[ ${AUTODISCOVER_SAN} == "y" ]]; then
|
||||||
|
SQL_ALIAS_DOMAINS=$(mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT ad.alias_domain FROM alias_domain ad INNER JOIN mta_sts m ON ad.target_domain = m.domain WHERE ad.active = 1 AND m.active = 1" -Bs)
|
||||||
|
if [[ $? -eq 0 ]]; then
|
||||||
|
while read alias_domain; do
|
||||||
|
if [[ -z "${alias_domain}" ]]; then
|
||||||
|
# ignore empty lines
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
# Only add mta-sts subdomain for alias domains
|
||||||
|
if [[ "mta-sts.${alias_domain}" != "${MAILCOW_HOSTNAME}" ]]; then
|
||||||
|
# Skip if mta-sts subdomain is covered by a wildcard
|
||||||
|
if is_covered_by_wildcard "mta-sts.${alias_domain}"; then
|
||||||
|
log_f "Alias domain mta-sts subdomain 'mta-sts.${alias_domain}' is covered by wildcard - skipping"
|
||||||
|
elif check_domain "mta-sts.${alias_domain}"; then
|
||||||
|
VALIDATED_CONFIG_DOMAINS+=("mta-sts.${alias_domain}")
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done <<< "${SQL_ALIAS_DOMAINS}"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if check_domain ${MAILCOW_HOSTNAME}; then
|
if check_domain ${MAILCOW_HOSTNAME}; then
|
||||||
@@ -274,13 +342,31 @@ while true; do
|
|||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Check if MAILCOW_HOSTNAME is covered by a wildcard in ADDITIONAL_SAN
|
||||||
|
MAILCOW_HOSTNAME_COVERED=0
|
||||||
|
if [[ ! -z ${VALIDATED_MAILCOW_HOSTNAME} ]]; then
|
||||||
|
if is_covered_by_wildcard "${VALIDATED_MAILCOW_HOSTNAME}"; then
|
||||||
|
MAILCOW_PARENT_DOMAIN=$(echo ${VALIDATED_MAILCOW_HOSTNAME} | cut -d. -f2-)
|
||||||
|
log_f "MAILCOW_HOSTNAME '${VALIDATED_MAILCOW_HOSTNAME}' is covered by wildcard '*.${MAILCOW_PARENT_DOMAIN}' - skipping explicit hostname"
|
||||||
|
MAILCOW_HOSTNAME_COVERED=1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
# Unique domains for server certificate
|
# Unique domains for server certificate
|
||||||
if [[ ${ENABLE_SSL_SNI} == "y" ]]; then
|
if [[ ${ENABLE_SSL_SNI} == "y" ]]; then
|
||||||
# create certificate for server name and fqdn SANs only
|
# create certificate for server name and fqdn SANs only
|
||||||
SERVER_SAN_VALIDATED=(${VALIDATED_MAILCOW_HOSTNAME} $(echo ${ADDITIONAL_VALIDATED_SAN[*]} | xargs -n1 | sort -u | xargs))
|
if [[ ${MAILCOW_HOSTNAME_COVERED} == "1" ]]; then
|
||||||
|
SERVER_SAN_VALIDATED=($(echo ${ADDITIONAL_VALIDATED_SAN[*]} | xargs -n1 | sort -u | xargs))
|
||||||
|
else
|
||||||
|
SERVER_SAN_VALIDATED=(${VALIDATED_MAILCOW_HOSTNAME} $(echo ${ADDITIONAL_VALIDATED_SAN[*]} | xargs -n1 | sort -u | xargs))
|
||||||
|
fi
|
||||||
else
|
else
|
||||||
# create certificate for all domains, including all subdomains from other domains [*]
|
# create certificate for all domains, including all subdomains from other domains [*]
|
||||||
SERVER_SAN_VALIDATED=(${VALIDATED_MAILCOW_HOSTNAME} $(echo ${VALIDATED_CONFIG_DOMAINS[*]} ${ADDITIONAL_VALIDATED_SAN[*]} | xargs -n1 | sort -u | xargs))
|
if [[ ${MAILCOW_HOSTNAME_COVERED} == "1" ]]; then
|
||||||
|
SERVER_SAN_VALIDATED=($(echo ${VALIDATED_CONFIG_DOMAINS[*]} ${ADDITIONAL_VALIDATED_SAN[*]} | xargs -n1 | sort -u | xargs))
|
||||||
|
else
|
||||||
|
SERVER_SAN_VALIDATED=(${VALIDATED_MAILCOW_HOSTNAME} $(echo ${VALIDATED_CONFIG_DOMAINS[*]} ${ADDITIONAL_VALIDATED_SAN[*]} | xargs -n1 | sort -u | xargs))
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
if [[ ! -z ${SERVER_SAN_VALIDATED[*]} ]]; then
|
if [[ ! -z ${SERVER_SAN_VALIDATED[*]} ]]; then
|
||||||
CERT_NAME=${SERVER_SAN_VALIDATED[0]}
|
CERT_NAME=${SERVER_SAN_VALIDATED[0]}
|
||||||
|
|||||||
@@ -80,6 +80,11 @@ check_domain(){
|
|||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [[ ${ACME_DNS_CHALLENGE} == "y" ]]; then
|
||||||
|
log_f "ACME_DNS_CHALLENGE=y - skipping IP and HTTP validation for ${DOMAIN}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
# Check if CNAME without v6 enabled target
|
# Check if CNAME without v6 enabled target
|
||||||
if [[ ! -z ${AAAA_DOMAIN} ]] && [[ -z $(echo ${AAAA_DOMAIN} | grep "^\([0-9a-fA-F]\{0,4\}:\)\{1,7\}[0-9a-fA-F]\{0,4\}$") ]]; then
|
if [[ ! -z ${AAAA_DOMAIN} ]] && [[ -z $(echo ${AAAA_DOMAIN} | grep "^\([0-9a-fA-F]\{0,4\}:\)\{1,7\}[0-9a-fA-F]\{0,4\}$") ]]; then
|
||||||
AAAA_DOMAIN=
|
AAAA_DOMAIN=
|
||||||
@@ -130,3 +135,32 @@ verify_challenge_path(){
|
|||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Check if a domain is covered by a wildcard (*.example.com) in ADDITIONAL_SAN
|
||||||
|
# Usage: is_covered_by_wildcard "subdomain.example.com"
|
||||||
|
# Returns: 0 if covered, 1 if not covered
|
||||||
|
# Note: Only returns 0 (covered) when DNS-01 challenge is enabled,
|
||||||
|
# as wildcards cannot be validated with HTTP-01 challenge
|
||||||
|
is_covered_by_wildcard() {
|
||||||
|
local DOMAIN=$1
|
||||||
|
|
||||||
|
# Only skip if DNS challenge is enabled (wildcards require DNS-01)
|
||||||
|
if [[ ${ACME_DNS_CHALLENGE} != "y" ]]; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Return early if no ADDITIONAL_SAN is set
|
||||||
|
if [[ -z ${ADDITIONAL_SAN} ]]; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Extract parent domain (e.g., mail.example.com -> example.com)
|
||||||
|
local PARENT_DOMAIN=$(echo ${DOMAIN} | cut -d. -f2-)
|
||||||
|
|
||||||
|
# Check if ADDITIONAL_SAN contains a wildcard for this parent domain
|
||||||
|
if [[ "${ADDITIONAL_SAN}" == *"*.${PARENT_DOMAIN}"* ]]; then
|
||||||
|
return 0 # Covered by wildcard
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 1 # Not covered
|
||||||
|
}
|
||||||
|
|||||||
Executable
+57
@@ -0,0 +1,57 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
SCRIPT_SOURCE="${BASH_SOURCE[0]:-${0}}"
|
||||||
|
if [[ "${SCRIPT_SOURCE}" == "${0}" ]]; then
|
||||||
|
__dns_loader_standalone=1
|
||||||
|
else
|
||||||
|
__dns_loader_standalone=0
|
||||||
|
fi
|
||||||
|
|
||||||
|
CONFIG_PATH="${ACME_DNS_CONFIG_FILE:-/etc/acme/dns-01.conf}"
|
||||||
|
|
||||||
|
if [[ ! -f "${CONFIG_PATH}" ]]; then
|
||||||
|
if [[ $__dns_loader_standalone -eq 1 ]]; then
|
||||||
|
exit 0
|
||||||
|
else
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
source /srv/functions.sh
|
||||||
|
|
||||||
|
log_f "Loading DNS-01 configuration from ${CONFIG_PATH}"
|
||||||
|
|
||||||
|
LINE_NO=0
|
||||||
|
while IFS= read -r line || [[ -n "${line}" ]]; do
|
||||||
|
LINE_NO=$((LINE_NO+1))
|
||||||
|
line="${line%$'\r'}"
|
||||||
|
line_trimmed="$(printf '%s' "${line}" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')"
|
||||||
|
[[ -z "${line_trimmed}" ]] && continue
|
||||||
|
[[ "${line_trimmed:0:1}" == "#" ]] && continue
|
||||||
|
if [[ "${line_trimmed}" != *=* ]]; then
|
||||||
|
log_f "Skipping invalid DNS config line ${LINE_NO} (missing key=value)"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
KEY="${line_trimmed%%=*}"
|
||||||
|
VALUE="${line_trimmed#*=}"
|
||||||
|
KEY="$(printf '%s' "${KEY}" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')"
|
||||||
|
VALUE="$(printf '%s' "${VALUE}" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')"
|
||||||
|
if [[ -z "${KEY}" ]]; then
|
||||||
|
log_f "Skipping invalid DNS config line ${LINE_NO} (empty key)"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
if [[ "${VALUE}" =~ ^\".*\"$ ]]; then
|
||||||
|
VALUE="${VALUE:1:-1}"
|
||||||
|
elif [[ "${VALUE}" =~ ^\'.*\'$ ]]; then
|
||||||
|
VALUE="${VALUE:1:-1}"
|
||||||
|
fi
|
||||||
|
export "${KEY}"="${VALUE}"
|
||||||
|
log_f "Exported DNS config key ${KEY}"
|
||||||
|
|
||||||
|
done < "${CONFIG_PATH}"
|
||||||
|
|
||||||
|
if [[ $__dns_loader_standalone -eq 1 ]]; then
|
||||||
|
exit 0
|
||||||
|
else
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
@@ -0,0 +1,177 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Return values / exit codes
|
||||||
|
# 0 = cert created successfully
|
||||||
|
# 1 = cert renewed successfully
|
||||||
|
# 2 = cert not due for renewal
|
||||||
|
# * = errors
|
||||||
|
|
||||||
|
source /srv/functions.sh
|
||||||
|
|
||||||
|
CERT_DOMAINS=(${DOMAINS[@]})
|
||||||
|
CERT_DOMAIN=${CERT_DOMAINS[0]}
|
||||||
|
ACME_BASE=/var/lib/acme
|
||||||
|
|
||||||
|
# Load optional DNS provider secrets from /etc/acme/dns-01.conf
|
||||||
|
if [[ -f /srv/load-dns-config.sh ]]; then
|
||||||
|
source /srv/load-dns-config.sh
|
||||||
|
if declare -F log_f >/dev/null; then
|
||||||
|
log_f "ACME_DNS_CHALLENGE is enabled, DNS provider secrets loaded"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
TYPE=${1}
|
||||||
|
PREFIX=""
|
||||||
|
# only support rsa certificates for now
|
||||||
|
if [[ "${TYPE}" != "rsa" ]]; then
|
||||||
|
log_f "Unknown certificate type '${TYPE}' requested"
|
||||||
|
exit 5
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "${ACME_DNS_PROVIDER}" ]]; then
|
||||||
|
log_f "ACME_DNS_PROVIDER is required when ACME_DNS_CHALLENGE is enabled"
|
||||||
|
exit 6
|
||||||
|
fi
|
||||||
|
|
||||||
|
DOMAINS_FILE=${ACME_BASE}/${CERT_DOMAIN}/domains
|
||||||
|
CERT=${ACME_BASE}/${CERT_DOMAIN}/${PREFIX}cert.pem
|
||||||
|
SHARED_KEY=${ACME_BASE}/acme/${PREFIX}key.pem # must already exist
|
||||||
|
KEY=${ACME_BASE}/${CERT_DOMAIN}/${PREFIX}key.pem
|
||||||
|
CSR=${ACME_BASE}/${CERT_DOMAIN}/${PREFIX}acme.csr
|
||||||
|
|
||||||
|
if [[ -z ${CERT_DOMAINS[*]} ]]; then
|
||||||
|
log_f "Missing CERT_DOMAINS to obtain a certificate"
|
||||||
|
exit 3
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "${LE_STAGING}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
|
||||||
|
if [[ ! -z "${DIRECTORY_URL}" ]]; then
|
||||||
|
log_f "Cannot use DIRECTORY_URL with LE_STAGING=y - ignoring DIRECTORY_URL"
|
||||||
|
fi
|
||||||
|
log_f "Using Let's Encrypt staging servers"
|
||||||
|
ACME_SH_SERVER_ARGS=("--staging")
|
||||||
|
elif [[ ! -z "${DIRECTORY_URL}" ]]; then
|
||||||
|
log_f "Using custom directory URL ${DIRECTORY_URL}"
|
||||||
|
ACME_SH_SERVER_ARGS=("--server" "${DIRECTORY_URL}")
|
||||||
|
else
|
||||||
|
log_f "Using Let's Encrypt production servers"
|
||||||
|
ACME_SH_SERVER_ARGS=("--server" "letsencrypt")
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -f ${DOMAINS_FILE} && "$(cat ${DOMAINS_FILE})" == "${CERT_DOMAINS[*]}" ]]; then
|
||||||
|
if [[ ! -f ${CERT} || ! -f "${KEY}" || -f "${ACME_BASE}/force_renew" ]]; then
|
||||||
|
log_f "Certificate ${CERT} doesn't exist yet or forced renewal - start obtaining"
|
||||||
|
elif ! openssl x509 -checkend 2592000 -noout -in ${CERT} > /dev/null; then
|
||||||
|
log_f "Certificate ${CERT} is due for renewal (< 30 days) - start renewing"
|
||||||
|
else
|
||||||
|
log_f "Certificate ${CERT} validation done, neither changed nor due for renewal."
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
log_f "Certificate ${CERT} missing or changed domains '${CERT_DOMAINS[*]}' - start obtaining"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Make backup
|
||||||
|
if [[ -f ${CERT} ]]; then
|
||||||
|
DATE=$(date +%Y-%m-%d_%H_%M_%S)
|
||||||
|
BACKUP_DIR=${ACME_BASE}/backups/${CERT_DOMAIN}/${PREFIX}${DATE}
|
||||||
|
log_f "Creating backups in ${BACKUP_DIR} ..."
|
||||||
|
mkdir -p ${BACKUP_DIR}/
|
||||||
|
[[ -f ${DOMAINS_FILE} ]] && cp ${DOMAINS_FILE} ${BACKUP_DIR}/
|
||||||
|
[[ -f ${CERT} ]] && cp ${CERT} ${BACKUP_DIR}/
|
||||||
|
[[ -f ${KEY} ]] && cp ${KEY} ${BACKUP_DIR}/
|
||||||
|
[[ -f ${CSR} ]] && cp ${CSR} ${BACKUP_DIR}/
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p ${ACME_BASE}/${CERT_DOMAIN}
|
||||||
|
if [[ ! -f ${KEY} ]]; then
|
||||||
|
log_f "Copying shared private key for this certificate..."
|
||||||
|
cp ${SHARED_KEY} ${KEY}
|
||||||
|
chmod 600 ${KEY}
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Generating CSR to keep layout parity with HTTP challenge flow
|
||||||
|
printf "[SAN]\nsubjectAltName=" > /tmp/_SAN
|
||||||
|
printf "DNS:%s," "${CERT_DOMAINS[@]}" >> /tmp/_SAN
|
||||||
|
sed -i '$s/,$//' /tmp/_SAN
|
||||||
|
openssl req -new -sha256 -key ${KEY} -subj "/" -reqexts SAN -config <(cat "$(openssl version -d | sed 's/.*\"\(.*\)\"/\1/g')/openssl.cnf" /tmp/_SAN) > ${CSR}
|
||||||
|
|
||||||
|
log_f "Checking resolver..."
|
||||||
|
until dig letsencrypt.org +time=3 +tries=1 @unbound > /dev/null; do
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
log_f "Resolver OK"
|
||||||
|
|
||||||
|
ACME_SH_BIN_PATH=${ACME_SH_BIN:-/opt/acme.sh/acme.sh}
|
||||||
|
ACME_SH_WORK_HOME=${ACME_SH_CONFIG_HOME:-/var/lib/acme/acme-sh}
|
||||||
|
mkdir -p ${ACME_SH_WORK_HOME}
|
||||||
|
|
||||||
|
if [[ ! -x ${ACME_SH_BIN_PATH} ]]; then
|
||||||
|
log_f "acme.sh binary not found at ${ACME_SH_BIN_PATH}"
|
||||||
|
exit 7
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -f ${ACME_SH_WORK_HOME}/account.conf ]]; then
|
||||||
|
if [[ -z "${ACME_ACCOUNT_EMAIL}" ]]; then
|
||||||
|
log_f "ACME_ACCOUNT_EMAIL is required to register a new acme.sh account"
|
||||||
|
exit 8
|
||||||
|
fi
|
||||||
|
log_f "Registering acme.sh account for ${ACME_ACCOUNT_EMAIL}"
|
||||||
|
REGISTER_CMD=("${ACME_SH_BIN_PATH}" "--home" "${ACME_SH_WORK_HOME}" "--config-home" "${ACME_SH_WORK_HOME}" "--cert-home" "${ACME_SH_WORK_HOME}" "--register-account" "-m" "${ACME_ACCOUNT_EMAIL}")
|
||||||
|
REGISTER_CMD+=("${ACME_SH_SERVER_ARGS[@]}")
|
||||||
|
REGISTER_RESPONSE=$("${REGISTER_CMD[@]}" 2>&1)
|
||||||
|
if [[ $? -ne 0 ]]; then
|
||||||
|
log_f "Failed to register acme.sh account: ${REGISTER_RESPONSE}"
|
||||||
|
exit 9
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
TMP_CERT=$(mktemp /tmp/acme-cert.XXXXXX)
|
||||||
|
TMP_FULLCHAIN=$(mktemp /tmp/acme-fullchain.XXXXXX)
|
||||||
|
|
||||||
|
ACME_CMD=("${ACME_SH_BIN_PATH}" "--home" "${ACME_SH_WORK_HOME}" "--config-home" "${ACME_SH_WORK_HOME}" "--cert-home" "${ACME_SH_WORK_HOME}")
|
||||||
|
ACME_CMD+=("${ACME_SH_SERVER_ARGS[@]}")
|
||||||
|
ACME_CMD+=("--issue" "--dns" "${ACME_DNS_PROVIDER}" "--key-file" "${KEY}" "--cert-file" "${TMP_CERT}" "--fullchain-file" "${TMP_FULLCHAIN}" "--force")
|
||||||
|
for domain in "${CERT_DOMAINS[@]}"; do
|
||||||
|
ACME_CMD+=("-d" "${domain}")
|
||||||
|
done
|
||||||
|
|
||||||
|
log_f "Using command ${ACME_CMD[*]}"
|
||||||
|
if [[ -n "${ACME_DNS_PROVIDER}" ]]; then
|
||||||
|
log_f "DNS provider: ${ACME_DNS_PROVIDER}"
|
||||||
|
fi
|
||||||
|
if compgen -A variable | grep -Eq "^DNS_|^ACME_"; then
|
||||||
|
LOG_KEYS=$(env | grep -E "^(DNS_|ACME_)" | cut -d= -f1 | tr '\n' ' ')
|
||||||
|
log_f "Available DNS/ACME env keys: ${LOG_KEYS}" redis_only
|
||||||
|
fi
|
||||||
|
ACME_RESPONSE=$("${ACME_CMD[@]}" 2>&1 | tee /dev/fd/5; exit ${PIPESTATUS[0]})
|
||||||
|
SUCCESS="$?"
|
||||||
|
ACME_RESPONSE_B64=$(echo "${ACME_RESPONSE}" | openssl enc -e -A -base64)
|
||||||
|
log_f "${ACME_RESPONSE_B64}" redis_only b64
|
||||||
|
|
||||||
|
case "$SUCCESS" in
|
||||||
|
0)
|
||||||
|
log_f "Deploying certificate ${CERT}..."
|
||||||
|
if verify_hash_match ${TMP_FULLCHAIN} ${KEY}; then
|
||||||
|
RETURN=0
|
||||||
|
if [[ -f ${CERT} ]]; then
|
||||||
|
RETURN=1
|
||||||
|
fi
|
||||||
|
mv -f ${TMP_FULLCHAIN} ${CERT}
|
||||||
|
rm -f ${TMP_CERT}
|
||||||
|
echo -n ${CERT_DOMAINS[*]} > ${DOMAINS_FILE}
|
||||||
|
log_f "Certificate successfully obtained via DNS challenge"
|
||||||
|
exit ${RETURN}
|
||||||
|
else
|
||||||
|
log_f "Certificate was requested, but key and certificate hashes do not match"
|
||||||
|
rm -f ${TMP_CERT} ${TMP_FULLCHAIN}
|
||||||
|
exit 4
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
log_f "Failed to obtain certificate ${CERT} for domains '${CERT_DOMAINS[*]}' via DNS challenge"
|
||||||
|
redis-cli -h redis -a ${REDISPASS} --no-auth-warning SET ACME_FAIL_TIME "$(date +%s)"
|
||||||
|
rm -f ${TMP_CERT} ${TMP_FULLCHAIN}
|
||||||
|
exit 100${SUCCESS}
|
||||||
|
;;
|
||||||
|
esac
|
||||||
@@ -20,6 +20,10 @@ if [[ "${TYPE}" != "rsa" ]]; then
|
|||||||
log_f "Unknown certificate type '${TYPE}' requested"
|
log_f "Unknown certificate type '${TYPE}' requested"
|
||||||
exit 5
|
exit 5
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [[ "${ACME_DNS_CHALLENGE}" == "y" ]]; then
|
||||||
|
exec /srv/obtain-certificate-dns.sh "$@"
|
||||||
|
fi
|
||||||
DOMAINS_FILE=${ACME_BASE}/${CERT_DOMAIN}/domains
|
DOMAINS_FILE=${ACME_BASE}/${CERT_DOMAIN}/domains
|
||||||
CERT=${ACME_BASE}/${CERT_DOMAIN}/${PREFIX}cert.pem
|
CERT=${ACME_BASE}/${CERT_DOMAIN}/${PREFIX}cert.pem
|
||||||
SHARED_KEY=${ACME_BASE}/acme/${PREFIX}key.pem # must already exist
|
SHARED_KEY=${ACME_BASE}/acme/${PREFIX}key.pem # must already exist
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
FROM debian:bookworm-slim
|
FROM debian:trixie-slim
|
||||||
|
|
||||||
RUN apt update && apt install pigz -y --no-install-recommends
|
RUN apt update && apt install pigz zstd -y --no-install-recommends
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
FROM alpine:3.21 AS builder
|
FROM alpine:3.24 AS builder
|
||||||
|
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
ENV CLAMD_VERSION=1.4.2
|
ENV CLAMD_VERSION=1.4.6
|
||||||
|
|
||||||
RUN apk upgrade --no-cache \
|
RUN apk upgrade --no-cache \
|
||||||
&& apk add --update --no-cache \
|
&& apk add --update --no-cache \
|
||||||
@@ -68,7 +68,7 @@ RUN wget -P /src https://www.clamav.net/downloads/production/clamav-${CLAMD_VERS
|
|||||||
"/clamav/etc/clamav/clamav-milter.conf.sample" > "/clamav/etc/clamav/clamav-milter.conf" || exit 1
|
"/clamav/etc/clamav/clamav-milter.conf.sample" > "/clamav/etc/clamav/clamav-milter.conf" || exit 1
|
||||||
|
|
||||||
|
|
||||||
FROM alpine:3.21
|
FROM alpine:3.24
|
||||||
|
|
||||||
LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
|
LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM alpine:3.21
|
FROM alpine:3.23
|
||||||
|
|
||||||
LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
|
LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
|
||||||
|
|
||||||
|
|||||||
@@ -110,12 +110,12 @@ async def get_container(container_id : str):
|
|||||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||||
|
|
||||||
@app.get("/containers/json")
|
@app.get("/containers/json")
|
||||||
async def get_containers():
|
async def get_containers(all: bool = False):
|
||||||
global dockerapi
|
global dockerapi
|
||||||
|
|
||||||
containers = {}
|
containers = {}
|
||||||
try:
|
try:
|
||||||
for container in (await dockerapi.async_docker_client.containers.list()):
|
for container in (await dockerapi.async_docker_client.containers.list(all=all)):
|
||||||
container_info = await container.show()
|
container_info = await container.show()
|
||||||
containers.update({container_info['Id']: container_info})
|
containers.update({container_info['Id']: container_info})
|
||||||
return Response(content=json.dumps(containers, indent=4), media_type="application/json")
|
return Response(content=json.dumps(containers, indent=4), media_type="application/json")
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ FROM alpine:3.21
|
|||||||
LABEL maintainer="The Infrastructure Company GmbH <info@servercow.de>"
|
LABEL maintainer="The Infrastructure Company GmbH <info@servercow.de>"
|
||||||
|
|
||||||
# renovate: datasource=github-releases depName=tianon/gosu versioning=semver-coerced extractVersion=^(?<version>.*)$
|
# renovate: datasource=github-releases depName=tianon/gosu versioning=semver-coerced extractVersion=^(?<version>.*)$
|
||||||
ARG GOSU_VERSION=1.17
|
ARG GOSU_VERSION=1.19
|
||||||
|
|
||||||
ENV LANG=C.UTF-8
|
ENV LANG=C.UTF-8
|
||||||
ENV LC_ALL=C.UTF-8
|
ENV LC_ALL=C.UTF-8
|
||||||
|
|||||||
@@ -204,16 +204,17 @@ EOF
|
|||||||
# Create random master Password for SOGo SSO
|
# Create random master Password for SOGo SSO
|
||||||
RAND_PASS=$(cat /dev/urandom | tr -dc 'a-z0-9' | fold -w 32 | head -n 1)
|
RAND_PASS=$(cat /dev/urandom | tr -dc 'a-z0-9' | fold -w 32 | head -n 1)
|
||||||
echo -n ${RAND_PASS} > /etc/phpfpm/sogo-sso.pass
|
echo -n ${RAND_PASS} > /etc/phpfpm/sogo-sso.pass
|
||||||
# Creating additional creds file for SOGo notify crons (calendars, etc)
|
|
||||||
echo -n ${RAND_USER}@mailcow.local:${RAND_PASS} > /etc/sogo/cron.creds
|
|
||||||
cat <<EOF > /etc/dovecot/sogo-sso.conf
|
cat <<EOF > /etc/dovecot/sogo-sso.conf
|
||||||
# Autogenerated by mailcow
|
# Autogenerated by mailcow
|
||||||
passdb {
|
passdb {
|
||||||
driver = static
|
driver = static
|
||||||
args = allow_real_nets=${IPV4_NETWORK}.248/32 password={plain}${RAND_PASS}
|
args = allow_nets=${IPV4_NETWORK}.248/32 password={plain}${RAND_PASS}
|
||||||
}
|
}
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
|
# Creating additional creds file for SOGo notify crons (calendars, etc) (dummy user, sso password)
|
||||||
|
echo -n ${RAND_USER}@mailcow.local:${RAND_PASS} > /etc/sogo/cron.creds
|
||||||
|
|
||||||
if [[ "${MASTER}" =~ ^([nN][oO]|[nN])+$ ]]; then
|
if [[ "${MASTER}" =~ ^([nN][oO]|[nN])+$ ]]; then
|
||||||
# Toggling MASTER will result in a rebuild of containers, so the quota script will be recreated
|
# Toggling MASTER will result in a rebuild of containers, so the quota script will be recreated
|
||||||
cat <<'EOF' > /usr/local/bin/quota_notify.py
|
cat <<'EOF' > /usr/local/bin/quota_notify.py
|
||||||
|
|||||||
@@ -5,35 +5,13 @@ use LockFile::Simple qw(lock trylock unlock);
|
|||||||
use Proc::ProcessTable;
|
use Proc::ProcessTable;
|
||||||
use Data::Dumper qw(Dumper);
|
use Data::Dumper qw(Dumper);
|
||||||
use IPC::Run 'run';
|
use IPC::Run 'run';
|
||||||
|
use JSON::PP;
|
||||||
use File::Temp;
|
use File::Temp;
|
||||||
use Try::Tiny;
|
use Try::Tiny;
|
||||||
|
use POSIX ();
|
||||||
use sigtrap 'handler' => \&sig_handler, qw(INT TERM KILL QUIT);
|
use sigtrap 'handler' => \&sig_handler, qw(INT TERM KILL QUIT);
|
||||||
|
|
||||||
sub trim { my $s = shift; $s =~ s/^\s+|\s+$//g; return $s };
|
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";
|
$run_dir="/tmp";
|
||||||
$dsn = 'DBI:mysql:database=' . $ENV{'DBNAME'} . ';mysql_socket=/var/run/mysqld/mysqld.sock';
|
$dsn = 'DBI:mysql:database=' . $ENV{'DBNAME'} . ';mysql_socket=/var/run/mysqld/mysqld.sock';
|
||||||
@@ -46,6 +24,16 @@ $dbh = DBI->connect($dsn, $ENV{'DBUSER'}, $ENV{'DBPASS'}, {
|
|||||||
});
|
});
|
||||||
$dbh->do("UPDATE imapsync SET is_running = 0");
|
$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 {
|
sub sig_handler {
|
||||||
# Send die to force exception in "run"
|
# Send die to force exception in "run"
|
||||||
die "sig_handler received signal, preparing to exit...\n";
|
die "sig_handler received signal, preparing to exit...\n";
|
||||||
@@ -55,47 +43,73 @@ open my $file, '<', "/etc/sogo/sieve.creds";
|
|||||||
my $creds = <$file>;
|
my $creds = <$file>;
|
||||||
close $file;
|
close $file;
|
||||||
my ($master_user, $master_pass) = split /:/, $creds;
|
my ($master_user, $master_pass) = split /:/, $creds;
|
||||||
my $sth = $dbh->prepare("SELECT id,
|
# Source-based schema: join imapsync_source to read host/port/encryption/auth-type
|
||||||
user1,
|
my $sth = $dbh->prepare("SELECT
|
||||||
user2,
|
i.id,
|
||||||
host1,
|
i.user1,
|
||||||
authmech1,
|
i.user2,
|
||||||
password1,
|
s.host1,
|
||||||
exclude,
|
s.auth_type,
|
||||||
port1,
|
i.password1,
|
||||||
enc1,
|
i.exclude,
|
||||||
delete2duplicates,
|
s.port1,
|
||||||
maxage,
|
s.enc1,
|
||||||
subfolder2,
|
i.delete2duplicates,
|
||||||
delete1,
|
i.maxage,
|
||||||
delete2,
|
i.subfolder2,
|
||||||
automap,
|
i.delete1,
|
||||||
skipcrossduplicates,
|
i.delete2,
|
||||||
maxbytespersecond,
|
i.automap,
|
||||||
custom_params,
|
i.skipcrossduplicates,
|
||||||
subscribeall,
|
i.maxbytespersecond,
|
||||||
timeout1,
|
i.custom_params,
|
||||||
timeout2,
|
i.subscribeall,
|
||||||
dry
|
i.timeout1,
|
||||||
FROM imapsync
|
i.timeout2,
|
||||||
WHERE active = 1
|
i.dry,
|
||||||
AND is_running = 0
|
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 (
|
AND (
|
||||||
UNIX_TIMESTAMP(NOW()) - UNIX_TIMESTAMP(last_run) > mins_interval * 60
|
UNIX_TIMESTAMP(NOW()) - UNIX_TIMESTAMP(i.last_run) > i.mins_interval * 60
|
||||||
OR
|
OR
|
||||||
last_run IS NULL)
|
i.last_run IS NULL)
|
||||||
ORDER BY last_run");
|
ORDER BY i.last_run ASC, i.prio DESC, i.id ASC
|
||||||
|
LIMIT $max_parallel");
|
||||||
|
|
||||||
$sth->execute();
|
$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];
|
$id = @$row[0];
|
||||||
$user1 = @$row[1];
|
$user1 = @$row[1];
|
||||||
$user2 = @$row[2];
|
$user2 = @$row[2];
|
||||||
$host1 = @$row[3];
|
$host1 = @$row[3];
|
||||||
$authmech1 = @$row[4];
|
$auth_type = @$row[4];
|
||||||
$password1 = @$row[5];
|
$password1 = @$row[5];
|
||||||
$exclude = @$row[6];
|
$exclude = @$row[6];
|
||||||
$port1 = @$row[7];
|
$port1 = @$row[7];
|
||||||
@@ -113,21 +127,78 @@ while ($row = $sth->fetchrow_arrayref()) {
|
|||||||
$timeout1 = @$row[19];
|
$timeout1 = @$row[19];
|
||||||
$timeout2 = @$row[20];
|
$timeout2 = @$row[20];
|
||||||
$dry = @$row[21];
|
$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; }
|
if ($enc1 eq "TLS") { $enc1 = "--tls1"; } elsif ($enc1 eq "SSL") { $enc1 = "--ssl1"; } else { undef $enc1; }
|
||||||
|
|
||||||
my $template = $run_dir . '/imapsync.XXXXXXX';
|
my $template = $run_dir . '/imapsync.XXXXXXX';
|
||||||
my $passfile1 = File::Temp->new(TEMPLATE => $template);
|
my $passfile1 = File::Temp->new(TEMPLATE => $template);
|
||||||
my $passfile2 = File::Temp->new(TEMPLATE => $template);
|
my $passfile2 = File::Temp->new(TEMPLATE => $template);
|
||||||
|
my $tokenfile1;
|
||||||
|
|
||||||
binmode( $passfile1, ":utf8" );
|
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";
|
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;
|
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",
|
my $generated_cmds = [ "/usr/local/bin/imapsync",
|
||||||
"--tmpdir", "/tmp",
|
"--tmpdir", "/tmp",
|
||||||
"--nofoldersizes",
|
"--nofoldersizes",
|
||||||
@@ -137,7 +208,7 @@ while ($row = $sth->fetchrow_arrayref()) {
|
|||||||
($exclude eq "" ? () : ("--exclude", $exclude)),
|
($exclude eq "" ? () : ("--exclude", $exclude)),
|
||||||
($subfolder2 eq "" ? () : ('--subfolder2', $subfolder2)),
|
($subfolder2 eq "" ? () : ('--subfolder2', $subfolder2)),
|
||||||
($maxage eq "0" ? () : ('--maxage', $maxage)),
|
($maxage eq "0" ? () : ('--maxage', $maxage)),
|
||||||
($maxbytespersecond eq "0" ? () : ('--maxbytespersecond', $maxbytespersecond)),
|
($eff_bw <= 0 ? () : ('--maxbytespersecond', $eff_bw)),
|
||||||
($delete2duplicates ne "1" ? () : ('--delete2duplicates')),
|
($delete2duplicates ne "1" ? () : ('--delete2duplicates')),
|
||||||
($subscribeall ne "1" ? () : ('--subscribeall')),
|
($subscribeall ne "1" ? () : ('--subscribeall')),
|
||||||
($delete1 ne "1" ? () : ('--delete')),
|
($delete1 ne "1" ? () : ('--delete')),
|
||||||
@@ -147,7 +218,7 @@ while ($row = $sth->fetchrow_arrayref()) {
|
|||||||
(!defined($enc1) ? () : ($enc1)),
|
(!defined($enc1) ? () : ($enc1)),
|
||||||
"--host1", $host1,
|
"--host1", $host1,
|
||||||
"--user1", $user1,
|
"--user1", $user1,
|
||||||
"--passfile1", $passfile1->filename,
|
@auth_args,
|
||||||
"--port1", $port1,
|
"--port1", $port1,
|
||||||
"--host2", "localhost",
|
"--host2", "localhost",
|
||||||
"--user2", $user2 . '*' . trim($master_user),
|
"--user2", $user2 . '*' . trim($master_user),
|
||||||
@@ -187,10 +258,19 @@ while ($row = $sth->fetchrow_arrayref()) {
|
|||||||
$update->execute();
|
$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();
|
$dbh->disconnect();
|
||||||
|
|
||||||
$lockmgr->unlock($lock_file);
|
$lockmgr->unlock($lock_file);
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ try:
|
|||||||
if max_score == "":
|
if max_score == "":
|
||||||
max_score = 9999.0
|
max_score = 9999.0
|
||||||
|
|
||||||
def query_mysql(query, headers = True, update = False):
|
def query_mysql(query, params = None, headers = True, update = False):
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
cnx = MySQLdb.connect(user=os.environ.get('DBUSER'), password=os.environ.get('DBPASS'), database=os.environ.get('DBNAME'), charset="utf8mb4", collation="utf8mb4_general_ci")
|
cnx = MySQLdb.connect(user=os.environ.get('DBUSER'), password=os.environ.get('DBPASS'), database=os.environ.get('DBNAME'), charset="utf8mb4", collation="utf8mb4_general_ci")
|
||||||
@@ -57,7 +57,10 @@ try:
|
|||||||
else:
|
else:
|
||||||
break
|
break
|
||||||
cur = cnx.cursor()
|
cur = cnx.cursor()
|
||||||
cur.execute(query)
|
if params:
|
||||||
|
cur.execute(query, params)
|
||||||
|
else:
|
||||||
|
cur.execute(query)
|
||||||
if not update:
|
if not update:
|
||||||
result = []
|
result = []
|
||||||
columns = tuple( [d[0] for d in cur.description] )
|
columns = tuple( [d[0] for d in cur.description] )
|
||||||
@@ -76,7 +79,7 @@ try:
|
|||||||
|
|
||||||
def notify_rcpt(rcpt, msg_count, quarantine_acl, category):
|
def notify_rcpt(rcpt, msg_count, quarantine_acl, category):
|
||||||
if category == "add_header": category = "add header"
|
if category == "add_header": category = "add header"
|
||||||
meta_query = query_mysql('SELECT `qhash`, id, subject, score, sender, created, action FROM quarantine WHERE notified = 0 AND rcpt = "%s" AND score < %f AND (action = "%s" OR "all" = "%s")' % (rcpt, max_score, category, category))
|
meta_query = query_mysql('SELECT `qhash`, id, subject, score, sender, created, action FROM quarantine WHERE notified = 0 AND rcpt = %s AND score < %s AND (action = %s OR "all" = %s)', (rcpt, max_score, category, category))
|
||||||
print("%s: %d of %d messages qualify for notification" % (rcpt, len(meta_query), msg_count))
|
print("%s: %d of %d messages qualify for notification" % (rcpt, len(meta_query), msg_count))
|
||||||
if len(meta_query) == 0:
|
if len(meta_query) == 0:
|
||||||
return
|
return
|
||||||
@@ -130,7 +133,7 @@ try:
|
|||||||
server.sendmail(msg['From'], [str(redirect)] + [str(bcc)], text)
|
server.sendmail(msg['From'], [str(redirect)] + [str(bcc)], text)
|
||||||
server.quit()
|
server.quit()
|
||||||
for res in meta_query:
|
for res in meta_query:
|
||||||
query_mysql('UPDATE quarantine SET notified = 1 WHERE id = "%d"' % (res['id']), update = True)
|
query_mysql('UPDATE quarantine SET notified = 1 WHERE id = %s', (res['id'],), update = True)
|
||||||
r.hset('Q_LAST_NOTIFIED', record['rcpt'], time_now)
|
r.hset('Q_LAST_NOTIFIED', record['rcpt'], time_now)
|
||||||
break
|
break
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
@@ -138,7 +141,7 @@ try:
|
|||||||
print('%s' % (ex))
|
print('%s' % (ex))
|
||||||
time.sleep(3)
|
time.sleep(3)
|
||||||
|
|
||||||
records = query_mysql('SELECT IFNULL(user_acl.quarantine, 0) AS quarantine_acl, count(id) AS counter, rcpt FROM quarantine LEFT OUTER JOIN user_acl ON user_acl.username = rcpt WHERE notified = 0 AND score < %f AND rcpt in (SELECT username FROM mailbox) GROUP BY rcpt' % (max_score))
|
records = query_mysql('SELECT IFNULL(user_acl.quarantine, 0) AS quarantine_acl, count(id) AS counter, rcpt FROM quarantine LEFT OUTER JOIN user_acl ON user_acl.username = rcpt WHERE notified = 0 AND score < %s AND rcpt in (SELECT username FROM mailbox) GROUP BY rcpt', (max_score,))
|
||||||
|
|
||||||
for record in records:
|
for record in records:
|
||||||
attrs = ''
|
attrs = ''
|
||||||
@@ -156,7 +159,7 @@ try:
|
|||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print('Could not determine last notification for %s, assuming never' % (record['rcpt']))
|
print('Could not determine last notification for %s, assuming never' % (record['rcpt']))
|
||||||
last_notification = 0
|
last_notification = 0
|
||||||
attrs_json = query_mysql('SELECT attributes FROM mailbox WHERE username = "%s"' % (record['rcpt']))
|
attrs_json = query_mysql('SELECT attributes FROM mailbox WHERE username = %s', (record['rcpt'],))
|
||||||
attrs = attrs_json[0]['attributes']
|
attrs = attrs_json[0]['attributes']
|
||||||
if isinstance(attrs, str):
|
if isinstance(attrs, str):
|
||||||
# if attr is str then just load it
|
# if attr is str then just load it
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM alpine:3.21
|
FROM alpine:3.23
|
||||||
|
|
||||||
LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
|
LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
|
||||||
backend=iptables
|
backend=nftables
|
||||||
|
|
||||||
nft list table ip filter &>/dev/null
|
nft list table ip filter &>/dev/null
|
||||||
nftables_found=$?
|
nftables_found=$?
|
||||||
|
|||||||
@@ -449,6 +449,11 @@ if __name__ == '__main__':
|
|||||||
tables = NFTables(chain_name, logger)
|
tables = NFTables(chain_name, logger)
|
||||||
else:
|
else:
|
||||||
logger.logInfo('Using IPTables backend')
|
logger.logInfo('Using IPTables backend')
|
||||||
|
logger.logWarn(
|
||||||
|
"DEPRECATION: iptables-legacy is deprecated and will be removed in future releases. "
|
||||||
|
"Please switch to nftables on your host to ensure complete compatibility."
|
||||||
|
)
|
||||||
|
time.sleep(5)
|
||||||
tables = IPTables(chain_name, logger)
|
tables = IPTables(chain_name, logger)
|
||||||
|
|
||||||
clear()
|
clear()
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import time
|
import time
|
||||||
import json
|
import json
|
||||||
|
import datetime
|
||||||
|
|
||||||
class Logger:
|
class Logger:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
@@ -8,17 +9,28 @@ class Logger:
|
|||||||
def set_redis(self, redis):
|
def set_redis(self, redis):
|
||||||
self.r = redis
|
self.r = redis
|
||||||
|
|
||||||
|
def _format_timestamp(self):
|
||||||
|
# Local time with milliseconds
|
||||||
|
return datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
|
||||||
def log(self, priority, message):
|
def log(self, priority, message):
|
||||||
tolog = {}
|
# build redis-friendly dict
|
||||||
tolog['time'] = int(round(time.time()))
|
tolog = {
|
||||||
tolog['priority'] = priority
|
'time': int(round(time.time())), # keep raw timestamp for Redis
|
||||||
tolog['message'] = message
|
'priority': priority,
|
||||||
print(message)
|
'message': message
|
||||||
|
}
|
||||||
|
|
||||||
|
# print human-readable message with timestamp
|
||||||
|
ts = self._format_timestamp()
|
||||||
|
print(f"{ts} {priority.upper()}: {message}", flush=True)
|
||||||
|
|
||||||
|
# also push JSON to Redis if connected
|
||||||
if self.r is not None:
|
if self.r is not None:
|
||||||
try:
|
try:
|
||||||
self.r.lpush('NETFILTER_LOG', json.dumps(tolog, ensure_ascii=False))
|
self.r.lpush('NETFILTER_LOG', json.dumps(tolog, ensure_ascii=False))
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print('Failed logging to redis: %s' % (ex))
|
print(f'{ts} WARN: Failed logging to redis: {ex}', flush=True)
|
||||||
|
|
||||||
def logWarn(self, message):
|
def logWarn(self, message):
|
||||||
self.log('warn', message)
|
self.log('warn', message)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM nginx:alpine
|
FROM nginx:1.30.4-alpine
|
||||||
LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>"
|
LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>"
|
||||||
|
|
||||||
ENV PIP_BREAK_SYSTEM_PACKAGES=1
|
ENV PIP_BREAK_SYSTEM_PACKAGES=1
|
||||||
|
|||||||
@@ -3,17 +3,17 @@ FROM php:8.2-fpm-alpine3.21
|
|||||||
LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
|
LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
|
||||||
|
|
||||||
# renovate: datasource=github-tags depName=krakjoe/apcu versioning=semver-coerced extractVersion=^v(?<version>.*)$
|
# renovate: datasource=github-tags depName=krakjoe/apcu versioning=semver-coerced extractVersion=^v(?<version>.*)$
|
||||||
ARG APCU_PECL_VERSION=5.1.26
|
ARG APCU_PECL_VERSION=5.1.28
|
||||||
# renovate: datasource=github-tags depName=Imagick/imagick versioning=semver-coerced extractVersion=(?<version>.*)$
|
# renovate: datasource=github-tags depName=Imagick/imagick versioning=semver-coerced extractVersion=(?<version>.*)$
|
||||||
ARG IMAGICK_PECL_VERSION=3.8.0
|
ARG IMAGICK_PECL_VERSION=3.8.1
|
||||||
# renovate: datasource=github-tags depName=php/pecl-mail-mailparse versioning=semver-coerced extractVersion=^v(?<version>.*)$
|
# renovate: datasource=github-tags depName=php/pecl-mail-mailparse versioning=semver-coerced extractVersion=^v(?<version>.*)$
|
||||||
ARG MAILPARSE_PECL_VERSION=3.1.8
|
ARG MAILPARSE_PECL_VERSION=3.2.0
|
||||||
# renovate: datasource=github-tags depName=php-memcached-dev/php-memcached versioning=semver-coerced extractVersion=^v(?<version>.*)$
|
# renovate: datasource=github-tags depName=php-memcached-dev/php-memcached versioning=semver-coerced extractVersion=^v(?<version>.*)$
|
||||||
ARG MEMCACHED_PECL_VERSION=3.3.0
|
ARG MEMCACHED_PECL_VERSION=3.4.0
|
||||||
# renovate: datasource=github-tags depName=phpredis/phpredis versioning=semver-coerced extractVersion=(?<version>.*)$
|
# renovate: datasource=github-tags depName=phpredis/phpredis versioning=semver-coerced extractVersion=(?<version>.*)$
|
||||||
ARG REDIS_PECL_VERSION=6.2.0
|
ARG REDIS_PECL_VERSION=6.3.0
|
||||||
# renovate: datasource=github-tags depName=composer/composer versioning=semver-coerced extractVersion=(?<version>.*)$
|
# renovate: datasource=github-tags depName=composer/composer versioning=semver-coerced extractVersion=(?<version>.*)$
|
||||||
ARG COMPOSER_VERSION=2.8.6
|
ARG COMPOSER_VERSION=2.10.2
|
||||||
|
|
||||||
RUN apk add -U --no-cache autoconf \
|
RUN apk add -U --no-cache autoconf \
|
||||||
aspell-dev \
|
aspell-dev \
|
||||||
|
|||||||
@@ -167,7 +167,7 @@ DELIMITER //
|
|||||||
CREATE EVENT clean_spamalias
|
CREATE EVENT clean_spamalias
|
||||||
ON SCHEDULE EVERY 1 DAY DO
|
ON SCHEDULE EVERY 1 DAY DO
|
||||||
BEGIN
|
BEGIN
|
||||||
DELETE FROM spamalias WHERE validity < UNIX_TIMESTAMP();
|
DELETE FROM spamalias WHERE validity < UNIX_TIMESTAMP() AND permanent = 0;
|
||||||
END;
|
END;
|
||||||
//
|
//
|
||||||
DELIMITER ;
|
DELIMITER ;
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
FROM golang:1.25-bookworm AS builder
|
FROM golang:1.26-trixie AS builder
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
|
|
||||||
ENV CGO_ENABLED=0 \
|
ENV CGO_ENABLED=0 \
|
||||||
GO111MODULE=on \
|
GO111MODULE=on \
|
||||||
NOOPT=1 \
|
NOOPT=1 \
|
||||||
VERSION=1.8.14
|
VERSION=1.11.0
|
||||||
|
|
||||||
RUN git clone --branch v${VERSION} https://github.com/Zuplu/postfix-tlspol && \
|
RUN git clone --branch v${VERSION} https://github.com/Zuplu/postfix-tlspol && \
|
||||||
cd /src/postfix-tlspol && \
|
cd /src/postfix-tlspol && \
|
||||||
scripts/build.sh build-only
|
scripts/build.sh build-only
|
||||||
|
|
||||||
|
|
||||||
FROM debian:bookworm-slim
|
FROM debian:trixie-slim
|
||||||
LABEL maintainer="The Infrastructure Company GmbH <info@servercow.de>"
|
LABEL maintainer="The Infrastructure Company GmbH <info@servercow.de>"
|
||||||
|
|
||||||
ARG DEBIAN_FRONTEND=noninteractive
|
ARG DEBIAN_FRONTEND=noninteractive
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@version: 3.38
|
@version: 4.8
|
||||||
@include "scl.conf"
|
@include "scl.conf"
|
||||||
options {
|
options {
|
||||||
chain_hostnames(off);
|
chain_hostnames(off);
|
||||||
@@ -7,7 +7,7 @@ options {
|
|||||||
dns_cache(no);
|
dns_cache(no);
|
||||||
use_fqdn(no);
|
use_fqdn(no);
|
||||||
owner("root"); group("adm"); perm(0640);
|
owner("root"); group("adm"); perm(0640);
|
||||||
stats_freq(0);
|
stats(freq(0));
|
||||||
bad_hostname("^gconfd$");
|
bad_hostname("^gconfd$");
|
||||||
};
|
};
|
||||||
source s_src {
|
source s_src {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@version: 3.38
|
@version: 4.8
|
||||||
@include "scl.conf"
|
@include "scl.conf"
|
||||||
options {
|
options {
|
||||||
chain_hostnames(off);
|
chain_hostnames(off);
|
||||||
@@ -7,7 +7,7 @@ options {
|
|||||||
dns_cache(no);
|
dns_cache(no);
|
||||||
use_fqdn(no);
|
use_fqdn(no);
|
||||||
owner("root"); group("adm"); perm(0640);
|
owner("root"); group("adm"); perm(0640);
|
||||||
stats_freq(0);
|
stats(freq(0));
|
||||||
bad_hostname("^gconfd$");
|
bad_hostname("^gconfd$");
|
||||||
};
|
};
|
||||||
source s_src {
|
source s_src {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM debian:bookworm-slim
|
FROM debian:trixie-slim
|
||||||
|
|
||||||
LABEL maintainer="The Infrastructure Company GmbH <info@servercow.de>"
|
LABEL maintainer="The Infrastructure Company GmbH <info@servercow.de>"
|
||||||
|
|
||||||
|
|||||||
@@ -329,14 +329,17 @@ query = SELECT goto FROM alias
|
|||||||
SELECT id FROM alias
|
SELECT id FROM alias
|
||||||
WHERE address='%s'
|
WHERE address='%s'
|
||||||
AND (active='1' OR active='2')
|
AND (active='1' OR active='2')
|
||||||
|
AND sender_allowed='1'
|
||||||
), (
|
), (
|
||||||
SELECT id FROM alias
|
SELECT id FROM alias
|
||||||
WHERE address='@%d'
|
WHERE address='@%d'
|
||||||
AND (active='1' OR active='2')
|
AND (active='1' OR active='2')
|
||||||
|
AND sender_allowed='1'
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
AND active='1'
|
AND active='1'
|
||||||
|
AND sender_allowed='1'
|
||||||
AND (domain IN
|
AND (domain IN
|
||||||
(SELECT domain FROM domain
|
(SELECT domain FROM domain
|
||||||
WHERE domain='%d'
|
WHERE domain='%d'
|
||||||
@@ -390,7 +393,7 @@ hosts = unix:/var/run/mysqld/mysqld.sock
|
|||||||
dbname = ${DBNAME}
|
dbname = ${DBNAME}
|
||||||
query = SELECT goto FROM spamalias
|
query = SELECT goto FROM spamalias
|
||||||
WHERE address='%s'
|
WHERE address='%s'
|
||||||
AND validity >= UNIX_TIMESTAMP()
|
AND (validity >= UNIX_TIMESTAMP() OR permanent != 0)
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
if [ ! -f /opt/postfix/conf/dns_blocklists.cf ]; then
|
if [ ! -f /opt/postfix/conf/dns_blocklists.cf ]; then
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@version: 3.38
|
@version: 4.8
|
||||||
@include "scl.conf"
|
@include "scl.conf"
|
||||||
options {
|
options {
|
||||||
chain_hostnames(off);
|
chain_hostnames(off);
|
||||||
@@ -7,7 +7,7 @@ options {
|
|||||||
dns_cache(no);
|
dns_cache(no);
|
||||||
use_fqdn(no);
|
use_fqdn(no);
|
||||||
owner("root"); group("adm"); perm(0640);
|
owner("root"); group("adm"); perm(0640);
|
||||||
stats_freq(0);
|
stats(freq(0));
|
||||||
bad_hostname("^gconfd$");
|
bad_hostname("^gconfd$");
|
||||||
};
|
};
|
||||||
source s_src {
|
source s_src {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@version: 3.38
|
@version: 4.8
|
||||||
@include "scl.conf"
|
@include "scl.conf"
|
||||||
options {
|
options {
|
||||||
chain_hostnames(off);
|
chain_hostnames(off);
|
||||||
@@ -7,7 +7,7 @@ options {
|
|||||||
dns_cache(no);
|
dns_cache(no);
|
||||||
use_fqdn(no);
|
use_fqdn(no);
|
||||||
owner("root"); group("adm"); perm(0640);
|
owner("root"); group("adm"); perm(0640);
|
||||||
stats_freq(0);
|
stats(freq(0));
|
||||||
bad_hostname("^gconfd$");
|
bad_hostname("^gconfd$");
|
||||||
};
|
};
|
||||||
source s_src {
|
source s_src {
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
FROM debian:bookworm-slim
|
FROM debian:trixie-slim
|
||||||
LABEL maintainer="The Infrastructure Company GmbH <info@servercow.de>"
|
LABEL maintainer="The Infrastructure Company GmbH <info@servercow.de>"
|
||||||
|
|
||||||
ARG DEBIAN_FRONTEND=noninteractive
|
ARG DEBIAN_FRONTEND=noninteractive
|
||||||
ARG RSPAMD_VER=rspamd_3.12.1-1~6dbfca2fa
|
ARG RSPAMD_VER=rspamd_4.1.4-1~beb659b
|
||||||
ARG CODENAME=bookworm
|
ARG CODENAME=trixie
|
||||||
ENV LC_ALL=C
|
ENV LC_ALL=C
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
|||||||
@@ -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>"
|
LABEL maintainer="The Infrastructure Company GmbH <info@servercow.de>"
|
||||||
|
|
||||||
ARG DEBIAN_FRONTEND=noninteractive
|
ARG DEBIAN_FRONTEND=noninteractive
|
||||||
ARG DEBIAN_VERSION=bookworm
|
ARG SOGO_VERSION=SOGo-5.12.10
|
||||||
ARG SOGO_DEBIAN_REPOSITORY=https://packagingv2.sogo.nu/sogo-nightly-debian/
|
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>.*)$
|
# renovate: datasource=github-releases depName=tianon/gosu versioning=semver-coerced extractVersion=^(?<version>.*)$
|
||||||
ARG GOSU_VERSION=1.17
|
ARG GOSU_VERSION=1.19
|
||||||
ENV LC_ALL=C
|
ENV LC_ALL=C
|
||||||
|
|
||||||
# Prerequisites
|
# Install dependencies, build SOPE and SOGo, then clean up (all in one layer to minimize image size)
|
||||||
RUN echo "Building from repository $SOGO_DEBIAN_REPOSITORY" \
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
&& apt-get update && apt-get install -y --no-install-recommends \
|
# Build dependencies
|
||||||
apt-transport-https \
|
git \
|
||||||
ca-certificates \
|
build-essential \
|
||||||
gettext \
|
gobjc \
|
||||||
gnupg \
|
pkg-config \
|
||||||
mariadb-client \
|
gnustep-make \
|
||||||
rsync \
|
gnustep-base-runtime \
|
||||||
supervisor \
|
libgnustep-base-dev \
|
||||||
syslog-ng \
|
libxml2-dev \
|
||||||
syslog-ng-core \
|
libldap2-dev \
|
||||||
syslog-ng-mod-redis \
|
libssl-dev \
|
||||||
dirmngr \
|
zlib1g-dev \
|
||||||
netcat-traditional \
|
libpq-dev \
|
||||||
psmisc \
|
libmariadb-dev-compat \
|
||||||
wget \
|
libmemcached-dev \
|
||||||
patch \
|
libsodium-dev \
|
||||||
|
libcurl4-openssl-dev \
|
||||||
|
libzip-dev \
|
||||||
|
libytnef0-dev \
|
||||||
|
libwbxml2-dev \
|
||||||
|
curl \
|
||||||
|
ca-certificates \
|
||||||
|
# Runtime dependencies
|
||||||
|
apt-transport-https \
|
||||||
|
gettext \
|
||||||
|
gnupg \
|
||||||
|
mariadb-client \
|
||||||
|
rsync \
|
||||||
|
supervisor \
|
||||||
|
syslog-ng \
|
||||||
|
syslog-ng-core \
|
||||||
|
syslog-ng-mod-redis \
|
||||||
|
dirmngr \
|
||||||
|
netcat-traditional \
|
||||||
|
psmisc \
|
||||||
|
wget \
|
||||||
|
patch \
|
||||||
|
libobjc4 \
|
||||||
|
libxml2 \
|
||||||
|
libldap-2.5-0 \
|
||||||
|
libssl3 \
|
||||||
|
zlib1g \
|
||||||
|
libmariadb3 \
|
||||||
|
libmemcached11 \
|
||||||
|
libsodium23 \
|
||||||
|
libcurl4 \
|
||||||
|
libzip4 \
|
||||||
|
libytnef0 \
|
||||||
|
libwbxml2-1 \
|
||||||
|
# Download gosu
|
||||||
&& dpkgArch="$(dpkg --print-architecture | awk -F- '{ print $NF }')" \
|
&& dpkgArch="$(dpkg --print-architecture | awk -F- '{ print $NF }')" \
|
||||||
&& wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$dpkgArch" \
|
&& wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$dpkgArch" \
|
||||||
&& chmod +x /usr/local/bin/gosu \
|
&& chmod +x /usr/local/bin/gosu \
|
||||||
&& gosu nobody true \
|
&& gosu nobody true \
|
||||||
&& mkdir /usr/share/doc/sogo \
|
# Build SOPE
|
||||||
&& touch /usr/share/doc/sogo/empty.sh \
|
&& git clone --depth 1 --branch ${SOPE_VERSION} https://github.com/Alinto/sope.git /tmp/sope \
|
||||||
&& wget -O- https://keys.openpgp.org/vks/v1/by-fingerprint/74FFC6D72B925A34B5D356BDF8A27B36A6E2EAE9 | gpg --dearmor | apt-key add - \
|
&& cd /tmp/sope \
|
||||||
&& echo "deb [trusted=yes] ${SOGO_DEBIAN_REPOSITORY} ${DEBIAN_VERSION} main" > /etc/apt/sources.list.d/sogo.list \
|
&& rm -rf .git \
|
||||||
&& apt-get update && apt-get install -y --no-install-recommends \
|
&& . /usr/share/GNUstep/Makefiles/GNUstep.sh \
|
||||||
sogo \
|
&& ./configure --prefix=/usr --disable-debug --disable-strip \
|
||||||
sogo-activesync \
|
&& make -j$(nproc) \
|
||||||
&& apt-get autoclean \
|
&& make install \
|
||||||
|
&& cd / \
|
||||||
|
&& rm -rf /tmp/sope \
|
||||||
|
# Build SOGo with security patches
|
||||||
|
&& git clone --depth 1 --branch ${SOGO_VERSION} https://github.com/Alinto/sogo.git /tmp/sogo \
|
||||||
|
&& cd /tmp/sogo \
|
||||||
|
&& git config user.email "builder@mailcow.local" \
|
||||||
|
&& git config user.name "SOGo Builder" \
|
||||||
|
&& for patch in ${SOGO_SECURITY_PATCHES}; do \
|
||||||
|
echo "Applying security patch: ${patch}"; \
|
||||||
|
git fetch origin ${patch} && git cherry-pick ${patch}; \
|
||||||
|
done \
|
||||||
|
&& rm -rf .git \
|
||||||
|
&& . /usr/share/GNUstep/Makefiles/GNUstep.sh \
|
||||||
|
&& ./configure --disable-debug --disable-strip \
|
||||||
|
&& make -j$(nproc) \
|
||||||
|
&& make install \
|
||||||
|
&& cd /tmp/sogo/ActiveSync \
|
||||||
|
&& . /usr/share/GNUstep/Makefiles/GNUstep.sh \
|
||||||
|
&& make -j$(nproc) install \
|
||||||
|
&& cd / \
|
||||||
|
&& rm -rf /tmp/sogo \
|
||||||
|
# Strip binaries
|
||||||
|
&& strip --strip-unneeded /usr/local/sbin/sogod 2>/dev/null || true \
|
||||||
|
&& strip --strip-unneeded /usr/local/sbin/sogo-tool 2>/dev/null || true \
|
||||||
|
&& strip --strip-unneeded /usr/local/sbin/sogo-ealarms-notify 2>/dev/null || true \
|
||||||
|
&& strip --strip-unneeded /usr/local/sbin/sogo-slapd-sockd 2>/dev/null || true \
|
||||||
|
# Remove build dependencies and clean up
|
||||||
|
&& apt-get purge -y --auto-remove \
|
||||||
|
git \
|
||||||
|
build-essential \
|
||||||
|
gobjc \
|
||||||
|
gnustep-make \
|
||||||
|
libgnustep-base-dev \
|
||||||
|
libxml2-dev \
|
||||||
|
libldap2-dev \
|
||||||
|
libssl-dev \
|
||||||
|
zlib1g-dev \
|
||||||
|
libpq-dev \
|
||||||
|
libmariadb-dev-compat \
|
||||||
|
libmemcached-dev \
|
||||||
|
libsodium-dev \
|
||||||
|
libcurl4-openssl-dev \
|
||||||
|
libzip-dev \
|
||||||
|
libytnef0-dev \
|
||||||
|
curl \
|
||||||
|
&& apt-get autoremove -y \
|
||||||
|
&& apt-get clean \
|
||||||
&& rm -rf /var/lib/apt/lists/* \
|
&& rm -rf /var/lib/apt/lists/* \
|
||||||
|
&& rm -rf /usr/share/doc/* \
|
||||||
|
&& rm -rf /usr/share/man/* \
|
||||||
|
&& rm -rf /var/cache/debconf/* \
|
||||||
|
&& rm -rf /tmp/* \
|
||||||
|
&& rm -rf /root/.cache \
|
||||||
|
&& find /usr/local/lib -name '*.a' -delete \
|
||||||
|
&& find /usr/lib -name '*.a' -delete \
|
||||||
|
&& mkdir -p /usr/share/doc/sogo \
|
||||||
|
&& touch /usr/share/doc/sogo/empty.sh \
|
||||||
&& touch /etc/default/locale
|
&& touch /etc/default/locale
|
||||||
|
|
||||||
|
# Configure library paths
|
||||||
|
RUN echo "/usr/lib64" > /etc/ld.so.conf.d/sogo.conf \
|
||||||
|
&& echo "/usr/local/lib/sogo" >> /etc/ld.so.conf.d/sogo.conf \
|
||||||
|
&& echo "/usr/local/lib/GNUstep/Frameworks/SOGo.framework/Versions/5/sogo" >> /etc/ld.so.conf.d/sogo.conf \
|
||||||
|
&& ldconfig
|
||||||
|
|
||||||
|
# Create sogo user and group
|
||||||
|
RUN groupadd -r -g 999 sogo \
|
||||||
|
&& useradd -r -u 999 -g sogo -d /var/lib/sogo -s /bin/bash -c "SOGo Daemon" sogo \
|
||||||
|
&& mkdir -p /var/lib/sogo /var/run/sogo /var/log/sogo /var/spool/sogo \
|
||||||
|
&& chown -R sogo:sogo /var/lib/sogo /var/run/sogo /var/log/sogo /var/spool/sogo
|
||||||
|
|
||||||
|
# Create symlinks for SOGo binaries
|
||||||
|
RUN ln -s /usr/local/sbin/sogod /usr/sbin/sogod \
|
||||||
|
&& ln -s /usr/local/sbin/sogo-tool /usr/sbin/sogo-tool \
|
||||||
|
&& ln -s /usr/local/sbin/sogo-ealarms-notify /usr/sbin/sogo-ealarms-notify \
|
||||||
|
&& ln -s /usr/local/sbin/sogo-slapd-sockd /usr/sbin/sogo-slapd-sockd
|
||||||
|
|
||||||
|
# Copy configuration files and scripts
|
||||||
COPY ./bootstrap-sogo.sh /bootstrap-sogo.sh
|
COPY ./bootstrap-sogo.sh /bootstrap-sogo.sh
|
||||||
COPY syslog-ng.conf /etc/syslog-ng/syslog-ng.conf
|
COPY syslog-ng.conf /etc/syslog-ng/syslog-ng.conf
|
||||||
COPY syslog-ng-redis_slave.conf /etc/syslog-ng/syslog-ng-redis_slave.conf
|
COPY syslog-ng-redis_slave.conf /etc/syslog-ng/syslog-ng-redis_slave.conf
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
--- /usr/lib/GNUstep/SOGo/Templates/UIxAclEditor.wox 2018-08-17 18:29:57.987504204 +0200
|
--- /usr/local/lib/GNUstep/SOGo/Templates/UIxAclEditor.wox 2018-08-17 18:29:57.987504204 +0200
|
||||||
+++ /usr/lib/GNUstep/SOGo/Templates/UIxAclEditor.wox 2018-08-17 18:29:35.918291298 +0200
|
+++ /usr/local/lib/GNUstep/SOGo/Templates/UIxAclEditor.wox 2018-08-17 18:29:35.918291298 +0200
|
||||||
@@ -46,7 +46,7 @@
|
@@ -46,7 +46,7 @@
|
||||||
</md-item-template>
|
</md-item-template>
|
||||||
</md-autocomplete>
|
</md-autocomplete>
|
||||||
|
|||||||
@@ -50,10 +50,6 @@ cat <<EOF > /var/lib/sogo/GNUstep/Defaults/sogod.plist
|
|||||||
<string>YES</string>
|
<string>YES</string>
|
||||||
<key>SOGoEncryptionKey</key>
|
<key>SOGoEncryptionKey</key>
|
||||||
<string>${RAND_PASS}</string>
|
<string>${RAND_PASS}</string>
|
||||||
<key>SOGoURLEncryptionEnabled</key>
|
|
||||||
<string>YES</string>
|
|
||||||
<key>SOGoURLEncryptionPassphrase</key>
|
|
||||||
<string>${SOGO_URL_ENCRYPTION_KEY}</string>
|
|
||||||
<key>OCSAdminURL</key>
|
<key>OCSAdminURL</key>
|
||||||
<string>mysql://${DBUSER}:${DBPASS}@%2Fvar%2Frun%2Fmysqld%2Fmysqld.sock/${DBNAME}/sogo_admin</string>
|
<string>mysql://${DBUSER}:${DBPASS}@%2Fvar%2Frun%2Fmysqld%2Fmysqld.sock/${DBNAME}/sogo_admin</string>
|
||||||
<key>OCSCacheFolderURL</key>
|
<key>OCSCacheFolderURL</key>
|
||||||
@@ -134,18 +130,22 @@ chmod 600 /var/lib/sogo/GNUstep/Defaults/sogod.plist
|
|||||||
# Patch ACLs
|
# Patch ACLs
|
||||||
#if [[ ${ACL_ANYONE} == 'allow' ]]; then
|
#if [[ ${ACL_ANYONE} == 'allow' ]]; then
|
||||||
# #enable any or authenticated targets for ACL
|
# #enable any or authenticated targets for ACL
|
||||||
# if patch -R -sfN --dry-run /usr/lib/GNUstep/SOGo/Templates/UIxAclEditor.wox < /acl.diff > /dev/null; then
|
# if patch -R -sfN --dry-run /usr/local/lib/GNUstep/SOGo/Templates/UIxAclEditor.wox < /acl.diff > /dev/null; then
|
||||||
# patch -R /usr/lib/GNUstep/SOGo/Templates/UIxAclEditor.wox < /acl.diff;
|
# patch -R /usr/local/lib/GNUstep/SOGo/Templates/UIxAclEditor.wox < /acl.diff;
|
||||||
# fi
|
# fi
|
||||||
#else
|
#else
|
||||||
# #disable any or authenticated targets for ACL
|
# #disable any or authenticated targets for ACL
|
||||||
# if patch -sfN --dry-run /usr/lib/GNUstep/SOGo/Templates/UIxAclEditor.wox < /acl.diff > /dev/null; then
|
# if patch -sfN --dry-run /usr/local/lib/GNUstep/SOGo/Templates/UIxAclEditor.wox < /acl.diff > /dev/null; then
|
||||||
# patch /usr/lib/GNUstep/SOGo/Templates/UIxAclEditor.wox < /acl.diff;
|
# patch /usr/local/lib/GNUstep/SOGo/Templates/UIxAclEditor.wox < /acl.diff;
|
||||||
# fi
|
# fi
|
||||||
#fi
|
#fi
|
||||||
|
|
||||||
if patch -R -sfN --dry-run /usr/lib/GNUstep/SOGo/Templates/UIxTopnavToolbar.wox < /navMailcowBtns.diff > /dev/null; then
|
# Apply custom UI patch (reverse patch to ADD buttons)
|
||||||
patch -R /usr/lib/GNUstep/SOGo/Templates/UIxTopnavToolbar.wox < /navMailcowBtns.diff;
|
if patch -R -sfN --dry-run /usr/local/lib/GNUstep/SOGo/Templates/UIxTopnavToolbar.wox < /navMailcowBtns.diff > /dev/null; then
|
||||||
|
echo "Applying navMailcowBtns patch (reverse to add buttons)..."
|
||||||
|
patch -R /usr/local/lib/GNUstep/SOGo/Templates/UIxTopnavToolbar.wox < /navMailcowBtns.diff;
|
||||||
|
else
|
||||||
|
echo "navMailcowBtns patch already applied or cannot be applied"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Rename custom logo, if any
|
# Rename custom logo, if any
|
||||||
@@ -153,7 +153,7 @@ fi
|
|||||||
|
|
||||||
# Rsync web content
|
# Rsync web content
|
||||||
echo "Syncing web content with named volume"
|
echo "Syncing web content with named volume"
|
||||||
rsync -a /usr/lib/GNUstep/SOGo/. /sogo_web/
|
rsync -a /usr/local/lib/GNUstep/SOGo/. /sogo_web/
|
||||||
|
|
||||||
# Chown backup path
|
# Chown backup path
|
||||||
chown -R sogo:sogo /sogo_backup
|
chown -R sogo:sogo /sogo_backup
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
FROM alpine:3.21
|
FROM alpine:3.23
|
||||||
|
|
||||||
LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
|
LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
|
||||||
|
|
||||||
|
# install unbound from alpine:edge to get security patches
|
||||||
|
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 \
|
RUN apk add --update --no-cache \
|
||||||
curl \
|
curl \
|
||||||
bind-tools \
|
bind-tools \
|
||||||
coreutils \
|
coreutils \
|
||||||
unbound \
|
|
||||||
bash \
|
bash \
|
||||||
openssl \
|
openssl \
|
||||||
drill \
|
drill \
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM alpine:3.21
|
FROM alpine:3.23
|
||||||
|
|
||||||
LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
|
LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
|
||||||
|
|
||||||
@@ -37,5 +37,6 @@ RUN apk add --update \
|
|||||||
COPY watchdog.sh /watchdog.sh
|
COPY watchdog.sh /watchdog.sh
|
||||||
COPY check_mysql_slavestatus.sh /usr/lib/nagios/plugins/check_mysql_slavestatus.sh
|
COPY check_mysql_slavestatus.sh /usr/lib/nagios/plugins/check_mysql_slavestatus.sh
|
||||||
COPY check_dns.sh /usr/lib/mailcow/check_dns.sh
|
COPY check_dns.sh /usr/lib/mailcow/check_dns.sh
|
||||||
|
COPY client.cnf /etc/my.cnf.d/client.cnf
|
||||||
|
|
||||||
CMD ["/watchdog.sh"]
|
CMD ["/watchdog.sh"]
|
||||||
|
|||||||
@@ -19,19 +19,19 @@ if [ -z "$HOST" ]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# run dig and measure the time it takes to run
|
# 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_output=$(dig +short +timeout=2 +tries=1 "$HOST" @"$SERVER" 2>/dev/null)
|
||||||
dig_rc=$?
|
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 ',' -)
|
dig_output_ips=$(echo "$dig_output" | grep -E '^[0-9.]+$' | sort | paste -sd ',' -)
|
||||||
END_TIME=$(date +%s%3N)
|
ELAPSED_TIME=$(perl -e "printf('%.3f', $END_TIME - $START_TIME)")
|
||||||
ELAPSED_TIME=$((END_TIME - START_TIME))
|
|
||||||
|
|
||||||
# validate and perform nagios like output and exit codes
|
# validate and perform nagios like output and exit codes
|
||||||
if [ $dig_rc -ne 0 ] || [ -z "$dig_output" ]; then
|
if [ $dig_rc -ne 0 ] || [ -z "$dig_output" ]; then
|
||||||
echo "Domain $HOST was not found by the server"
|
echo "Domain $HOST was not found by the server"
|
||||||
exit 2
|
exit 2
|
||||||
elif [ $dig_rc -eq 0 ]; then
|
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
|
exit 0
|
||||||
else
|
else
|
||||||
echo "Unknown error"
|
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
|
fi
|
||||||
|
|
||||||
# Wait for containers
|
# Wait for containers
|
||||||
while ! mariadb-admin status --ssl=false --socket=/var/run/mysqld/mysqld.sock -u${DBUSER} -p${DBPASS} --silent; do
|
while ! mariadb-admin status --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u${DBUSER} -p${DBPASS} --silent; do
|
||||||
echo "Waiting for SQL..."
|
echo "Waiting for SQL..."
|
||||||
sleep 2
|
sleep 2
|
||||||
done
|
done
|
||||||
@@ -359,8 +359,8 @@ mysql_checks() {
|
|||||||
while [ ${err_count} -lt ${THRESHOLD} ]; do
|
while [ ${err_count} -lt ${THRESHOLD} ]; do
|
||||||
touch /tmp/mysql-mailcow; echo "$(tail -50 /tmp/mysql-mailcow)" > /tmp/mysql-mailcow
|
touch /tmp/mysql-mailcow; echo "$(tail -50 /tmp/mysql-mailcow)" > /tmp/mysql-mailcow
|
||||||
err_c_cur=${err_count}
|
err_c_cur=${err_count}
|
||||||
/usr/lib/nagios/plugins/check_mysql -s /var/run/mysqld/mysqld.sock -u ${DBUSER} -p ${DBPASS} -d ${DBNAME} 2>> /tmp/mysql-mailcow 1>&2; err_count=$(( ${err_count} + $? ))
|
/usr/lib/nagios/plugins/check_mysql -f /etc/my.cnf.d/client.cnf -s /var/run/mysqld/mysqld.sock -u ${DBUSER} -p ${DBPASS} -d ${DBNAME} 2>> /tmp/mysql-mailcow 1>&2; err_count=$(( ${err_count} + $? ))
|
||||||
/usr/lib/nagios/plugins/check_mysql_query -s /var/run/mysqld/mysqld.sock -u ${DBUSER} -p ${DBPASS} -d ${DBNAME} -q "SELECT COUNT(*) FROM information_schema.tables" 2>> /tmp/mysql-mailcow 1>&2; err_count=$(( ${err_count} + $? ))
|
/usr/lib/nagios/plugins/check_mysql_query -f /etc/my.cnf.d/client.cnf -s /var/run/mysqld/mysqld.sock -u ${DBUSER} -p ${DBPASS} -d ${DBNAME} -q "SELECT COUNT(*) FROM information_schema.tables" 2>> /tmp/mysql-mailcow 1>&2; err_count=$(( ${err_count} + $? ))
|
||||||
[ ${err_c_cur} -eq ${err_count} ] && [ ! $((${err_count} - 1)) -lt 0 ] && err_count=$((${err_count} - 1)) diff_c=1
|
[ ${err_c_cur} -eq ${err_count} ] && [ ! $((${err_count} - 1)) -lt 0 ] && err_count=$((${err_count} - 1)) diff_c=1
|
||||||
[ ${err_c_cur} -ne ${err_count} ] && diff_c=$(( ${err_c_cur} - ${err_count} ))
|
[ ${err_c_cur} -ne ${err_count} ] && diff_c=$(( ${err_c_cur} - ${err_count} ))
|
||||||
progress "MySQL/MariaDB" ${THRESHOLD} $(( ${THRESHOLD} - ${err_count} )) ${diff_c}
|
progress "MySQL/MariaDB" ${THRESHOLD} $(( ${THRESHOLD} - ${err_count} )) ${diff_c}
|
||||||
@@ -384,7 +384,7 @@ mysql_repl_checks() {
|
|||||||
while [ ${err_count} -lt ${THRESHOLD} ]; do
|
while [ ${err_count} -lt ${THRESHOLD} ]; do
|
||||||
touch /tmp/mysql_repl_checks; echo "$(tail -50 /tmp/mysql_repl_checks)" > /tmp/mysql_repl_checks
|
touch /tmp/mysql_repl_checks; echo "$(tail -50 /tmp/mysql_repl_checks)" > /tmp/mysql_repl_checks
|
||||||
err_c_cur=${err_count}
|
err_c_cur=${err_count}
|
||||||
/usr/lib/nagios/plugins/check_mysql_slavestatus.sh -S /var/run/mysqld/mysqld.sock -u root -p ${DBROOT} 2>> /tmp/mysql_repl_checks 1>&2; err_count=$(( ${err_count} + $? ))
|
/usr/lib/nagios/plugins/check_mysql_slavestatus.sh -o /etc/my.cnf.d/client.cnf -S /var/run/mysqld/mysqld.sock -u root -p ${DBROOT} 2>> /tmp/mysql_repl_checks 1>&2; err_count=$(( ${err_count} + $? ))
|
||||||
[ ${err_c_cur} -eq ${err_count} ] && [ ! $((${err_count} - 1)) -lt 0 ] && err_count=$((${err_count} - 1)) diff_c=1
|
[ ${err_c_cur} -eq ${err_count} ] && [ ! $((${err_count} - 1)) -lt 0 ] && err_count=$((${err_count} - 1)) diff_c=1
|
||||||
[ ${err_c_cur} -ne ${err_count} ] && diff_c=$(( ${err_c_cur} - ${err_count} ))
|
[ ${err_c_cur} -ne ${err_count} ] && diff_c=$(( ${err_c_cur} - ${err_count} ))
|
||||||
progress "MySQL/MariaDB replication" ${THRESHOLD} $(( ${THRESHOLD} - ${err_count} )) ${diff_c}
|
progress "MySQL/MariaDB replication" ${THRESHOLD} $(( ${THRESHOLD} - ${err_count} )) ${diff_c}
|
||||||
|
|||||||
@@ -80,14 +80,21 @@ if ($isSOGoRequest) {
|
|||||||
}
|
}
|
||||||
if ($result === false){
|
if ($result === false){
|
||||||
// If it's a SOGo Request, don't check for protocol access
|
// If it's a SOGo Request, don't check for protocol access
|
||||||
$service = ($isSOGoRequest) ? false : array($post['service'] => true);
|
if ($isSOGoRequest) {
|
||||||
$result = apppass_login($post['username'], $post['password'], $service, array(
|
$service = 'SOGO';
|
||||||
|
$post['service'] = 'NONE';
|
||||||
|
} else {
|
||||||
|
$service = $post['service'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = apppass_login($post['username'], $post['password'], array(
|
||||||
|
'service' => $post['service'],
|
||||||
'is_internal' => true,
|
'is_internal' => true,
|
||||||
'remote_addr' => $post['real_rip']
|
'remote_addr' => $post['real_rip']
|
||||||
));
|
));
|
||||||
if ($result) {
|
if ($result) {
|
||||||
error_log('MAILCOWAUTH: App auth for user ' . $post['username'] . " with service " . $post['service'] . " from IP " . $post['real_rip']);
|
error_log('MAILCOWAUTH: App auth for user ' . $post['username'] . " with service " . $service . " from IP " . $post['real_rip']);
|
||||||
set_sasl_log($post['username'], $post['real_rip'], $post['service']);
|
set_sasl_log($post['username'], $post['real_rip'], $service);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ($result === false){
|
if ($result === false){
|
||||||
|
|||||||
@@ -32,8 +32,17 @@ function auth_password_verify(request, password)
|
|||||||
-- Returning PASSDB_RESULT_PASSWORD_MISMATCH will reset the user's auth cache entry.
|
-- Returning PASSDB_RESULT_PASSWORD_MISMATCH will reset the user's auth cache entry.
|
||||||
-- Returning PASSDB_RESULT_INTERNAL_FAILURE keeps the existing cache entry,
|
-- Returning PASSDB_RESULT_INTERNAL_FAILURE keeps the existing cache entry,
|
||||||
-- even if the TTL has expired. Useful to avoid cache eviction during backend issues.
|
-- even if the TTL has expired. Useful to avoid cache eviction during backend issues.
|
||||||
|
|
||||||
|
-- On a network-level failure (nginx unreachable, DNS failure, timeout) https.request
|
||||||
|
-- returns nil plus an error string, so c is not a numeric HTTP status code. Treat this
|
||||||
|
-- as a backend outage and keep the cache entry, rather than wiping it as a mismatch.
|
||||||
|
if type(c) ~= "number" then
|
||||||
|
dovecot.i_info("HTTP request to auth backend failed with " .. tostring(c) .. " for user " .. request.user)
|
||||||
|
return dovecot.auth.PASSDB_RESULT_INTERNAL_FAILURE, "Upstream unreachable"
|
||||||
|
end
|
||||||
|
|
||||||
if c ~= 200 and c ~= 401 then
|
if c ~= 200 and c ~= 401 then
|
||||||
dovecot.i_info("HTTP request failed with " .. c .. " for user " .. request.user)
|
dovecot.i_info("HTTP request failed with " .. tostring(c) .. " for user " .. request.user)
|
||||||
return dovecot.auth.PASSDB_RESULT_PASSWORD_MISMATCH, "Upstream error"
|
return dovecot.auth.PASSDB_RESULT_PASSWORD_MISMATCH, "Upstream error"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,3 @@
|
|||||||
# global_sieve_before script
|
# global_sieve_before script
|
||||||
# global_sieve_before -> user sieve_before (mailcow UI) -> user sieve_after (mailcow UI) -> global_sieve_after
|
# global_sieve_before -> user sieve_before (mailcow UI) -> user sieve_after (mailcow UI) -> global_sieve_after
|
||||||
|
|
||||||
require ["mailbox", "fileinto"];
|
|
||||||
|
|
||||||
if header :contains ["Chat-Version"] [""] {
|
|
||||||
if mailboxexists "DeltaChat" {
|
|
||||||
fileinto "DeltaChat";
|
|
||||||
} else {
|
|
||||||
fileinto :create "DeltaChat";
|
|
||||||
}
|
|
||||||
stop;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ events {
|
|||||||
http {
|
http {
|
||||||
include /etc/nginx/mime.types;
|
include /etc/nginx/mime.types;
|
||||||
default_type application/octet-stream;
|
default_type application/octet-stream;
|
||||||
|
server_tokens off;
|
||||||
|
|
||||||
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||||||
'$status $body_bytes_sent "$http_referer" '
|
'$status $body_bytes_sent "$http_referer" '
|
||||||
@@ -41,12 +42,19 @@ http {
|
|||||||
https https;
|
https https;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
map $server_port $sogo_auth_internal {
|
||||||
|
65510 "1";
|
||||||
|
default "";
|
||||||
|
}
|
||||||
|
|
||||||
{% if HTTP_REDIRECT %}
|
{% if HTTP_REDIRECT %}
|
||||||
# HTTP to HTTPS redirect
|
# HTTP to HTTPS redirect
|
||||||
server {
|
server {
|
||||||
root /web;
|
root /web;
|
||||||
listen {{ HTTP_PORT }} default_server;
|
listen {{ HTTP_PORT }} default_server;
|
||||||
|
{% if ENABLE_IPV6 %}
|
||||||
listen [::]:{{ HTTP_PORT }} default_server;
|
listen [::]:{{ HTTP_PORT }} default_server;
|
||||||
|
{%endif%}
|
||||||
|
|
||||||
server_name {{ MAILCOW_HOSTNAME }} autodiscover.* autoconfig.* mta-sts.* {{ ADDITIONAL_SERVER_NAMES | join(' ') }};
|
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 Strict-Transport-Security "max-age=15768000;";
|
||||||
add_header X-Content-Type-Options nosniff;
|
add_header X-Content-Type-Options nosniff;
|
||||||
add_header X-XSS-Protection "1; mode=block";
|
|
||||||
add_header X-Robots-Tag none;
|
add_header X-Robots-Tag none;
|
||||||
add_header X-Download-Options noopen;
|
add_header X-Download-Options noopen;
|
||||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||||
@@ -117,6 +116,9 @@ location ~ \.php$ {
|
|||||||
include /etc/nginx/fastcgi_params;
|
include /etc/nginx/fastcgi_params;
|
||||||
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||||
fastcgi_param PATH_INFO $fastcgi_path_info;
|
fastcgi_param PATH_INFO $fastcgi_path_info;
|
||||||
|
fastcgi_param HTTP_X_REAL_IP $remote_addr;
|
||||||
|
# trusted internal-auth marker; empty for external clients (see nginx.conf map)
|
||||||
|
fastcgi_param SOGO_AUTH_INTERNAL $sogo_auth_internal;
|
||||||
fastcgi_read_timeout 3600;
|
fastcgi_read_timeout 3600;
|
||||||
fastcgi_send_timeout 3600;
|
fastcgi_send_timeout 3600;
|
||||||
}
|
}
|
||||||
@@ -262,19 +264,19 @@ location ~* /sogo$ {
|
|||||||
}
|
}
|
||||||
|
|
||||||
location /SOGo.woa/WebServerResources/ {
|
location /SOGo.woa/WebServerResources/ {
|
||||||
alias /usr/lib/GNUstep/SOGo/WebServerResources/;
|
alias /usr/local/lib/GNUstep/SOGo/WebServerResources/;
|
||||||
}
|
}
|
||||||
|
|
||||||
location /.woa/WebServerResources/ {
|
location /.woa/WebServerResources/ {
|
||||||
alias /usr/lib/GNUstep/SOGo/WebServerResources/;
|
alias /usr/local/lib/GNUstep/SOGo/WebServerResources/;
|
||||||
}
|
}
|
||||||
|
|
||||||
location /SOGo/WebServerResources/ {
|
location /SOGo/WebServerResources/ {
|
||||||
alias /usr/lib/GNUstep/SOGo/WebServerResources/;
|
alias /usr/local/lib/GNUstep/SOGo/WebServerResources/;
|
||||||
}
|
}
|
||||||
|
|
||||||
location (^/SOGo/so/ControlPanel/Products/[^/]*UI/Resources/.*\.(jpg|png|gif|css|js)$) {
|
location (^/SOGo/so/ControlPanel/Products/[^/]*UI/Resources/.*\.(jpg|png|gif|css|js)$) {
|
||||||
alias /usr/lib/GNUstep/SOGo/$1.SOGo/Resources/$2;
|
alias /usr/local/lib/GNUstep/SOGo/$1.SOGo/Resources/$2;
|
||||||
}
|
}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
|||||||
@@ -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=1
|
||||||
opcache.enable_cli=1
|
opcache.enable_cli=1
|
||||||
opcache.interned_strings_buffer=16
|
opcache.interned_strings_buffer=16
|
||||||
opcache.max_accelerated_files=10000
|
opcache.max_accelerated_files=10000
|
||||||
opcache.memory_consumption=128
|
opcache.memory_consumption=128
|
||||||
opcache.save_comments=1
|
opcache.save_comments=1
|
||||||
opcache.revalidate_freq=1
|
opcache.validate_timestamps=0
|
||||||
|
|
||||||
|
; JIT
|
||||||
|
; Disabled for now due to some PHP segmentation faults observed
|
||||||
|
; in certain environments. Possibly some PHP or PHP extension bug.
|
||||||
|
opcache.jit=disable
|
||||||
|
opcache.jit_buffer_size=0
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ postscreen_access_list = permit_mynetworks,
|
|||||||
cidr:/opt/postfix/conf/postscreen_access.cidr,
|
cidr:/opt/postfix/conf/postscreen_access.cidr,
|
||||||
tcp:127.0.0.1:10027
|
tcp:127.0.0.1:10027
|
||||||
postscreen_bare_newline_enable = no
|
postscreen_bare_newline_enable = no
|
||||||
postscreen_blacklist_action = drop
|
postscreen_denylist_action = drop
|
||||||
postscreen_cache_cleanup_interval = 24h
|
postscreen_cache_cleanup_interval = 24h
|
||||||
postscreen_cache_map = proxy:btree:$data_directory/postscreen_cache
|
postscreen_cache_map = proxy:btree:$data_directory/postscreen_cache
|
||||||
postscreen_dnsbl_action = enforce
|
postscreen_dnsbl_action = enforce
|
||||||
@@ -107,8 +107,6 @@ smtpd_sender_restrictions = reject_authenticated_sender_login_mismatch,
|
|||||||
reject_unknown_sender_domain
|
reject_unknown_sender_domain
|
||||||
smtpd_soft_error_limit = 3
|
smtpd_soft_error_limit = 3
|
||||||
smtpd_tls_auth_only = yes
|
smtpd_tls_auth_only = yes
|
||||||
smtpd_tls_dh1024_param_file = /etc/ssl/mail/dhparams.pem
|
|
||||||
smtpd_tls_eecdh_grade = auto
|
|
||||||
smtpd_tls_exclude_ciphers = ECDHE-RSA-RC4-SHA, RC4, aNULL, DES-CBC3-SHA, ECDHE-RSA-DES-CBC3-SHA, EDH-RSA-DES-CBC3-SHA
|
smtpd_tls_exclude_ciphers = ECDHE-RSA-RC4-SHA, RC4, aNULL, DES-CBC3-SHA, ECDHE-RSA-DES-CBC3-SHA, EDH-RSA-DES-CBC3-SHA
|
||||||
smtpd_tls_loglevel = 1
|
smtpd_tls_loglevel = 1
|
||||||
|
|
||||||
@@ -152,7 +150,7 @@ smtp_sasl_auth_enable = yes
|
|||||||
smtp_sasl_password_maps = proxy:mysql:/opt/postfix/conf/sql/mysql_sasl_passwd_maps_sender_dependent.cf
|
smtp_sasl_password_maps = proxy:mysql:/opt/postfix/conf/sql/mysql_sasl_passwd_maps_sender_dependent.cf
|
||||||
smtp_sasl_security_options =
|
smtp_sasl_security_options =
|
||||||
smtp_sasl_mechanism_filter = plain, login
|
smtp_sasl_mechanism_filter = plain, login
|
||||||
smtp_tls_policy_maps = proxy:mysql:/opt/postfix/conf/sql/mysql_tls_policy_override_maps.cf 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
|
smtp_header_checks = pcre:/opt/postfix/conf/anonymize_headers.pcre
|
||||||
mail_name = Postcow
|
mail_name = Postcow
|
||||||
# local_transport map catches local destinations and prevents routing local dests when the next map would route "*"
|
# local_transport map catches local destinations and prevents routing local dests when the next map would route "*"
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ smtps inet n - n - - smtpd
|
|||||||
# TLS protocol can be modified by setting submission_smtpd_tls_mandatory_protocols in extra.cf
|
# TLS protocol can be modified by setting submission_smtpd_tls_mandatory_protocols in extra.cf
|
||||||
submission inet n - n - - smtpd
|
submission inet n - n - - smtpd
|
||||||
-o smtpd_client_restrictions=permit_mynetworks,permit_sasl_authenticated,reject
|
-o smtpd_client_restrictions=permit_mynetworks,permit_sasl_authenticated,reject
|
||||||
-o smtpd_enforce_tls=yes
|
|
||||||
-o smtpd_tls_security_level=encrypt
|
-o smtpd_tls_security_level=encrypt
|
||||||
-o smtpd_tls_mandatory_protocols=$submission_smtpd_tls_mandatory_protocols
|
-o smtpd_tls_mandatory_protocols=$submission_smtpd_tls_mandatory_protocols
|
||||||
-o tls_preempt_cipherlist=yes
|
-o tls_preempt_cipherlist=yes
|
||||||
@@ -38,7 +37,6 @@ submission inet n - n - - smtpd
|
|||||||
10587 inet n - n - - smtpd
|
10587 inet n - n - - smtpd
|
||||||
-o smtpd_upstream_proxy_protocol=haproxy
|
-o smtpd_upstream_proxy_protocol=haproxy
|
||||||
-o smtpd_client_restrictions=permit_mynetworks,permit_sasl_authenticated,reject
|
-o smtpd_client_restrictions=permit_mynetworks,permit_sasl_authenticated,reject
|
||||||
-o smtpd_enforce_tls=yes
|
|
||||||
-o smtpd_tls_security_level=encrypt
|
-o smtpd_tls_security_level=encrypt
|
||||||
-o smtpd_tls_mandatory_protocols=$submission_smtpd_tls_mandatory_protocols
|
-o smtpd_tls_mandatory_protocols=$submission_smtpd_tls_mandatory_protocols
|
||||||
-o tls_preempt_cipherlist=yes
|
-o tls_preempt_cipherlist=yes
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
# Whitelist generated by Postwhite v3.4 on Wed Oct 1 00:21:33 UTC 2025
|
# Whitelist generated by Postwhite v3.4 on Sat Aug 1 00:35:20 UTC 2026
|
||||||
# https://github.com/stevejenkins/postwhite/
|
# https://github.com/stevejenkins/postwhite/
|
||||||
# 2216 total rules
|
# 2256 total rules
|
||||||
2a00:1450:4000::/36 permit
|
2a00:1450:4000::/36 permit
|
||||||
|
2a00:1450:4864::/56 permit
|
||||||
2a01:111:f400::/48 permit
|
2a01:111:f400::/48 permit
|
||||||
2a01:111:f403:2800::/53 permit
|
2a01:111:f403:2800::/53 permit
|
||||||
2a01:111:f403:8000::/51 permit
|
2a01:111:f403:8000::/51 permit
|
||||||
@@ -28,9 +29,15 @@
|
|||||||
2a01:b747:3004:200::/56 permit
|
2a01:b747:3004:200::/56 permit
|
||||||
2a01:b747:3005:200::/56 permit
|
2a01:b747:3005:200::/56 permit
|
||||||
2a01:b747:3006:200::/56 permit
|
2a01:b747:3006:200::/56 permit
|
||||||
|
2a01:b747:3007::/56 permit
|
||||||
2a02:a60:0:5::/64 permit
|
2a02:a60:0:5::/64 permit
|
||||||
|
2a0f:f640::/56 permit
|
||||||
2c0f:fb50:4000::/36 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.217.30 permit
|
||||||
|
2.207.223.160/27 permit
|
||||||
3.64.237.68 permit
|
3.64.237.68 permit
|
||||||
3.65.3.180 permit
|
3.65.3.180 permit
|
||||||
3.70.123.177 permit
|
3.70.123.177 permit
|
||||||
@@ -51,13 +58,15 @@
|
|||||||
8.25.196.0/23 permit
|
8.25.196.0/23 permit
|
||||||
8.36.116.0/24 permit
|
8.36.116.0/24 permit
|
||||||
8.39.54.0/23 permit
|
8.39.54.0/23 permit
|
||||||
|
8.39.54.240/29 permit
|
||||||
8.39.54.250/31 permit
|
8.39.54.250/31 permit
|
||||||
8.39.144.0/24 permit
|
8.39.144.0/24 permit
|
||||||
8.40.222.0/23 permit
|
8.40.222.0/23 permit
|
||||||
|
8.40.222.240/29 permit
|
||||||
8.40.222.250/31 permit
|
8.40.222.250/31 permit
|
||||||
|
8.47.10.9 permit
|
||||||
12.130.86.238 permit
|
12.130.86.238 permit
|
||||||
13.107.213.41 permit
|
13.108.16.0/20 permit
|
||||||
13.107.246.41 permit
|
|
||||||
13.110.208.0/21 permit
|
13.110.208.0/21 permit
|
||||||
13.110.209.0/24 permit
|
13.110.209.0/24 permit
|
||||||
13.110.216.0/22 permit
|
13.110.216.0/22 permit
|
||||||
@@ -66,6 +75,7 @@
|
|||||||
13.111.191.0/24 permit
|
13.111.191.0/24 permit
|
||||||
13.216.7.111 permit
|
13.216.7.111 permit
|
||||||
13.216.54.180 permit
|
13.216.54.180 permit
|
||||||
|
13.247.164.219 permit
|
||||||
15.200.21.50 permit
|
15.200.21.50 permit
|
||||||
15.200.44.248 permit
|
15.200.44.248 permit
|
||||||
15.200.201.185 permit
|
15.200.201.185 permit
|
||||||
@@ -273,6 +283,7 @@
|
|||||||
50.56.130.221 permit
|
50.56.130.221 permit
|
||||||
50.56.130.222 permit
|
50.56.130.222 permit
|
||||||
50.112.246.219 permit
|
50.112.246.219 permit
|
||||||
|
51.83.17.38 permit
|
||||||
52.1.14.157 permit
|
52.1.14.157 permit
|
||||||
52.5.230.59 permit
|
52.5.230.59 permit
|
||||||
52.6.74.205 permit
|
52.6.74.205 permit
|
||||||
@@ -299,15 +310,6 @@
|
|||||||
52.94.124.0/28 permit
|
52.94.124.0/28 permit
|
||||||
52.95.48.152/29 permit
|
52.95.48.152/29 permit
|
||||||
52.95.49.88/29 permit
|
52.95.49.88/29 permit
|
||||||
52.96.91.34 permit
|
|
||||||
52.96.111.82 permit
|
|
||||||
52.96.172.98 permit
|
|
||||||
52.96.214.50 permit
|
|
||||||
52.96.222.194 permit
|
|
||||||
52.96.222.226 permit
|
|
||||||
52.96.223.2 permit
|
|
||||||
52.96.228.130 permit
|
|
||||||
52.96.229.242 permit
|
|
||||||
52.100.0.0/15 permit
|
52.100.0.0/15 permit
|
||||||
52.102.0.0/16 permit
|
52.102.0.0/16 permit
|
||||||
52.103.0.0/17 permit
|
52.103.0.0/17 permit
|
||||||
@@ -373,6 +375,7 @@
|
|||||||
64.132.88.0/23 permit
|
64.132.88.0/23 permit
|
||||||
64.132.92.0/24 permit
|
64.132.92.0/24 permit
|
||||||
64.181.194.190 permit
|
64.181.194.190 permit
|
||||||
|
64.181.213.254 permit
|
||||||
64.207.219.7 permit
|
64.207.219.7 permit
|
||||||
64.207.219.8 permit
|
64.207.219.8 permit
|
||||||
64.207.219.9 permit
|
64.207.219.9 permit
|
||||||
@@ -402,27 +405,8 @@
|
|||||||
64.207.219.143 permit
|
64.207.219.143 permit
|
||||||
64.233.160.0/19 permit
|
64.233.160.0/19 permit
|
||||||
65.52.80.137 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.29.77 permit
|
||||||
65.55.33.64/28 permit
|
|
||||||
65.55.34.0/24 permit
|
|
||||||
65.55.42.224/28 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.110.161.77 permit
|
||||||
65.123.29.213 permit
|
65.123.29.213 permit
|
||||||
65.123.29.220 permit
|
65.123.29.220 permit
|
||||||
@@ -477,7 +461,11 @@
|
|||||||
66.218.75.252/31 permit
|
66.218.75.252/31 permit
|
||||||
66.218.75.254 permit
|
66.218.75.254 permit
|
||||||
66.220.144.128/25 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.0/24 permit
|
||||||
|
66.220.155.178 permit
|
||||||
|
66.220.155.179 permit
|
||||||
66.220.157.0/25 permit
|
66.220.157.0/25 permit
|
||||||
66.231.80.0/20 permit
|
66.231.80.0/20 permit
|
||||||
66.240.227.0/24 permit
|
66.240.227.0/24 permit
|
||||||
@@ -542,8 +530,9 @@
|
|||||||
69.162.98.0/24 permit
|
69.162.98.0/24 permit
|
||||||
69.169.224.0/20 permit
|
69.169.224.0/20 permit
|
||||||
69.171.232.0/24 permit
|
69.171.232.0/24 permit
|
||||||
|
69.171.232.180 permit
|
||||||
|
69.171.232.181 permit
|
||||||
69.171.244.0/23 permit
|
69.171.244.0/23 permit
|
||||||
70.37.151.128/25 permit
|
|
||||||
70.42.149.35 permit
|
70.42.149.35 permit
|
||||||
72.3.185.0/24 permit
|
72.3.185.0/24 permit
|
||||||
72.14.192.0/18 permit
|
72.14.192.0/18 permit
|
||||||
@@ -640,7 +629,6 @@
|
|||||||
74.208.4.220 permit
|
74.208.4.220 permit
|
||||||
74.208.4.221 permit
|
74.208.4.221 permit
|
||||||
74.209.250.0/24 permit
|
74.209.250.0/24 permit
|
||||||
75.2.70.75 permit
|
|
||||||
76.223.128.0/19 permit
|
76.223.128.0/19 permit
|
||||||
76.223.176.0/20 permit
|
76.223.176.0/20 permit
|
||||||
77.238.176.0/24 permit
|
77.238.176.0/24 permit
|
||||||
@@ -665,6 +653,7 @@
|
|||||||
77.238.189.148/30 permit
|
77.238.189.148/30 permit
|
||||||
79.135.106.0/24 permit
|
79.135.106.0/24 permit
|
||||||
79.135.107.0/24 permit
|
79.135.107.0/24 permit
|
||||||
|
80.225.160.128/25 permit
|
||||||
81.169.146.243 permit
|
81.169.146.243 permit
|
||||||
81.169.146.245 permit
|
81.169.146.245 permit
|
||||||
81.169.146.246 permit
|
81.169.146.246 permit
|
||||||
@@ -683,6 +672,9 @@
|
|||||||
82.165.159.45 permit
|
82.165.159.45 permit
|
||||||
82.165.159.130 permit
|
82.165.159.130 permit
|
||||||
82.165.159.131 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.206.169 permit
|
||||||
85.9.210.45 permit
|
85.9.210.45 permit
|
||||||
85.158.136.0/21 permit
|
85.158.136.0/21 permit
|
||||||
@@ -733,11 +725,11 @@
|
|||||||
87.248.117.205 permit
|
87.248.117.205 permit
|
||||||
87.253.232.0/21 permit
|
87.253.232.0/21 permit
|
||||||
89.22.108.0/24 permit
|
89.22.108.0/24 permit
|
||||||
91.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
|
91.211.240.0/22 permit
|
||||||
94.236.119.0/26 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.131.104.0/21 permit
|
||||||
95.217.114.154 permit
|
95.217.114.154 permit
|
||||||
96.43.144.0/20 permit
|
96.43.144.0/20 permit
|
||||||
@@ -1230,9 +1222,9 @@
|
|||||||
98.139.245.208/30 permit
|
98.139.245.208/30 permit
|
||||||
98.139.245.212/31 permit
|
98.139.245.212/31 permit
|
||||||
99.78.197.208/28 permit
|
99.78.197.208/28 permit
|
||||||
99.83.190.102 permit
|
|
||||||
103.9.96.0/22 permit
|
103.9.96.0/22 permit
|
||||||
103.28.42.0/24 permit
|
103.28.42.0/24 permit
|
||||||
|
103.84.217.15 permit
|
||||||
103.84.217.238 permit
|
103.84.217.238 permit
|
||||||
103.89.75.238 permit
|
103.89.75.238 permit
|
||||||
103.151.192.0/23 permit
|
103.151.192.0/23 permit
|
||||||
@@ -1378,11 +1370,6 @@
|
|||||||
108.179.144.0/20 permit
|
108.179.144.0/20 permit
|
||||||
109.224.244.0/24 permit
|
109.224.244.0/24 permit
|
||||||
109.237.142.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.199.64/29 permit
|
||||||
112.19.242.64/29 permit
|
112.19.242.64/29 permit
|
||||||
116.214.12.47 permit
|
116.214.12.47 permit
|
||||||
@@ -1400,6 +1387,7 @@
|
|||||||
117.120.16.0/21 permit
|
117.120.16.0/21 permit
|
||||||
119.42.242.52/31 permit
|
119.42.242.52/31 permit
|
||||||
119.42.242.156 permit
|
119.42.242.156 permit
|
||||||
|
121.244.91.31 permit
|
||||||
121.244.91.48 permit
|
121.244.91.48 permit
|
||||||
121.244.91.52 permit
|
121.244.91.52 permit
|
||||||
122.15.156.182 permit
|
122.15.156.182 permit
|
||||||
@@ -1430,6 +1418,7 @@
|
|||||||
128.245.248.0/21 permit
|
128.245.248.0/21 permit
|
||||||
129.41.77.70 permit
|
129.41.77.70 permit
|
||||||
129.41.169.249 permit
|
129.41.169.249 permit
|
||||||
|
129.77.16.0/20 permit
|
||||||
129.80.5.164 permit
|
129.80.5.164 permit
|
||||||
129.80.64.36 permit
|
129.80.64.36 permit
|
||||||
129.80.67.121 permit
|
129.80.67.121 permit
|
||||||
@@ -1438,6 +1427,20 @@
|
|||||||
129.146.88.28 permit
|
129.146.88.28 permit
|
||||||
129.146.147.105 permit
|
129.146.147.105 permit
|
||||||
129.146.236.58 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.151.67.221 permit
|
||||||
129.153.62.216 permit
|
129.153.62.216 permit
|
||||||
129.153.104.71 permit
|
129.153.104.71 permit
|
||||||
@@ -1446,19 +1449,23 @@
|
|||||||
129.153.194.228 permit
|
129.153.194.228 permit
|
||||||
129.154.255.129 permit
|
129.154.255.129 permit
|
||||||
129.158.56.255 permit
|
129.158.56.255 permit
|
||||||
|
129.158.62.153 permit
|
||||||
129.159.22.159 permit
|
129.159.22.159 permit
|
||||||
129.159.87.137 permit
|
129.159.87.137 permit
|
||||||
129.213.195.191 permit
|
129.213.195.191 permit
|
||||||
|
130.35.116.0/25 permit
|
||||||
130.61.9.72 permit
|
130.61.9.72 permit
|
||||||
130.162.39.83 permit
|
130.162.39.83 permit
|
||||||
130.248.172.0/24 permit
|
130.248.172.0/24 permit
|
||||||
130.248.173.0/24 permit
|
130.248.173.0/24 permit
|
||||||
|
131.186.12.0/25 permit
|
||||||
131.253.30.0/24 permit
|
131.253.30.0/24 permit
|
||||||
131.253.121.0/26 permit
|
131.253.121.0/26 permit
|
||||||
132.145.13.209 permit
|
132.145.13.209 permit
|
||||||
132.226.26.225 permit
|
132.226.26.225 permit
|
||||||
132.226.49.32 permit
|
132.226.49.32 permit
|
||||||
132.226.56.24 permit
|
132.226.56.24 permit
|
||||||
|
134.98.248.128/25 permit
|
||||||
134.128.64.0/19 permit
|
134.128.64.0/19 permit
|
||||||
134.128.96.0/19 permit
|
134.128.96.0/19 permit
|
||||||
134.170.27.8 permit
|
134.170.27.8 permit
|
||||||
@@ -1481,6 +1488,7 @@
|
|||||||
136.143.184.0/24 permit
|
136.143.184.0/24 permit
|
||||||
136.143.188.0/24 permit
|
136.143.188.0/24 permit
|
||||||
136.143.190.0/23 permit
|
136.143.190.0/23 permit
|
||||||
|
136.146.128.0/20 permit
|
||||||
136.147.128.0/20 permit
|
136.147.128.0/20 permit
|
||||||
136.147.135.0/24 permit
|
136.147.135.0/24 permit
|
||||||
136.147.176.0/20 permit
|
136.147.176.0/20 permit
|
||||||
@@ -1488,6 +1496,10 @@
|
|||||||
136.147.182.0/24 permit
|
136.147.182.0/24 permit
|
||||||
136.147.224.0/20 permit
|
136.147.224.0/20 permit
|
||||||
136.179.50.206 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.60.152.0/22 permit
|
||||||
139.138.35.44 permit
|
139.138.35.44 permit
|
||||||
139.138.46.121 permit
|
139.138.46.121 permit
|
||||||
@@ -1496,8 +1508,11 @@
|
|||||||
139.138.57.55 permit
|
139.138.57.55 permit
|
||||||
139.138.58.119 permit
|
139.138.58.119 permit
|
||||||
139.167.79.86 permit
|
139.167.79.86 permit
|
||||||
|
139.177.108.0/25 permit
|
||||||
139.180.17.0/24 permit
|
139.180.17.0/24 permit
|
||||||
140.238.148.191 permit
|
140.238.148.191 permit
|
||||||
|
141.148.55.217 permit
|
||||||
|
141.148.91.244 permit
|
||||||
141.148.159.229 permit
|
141.148.159.229 permit
|
||||||
141.193.32.0/23 permit
|
141.193.32.0/23 permit
|
||||||
141.193.184.32/27 permit
|
141.193.184.32/27 permit
|
||||||
@@ -1532,6 +1547,9 @@
|
|||||||
146.88.28.0/24 permit
|
146.88.28.0/24 permit
|
||||||
146.148.116.76 permit
|
146.148.116.76 permit
|
||||||
147.154.32.0/25 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.47 permit
|
||||||
147.243.1.48 permit
|
147.243.1.48 permit
|
||||||
147.243.1.153 permit
|
147.243.1.153 permit
|
||||||
@@ -1539,40 +1557,38 @@
|
|||||||
147.243.128.26 permit
|
147.243.128.26 permit
|
||||||
148.105.0.0/16 permit
|
148.105.0.0/16 permit
|
||||||
148.105.8.0/21 permit
|
148.105.8.0/21 permit
|
||||||
|
148.116.32.128/25 permit
|
||||||
149.72.0.0/16 permit
|
149.72.0.0/16 permit
|
||||||
149.72.234.184 permit
|
149.72.234.184 permit
|
||||||
149.72.248.236 permit
|
149.72.248.236 permit
|
||||||
149.97.173.180 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
|
150.230.98.160 permit
|
||||||
151.145.38.14 permit
|
151.145.38.14 permit
|
||||||
152.67.105.195 permit
|
152.67.105.195 permit
|
||||||
152.69.200.236 permit
|
152.69.200.236 permit
|
||||||
152.70.155.126 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.208.51 permit
|
||||||
155.248.220.138 permit
|
155.248.220.138 permit
|
||||||
155.248.234.149 permit
|
155.248.234.149 permit
|
||||||
155.248.237.141 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.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.30.128/25 permit
|
||||||
157.58.196.96/29 permit
|
157.58.196.96/29 permit
|
||||||
157.58.249.3 permit
|
157.58.249.3 permit
|
||||||
|
157.137.32.128/25 permit
|
||||||
|
157.137.96.128/25 permit
|
||||||
157.151.208.65 permit
|
157.151.208.65 permit
|
||||||
157.255.1.64/29 permit
|
157.255.1.64/29 permit
|
||||||
158.101.211.207 permit
|
158.101.211.207 permit
|
||||||
158.247.16.0/20 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.154.0/24 permit
|
||||||
159.92.155.0/24 permit
|
159.92.155.0/24 permit
|
||||||
159.92.157.0/24 permit
|
159.92.157.0/24 permit
|
||||||
@@ -1594,8 +1610,10 @@
|
|||||||
159.135.224.0/20 permit
|
159.135.224.0/20 permit
|
||||||
159.135.228.10 permit
|
159.135.228.10 permit
|
||||||
159.183.0.0/16 permit
|
159.183.0.0/16 permit
|
||||||
|
159.183.14.233 permit
|
||||||
159.183.68.71 permit
|
159.183.68.71 permit
|
||||||
159.183.79.38 permit
|
159.183.79.38 permit
|
||||||
|
159.183.121.182 permit
|
||||||
159.183.129.172 permit
|
159.183.129.172 permit
|
||||||
160.1.62.192 permit
|
160.1.62.192 permit
|
||||||
161.38.192.0/20 permit
|
161.38.192.0/20 permit
|
||||||
@@ -1604,6 +1622,7 @@
|
|||||||
161.71.64.0/20 permit
|
161.71.64.0/20 permit
|
||||||
162.88.4.0/23 permit
|
162.88.4.0/23 permit
|
||||||
162.88.8.0/24 permit
|
162.88.8.0/24 permit
|
||||||
|
162.88.24.0/23 permit
|
||||||
162.88.24.0/24 permit
|
162.88.24.0/24 permit
|
||||||
162.88.25.0/24 permit
|
162.88.25.0/24 permit
|
||||||
162.88.36.0/24 permit
|
162.88.36.0/24 permit
|
||||||
@@ -1615,12 +1634,19 @@
|
|||||||
163.114.135.16 permit
|
163.114.135.16 permit
|
||||||
163.116.128.0/17 permit
|
163.116.128.0/17 permit
|
||||||
163.192.116.87 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.23.32 permit
|
||||||
164.152.25.241 permit
|
164.152.25.241 permit
|
||||||
164.177.132.168/30 permit
|
164.177.132.168/30 permit
|
||||||
|
165.1.100.0/25 permit
|
||||||
165.173.128.0/24 permit
|
165.173.128.0/24 permit
|
||||||
|
165.173.180.0/24 permit
|
||||||
165.173.180.250/31 permit
|
165.173.180.250/31 permit
|
||||||
|
165.173.182.0/24 permit
|
||||||
165.173.182.250/31 permit
|
165.173.182.250/31 permit
|
||||||
|
165.173.189.205 permit
|
||||||
166.78.68.0/22 permit
|
166.78.68.0/22 permit
|
||||||
166.78.68.221 permit
|
166.78.68.221 permit
|
||||||
166.78.69.169 permit
|
166.78.69.169 permit
|
||||||
@@ -1639,9 +1665,12 @@
|
|||||||
167.89.75.126 permit
|
167.89.75.126 permit
|
||||||
167.89.75.136 permit
|
167.89.75.136 permit
|
||||||
167.89.75.164 permit
|
167.89.75.164 permit
|
||||||
167.89.101.2 permit
|
|
||||||
167.89.101.192/28 permit
|
|
||||||
167.220.67.232/29 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.5.36 permit
|
||||||
168.138.73.51 permit
|
168.138.73.51 permit
|
||||||
168.138.77.31 permit
|
168.138.77.31 permit
|
||||||
@@ -1658,17 +1687,17 @@
|
|||||||
169.148.144.0/25 permit
|
169.148.144.0/25 permit
|
||||||
169.148.144.10 permit
|
169.148.144.10 permit
|
||||||
169.148.146.0/23 permit
|
169.148.146.0/23 permit
|
||||||
|
169.148.174.10 permit
|
||||||
169.148.175.3 permit
|
169.148.175.3 permit
|
||||||
|
169.148.179.3 permit
|
||||||
169.148.188.0/24 permit
|
169.148.188.0/24 permit
|
||||||
169.148.188.182 permit
|
169.148.188.182 permit
|
||||||
|
170.9.232.254 permit
|
||||||
170.10.128.0/24 permit
|
170.10.128.0/24 permit
|
||||||
170.10.129.0/24 permit
|
170.10.129.0/24 permit
|
||||||
170.10.132.56/29 permit
|
170.10.132.56/29 permit
|
||||||
170.10.132.64/29 permit
|
170.10.132.64/29 permit
|
||||||
170.10.133.0/24 permit
|
170.10.133.0/24 permit
|
||||||
172.217.32.0/20 permit
|
|
||||||
172.253.56.0/21 permit
|
|
||||||
172.253.112.0/20 permit
|
|
||||||
173.0.84.0/29 permit
|
173.0.84.0/29 permit
|
||||||
173.0.84.224/27 permit
|
173.0.84.224/27 permit
|
||||||
173.0.94.244/30 permit
|
173.0.94.244/30 permit
|
||||||
@@ -1677,6 +1706,7 @@
|
|||||||
173.203.81.39 permit
|
173.203.81.39 permit
|
||||||
173.224.161.128/25 permit
|
173.224.161.128/25 permit
|
||||||
173.224.165.0/26 permit
|
173.224.165.0/26 permit
|
||||||
|
173.224.166.48/28 permit
|
||||||
174.36.84.8/29 permit
|
174.36.84.8/29 permit
|
||||||
174.36.84.16/29 permit
|
174.36.84.16/29 permit
|
||||||
174.36.84.32/29 permit
|
174.36.84.32/29 permit
|
||||||
@@ -1696,8 +1726,7 @@
|
|||||||
182.50.78.64/28 permit
|
182.50.78.64/28 permit
|
||||||
183.240.219.64/29 permit
|
183.240.219.64/29 permit
|
||||||
185.4.120.0/22 permit
|
185.4.120.0/22 permit
|
||||||
185.11.253.128/27 permit
|
185.11.255.144 permit
|
||||||
185.11.255.0/24 permit
|
|
||||||
185.12.80.0/22 permit
|
185.12.80.0/22 permit
|
||||||
185.28.196.0/22 permit
|
185.28.196.0/22 permit
|
||||||
185.58.84.93 permit
|
185.58.84.93 permit
|
||||||
@@ -1711,8 +1740,16 @@
|
|||||||
185.138.56.128/25 permit
|
185.138.56.128/25 permit
|
||||||
185.189.236.0/22 permit
|
185.189.236.0/22 permit
|
||||||
185.211.120.0/22 permit
|
185.211.120.0/22 permit
|
||||||
185.233.188.0/23 permit
|
185.233.188.68 permit
|
||||||
185.233.190.0/23 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.236.0/22 permit
|
||||||
185.250.239.148 permit
|
185.250.239.148 permit
|
||||||
185.250.239.168 permit
|
185.250.239.168 permit
|
||||||
@@ -1771,8 +1808,24 @@
|
|||||||
192.18.139.154 permit
|
192.18.139.154 permit
|
||||||
192.18.145.36 permit
|
192.18.145.36 permit
|
||||||
192.18.152.58 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.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.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.30.252.0/22 permit
|
||||||
192.161.144.0/20 permit
|
192.161.144.0/20 permit
|
||||||
192.162.87.0/24 permit
|
192.162.87.0/24 permit
|
||||||
@@ -1780,15 +1833,12 @@
|
|||||||
192.237.159.42 permit
|
192.237.159.42 permit
|
||||||
192.237.159.43 permit
|
192.237.159.43 permit
|
||||||
192.254.112.0/20 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.109.254.0/23 permit
|
||||||
193.122.128.100 permit
|
193.122.128.100 permit
|
||||||
193.123.56.63 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.191 permit
|
||||||
193.142.157.198 permit
|
193.142.157.198 permit
|
||||||
194.19.134.0/25 permit
|
194.19.134.0/25 permit
|
||||||
@@ -1812,6 +1862,7 @@
|
|||||||
194.97.212.12 permit
|
194.97.212.12 permit
|
||||||
194.106.220.0/23 permit
|
194.106.220.0/23 permit
|
||||||
194.113.24.0/22 permit
|
194.113.24.0/22 permit
|
||||||
|
194.113.42.0/26 permit
|
||||||
194.154.193.192/27 permit
|
194.154.193.192/27 permit
|
||||||
195.4.92.0/23 permit
|
195.4.92.0/23 permit
|
||||||
195.54.172.0/23 permit
|
195.54.172.0/23 permit
|
||||||
@@ -1825,6 +1876,7 @@
|
|||||||
198.61.254.21 permit
|
198.61.254.21 permit
|
||||||
198.61.254.231 permit
|
198.61.254.231 permit
|
||||||
198.178.234.57 permit
|
198.178.234.57 permit
|
||||||
|
198.202.211.1 permit
|
||||||
198.244.48.0/20 permit
|
198.244.48.0/20 permit
|
||||||
198.244.56.107 permit
|
198.244.56.107 permit
|
||||||
198.244.56.108 permit
|
198.244.56.108 permit
|
||||||
@@ -1908,7 +1960,6 @@
|
|||||||
204.14.232.64/28 permit
|
204.14.232.64/28 permit
|
||||||
204.14.234.64/28 permit
|
204.14.234.64/28 permit
|
||||||
204.75.142.0/24 permit
|
204.75.142.0/24 permit
|
||||||
204.79.197.212 permit
|
|
||||||
204.92.114.187 permit
|
204.92.114.187 permit
|
||||||
204.92.114.203 permit
|
204.92.114.203 permit
|
||||||
204.92.114.204/31 permit
|
204.92.114.204/31 permit
|
||||||
@@ -1936,24 +1987,13 @@
|
|||||||
206.165.246.80/29 permit
|
206.165.246.80/29 permit
|
||||||
206.191.224.0/19 permit
|
206.191.224.0/19 permit
|
||||||
206.246.157.1 permit
|
206.246.157.1 permit
|
||||||
207.46.4.128/25 permit
|
|
||||||
207.46.22.35 permit
|
207.46.22.35 permit
|
||||||
207.46.50.72 permit
|
207.46.50.72 permit
|
||||||
207.46.50.82 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.71 permit
|
||||||
207.46.52.79 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.38.0/24 permit
|
||||||
207.67.98.192/27 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.97.204.96/29 permit
|
||||||
207.126.144.0/20 permit
|
207.126.144.0/20 permit
|
||||||
207.171.160.0/19 permit
|
207.171.160.0/19 permit
|
||||||
@@ -1961,6 +2001,7 @@
|
|||||||
207.211.30.128/25 permit
|
207.211.30.128/25 permit
|
||||||
207.211.31.0/25 permit
|
207.211.31.0/25 permit
|
||||||
207.211.41.113 permit
|
207.211.41.113 permit
|
||||||
|
207.211.132.0/25 permit
|
||||||
207.218.90.0/24 permit
|
207.218.90.0/24 permit
|
||||||
207.218.90.122 permit
|
207.218.90.122 permit
|
||||||
207.250.68.0/24 permit
|
207.250.68.0/24 permit
|
||||||
@@ -1968,6 +2009,8 @@
|
|||||||
208.43.21.28/30 permit
|
208.43.21.28/30 permit
|
||||||
208.43.21.64/29 permit
|
208.43.21.64/29 permit
|
||||||
208.43.21.72/30 permit
|
208.43.21.72/30 permit
|
||||||
|
208.56.9.224 permit
|
||||||
|
208.56.13.196 permit
|
||||||
208.64.132.0/22 permit
|
208.64.132.0/22 permit
|
||||||
208.71.40.63 permit
|
208.71.40.63 permit
|
||||||
208.71.40.64/31 permit
|
208.71.40.64/31 permit
|
||||||
@@ -1994,6 +2037,7 @@
|
|||||||
208.71.42.214 permit
|
208.71.42.214 permit
|
||||||
208.72.249.240/29 permit
|
208.72.249.240/29 permit
|
||||||
208.75.120.0/22 permit
|
208.75.120.0/22 permit
|
||||||
|
208.76.62.0/23 permit
|
||||||
208.76.62.0/24 permit
|
208.76.62.0/24 permit
|
||||||
208.76.63.0/24 permit
|
208.76.63.0/24 permit
|
||||||
208.82.237.96/29 permit
|
208.82.237.96/29 permit
|
||||||
@@ -2102,14 +2146,10 @@
|
|||||||
212.227.126.225 permit
|
212.227.126.225 permit
|
||||||
212.227.126.226 permit
|
212.227.126.226 permit
|
||||||
212.227.126.227 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.139 permit
|
||||||
213.199.128.145 permit
|
213.199.128.145 permit
|
||||||
213.199.138.181 permit
|
213.199.138.181 permit
|
||||||
213.199.138.191 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.242 permit
|
||||||
216.17.150.251 permit
|
216.17.150.251 permit
|
||||||
216.24.224.0/20 permit
|
216.24.224.0/20 permit
|
||||||
@@ -2137,7 +2177,6 @@
|
|||||||
216.39.62.60/31 permit
|
216.39.62.60/31 permit
|
||||||
216.39.62.136/29 permit
|
216.39.62.136/29 permit
|
||||||
216.39.62.144/31 permit
|
216.39.62.144/31 permit
|
||||||
216.58.192.0/19 permit
|
|
||||||
216.66.217.240/29 permit
|
216.66.217.240/29 permit
|
||||||
216.71.138.33 permit
|
216.71.138.33 permit
|
||||||
216.71.152.207 permit
|
216.71.152.207 permit
|
||||||
@@ -2159,6 +2198,7 @@
|
|||||||
216.136.168.80/28 permit
|
216.136.168.80/28 permit
|
||||||
216.139.64.0/19 permit
|
216.139.64.0/19 permit
|
||||||
216.145.221.0/24 permit
|
216.145.221.0/24 permit
|
||||||
|
216.146.32.0/23 permit
|
||||||
216.146.32.0/24 permit
|
216.146.32.0/24 permit
|
||||||
216.146.33.0/24 permit
|
216.146.33.0/24 permit
|
||||||
216.198.0.0/18 permit
|
216.198.0.0/18 permit
|
||||||
@@ -2179,6 +2219,7 @@
|
|||||||
223.165.120.0/23 permit
|
223.165.120.0/23 permit
|
||||||
2001:0868:0100:0600::/64 permit
|
2001:0868:0100:0600::/64 permit
|
||||||
2001:4860:4000::/36 permit
|
2001:4860:4000::/36 permit
|
||||||
|
2001:4860:4864::/56 permit
|
||||||
2001:748:100:40::2:0/112 permit
|
2001:748:100:40::2:0/112 permit
|
||||||
2001:748:400:1300::3 permit
|
2001:748:400:1300::3 permit
|
||||||
2001:748:400:1300::4 permit
|
2001:748:400:1300::4 permit
|
||||||
@@ -2196,15 +2237,13 @@
|
|||||||
2001:748:400:3301::3 permit
|
2001:748:400:3301::3 permit
|
||||||
2001:748:400:3301::4 permit
|
2001:748:400:3301::4 permit
|
||||||
2404:6800:4000::/36 permit
|
2404:6800:4000::/36 permit
|
||||||
2603:1010:3:3::5b permit
|
2404:6800:4864::/56 permit
|
||||||
2603:1020:201:10::10f permit
|
2603:1061:14:75::1 permit
|
||||||
2603:1030:20e:3::23c permit
|
|
||||||
2603:1030:b:3::152 permit
|
|
||||||
2603:1030:c02:8::14 permit
|
|
||||||
2607:13c0:0001:0000:0000:0000:0000:7000/116 permit
|
2607:13c0:0001:0000:0000:0000:0000:7000/116 permit
|
||||||
2607:13c0:0002:0000:0000:0000:0000:1000/116 permit
|
2607:13c0:0002:0000:0000:0000:0000:1000/116 permit
|
||||||
2607:13c0:0004:0000:0000:0000:0000:0000/116 permit
|
2607:13c0:0004:0000:0000:0000:0000:0000/116 permit
|
||||||
2607:f8b0:4000::/36 permit
|
2607:f8b0:4000::/36 permit
|
||||||
|
2607:f8b0:4864::/56 permit
|
||||||
2620:109:c003:104::/64 permit
|
2620:109:c003:104::/64 permit
|
||||||
2620:109:c003:104::215 permit
|
2620:109:c003:104::215 permit
|
||||||
2620:109:c006:104::/64 permit
|
2620:109:c006:104::/64 permit
|
||||||
@@ -2217,5 +2256,6 @@
|
|||||||
2620:119:50c0:207::/64 permit
|
2620:119:50c0:207::/64 permit
|
||||||
2620:119:50c0:207::215 permit
|
2620:119:50c0:207::215 permit
|
||||||
2800:3f0:4000::/36 permit
|
2800:3f0:4000::/36 permit
|
||||||
|
2800:3f0:4864::/56 permit
|
||||||
49.12.4.251 permit # checks.mailcow.email
|
49.12.4.251 permit # checks.mailcow.email
|
||||||
2a01:4f8:c17:7906::10 permit # checks.mailcow.email
|
2a01:4f8:c17:7906::10 permit # checks.mailcow.email
|
||||||
|
|||||||
@@ -3,8 +3,7 @@ rules {
|
|||||||
backend = "http";
|
backend = "http";
|
||||||
url = "http://nginx:9081/pipe.php";
|
url = "http://nginx:9081/pipe.php";
|
||||||
selector = "reject_no_global_bl";
|
selector = "reject_no_global_bl";
|
||||||
formatter = "default";
|
formatter = "multipart";
|
||||||
meta_headers = true;
|
|
||||||
}
|
}
|
||||||
RLINFO {
|
RLINFO {
|
||||||
backend = "http";
|
backend = "http";
|
||||||
@@ -16,8 +15,7 @@ rules {
|
|||||||
backend = "http";
|
backend = "http";
|
||||||
url = "http://nginx:9081/pushover.php";
|
url = "http://nginx:9081/pushover.php";
|
||||||
selector = "mailcow_rcpt";
|
selector = "mailcow_rcpt";
|
||||||
formatter = "json";
|
formatter = "multipart";
|
||||||
meta_headers = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -146,8 +146,171 @@ rspamd_config:register_symbol({
|
|||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Helper function to parse IPv6 into 8 segments
|
||||||
|
local function ipv6_to_segments(ip_str)
|
||||||
|
-- Remove zone identifier if present (e.g., %eth0)
|
||||||
|
ip_str = ip_str:gsub("%%.*$", "")
|
||||||
|
|
||||||
|
local segments = {}
|
||||||
|
|
||||||
|
-- Handle :: compression
|
||||||
|
if ip_str:find('::') then
|
||||||
|
local before, after = ip_str:match('^(.*)::(.*)$')
|
||||||
|
before = before or ''
|
||||||
|
after = after or ''
|
||||||
|
|
||||||
|
local before_parts = {}
|
||||||
|
local after_parts = {}
|
||||||
|
|
||||||
|
if before ~= '' then
|
||||||
|
for seg in before:gmatch('[^:]+') do
|
||||||
|
table.insert(before_parts, tonumber(seg, 16) or 0)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if after ~= '' then
|
||||||
|
for seg in after:gmatch('[^:]+') do
|
||||||
|
table.insert(after_parts, tonumber(seg, 16) or 0)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Add before segments
|
||||||
|
for _, seg in ipairs(before_parts) do
|
||||||
|
table.insert(segments, seg)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Add compressed zeros
|
||||||
|
local zeros_needed = 8 - #before_parts - #after_parts
|
||||||
|
for i = 1, zeros_needed do
|
||||||
|
table.insert(segments, 0)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Add after segments
|
||||||
|
for _, seg in ipairs(after_parts) do
|
||||||
|
table.insert(segments, seg)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
-- No compression
|
||||||
|
for seg in ip_str:gmatch('[^:]+') do
|
||||||
|
table.insert(segments, tonumber(seg, 16) or 0)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Ensure we have exactly 8 segments
|
||||||
|
while #segments < 8 do
|
||||||
|
table.insert(segments, 0)
|
||||||
|
end
|
||||||
|
|
||||||
|
return segments
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Generate all common IPv6 notations
|
||||||
|
local function get_ipv6_variants(ip_str)
|
||||||
|
local variants = {}
|
||||||
|
local seen = {}
|
||||||
|
|
||||||
|
local function add_variant(v)
|
||||||
|
if v and not seen[v] then
|
||||||
|
table.insert(variants, v)
|
||||||
|
seen[v] = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- For IPv4, just return the original
|
||||||
|
if not ip_str:find(':') then
|
||||||
|
add_variant(ip_str)
|
||||||
|
return variants
|
||||||
|
end
|
||||||
|
|
||||||
|
local segments = ipv6_to_segments(ip_str)
|
||||||
|
|
||||||
|
-- 1. Fully expanded form (all zeros shown as 0000)
|
||||||
|
local expanded_parts = {}
|
||||||
|
for _, seg in ipairs(segments) do
|
||||||
|
table.insert(expanded_parts, string.format('%04x', seg))
|
||||||
|
end
|
||||||
|
add_variant(table.concat(expanded_parts, ':'))
|
||||||
|
|
||||||
|
-- 2. Standard form (no leading zeros, but all segments present)
|
||||||
|
local standard_parts = {}
|
||||||
|
for _, seg in ipairs(segments) do
|
||||||
|
table.insert(standard_parts, string.format('%x', seg))
|
||||||
|
end
|
||||||
|
add_variant(table.concat(standard_parts, ':'))
|
||||||
|
|
||||||
|
-- 3. Find all possible :: compressions
|
||||||
|
-- RFC 5952: compress the longest run of consecutive zeros
|
||||||
|
-- But we need to check all possibilities since Redis might have any form
|
||||||
|
|
||||||
|
-- Find all zero runs
|
||||||
|
local zero_runs = {}
|
||||||
|
local in_run = false
|
||||||
|
local run_start = 0
|
||||||
|
local run_length = 0
|
||||||
|
|
||||||
|
for i = 1, 8 do
|
||||||
|
if segments[i] == 0 then
|
||||||
|
if not in_run then
|
||||||
|
in_run = true
|
||||||
|
run_start = i
|
||||||
|
run_length = 1
|
||||||
|
else
|
||||||
|
run_length = run_length + 1
|
||||||
|
end
|
||||||
|
else
|
||||||
|
if in_run then
|
||||||
|
if run_length >= 1 then -- Allow single zero compression too
|
||||||
|
table.insert(zero_runs, {start = run_start, length = run_length})
|
||||||
|
end
|
||||||
|
in_run = false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Don't forget the last run
|
||||||
|
if in_run and run_length >= 1 then
|
||||||
|
table.insert(zero_runs, {start = run_start, length = run_length})
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Generate variant for each zero run compression
|
||||||
|
for _, run in ipairs(zero_runs) do
|
||||||
|
local parts = {}
|
||||||
|
|
||||||
|
-- Before compression
|
||||||
|
for i = 1, run.start - 1 do
|
||||||
|
table.insert(parts, string.format('%x', segments[i]))
|
||||||
|
end
|
||||||
|
|
||||||
|
-- The compression
|
||||||
|
if run.start == 1 then
|
||||||
|
table.insert(parts, '')
|
||||||
|
table.insert(parts, '')
|
||||||
|
elseif run.start + run.length - 1 == 8 then
|
||||||
|
table.insert(parts, '')
|
||||||
|
table.insert(parts, '')
|
||||||
|
else
|
||||||
|
table.insert(parts, '')
|
||||||
|
end
|
||||||
|
|
||||||
|
-- After compression
|
||||||
|
for i = run.start + run.length, 8 do
|
||||||
|
table.insert(parts, string.format('%x', segments[i]))
|
||||||
|
end
|
||||||
|
|
||||||
|
local compressed = table.concat(parts, ':'):gsub('::+', '::')
|
||||||
|
add_variant(compressed)
|
||||||
|
end
|
||||||
|
|
||||||
|
return variants
|
||||||
|
end
|
||||||
|
|
||||||
local from_ip_string = tostring(ip)
|
local from_ip_string = tostring(ip)
|
||||||
ip_check_table = {from_ip_string}
|
local ip_check_table = {}
|
||||||
|
|
||||||
|
-- Add all variants of the exact IP
|
||||||
|
for _, variant in ipairs(get_ipv6_variants(from_ip_string)) do
|
||||||
|
table.insert(ip_check_table, variant)
|
||||||
|
end
|
||||||
|
|
||||||
local maxbits = 128
|
local maxbits = 128
|
||||||
local minbits = 32
|
local minbits = 32
|
||||||
@@ -155,10 +318,18 @@ rspamd_config:register_symbol({
|
|||||||
maxbits = 32
|
maxbits = 32
|
||||||
minbits = 8
|
minbits = 8
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Add all CIDR notations with variants
|
||||||
for i=maxbits,minbits,-1 do
|
for i=maxbits,minbits,-1 do
|
||||||
local nip = ip:apply_mask(i):to_string() .. "/" .. i
|
local masked_ip = ip:apply_mask(i)
|
||||||
table.insert(ip_check_table, nip)
|
local cidr_base = masked_ip:to_string()
|
||||||
|
|
||||||
|
for _, variant in ipairs(get_ipv6_variants(cidr_base)) do
|
||||||
|
local cidr = variant .. "/" .. i
|
||||||
|
table.insert(ip_check_table, cidr)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local function keep_spam_cb(err, data)
|
local function keep_spam_cb(err, data)
|
||||||
if err then
|
if err then
|
||||||
rspamd_logger.infox(rspamd_config, "keep_spam query request for ip %s returned invalid or empty data (\"%s\") or error (\"%s\")", ip, data, err)
|
rspamd_logger.infox(rspamd_config, "keep_spam query request for ip %s returned invalid or empty data (\"%s\") or error (\"%s\")", ip, data, err)
|
||||||
@@ -166,12 +337,15 @@ rspamd_config:register_symbol({
|
|||||||
else
|
else
|
||||||
for k,v in pairs(data) do
|
for k,v in pairs(data) do
|
||||||
if (v and v ~= userdata and v == '1') then
|
if (v and v ~= userdata and v == '1') then
|
||||||
rspamd_logger.infox(rspamd_config, "found ip in keep_spam map, setting pre-result")
|
rspamd_logger.infox(rspamd_config, "found ip %s (checked as: %s) in keep_spam map, setting pre-result accept", from_ip_string, ip_check_table[k])
|
||||||
task:set_pre_result('accept', 'ip matched with forward hosts', 'keep_spam')
|
task:set_pre_result('accept', 'ip matched with forward hosts', 'keep_spam')
|
||||||
|
task:set_flag('no_stat')
|
||||||
|
return
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
table.insert(ip_check_table, 1, 'KEEP_SPAM')
|
table.insert(ip_check_table, 1, 'KEEP_SPAM')
|
||||||
local redis_ret_user = rspamd_redis_make_request(task,
|
local redis_ret_user = rspamd_redis_make_request(task,
|
||||||
redis_params, -- connect params
|
redis_params, -- connect params
|
||||||
@@ -210,6 +384,7 @@ rspamd_config:register_symbol({
|
|||||||
rspamd_config:register_symbol({
|
rspamd_config:register_symbol({
|
||||||
name = 'TAG_MOO',
|
name = 'TAG_MOO',
|
||||||
type = 'postfilter',
|
type = 'postfilter',
|
||||||
|
flags = 'ignore_passthrough',
|
||||||
callback = function(task)
|
callback = function(task)
|
||||||
local util = require("rspamd_util")
|
local util = require("rspamd_util")
|
||||||
local rspamd_logger = require "rspamd_logger"
|
local rspamd_logger = require "rspamd_logger"
|
||||||
@@ -217,9 +392,7 @@ rspamd_config:register_symbol({
|
|||||||
local rspamd_http = require "rspamd_http"
|
local rspamd_http = require "rspamd_http"
|
||||||
local rcpts = task:get_recipients('smtp')
|
local rcpts = task:get_recipients('smtp')
|
||||||
local lua_util = require "lua_util"
|
local lua_util = require "lua_util"
|
||||||
|
|
||||||
local tagged_rcpt = task:get_symbol("TAGGED_RCPT")
|
local tagged_rcpt = task:get_symbol("TAGGED_RCPT")
|
||||||
local mailcow_domain = task:get_symbol("RCPT_MAILCOW_DOMAIN")
|
|
||||||
|
|
||||||
local function remove_moo_tag()
|
local function remove_moo_tag()
|
||||||
local moo_tag_header = task:get_header('X-Moo-Tag', false)
|
local moo_tag_header = task:get_header('X-Moo-Tag', false)
|
||||||
@@ -231,101 +404,147 @@ rspamd_config:register_symbol({
|
|||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
|
|
||||||
if tagged_rcpt and tagged_rcpt[1].options and mailcow_domain then
|
-- Check if we have exactly one recipient
|
||||||
local tag = tagged_rcpt[1].options[1]
|
if not (rcpts and #rcpts == 1) then
|
||||||
rspamd_logger.infox("found tag: %s", tag)
|
rspamd_logger.infox("TAG_MOO: not exactly one rcpt (%s), removing moo tag", rcpts and #rcpts or 0)
|
||||||
local action = task:get_metric_action('default')
|
remove_moo_tag()
|
||||||
rspamd_logger.infox("metric action now: %s", action)
|
return
|
||||||
|
end
|
||||||
|
|
||||||
if action ~= 'no action' and action ~= 'greylist' then
|
local rcpt_addr = rcpts[1]['addr']
|
||||||
rspamd_logger.infox("skipping tag handler for action: %s", action)
|
local rcpt_user = rcpts[1]['user']
|
||||||
remove_moo_tag()
|
local rcpt_domain = rcpts[1]['domain']
|
||||||
return true
|
|
||||||
|
-- Check if recipient has a tag (contains '+')
|
||||||
|
local tag = nil
|
||||||
|
if tagged_rcpt ~= nil then
|
||||||
|
tag = tagged_rcpt
|
||||||
|
rspamd_logger.infox("TAG_MOO: found tag in recipient: %s (base: %s, tag: %s)", rcpt_addr, base_user, tag)
|
||||||
|
end
|
||||||
|
|
||||||
|
if not tag then
|
||||||
|
rspamd_logger.infox("TAG_MOO: no tag found in recipient %s, removing moo tag", rcpt_addr)
|
||||||
|
remove_moo_tag()
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Optional: Check if domain is a mailcow domain
|
||||||
|
-- When KEEP_SPAM is active, RCPT_MAILCOW_DOMAIN might not be set
|
||||||
|
-- If the mail is being delivered, we can assume it's valid
|
||||||
|
local mailcow_domain = task:get_symbol("RCPT_MAILCOW_DOMAIN")
|
||||||
|
if not mailcow_domain then
|
||||||
|
rspamd_logger.infox("TAG_MOO: RCPT_MAILCOW_DOMAIN not set (possibly due to pre-result), proceeding anyway for domain %s", rcpt_domain)
|
||||||
|
end
|
||||||
|
|
||||||
|
local action = task:get_metric_action('default')
|
||||||
|
rspamd_logger.infox("TAG_MOO: metric action: %s", action)
|
||||||
|
|
||||||
|
-- Check if we have a pre-result (e.g., from KEEP_SPAM or POSTMASTER_HANDLER)
|
||||||
|
local allow_processing = false
|
||||||
|
|
||||||
|
if task.has_pre_result then
|
||||||
|
local has_pre, pre_action = task:has_pre_result()
|
||||||
|
if has_pre then
|
||||||
|
rspamd_logger.infox("TAG_MOO: pre-result detected: %s", tostring(pre_action))
|
||||||
|
if pre_action == 'accept' then
|
||||||
|
allow_processing = true
|
||||||
|
rspamd_logger.infox("TAG_MOO: pre-result is accept, will process")
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
local function http_callback(err_message, code, body, headers)
|
-- Allow processing for mild actions or when we have pre-result accept
|
||||||
if body ~= nil and body ~= "" then
|
if not allow_processing and action ~= 'no action' and action ~= 'greylist' then
|
||||||
rspamd_logger.infox(rspamd_config, "expanding rcpt to \"%s\"", body)
|
rspamd_logger.infox("TAG_MOO: skipping tag handler for action: %s", action)
|
||||||
|
remove_moo_tag()
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
local function tag_callback_subject(err, data)
|
rspamd_logger.infox("TAG_MOO: processing allowed")
|
||||||
if err or type(data) ~= 'string' then
|
|
||||||
rspamd_logger.infox(rspamd_config, "subject tag handler rcpt %s returned invalid or empty data (\"%s\") or error (\"%s\") - trying subfolder tag handler...", body, data, err)
|
|
||||||
|
|
||||||
local function tag_callback_subfolder(err, data)
|
local function http_callback(err_message, code, body, headers)
|
||||||
if err or type(data) ~= 'string' then
|
if body ~= nil and body ~= "" then
|
||||||
rspamd_logger.infox(rspamd_config, "subfolder tag handler for rcpt %s returned invalid or empty data (\"%s\") or error (\"%s\")", body, data, err)
|
rspamd_logger.infox(rspamd_config, "TAG_MOO: expanding rcpt to \"%s\"", body)
|
||||||
remove_moo_tag()
|
|
||||||
else
|
|
||||||
rspamd_logger.infox("Add X-Moo-Tag header")
|
|
||||||
task:set_milter_reply({
|
|
||||||
add_headers = {['X-Moo-Tag'] = 'YES'}
|
|
||||||
})
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local redis_ret_subfolder = rspamd_redis_make_request(task,
|
local function tag_callback_subject(err, data)
|
||||||
redis_params, -- connect params
|
if err or type(data) ~= 'string' or data == '' then
|
||||||
body, -- hash key
|
rspamd_logger.infox(rspamd_config, "TAG_MOO: subject tag handler rcpt %s returned invalid or empty data (\"%s\") or error (\"%s\") - trying subfolder tag handler...", body, data, err)
|
||||||
false, -- is write
|
|
||||||
tag_callback_subfolder, --callback
|
local function tag_callback_subfolder(err, data)
|
||||||
'HGET', -- command
|
if err or type(data) ~= 'string' or data == '' then
|
||||||
{'RCPT_WANTS_SUBFOLDER_TAG', body} -- arguments
|
rspamd_logger.infox(rspamd_config, "TAG_MOO: subfolder tag handler for rcpt %s returned invalid or empty data (\"%s\") or error (\"%s\")", body, data, err)
|
||||||
)
|
|
||||||
if not redis_ret_subfolder then
|
|
||||||
rspamd_logger.infox(rspamd_config, "cannot make request to load tag handler for rcpt")
|
|
||||||
remove_moo_tag()
|
remove_moo_tag()
|
||||||
|
else
|
||||||
|
rspamd_logger.infox("TAG_MOO: User wants subfolder tag, adding X-Moo-Tag header")
|
||||||
|
task:set_milter_reply({
|
||||||
|
add_headers = {['X-Moo-Tag'] = 'YES'}
|
||||||
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
else
|
|
||||||
rspamd_logger.infox("user wants subject modified for tagged mail")
|
|
||||||
local sbj = task:get_header('Subject')
|
|
||||||
new_sbj = '=?UTF-8?B?' .. tostring(util.encode_base64('[' .. tag .. '] ' .. sbj)) .. '?='
|
|
||||||
task:set_milter_reply({
|
|
||||||
remove_headers = {
|
|
||||||
['Subject'] = 1,
|
|
||||||
['X-Moo-Tag'] = 0
|
|
||||||
},
|
|
||||||
add_headers = {['Subject'] = new_sbj}
|
|
||||||
})
|
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
local redis_ret_subject = rspamd_redis_make_request(task,
|
local redis_ret_subfolder = rspamd_redis_make_request(task,
|
||||||
redis_params, -- connect params
|
redis_params, -- connect params
|
||||||
body, -- hash key
|
body, -- hash key
|
||||||
false, -- is write
|
false, -- is write
|
||||||
tag_callback_subject, --callback
|
tag_callback_subfolder, --callback
|
||||||
'HGET', -- command
|
'HGET', -- command
|
||||||
{'RCPT_WANTS_SUBJECT_TAG', body} -- arguments
|
{'RCPT_WANTS_SUBFOLDER_TAG', body} -- arguments
|
||||||
)
|
)
|
||||||
if not redis_ret_subject then
|
if not redis_ret_subfolder then
|
||||||
rspamd_logger.infox(rspamd_config, "cannot make request to load tag handler for rcpt")
|
rspamd_logger.infox(rspamd_config, "TAG_MOO: cannot make request to load tag handler for rcpt")
|
||||||
remove_moo_tag()
|
|
||||||
end
|
|
||||||
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if rcpts and #rcpts == 1 then
|
|
||||||
for _,rcpt in ipairs(rcpts) do
|
|
||||||
local rcpt_split = rspamd_str_split(rcpt['addr'], '@')
|
|
||||||
if #rcpt_split == 2 then
|
|
||||||
if rcpt_split[1] == 'postmaster' then
|
|
||||||
rspamd_logger.infox(rspamd_config, "not expanding postmaster alias")
|
|
||||||
remove_moo_tag()
|
remove_moo_tag()
|
||||||
else
|
|
||||||
rspamd_http.request({
|
|
||||||
task=task,
|
|
||||||
url='http://nginx:8081/aliasexp.php',
|
|
||||||
body='',
|
|
||||||
callback=http_callback,
|
|
||||||
headers={Rcpt=rcpt['addr']},
|
|
||||||
})
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
else
|
||||||
|
rspamd_logger.infox("TAG_MOO: user wants subject modified for tagged mail")
|
||||||
|
local sbj = task:get_header('Subject') or ''
|
||||||
|
local tag_value = tag[1] and tag[1].options and tag[1].options[1] or ''
|
||||||
|
new_sbj = '=?UTF-8?B?' .. tostring(util.encode_base64('[' .. tag_value .. '] ' .. sbj)) .. '?='
|
||||||
|
task:set_milter_reply({
|
||||||
|
remove_headers = {
|
||||||
|
['Subject'] = 1,
|
||||||
|
['X-Moo-Tag'] = 0
|
||||||
|
},
|
||||||
|
add_headers = {['Subject'] = new_sbj}
|
||||||
|
})
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local redis_ret_subject = rspamd_redis_make_request(task,
|
||||||
|
redis_params, -- connect params
|
||||||
|
body, -- hash key
|
||||||
|
false, -- is write
|
||||||
|
tag_callback_subject, --callback
|
||||||
|
'HGET', -- command
|
||||||
|
{'RCPT_WANTS_SUBJECT_TAG', body} -- arguments
|
||||||
|
)
|
||||||
|
if not redis_ret_subject then
|
||||||
|
rspamd_logger.infox(rspamd_config, "TAG_MOO: cannot make request to load tag handler for rcpt")
|
||||||
|
remove_moo_tag()
|
||||||
|
end
|
||||||
|
else
|
||||||
|
rspamd_logger.infox("TAG_MOO: alias expansion returned empty body")
|
||||||
|
remove_moo_tag()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local rcpt_split = rspamd_str_split(rcpt_addr, '@')
|
||||||
|
if #rcpt_split == 2 then
|
||||||
|
if rcpt_split[1]:match('^postmaster') then
|
||||||
|
rspamd_logger.infox(rspamd_config, "TAG_MOO: not expanding postmaster alias")
|
||||||
|
remove_moo_tag()
|
||||||
|
else
|
||||||
|
rspamd_logger.infox("TAG_MOO: requesting alias expansion for %s", rcpt_addr)
|
||||||
|
rspamd_http.request({
|
||||||
|
task=task,
|
||||||
|
url='http://nginx:8081/aliasexp.php',
|
||||||
|
body='',
|
||||||
|
callback=http_callback,
|
||||||
|
headers={Rcpt=rcpt_addr},
|
||||||
|
})
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
|
rspamd_logger.infox("TAG_MOO: invalid rcpt format")
|
||||||
remove_moo_tag()
|
remove_moo_tag()
|
||||||
end
|
end
|
||||||
end,
|
end,
|
||||||
@@ -335,6 +554,7 @@ rspamd_config:register_symbol({
|
|||||||
rspamd_config:register_symbol({
|
rspamd_config:register_symbol({
|
||||||
name = 'BCC',
|
name = 'BCC',
|
||||||
type = 'postfilter',
|
type = 'postfilter',
|
||||||
|
flags = 'ignore_passthrough',
|
||||||
callback = function(task)
|
callback = function(task)
|
||||||
local util = require("rspamd_util")
|
local util = require("rspamd_util")
|
||||||
local rspamd_http = require "rspamd_http"
|
local rspamd_http = require "rspamd_http"
|
||||||
@@ -363,11 +583,13 @@ rspamd_config:register_symbol({
|
|||||||
local email_content = tostring(task:get_content())
|
local email_content = tostring(task:get_content())
|
||||||
email_content = string.gsub(email_content, "\r\n%.", "\r\n..")
|
email_content = string.gsub(email_content, "\r\n%.", "\r\n..")
|
||||||
-- send mail
|
-- send mail
|
||||||
|
local from_smtp = task:get_from('smtp')
|
||||||
|
local from_addr = (from_smtp and from_smtp[1] and from_smtp[1].addr) or 'mailer-daemon@localhost'
|
||||||
lua_smtp.sendmail({
|
lua_smtp.sendmail({
|
||||||
task = task,
|
task = task,
|
||||||
host = os.getenv("IPV4_NETWORK") .. '.253',
|
host = os.getenv("IPV4_NETWORK") .. '.253',
|
||||||
port = 591,
|
port = 591,
|
||||||
from = task:get_from(stp)[1].addr,
|
from = from_addr,
|
||||||
recipients = bcc_dest,
|
recipients = bcc_dest,
|
||||||
helo = 'bcc',
|
helo = 'bcc',
|
||||||
timeout = 20,
|
timeout = 20,
|
||||||
@@ -397,27 +619,41 @@ rspamd_config:register_symbol({
|
|||||||
end
|
end
|
||||||
|
|
||||||
local action = task:get_metric_action('default')
|
local action = task:get_metric_action('default')
|
||||||
rspamd_logger.infox("metric action now: %s", action)
|
rspamd_logger.infox("BCC: metric action: %s", action)
|
||||||
|
|
||||||
|
-- Check for pre-result accept (e.g., from KEEP_SPAM)
|
||||||
|
local allow_bcc = false
|
||||||
|
if task.has_pre_result then
|
||||||
|
local has_pre, pre_action = task:has_pre_result()
|
||||||
|
if has_pre and pre_action == 'accept' then
|
||||||
|
allow_bcc = true
|
||||||
|
rspamd_logger.infox("BCC: pre-result accept detected, will send BCC")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Allow BCC for mild actions or when we have pre-result accept
|
||||||
|
if not allow_bcc and action ~= 'no action' and action ~= 'add header' and action ~= 'rewrite subject' then
|
||||||
|
rspamd_logger.infox("BCC: skipping for action: %s", action)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
local function rcpt_callback(err_message, code, body, headers)
|
local function rcpt_callback(err_message, code, body, headers)
|
||||||
if err_message == nil and code == 201 and body ~= nil then
|
if err_message == nil and code == 201 and body ~= nil then
|
||||||
if action == 'no action' or action == 'add header' or action == 'rewrite subject' then
|
rspamd_logger.infox("BCC: sending BCC to %s for rcpt match", body)
|
||||||
send_mail(task, body)
|
send_mail(task, body)
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local function from_callback(err_message, code, body, headers)
|
local function from_callback(err_message, code, body, headers)
|
||||||
if err_message == nil and code == 201 and body ~= nil then
|
if err_message == nil and code == 201 and body ~= nil then
|
||||||
if action == 'no action' or action == 'add header' or action == 'rewrite subject' then
|
rspamd_logger.infox("BCC: sending BCC to %s for from match", body)
|
||||||
send_mail(task, body)
|
send_mail(task, body)
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
if rcpt_table then
|
if rcpt_table then
|
||||||
for _,e in ipairs(rcpt_table) do
|
for _,e in ipairs(rcpt_table) do
|
||||||
rspamd_logger.infox(rspamd_config, "checking bcc for rcpt address %s", e)
|
rspamd_logger.infox(rspamd_config, "BCC: checking bcc for rcpt address %s", e)
|
||||||
rspamd_http.request({
|
rspamd_http.request({
|
||||||
task=task,
|
task=task,
|
||||||
url='http://nginx:8081/bcc.php',
|
url='http://nginx:8081/bcc.php',
|
||||||
@@ -430,7 +666,7 @@ rspamd_config:register_symbol({
|
|||||||
|
|
||||||
if from_table then
|
if from_table then
|
||||||
for _,e in ipairs(from_table) do
|
for _,e in ipairs(from_table) do
|
||||||
rspamd_logger.infox(rspamd_config, "checking bcc for from address %s", e)
|
rspamd_logger.infox(rspamd_config, "BCC: checking bcc for from address %s", e)
|
||||||
rspamd_http.request({
|
rspamd_http.request({
|
||||||
task=task,
|
task=task,
|
||||||
url='http://nginx:8081/bcc.php',
|
url='http://nginx:8081/bcc.php',
|
||||||
@@ -441,7 +677,7 @@ rspamd_config:register_symbol({
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
return true
|
-- Don't return true to avoid symbol being logged
|
||||||
end,
|
end,
|
||||||
priority = 20
|
priority = 20
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -32,47 +32,42 @@ function parse_email($email) {
|
|||||||
$a = strrpos($email, '@');
|
$a = strrpos($email, '@');
|
||||||
return array('local' => substr($email, 0, $a), 'domain' => substr(substr($email, $a), 1));
|
return array('local' => substr($email, 0, $a), 'domain' => substr(substr($email, $a), 1));
|
||||||
}
|
}
|
||||||
if (!function_exists('getallheaders')) {
|
// rspamd metadata_exporter (multipart formatter):
|
||||||
function getallheaders() {
|
// - $_POST['metadata'] JSON with the rspamd metadata
|
||||||
if (!is_array($_SERVER)) {
|
// - $_FILES['message'] raw RFC822 message
|
||||||
return array();
|
if (empty($_POST['metadata']) || !isset($_FILES['message']) || $_FILES['message']['error'] !== UPLOAD_ERR_OK) {
|
||||||
}
|
error_log("QUARANTINE: missing multipart parts from rspamd" . PHP_EOL);
|
||||||
$headers = array();
|
http_response_code(400);
|
||||||
foreach ($_SERVER as $name => $value) {
|
exit;
|
||||||
if (substr($name, 0, 5) == 'HTTP_') {
|
|
||||||
$headers[str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($name, 5)))))] = $value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return $headers;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$raw_data_content = file_get_contents('php://input');
|
$meta = json_decode($_POST['metadata'], true);
|
||||||
|
if (!is_array($meta)) {
|
||||||
|
error_log("QUARANTINE: cannot decode metadata JSON" . PHP_EOL);
|
||||||
|
http_response_code(400);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$raw_data_content = file_get_contents($_FILES['message']['tmp_name']);
|
||||||
$raw_data = mb_convert_encoding($raw_data_content, 'HTML-ENTITIES', "UTF-8");
|
$raw_data = mb_convert_encoding($raw_data_content, 'HTML-ENTITIES', "UTF-8");
|
||||||
$headers = getallheaders();
|
$raw_size = (int)$_FILES['message']['size'];
|
||||||
|
|
||||||
$qid = $headers['X-Rspamd-Qid'];
|
$qid = $meta['qid'] ?? 'unknown';
|
||||||
$fuzzy = $headers['X-Rspamd-Fuzzy'];
|
$subject = $meta['subject'] ?? '';
|
||||||
$subject = iconv_mime_decode($headers['X-Rspamd-Subject']);
|
$score = $meta['score'] ?? 0;
|
||||||
$score = $headers['X-Rspamd-Score'];
|
$rcpts = $meta['rcpt'] ?? array();
|
||||||
$rcpts = $headers['X-Rspamd-Rcpt'];
|
$user = $meta['user'] ?? 'unknown';
|
||||||
$user = $headers['X-Rspamd-User'];
|
$ip = $meta['ip'] ?? 'unknown';
|
||||||
$ip = $headers['X-Rspamd-Ip'];
|
$action = $meta['action'] ?? 'no action';
|
||||||
$action = $headers['X-Rspamd-Action'];
|
$sender = $meta['from'] ?? '';
|
||||||
$sender = $headers['X-Rspamd-From'];
|
$symbols = json_encode($meta['symbols'] ?? array());
|
||||||
$symbols = $headers['X-Rspamd-Symbols'];
|
$fuzzy = json_encode(is_array($meta['fuzzy'] ?? null) ? $meta['fuzzy'] : array());
|
||||||
|
|
||||||
$raw_size = (int)$_SERVER['CONTENT_LENGTH'];
|
|
||||||
|
|
||||||
if (empty($sender)) {
|
if (empty($sender)) {
|
||||||
error_log("QUARANTINE: Unknown sender, assuming empty-env-from@localhost" . PHP_EOL);
|
error_log("QUARANTINE: Unknown sender, assuming empty-env-from@localhost" . PHP_EOL);
|
||||||
$sender = 'empty-env-from@localhost';
|
$sender = 'empty-env-from@localhost';
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($fuzzy == 'unknown') {
|
|
||||||
$fuzzy = '[]';
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$max_size = (int)$redis->Get('Q_MAX_SIZE');
|
$max_size = (int)$redis->Get('Q_MAX_SIZE');
|
||||||
if (($max_size * 1048576) < $raw_size) {
|
if (($max_size * 1048576) < $raw_size) {
|
||||||
@@ -94,7 +89,7 @@ catch (RedisException $e) {
|
|||||||
$rcpt_final_mailboxes = array();
|
$rcpt_final_mailboxes = array();
|
||||||
|
|
||||||
// Loop through all rcpts
|
// Loop through all rcpts
|
||||||
foreach (json_decode($rcpts, true) as $rcpt) {
|
foreach ($rcpts as $rcpt) {
|
||||||
// Remove tag
|
// Remove tag
|
||||||
$rcpt = preg_replace('/^(.*?)\+.*(@.*)$/', '$1$2', $rcpt);
|
$rcpt = preg_replace('/^(.*?)\+.*(@.*)$/', '$1$2', $rcpt);
|
||||||
|
|
||||||
|
|||||||
@@ -32,50 +32,46 @@ function parse_email($email) {
|
|||||||
$a = strrpos($email, '@');
|
$a = strrpos($email, '@');
|
||||||
return array('local' => substr($email, 0, $a), 'domain' => substr(substr($email, $a), 1));
|
return array('local' => substr($email, 0, $a), 'domain' => substr(substr($email, $a), 1));
|
||||||
}
|
}
|
||||||
if (!function_exists('getallheaders')) {
|
// rspamd metadata_exporter (multipart formatter): metadata JSON arrives as $_POST['metadata'].
|
||||||
function getallheaders() {
|
if (empty($_POST['metadata'])) {
|
||||||
if (!is_array($_SERVER)) {
|
error_log("NOTIFY: missing metadata part from rspamd" . PHP_EOL);
|
||||||
return array();
|
http_response_code(400);
|
||||||
}
|
exit;
|
||||||
$headers = array();
|
|
||||||
foreach ($_SERVER as $name => $value) {
|
|
||||||
if (substr($name, 0, 5) == 'HTTP_') {
|
|
||||||
$headers[str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($name, 5)))))] = $value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return $headers;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$headers = getallheaders();
|
$meta = json_decode($_POST['metadata'], true);
|
||||||
$json_body = json_decode(file_get_contents('php://input'));
|
if (!is_array($meta)) {
|
||||||
|
error_log("NOTIFY: cannot decode metadata JSON" . PHP_EOL);
|
||||||
|
http_response_code(400);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
$qid = $headers['X-Rspamd-Qid'];
|
$qid = $meta['qid'] ?? 'unknown';
|
||||||
$rcpts = $headers['X-Rspamd-Rcpt'];
|
$rcpts = $meta['rcpt'] ?? array();
|
||||||
$sender = $headers['X-Rspamd-From'];
|
$sender = $meta['from'] ?? '';
|
||||||
$ip = $headers['X-Rspamd-Ip'];
|
$ip = $meta['ip'] ?? 'unknown';
|
||||||
$subject = iconv_mime_decode($headers['X-Rspamd-Subject']);
|
$subject = $meta['subject'] ?? '';
|
||||||
$messageid= $json_body->message_id;
|
$messageid= $meta['message_id'] ?? '';
|
||||||
$priority = 0;
|
$priority = 0;
|
||||||
|
|
||||||
$symbols_array = json_decode($headers['X-Rspamd-Symbols'], true);
|
$symbols_array = $meta['symbols'] ?? array();
|
||||||
if (is_array($symbols_array)) {
|
if (is_array($symbols_array)) {
|
||||||
foreach ($symbols_array as $symbol) {
|
foreach ($symbols_array as $symbol) {
|
||||||
if ($symbol['name'] == 'HAS_X_PRIO_ONE') {
|
if (($symbol['name'] ?? null) == 'HAS_X_PRIO_ONE') {
|
||||||
$priority = 1;
|
$priority = 1;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$sender_address = $json_body->header_from[0];
|
$sender_address = $meta['header_from'][0] ?? '';
|
||||||
$sender_name = '-';
|
$sender_name = '-';
|
||||||
if (preg_match('/(?<name>.*?)<(?<address>.*?)>/i', $sender_address, $matches)) {
|
if (preg_match('/(?<name>.*?)<(?<address>.*?)>/i', $sender_address, $matches)) {
|
||||||
$sender_address = $matches['address'];
|
$sender_address = $matches['address'];
|
||||||
$sender_name = trim($matches['name'], '"\' ');
|
$sender_name = trim($matches['name'], '"\' ');
|
||||||
}
|
}
|
||||||
|
|
||||||
$to_address = $json_body->header_to[0];
|
$to_address = $meta['header_to'][0] ?? '';
|
||||||
$to_name = '-';
|
$to_name = '-';
|
||||||
if (preg_match('/(?<name>.*?)<(?<address>.*?)>/i', $to_address, $matches)) {
|
if (preg_match('/(?<name>.*?)<(?<address>.*?)>/i', $to_address, $matches)) {
|
||||||
$to_address = $matches['address'];
|
$to_address = $matches['address'];
|
||||||
@@ -85,7 +81,7 @@ if (preg_match('/(?<name>.*?)<(?<address>.*?)>/i', $to_address, $matches)) {
|
|||||||
$rcpt_final_mailboxes = array();
|
$rcpt_final_mailboxes = array();
|
||||||
|
|
||||||
// Loop through all rcpts
|
// Loop through all rcpts
|
||||||
foreach (json_decode($rcpts, true) as $rcpt) {
|
foreach ($rcpts as $rcpt) {
|
||||||
// Remove tag
|
// Remove tag
|
||||||
$rcpt = preg_replace('/^(.*?)\+.*(@.*)$/', '$1$2', $rcpt);
|
$rcpt = preg_replace('/^(.*?)\+.*(@.*)$/', '$1$2', $rcpt);
|
||||||
|
|
||||||
|
|||||||
@@ -86,6 +86,12 @@
|
|||||||
SOGoMaximumFailedLoginInterval = 900;
|
SOGoMaximumFailedLoginInterval = 900;
|
||||||
SOGoFailedLoginBlockInterval = 900;
|
SOGoFailedLoginBlockInterval = 900;
|
||||||
|
|
||||||
|
// Enable SOGo URL Description for GDPR compliance, this may cause some issues with calendars and contacts. Also uncomment the encryption key below to use it.
|
||||||
|
//SOGoURLEncryptionEnabled = NO;
|
||||||
|
|
||||||
|
// Set a 16 character encryption key for SOGo URL Description, change this to your own value
|
||||||
|
//SOGoURLPathEncryptionKey = "SOGoSuperSecret0";
|
||||||
|
|
||||||
GCSChannelCollectionTimer = 60;
|
GCSChannelCollectionTimer = 60;
|
||||||
GCSChannelExpireAge = 60;
|
GCSChannelExpireAge = 60;
|
||||||
|
|
||||||
|
|||||||
@@ -2,18 +2,7 @@
|
|||||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
|
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
|
||||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.admin.inc.php';
|
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.admin.inc.php';
|
||||||
|
|
||||||
if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'domainadmin') {
|
protect_route(['admin']);
|
||||||
header('Location: /domainadmin/mailbox');
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user') {
|
|
||||||
header('Location: /user');
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
elseif (!isset($_SESSION['mailcow_cc_role']) || $_SESSION['mailcow_cc_role'] != "admin") {
|
|
||||||
header('Location: /admin');
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
|
|
||||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php';
|
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php';
|
||||||
$_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
|
$_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
|
||||||
|
|||||||
@@ -3,8 +3,11 @@ require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
|
|||||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.admin.inc.php';
|
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.admin.inc.php';
|
||||||
|
|
||||||
if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'admin') {
|
if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'admin') {
|
||||||
header('Location: /admin/dashboard');
|
// Only redirect to dashboard if NO pending actions
|
||||||
exit();
|
if (empty($_SESSION['pending_tfa_setup']) && empty($_SESSION['pending_pw_update'])) {
|
||||||
|
header('Location: /admin/dashboard');
|
||||||
|
exit();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'domainadmin') {
|
elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'domainadmin') {
|
||||||
header('Location: /domainadmin/mailbox');
|
header('Location: /domainadmin/mailbox');
|
||||||
|
|||||||
@@ -2,18 +2,7 @@
|
|||||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
|
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
|
||||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.admin.inc.php';
|
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.admin.inc.php';
|
||||||
|
|
||||||
if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'domainadmin') {
|
protect_route(['admin']);
|
||||||
header('Location: /domainadmin/mailbox');
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user') {
|
|
||||||
header('Location: /user');
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
elseif (!isset($_SESSION['mailcow_cc_role']) || $_SESSION['mailcow_cc_role'] != "admin") {
|
|
||||||
header('Location: /admin');
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
|
|
||||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php';
|
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php';
|
||||||
$_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
|
$_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
|
||||||
|
|||||||
@@ -2,19 +2,7 @@
|
|||||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
|
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
|
||||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.admin.inc.php';
|
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.admin.inc.php';
|
||||||
|
|
||||||
if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'domainadmin') {
|
protect_route(['admin']);
|
||||||
header('Location: /domainadmin/mailbox');
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user') {
|
|
||||||
header('Location: /user');
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
elseif (!isset($_SESSION['mailcow_cc_role']) || $_SESSION['mailcow_cc_role'] != "admin") {
|
|
||||||
header('Location: /admin');
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php';
|
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php';
|
||||||
$js_minifier->add('/web/js/site/queue.js');
|
$js_minifier->add('/web/js/site/queue.js');
|
||||||
|
|||||||
@@ -2,18 +2,7 @@
|
|||||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
|
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
|
||||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.admin.inc.php';
|
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.admin.inc.php';
|
||||||
|
|
||||||
if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'domainadmin') {
|
protect_route(['admin']);
|
||||||
header('Location: /domainadmin/mailbox');
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user') {
|
|
||||||
header('Location: /user');
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
elseif (!isset($_SESSION['mailcow_cc_role']) || $_SESSION['mailcow_cc_role'] != "admin") {
|
|
||||||
header('Location: /admin');
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
|
|
||||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php';
|
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php';
|
||||||
$_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
|
$_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
|
||||||
@@ -127,6 +116,7 @@ $template_data = [
|
|||||||
'ip_check' => customize('get', 'ip_check'),
|
'ip_check' => customize('get', 'ip_check'),
|
||||||
'custom_login' => customize('get', 'custom_login'),
|
'custom_login' => customize('get', 'custom_login'),
|
||||||
'password_complexity' => password_complexity('get'),
|
'password_complexity' => password_complexity('get'),
|
||||||
|
'imapsync_settings' => imapsync_get_settings(),
|
||||||
'show_rspamd_global_filters' => @$_SESSION['show_rspamd_global_filters'],
|
'show_rspamd_global_filters' => @$_SESSION['show_rspamd_global_filters'],
|
||||||
'cors_settings' => $cors_settings,
|
'cors_settings' => $cors_settings,
|
||||||
'is_https' => isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on',
|
'is_https' => isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on',
|
||||||
|
|||||||
+700
-7
@@ -185,6 +185,9 @@ paths:
|
|||||||
example:
|
example:
|
||||||
username: info@domain.tld
|
username: info@domain.tld
|
||||||
domain: domain.tld
|
domain: domain.tld
|
||||||
|
description: my time limited alias
|
||||||
|
validity: 8760
|
||||||
|
permanent: false
|
||||||
properties:
|
properties:
|
||||||
username:
|
username:
|
||||||
description: 'the mailbox an alias should be created for'
|
description: 'the mailbox an alias should be created for'
|
||||||
@@ -192,6 +195,15 @@ paths:
|
|||||||
domain:
|
domain:
|
||||||
description: "the domain"
|
description: "the domain"
|
||||||
type: string
|
type: string
|
||||||
|
description:
|
||||||
|
description: "a description for the alias, defaults to an empty string"
|
||||||
|
type: string
|
||||||
|
validity:
|
||||||
|
description: "how many hours the alias stays valid, 1 to 87600, defaults to 8760 (one year)"
|
||||||
|
type: integer
|
||||||
|
permanent:
|
||||||
|
description: "keep the alias after it expired, defaults to false"
|
||||||
|
type: boolean
|
||||||
type: object
|
type: object
|
||||||
summary: Create time limited alias
|
summary: Create time limited alias
|
||||||
/api/v1/add/app-passwd:
|
/api/v1/add/app-passwd:
|
||||||
@@ -1072,6 +1084,7 @@ paths:
|
|||||||
password2: "*"
|
password2: "*"
|
||||||
quota: "3072"
|
quota: "3072"
|
||||||
force_pw_update: "1"
|
force_pw_update: "1"
|
||||||
|
force_tfa: "1"
|
||||||
tls_enforce_in: "1"
|
tls_enforce_in: "1"
|
||||||
tls_enforce_out: "1"
|
tls_enforce_out: "1"
|
||||||
tags: ["tag1", "tag2"]
|
tags: ["tag1", "tag2"]
|
||||||
@@ -1118,6 +1131,7 @@ paths:
|
|||||||
password2: atedismonsin
|
password2: atedismonsin
|
||||||
quota: "3072"
|
quota: "3072"
|
||||||
force_pw_update: "1"
|
force_pw_update: "1"
|
||||||
|
force_tfa: "1"
|
||||||
tls_enforce_in: "1"
|
tls_enforce_in: "1"
|
||||||
tls_enforce_out: "1"
|
tls_enforce_out: "1"
|
||||||
tags: ["tag1", "tag2"]
|
tags: ["tag1", "tag2"]
|
||||||
@@ -1151,6 +1165,9 @@ paths:
|
|||||||
force_pw_update:
|
force_pw_update:
|
||||||
description: forces the user to update its password on first login
|
description: forces the user to update its password on first login
|
||||||
type: boolean
|
type: boolean
|
||||||
|
force_tfa:
|
||||||
|
description: force 2FA enrollment at login
|
||||||
|
type: boolean
|
||||||
tls_enforce_in:
|
tls_enforce_in:
|
||||||
description: force inbound email tls encryption
|
description: force inbound email tls encryption
|
||||||
type: boolean
|
type: boolean
|
||||||
@@ -1520,7 +1537,9 @@ paths:
|
|||||||
timeout1: "600"
|
timeout1: "600"
|
||||||
timeout2: "600"
|
timeout2: "600"
|
||||||
exclude: "(?i)spam|(?i)junk"
|
exclude: "(?i)spam|(?i)junk"
|
||||||
custom_params: "--dry"
|
custom_params:
|
||||||
|
- {o: dry, v: ""}
|
||||||
|
- {o: folder, v: INBOX}
|
||||||
delete2duplicates: "1"
|
delete2duplicates: "1"
|
||||||
delete1: "1"
|
delete1: "1"
|
||||||
delete2: "0"
|
delete2: "0"
|
||||||
@@ -1569,8 +1588,19 @@ paths:
|
|||||||
description: exclude objects (regex)
|
description: exclude objects (regex)
|
||||||
type: string
|
type: string
|
||||||
custom_params:
|
custom_params:
|
||||||
description: custom parameters
|
description: >-
|
||||||
type: string
|
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:
|
delete2duplicates:
|
||||||
description: delete duplicates on destination (--delete2duplicates)
|
description: delete duplicates on destination (--delete2duplicates)
|
||||||
type: boolean
|
type: boolean
|
||||||
@@ -1593,6 +1623,151 @@ paths:
|
|||||||
description: enables or disables the sync job
|
description: enables or disables the sync job
|
||||||
type: boolean
|
type: boolean
|
||||||
type: object
|
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:
|
/api/v1/add/tls-policy-map:
|
||||||
post:
|
post:
|
||||||
responses:
|
responses:
|
||||||
@@ -2454,6 +2629,90 @@ paths:
|
|||||||
type: object
|
type: object
|
||||||
type: object
|
type: object
|
||||||
summary: Delete mails in Quarantine
|
summary: Delete mails in Quarantine
|
||||||
|
/api/v1/edit/qitem:
|
||||||
|
post:
|
||||||
|
responses:
|
||||||
|
"401":
|
||||||
|
$ref: "#/components/responses/Unauthorized"
|
||||||
|
"200":
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
examples:
|
||||||
|
release:
|
||||||
|
value:
|
||||||
|
- log:
|
||||||
|
- quarantine
|
||||||
|
- edit
|
||||||
|
- id:
|
||||||
|
- "33"
|
||||||
|
action: release
|
||||||
|
msg:
|
||||||
|
- item_released
|
||||||
|
- "33"
|
||||||
|
type: success
|
||||||
|
learnham:
|
||||||
|
value:
|
||||||
|
- log:
|
||||||
|
- quarantine
|
||||||
|
- edit
|
||||||
|
- id:
|
||||||
|
- "34"
|
||||||
|
action: learnham
|
||||||
|
msg:
|
||||||
|
- item_learned
|
||||||
|
- "34"
|
||||||
|
type: success
|
||||||
|
schema:
|
||||||
|
properties:
|
||||||
|
log:
|
||||||
|
description: contains request object
|
||||||
|
items: {}
|
||||||
|
type: array
|
||||||
|
msg:
|
||||||
|
items: {}
|
||||||
|
type: array
|
||||||
|
type:
|
||||||
|
enum:
|
||||||
|
- success
|
||||||
|
- danger
|
||||||
|
- error
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
description: OK
|
||||||
|
headers: {}
|
||||||
|
tags:
|
||||||
|
- Quarantine
|
||||||
|
description: >-
|
||||||
|
Using this endpoint you can perform actions on quarantine items. It is possible to release
|
||||||
|
emails from quarantine into to the inbox, or learn them as ham to improve Rspamd filtering.
|
||||||
|
You must provide the quarantine item IDs. You can get the IDs using the GET method.
|
||||||
|
operationId: Edit mails in Quarantine
|
||||||
|
requestBody:
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
example:
|
||||||
|
items:
|
||||||
|
- "33"
|
||||||
|
- "34"
|
||||||
|
attr:
|
||||||
|
action: release
|
||||||
|
properties:
|
||||||
|
items:
|
||||||
|
description: contains list of quarantine item IDs to release or learn as ham
|
||||||
|
type: object
|
||||||
|
attr:
|
||||||
|
description: attributes for the action
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
action:
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- release
|
||||||
|
- learnham
|
||||||
|
description: "release - return email to inbox; learnham - learn as ham to improve filtering"
|
||||||
|
type: object
|
||||||
|
summary: Edit mails in Quarantine
|
||||||
/api/v1/delete/recipient_map:
|
/api/v1/delete/recipient_map:
|
||||||
post:
|
post:
|
||||||
responses:
|
responses:
|
||||||
@@ -2681,6 +2940,62 @@ paths:
|
|||||||
type: object
|
type: object
|
||||||
type: object
|
type: object
|
||||||
summary: Delete sync job
|
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:
|
/api/v1/delete/tls-policy-map:
|
||||||
post:
|
post:
|
||||||
responses:
|
responses:
|
||||||
@@ -3330,6 +3645,7 @@ paths:
|
|||||||
- mailbox
|
- mailbox
|
||||||
- active: "1"
|
- active: "1"
|
||||||
force_pw_update: "0"
|
force_pw_update: "0"
|
||||||
|
force_tfa: "0"
|
||||||
name: Full name
|
name: Full name
|
||||||
password: "*"
|
password: "*"
|
||||||
password2: "*"
|
password2: "*"
|
||||||
@@ -3380,6 +3696,7 @@ paths:
|
|||||||
attr:
|
attr:
|
||||||
active: "1"
|
active: "1"
|
||||||
force_pw_update: "0"
|
force_pw_update: "0"
|
||||||
|
force_tfa: "0"
|
||||||
name: Full name
|
name: Full name
|
||||||
authsource: mailcow
|
authsource: mailcow
|
||||||
password: ""
|
password: ""
|
||||||
@@ -3403,6 +3720,9 @@ paths:
|
|||||||
force_pw_update:
|
force_pw_update:
|
||||||
description: force user to change password on next login
|
description: force user to change password on next login
|
||||||
type: boolean
|
type: boolean
|
||||||
|
force_tfa:
|
||||||
|
description: force 2FA enrollment at login
|
||||||
|
type: boolean
|
||||||
name:
|
name:
|
||||||
description: Full name of the mailbox user
|
description: Full name of the mailbox user
|
||||||
type: string
|
type: string
|
||||||
@@ -3778,8 +4098,25 @@ paths:
|
|||||||
etc.)
|
etc.)
|
||||||
type: boolean
|
type: boolean
|
||||||
custom_params:
|
custom_params:
|
||||||
description: Custom parameters passed to imapsync command
|
description: >-
|
||||||
type: string
|
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:
|
delete1:
|
||||||
description: Delete from source when completed
|
description: Delete from source when completed
|
||||||
type: boolean
|
type: boolean
|
||||||
@@ -3847,6 +4184,269 @@ paths:
|
|||||||
type: object
|
type: object
|
||||||
type: object
|
type: object
|
||||||
summary: Update sync job
|
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:
|
/api/v1/edit/user-acl:
|
||||||
post:
|
post:
|
||||||
responses:
|
responses:
|
||||||
@@ -4797,6 +5397,7 @@ paths:
|
|||||||
- active: "1"
|
- active: "1"
|
||||||
attributes:
|
attributes:
|
||||||
force_pw_update: "0"
|
force_pw_update: "0"
|
||||||
|
force_tfa: "0"
|
||||||
mailbox_format: "maildir:"
|
mailbox_format: "maildir:"
|
||||||
quarantine_notification: never
|
quarantine_notification: never
|
||||||
sogo_access: "1"
|
sogo_access: "1"
|
||||||
@@ -4813,6 +5414,7 @@ paths:
|
|||||||
quota: 3221225472
|
quota: 3221225472
|
||||||
quota_used: 0
|
quota_used: 0
|
||||||
rl: false
|
rl: false
|
||||||
|
sender_acl: ["otherbox@doman3.tld", "@aliasdomain.tld"]
|
||||||
spam_aliases: 0
|
spam_aliases: 0
|
||||||
username: info@doman3.tld
|
username: info@doman3.tld
|
||||||
tags: ["tag1", "tag2"]
|
tags: ["tag1", "tag2"]
|
||||||
@@ -5545,6 +6147,95 @@ paths:
|
|||||||
description: You can list all syn jobs existing in system.
|
description: You can list all syn jobs existing in system.
|
||||||
operationId: Get sync jobs
|
operationId: Get sync jobs
|
||||||
summary: 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}":
|
"/api/v1/get/tls-policy-map/{id}":
|
||||||
get:
|
get:
|
||||||
parameters:
|
parameters:
|
||||||
@@ -5721,6 +6412,7 @@ paths:
|
|||||||
- active: "1"
|
- active: "1"
|
||||||
attributes:
|
attributes:
|
||||||
force_pw_update: "0"
|
force_pw_update: "0"
|
||||||
|
force_tfa: "0"
|
||||||
mailbox_format: "maildir:"
|
mailbox_format: "maildir:"
|
||||||
quarantine_notification: never
|
quarantine_notification: never
|
||||||
sogo_access: "1"
|
sogo_access: "1"
|
||||||
@@ -5738,6 +6430,7 @@ paths:
|
|||||||
quota: 3221225472
|
quota: 3221225472
|
||||||
quota_used: 0
|
quota_used: 0
|
||||||
rl: false
|
rl: false
|
||||||
|
sender_acl: ["otherbox@domain3.tld", "@aliasdomain.tld"]
|
||||||
spam_aliases: 0
|
spam_aliases: 0
|
||||||
username: info@domain3.tld
|
username: info@domain3.tld
|
||||||
tags: ["tag1", "tag2"]
|
tags: ["tag1", "tag2"]
|
||||||
@@ -5760,7 +6453,7 @@ paths:
|
|||||||
response:
|
response:
|
||||||
value:
|
value:
|
||||||
- type: "success"
|
- type: "success"
|
||||||
log: ["cors", "edit", {"allowed_origins": ["*", "mail.mailcow.tld"], "allowed_methods": ["POST", "GET", "DELETE", "PUT"]}]
|
log: ["cors", "edit", {"allowed_origins": ["*", "https://mail.mailcow.tld"], "allowed_methods": ["POST", "GET", "DELETE", "PUT"]}]
|
||||||
msg: "cors_headers_edited"
|
msg: "cors_headers_edited"
|
||||||
description: OK
|
description: OK
|
||||||
headers: { }
|
headers: { }
|
||||||
@@ -5777,7 +6470,7 @@ paths:
|
|||||||
schema:
|
schema:
|
||||||
example:
|
example:
|
||||||
attr:
|
attr:
|
||||||
allowed_origins: ["*", "mail.mailcow.tld"]
|
allowed_origins: ["*", "https://mail.mailcow.tld"]
|
||||||
allowed_methods: ["POST", "GET", "DELETE", "PUT"]
|
allowed_methods: ["POST", "GET", "DELETE", "PUT"]
|
||||||
properties:
|
properties:
|
||||||
attr:
|
attr:
|
||||||
|
|||||||
@@ -29,8 +29,8 @@ header('Content-Type: application/xml');
|
|||||||
<clientConfig version="1.1">
|
<clientConfig version="1.1">
|
||||||
<emailProvider id="<?=$mailcow_hostname; ?>">
|
<emailProvider id="<?=$mailcow_hostname; ?>">
|
||||||
<domain>%EMAILDOMAIN%</domain>
|
<domain>%EMAILDOMAIN%</domain>
|
||||||
<displayName>A mailcow mail server</displayName>
|
<displayName><?=$autodiscover_config['displayName']; ?></displayName>
|
||||||
<displayShortName>mail server</displayShortName>
|
<displayShortName><?=$autodiscover_config['displayShortName']; ?></displayShortName>
|
||||||
|
|
||||||
<incomingServer type="imap">
|
<incomingServer type="imap">
|
||||||
<hostname><?=$autodiscover_config['imap']['server']; ?></hostname>
|
<hostname><?=$autodiscover_config['imap']['server']; ?></hostname>
|
||||||
|
|||||||
+150
-85
@@ -7,6 +7,8 @@ if(file_exists('inc/vars.local.inc.php')) {
|
|||||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.inc.php';
|
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.inc.php';
|
||||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.auth.inc.php';
|
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.auth.inc.php';
|
||||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/sessions.inc.php';
|
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/sessions.inc.php';
|
||||||
|
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.mailbox.inc.php';
|
||||||
|
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.ratelimit.inc.php';
|
||||||
$default_autodiscover_config = $autodiscover_config;
|
$default_autodiscover_config = $autodiscover_config;
|
||||||
$autodiscover_config = array_merge($default_autodiscover_config, $autodiscover_config);
|
$autodiscover_config = array_merge($default_autodiscover_config, $autodiscover_config);
|
||||||
|
|
||||||
@@ -58,101 +60,31 @@ $pdo = new PDO($dsn, $database_user, $database_pass, $opt);
|
|||||||
$iam_provider = identity_provider('init');
|
$iam_provider = identity_provider('init');
|
||||||
$iam_settings = identity_provider('get');
|
$iam_settings = identity_provider('get');
|
||||||
|
|
||||||
$login_user = strtolower(trim($_SERVER['PHP_AUTH_USER']));
|
// Passwordless autodiscover - no authentication required
|
||||||
$login_pass = trim(htmlspecialchars_decode($_SERVER['PHP_AUTH_PW']));
|
// Email will be extracted from the request body
|
||||||
|
$login_user = null;
|
||||||
|
$login_role = null;
|
||||||
|
|
||||||
if (empty($_SERVER['PHP_AUTH_USER']) || empty($_SERVER['PHP_AUTH_PW'])) {
|
header("Content-Type: application/xml");
|
||||||
$json = json_encode(
|
echo '<?xml version="1.0" encoding="utf-8" ?>' . PHP_EOL;
|
||||||
array(
|
|
||||||
"time" => time(),
|
|
||||||
"ua" => $_SERVER['HTTP_USER_AGENT'],
|
|
||||||
"user" => "none",
|
|
||||||
"ip" => $_SERVER['REMOTE_ADDR'],
|
|
||||||
"service" => "Error: must be authenticated"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
$redis->lPush('AUTODISCOVER_LOG', $json);
|
|
||||||
header('WWW-Authenticate: Basic realm="' . $_SERVER['HTTP_HOST'] . '"');
|
|
||||||
header('HTTP/1.0 401 Unauthorized');
|
|
||||||
exit(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
$login_role = check_login($login_user, $login_pass, array('eas' => TRUE));
|
|
||||||
|
|
||||||
if ($login_role === "user") {
|
|
||||||
header("Content-Type: application/xml");
|
|
||||||
echo '<?xml version="1.0" encoding="utf-8" ?>' . PHP_EOL;
|
|
||||||
?>
|
?>
|
||||||
<Autodiscover xmlns="http://schemas.microsoft.com/exchange/autodiscover/responseschema/2006">
|
<Autodiscover xmlns="http://schemas.microsoft.com/exchange/autodiscover/responseschema/2006">
|
||||||
<?php
|
<?php
|
||||||
if(!$data) {
|
if(!$data) {
|
||||||
try {
|
|
||||||
$json = json_encode(
|
|
||||||
array(
|
|
||||||
"time" => time(),
|
|
||||||
"ua" => $_SERVER['HTTP_USER_AGENT'],
|
|
||||||
"user" => $_SERVER['PHP_AUTH_USER'],
|
|
||||||
"ip" => $_SERVER['REMOTE_ADDR'],
|
|
||||||
"service" => "Error: invalid or missing request data"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
$redis->lPush('AUTODISCOVER_LOG', $json);
|
|
||||||
$redis->lTrim('AUTODISCOVER_LOG', 0, 100);
|
|
||||||
}
|
|
||||||
catch (RedisException $e) {
|
|
||||||
$_SESSION['return'][] = array(
|
|
||||||
'type' => 'danger',
|
|
||||||
'msg' => 'Redis: '.$e
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
list($usec, $sec) = explode(' ', microtime());
|
|
||||||
?>
|
|
||||||
<Response>
|
|
||||||
<Error Time="<?=date('H:i:s', $sec) . substr($usec, 0, strlen($usec) - 2);?>" Id="2477272013">
|
|
||||||
<ErrorCode>600</ErrorCode>
|
|
||||||
<Message>Invalid Request</Message>
|
|
||||||
<DebugData />
|
|
||||||
</Error>
|
|
||||||
</Response>
|
|
||||||
</Autodiscover>
|
|
||||||
<?php
|
|
||||||
exit(0);
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
$discover = new SimpleXMLElement($data);
|
|
||||||
$email = $discover->Request->EMailAddress;
|
|
||||||
} catch (Exception $e) {
|
|
||||||
$email = $_SERVER['PHP_AUTH_USER'];
|
|
||||||
}
|
|
||||||
|
|
||||||
$username = trim($email);
|
|
||||||
try {
|
|
||||||
$stmt = $pdo->prepare("SELECT `name` FROM `mailbox` WHERE `username`= :username");
|
|
||||||
$stmt->execute(array(':username' => $username));
|
|
||||||
$MailboxData = $stmt->fetch(PDO::FETCH_ASSOC);
|
|
||||||
}
|
|
||||||
catch(PDOException $e) {
|
|
||||||
die("Failed to determine name from SQL");
|
|
||||||
}
|
|
||||||
if (!empty($MailboxData['name'])) {
|
|
||||||
$displayname = $MailboxData['name'];
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
$displayname = $email;
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
$json = json_encode(
|
$json = json_encode(
|
||||||
array(
|
array(
|
||||||
"time" => time(),
|
"time" => time(),
|
||||||
"ua" => $_SERVER['HTTP_USER_AGENT'],
|
"ua" => $_SERVER['HTTP_USER_AGENT'],
|
||||||
"user" => $_SERVER['PHP_AUTH_USER'],
|
"user" => "none",
|
||||||
"ip" => $_SERVER['REMOTE_ADDR'],
|
"ip" => $_SERVER['REMOTE_ADDR'],
|
||||||
"service" => $autodiscover_config['autodiscoverType']
|
"service" => "Error: invalid or missing request data"
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
$redis->lPush('AUTODISCOVER_LOG', $json);
|
$redis->lPush('AUTODISCOVER_LOG', $json);
|
||||||
$redis->lTrim('AUTODISCOVER_LOG', 0, 100);
|
$redis->lTrim('AUTODISCOVER_LOG', 0, 100);
|
||||||
|
$redis->publish("F2B_CHANNEL", "Autodiscover: Invalid request by " . $_SERVER['REMOTE_ADDR']);
|
||||||
|
error_log("Autodiscover: Invalid request by " . $_SERVER['REMOTE_ADDR']);
|
||||||
}
|
}
|
||||||
catch (RedisException $e) {
|
catch (RedisException $e) {
|
||||||
$_SESSION['return'][] = array(
|
$_SESSION['return'][] = array(
|
||||||
@@ -161,7 +93,143 @@ if ($login_role === "user") {
|
|||||||
);
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if ($autodiscover_config['autodiscoverType'] == 'imap') {
|
list($usec, $sec) = explode(' ', microtime());
|
||||||
|
?>
|
||||||
|
<Response>
|
||||||
|
<Error Time="<?=date('H:i:s', $sec) . substr($usec, 0, strlen($usec) - 2);?>" Id="<?=rand(1000000000, 9999999999);?>">
|
||||||
|
<ErrorCode>600</ErrorCode>
|
||||||
|
<Message>Invalid Request</Message>
|
||||||
|
<DebugData />
|
||||||
|
</Error>
|
||||||
|
</Response>
|
||||||
|
</Autodiscover>
|
||||||
|
<?php
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
$discover = new SimpleXMLElement($data);
|
||||||
|
$email = $discover->Request->EMailAddress;
|
||||||
|
} catch (Exception $e) {
|
||||||
|
// If parsing fails, return error
|
||||||
|
try {
|
||||||
|
$json = json_encode(
|
||||||
|
array(
|
||||||
|
"time" => time(),
|
||||||
|
"ua" => $_SERVER['HTTP_USER_AGENT'],
|
||||||
|
"user" => "none",
|
||||||
|
"ip" => $_SERVER['REMOTE_ADDR'],
|
||||||
|
"service" => "Error: could not parse email from request"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
$redis->lPush('AUTODISCOVER_LOG', $json);
|
||||||
|
$redis->lTrim('AUTODISCOVER_LOG', 0, 100);
|
||||||
|
$redis->publish("F2B_CHANNEL", "Autodiscover: Malformed XML by " . $_SERVER['REMOTE_ADDR']);
|
||||||
|
error_log("Autodiscover: Malformed XML by " . $_SERVER['REMOTE_ADDR']);
|
||||||
|
}
|
||||||
|
catch (RedisException $e) {
|
||||||
|
// Silently fail
|
||||||
|
}
|
||||||
|
list($usec, $sec) = explode(' ', microtime());
|
||||||
|
?>
|
||||||
|
<Response>
|
||||||
|
<Error Time="<?=date('H:i:s', $sec) . substr($usec, 0, strlen($usec) - 2);?>" Id="<?=rand(1000000000, 9999999999);?>">
|
||||||
|
<ErrorCode>600</ErrorCode>
|
||||||
|
<Message>Invalid Request</Message>
|
||||||
|
<DebugData />
|
||||||
|
</Error>
|
||||||
|
</Response>
|
||||||
|
</Autodiscover>
|
||||||
|
<?php
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
$username = trim((string)$email);
|
||||||
|
try {
|
||||||
|
$stmt = $pdo->prepare("SELECT `mailbox`.`name`, `mailbox`.`active` FROM `mailbox`
|
||||||
|
INNER JOIN `domain` ON `mailbox`.`domain` = `domain`.`domain`
|
||||||
|
WHERE `mailbox`.`username` = :username
|
||||||
|
AND `mailbox`.`active` = '1'
|
||||||
|
AND `domain`.`active` = '1'");
|
||||||
|
$stmt->execute(array(':username' => $username));
|
||||||
|
$MailboxData = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
}
|
||||||
|
catch(PDOException $e) {
|
||||||
|
// Database error - return error response with complete XML
|
||||||
|
list($usec, $sec) = explode(' ', microtime());
|
||||||
|
?>
|
||||||
|
<Response>
|
||||||
|
<Error Time="<?=date('H:i:s', $sec) . substr($usec, 0, strlen($usec) - 2);?>" Id="<?=rand(1000000000, 9999999999);?>">
|
||||||
|
<ErrorCode>500</ErrorCode>
|
||||||
|
<Message>Database Error</Message>
|
||||||
|
<DebugData />
|
||||||
|
</Error>
|
||||||
|
</Response>
|
||||||
|
</Autodiscover>
|
||||||
|
<?php
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mailbox not found or not active - return generic error to prevent user enumeration
|
||||||
|
if (empty($MailboxData)) {
|
||||||
|
try {
|
||||||
|
$json = json_encode(
|
||||||
|
array(
|
||||||
|
"time" => time(),
|
||||||
|
"ua" => $_SERVER['HTTP_USER_AGENT'],
|
||||||
|
"user" => $email,
|
||||||
|
"ip" => $_SERVER['REMOTE_ADDR'],
|
||||||
|
"service" => "Error: mailbox not found or inactive"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
$redis->lPush('AUTODISCOVER_LOG', $json);
|
||||||
|
$redis->lTrim('AUTODISCOVER_LOG', 0, 100);
|
||||||
|
$redis->publish("F2B_CHANNEL", "Autodiscover: Invalid mailbox attempt by " . $_SERVER['REMOTE_ADDR']);
|
||||||
|
error_log("Autodiscover: Invalid mailbox attempt by " . $_SERVER['REMOTE_ADDR']);
|
||||||
|
}
|
||||||
|
catch (RedisException $e) {
|
||||||
|
// Silently fail
|
||||||
|
}
|
||||||
|
list($usec, $sec) = explode(' ', microtime());
|
||||||
|
?>
|
||||||
|
<Response>
|
||||||
|
<Error Time="<?=date('H:i:s', $sec) . substr($usec, 0, strlen($usec) - 2);?>" Id="<?=rand(1000000000, 9999999999);?>">
|
||||||
|
<ErrorCode>600</ErrorCode>
|
||||||
|
<Message>Invalid Request</Message>
|
||||||
|
<DebugData />
|
||||||
|
</Error>
|
||||||
|
</Response>
|
||||||
|
</Autodiscover>
|
||||||
|
<?php
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($MailboxData['name'])) {
|
||||||
|
$displayname = $MailboxData['name'];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$displayname = $email;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
$json = json_encode(
|
||||||
|
array(
|
||||||
|
"time" => time(),
|
||||||
|
"ua" => $_SERVER['HTTP_USER_AGENT'],
|
||||||
|
"user" => $email,
|
||||||
|
"ip" => $_SERVER['REMOTE_ADDR'],
|
||||||
|
"service" => $autodiscover_config['autodiscoverType']
|
||||||
|
)
|
||||||
|
);
|
||||||
|
$redis->lPush('AUTODISCOVER_LOG', $json);
|
||||||
|
$redis->lTrim('AUTODISCOVER_LOG', 0, 100);
|
||||||
|
}
|
||||||
|
catch (RedisException $e) {
|
||||||
|
$_SESSION['return'][] = array(
|
||||||
|
'type' => 'danger',
|
||||||
|
'msg' => 'Redis: '.$e
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if ($autodiscover_config['autodiscoverType'] == 'imap') {
|
||||||
?>
|
?>
|
||||||
<Response xmlns="http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a">
|
<Response xmlns="http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a">
|
||||||
<User>
|
<User>
|
||||||
@@ -236,6 +304,3 @@ if ($login_role === "user") {
|
|||||||
}
|
}
|
||||||
?>
|
?>
|
||||||
</Autodiscover>
|
</Autodiscover>
|
||||||
<?php
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
|
|||||||
@@ -3,8 +3,11 @@ require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
|
|||||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.domainadmin.inc.php';
|
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.domainadmin.inc.php';
|
||||||
|
|
||||||
if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'domainadmin') {
|
if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'domainadmin') {
|
||||||
header('Location: /domainadmin/mailbox');
|
// Only redirect to mailbox if NO pending actions
|
||||||
exit();
|
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') {
|
elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'admin') {
|
||||||
header('Location: /admin/dashboard');
|
header('Location: /admin/dashboard');
|
||||||
|
|||||||
@@ -2,18 +2,7 @@
|
|||||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
|
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
|
||||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.domainadmin.inc.php';
|
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.domainadmin.inc.php';
|
||||||
|
|
||||||
if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'admin') {
|
protect_route(['domainadmin']);
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php';
|
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php';
|
||||||
$_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
|
$_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
|
||||||
|
|||||||
@@ -2,41 +2,28 @@
|
|||||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
|
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
|
||||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.domainadmin.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
|
||||||
|
*/
|
||||||
|
|
||||||
/*
|
protect_route(['domainadmin']);
|
||||||
/ DOMAIN ADMIN
|
|
||||||
*/
|
|
||||||
|
|
||||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php';
|
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php';
|
||||||
$_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
|
$_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
|
||||||
$tfa_data = get_tfa();
|
$tfa_data = get_tfa();
|
||||||
$fido2_data = fido2(array("action" => "get_friendly_names"));
|
$fido2_data = fido2(array("action" => "get_friendly_names"));
|
||||||
$username = $_SESSION['mailcow_cc_username'];
|
$username = $_SESSION['mailcow_cc_username'];
|
||||||
|
|
||||||
$template = 'domainadmin.twig';
|
$template = 'domainadmin.twig';
|
||||||
$template_data = [
|
$template_data = [
|
||||||
'acl' => $_SESSION['acl'],
|
'acl' => $_SESSION['acl'],
|
||||||
'acl_json' => json_encode($_SESSION['acl']),
|
'acl_json' => json_encode($_SESSION['acl']),
|
||||||
'user_spam_score' => mailbox('get', 'spam_score', $username),
|
'user_spam_score' => mailbox('get', 'spam_score', $username),
|
||||||
'tfa_data' => $tfa_data,
|
'tfa_data' => $tfa_data,
|
||||||
'fido2_data' => $fido2_data,
|
'fido2_data' => $fido2_data,
|
||||||
'lang_user' => json_encode($lang['user']),
|
'lang_user' => json_encode($lang['user']),
|
||||||
'lang_datatables' => json_encode($lang['datatables']),
|
'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();
|
|
||||||
}
|
|
||||||
|
|
||||||
$js_minifier->add('/web/js/site/user.js');
|
$js_minifier->add('/web/js/site/user.js');
|
||||||
$js_minifier->add('/web/js/site/pwgen.js');
|
$js_minifier->add('/web/js/site/pwgen.js');
|
||||||
|
|||||||
+10
-6
@@ -1,10 +1,8 @@
|
|||||||
<?php
|
<?php
|
||||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.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)) {
|
protect_route();
|
||||||
header('Location: /');
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php';
|
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php';
|
||||||
|
|
||||||
$template = 'edit.twig';
|
$template = 'edit.twig';
|
||||||
@@ -206,9 +204,15 @@ if (isset($_SESSION['mailcow_cc_role'])) {
|
|||||||
if (isset($_GET['syncjob']) &&
|
if (isset($_GET['syncjob']) &&
|
||||||
is_numeric($_GET['syncjob'])) {
|
is_numeric($_GET['syncjob'])) {
|
||||||
$id = $_GET["syncjob"];
|
$id = $_GET["syncjob"];
|
||||||
$result = mailbox('get', 'syncjob_details', $id);
|
$result = syncjob('get', 'job', $id);
|
||||||
$template = 'edit/syncjob.twig';
|
$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']) &&
|
elseif (isset($_GET['filter']) &&
|
||||||
is_numeric($_GET['filter'])) {
|
is_numeric($_GET['filter'])) {
|
||||||
$id = $_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 (count($mta_sts) > 0 && $mta_sts['active'] == 1) {
|
||||||
if (!in_array($domain, $alias_domains)) {
|
if (!in_array($domain, $alias_domains)) {
|
||||||
$records[] = array(
|
$records[] = array(
|
||||||
@@ -371,13 +380,13 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm
|
|||||||
else {
|
else {
|
||||||
$state = state_nomatch;
|
$state = state_nomatch;
|
||||||
}
|
}
|
||||||
$state .= '<br />' . $current[$data_field[$current['type']]];
|
$state .= '<br />' . htmlspecialchars($current[$data_field[$current['type']]]);
|
||||||
}
|
}
|
||||||
if ($current['type'] == 'TXT' &&
|
if ($current['type'] == 'TXT' &&
|
||||||
stripos($current['txt'], 'v=dmarc') === 0 &&
|
stripos($current['txt'], 'v=dmarc') === 0 &&
|
||||||
$record[2] == $dmarc_link) {
|
$record[2] == $dmarc_link) {
|
||||||
$current['txt'] = str_replace(' ', '', $current['txt']);
|
$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' &&
|
elseif ($current['type'] == 'TXT' &&
|
||||||
stripos($current['txt'], 'v=spf') === 0 &&
|
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)) {
|
if (in_array($ip, $rslt) && in_array(expand_ipv6($ip6), $rslt)) {
|
||||||
$state = state_good;
|
$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' &&
|
elseif ($current['type'] == 'TXT' &&
|
||||||
stripos($current['txt'], 'v=dkim') === 0 &&
|
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) {
|
if ($state == state_nomatch) {
|
||||||
$state = array();
|
$state = array();
|
||||||
foreach ($currents as $current) {
|
foreach ($currents as $current) {
|
||||||
$state[] = $current[$data_field[$current['type']]];
|
$state[] = htmlspecialchars($current[$data_field[$current['type']]]);
|
||||||
}
|
}
|
||||||
$state = implode('<br />', $state);
|
$state = implode('<br />', $state);
|
||||||
}
|
}
|
||||||
@@ -427,12 +436,22 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm
|
|||||||
<td>%s</td>
|
<td>%s</td>
|
||||||
<td class="dns-found">%s</td>
|
<td class="dns-found">%s</td>
|
||||||
<td class="dns-recommended">%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);
|
$record[3] = explode('<br />', $state);
|
||||||
}
|
}
|
||||||
|
|
||||||
unset($record);
|
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);
|
$dns_data = sprintf("\$ORIGIN %s.\n", $domain);
|
||||||
foreach ($records as $record) {
|
foreach ($records as $record) {
|
||||||
if ($domain == substr($record[0], -strlen($domain))) {
|
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_optional, '', $val);
|
||||||
$val = str_replace(state_good, '', $val);
|
$val = str_replace(state_good, '', $val);
|
||||||
if (strlen($val) > 0) {
|
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);
|
$vals[] = sprintf("%s\tIN\t%s\t%s\n", $label, $record[1], $val);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
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);
|
$vals[] = sprintf("%s\tIN\t%s\t%s\n", $label, $record[1], $val);
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ($vals as $val) {
|
foreach ($vals as $val) {
|
||||||
$dns_data .= str_replace($domain, $domain . '.', $val);
|
$dns_data .= $val;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
?>
|
?>
|
||||||
</table>
|
</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>
|
<script>
|
||||||
var zonefile_dl_link = document.getElementById('download-zonefile');
|
var zonefile_dl_link = document.getElementById('download-zonefile');
|
||||||
var zonefile = atob(zonefile_dl_link.getAttribute('data-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 (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'];
|
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;
|
||||||
@@ -64,12 +64,15 @@ $globalVariables = [
|
|||||||
'pending_tfa_methods' => @$_SESSION['pending_tfa_methods'],
|
'pending_tfa_methods' => @$_SESSION['pending_tfa_methods'],
|
||||||
'pending_tfa_authmechs' => $pending_tfa_authmechs,
|
'pending_tfa_authmechs' => $pending_tfa_authmechs,
|
||||||
'pending_mailcow_cc_username' => @$_SESSION['pending_mailcow_cc_username'],
|
'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_footer' => json_encode($lang['footer']),
|
||||||
'lang_acl' => json_encode($lang['acl']),
|
'lang_acl' => json_encode($lang['acl']),
|
||||||
'lang_tfa' => json_encode($lang['tfa']),
|
'lang_tfa' => json_encode($lang['tfa']),
|
||||||
'lang_fido2' => json_encode($lang['fido2']),
|
'lang_fido2' => json_encode($lang['fido2']),
|
||||||
'lang_success' => json_encode($lang['success']),
|
'lang_success' => json_encode($lang['success']),
|
||||||
'lang_danger' => json_encode($lang['danger']),
|
'lang_danger' => json_encode($lang['danger']),
|
||||||
|
'lang_syncjobs' => json_encode($lang['syncjobs']),
|
||||||
'docker_timeout' => $DOCKER_TIMEOUT,
|
'docker_timeout' => $DOCKER_TIMEOUT,
|
||||||
'session_lifetime' => (int)$SESSION_LIFETIME,
|
'session_lifetime' => (int)$SESSION_LIFETIME,
|
||||||
'csrf_token' => $_SESSION['CSRF']['TOKEN'],
|
'csrf_token' => $_SESSION['CSRF']['TOKEN'],
|
||||||
|
|||||||
@@ -121,34 +121,56 @@ function admin($_action, $_data = null) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!empty($password)) {
|
// Check if this is a self password change via forced update
|
||||||
if (password_check($password, $password2) !== true) {
|
if ($username == $_SESSION['mailcow_cc_username'] && !empty($_SESSION['pending_pw_update'])) {
|
||||||
return false;
|
// 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);
|
} else {
|
||||||
$stmt = $pdo->prepare("UPDATE `admin` SET `username` = :username_new, `active` = :active, `password` = :password_hashed WHERE `username` = :username");
|
// Normal admin edit: update all attributes
|
||||||
$stmt->execute(array(
|
$force_tfa = intval($_data['force_tfa'] ?? 0) ? 1 : 0;
|
||||||
':password_hashed' => $password_hashed,
|
$force_pw_update = intval($_data['force_pw_update'] ?? 0) ? 1 : 0;
|
||||||
':username_new' => $username_new,
|
if (!empty($password)) {
|
||||||
':username' => $username,
|
if (password_check($password, $password2) !== true) {
|
||||||
':active' => $active
|
return false;
|
||||||
));
|
}
|
||||||
if (isset($_data['disable_tfa'])) {
|
$password_hashed = hash_password($password);
|
||||||
$stmt = $pdo->prepare("UPDATE `tfa` SET `active` = '0' WHERE `username` = :username");
|
$stmt = $pdo->prepare("UPDATE `admin` SET `username` = :username_new, `active` = :active, `password` = :password_hashed,
|
||||||
$stmt->execute(array(':username' => $username));
|
`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 {
|
else {
|
||||||
$stmt = $pdo->prepare("UPDATE `tfa` SET `username` = :username_new WHERE `username` = :username");
|
$stmt = $pdo->prepare("UPDATE `admin` SET `username` = :username_new, `active` = :active,
|
||||||
$stmt->execute(array(':username_new' => $username_new, ':username' => $username));
|
`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'])) {
|
if (isset($_data['disable_tfa'])) {
|
||||||
$stmt = $pdo->prepare("UPDATE `tfa` SET `active` = '0' WHERE `username` = :username");
|
$stmt = $pdo->prepare("UPDATE `tfa` SET `active` = '0' WHERE `username` = :username");
|
||||||
$stmt->execute(array(':username' => $username));
|
$stmt->execute(array(':username' => $username));
|
||||||
@@ -223,7 +245,8 @@ function admin($_action, $_data = null) {
|
|||||||
`tfa`.`active` AS `tfa_active`,
|
`tfa`.`active` AS `tfa_active`,
|
||||||
`admin`.`username`,
|
`admin`.`username`,
|
||||||
`admin`.`created`,
|
`admin`.`created`,
|
||||||
`admin`.`active` AS `active`
|
`admin`.`active` AS `active`,
|
||||||
|
`admin`.`attributes` AS `attributes`
|
||||||
FROM `admin`
|
FROM `admin`
|
||||||
LEFT OUTER JOIN `tfa` ON `tfa`.`username`=`admin`.`username`
|
LEFT OUTER JOIN `tfa` ON `tfa`.`username`=`admin`.`username`
|
||||||
WHERE `admin`.`username`= :admin AND `superadmin` = '1'");
|
WHERE `admin`.`username`= :admin AND `superadmin` = '1'");
|
||||||
@@ -240,6 +263,7 @@ function admin($_action, $_data = null) {
|
|||||||
$admindata['active'] = $row['active'];
|
$admindata['active'] = $row['active'];
|
||||||
$admindata['active_int'] = $row['active'];
|
$admindata['active_int'] = $row['active'];
|
||||||
$admindata['created'] = $row['created'];
|
$admindata['created'] = $row['created'];
|
||||||
|
$admindata['attributes'] = json_decode($row['attributes'], true) ?? array('force_tfa' => '0', 'force_pw_update' => '0');
|
||||||
return $admindata;
|
return $admindata;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
<?php
|
<?php
|
||||||
function check_login($user, $pass, $app_passwd_data = false, $extra = null) {
|
function check_login($user, $pass, $extra = null) {
|
||||||
global $pdo;
|
global $pdo;
|
||||||
global $redis;
|
global $redis;
|
||||||
|
|
||||||
$is_internal = $extra['is_internal'];
|
$is_internal = $extra['is_internal'];
|
||||||
$role = $extra['role'];
|
$role = $extra['role'];
|
||||||
|
$extra['service'] = !isset($extra['service']) ? 'NONE' : $extra['service'];
|
||||||
|
|
||||||
// Try validate admin
|
// Try validate admin
|
||||||
if (!isset($role) || $role == "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
|
// Try validate app password
|
||||||
if (!isset($role) || $role == "app") {
|
if (!isset($role) || $role == "app") {
|
||||||
$result = apppass_login($user, $pass, $app_passwd_data);
|
$result = apppass_login($user, $pass, $extra);
|
||||||
if ($result !== false) {
|
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']);
|
$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;
|
return $result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try validate user
|
// Try validate user
|
||||||
if (!isset($role) || $role == "user") {
|
if (!isset($role) || $role == "user") {
|
||||||
$result = user_login($user, $pass);
|
$result = user_login($user, $pass, $extra);
|
||||||
if ($result !== false) {
|
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']);
|
$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;
|
return $result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -95,7 +82,7 @@ function admin_login($user, $pass){
|
|||||||
}
|
}
|
||||||
|
|
||||||
$user = strtolower(trim($user));
|
$user = strtolower(trim($user));
|
||||||
$stmt = $pdo->prepare("SELECT `password` FROM `admin`
|
$stmt = $pdo->prepare("SELECT `password`, `attributes` FROM `admin`
|
||||||
WHERE `superadmin` = '1'
|
WHERE `superadmin` = '1'
|
||||||
AND `active` = '1'
|
AND `active` = '1'
|
||||||
AND `username` = :user");
|
AND `username` = :user");
|
||||||
@@ -104,6 +91,13 @@ function admin_login($user, $pass){
|
|||||||
|
|
||||||
// verify password
|
// verify password
|
||||||
if (verify_hash($row['password'], $pass)) {
|
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
|
// check for tfa authenticators
|
||||||
$authenticators = get_tfa($user);
|
$authenticators = get_tfa($user);
|
||||||
if (isset($authenticators['additional']) && is_array($authenticators['additional']) && count($authenticators['additional']) > 0) {
|
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"
|
// Reactivate TFA if it was set to "deactivate TFA for next login"
|
||||||
$stmt = $pdo->prepare("UPDATE `tfa` SET `active`='1' WHERE `username` = :user");
|
$stmt = $pdo->prepare("UPDATE `tfa` SET `active`='1' WHERE `username` = :user");
|
||||||
$stmt->execute(array(':user' => $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(
|
$_SESSION['return'][] = array(
|
||||||
'type' => 'success',
|
'type' => 'success',
|
||||||
'log' => array(__FUNCTION__, $user, '*'),
|
'log' => array(__FUNCTION__, $user, '*'),
|
||||||
@@ -148,7 +146,7 @@ function domainadmin_login($user, $pass){
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
$stmt = $pdo->prepare("SELECT `password` FROM `admin`
|
$stmt = $pdo->prepare("SELECT `password`, `attributes` FROM `admin`
|
||||||
WHERE `superadmin` = '0'
|
WHERE `superadmin` = '0'
|
||||||
AND `active`='1'
|
AND `active`='1'
|
||||||
AND `username` = :user");
|
AND `username` = :user");
|
||||||
@@ -157,6 +155,13 @@ function domainadmin_login($user, $pass){
|
|||||||
|
|
||||||
// verify password
|
// verify password
|
||||||
if (verify_hash($row['password'], $pass) !== false) {
|
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
|
// check for tfa authenticators
|
||||||
$authenticators = get_tfa($user);
|
$authenticators = get_tfa($user);
|
||||||
if (isset($authenticators['additional']) && is_array($authenticators['additional']) && count($authenticators['additional']) > 0) {
|
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"
|
// Reactivate TFA if it was set to "deactivate TFA for next login"
|
||||||
$stmt = $pdo->prepare("UPDATE `tfa` SET `active`='1' WHERE `username` = :user");
|
$stmt = $pdo->prepare("UPDATE `tfa` SET `active`='1' WHERE `username` = :user");
|
||||||
$stmt->execute(array(':user' => $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(
|
$_SESSION['return'][] = array(
|
||||||
'type' => 'success',
|
'type' => 'success',
|
||||||
'log' => array(__FUNCTION__, $user, '*'),
|
'log' => array(__FUNCTION__, $user, '*'),
|
||||||
@@ -193,7 +202,7 @@ function user_login($user, $pass, $extra = null){
|
|||||||
global $iam_settings;
|
global $iam_settings;
|
||||||
|
|
||||||
$is_internal = $extra['is_internal'];
|
$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 (!filter_var($user, FILTER_VALIDATE_EMAIL) && !ctype_alnum(str_replace(array('_', '.', '-'), '', $user))) {
|
||||||
if (!$is_internal){
|
if (!$is_internal){
|
||||||
@@ -236,10 +245,10 @@ function user_login($user, $pass, $extra = null){
|
|||||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
if (!empty($row)) {
|
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);
|
$row['attributes'] = json_decode($row['attributes'], true);
|
||||||
if (isset($service)) {
|
if ($extra['service'] != 'NONE') {
|
||||||
$key = strtolower($service) . "_access";
|
$key = strtolower($extra['service']) . "_access";
|
||||||
if (isset($row['attributes'][$key]) && $row['attributes'][$key] != '1') {
|
if (isset($row['attributes'][$key]) && $row['attributes'][$key] != '1') {
|
||||||
return false;
|
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
|
// check if user has access to service (imap, smtp, pop3, sieve) if service is set
|
||||||
$row['attributes'] = json_decode($row['attributes'], true);
|
$row['attributes'] = json_decode($row['attributes'], true);
|
||||||
if (isset($service)) {
|
if ($extra['service'] != 'NONE') {
|
||||||
$key = strtolower($service) . "_access";
|
$key = strtolower($extra['service']) . "_access";
|
||||||
if (isset($row['attributes'][$key]) && $row['attributes'][$key] != '1') {
|
if (isset($row['attributes'][$key]) && $row['attributes'][$key] != '1') {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -278,6 +287,8 @@ function user_login($user, $pass, $extra = null){
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$row['attributes'] = json_decode($row['attributes'], true);
|
||||||
|
|
||||||
// check for tfa authenticators
|
// check for tfa authenticators
|
||||||
$authenticators = get_tfa($user);
|
$authenticators = get_tfa($user);
|
||||||
if (isset($authenticators['additional']) && is_array($authenticators['additional']) && count($authenticators['additional']) > 0 && !$is_internal) {
|
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"
|
// Reactivate TFA if it was set to "deactivate TFA for next login"
|
||||||
$stmt = $pdo->prepare("UPDATE `tfa` SET `active`='1' WHERE `username` = :user");
|
$stmt = $pdo->prepare("UPDATE `tfa` SET `active`='1' WHERE `username` = :user");
|
||||||
$stmt->execute(array(':user' => $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(
|
$_SESSION['return'][] = array(
|
||||||
'type' => 'success',
|
'type' => 'success',
|
||||||
'log' => array(__FUNCTION__, $user, '*', 'Provider: Keycloak'),
|
'log' => array(__FUNCTION__, $user, '*', 'Provider: Keycloak'),
|
||||||
@@ -330,6 +345,8 @@ function user_login($user, $pass, $extra = null){
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$row['attributes'] = json_decode($row['attributes'], true);
|
||||||
|
|
||||||
// check for tfa authenticators
|
// check for tfa authenticators
|
||||||
$authenticators = get_tfa($user);
|
$authenticators = get_tfa($user);
|
||||||
if (isset($authenticators['additional']) && is_array($authenticators['additional']) && count($authenticators['additional']) > 0 && !$is_internal) {
|
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"
|
// Reactivate TFA if it was set to "deactivate TFA for next login"
|
||||||
$stmt = $pdo->prepare("UPDATE `tfa` SET `active`='1' WHERE `username` = :user");
|
$stmt = $pdo->prepare("UPDATE `tfa` SET `active`='1' WHERE `username` = :user");
|
||||||
$stmt->execute(array(':user' => $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(
|
$_SESSION['return'][] = array(
|
||||||
'type' => 'success',
|
'type' => 'success',
|
||||||
'log' => array(__FUNCTION__, $user, '*', 'Provider: LDAP'),
|
'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"
|
// Reactivate TFA if it was set to "deactivate TFA for next login"
|
||||||
$stmt = $pdo->prepare("UPDATE `tfa` SET `active`='1' WHERE `username` = :user");
|
$stmt = $pdo->prepare("UPDATE `tfa` SET `active`='1' WHERE `username` = :user");
|
||||||
$stmt->execute(array(':user' => $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(
|
$_SESSION['return'][] = array(
|
||||||
'type' => 'success',
|
'type' => 'success',
|
||||||
'log' => array(__FUNCTION__, $user, '*', 'Provider: mailcow'),
|
'log' => array(__FUNCTION__, $user, '*', 'Provider: mailcow'),
|
||||||
@@ -408,7 +433,7 @@ function user_login($user, $pass, $extra = null){
|
|||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
function apppass_login($user, $pass, $app_passwd_data, $extra = null){
|
function apppass_login($user, $pass, $extra = null){
|
||||||
global $pdo;
|
global $pdo;
|
||||||
|
|
||||||
$is_internal = $extra['is_internal'];
|
$is_internal = $extra['is_internal'];
|
||||||
@@ -424,20 +449,8 @@ function apppass_login($user, $pass, $app_passwd_data, $extra = null){
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
$protocol = false;
|
$extra['service'] = !isset($extra['service']) ? 'NONE' : $extra['service'];
|
||||||
if ($app_passwd_data['eas']){
|
if (!$is_internal && $extra['service'] == 'NONE') {
|
||||||
$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) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -458,7 +471,7 @@ function apppass_login($user, $pass, $app_passwd_data, $extra = null){
|
|||||||
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
foreach ($rows as $row) {
|
foreach ($rows as $row) {
|
||||||
if ($protocol && $row[$protocol . '_access'] != '1'){
|
if ($extra['service'] != 'NONE' && $row[strtolower($extra['service']) . '_access'] != '1'){
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ function docker($action, $service_name = null, $attr1 = null, $attr2 = null, $ex
|
|||||||
break;
|
break;
|
||||||
case 'info':
|
case 'info':
|
||||||
if (empty($service_name)) {
|
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_RETURNTRANSFER, 1);
|
||||||
curl_setopt($curl, CURLOPT_POST, 0);
|
curl_setopt($curl, CURLOPT_POST, 0);
|
||||||
curl_setopt($curl, CURLOPT_TIMEOUT, $DOCKER_TIMEOUT);
|
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 (!empty($password)) {
|
||||||
if (password_check($password, $password2) !== true) {
|
if (password_check($password, $password2) !== true) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
$password_hashed = hash_password($password);
|
$password_hashed = hash_password($password);
|
||||||
$stmt = $pdo->prepare("UPDATE `admin` SET `username` = :username_new, `active` = :active, `password` = :password_hashed WHERE `username` = :username");
|
$stmt = $pdo->prepare("UPDATE `admin` SET `username` = :username_new, `active` = :active, `password` = :password_hashed,
|
||||||
|
`attributes` = JSON_SET(COALESCE(`attributes`, '{}'), '$.force_tfa', :force_tfa, '$.force_pw_update', :force_pw_update)
|
||||||
|
WHERE `username` = :username");
|
||||||
$stmt->execute(array(
|
$stmt->execute(array(
|
||||||
':password_hashed' => $password_hashed,
|
':password_hashed' => $password_hashed,
|
||||||
':username_new' => $username_new,
|
':username_new' => $username_new,
|
||||||
':username' => $username,
|
':username' => $username,
|
||||||
':active' => $active
|
':active' => $active,
|
||||||
|
':force_tfa' => strval($force_tfa),
|
||||||
|
':force_pw_update' => strval($force_pw_update)
|
||||||
));
|
));
|
||||||
if (isset($_data['disable_tfa'])) {
|
if (isset($_data['disable_tfa'])) {
|
||||||
$stmt = $pdo->prepare("UPDATE `tfa` SET `active` = '0' WHERE `username` = :username");
|
$stmt = $pdo->prepare("UPDATE `tfa` SET `active` = '0' WHERE `username` = :username");
|
||||||
@@ -217,11 +223,15 @@ function domain_admin($_action, $_data = null) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
$stmt = $pdo->prepare("UPDATE `admin` SET `username` = :username_new, `active` = :active WHERE `username` = :username");
|
$stmt = $pdo->prepare("UPDATE `admin` SET `username` = :username_new, `active` = :active,
|
||||||
|
`attributes` = JSON_SET(COALESCE(`attributes`, '{}'), '$.force_tfa', :force_tfa, '$.force_pw_update', :force_pw_update)
|
||||||
|
WHERE `username` = :username");
|
||||||
$stmt->execute(array(
|
$stmt->execute(array(
|
||||||
':username_new' => $username_new,
|
':username_new' => $username_new,
|
||||||
':username' => $username,
|
':username' => $username,
|
||||||
':active' => $active
|
':active' => $active,
|
||||||
|
':force_tfa' => strval($force_tfa),
|
||||||
|
':force_pw_update' => strval($force_pw_update)
|
||||||
));
|
));
|
||||||
if (isset($_data['disable_tfa'])) {
|
if (isset($_data['disable_tfa'])) {
|
||||||
$stmt = $pdo->prepare("UPDATE `tfa` SET `active` = '0' WHERE `username` = :username");
|
$stmt = $pdo->prepare("UPDATE `tfa` SET `active` = '0' WHERE `username` = :username");
|
||||||
@@ -244,31 +254,37 @@ function domain_admin($_action, $_data = null) {
|
|||||||
// Can only edit itself
|
// Can only edit itself
|
||||||
elseif ($_SESSION['mailcow_cc_role'] == "domainadmin") {
|
elseif ($_SESSION['mailcow_cc_role'] == "domainadmin") {
|
||||||
$username = $_SESSION['mailcow_cc_username'];
|
$username = $_SESSION['mailcow_cc_username'];
|
||||||
$password_old = $_data['user_old_pass'];
|
$password_old = $_data['user_old_pass'] ?? '';
|
||||||
$password_new = $_data['user_new_pass'];
|
$password_new = $_data['user_new_pass'];
|
||||||
$password_new2 = $_data['user_new_pass2'];
|
$password_new2 = $_data['user_new_pass2'];
|
||||||
|
|
||||||
$stmt = $pdo->prepare("SELECT `password` FROM `admin`
|
// Only verify old password if this is NOT a forced password update
|
||||||
WHERE `username` = :user");
|
if (empty($_SESSION['pending_pw_update'])) {
|
||||||
$stmt->execute(array(':user' => $username));
|
$stmt = $pdo->prepare("SELECT `password` FROM `admin`
|
||||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
WHERE `username` = :user");
|
||||||
if (!verify_hash($row['password'], $password_old)) {
|
$stmt->execute(array(':user' => $username));
|
||||||
$_SESSION['return'][] = array(
|
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
'type' => 'danger',
|
if (!verify_hash($row['password'], $password_old)) {
|
||||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
$_SESSION['return'][] = array(
|
||||||
'msg' => 'access_denied'
|
'type' => 'danger',
|
||||||
);
|
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||||
return false;
|
'msg' => 'access_denied'
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (password_check($password_new, $password_new2) !== true) {
|
if (password_check($password_new, $password_new2) !== true) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
$password_hashed = hash_password($password_new);
|
$password_hashed = hash_password($password_new);
|
||||||
$stmt = $pdo->prepare("UPDATE `admin` SET `password` = :password_hashed WHERE `username` = :username");
|
$stmt = $pdo->prepare("UPDATE `admin` SET `password` = :password_hashed,
|
||||||
|
`attributes` = JSON_SET(COALESCE(`attributes`, '{}'), '$.force_pw_update', '0')
|
||||||
|
WHERE `username` = :username");
|
||||||
$stmt->execute(array(
|
$stmt->execute(array(
|
||||||
':password_hashed' => $password_hashed,
|
':password_hashed' => $password_hashed,
|
||||||
':username' => $username
|
':username' => $username
|
||||||
));
|
));
|
||||||
|
unset($_SESSION['pending_pw_update']);
|
||||||
$_SESSION['return'][] = array(
|
$_SESSION['return'][] = array(
|
||||||
'type' => 'success',
|
'type' => 'success',
|
||||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||||
@@ -360,9 +376,11 @@ function domain_admin($_action, $_data = null) {
|
|||||||
`tfa`.`active` AS `tfa_active`,
|
`tfa`.`active` AS `tfa_active`,
|
||||||
`domain_admins`.`username`,
|
`domain_admins`.`username`,
|
||||||
`domain_admins`.`created`,
|
`domain_admins`.`created`,
|
||||||
`domain_admins`.`active` AS `active`
|
`domain_admins`.`active` AS `active`,
|
||||||
|
`admin`.`attributes` AS `attributes`
|
||||||
FROM `domain_admins`
|
FROM `domain_admins`
|
||||||
LEFT OUTER JOIN `tfa` ON `tfa`.`username`=`domain_admins`.`username`
|
LEFT OUTER JOIN `tfa` ON `tfa`.`username`=`domain_admins`.`username`
|
||||||
|
LEFT OUTER JOIN `admin` ON `admin`.`username`=`domain_admins`.`username`
|
||||||
WHERE `domain_admins`.`username`= :domain_admin");
|
WHERE `domain_admins`.`username`= :domain_admin");
|
||||||
$stmt->execute(array(
|
$stmt->execute(array(
|
||||||
':domain_admin' => $_data
|
':domain_admin' => $_data
|
||||||
@@ -377,6 +395,7 @@ function domain_admin($_action, $_data = null) {
|
|||||||
$domainadmindata['active'] = $row['active'];
|
$domainadmindata['active'] = $row['active'];
|
||||||
$domainadmindata['active_int'] = $row['active'];
|
$domainadmindata['active_int'] = $row['active'];
|
||||||
$domainadmindata['created'] = $row['created'];
|
$domainadmindata['created'] = $row['created'];
|
||||||
|
$domainadmindata['attributes'] = json_decode($row['attributes'], true) ?? array('force_tfa' => '0', 'force_pw_update' => '0');
|
||||||
// GET SELECTED
|
// GET SELECTED
|
||||||
$stmt = $pdo->prepare("SELECT `domain` FROM `domain`
|
$stmt = $pdo->prepare("SELECT `domain` FROM `domain`
|
||||||
WHERE `domain` IN (
|
WHERE `domain` IN (
|
||||||
@@ -410,14 +429,25 @@ function domain_admin_sso($_action, $_data) {
|
|||||||
|
|
||||||
switch ($_action) {
|
switch ($_action) {
|
||||||
case 'check':
|
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 = $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(
|
$stmt->execute(array(
|
||||||
':token' => preg_replace('/[^a-zA-Z0-9-]/', '', $token)
|
':token' => $token
|
||||||
));
|
));
|
||||||
$return = $stmt->fetch(PDO::FETCH_ASSOC);
|
$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':
|
case 'issue':
|
||||||
if ($_SESSION['mailcow_cc_role'] != "admin") {
|
if ($_SESSION['mailcow_cc_role'] != "admin") {
|
||||||
$_SESSION['return'][] = array(
|
$_SESSION['return'][] = array(
|
||||||
|
|||||||
@@ -108,6 +108,14 @@ function fwdhost($_action, $_data = null) {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'delete':
|
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'];
|
$hosts = (array)$_data['forwardinghost'];
|
||||||
foreach ($hosts as $host) {
|
foreach ($hosts as $host) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
+308
-62
@@ -53,6 +53,39 @@ function valid_network($network) {
|
|||||||
function valid_hostname($hostname) {
|
function valid_hostname($hostname) {
|
||||||
return filter_var($hostname, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_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
|
// Thanks to https://stackoverflow.com/a/49373789
|
||||||
// Validates exact ip matches and ip-in-cidr, ipv4 and ipv6
|
// Validates exact ip matches and ip-in-cidr, ipv4 and ipv6
|
||||||
function ip_acl($ip, $networks) {
|
function ip_acl($ip, $networks) {
|
||||||
@@ -205,6 +238,42 @@ function password_complexity($_action, $_data = null) {
|
|||||||
break;
|
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) {
|
function password_check($password1, $password2) {
|
||||||
$password_complexity = password_complexity('get');
|
$password_complexity = password_complexity('get');
|
||||||
|
|
||||||
@@ -522,7 +591,7 @@ function logger($_data = false) {
|
|||||||
$type = $return['type'];
|
$type = $return['type'];
|
||||||
$msg = null;
|
$msg = null;
|
||||||
if (isset($return['msg'])) {
|
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;
|
$call = null;
|
||||||
if (isset($return['log'])) {
|
if (isset($return['log'])) {
|
||||||
@@ -564,6 +633,18 @@ function logger($_data = false) {
|
|||||||
return true;
|
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) {
|
function hasDomainAccess($username, $role, $domain) {
|
||||||
global $pdo;
|
global $pdo;
|
||||||
if (empty($domain) || !is_valid_domain_name($domain)) {
|
if (empty($domain) || !is_valid_domain_name($domain)) {
|
||||||
@@ -814,6 +895,32 @@ function verify_hash($hash, $password) {
|
|||||||
$hash = $components[4];
|
$hash = $components[4];
|
||||||
return hash_equals(hash_pbkdf2('sha1', $password, $salt, $rounds), $hash);
|
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":
|
case "PLAIN-MD4":
|
||||||
return hash_equals(hash('md4', $password), $hash);
|
return hash_equals(hash('md4', $password), $hash);
|
||||||
|
|
||||||
@@ -971,20 +1078,24 @@ function edit_user_account($_data) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// edit password
|
// edit password
|
||||||
if (!empty($password_old) && !empty($_data['user_new_pass']) && !empty($_data['user_new_pass2'])) {
|
$is_forced_pw_update = !empty($_SESSION['pending_pw_update']);
|
||||||
$stmt = $pdo->prepare("SELECT `password` FROM `mailbox`
|
if (((!empty($password_old) || $is_forced_pw_update) && !empty($_data['user_new_pass']) && !empty($_data['user_new_pass2']))) {
|
||||||
WHERE `kind` NOT REGEXP 'location|thing|group'
|
// Only verify old password if this is NOT a forced password update
|
||||||
AND `username` = :user AND authsource = 'mailcow'");
|
if (!$is_forced_pw_update) {
|
||||||
$stmt->execute(array(':user' => $username));
|
$stmt = $pdo->prepare("SELECT `password` FROM `mailbox`
|
||||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
WHERE `kind` NOT REGEXP 'location|thing|group'
|
||||||
|
AND `username` = :user AND authsource = 'mailcow'");
|
||||||
|
$stmt->execute(array(':user' => $username));
|
||||||
|
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
if (!verify_hash($row['password'], $password_old)) {
|
if (!verify_hash($row['password'], $password_old)) {
|
||||||
$_SESSION['return'][] = array(
|
$_SESSION['return'][] = array(
|
||||||
'type' => 'danger',
|
'type' => 'danger',
|
||||||
'log' => array(__FUNCTION__, $_data_log),
|
'log' => array(__FUNCTION__, $_data_log),
|
||||||
'msg' => 'access_denied'
|
'msg' => 'access_denied'
|
||||||
);
|
);
|
||||||
return false;
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$password_new = $_data['user_new_pass'];
|
$password_new = $_data['user_new_pass'];
|
||||||
@@ -1006,7 +1117,7 @@ function edit_user_account($_data) {
|
|||||||
update_sogo_static_view();
|
update_sogo_static_view();
|
||||||
}
|
}
|
||||||
// edit password recovery email
|
// 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" ) {
|
if (!isset($_SESSION['acl']['pw_reset']) || $_SESSION['acl']['pw_reset'] != "1" ) {
|
||||||
$_SESSION['return'][] = array(
|
$_SESSION['return'][] = array(
|
||||||
'type' => 'danger',
|
'type' => 'danger',
|
||||||
@@ -1016,6 +1127,21 @@ function edit_user_account($_data) {
|
|||||||
return false;
|
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;
|
$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)
|
$stmt = $pdo->prepare("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.recovery_email', :recovery_email)
|
||||||
WHERE `username` = :username AND authsource = 'mailcow'");
|
WHERE `username` = :username AND authsource = 'mailcow'");
|
||||||
@@ -1133,50 +1259,52 @@ function set_tfa($_data) {
|
|||||||
global $iam_settings;
|
global $iam_settings;
|
||||||
|
|
||||||
$_data_log = $_data;
|
$_data_log = $_data;
|
||||||
$access_denied = null;
|
|
||||||
!isset($_data_log['confirm_password']) ?: $_data_log['confirm_password'] = '*';
|
!isset($_data_log['confirm_password']) ?: $_data_log['confirm_password'] = '*';
|
||||||
$username = $_SESSION['mailcow_cc_username'];
|
|
||||||
|
|
||||||
// check for empty user and role
|
// skip password check if this is a forced TFA enrollment after login
|
||||||
if (!isset($_SESSION['mailcow_cc_role']) || empty($username)) $access_denied = true;
|
if (!empty($_SESSION['pending_tfa_setup'])) {
|
||||||
|
$username = $_SESSION['mailcow_cc_username'];
|
||||||
// check admin confirm password
|
if (empty($username) || !isset($_SESSION['mailcow_cc_role'])) {
|
||||||
if ($access_denied === null) {
|
$_SESSION['return'][] = array('type' => 'danger', 'log' => array(__FUNCTION__, $_data_log), 'msg' => 'access_denied');
|
||||||
$stmt = $pdo->prepare("SELECT `password` FROM `admin`
|
return false;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
|
$username = $_SESSION['mailcow_cc_username'];
|
||||||
|
$access_denied = null;
|
||||||
|
|
||||||
// check mailbox confirm password
|
if (!isset($_SESSION['mailcow_cc_role']) || empty($username)) $access_denied = true;
|
||||||
if ($access_denied === null) {
|
|
||||||
$stmt = $pdo->prepare("SELECT `password`, `authsource` FROM `mailbox`
|
// check admin password
|
||||||
WHERE `username` = :username");
|
if ($access_denied === null) {
|
||||||
$stmt->execute(array(':username' => $username));
|
$stmt = $pdo->prepare("SELECT `password` FROM `admin` WHERE `username` = :username");
|
||||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
$stmt->execute(array(':username' => $username));
|
||||||
if ($row) {
|
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
if ($row['authsource'] == 'ldap'){
|
if ($row) {
|
||||||
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;
|
if (!verify_hash($row['password'], $_data["confirm_password"])) $access_denied = true;
|
||||||
else $access_denied = false;
|
else $access_denied = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// set access_denied error
|
// check mailbox password
|
||||||
if ($access_denied){
|
if ($access_denied === null) {
|
||||||
$_SESSION['return'][] = array(
|
$stmt = $pdo->prepare("SELECT `password`, `authsource` FROM `mailbox` WHERE `username` = :username");
|
||||||
'type' => 'danger',
|
$stmt->execute(array(':username' => $username));
|
||||||
'log' => array(__FUNCTION__, $_data_log),
|
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
'msg' => 'access_denied'
|
if ($row) {
|
||||||
);
|
if ($row['authsource'] == 'ldap'){
|
||||||
return false;
|
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"]) {
|
switch ($_data["tfa_method"]) {
|
||||||
@@ -1229,6 +1357,7 @@ function set_tfa($_data) {
|
|||||||
);
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
unset($_SESSION['pending_tfa_setup']);
|
||||||
$_SESSION['return'][] = array(
|
$_SESSION['return'][] = array(
|
||||||
'type' => 'success',
|
'type' => 'success',
|
||||||
'log' => array(__FUNCTION__, $_data_log),
|
'log' => array(__FUNCTION__, $_data_log),
|
||||||
@@ -1242,6 +1371,7 @@ function set_tfa($_data) {
|
|||||||
//$stmt->execute(array(':username' => $username));
|
//$stmt->execute(array(':username' => $username));
|
||||||
$stmt = $pdo->prepare("INSERT INTO `tfa` (`username`, `key_id`, `authmech`, `secret`, `active`) VALUES (?, ?, 'totp', ?, '1')");
|
$stmt = $pdo->prepare("INSERT INTO `tfa` (`username`, `key_id`, `authmech`, `secret`, `active`) VALUES (?, ?, 'totp', ?, '1')");
|
||||||
$stmt->execute(array($username, $key_id, $_POST['totp_secret']));
|
$stmt->execute(array($username, $key_id, $_POST['totp_secret']));
|
||||||
|
unset($_SESSION['pending_tfa_setup']);
|
||||||
$_SESSION['return'][] = array(
|
$_SESSION['return'][] = array(
|
||||||
'type' => 'success',
|
'type' => 'success',
|
||||||
'log' => array(__FUNCTION__, $_data_log),
|
'log' => array(__FUNCTION__, $_data_log),
|
||||||
@@ -1270,6 +1400,7 @@ function set_tfa($_data) {
|
|||||||
0
|
0
|
||||||
));
|
));
|
||||||
|
|
||||||
|
unset($_SESSION['pending_tfa_setup']);
|
||||||
$_SESSION['return'][] = array(
|
$_SESSION['return'][] = array(
|
||||||
'type' => 'success',
|
'type' => 'success',
|
||||||
'log' => array(__FUNCTION__, $_data_log),
|
'log' => array(__FUNCTION__, $_data_log),
|
||||||
@@ -1277,6 +1408,25 @@ function set_tfa($_data) {
|
|||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case "none":
|
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 = $pdo->prepare("DELETE FROM `tfa` WHERE `username` = :username");
|
||||||
$stmt->execute(array(':username' => $username));
|
$stmt->execute(array(':username' => $username));
|
||||||
$_SESSION['return'][] = array(
|
$_SESSION['return'][] = array(
|
||||||
@@ -1529,6 +1679,26 @@ function unset_tfa_key($_data) {
|
|||||||
return false;
|
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
|
// check if it's last key
|
||||||
$stmt = $pdo->prepare("SELECT COUNT(*) AS `keys` FROM `tfa`
|
$stmt = $pdo->prepare("SELECT COUNT(*) AS `keys` FROM `tfa`
|
||||||
WHERE `username` = :username AND `active` = '1'");
|
WHERE `username` = :username AND `active` = '1'");
|
||||||
@@ -1561,6 +1731,15 @@ function unset_tfa_key($_data) {
|
|||||||
return false;
|
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) {
|
function get_tfa($username = null, $id = null) {
|
||||||
global $pdo;
|
global $pdo;
|
||||||
if (empty($username) && isset($_SESSION['mailcow_cc_username'])) {
|
if (empty($username) && isset($_SESSION['mailcow_cc_username'])) {
|
||||||
@@ -2149,10 +2328,13 @@ function cors($action, $data = null) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
$allowed_origins = isset($data['allowed_origins']) ? $data['allowed_origins'] : array($_SERVER['SERVER_NAME']);
|
$allowed_origins = isset($data['allowed_origins']) ? $data['allowed_origins'] : array(getBaseURL());
|
||||||
$allowed_origins = !is_array($allowed_origins) ? array_filter(array_map('trim', explode("\n", $allowed_origins))) : $allowed_origins;
|
$allowed_origins = !is_array($allowed_origins) ? array_filter(array_map('trim', explode("\n", $allowed_origins))) : $allowed_origins;
|
||||||
foreach ($allowed_origins as $origin) {
|
foreach ($allowed_origins as &$origin) {
|
||||||
if (!filter_var($origin, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME) && $origin != '*') {
|
if ($origin === '*') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!valid_origin($origin)) {
|
||||||
$_SESSION['return'][] = array(
|
$_SESSION['return'][] = array(
|
||||||
'type' => 'danger',
|
'type' => 'danger',
|
||||||
'log' => array(__FUNCTION__, $action, $data),
|
'log' => array(__FUNCTION__, $action, $data),
|
||||||
@@ -2160,7 +2342,10 @@ function cors($action, $data = null) {
|
|||||||
);
|
);
|
||||||
return false;
|
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 = 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;
|
$allowed_methods = !is_array($allowed_methods) ? array_map('trim', preg_split( "/( |,|;|\n)/", $allowed_methods)) : $allowed_methods;
|
||||||
@@ -2208,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 = !$cors_settings ? array('allowed_origins' => getBaseURL(), 'allowed_methods' => 'GET, POST, PUT, DELETE') : $cors_settings;
|
||||||
$cors_settings['allowed_origins'] = empty($cors_settings['allowed_origins']) ? $_SERVER['SERVER_NAME'] : $cors_settings['allowed_origins'];
|
$cors_settings['allowed_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'];
|
$cors_settings['allowed_methods'] = empty($cors_settings['allowed_methods']) ? 'GET, POST, PUT, DELETE, OPTION' : $cors_settings['allowed_methods'];
|
||||||
|
|
||||||
return $cors_settings;
|
return $cors_settings;
|
||||||
break;
|
break;
|
||||||
case "set_headers":
|
case "set_headers":
|
||||||
$cors_settings = cors('get');
|
$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
|
// check if requested origin is in allowed origins
|
||||||
$allowed_origins = explode(', ', $cors_settings['allowed_origins']);
|
$allow_origin = null;
|
||||||
$cors_settings['allowed_origins'] = $allowed_origins[0];
|
if (in_array('*', $allowed_origins, true)) {
|
||||||
if (in_array('*', $allowed_origins)){
|
$allow_origin = '*';
|
||||||
$cors_settings['allowed_origins'] = '*';
|
} else if ($origin !== false && in_array($origin, $allowed_origins, true)) {
|
||||||
} else if (array_key_exists('HTTP_ORIGIN', $_SERVER) && in_array($_SERVER['HTTP_ORIGIN'], $allowed_origins)) {
|
$allow_origin = $origin;
|
||||||
$cors_settings['allowed_origins'] = $_SERVER['HTTP_ORIGIN'];
|
|
||||||
}
|
}
|
||||||
// always allow OPTIONS for preflight request
|
// always allow OPTIONS for preflight request
|
||||||
$cors_settings["allowed_methods"] = empty($cors_settings["allowed_methods"]) ? 'OPTIONS' : $cors_settings["allowed_methods"] . ', ' . 'OPTIONS';
|
$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-Methods: '. $cors_settings['allowed_methods']);
|
||||||
header('Access-Control-Allow-Headers: Accept, Content-Type, X-Api-Key, Origin');
|
header('Access-Control-Allow-Headers: Accept, Content-Type, X-Api-Key, Origin');
|
||||||
|
|
||||||
@@ -3363,6 +3557,58 @@ function set_user_loggedin_session($user) {
|
|||||||
unset($_SESSION['pending_mailcow_cc_role']);
|
unset($_SESSION['pending_mailcow_cc_role']);
|
||||||
unset($_SESSION['pending_tfa_methods']);
|
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) {
|
function get_logs($application, $lines = false) {
|
||||||
if ($lines === false) {
|
if ($lines === false) {
|
||||||
$lines = $GLOBALS['LOG_LINES'] - 1;
|
$lines = $GLOBALS['LOG_LINES'] - 1;
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -170,6 +170,8 @@ function quarantine($_action, $_data = null) {
|
|||||||
}
|
}
|
||||||
elseif ($release_format == 'raw') {
|
elseif ($release_format == 'raw') {
|
||||||
$detail_row['msg'] = preg_replace('/^X-Spam-Flag: (.*)/m', 'X-Pre-Release-Spam-Flag: $1', $detail_row['msg']);
|
$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(
|
$postfix_talk = array(
|
||||||
array('220', 'HELO quarantine' . chr(10)),
|
array('220', 'HELO quarantine' . chr(10)),
|
||||||
array('250', 'MAIL FROM: ' . $sender . chr(10)),
|
array('250', 'MAIL FROM: ' . $sender . chr(10)),
|
||||||
@@ -465,12 +467,14 @@ function quarantine($_action, $_data = null) {
|
|||||||
}
|
}
|
||||||
elseif ($release_format == 'raw') {
|
elseif ($release_format == 'raw') {
|
||||||
$row['msg'] = preg_replace('/^X-Spam-Flag: (.*)/m', 'X-Pre-Release-Spam-Flag: $1', $row['msg']);
|
$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(
|
$postfix_talk = array(
|
||||||
array('220', 'HELO quarantine' . chr(10)),
|
array('220', 'HELO quarantine' . chr(10)),
|
||||||
array('250', 'MAIL FROM: ' . $sender . chr(10)),
|
array('250', 'MAIL FROM: ' . $sender . chr(10)),
|
||||||
array('250', 'RCPT TO: ' . $row['rcpt'] . chr(10)),
|
array('250', 'RCPT TO: ' . $row['rcpt'] . chr(10)),
|
||||||
array('250', 'DATA' . 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('250', 'QUIT' . chr(10)),
|
||||||
array('221', '')
|
array('221', '')
|
||||||
);
|
);
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -89,7 +89,7 @@ $globalVariables = [
|
|||||||
'app_links' => $app_links,
|
'app_links' => $app_links,
|
||||||
'app_links_processed' => $app_links_processed,
|
'app_links_processed' => $app_links_processed,
|
||||||
'is_root_uri' => (parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH) == '/'),
|
'is_root_uri' => (parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH) == '/'),
|
||||||
'uri' => $_SERVER['REQUEST_URI'],
|
'uri' => parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH) ?: '/',
|
||||||
];
|
];
|
||||||
|
|
||||||
foreach ($globalVariables as $globalVariableName => $globalVariableValue) {
|
foreach ($globalVariables as $globalVariableName => $globalVariableValue) {
|
||||||
|
|||||||
+212
-21
@@ -4,7 +4,7 @@ function init_db_schema()
|
|||||||
try {
|
try {
|
||||||
global $pdo;
|
global $pdo;
|
||||||
|
|
||||||
$db_version = "07102025_1015";
|
$db_version = "19082026_0900";
|
||||||
|
|
||||||
$stmt = $pdo->query("SHOW TABLES LIKE 'versions'");
|
$stmt = $pdo->query("SHOW TABLES LIKE 'versions'");
|
||||||
$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
|
$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
|
||||||
@@ -76,7 +76,8 @@ function init_db_schema()
|
|||||||
"superadmin" => "TINYINT(1) NOT NULL DEFAULT '0'",
|
"superadmin" => "TINYINT(1) NOT NULL DEFAULT '0'",
|
||||||
"created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)",
|
"created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)",
|
||||||
"modified" => "DATETIME ON UPDATE 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(
|
"keys" => array(
|
||||||
"primary" => array(
|
"primary" => array(
|
||||||
@@ -185,6 +186,7 @@ function init_db_schema()
|
|||||||
"public_comment" => "TEXT",
|
"public_comment" => "TEXT",
|
||||||
"sogo_visible" => "TINYINT(1) NOT NULL DEFAULT '1'",
|
"sogo_visible" => "TINYINT(1) NOT NULL DEFAULT '1'",
|
||||||
"internal" => "TINYINT(1) NOT NULL DEFAULT '0'",
|
"internal" => "TINYINT(1) NOT NULL DEFAULT '0'",
|
||||||
|
"sender_allowed" => "TINYINT(1) NOT NULL DEFAULT '1'",
|
||||||
"active" => "TINYINT(1) NOT NULL DEFAULT '1'"
|
"active" => "TINYINT(1) NOT NULL DEFAULT '1'"
|
||||||
),
|
),
|
||||||
"keys" => array(
|
"keys" => array(
|
||||||
@@ -554,7 +556,8 @@ function init_db_schema()
|
|||||||
"description" => "TEXT NOT NULL",
|
"description" => "TEXT NOT NULL",
|
||||||
"created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)",
|
"created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)",
|
||||||
"modified" => "DATETIME ON UPDATE CURRENT_TIMESTAMP",
|
"modified" => "DATETIME ON UPDATE CURRENT_TIMESTAMP",
|
||||||
"validity" => "INT(11)"
|
"validity" => "INT(11)",
|
||||||
|
"permanent" => "TINYINT(1) NOT NULL DEFAULT '0'"
|
||||||
),
|
),
|
||||||
"keys" => array(
|
"keys" => array(
|
||||||
"primary" => array(
|
"primary" => array(
|
||||||
@@ -716,7 +719,8 @@ function init_db_schema()
|
|||||||
"alias_domains" => "TINYINT(1) NOT NULL DEFAULT '0'",
|
"alias_domains" => "TINYINT(1) NOT NULL DEFAULT '0'",
|
||||||
"mailbox_relayhost" => "TINYINT(1) NOT NULL DEFAULT '1'",
|
"mailbox_relayhost" => "TINYINT(1) NOT NULL DEFAULT '1'",
|
||||||
"domain_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(
|
"keys" => array(
|
||||||
"primary" => array(
|
"primary" => array(
|
||||||
@@ -751,35 +755,145 @@ function init_db_schema()
|
|||||||
),
|
),
|
||||||
"attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC"
|
"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(
|
"imapsync" => array(
|
||||||
"cols" => array(
|
"cols" => array(
|
||||||
"id" => "INT NOT NULL AUTO_INCREMENT",
|
"id" => "INT NOT NULL AUTO_INCREMENT",
|
||||||
"user2" => "VARCHAR(255) NOT NULL",
|
"user2" => "VARCHAR(255) NOT NULL",
|
||||||
"host1" => "VARCHAR(255) NOT NULL",
|
"source_id" => "INT NOT NULL",
|
||||||
"authmech1" => "ENUM('PLAIN','LOGIN','CRAM-MD5') DEFAULT 'PLAIN'",
|
|
||||||
"regextrans2" => "VARCHAR(255) DEFAULT ''",
|
"regextrans2" => "VARCHAR(255) DEFAULT ''",
|
||||||
"authmd51" => "TINYINT(1) NOT NULL DEFAULT 0",
|
"authmd51" => "TINYINT(1) NOT NULL DEFAULT 0",
|
||||||
"domain2" => "VARCHAR(255) NOT NULL DEFAULT ''",
|
"domain2" => "VARCHAR(255) NOT NULL DEFAULT ''",
|
||||||
"subfolder2" => "VARCHAR(255) NOT NULL DEFAULT ''",
|
"subfolder2" => "VARCHAR(255) NOT NULL DEFAULT ''",
|
||||||
"user1" => "VARCHAR(255) NOT NULL",
|
"user1" => "VARCHAR(255) NOT NULL",
|
||||||
"password1" => "VARCHAR(255) NOT NULL",
|
"password1" => "VARCHAR(255) NOT NULL DEFAULT ''",
|
||||||
"exclude" => "VARCHAR(500) NOT NULL DEFAULT ''",
|
"exclude" => "VARCHAR(500) NOT NULL DEFAULT ''",
|
||||||
"maxage" => "SMALLINT NOT NULL DEFAULT '0'",
|
"maxage" => "SMALLINT NOT NULL DEFAULT '0'",
|
||||||
"mins_interval" => "SMALLINT UNSIGNED NOT NULL DEFAULT '0'",
|
"mins_interval" => "SMALLINT UNSIGNED NOT NULL DEFAULT '0'",
|
||||||
"maxbytespersecond" => "VARCHAR(50) 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'",
|
"delete2duplicates" => "TINYINT(1) NOT NULL DEFAULT '1'",
|
||||||
"delete1" => "TINYINT(1) NOT NULL DEFAULT '0'",
|
"delete1" => "TINYINT(1) NOT NULL DEFAULT '0'",
|
||||||
"delete2" => "TINYINT(1) NOT NULL DEFAULT '0'",
|
"delete2" => "TINYINT(1) NOT NULL DEFAULT '0'",
|
||||||
"automap" => "TINYINT(1) NOT NULL DEFAULT '0'",
|
"automap" => "TINYINT(1) NOT NULL DEFAULT '0'",
|
||||||
"skipcrossduplicates" => "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'",
|
"timeout1" => "SMALLINT NOT NULL DEFAULT '600'",
|
||||||
"timeout2" => "SMALLINT NOT NULL DEFAULT '600'",
|
"timeout2" => "SMALLINT NOT NULL DEFAULT '600'",
|
||||||
"subscribeall" => "TINYINT(1) NOT NULL DEFAULT '1'",
|
"subscribeall" => "TINYINT(1) NOT NULL DEFAULT '1'",
|
||||||
"dry" => "TINYINT(1) NOT NULL DEFAULT '0'",
|
"dry" => "TINYINT(1) NOT NULL DEFAULT '0'",
|
||||||
"is_running" => "TINYINT(1) NOT NULL DEFAULT '0'",
|
"is_running" => "TINYINT(1) NOT NULL DEFAULT '0'",
|
||||||
|
"prio" => "INT NOT NULL DEFAULT 0",
|
||||||
"returned_text" => "LONGTEXT",
|
"returned_text" => "LONGTEXT",
|
||||||
"last_run" => "TIMESTAMP NULL DEFAULT NULL",
|
"last_run" => "TIMESTAMP NULL DEFAULT NULL",
|
||||||
"success" => "TINYINT(1) UNSIGNED DEFAULT NULL",
|
"success" => "TINYINT(1) UNSIGNED DEFAULT NULL",
|
||||||
@@ -791,6 +905,32 @@ function init_db_schema()
|
|||||||
"keys" => array(
|
"keys" => array(
|
||||||
"primary" => array(
|
"primary" => array(
|
||||||
"" => array("id")
|
"" => 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"
|
"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 . "'");
|
$stmt = $pdo->query("SHOW TABLES LIKE '" . $table . "'");
|
||||||
$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
|
$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
|
||||||
if ($num_results != 0) {
|
if ($num_results != 0) {
|
||||||
@@ -1345,20 +1512,34 @@ function init_db_schema()
|
|||||||
AND (JSON_CONTAINS_PATH(`call`, 'one', '$[2].password')
|
AND (JSON_CONTAINS_PATH(`call`, 'one', '$[2].password')
|
||||||
OR JSON_CONTAINS_PATH(`call`, 'one', '$[2].password2'));");
|
OR JSON_CONTAINS_PATH(`call`, 'one', '$[2].password2'));");
|
||||||
|
|
||||||
// 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%';");
|
|
||||||
|
|
||||||
// Migrate webauthn tfa
|
// Migrate webauthn tfa
|
||||||
$stmt = $pdo->query("ALTER TABLE `tfa` MODIFY COLUMN `authmech` ENUM('yubi_otp', 'u2f', 'hotp', 'totp', 'webauthn')");
|
$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
|
// Inject admin if not exists
|
||||||
$stmt = $pdo->query("SELECT NULL FROM `admin`");
|
$stmt = $pdo->query("SELECT NULL FROM `admin`");
|
||||||
$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
|
$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
|
||||||
@@ -1388,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`, '$.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`, '$.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_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`, '$.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`, '$.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`, '$.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`, '$.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`, '$.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`, '$.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_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;");
|
$pdo->query("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.quarantine_category', \"reject\") WHERE JSON_VALUE(`attributes`, '$.quarantine_category') IS NULL;");
|
||||||
@@ -1445,6 +1633,7 @@ function init_db_schema()
|
|||||||
"rl_frame" => "s",
|
"rl_frame" => "s",
|
||||||
"rl_value" => "",
|
"rl_value" => "",
|
||||||
"force_pw_update" => intval($GLOBALS['MAILBOX_DEFAULT_ATTRIBUTES']['force_pw_update']),
|
"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']),
|
"sogo_access" => intval($GLOBALS['MAILBOX_DEFAULT_ATTRIBUTES']['sogo_access']),
|
||||||
"active" => 1,
|
"active" => 1,
|
||||||
"tls_enforce_in" => intval($GLOBALS['MAILBOX_DEFAULT_ATTRIBUTES']['tls_enforce_in']),
|
"tls_enforce_in" => intval($GLOBALS['MAILBOX_DEFAULT_ATTRIBUTES']['tls_enforce_in']),
|
||||||
@@ -1453,6 +1642,8 @@ function init_db_schema()
|
|||||||
"pop3_access" => intval($GLOBALS['MAILBOX_DEFAULT_ATTRIBUTES']['pop3_access']),
|
"pop3_access" => intval($GLOBALS['MAILBOX_DEFAULT_ATTRIBUTES']['pop3_access']),
|
||||||
"smtp_access" => intval($GLOBALS['MAILBOX_DEFAULT_ATTRIBUTES']['smtp_access']),
|
"smtp_access" => intval($GLOBALS['MAILBOX_DEFAULT_ATTRIBUTES']['smtp_access']),
|
||||||
"sieve_access" => intval($GLOBALS['MAILBOX_DEFAULT_ATTRIBUTES']['sieve_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_spam_alias" => 1,
|
||||||
"acl_tls_policy" => 1,
|
"acl_tls_policy" => 1,
|
||||||
"acl_spam_score" => 1,
|
"acl_spam_score" => 1,
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
"directorytree/ldaprecord": "^3.3",
|
"directorytree/ldaprecord": "^3.3",
|
||||||
"twig/twig": "^3.0",
|
"twig/twig": "^3.0",
|
||||||
"stevenmaguire/oauth2-keycloak": "^4.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",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "8f5a147cdb147b935a158b86f47a4747",
|
"content-hash": "50fb4a320500820e36f30eabc45222a0",
|
||||||
"packages": [
|
"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",
|
"name": "bshaffer/oauth2-server-php",
|
||||||
"version": "v1.11.1",
|
"version": "v1.11.1",
|
||||||
@@ -137,6 +191,56 @@
|
|||||||
],
|
],
|
||||||
"time": "2024-02-09T16:56:22+00:00"
|
"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",
|
"name": "ddeboer/imap",
|
||||||
"version": "1.13.1",
|
"version": "1.13.1",
|
||||||
@@ -214,30 +318,32 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "directorytree/ldaprecord",
|
"name": "directorytree/ldaprecord",
|
||||||
"version": "v2.20.5",
|
"version": "v3.8.5",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/DirectoryTree/LdapRecord.git",
|
"url": "https://github.com/DirectoryTree/LdapRecord.git",
|
||||||
"reference": "5bd0a5a9d257cf1049ae83055dbba4c3479ddf16"
|
"reference": "00e5f088f8c4028d5f398783cccc2e8119a27a65"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/DirectoryTree/LdapRecord/zipball/5bd0a5a9d257cf1049ae83055dbba4c3479ddf16",
|
"url": "https://api.github.com/repos/DirectoryTree/LdapRecord/zipball/00e5f088f8c4028d5f398783cccc2e8119a27a65",
|
||||||
"reference": "5bd0a5a9d257cf1049ae83055dbba4c3479ddf16",
|
"reference": "00e5f088f8c4028d5f398783cccc2e8119a27a65",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
|
"ext-iconv": "*",
|
||||||
"ext-json": "*",
|
"ext-json": "*",
|
||||||
"ext-ldap": "*",
|
"ext-ldap": "*",
|
||||||
"illuminate/contracts": "^5.0|^6.0|^7.0|^8.0|^9.0|^10.0",
|
"illuminate/collections": "^8.0|^9.0|^10.0|^11.0|^12.0",
|
||||||
"nesbot/carbon": "^1.0|^2.0",
|
"illuminate/contracts": "^8.0|^9.0|^10.0|^11.0|^12.0",
|
||||||
"php": ">=7.3",
|
"nesbot/carbon": "*",
|
||||||
"psr/log": "^1.0|^2.0|^3.0",
|
"php": ">=8.1",
|
||||||
"psr/simple-cache": "^1.0|^2.0",
|
"psr/log": "*",
|
||||||
"symfony/polyfill-php80": "^1.25",
|
"psr/simple-cache": "^1.0|^2.0|^3.0"
|
||||||
"tightenco/collect": "^5.6|^6.0|^7.0|^8.0|^9.0"
|
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
|
"fakerphp/faker": "^1.21",
|
||||||
|
"laravel/pint": "^1.6",
|
||||||
"mockery/mockery": "^1.0",
|
"mockery/mockery": "^1.0",
|
||||||
"phpunit/phpunit": "^9.0",
|
"phpunit/phpunit": "^9.0",
|
||||||
"spatie/ray": "^1.24"
|
"spatie/ray": "^1.24"
|
||||||
@@ -284,7 +390,7 @@
|
|||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2023-10-11T16:34:34+00:00"
|
"time": "2025-10-06T02:22:34+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "firebase/php-jwt",
|
"name": "firebase/php-jwt",
|
||||||
@@ -677,6 +783,107 @@
|
|||||||
],
|
],
|
||||||
"time": "2023-04-17T16:00:45+00:00"
|
"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",
|
"name": "illuminate/contracts",
|
||||||
"version": "v10.44.0",
|
"version": "v10.44.0",
|
||||||
@@ -725,6 +932,52 @@
|
|||||||
},
|
},
|
||||||
"time": "2024-01-15T18:52:32+00:00"
|
"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",
|
"name": "league/oauth2-client",
|
||||||
"version": "2.7.0",
|
"version": "2.7.0",
|
||||||
@@ -2452,145 +2705,6 @@
|
|||||||
],
|
],
|
||||||
"time": "2023-12-26T14:02:43+00:00"
|
"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",
|
"name": "twig/twig",
|
||||||
"version": "v3.14.0",
|
"version": "v3.14.0",
|
||||||
@@ -2674,10 +2788,10 @@
|
|||||||
"packages-dev": [],
|
"packages-dev": [],
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
"minimum-stability": "stable",
|
"minimum-stability": "stable",
|
||||||
"stability-flags": [],
|
"stability-flags": {},
|
||||||
"prefer-stable": false,
|
"prefer-stable": false,
|
||||||
"prefer-lowest": false,
|
"prefer-lowest": false,
|
||||||
"platform": [],
|
"platform": {},
|
||||||
"platform-dev": [],
|
"platform-dev": {},
|
||||||
"plugin-api-version": "2.6.0"
|
"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
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
{
|
||||||
|
"name": "bacon/bacon-qr-code",
|
||||||
|
"description": "BaconQrCode is a QR code generator for PHP.",
|
||||||
|
"license" : "BSD-2-Clause",
|
||||||
|
"homepage": "https://github.com/Bacon/BaconQrCode",
|
||||||
|
"require": {
|
||||||
|
"php": "^7.1 || ^8.0",
|
||||||
|
"ext-iconv": "*",
|
||||||
|
"dasprid/enum": "^1.0.3"
|
||||||
|
},
|
||||||
|
"suggest": {
|
||||||
|
"ext-imagick": "to generate QR code images"
|
||||||
|
},
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Ben Scholzen 'DASPRiD'",
|
||||||
|
"email": "mail@dasprids.de",
|
||||||
|
"homepage": "https://dasprids.de/",
|
||||||
|
"role": "Developer"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"BaconQrCode\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"phpunit/phpunit": "^7 | ^8 | ^9",
|
||||||
|
"spatie/phpunit-snapshot-assertions": "^4.2.9",
|
||||||
|
"squizlabs/php_codesniffer": "^3.4",
|
||||||
|
"phly/keep-a-changelog": "^2.1"
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"allow-plugins": {
|
||||||
|
"ocramius/package-versions": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"archive": {
|
||||||
|
"exclude": [
|
||||||
|
"/test",
|
||||||
|
"/phpunit.xml.dist"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd" bootstrap="vendor/autoload.php" colors="true">
|
||||||
|
<coverage processUncoveredFiles="true">
|
||||||
|
<include>
|
||||||
|
<directory suffix=".php">src</directory>
|
||||||
|
</include>
|
||||||
|
</coverage>
|
||||||
|
<testsuites>
|
||||||
|
<testsuite name="BaconQrCode Tests">
|
||||||
|
<directory>./test</directory>
|
||||||
|
</testsuite>
|
||||||
|
</testsuites>
|
||||||
|
</phpunit>
|
||||||
@@ -0,0 +1,372 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types = 1);
|
||||||
|
|
||||||
|
namespace BaconQrCode\Common;
|
||||||
|
|
||||||
|
use BaconQrCode\Exception\InvalidArgumentException;
|
||||||
|
use SplFixedArray;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A simple, fast array of bits.
|
||||||
|
*/
|
||||||
|
final class BitArray
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Bits represented as an array of integers.
|
||||||
|
*
|
||||||
|
* @var SplFixedArray<int>
|
||||||
|
*/
|
||||||
|
private $bits;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Size of the bit array in bits.
|
||||||
|
*
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
private $size;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new bit array with a given size.
|
||||||
|
*/
|
||||||
|
public function __construct(int $size = 0)
|
||||||
|
{
|
||||||
|
$this->size = $size;
|
||||||
|
$this->bits = SplFixedArray::fromArray(array_fill(0, ($this->size + 31) >> 3, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the size in bits.
|
||||||
|
*/
|
||||||
|
public function getSize() : int
|
||||||
|
{
|
||||||
|
return $this->size;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the size in bytes.
|
||||||
|
*/
|
||||||
|
public function getSizeInBytes() : int
|
||||||
|
{
|
||||||
|
return ($this->size + 7) >> 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensures that the array has a minimum capacity.
|
||||||
|
*/
|
||||||
|
public function ensureCapacity(int $size) : void
|
||||||
|
{
|
||||||
|
if ($size > count($this->bits) << 5) {
|
||||||
|
$this->bits->setSize(($size + 31) >> 5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a specific bit.
|
||||||
|
*/
|
||||||
|
public function get(int $i) : bool
|
||||||
|
{
|
||||||
|
return 0 !== ($this->bits[$i >> 5] & (1 << ($i & 0x1f)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets a specific bit.
|
||||||
|
*/
|
||||||
|
public function set(int $i) : void
|
||||||
|
{
|
||||||
|
$this->bits[$i >> 5] = $this->bits[$i >> 5] | 1 << ($i & 0x1f);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flips a specific bit.
|
||||||
|
*/
|
||||||
|
public function flip(int $i) : void
|
||||||
|
{
|
||||||
|
$this->bits[$i >> 5] ^= 1 << ($i & 0x1f);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the next set bit position from a given position.
|
||||||
|
*/
|
||||||
|
public function getNextSet(int $from) : int
|
||||||
|
{
|
||||||
|
if ($from >= $this->size) {
|
||||||
|
return $this->size;
|
||||||
|
}
|
||||||
|
|
||||||
|
$bitsOffset = $from >> 5;
|
||||||
|
$currentBits = $this->bits[$bitsOffset];
|
||||||
|
$bitsLength = count($this->bits);
|
||||||
|
$currentBits &= ~((1 << ($from & 0x1f)) - 1);
|
||||||
|
|
||||||
|
while (0 === $currentBits) {
|
||||||
|
if (++$bitsOffset === $bitsLength) {
|
||||||
|
return $this->size;
|
||||||
|
}
|
||||||
|
|
||||||
|
$currentBits = $this->bits[$bitsOffset];
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = ($bitsOffset << 5) + BitUtils::numberOfTrailingZeros($currentBits);
|
||||||
|
return $result > $this->size ? $this->size : $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the next unset bit position from a given position.
|
||||||
|
*/
|
||||||
|
public function getNextUnset(int $from) : int
|
||||||
|
{
|
||||||
|
if ($from >= $this->size) {
|
||||||
|
return $this->size;
|
||||||
|
}
|
||||||
|
|
||||||
|
$bitsOffset = $from >> 5;
|
||||||
|
$currentBits = ~$this->bits[$bitsOffset];
|
||||||
|
$bitsLength = count($this->bits);
|
||||||
|
$currentBits &= ~((1 << ($from & 0x1f)) - 1);
|
||||||
|
|
||||||
|
while (0 === $currentBits) {
|
||||||
|
if (++$bitsOffset === $bitsLength) {
|
||||||
|
return $this->size;
|
||||||
|
}
|
||||||
|
|
||||||
|
$currentBits = ~$this->bits[$bitsOffset];
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = ($bitsOffset << 5) + BitUtils::numberOfTrailingZeros($currentBits);
|
||||||
|
return $result > $this->size ? $this->size : $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets a bulk of bits.
|
||||||
|
*/
|
||||||
|
public function setBulk(int $i, int $newBits) : void
|
||||||
|
{
|
||||||
|
$this->bits[$i >> 5] = $newBits;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets a range of bits.
|
||||||
|
*
|
||||||
|
* @throws InvalidArgumentException if end is smaller than start
|
||||||
|
*/
|
||||||
|
public function setRange(int $start, int $end) : void
|
||||||
|
{
|
||||||
|
if ($end < $start) {
|
||||||
|
throw new InvalidArgumentException('End must be greater or equal to start');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($end === $start) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
--$end;
|
||||||
|
|
||||||
|
$firstInt = $start >> 5;
|
||||||
|
$lastInt = $end >> 5;
|
||||||
|
|
||||||
|
for ($i = $firstInt; $i <= $lastInt; ++$i) {
|
||||||
|
$firstBit = $i > $firstInt ? 0 : $start & 0x1f;
|
||||||
|
$lastBit = $i < $lastInt ? 31 : $end & 0x1f;
|
||||||
|
|
||||||
|
if (0 === $firstBit && 31 === $lastBit) {
|
||||||
|
$mask = 0x7fffffff;
|
||||||
|
} else {
|
||||||
|
$mask = 0;
|
||||||
|
|
||||||
|
for ($j = $firstBit; $j < $lastBit; ++$j) {
|
||||||
|
$mask |= 1 << $j;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->bits[$i] = $this->bits[$i] | $mask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears the bit array, unsetting every bit.
|
||||||
|
*/
|
||||||
|
public function clear() : void
|
||||||
|
{
|
||||||
|
$bitsLength = count($this->bits);
|
||||||
|
|
||||||
|
for ($i = 0; $i < $bitsLength; ++$i) {
|
||||||
|
$this->bits[$i] = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a range of bits is set or not set.
|
||||||
|
|
||||||
|
* @throws InvalidArgumentException if end is smaller than start
|
||||||
|
*/
|
||||||
|
public function isRange(int $start, int $end, bool $value) : bool
|
||||||
|
{
|
||||||
|
if ($end < $start) {
|
||||||
|
throw new InvalidArgumentException('End must be greater or equal to start');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($end === $start) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
--$end;
|
||||||
|
|
||||||
|
$firstInt = $start >> 5;
|
||||||
|
$lastInt = $end >> 5;
|
||||||
|
|
||||||
|
for ($i = $firstInt; $i <= $lastInt; ++$i) {
|
||||||
|
$firstBit = $i > $firstInt ? 0 : $start & 0x1f;
|
||||||
|
$lastBit = $i < $lastInt ? 31 : $end & 0x1f;
|
||||||
|
|
||||||
|
if (0 === $firstBit && 31 === $lastBit) {
|
||||||
|
$mask = 0x7fffffff;
|
||||||
|
} else {
|
||||||
|
$mask = 0;
|
||||||
|
|
||||||
|
for ($j = $firstBit; $j <= $lastBit; ++$j) {
|
||||||
|
$mask |= 1 << $j;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (($this->bits[$i] & $mask) !== ($value ? $mask : 0)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Appends a bit to the array.
|
||||||
|
*/
|
||||||
|
public function appendBit(bool $bit) : void
|
||||||
|
{
|
||||||
|
$this->ensureCapacity($this->size + 1);
|
||||||
|
|
||||||
|
if ($bit) {
|
||||||
|
$this->bits[$this->size >> 5] = $this->bits[$this->size >> 5] | (1 << ($this->size & 0x1f));
|
||||||
|
}
|
||||||
|
|
||||||
|
++$this->size;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Appends a number of bits (up to 32) to the array.
|
||||||
|
|
||||||
|
* @throws InvalidArgumentException if num bits is not between 0 and 32
|
||||||
|
*/
|
||||||
|
public function appendBits(int $value, int $numBits) : void
|
||||||
|
{
|
||||||
|
if ($numBits < 0 || $numBits > 32) {
|
||||||
|
throw new InvalidArgumentException('Num bits must be between 0 and 32');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->ensureCapacity($this->size + $numBits);
|
||||||
|
|
||||||
|
for ($numBitsLeft = $numBits; $numBitsLeft > 0; $numBitsLeft--) {
|
||||||
|
$this->appendBit((($value >> ($numBitsLeft - 1)) & 0x01) === 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Appends another bit array to this array.
|
||||||
|
*/
|
||||||
|
public function appendBitArray(self $other) : void
|
||||||
|
{
|
||||||
|
$otherSize = $other->getSize();
|
||||||
|
$this->ensureCapacity($this->size + $other->getSize());
|
||||||
|
|
||||||
|
for ($i = 0; $i < $otherSize; ++$i) {
|
||||||
|
$this->appendBit($other->get($i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Makes an exclusive-or comparision on the current bit array.
|
||||||
|
*
|
||||||
|
* @throws InvalidArgumentException if sizes don't match
|
||||||
|
*/
|
||||||
|
public function xorBits(self $other) : void
|
||||||
|
{
|
||||||
|
$bitsLength = count($this->bits);
|
||||||
|
$otherBits = $other->getBitArray();
|
||||||
|
|
||||||
|
if ($bitsLength !== count($otherBits)) {
|
||||||
|
throw new InvalidArgumentException('Sizes don\'t match');
|
||||||
|
}
|
||||||
|
|
||||||
|
for ($i = 0; $i < $bitsLength; ++$i) {
|
||||||
|
$this->bits[$i] = $this->bits[$i] ^ $otherBits[$i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts the bit array to a byte array.
|
||||||
|
*
|
||||||
|
* @return SplFixedArray<int>
|
||||||
|
*/
|
||||||
|
public function toBytes(int $bitOffset, int $numBytes) : SplFixedArray
|
||||||
|
{
|
||||||
|
$bytes = new SplFixedArray($numBytes);
|
||||||
|
|
||||||
|
for ($i = 0; $i < $numBytes; ++$i) {
|
||||||
|
$byte = 0;
|
||||||
|
|
||||||
|
for ($j = 0; $j < 8; ++$j) {
|
||||||
|
if ($this->get($bitOffset)) {
|
||||||
|
$byte |= 1 << (7 - $j);
|
||||||
|
}
|
||||||
|
|
||||||
|
++$bitOffset;
|
||||||
|
}
|
||||||
|
|
||||||
|
$bytes[$i] = $byte;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the internal bit array.
|
||||||
|
*
|
||||||
|
* @return SplFixedArray<int>
|
||||||
|
*/
|
||||||
|
public function getBitArray() : SplFixedArray
|
||||||
|
{
|
||||||
|
return $this->bits;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverses the array.
|
||||||
|
*/
|
||||||
|
public function reverse() : void
|
||||||
|
{
|
||||||
|
$newBits = new SplFixedArray(count($this->bits));
|
||||||
|
|
||||||
|
for ($i = 0; $i < $this->size; ++$i) {
|
||||||
|
if ($this->get($this->size - $i - 1)) {
|
||||||
|
$newBits[$i >> 5] = $newBits[$i >> 5] | (1 << ($i & 0x1f));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->bits = $newBits;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a string representation of the bit array.
|
||||||
|
*/
|
||||||
|
public function __toString() : string
|
||||||
|
{
|
||||||
|
$result = '';
|
||||||
|
|
||||||
|
for ($i = 0; $i < $this->size; ++$i) {
|
||||||
|
if (0 === ($i & 0x07)) {
|
||||||
|
$result .= ' ';
|
||||||
|
}
|
||||||
|
|
||||||
|
$result .= $this->get($i) ? 'X' : '.';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,313 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types = 1);
|
||||||
|
|
||||||
|
namespace BaconQrCode\Common;
|
||||||
|
|
||||||
|
use BaconQrCode\Exception\InvalidArgumentException;
|
||||||
|
use SplFixedArray;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bit matrix.
|
||||||
|
*
|
||||||
|
* Represents a 2D matrix of bits. In function arguments below, and throughout
|
||||||
|
* the common module, x is the column position, and y is the row position. The
|
||||||
|
* ordering is always x, y. The origin is at the top-left.
|
||||||
|
*/
|
||||||
|
class BitMatrix
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Width of the bit matrix.
|
||||||
|
*
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
private $width;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Height of the bit matrix.
|
||||||
|
*
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
private $height;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Size in bits of each individual row.
|
||||||
|
*
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
private $rowSize;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bits representation.
|
||||||
|
*
|
||||||
|
* @var SplFixedArray<int>
|
||||||
|
*/
|
||||||
|
private $bits;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws InvalidArgumentException if a dimension is smaller than zero
|
||||||
|
*/
|
||||||
|
public function __construct(int $width, int $height = null)
|
||||||
|
{
|
||||||
|
if (null === $height) {
|
||||||
|
$height = $width;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($width < 1 || $height < 1) {
|
||||||
|
throw new InvalidArgumentException('Both dimensions must be greater than zero');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->width = $width;
|
||||||
|
$this->height = $height;
|
||||||
|
$this->rowSize = ($width + 31) >> 5;
|
||||||
|
$this->bits = SplFixedArray::fromArray(array_fill(0, $this->rowSize * $height, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the requested bit, where true means black.
|
||||||
|
*/
|
||||||
|
public function get(int $x, int $y) : bool
|
||||||
|
{
|
||||||
|
$offset = $y * $this->rowSize + ($x >> 5);
|
||||||
|
return 0 !== (BitUtils::unsignedRightShift($this->bits[$offset], ($x & 0x1f)) & 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the given bit to true.
|
||||||
|
*/
|
||||||
|
public function set(int $x, int $y) : void
|
||||||
|
{
|
||||||
|
$offset = $y * $this->rowSize + ($x >> 5);
|
||||||
|
$this->bits[$offset] = $this->bits[$offset] | (1 << ($x & 0x1f));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flips the given bit.
|
||||||
|
*/
|
||||||
|
public function flip(int $x, int $y) : void
|
||||||
|
{
|
||||||
|
$offset = $y * $this->rowSize + ($x >> 5);
|
||||||
|
$this->bits[$offset] = $this->bits[$offset] ^ (1 << ($x & 0x1f));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears all bits (set to false).
|
||||||
|
*/
|
||||||
|
public function clear() : void
|
||||||
|
{
|
||||||
|
$max = count($this->bits);
|
||||||
|
|
||||||
|
for ($i = 0; $i < $max; ++$i) {
|
||||||
|
$this->bits[$i] = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets a square region of the bit matrix to true.
|
||||||
|
*
|
||||||
|
* @throws InvalidArgumentException if left or top are negative
|
||||||
|
* @throws InvalidArgumentException if width or height are smaller than 1
|
||||||
|
* @throws InvalidArgumentException if region does not fit into the matix
|
||||||
|
*/
|
||||||
|
public function setRegion(int $left, int $top, int $width, int $height) : void
|
||||||
|
{
|
||||||
|
if ($top < 0 || $left < 0) {
|
||||||
|
throw new InvalidArgumentException('Left and top must be non-negative');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($height < 1 || $width < 1) {
|
||||||
|
throw new InvalidArgumentException('Width and height must be at least 1');
|
||||||
|
}
|
||||||
|
|
||||||
|
$right = $left + $width;
|
||||||
|
$bottom = $top + $height;
|
||||||
|
|
||||||
|
if ($bottom > $this->height || $right > $this->width) {
|
||||||
|
throw new InvalidArgumentException('The region must fit inside the matrix');
|
||||||
|
}
|
||||||
|
|
||||||
|
for ($y = $top; $y < $bottom; ++$y) {
|
||||||
|
$offset = $y * $this->rowSize;
|
||||||
|
|
||||||
|
for ($x = $left; $x < $right; ++$x) {
|
||||||
|
$index = $offset + ($x >> 5);
|
||||||
|
$this->bits[$index] = $this->bits[$index] | (1 << ($x & 0x1f));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A fast method to retrieve one row of data from the matrix as a BitArray.
|
||||||
|
*/
|
||||||
|
public function getRow(int $y, BitArray $row = null) : BitArray
|
||||||
|
{
|
||||||
|
if (null === $row || $row->getSize() < $this->width) {
|
||||||
|
$row = new BitArray($this->width);
|
||||||
|
}
|
||||||
|
|
||||||
|
$offset = $y * $this->rowSize;
|
||||||
|
|
||||||
|
for ($x = 0; $x < $this->rowSize; ++$x) {
|
||||||
|
$row->setBulk($x << 5, $this->bits[$offset + $x]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $row;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets a row of data from a BitArray.
|
||||||
|
*/
|
||||||
|
public function setRow(int $y, BitArray $row) : void
|
||||||
|
{
|
||||||
|
$bits = $row->getBitArray();
|
||||||
|
|
||||||
|
for ($i = 0; $i < $this->rowSize; ++$i) {
|
||||||
|
$this->bits[$y * $this->rowSize + $i] = $bits[$i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is useful in detecting the enclosing rectangle of a 'pure' barcode.
|
||||||
|
*
|
||||||
|
* @return int[]|null
|
||||||
|
*/
|
||||||
|
public function getEnclosingRectangle() : ?array
|
||||||
|
{
|
||||||
|
$left = $this->width;
|
||||||
|
$top = $this->height;
|
||||||
|
$right = -1;
|
||||||
|
$bottom = -1;
|
||||||
|
|
||||||
|
for ($y = 0; $y < $this->height; ++$y) {
|
||||||
|
for ($x32 = 0; $x32 < $this->rowSize; ++$x32) {
|
||||||
|
$bits = $this->bits[$y * $this->rowSize + $x32];
|
||||||
|
|
||||||
|
if (0 !== $bits) {
|
||||||
|
if ($y < $top) {
|
||||||
|
$top = $y;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($y > $bottom) {
|
||||||
|
$bottom = $y;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($x32 * 32 < $left) {
|
||||||
|
$bit = 0;
|
||||||
|
|
||||||
|
while (($bits << (31 - $bit)) === 0) {
|
||||||
|
$bit++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (($x32 * 32 + $bit) < $left) {
|
||||||
|
$left = $x32 * 32 + $bit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($x32 * 32 + 31 > $right) {
|
||||||
|
$bit = 31;
|
||||||
|
|
||||||
|
while (0 === BitUtils::unsignedRightShift($bits, $bit)) {
|
||||||
|
--$bit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (($x32 * 32 + $bit) > $right) {
|
||||||
|
$right = $x32 * 32 + $bit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$width = $right - $left;
|
||||||
|
$height = $bottom - $top;
|
||||||
|
|
||||||
|
if ($width < 0 || $height < 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [$left, $top, $width, $height];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the most top left set bit.
|
||||||
|
*
|
||||||
|
* This is useful in detecting a corner of a 'pure' barcode.
|
||||||
|
*
|
||||||
|
* @return int[]|null
|
||||||
|
*/
|
||||||
|
public function getTopLeftOnBit() : ?array
|
||||||
|
{
|
||||||
|
$bitsOffset = 0;
|
||||||
|
|
||||||
|
while ($bitsOffset < count($this->bits) && 0 === $this->bits[$bitsOffset]) {
|
||||||
|
++$bitsOffset;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count($this->bits) === $bitsOffset) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$x = intdiv($bitsOffset, $this->rowSize);
|
||||||
|
$y = ($bitsOffset % $this->rowSize) << 5;
|
||||||
|
|
||||||
|
$bits = $this->bits[$bitsOffset];
|
||||||
|
$bit = 0;
|
||||||
|
|
||||||
|
while (0 === ($bits << (31 - $bit))) {
|
||||||
|
++$bit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$x += $bit;
|
||||||
|
|
||||||
|
return [$x, $y];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the most bottom right set bit.
|
||||||
|
*
|
||||||
|
* This is useful in detecting a corner of a 'pure' barcode.
|
||||||
|
*
|
||||||
|
* @return int[]|null
|
||||||
|
*/
|
||||||
|
public function getBottomRightOnBit() : ?array
|
||||||
|
{
|
||||||
|
$bitsOffset = count($this->bits) - 1;
|
||||||
|
|
||||||
|
while ($bitsOffset >= 0 && 0 === $this->bits[$bitsOffset]) {
|
||||||
|
--$bitsOffset;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($bitsOffset < 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$x = intdiv($bitsOffset, $this->rowSize);
|
||||||
|
$y = ($bitsOffset % $this->rowSize) << 5;
|
||||||
|
|
||||||
|
$bits = $this->bits[$bitsOffset];
|
||||||
|
$bit = 0;
|
||||||
|
|
||||||
|
while (0 === BitUtils::unsignedRightShift($bits, $bit)) {
|
||||||
|
--$bit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$x += $bit;
|
||||||
|
|
||||||
|
return [$x, $y];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the width of the matrix,
|
||||||
|
*/
|
||||||
|
public function getWidth() : int
|
||||||
|
{
|
||||||
|
return $this->width;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the height of the matrix.
|
||||||
|
*/
|
||||||
|
public function getHeight() : int
|
||||||
|
{
|
||||||
|
return $this->height;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types = 1);
|
||||||
|
|
||||||
|
namespace BaconQrCode\Common;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* General bit utilities.
|
||||||
|
*
|
||||||
|
* All utility methods are based on 32-bit integers and also work on 64-bit
|
||||||
|
* systems.
|
||||||
|
*/
|
||||||
|
final class BitUtils
|
||||||
|
{
|
||||||
|
private function __construct()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs an unsigned right shift.
|
||||||
|
*
|
||||||
|
* This is the same as the unsigned right shift operator ">>>" in other
|
||||||
|
* languages.
|
||||||
|
*/
|
||||||
|
public static function unsignedRightShift(int $a, int $b) : int
|
||||||
|
{
|
||||||
|
return (
|
||||||
|
$a >= 0
|
||||||
|
? $a >> $b
|
||||||
|
: (($a & 0x7fffffff) >> $b) | (0x40000000 >> ($b - 1))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the number of trailing zeros.
|
||||||
|
*/
|
||||||
|
public static function numberOfTrailingZeros(int $i) : int
|
||||||
|
{
|
||||||
|
$lastPos = strrpos(str_pad(decbin($i), 32, '0', STR_PAD_LEFT), '1');
|
||||||
|
return $lastPos === false ? 32 : 31 - $lastPos;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,183 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types = 1);
|
||||||
|
|
||||||
|
namespace BaconQrCode\Common;
|
||||||
|
|
||||||
|
use BaconQrCode\Exception\InvalidArgumentException;
|
||||||
|
use DASPRiD\Enum\AbstractEnum;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encapsulates a Character Set ECI, according to "Extended Channel Interpretations" 5.3.1.1 of ISO 18004.
|
||||||
|
*
|
||||||
|
* @method static self CP437()
|
||||||
|
* @method static self ISO8859_1()
|
||||||
|
* @method static self ISO8859_2()
|
||||||
|
* @method static self ISO8859_3()
|
||||||
|
* @method static self ISO8859_4()
|
||||||
|
* @method static self ISO8859_5()
|
||||||
|
* @method static self ISO8859_6()
|
||||||
|
* @method static self ISO8859_7()
|
||||||
|
* @method static self ISO8859_8()
|
||||||
|
* @method static self ISO8859_9()
|
||||||
|
* @method static self ISO8859_10()
|
||||||
|
* @method static self ISO8859_11()
|
||||||
|
* @method static self ISO8859_12()
|
||||||
|
* @method static self ISO8859_13()
|
||||||
|
* @method static self ISO8859_14()
|
||||||
|
* @method static self ISO8859_15()
|
||||||
|
* @method static self ISO8859_16()
|
||||||
|
* @method static self SJIS()
|
||||||
|
* @method static self CP1250()
|
||||||
|
* @method static self CP1251()
|
||||||
|
* @method static self CP1252()
|
||||||
|
* @method static self CP1256()
|
||||||
|
* @method static self UNICODE_BIG_UNMARKED()
|
||||||
|
* @method static self UTF8()
|
||||||
|
* @method static self ASCII()
|
||||||
|
* @method static self BIG5()
|
||||||
|
* @method static self GB18030()
|
||||||
|
* @method static self EUC_KR()
|
||||||
|
*/
|
||||||
|
final class CharacterSetEci extends AbstractEnum
|
||||||
|
{
|
||||||
|
protected const CP437 = [[0, 2]];
|
||||||
|
protected const ISO8859_1 = [[1, 3], 'ISO-8859-1'];
|
||||||
|
protected const ISO8859_2 = [[4], 'ISO-8859-2'];
|
||||||
|
protected const ISO8859_3 = [[5], 'ISO-8859-3'];
|
||||||
|
protected const ISO8859_4 = [[6], 'ISO-8859-4'];
|
||||||
|
protected const ISO8859_5 = [[7], 'ISO-8859-5'];
|
||||||
|
protected const ISO8859_6 = [[8], 'ISO-8859-6'];
|
||||||
|
protected const ISO8859_7 = [[9], 'ISO-8859-7'];
|
||||||
|
protected const ISO8859_8 = [[10], 'ISO-8859-8'];
|
||||||
|
protected const ISO8859_9 = [[11], 'ISO-8859-9'];
|
||||||
|
protected const ISO8859_10 = [[12], 'ISO-8859-10'];
|
||||||
|
protected const ISO8859_11 = [[13], 'ISO-8859-11'];
|
||||||
|
protected const ISO8859_12 = [[14], 'ISO-8859-12'];
|
||||||
|
protected const ISO8859_13 = [[15], 'ISO-8859-13'];
|
||||||
|
protected const ISO8859_14 = [[16], 'ISO-8859-14'];
|
||||||
|
protected const ISO8859_15 = [[17], 'ISO-8859-15'];
|
||||||
|
protected const ISO8859_16 = [[18], 'ISO-8859-16'];
|
||||||
|
protected const SJIS = [[20], 'Shift_JIS'];
|
||||||
|
protected const CP1250 = [[21], 'windows-1250'];
|
||||||
|
protected const CP1251 = [[22], 'windows-1251'];
|
||||||
|
protected const CP1252 = [[23], 'windows-1252'];
|
||||||
|
protected const CP1256 = [[24], 'windows-1256'];
|
||||||
|
protected const UNICODE_BIG_UNMARKED = [[25], 'UTF-16BE', 'UnicodeBig'];
|
||||||
|
protected const UTF8 = [[26], 'UTF-8'];
|
||||||
|
protected const ASCII = [[27, 170], 'US-ASCII'];
|
||||||
|
protected const BIG5 = [[28]];
|
||||||
|
protected const GB18030 = [[29], 'GB2312', 'EUC_CN', 'GBK'];
|
||||||
|
protected const EUC_KR = [[30], 'EUC-KR'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var int[]
|
||||||
|
*/
|
||||||
|
private $values;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var string[]
|
||||||
|
*/
|
||||||
|
private $otherEncodingNames;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array<int, self>|null
|
||||||
|
*/
|
||||||
|
private static $valueToEci;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array<string, self>|null
|
||||||
|
*/
|
||||||
|
private static $nameToEci;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param int[] $values
|
||||||
|
*/
|
||||||
|
public function __construct(array $values, string ...$otherEncodingNames)
|
||||||
|
{
|
||||||
|
$this->values = $values;
|
||||||
|
$this->otherEncodingNames = $otherEncodingNames;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the primary value.
|
||||||
|
*/
|
||||||
|
public function getValue() : int
|
||||||
|
{
|
||||||
|
return $this->values[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets character set ECI by value.
|
||||||
|
*
|
||||||
|
* Returns the representing ECI of a given value, or null if it is legal but unsupported.
|
||||||
|
*
|
||||||
|
* @throws InvalidArgumentException if value is not between 0 and 900
|
||||||
|
*/
|
||||||
|
public static function getCharacterSetEciByValue(int $value) : ?self
|
||||||
|
{
|
||||||
|
if ($value < 0 || $value >= 900) {
|
||||||
|
throw new InvalidArgumentException('Value must be between 0 and 900');
|
||||||
|
}
|
||||||
|
|
||||||
|
$valueToEci = self::valueToEci();
|
||||||
|
|
||||||
|
if (! array_key_exists($value, $valueToEci)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $valueToEci[$value];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns character set ECI by name.
|
||||||
|
*
|
||||||
|
* Returns the representing ECI of a given name, or null if it is legal but unsupported
|
||||||
|
*/
|
||||||
|
public static function getCharacterSetEciByName(string $name) : ?self
|
||||||
|
{
|
||||||
|
$nameToEci = self::nameToEci();
|
||||||
|
$name = strtolower($name);
|
||||||
|
|
||||||
|
if (! array_key_exists($name, $nameToEci)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $nameToEci[$name];
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function valueToEci() : array
|
||||||
|
{
|
||||||
|
if (null !== self::$valueToEci) {
|
||||||
|
return self::$valueToEci;
|
||||||
|
}
|
||||||
|
|
||||||
|
self::$valueToEci = [];
|
||||||
|
|
||||||
|
foreach (self::values() as $eci) {
|
||||||
|
foreach ($eci->values as $value) {
|
||||||
|
self::$valueToEci[$value] = $eci;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::$valueToEci;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function nameToEci() : array
|
||||||
|
{
|
||||||
|
if (null !== self::$nameToEci) {
|
||||||
|
return self::$nameToEci;
|
||||||
|
}
|
||||||
|
|
||||||
|
self::$nameToEci = [];
|
||||||
|
|
||||||
|
foreach (self::values() as $eci) {
|
||||||
|
self::$nameToEci[strtolower($eci->name())] = $eci;
|
||||||
|
|
||||||
|
foreach ($eci->otherEncodingNames as $name) {
|
||||||
|
self::$nameToEci[strtolower($name)] = $eci;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::$nameToEci;
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user