mirror of
https://github.com/mailcow/mailcow-dockerized.git
synced 2026-10-21 18:28:13 +00:00
Compare commits
292
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2bad774bcc | ||
|
|
015fd04a37 | ||
|
|
ca8546fa4a | ||
|
|
6f6edfb85e | ||
|
|
6c877fc0ac | ||
|
|
4a29767a31 | ||
|
|
cd29426933 | ||
|
|
37be306f1c | ||
|
|
a351017455 | ||
|
|
887b8a3594 | ||
|
|
571c1b5b29 | ||
|
|
8cd231c9b9 | ||
|
|
74facc4b5b | ||
|
|
ff6329d01b | ||
|
|
e8209d6504 | ||
|
|
626d006a3d | ||
|
|
e88c734d0a | ||
|
|
86bba266f6 | ||
|
|
e3ed8d2fc2 | ||
|
|
88d92261e4 | ||
|
|
017d0c7ff0 | ||
|
|
a28fa19044 | ||
|
|
fa2ad14fc4 | ||
|
|
a480a96e10 | ||
|
|
42d387ca42 | ||
|
|
fe4e675754 | ||
|
|
b05f59b705 | ||
|
|
c4d0ca56d2 | ||
|
|
7ec61fde8a | ||
|
|
d80b15b06c | ||
|
|
ed4fe41d7a | ||
|
|
6b9d875773 | ||
|
|
4d5b9d1c80 | ||
|
|
8abf453e1c | ||
|
|
8bd9e12a73 | ||
|
|
c665d430de | ||
|
|
2195a674ff | ||
|
|
089c44aee1 | ||
|
|
6644699e65 | ||
|
|
91d3b7246a | ||
|
|
b362c7105d | ||
|
|
555e7ed706 | ||
|
|
59e2a9ac77 | ||
|
|
feed5ad183 | ||
|
|
f1f7a9800b | ||
|
|
8c85044781 | ||
|
|
8fb32e4b59 | ||
|
|
b5fa926bc0 | ||
|
|
d1a2f4e168 | ||
|
|
f7a536d634 | ||
|
|
8b456a33e7 | ||
|
|
54da526d41 | ||
|
|
50be483c39 | ||
|
|
d64c923aca | ||
|
|
2d5f166ba8 | ||
|
|
94b5a623ab | ||
|
|
e406ecd461 | ||
|
|
68a95c3af3 | ||
|
|
bd037d5644 | ||
|
|
7036dbf4f8 | ||
|
|
2f10a4633f | ||
|
|
36c70db86c | ||
|
|
9cd16f0001 | ||
|
|
f4961c4023 | ||
|
|
38de21592c | ||
|
|
f44bd2f36a | ||
|
|
95a77f2dcb | ||
|
|
e856510fb2 | ||
|
|
d51d06d716 | ||
|
|
54170d075a | ||
|
|
b4bb1a625b | ||
|
|
2e5a29bf69 | ||
|
|
e245ac04d9 | ||
|
|
c877fdf0a5 | ||
|
|
fea38c8e1b | ||
|
|
cc9af65852 | ||
|
|
92cc8bec90 | ||
|
|
145745329e | ||
|
|
8e72d22c56 | ||
|
|
ddd76d99cd | ||
|
|
8c9524a2fe | ||
|
|
2ebd32d2ee | ||
|
|
4b62af9d02 | ||
|
|
90ca1bf25a | ||
|
|
14772c3a20 | ||
|
|
e5d0ed8c4c | ||
|
|
c17cc8a792 | ||
|
|
4cba448671 | ||
|
|
7dab6c63d1 | ||
|
|
5408bc809d | ||
|
|
bd5c18b145 | ||
|
|
0f06c8e563 | ||
|
|
fffe1aec3d | ||
|
|
6f7fee49cd | ||
|
|
c1d75cf808 | ||
|
|
473fe2885d | ||
|
|
31e1550668 | ||
|
|
315f55bc65 | ||
|
|
e696ee2f6c | ||
|
|
d65aa11870 | ||
|
|
f6630e5b43 | ||
|
|
98e26c5603 | ||
|
|
3098caebbc | ||
|
|
94e23a3cd3 | ||
|
|
1840074991 | ||
|
|
99768e16f5 | ||
|
|
1f8b4679cc | ||
|
|
9e723e942d | ||
|
|
47e4552b44 | ||
|
|
ddcce811c1 | ||
|
|
7e6c36dce1 | ||
|
|
fa154c71f3 | ||
|
|
83a045ed3e | ||
|
|
064817ac70 | ||
|
|
2bd7a24b8c | ||
|
|
ecc2462e4c | ||
|
|
2d8db72d46 | ||
|
|
277a307fb9 | ||
|
|
d455555621 | ||
|
|
9ea2ff1dff | ||
|
|
24cc369d84 | ||
|
|
5f5367f2f9 | ||
|
|
03c31f825a | ||
|
|
c0050e8836 | ||
|
|
15891960f7 | ||
|
|
cce02e2b15 | ||
|
|
0fafda696b | ||
|
|
843db5854c | ||
|
|
23e4e4f373 | ||
|
|
175878f8f1 | ||
|
|
3fcda21c4e | ||
|
|
eac1bf02fc | ||
|
|
a7f2b2145f | ||
|
|
faac56a611 | ||
|
|
be37bc5a68 | ||
|
|
b227c76156 | ||
|
|
db4e3b4d54 | ||
|
|
ffbc37a00c | ||
|
|
e6fa0c5099 | ||
|
|
ce3ceaf5b8 | ||
|
|
efc2e76159 | ||
|
|
7d17715e2e | ||
|
|
aed440bcb3 | ||
|
|
4f2348631a | ||
|
|
da586682f6 | ||
|
|
22ae1f5363 | ||
|
|
c9cf8d7f38 | ||
|
|
cbd3d8b9bc | ||
|
|
f553f38635 | ||
|
|
07373907e3 | ||
|
|
104af58628 | ||
|
|
4ddcee28e4 | ||
|
|
b0d16bbcee | ||
|
|
9175e5f086 | ||
|
|
ff3d571054 | ||
|
|
a8e945f3da | ||
|
|
33547d1d73 | ||
|
|
539c32d99c | ||
|
|
d42c3823c4 | ||
|
|
8ead057ce9 | ||
|
|
886dbcc419 | ||
|
|
b0873edb6a | ||
|
|
694d751915 | ||
|
|
dc15994d40 | ||
|
|
ec24825280 | ||
|
|
08278a8be9 | ||
|
|
5a00b5124b | ||
|
|
8c039f694f | ||
|
|
95bf46c1e4 | ||
|
|
edde35156d | ||
|
|
84e3c32f13 | ||
|
|
ecb848493b | ||
|
|
8a65b9d1c6 | ||
|
|
ed9264fd2a | ||
|
|
7817dda43f | ||
|
|
018e292854 | ||
|
|
127fb1e8f5 | ||
|
|
09f09cb850 | ||
|
|
d4bf377a96 | ||
|
|
abd6fe8c79 | ||
|
|
5f8382ef44 | ||
|
|
03eccd4e42 | ||
|
|
1da8d1c894 | ||
|
|
d1feebf164 | ||
|
|
293b885a85 | ||
|
|
1e0850193a | ||
|
|
33acf56526 | ||
|
|
bea9ad7e8f | ||
|
|
e7ea3aa608 | ||
|
|
5888e248c3 | ||
|
|
2e176339ba | ||
|
|
8f883f3d37 | ||
|
|
709117fe19 | ||
|
|
82ea418423 | ||
|
|
fd24163c6e | ||
|
|
3caef45a12 | ||
|
|
8760e7e5db | ||
|
|
e848226062 | ||
|
|
efaeb77e13 | ||
|
|
569b4cf985 | ||
|
|
6c857243ec | ||
|
|
905e93627c | ||
|
|
598ea21827 | ||
|
|
6012cf4486 | ||
|
|
fe71f84c82 | ||
|
|
763ecbc93e | ||
|
|
97312c1a9d | ||
|
|
baf0e7e658 | ||
|
|
a4ca5f9363 | ||
|
|
dcbea71e67 | ||
|
|
524f19ff77 | ||
|
|
4e33c7143f | ||
|
|
4c184b25f4 | ||
|
|
ddc2309f1e | ||
|
|
bccfcefd17 | ||
|
|
d40252ccce | ||
|
|
4abb5cbfab | ||
|
|
b695936273 | ||
|
|
d9463c7950 | ||
|
|
579542381a | ||
|
|
ce5659f300 | ||
|
|
1967cb642f | ||
|
|
ba0eb04ebe | ||
|
|
609ce6b0d6 | ||
|
|
af61e2d303 | ||
|
|
c6e3f517e1 | ||
|
|
6eaa8d43e9 | ||
|
|
c033cd6254 | ||
|
|
7562578b74 | ||
|
|
43f570e761 | ||
|
|
5e478a32df | ||
|
|
22c94a4d2f | ||
|
|
99dc0f6616 | ||
|
|
4732f568fa | ||
|
|
96d4802cb2 | ||
|
|
ad5b94af5e | ||
|
|
7fce984cac | ||
|
|
404e2f0190 | ||
|
|
1c52eaa3a4 | ||
|
|
c7e04b4146 | ||
|
|
075959aea9 | ||
|
|
885ba2510e | ||
|
|
87563249f1 | ||
|
|
fb1686065d | ||
|
|
0428f5c9bd | ||
|
|
a70c23065c | ||
|
|
caaa4a414d | ||
|
|
c3d841340c | ||
|
|
2e8897c2cf | ||
|
|
b8cd00111f | ||
|
|
81cda80651 | ||
|
|
c1d4f04c22 | ||
|
|
82276cd1ca | ||
|
|
56ea4302ed | ||
|
|
c06112b26e | ||
|
|
aa5a4f0998 | ||
|
|
bf4f471cfd | ||
|
|
978bff9dbc | ||
|
|
869d9af7dd | ||
|
|
af10499ecb | ||
|
|
a1a4d8ff98 | ||
|
|
95d61e8aa2 | ||
|
|
ec8dd1a54f | ||
|
|
382ee34d0e | ||
|
|
0999c9e9ab | ||
|
|
c485968e7f | ||
|
|
e727620bd3 | ||
|
|
71fa3ecebc | ||
|
|
70101d1187 | ||
|
|
c060c205d3 | ||
|
|
5ca900749c | ||
|
|
b005803fe0 | ||
|
|
ec77406dba | ||
|
|
ee15721550 | ||
|
|
038b2efb75 | ||
|
|
1fe4cd03e9 | ||
|
|
12e02e67ff | ||
|
|
b6f57dfb78 | ||
|
|
3ebf2c2d2d | ||
|
|
1bac6f1ee7 | ||
|
|
67e7acd6bd | ||
|
|
910ce573d6 | ||
|
|
689336b3e1 | ||
|
|
01cf72cdef | ||
|
|
4cdb97c699 | ||
|
|
1bd795a9c6 | ||
|
|
39f29e6c30 | ||
|
|
1ab6af21e3 | ||
|
|
5d95c48e0d | ||
|
|
890295bbfc | ||
|
|
a52e977b89 | ||
|
|
95e0608749 |
No files matched your search
@@ -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.1
|
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@v6
|
- 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@v6
|
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@v1.0.2
|
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@v6
|
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@v6
|
uses: actions/checkout@v7
|
||||||
|
|
||||||
- name: Generate postscreen_access.cidr
|
- name: Generate postscreen_access.cidr
|
||||||
run: |
|
run: |
|
||||||
bash helper-scripts/update_postscreen_whitelist.sh
|
bash helper-scripts/update_postscreen_whitelist.sh
|
||||||
|
|
||||||
- name: Create Pull Request
|
- name: Create Pull Request
|
||||||
uses: peter-evans/create-pull-request@v7
|
uses: peter-evans/create-pull-request@v8
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.mailcow_action_Update_postscreen_access_cidr_pat }}
|
token: ${{ secrets.mailcow_action_Update_postscreen_access_cidr_pat }}
|
||||||
commit-message: update postscreen_access.cidr
|
commit-message: update postscreen_access.cidr
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ data/conf/sogo/cron.creds
|
|||||||
data/conf/sogo/custom-fulllogo.svg
|
data/conf/sogo/custom-fulllogo.svg
|
||||||
data/conf/sogo/custom-shortlogo.svg
|
data/conf/sogo/custom-shortlogo.svg
|
||||||
data/conf/sogo/custom-fulllogo.png
|
data/conf/sogo/custom-fulllogo.png
|
||||||
|
data/conf/acme/dns-01.conf
|
||||||
data/gitea/
|
data/gitea/
|
||||||
data/gogs/
|
data/gogs/
|
||||||
data/hooks/dovecot/*
|
data/hooks/dovecot/*
|
||||||
@@ -63,6 +64,8 @@ data/web/templates/cache/*
|
|||||||
!data/web/templates/cache/.gitkeep
|
!data/web/templates/cache/.gitkeep
|
||||||
data/web/.well-known/acme-challenge
|
data/web/.well-known/acme-challenge
|
||||||
data/web/css/build/0081-custom-mailcow.css
|
data/web/css/build/0081-custom-mailcow.css
|
||||||
|
data/web/css/themes/*/
|
||||||
|
!data/web/css/themes/servercow/
|
||||||
data/web/inc/vars.local.inc.php
|
data/web/inc/vars.local.inc.php
|
||||||
data/web/inc/app_info.inc.php
|
data/web/inc/app_info.inc.php
|
||||||
data/web/nextcloud*/
|
data/web/nextcloud*/
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,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>"
|
||||||
|
|
||||||
|
|||||||
@@ -95,51 +95,6 @@ echo "$(clamd -V) is starting... please wait a moment."
|
|||||||
nice -n10 clamd &
|
nice -n10 clamd &
|
||||||
BACKGROUND_TASKS+=($!)
|
BACKGROUND_TASKS+=($!)
|
||||||
|
|
||||||
# Give clamd time to start up, especially with limited resources
|
|
||||||
# This grace period allows clamd to initialize fully before health checks begin
|
|
||||||
# Can be configured via CLAMD_STARTUP_TIMEOUT environment variable
|
|
||||||
STARTUP_GRACE_PERIOD=${CLAMD_STARTUP_TIMEOUT:-600} # Default: 10 minutes in seconds
|
|
||||||
echo "Waiting up to ${STARTUP_GRACE_PERIOD} seconds for clamd to start up..."
|
|
||||||
|
|
||||||
# Helper function to check if clamd is ready
|
|
||||||
clamd_is_ready() {
|
|
||||||
[ "$(echo "PING" | nc -w 1 localhost 3310 2>/dev/null)" = "PONG" ]
|
|
||||||
}
|
|
||||||
|
|
||||||
# Wait for clamd to be ready or until timeout
|
|
||||||
START_TIME=$(date +%s)
|
|
||||||
POLL_INTERVAL=10
|
|
||||||
CLAMD_READY=0
|
|
||||||
|
|
||||||
while true; do
|
|
||||||
CURRENT_TIME=$(date +%s)
|
|
||||||
ELAPSED=$((CURRENT_TIME - START_TIME))
|
|
||||||
|
|
||||||
# Check if clamd is responsive by attempting to connect on localhost
|
|
||||||
# clamd listens on 0.0.0.0:3310 (configured in Dockerfile)
|
|
||||||
if clamd_is_ready; then
|
|
||||||
echo "clamd is ready after ${ELAPSED} seconds"
|
|
||||||
CLAMD_READY=1
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check if we've exceeded the timeout
|
|
||||||
if [ ${ELAPSED} -ge ${STARTUP_GRACE_PERIOD} ]; then
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
|
|
||||||
sleep ${POLL_INTERVAL}
|
|
||||||
done
|
|
||||||
|
|
||||||
# Report final status only if not already reported as ready
|
|
||||||
if [ ${CLAMD_READY} -eq 0 ]; then
|
|
||||||
if clamd_is_ready; then
|
|
||||||
echo "clamd is now ready (started during final check)"
|
|
||||||
else
|
|
||||||
echo "Warning: clamd did not respond to PING within ${STARTUP_GRACE_PERIOD} seconds - it may still be starting up"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
while true; do
|
while true; do
|
||||||
for bg_task in ${BACKGROUND_TASKS[*]}; do
|
for bg_task in ${BACKGROUND_TASKS[*]}; do
|
||||||
if ! kill -0 ${bg_task} 1>&2; then
|
if ! kill -0 ${bg_task} 1>&2; then
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>"
|
||||||
|
|
||||||
@@ -40,4 +40,4 @@ COPY ./docker-entrypoint.sh /app/
|
|||||||
|
|
||||||
RUN chmod +x /app/docker-entrypoint.sh
|
RUN chmod +x /app/docker-entrypoint.sh
|
||||||
|
|
||||||
CMD ["/bin/sh", "-c", "/app/docker-entrypoint.sh"]
|
CMD ["/bin/sh", "-c", "/app/docker-entrypoint.sh"]
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
FROM php:8.2-fpm-alpine3.21
|
FROM php:8.2-fpm-alpine3.24
|
||||||
|
|
||||||
LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
|
LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
|
||||||
|
|
||||||
# renovate: datasource=github-tags depName=krakjoe/apcu versioning=semver-coerced extractVersion=^v(?<version>.*)$
|
# renovate: datasource=github-tags depName=krakjoe/apcu versioning=semver-coerced extractVersion=^v(?<version>.*)$
|
||||||
ARG APCU_PECL_VERSION=5.1.27
|
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.9
|
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 \
|
||||||
@@ -47,6 +47,7 @@ RUN apk add -U --no-cache autoconf \
|
|||||||
libressl-dev \
|
libressl-dev \
|
||||||
librsvg \
|
librsvg \
|
||||||
libtool \
|
libtool \
|
||||||
|
libwebp \
|
||||||
libwebp-dev \
|
libwebp-dev \
|
||||||
libxml2-dev \
|
libxml2-dev \
|
||||||
libxpm \
|
libxpm \
|
||||||
@@ -63,13 +64,16 @@ RUN apk add -U --no-cache autoconf \
|
|||||||
samba-client \
|
samba-client \
|
||||||
zlib-dev \
|
zlib-dev \
|
||||||
tzdata \
|
tzdata \
|
||||||
&& pecl install APCu-${APCU_PECL_VERSION} \
|
&& docker-php-source extract \
|
||||||
&& pecl install imagick-${IMAGICK_PECL_VERSION} \
|
# PECL extensions from their GitHub tags (pecl.php.net is not reliable enough for builds)
|
||||||
&& pecl install mailparse-${MAILPARSE_PECL_VERSION} \
|
&& mkdir -p /usr/src/php/ext/apcu /usr/src/php/ext/imagick /usr/src/php/ext/mailparse /usr/src/php/ext/memcached /usr/src/php/ext/redis \
|
||||||
&& pecl install memcached-${MEMCACHED_PECL_VERSION} \
|
&& curl -fsSL https://github.com/krakjoe/apcu/archive/refs/tags/v${APCU_PECL_VERSION}.tar.gz | tar -xz --strip-components=1 -C /usr/src/php/ext/apcu \
|
||||||
&& pecl install redis-${REDIS_PECL_VERSION} \
|
&& curl -fsSL https://github.com/Imagick/imagick/archive/refs/tags/${IMAGICK_PECL_VERSION}.tar.gz | tar -xz --strip-components=1 -C /usr/src/php/ext/imagick \
|
||||||
&& docker-php-ext-enable apcu imagick memcached mailparse redis \
|
&& sed -i "s/@PACKAGE_VERSION@/${IMAGICK_PECL_VERSION}/" /usr/src/php/ext/imagick/php_imagick.h \
|
||||||
&& pecl clear-cache \
|
&& curl -fsSL https://github.com/php/pecl-mail-mailparse/archive/refs/tags/v${MAILPARSE_PECL_VERSION}.tar.gz | tar -xz --strip-components=1 -C /usr/src/php/ext/mailparse \
|
||||||
|
&& curl -fsSL https://github.com/php-memcached-dev/php-memcached/archive/refs/tags/v${MEMCACHED_PECL_VERSION}.tar.gz | tar -xz --strip-components=1 -C /usr/src/php/ext/memcached \
|
||||||
|
&& curl -fsSL https://github.com/phpredis/phpredis/archive/refs/tags/${REDIS_PECL_VERSION}.tar.gz | tar -xz --strip-components=1 -C /usr/src/php/ext/redis \
|
||||||
|
&& docker-php-ext-install -j 4 apcu imagick mailparse memcached redis \
|
||||||
&& docker-php-ext-configure intl \
|
&& docker-php-ext-configure intl \
|
||||||
&& docker-php-ext-configure exif \
|
&& docker-php-ext-configure exif \
|
||||||
&& docker-php-ext-configure gd --with-freetype=/usr/include/ \
|
&& docker-php-ext-configure gd --with-freetype=/usr/include/ \
|
||||||
@@ -83,6 +87,7 @@ RUN apk add -U --no-cache autoconf \
|
|||||||
&& curl --silent --show-error https://getcomposer.org/installer | php -- --version=${COMPOSER_VERSION} \
|
&& curl --silent --show-error https://getcomposer.org/installer | php -- --version=${COMPOSER_VERSION} \
|
||||||
&& mv composer.phar /usr/local/bin/composer \
|
&& mv composer.phar /usr/local/bin/composer \
|
||||||
&& chmod +x /usr/local/bin/composer \
|
&& chmod +x /usr/local/bin/composer \
|
||||||
|
&& docker-php-source delete \
|
||||||
&& apk del --purge autoconf \
|
&& apk del --purge autoconf \
|
||||||
aspell-dev \
|
aspell-dev \
|
||||||
cyrus-sasl-dev \
|
cyrus-sasl-dev \
|
||||||
|
|||||||
@@ -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.22
|
VERSION=1.14.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'
|
||||||
|
|||||||
@@ -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.13.2-1~8bf602278
|
ARG RSPAMD_VER=rspamd_4.2.1-1~b7a16ae
|
||||||
ARG CODENAME=bookworm
|
ARG CODENAME=trixie
|
||||||
ENV LC_ALL=C
|
ENV LC_ALL=C
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
|||||||
@@ -1,47 +1,167 @@
|
|||||||
FROM debian:bookworm-slim
|
# SOGo built from source to enable security patch application
|
||||||
|
# Repository: https://github.com/Alinto/sogo
|
||||||
|
# Version: SOGo-5.12.11
|
||||||
|
#
|
||||||
|
# Applied security patches:
|
||||||
|
# -
|
||||||
|
#
|
||||||
|
# To add new patches, modify SOGO_SECURITY_PATCHES ARG below with space-separated commit hashes
|
||||||
|
|
||||||
|
FROM debian:bookworm
|
||||||
|
|
||||||
LABEL maintainer="The Infrastructure Company GmbH <info@servercow.de>"
|
LABEL maintainer="The Infrastructure Company GmbH <info@servercow.de>"
|
||||||
|
|
||||||
ARG DEBIAN_FRONTEND=noninteractive
|
ARG DEBIAN_FRONTEND=noninteractive
|
||||||
ARG DEBIAN_VERSION=bookworm
|
ARG SOGO_VERSION=SOGo-5.12.11
|
||||||
ARG SOGO_DEBIAN_REPOSITORY=https://packagingv2.sogo.nu/sogo-nightly-debian/
|
ARG SOPE_VERSION=SOPE-5.12.11
|
||||||
|
# Security patches to apply (space-separated commit hashes)
|
||||||
|
ARG SOGO_SECURITY_PATCHES=""
|
||||||
# renovate: datasource=github-releases depName=tianon/gosu versioning=semver-coerced extractVersion=^(?<version>.*)$
|
# renovate: datasource=github-releases depName=tianon/gosu versioning=semver-coerced extractVersion=^(?<version>.*)$
|
||||||
ARG GOSU_VERSION=1.17
|
ARG GOSU_VERSION=1.19
|
||||||
ENV LC_ALL=C
|
ENV LC_ALL=C
|
||||||
|
|
||||||
# Prerequisites
|
# Install dependencies, build SOPE and SOGo, then clean up (all in one layer to minimize image size)
|
||||||
RUN echo "Building from repository $SOGO_DEBIAN_REPOSITORY" \
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
&& apt-get update && apt-get install -y --no-install-recommends \
|
# Build dependencies
|
||||||
apt-transport-https \
|
git \
|
||||||
ca-certificates \
|
build-essential \
|
||||||
gettext \
|
gobjc \
|
||||||
gnupg \
|
pkg-config \
|
||||||
mariadb-client \
|
gnustep-make \
|
||||||
rsync \
|
gnustep-base-runtime \
|
||||||
supervisor \
|
libgnustep-base-dev \
|
||||||
syslog-ng \
|
libxml2-dev \
|
||||||
syslog-ng-core \
|
libldap2-dev \
|
||||||
syslog-ng-mod-redis \
|
libssl-dev \
|
||||||
dirmngr \
|
zlib1g-dev \
|
||||||
netcat-traditional \
|
libpq-dev \
|
||||||
psmisc \
|
libmariadb-dev-compat \
|
||||||
wget \
|
libmemcached-dev \
|
||||||
patch \
|
libsodium-dev \
|
||||||
|
libcurl4-openssl-dev \
|
||||||
|
libzip-dev \
|
||||||
|
libytnef0-dev \
|
||||||
|
libwbxml2-dev \
|
||||||
|
curl \
|
||||||
|
ca-certificates \
|
||||||
|
# Runtime dependencies
|
||||||
|
apt-transport-https \
|
||||||
|
gettext \
|
||||||
|
gnupg \
|
||||||
|
mariadb-client \
|
||||||
|
rsync \
|
||||||
|
supervisor \
|
||||||
|
syslog-ng \
|
||||||
|
syslog-ng-core \
|
||||||
|
syslog-ng-mod-redis \
|
||||||
|
dirmngr \
|
||||||
|
netcat-traditional \
|
||||||
|
psmisc \
|
||||||
|
wget \
|
||||||
|
patch \
|
||||||
|
libobjc4 \
|
||||||
|
libxml2 \
|
||||||
|
libldap-2.5-0 \
|
||||||
|
libssl3 \
|
||||||
|
zlib1g \
|
||||||
|
libmariadb3 \
|
||||||
|
libmemcached11 \
|
||||||
|
libsodium23 \
|
||||||
|
libcurl4 \
|
||||||
|
libzip4 \
|
||||||
|
libytnef0 \
|
||||||
|
libwbxml2-1 \
|
||||||
|
# Download gosu
|
||||||
&& dpkgArch="$(dpkg --print-architecture | awk -F- '{ print $NF }')" \
|
&& dpkgArch="$(dpkg --print-architecture | awk -F- '{ print $NF }')" \
|
||||||
&& wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$dpkgArch" \
|
&& wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$dpkgArch" \
|
||||||
&& chmod +x /usr/local/bin/gosu \
|
&& chmod +x /usr/local/bin/gosu \
|
||||||
&& gosu nobody true \
|
&& gosu nobody true \
|
||||||
&& mkdir /usr/share/doc/sogo \
|
# Build SOPE
|
||||||
&& touch /usr/share/doc/sogo/empty.sh \
|
&& git clone --depth 1 --branch ${SOPE_VERSION} https://github.com/Alinto/sope.git /tmp/sope \
|
||||||
&& wget -O- https://keys.openpgp.org/vks/v1/by-fingerprint/74FFC6D72B925A34B5D356BDF8A27B36A6E2EAE9 | gpg --dearmor | apt-key add - \
|
&& cd /tmp/sope \
|
||||||
&& echo "deb [trusted=yes] ${SOGO_DEBIAN_REPOSITORY} ${DEBIAN_VERSION} main" > /etc/apt/sources.list.d/sogo.list \
|
&& rm -rf .git \
|
||||||
&& apt-get update && apt-get install -y --no-install-recommends \
|
&& . /usr/share/GNUstep/Makefiles/GNUstep.sh \
|
||||||
sogo \
|
&& ./configure --prefix=/usr --disable-debug --disable-strip \
|
||||||
sogo-activesync \
|
&& make -j$(nproc) \
|
||||||
&& apt-get autoclean \
|
&& make install \
|
||||||
|
&& cd / \
|
||||||
|
&& rm -rf /tmp/sope \
|
||||||
|
# Build SOGo with security patches
|
||||||
|
&& git clone --depth 1 --branch ${SOGO_VERSION} https://github.com/Alinto/sogo.git /tmp/sogo \
|
||||||
|
&& cd /tmp/sogo \
|
||||||
|
&& git config user.email "builder@mailcow.local" \
|
||||||
|
&& git config user.name "SOGo Builder" \
|
||||||
|
&& for patch in ${SOGO_SECURITY_PATCHES}; do \
|
||||||
|
echo "Applying security patch: ${patch}"; \
|
||||||
|
git fetch origin ${patch} && git cherry-pick ${patch}; \
|
||||||
|
done \
|
||||||
|
&& rm -rf .git \
|
||||||
|
&& . /usr/share/GNUstep/Makefiles/GNUstep.sh \
|
||||||
|
&& ./configure --disable-debug --disable-strip \
|
||||||
|
&& make -j$(nproc) \
|
||||||
|
&& make install \
|
||||||
|
&& cd /tmp/sogo/ActiveSync \
|
||||||
|
&& . /usr/share/GNUstep/Makefiles/GNUstep.sh \
|
||||||
|
&& make -j$(nproc) install \
|
||||||
|
&& cd / \
|
||||||
|
&& rm -rf /tmp/sogo \
|
||||||
|
# Strip binaries
|
||||||
|
&& strip --strip-unneeded /usr/local/sbin/sogod 2>/dev/null || true \
|
||||||
|
&& strip --strip-unneeded /usr/local/sbin/sogo-tool 2>/dev/null || true \
|
||||||
|
&& strip --strip-unneeded /usr/local/sbin/sogo-ealarms-notify 2>/dev/null || true \
|
||||||
|
&& strip --strip-unneeded /usr/local/sbin/sogo-slapd-sockd 2>/dev/null || true \
|
||||||
|
# Remove build dependencies and clean up
|
||||||
|
&& apt-get purge -y --auto-remove \
|
||||||
|
git \
|
||||||
|
build-essential \
|
||||||
|
gobjc \
|
||||||
|
gnustep-make \
|
||||||
|
libgnustep-base-dev \
|
||||||
|
libxml2-dev \
|
||||||
|
libldap2-dev \
|
||||||
|
libssl-dev \
|
||||||
|
zlib1g-dev \
|
||||||
|
libpq-dev \
|
||||||
|
libmariadb-dev-compat \
|
||||||
|
libmemcached-dev \
|
||||||
|
libsodium-dev \
|
||||||
|
libcurl4-openssl-dev \
|
||||||
|
libzip-dev \
|
||||||
|
libytnef0-dev \
|
||||||
|
curl \
|
||||||
|
&& apt-get autoremove -y \
|
||||||
|
&& apt-get clean \
|
||||||
&& rm -rf /var/lib/apt/lists/* \
|
&& rm -rf /var/lib/apt/lists/* \
|
||||||
|
&& rm -rf /usr/share/doc/* \
|
||||||
|
&& rm -rf /usr/share/man/* \
|
||||||
|
&& rm -rf /var/cache/debconf/* \
|
||||||
|
&& rm -rf /tmp/* \
|
||||||
|
&& rm -rf /root/.cache \
|
||||||
|
&& find /usr/local/lib -name '*.a' -delete \
|
||||||
|
&& find /usr/lib -name '*.a' -delete \
|
||||||
|
&& mkdir -p /usr/share/doc/sogo \
|
||||||
|
&& touch /usr/share/doc/sogo/empty.sh \
|
||||||
&& touch /etc/default/locale
|
&& touch /etc/default/locale
|
||||||
|
|
||||||
|
# Configure library paths
|
||||||
|
RUN echo "/usr/lib64" > /etc/ld.so.conf.d/sogo.conf \
|
||||||
|
&& echo "/usr/local/lib/sogo" >> /etc/ld.so.conf.d/sogo.conf \
|
||||||
|
&& echo "/usr/local/lib/GNUstep/Frameworks/SOGo.framework/Versions/5/sogo" >> /etc/ld.so.conf.d/sogo.conf \
|
||||||
|
&& ldconfig
|
||||||
|
|
||||||
|
# Create sogo user and group
|
||||||
|
RUN groupadd -r -g 999 sogo \
|
||||||
|
&& useradd -r -u 999 -g sogo -d /var/lib/sogo -s /bin/bash -c "SOGo Daemon" sogo \
|
||||||
|
&& mkdir -p /var/lib/sogo /var/run/sogo /var/log/sogo /var/spool/sogo \
|
||||||
|
&& chown -R sogo:sogo /var/lib/sogo /var/run/sogo /var/log/sogo /var/spool/sogo
|
||||||
|
|
||||||
|
# Create symlinks for SOGo binaries
|
||||||
|
RUN ln -s /usr/local/sbin/sogod /usr/sbin/sogod \
|
||||||
|
&& ln -s /usr/local/sbin/sogo-tool /usr/sbin/sogo-tool \
|
||||||
|
&& ln -s /usr/local/sbin/sogo-ealarms-notify /usr/sbin/sogo-ealarms-notify \
|
||||||
|
&& ln -s /usr/local/sbin/sogo-slapd-sockd /usr/sbin/sogo-slapd-sockd
|
||||||
|
|
||||||
|
# Copy configuration files and scripts
|
||||||
COPY ./bootstrap-sogo.sh /bootstrap-sogo.sh
|
COPY ./bootstrap-sogo.sh /bootstrap-sogo.sh
|
||||||
COPY syslog-ng.conf /etc/syslog-ng/syslog-ng.conf
|
COPY syslog-ng.conf /etc/syslog-ng/syslog-ng.conf
|
||||||
COPY syslog-ng-redis_slave.conf /etc/syslog-ng/syslog-ng-redis_slave.conf
|
COPY syslog-ng-redis_slave.conf /etc/syslog-ng/syslog-ng-redis_slave.conf
|
||||||
@@ -56,4 +176,4 @@ RUN chmod +x /bootstrap-sogo.sh \
|
|||||||
|
|
||||||
ENTRYPOINT ["/docker-entrypoint.sh"]
|
ENTRYPOINT ["/docker-entrypoint.sh"]
|
||||||
|
|
||||||
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/supervisord.conf"]
|
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/supervisord.conf"]
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
--- /usr/lib/GNUstep/SOGo/Templates/UIxAclEditor.wox 2018-08-17 18:29:57.987504204 +0200
|
--- /usr/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>
|
||||||
|
|||||||
@@ -130,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
|
||||||
@@ -149,7 +153,7 @@ fi
|
|||||||
|
|
||||||
# Rsync web content
|
# Rsync web content
|
||||||
echo "Syncing web content with named volume"
|
echo "Syncing web content with named volume"
|
||||||
rsync -a /usr/lib/GNUstep/SOGo/. /sogo_web/
|
rsync -a /usr/local/lib/GNUstep/SOGo/. /sogo_web/
|
||||||
|
|
||||||
# Chown backup path
|
# Chown backup path
|
||||||
chown -R sogo:sogo /sogo_backup
|
chown -R sogo:sogo /sogo_backup
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
FROM alpine:3.21
|
FROM alpine:3.23
|
||||||
|
|
||||||
LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
|
LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
|
||||||
|
|
||||||
|
# install unbound from alpine:edge to get security patches
|
||||||
|
# minimum version is pinned
|
||||||
|
RUN apk add --no-cache --repository=https://dl-cdn.alpinelinux.org/alpine/edge/main "unbound>=1.26.1-r0"
|
||||||
|
|
||||||
|
# install other packages from regular alpine stable repo
|
||||||
RUN apk add --update --no-cache \
|
RUN apk add --update --no-cache \
|
||||||
curl \
|
curl \
|
||||||
bind-tools \
|
bind-tools \
|
||||||
coreutils \
|
coreutils \
|
||||||
unbound \
|
|
||||||
bash \
|
bash \
|
||||||
openssl \
|
openssl \
|
||||||
drill \
|
drill \
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM alpine:3.21
|
FROM alpine:3.23
|
||||||
|
|
||||||
LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
|
LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
|
||||||
|
|
||||||
@@ -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}
|
||||||
|
|||||||
File renamed without changes.
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -42,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(' ') }};
|
||||||
|
|
||||||
|
|||||||
@@ -116,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;
|
||||||
}
|
}
|
||||||
@@ -261,19 +264,19 @@ location ~* /sogo$ {
|
|||||||
}
|
}
|
||||||
|
|
||||||
location /SOGo.woa/WebServerResources/ {
|
location /SOGo.woa/WebServerResources/ {
|
||||||
alias /usr/lib/GNUstep/SOGo/WebServerResources/;
|
alias /usr/local/lib/GNUstep/SOGo/WebServerResources/;
|
||||||
}
|
}
|
||||||
|
|
||||||
location /.woa/WebServerResources/ {
|
location /.woa/WebServerResources/ {
|
||||||
alias /usr/lib/GNUstep/SOGo/WebServerResources/;
|
alias /usr/local/lib/GNUstep/SOGo/WebServerResources/;
|
||||||
}
|
}
|
||||||
|
|
||||||
location /SOGo/WebServerResources/ {
|
location /SOGo/WebServerResources/ {
|
||||||
alias /usr/lib/GNUstep/SOGo/WebServerResources/;
|
alias /usr/local/lib/GNUstep/SOGo/WebServerResources/;
|
||||||
}
|
}
|
||||||
|
|
||||||
location (^/SOGo/so/ControlPanel/Products/[^/]*UI/Resources/.*\.(jpg|png|gif|css|js)$) {
|
location (^/SOGo/so/ControlPanel/Products/[^/]*UI/Resources/.*\.(jpg|png|gif|css|js)$) {
|
||||||
alias /usr/lib/GNUstep/SOGo/$1.SOGo/Resources/$2;
|
alias /usr/local/lib/GNUstep/SOGo/$1.SOGo/Resources/$2;
|
||||||
}
|
}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
max_execution_time = 3600
|
max_execution_time = 3600
|
||||||
max_input_time = 3600
|
max_input_time = 3600
|
||||||
memory_limit = 512M
|
memory_limit = 512M
|
||||||
|
; not used by mailcow, disabled as hardening (PHP security release 2026-10-21)
|
||||||
|
session.upload_progress.enabled = Off
|
||||||
@@ -37,7 +37,7 @@ postscreen_access_list = permit_mynetworks,
|
|||||||
cidr:/opt/postfix/conf/postscreen_access.cidr,
|
cidr:/opt/postfix/conf/postscreen_access.cidr,
|
||||||
tcp:127.0.0.1:10027
|
tcp:127.0.0.1:10027
|
||||||
postscreen_bare_newline_enable = no
|
postscreen_bare_newline_enable = no
|
||||||
postscreen_blacklist_action = drop
|
postscreen_denylist_action = drop
|
||||||
postscreen_cache_cleanup_interval = 24h
|
postscreen_cache_cleanup_interval = 24h
|
||||||
postscreen_cache_map = proxy:btree:$data_directory/postscreen_cache
|
postscreen_cache_map = proxy:btree:$data_directory/postscreen_cache
|
||||||
postscreen_dnsbl_action = enforce
|
postscreen_dnsbl_action = enforce
|
||||||
@@ -107,8 +107,6 @@ smtpd_sender_restrictions = reject_authenticated_sender_login_mismatch,
|
|||||||
reject_unknown_sender_domain
|
reject_unknown_sender_domain
|
||||||
smtpd_soft_error_limit = 3
|
smtpd_soft_error_limit = 3
|
||||||
smtpd_tls_auth_only = yes
|
smtpd_tls_auth_only = yes
|
||||||
smtpd_tls_dh1024_param_file = /etc/ssl/mail/dhparams.pem
|
|
||||||
smtpd_tls_eecdh_grade = auto
|
|
||||||
smtpd_tls_exclude_ciphers = ECDHE-RSA-RC4-SHA, RC4, aNULL, DES-CBC3-SHA, ECDHE-RSA-DES-CBC3-SHA, EDH-RSA-DES-CBC3-SHA
|
smtpd_tls_exclude_ciphers = ECDHE-RSA-RC4-SHA, RC4, aNULL, DES-CBC3-SHA, ECDHE-RSA-DES-CBC3-SHA, EDH-RSA-DES-CBC3-SHA
|
||||||
smtpd_tls_loglevel = 1
|
smtpd_tls_loglevel = 1
|
||||||
|
|
||||||
@@ -152,7 +150,7 @@ smtp_sasl_auth_enable = yes
|
|||||||
smtp_sasl_password_maps = proxy:mysql:/opt/postfix/conf/sql/mysql_sasl_passwd_maps_sender_dependent.cf
|
smtp_sasl_password_maps = proxy:mysql:/opt/postfix/conf/sql/mysql_sasl_passwd_maps_sender_dependent.cf
|
||||||
smtp_sasl_security_options =
|
smtp_sasl_security_options =
|
||||||
smtp_sasl_mechanism_filter = plain, login
|
smtp_sasl_mechanism_filter = plain, login
|
||||||
smtp_tls_policy_maps = proxy:mysql:/opt/postfix/conf/sql/mysql_tls_policy_override_maps.cf 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 Mon Dec 1 00:24:43 UTC 2025
|
# Whitelist generated by Postwhite v3.4 on Thu Oct 1 00:35:20 UTC 2026
|
||||||
# https://github.com/stevejenkins/postwhite/
|
# https://github.com/stevejenkins/postwhite/
|
||||||
# 2186 total rules
|
# 2246 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,11 +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
|
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.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
|
||||||
@@ -52,10 +57,15 @@
|
|||||||
8.25.194.0/23 permit
|
8.25.194.0/23 permit
|
||||||
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.240/29 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.240/29 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.69 permit
|
|
||||||
13.107.246.69 permit
|
|
||||||
13.108.16.0/20 permit
|
13.108.16.0/20 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
|
||||||
@@ -115,7 +125,6 @@
|
|||||||
23.249.208.0/20 permit
|
23.249.208.0/20 permit
|
||||||
23.251.224.0/19 permit
|
23.251.224.0/19 permit
|
||||||
23.253.141.0/24 permit
|
23.253.141.0/24 permit
|
||||||
23.253.182.0/23 permit
|
|
||||||
23.253.182.103 permit
|
23.253.182.103 permit
|
||||||
23.253.183.145 permit
|
23.253.183.145 permit
|
||||||
23.253.183.146 permit
|
23.253.183.146 permit
|
||||||
@@ -137,24 +146,7 @@
|
|||||||
27.123.206.80/28 permit
|
27.123.206.80/28 permit
|
||||||
31.47.251.17 permit
|
31.47.251.17 permit
|
||||||
31.186.239.0/24 permit
|
31.186.239.0/24 permit
|
||||||
34.2.64.0/22 permit
|
34.2.64.0/19 permit
|
||||||
34.2.68.0/23 permit
|
|
||||||
34.2.70.0/23 permit
|
|
||||||
34.2.71.64/26 permit
|
|
||||||
34.2.72.0/22 permit
|
|
||||||
34.2.75.0/26 permit
|
|
||||||
34.2.78.0/23 permit
|
|
||||||
34.2.80.0/23 permit
|
|
||||||
34.2.82.0/23 permit
|
|
||||||
34.2.84.0/24 permit
|
|
||||||
34.2.84.64/26 permit
|
|
||||||
34.2.85.0/24 permit
|
|
||||||
34.2.85.64/26 permit
|
|
||||||
34.2.86.0/23 permit
|
|
||||||
34.2.88.0/23 permit
|
|
||||||
34.2.90.0/23 permit
|
|
||||||
34.2.92.0/23 permit
|
|
||||||
34.2.94.0/23 permit
|
|
||||||
34.70.158.162 permit
|
34.70.158.162 permit
|
||||||
34.74.74.140 permit
|
34.74.74.140 permit
|
||||||
34.83.159.189 permit
|
34.83.159.189 permit
|
||||||
@@ -169,6 +161,7 @@
|
|||||||
34.215.104.144 permit
|
34.215.104.144 permit
|
||||||
34.218.115.239 permit
|
34.218.115.239 permit
|
||||||
34.225.212.172 permit
|
34.225.212.172 permit
|
||||||
|
34.241.242.183 permit
|
||||||
35.83.148.184 permit
|
35.83.148.184 permit
|
||||||
35.155.198.111 permit
|
35.155.198.111 permit
|
||||||
35.158.23.94 permit
|
35.158.23.94 permit
|
||||||
@@ -180,6 +173,7 @@
|
|||||||
35.205.92.9 permit
|
35.205.92.9 permit
|
||||||
35.228.216.85 permit
|
35.228.216.85 permit
|
||||||
35.242.169.159 permit
|
35.242.169.159 permit
|
||||||
|
37.187.220.204 permit
|
||||||
37.188.97.188 permit
|
37.188.97.188 permit
|
||||||
37.218.248.47 permit
|
37.218.248.47 permit
|
||||||
37.218.249.47 permit
|
37.218.249.47 permit
|
||||||
@@ -192,6 +186,7 @@
|
|||||||
40.233.64.216 permit
|
40.233.64.216 permit
|
||||||
40.233.83.78 permit
|
40.233.83.78 permit
|
||||||
40.233.88.28 permit
|
40.233.88.28 permit
|
||||||
|
43.239.212.33 permit
|
||||||
44.206.138.57 permit
|
44.206.138.57 permit
|
||||||
44.210.169.44 permit
|
44.210.169.44 permit
|
||||||
44.217.45.156 permit
|
44.217.45.156 permit
|
||||||
@@ -252,8 +247,8 @@
|
|||||||
46.228.39.112/31 permit
|
46.228.39.112/31 permit
|
||||||
46.243.88.174 permit
|
46.243.88.174 permit
|
||||||
46.243.88.175 permit
|
46.243.88.175 permit
|
||||||
46.243.88.176 permit
|
46.243.88.176/30 permit
|
||||||
46.243.88.177 permit
|
46.243.95.96/27 permit
|
||||||
46.243.95.179 permit
|
46.243.95.179 permit
|
||||||
46.243.95.180 permit
|
46.243.95.180 permit
|
||||||
50.16.246.183 permit
|
50.16.246.183 permit
|
||||||
@@ -271,8 +266,11 @@
|
|||||||
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
|
||||||
|
50.152.146.25 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.12.53.23 permit
|
52.12.53.23 permit
|
||||||
52.13.214.179 permit
|
52.13.214.179 permit
|
||||||
52.26.1.71 permit
|
52.26.1.71 permit
|
||||||
@@ -296,14 +294,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.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
|
||||||
@@ -337,6 +327,7 @@
|
|||||||
54.244.54.130 permit
|
54.244.54.130 permit
|
||||||
54.244.242.0/24 permit
|
54.244.242.0/24 permit
|
||||||
54.255.61.23 permit
|
54.255.61.23 permit
|
||||||
|
56.124.6.228 permit
|
||||||
57.103.64.0/18 permit
|
57.103.64.0/18 permit
|
||||||
57.129.93.249 permit
|
57.129.93.249 permit
|
||||||
62.13.128.0/24 permit
|
62.13.128.0/24 permit
|
||||||
@@ -367,7 +358,9 @@
|
|||||||
64.127.115.252 permit
|
64.127.115.252 permit
|
||||||
64.132.88.0/23 permit
|
64.132.88.0/23 permit
|
||||||
64.132.92.0/24 permit
|
64.132.92.0/24 permit
|
||||||
|
64.181.136.241 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
|
||||||
@@ -397,22 +390,12 @@
|
|||||||
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.121.120/29 permit
|
|
||||||
65.55.29.77 permit
|
65.55.29.77 permit
|
||||||
65.55.33.64/28 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.94.0/25 permit
|
|
||||||
65.55.113.64/26 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
|
||||||
|
65.154.166.0/24 permit
|
||||||
65.212.180.36 permit
|
65.212.180.36 permit
|
||||||
66.102.0.0/20 permit
|
66.102.0.0/20 permit
|
||||||
66.119.150.192/26 permit
|
66.119.150.192/26 permit
|
||||||
@@ -463,7 +446,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
|
||||||
@@ -528,8 +515,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
|
||||||
@@ -650,18 +638,29 @@
|
|||||||
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.88.38.0/23 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
|
||||||
81.223.46.0/27 permit
|
81.223.46.0/27 permit
|
||||||
|
82.165.159.2 permit
|
||||||
|
82.165.159.3 permit
|
||||||
|
82.165.159.4 permit
|
||||||
82.165.159.12 permit
|
82.165.159.12 permit
|
||||||
82.165.159.13 permit
|
82.165.159.13 permit
|
||||||
82.165.159.14 permit
|
82.165.159.14 permit
|
||||||
|
82.165.159.34 permit
|
||||||
|
82.165.159.35 permit
|
||||||
82.165.159.40 permit
|
82.165.159.40 permit
|
||||||
82.165.159.41 permit
|
82.165.159.41 permit
|
||||||
82.165.159.42 permit
|
82.165.159.42 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
|
||||||
@@ -712,11 +711,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
|
||||||
@@ -725,6 +724,7 @@
|
|||||||
96.43.148.64/28 permit
|
96.43.148.64/28 permit
|
||||||
96.43.148.64/31 permit
|
96.43.148.64/31 permit
|
||||||
96.43.151.64/28 permit
|
96.43.151.64/28 permit
|
||||||
|
98.77.0.0/16 permit
|
||||||
98.97.248.0/21 permit
|
98.97.248.0/21 permit
|
||||||
98.136.44.181 permit
|
98.136.44.181 permit
|
||||||
98.136.44.182/31 permit
|
98.136.44.182/31 permit
|
||||||
@@ -1211,14 +1211,15 @@
|
|||||||
99.78.197.208/28 permit
|
99.78.197.208/28 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.89.75.238 permit
|
||||||
103.151.192.0/23 permit
|
103.151.192.0/23 permit
|
||||||
103.168.172.128/27 permit
|
103.168.172.128/27 permit
|
||||||
103.237.104.0/22 permit
|
103.237.104.0/22 permit
|
||||||
104.43.243.237 permit
|
104.43.243.237 permit
|
||||||
104.44.112.128/25 permit
|
104.44.112.128/25 permit
|
||||||
104.47.0.0/17 permit
|
104.47.0.0/17 permit
|
||||||
104.130.96.0/28 permit
|
|
||||||
104.130.122.0/23 permit
|
|
||||||
106.10.144.64/27 permit
|
106.10.144.64/27 permit
|
||||||
106.10.144.100/31 permit
|
106.10.144.100/31 permit
|
||||||
106.10.144.103 permit
|
106.10.144.103 permit
|
||||||
@@ -1354,11 +1355,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
|
||||||
@@ -1376,6 +1372,10 @@
|
|||||||
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.52 permit
|
||||||
|
122.15.156.182 permit
|
||||||
123.126.78.64/29 permit
|
123.126.78.64/29 permit
|
||||||
124.108.96.24/31 permit
|
124.108.96.24/31 permit
|
||||||
124.108.96.28/31 permit
|
124.108.96.28/31 permit
|
||||||
@@ -1412,6 +1412,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
|
||||||
@@ -1420,26 +1434,34 @@
|
|||||||
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.162.194.23 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.128.64.0/19 permit
|
134.98.248.128/25 permit
|
||||||
134.128.96.0/19 permit
|
134.128.64.0/18 permit
|
||||||
134.170.27.8 permit
|
134.170.27.8 permit
|
||||||
134.170.113.0/26 permit
|
134.170.113.0/26 permit
|
||||||
134.170.141.64/26 permit
|
134.170.141.64/26 permit
|
||||||
134.170.143.0/24 permit
|
134.170.143.0/24 permit
|
||||||
134.170.174.0/24 permit
|
134.170.174.0/24 permit
|
||||||
|
135.84.80.0/24 permit
|
||||||
|
135.84.81.0/24 permit
|
||||||
|
135.84.82.0/24 permit
|
||||||
|
135.84.83.0/24 permit
|
||||||
135.84.216.0/22 permit
|
135.84.216.0/22 permit
|
||||||
136.143.160.0/24 permit
|
136.143.160.0/24 permit
|
||||||
136.143.161.0/24 permit
|
136.143.161.0/24 permit
|
||||||
@@ -1459,6 +1481,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
|
||||||
@@ -1466,8 +1492,12 @@
|
|||||||
139.138.46.219 permit
|
139.138.46.219 permit
|
||||||
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.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
|
||||||
@@ -1494,14 +1524,14 @@
|
|||||||
146.20.14.105 permit
|
146.20.14.105 permit
|
||||||
146.20.14.106 permit
|
146.20.14.106 permit
|
||||||
146.20.14.107 permit
|
146.20.14.107 permit
|
||||||
146.20.112.0/26 permit
|
|
||||||
146.20.113.0/24 permit
|
|
||||||
146.20.191.0/24 permit
|
|
||||||
146.20.215.0/24 permit
|
146.20.215.0/24 permit
|
||||||
146.20.215.182 permit
|
146.20.215.182 permit
|
||||||
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
|
||||||
@@ -1509,37 +1539,41 @@
|
|||||||
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.178 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.165.251 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.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.13.33.181 permit
|
||||||
|
159.26.176.0/20 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
|
||||||
@@ -1561,16 +1595,21 @@
|
|||||||
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.33.94.110 permit
|
||||||
|
161.33.228.244 permit
|
||||||
161.38.192.0/20 permit
|
161.38.192.0/20 permit
|
||||||
161.38.204.0/22 permit
|
161.38.204.0/22 permit
|
||||||
161.71.32.0/19 permit
|
161.71.32.0/19 permit
|
||||||
161.71.64.0/20 permit
|
161.71.64.0/20 permit
|
||||||
162.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
|
||||||
@@ -1582,10 +1621,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
|
||||||
166.78.68.0/22 permit
|
165.1.100.0/25 permit
|
||||||
|
165.173.128.0/24 permit
|
||||||
|
165.173.180.0/24 permit
|
||||||
|
165.173.180.250/31 permit
|
||||||
|
165.173.182.0/24 permit
|
||||||
|
165.173.182.250/31 permit
|
||||||
|
165.173.189.205 permit
|
||||||
166.78.68.221 permit
|
166.78.68.221 permit
|
||||||
166.78.69.169 permit
|
166.78.69.169 permit
|
||||||
166.78.69.170 permit
|
166.78.69.170 permit
|
||||||
@@ -1603,9 +1651,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
|
||||||
@@ -1614,6 +1665,20 @@
|
|||||||
168.245.12.252 permit
|
168.245.12.252 permit
|
||||||
168.245.46.9 permit
|
168.245.46.9 permit
|
||||||
168.245.127.231 permit
|
168.245.127.231 permit
|
||||||
|
169.148.129.0/24 permit
|
||||||
|
169.148.131.0/24 permit
|
||||||
|
169.148.138.0/24 permit
|
||||||
|
169.148.142.10 permit
|
||||||
|
169.148.142.33 permit
|
||||||
|
169.148.144.0/25 permit
|
||||||
|
169.148.144.10 permit
|
||||||
|
169.148.146.0/23 permit
|
||||||
|
169.148.174.10 permit
|
||||||
|
169.148.175.3 permit
|
||||||
|
169.148.179.3 permit
|
||||||
|
169.148.188.0/24 permit
|
||||||
|
169.148.188.182 permit
|
||||||
|
170.9.232.254 permit
|
||||||
170.10.128.0/24 permit
|
170.10.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
|
||||||
@@ -1623,11 +1688,11 @@
|
|||||||
173.0.84.224/27 permit
|
173.0.84.224/27 permit
|
||||||
173.0.94.244/30 permit
|
173.0.94.244/30 permit
|
||||||
173.194.0.0/16 permit
|
173.194.0.0/16 permit
|
||||||
173.194.0.0/17 permit
|
|
||||||
173.203.79.182 permit
|
173.203.79.182 permit
|
||||||
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
|
||||||
@@ -1642,13 +1707,14 @@
|
|||||||
175.41.215.51 permit
|
175.41.215.51 permit
|
||||||
176.32.105.0/24 permit
|
176.32.105.0/24 permit
|
||||||
176.32.127.0/24 permit
|
176.32.127.0/24 permit
|
||||||
|
176.94.34.32/27 permit
|
||||||
|
176.94.34.64/27 permit
|
||||||
178.236.10.128/26 permit
|
178.236.10.128/26 permit
|
||||||
182.50.76.0/22 permit
|
182.50.76.0/22 permit
|
||||||
182.50.78.64/28 permit
|
182.50.78.64/28 permit
|
||||||
183.240.219.64/29 permit
|
183.240.219.64/29 permit
|
||||||
185.4.120.0/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
|
||||||
@@ -1662,8 +1728,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
|
||||||
@@ -1716,30 +1790,42 @@
|
|||||||
188.125.85.234/31 permit
|
188.125.85.234/31 permit
|
||||||
188.125.85.236/31 permit
|
188.125.85.236/31 permit
|
||||||
188.125.85.238 permit
|
188.125.85.238 permit
|
||||||
188.165.51.139 permit
|
|
||||||
188.172.128.0/20 permit
|
188.172.128.0/20 permit
|
||||||
192.0.64.0/18 permit
|
192.0.64.0/18 permit
|
||||||
|
192.9.166.184 permit
|
||||||
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
|
||||||
192.237.158.0/23 permit
|
|
||||||
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
|
||||||
@@ -1773,7 +1859,6 @@
|
|||||||
198.21.0.0/21 permit
|
198.21.0.0/21 permit
|
||||||
198.37.144.0/20 permit
|
198.37.144.0/20 permit
|
||||||
198.37.152.186 permit
|
198.37.152.186 permit
|
||||||
198.61.254.0/23 permit
|
|
||||||
198.61.254.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
|
||||||
@@ -1799,7 +1884,16 @@
|
|||||||
199.16.156.0/22 permit
|
199.16.156.0/22 permit
|
||||||
199.33.145.1 permit
|
199.33.145.1 permit
|
||||||
199.33.145.32 permit
|
199.33.145.32 permit
|
||||||
|
199.34.22.36 permit
|
||||||
199.59.148.0/22 permit
|
199.59.148.0/22 permit
|
||||||
|
199.67.80.2 permit
|
||||||
|
199.67.80.20 permit
|
||||||
|
199.67.82.2 permit
|
||||||
|
199.67.82.20 permit
|
||||||
|
199.67.84.0/24 permit
|
||||||
|
199.67.86.0/24 permit
|
||||||
|
199.67.88.0/24 permit
|
||||||
|
199.67.90.0/24 permit
|
||||||
199.101.161.130 permit
|
199.101.161.130 permit
|
||||||
199.101.162.0/25 permit
|
199.101.162.0/25 permit
|
||||||
199.122.120.0/21 permit
|
199.122.120.0/21 permit
|
||||||
@@ -1851,13 +1945,17 @@
|
|||||||
204.14.232.0/21 permit
|
204.14.232.0/21 permit
|
||||||
204.14.232.64/28 permit
|
204.14.232.64/28 permit
|
||||||
204.14.234.64/28 permit
|
204.14.234.64/28 permit
|
||||||
|
204.75.18.128/27 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
|
||||||
|
204.141.32.0/23 permit
|
||||||
|
204.141.42.0/23 permit
|
||||||
204.216.164.202 permit
|
204.216.164.202 permit
|
||||||
204.220.160.0/21 permit
|
204.220.90.0/23 permit
|
||||||
|
204.220.92.0/22 permit
|
||||||
|
204.220.160.0/20 permit
|
||||||
204.220.168.0/21 permit
|
204.220.168.0/21 permit
|
||||||
204.220.176.0/20 permit
|
204.220.176.0/20 permit
|
||||||
204.220.181.105 permit
|
204.220.181.105 permit
|
||||||
@@ -1878,23 +1976,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.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
|
||||||
@@ -1902,6 +1990,9 @@
|
|||||||
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.211.147.87 permit
|
||||||
|
207.211.159.110 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
|
||||||
@@ -1909,6 +2000,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
|
||||||
@@ -1935,6 +2028,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
|
||||||
@@ -1948,7 +2042,6 @@
|
|||||||
209.43.22.0/28 permit
|
209.43.22.0/28 permit
|
||||||
209.46.117.168 permit
|
209.46.117.168 permit
|
||||||
209.46.117.179 permit
|
209.46.117.179 permit
|
||||||
209.61.151.0/24 permit
|
|
||||||
209.61.151.236 permit
|
209.61.151.236 permit
|
||||||
209.61.151.249 permit
|
209.61.151.249 permit
|
||||||
209.61.151.251 permit
|
209.61.151.251 permit
|
||||||
@@ -1993,11 +2086,19 @@
|
|||||||
212.82.111.228/31 permit
|
212.82.111.228/31 permit
|
||||||
212.82.111.230 permit
|
212.82.111.230 permit
|
||||||
212.123.28.40 permit
|
212.123.28.40 permit
|
||||||
|
212.227.15.3 permit
|
||||||
|
212.227.15.4 permit
|
||||||
|
212.227.15.5 permit
|
||||||
|
212.227.15.6 permit
|
||||||
212.227.15.7 permit
|
212.227.15.7 permit
|
||||||
212.227.15.8 permit
|
212.227.15.8 permit
|
||||||
|
212.227.15.14 permit
|
||||||
212.227.15.15 permit
|
212.227.15.15 permit
|
||||||
212.227.15.18 permit
|
212.227.15.18 permit
|
||||||
212.227.15.19 permit
|
212.227.15.19 permit
|
||||||
|
212.227.15.25 permit
|
||||||
|
212.227.15.26 permit
|
||||||
|
212.227.15.29 permit
|
||||||
212.227.15.44 permit
|
212.227.15.44 permit
|
||||||
212.227.15.45 permit
|
212.227.15.45 permit
|
||||||
212.227.15.46 permit
|
212.227.15.46 permit
|
||||||
@@ -2005,11 +2106,17 @@
|
|||||||
212.227.15.50 permit
|
212.227.15.50 permit
|
||||||
212.227.15.52 permit
|
212.227.15.52 permit
|
||||||
212.227.15.53 permit
|
212.227.15.53 permit
|
||||||
|
212.227.15.54 permit
|
||||||
|
212.227.15.55 permit
|
||||||
212.227.17.1 permit
|
212.227.17.1 permit
|
||||||
212.227.17.2 permit
|
212.227.17.2 permit
|
||||||
212.227.17.7 permit
|
212.227.17.7 permit
|
||||||
|
212.227.17.11 permit
|
||||||
|
212.227.17.12 permit
|
||||||
212.227.17.16 permit
|
212.227.17.16 permit
|
||||||
212.227.17.17 permit
|
212.227.17.17 permit
|
||||||
|
212.227.17.18 permit
|
||||||
|
212.227.17.19 permit
|
||||||
212.227.17.20 permit
|
212.227.17.20 permit
|
||||||
212.227.17.21 permit
|
212.227.17.21 permit
|
||||||
212.227.17.22 permit
|
212.227.17.22 permit
|
||||||
@@ -2029,14 +2136,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
|
||||||
@@ -2064,7 +2167,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
|
||||||
@@ -2086,6 +2188,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
|
||||||
@@ -2094,6 +2197,8 @@
|
|||||||
216.205.24.0/24 permit
|
216.205.24.0/24 permit
|
||||||
216.221.160.0/19 permit
|
216.221.160.0/19 permit
|
||||||
216.239.32.0/19 permit
|
216.239.32.0/19 permit
|
||||||
|
217.72.192.77 permit
|
||||||
|
217.72.192.78 permit
|
||||||
217.77.141.52 permit
|
217.77.141.52 permit
|
||||||
217.77.141.59 permit
|
217.77.141.59 permit
|
||||||
217.175.194.0/24 permit
|
217.175.194.0/24 permit
|
||||||
@@ -2104,6 +2209,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
|
||||||
@@ -2121,12 +2227,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:1d0::1 permit
|
||||||
2603:1030:20e:3::23c permit
|
2607:13c0:0001:0000:0000:0000:0000:7000/116 permit
|
||||||
2603:1030:b:3::152 permit
|
2607:13c0:0002:0000:0000:0000:0000:1000/116 permit
|
||||||
2603:1030:c02:8::14 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
|
||||||
@@ -2139,5 +2246,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
|
||||||
})
|
})
|
||||||
@@ -620,6 +856,8 @@ rspamd_config:register_symbol({
|
|||||||
|
|
||||||
local seen_cte
|
local seen_cte
|
||||||
local newline_s = newline(task)
|
local newline_s = newline(task)
|
||||||
|
-- use the CTE add_text_footer actually applied
|
||||||
|
local new_cte = rewrite.new_cte or 'quoted-printable'
|
||||||
|
|
||||||
local function rewrite_ct_cb(name, hdr)
|
local function rewrite_ct_cb(name, hdr)
|
||||||
if rewrite.need_rewrite_ct then
|
if rewrite.need_rewrite_ct then
|
||||||
@@ -641,13 +879,13 @@ rspamd_config:register_symbol({
|
|||||||
return
|
return
|
||||||
elseif name:lower() == 'content-transfer-encoding' then
|
elseif name:lower() == 'content-transfer-encoding' then
|
||||||
out[#out + 1] = string.format('%s: %s',
|
out[#out + 1] = string.format('%s: %s',
|
||||||
'Content-Transfer-Encoding', 'quoted-printable')
|
'Content-Transfer-Encoding', new_cte)
|
||||||
-- update Content-Transfer-Encoding header
|
-- update Content-Transfer-Encoding header
|
||||||
task:set_milter_reply({
|
task:set_milter_reply({
|
||||||
remove_headers = {['Content-Transfer-Encoding'] = 0},
|
remove_headers = {['Content-Transfer-Encoding'] = 0},
|
||||||
})
|
})
|
||||||
task:set_milter_reply({
|
task:set_milter_reply({
|
||||||
add_headers = {['Content-Transfer-Encoding'] = 'quoted-printable'}
|
add_headers = {['Content-Transfer-Encoding'] = new_cte}
|
||||||
})
|
})
|
||||||
seen_cte = true
|
seen_cte = true
|
||||||
return
|
return
|
||||||
@@ -659,7 +897,7 @@ rspamd_config:register_symbol({
|
|||||||
task:headers_foreach(rewrite_ct_cb, {full = true})
|
task:headers_foreach(rewrite_ct_cb, {full = true})
|
||||||
|
|
||||||
if not seen_cte and rewrite.need_rewrite_ct then
|
if not seen_cte and rewrite.need_rewrite_ct then
|
||||||
out[#out + 1] = string.format('%s: %s', 'Content-Transfer-Encoding', 'quoted-printable')
|
out[#out + 1] = string.format('%s: %s', 'Content-Transfer-Encoding', new_cte)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- End of headers
|
-- End of headers
|
||||||
@@ -678,7 +916,7 @@ rspamd_config:register_symbol({
|
|||||||
out_parts[#out_parts + 1] = o
|
out_parts[#out_parts + 1] = o
|
||||||
out_parts[#out_parts + 1] = newline_s
|
out_parts[#out_parts + 1] = newline_s
|
||||||
else
|
else
|
||||||
local removePrefix = "--\x0D\x0AContent-Type"
|
local removePrefix = "--\x0D\x0AContent-"
|
||||||
if string.lower(string.sub(tostring(o[1]), 1, string.len(removePrefix))) == string.lower(removePrefix) then
|
if string.lower(string.sub(tostring(o[1]), 1, string.len(removePrefix))) == string.lower(removePrefix) then
|
||||||
o[1] = string.sub(tostring(o[1]), string.len("--\x0D\x0A") + 1)
|
o[1] = string.sub(tostring(o[1]), string.len("--\x0D\x0A") + 1)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -32,47 +32,42 @@ function parse_email($email) {
|
|||||||
$a = strrpos($email, '@');
|
$a = strrpos($email, '@');
|
||||||
return array('local' => substr($email, 0, $a), 'domain' => substr(substr($email, $a), 1));
|
return array('local' => substr($email, 0, $a), 'domain' => substr(substr($email, $a), 1));
|
||||||
}
|
}
|
||||||
if (!function_exists('getallheaders')) {
|
// rspamd metadata_exporter (multipart formatter):
|
||||||
function getallheaders() {
|
// - $_POST['metadata'] JSON with the rspamd metadata
|
||||||
if (!is_array($_SERVER)) {
|
// - $_FILES['message'] raw RFC822 message
|
||||||
return array();
|
if (empty($_POST['metadata']) || !isset($_FILES['message']) || $_FILES['message']['error'] !== UPLOAD_ERR_OK) {
|
||||||
}
|
error_log("QUARANTINE: missing multipart parts from rspamd" . PHP_EOL);
|
||||||
$headers = array();
|
http_response_code(400);
|
||||||
foreach ($_SERVER as $name => $value) {
|
exit;
|
||||||
if (substr($name, 0, 5) == 'HTTP_') {
|
|
||||||
$headers[str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($name, 5)))))] = $value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return $headers;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$raw_data_content = file_get_contents('php://input');
|
$meta = json_decode($_POST['metadata'], true);
|
||||||
|
if (!is_array($meta)) {
|
||||||
|
error_log("QUARANTINE: cannot decode metadata JSON" . PHP_EOL);
|
||||||
|
http_response_code(400);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$raw_data_content = file_get_contents($_FILES['message']['tmp_name']);
|
||||||
$raw_data = mb_convert_encoding($raw_data_content, 'HTML-ENTITIES', "UTF-8");
|
$raw_data = mb_convert_encoding($raw_data_content, 'HTML-ENTITIES', "UTF-8");
|
||||||
$headers = getallheaders();
|
$raw_size = (int)$_FILES['message']['size'];
|
||||||
|
|
||||||
$qid = $headers['X-Rspamd-Qid'];
|
$qid = $meta['qid'] ?? 'unknown';
|
||||||
$fuzzy = $headers['X-Rspamd-Fuzzy'];
|
$subject = $meta['subject'] ?? '';
|
||||||
$subject = iconv_mime_decode($headers['X-Rspamd-Subject']);
|
$score = $meta['score'] ?? 0;
|
||||||
$score = $headers['X-Rspamd-Score'];
|
$rcpts = $meta['rcpt'] ?? array();
|
||||||
$rcpts = $headers['X-Rspamd-Rcpt'];
|
$user = $meta['user'] ?? 'unknown';
|
||||||
$user = $headers['X-Rspamd-User'];
|
$ip = $meta['ip'] ?? 'unknown';
|
||||||
$ip = $headers['X-Rspamd-Ip'];
|
$action = $meta['action'] ?? 'no action';
|
||||||
$action = $headers['X-Rspamd-Action'];
|
$sender = $meta['from'] ?? '';
|
||||||
$sender = $headers['X-Rspamd-From'];
|
$symbols = json_encode($meta['symbols'] ?? array());
|
||||||
$symbols = $headers['X-Rspamd-Symbols'];
|
$fuzzy = json_encode(is_array($meta['fuzzy'] ?? null) ? $meta['fuzzy'] : array());
|
||||||
|
|
||||||
$raw_size = (int)$_SERVER['CONTENT_LENGTH'];
|
|
||||||
|
|
||||||
if (empty($sender)) {
|
if (empty($sender)) {
|
||||||
error_log("QUARANTINE: Unknown sender, assuming empty-env-from@localhost" . PHP_EOL);
|
error_log("QUARANTINE: Unknown sender, assuming empty-env-from@localhost" . PHP_EOL);
|
||||||
$sender = 'empty-env-from@localhost';
|
$sender = 'empty-env-from@localhost';
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($fuzzy == 'unknown') {
|
|
||||||
$fuzzy = '[]';
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$max_size = (int)$redis->Get('Q_MAX_SIZE');
|
$max_size = (int)$redis->Get('Q_MAX_SIZE');
|
||||||
if (($max_size * 1048576) < $raw_size) {
|
if (($max_size * 1048576) < $raw_size) {
|
||||||
@@ -94,7 +89,7 @@ catch (RedisException $e) {
|
|||||||
$rcpt_final_mailboxes = array();
|
$rcpt_final_mailboxes = array();
|
||||||
|
|
||||||
// Loop through all rcpts
|
// Loop through all rcpts
|
||||||
foreach (json_decode($rcpts, true) as $rcpt) {
|
foreach ($rcpts as $rcpt) {
|
||||||
// Remove tag
|
// Remove tag
|
||||||
$rcpt = preg_replace('/^(.*?)\+.*(@.*)$/', '$1$2', $rcpt);
|
$rcpt = preg_replace('/^(.*?)\+.*(@.*)$/', '$1$2', $rcpt);
|
||||||
|
|
||||||
@@ -158,12 +153,12 @@ foreach (json_decode($rcpts, true) as $rcpt) {
|
|||||||
|
|
||||||
// Loop through all found gotos
|
// Loop through all found gotos
|
||||||
foreach ($gotos_array as $index => &$goto) {
|
foreach ($gotos_array as $index => &$goto) {
|
||||||
error_log("RCPT RESOVLER: http pipe: query " . $goto . " as username from mailbox" . PHP_EOL);
|
error_log("RCPT RESOLVER: http pipe: query " . $goto . " as username from mailbox" . PHP_EOL);
|
||||||
$stmt = $pdo->prepare("SELECT `username` FROM `mailbox` WHERE `username` = :goto AND (`active`= '1' OR `active`= '2');");
|
$stmt = $pdo->prepare("SELECT `username` FROM `mailbox` WHERE `username` = :goto AND (`active`= '1' OR `active`= '2');");
|
||||||
$stmt->execute(array(':goto' => $goto));
|
$stmt->execute(array(':goto' => $goto));
|
||||||
$username = $stmt->fetch(PDO::FETCH_ASSOC)['username'];
|
$username = $stmt->fetch(PDO::FETCH_ASSOC)['username'];
|
||||||
if (!empty($username)) {
|
if (!empty($username)) {
|
||||||
error_log("RCPT RESOVLER: http pipe: mailbox found: " . $username . PHP_EOL);
|
error_log("RCPT RESOLVER: http pipe: mailbox found: " . $username . PHP_EOL);
|
||||||
// Current goto is a mailbox, save to rcpt_final_mailboxes if not a duplicate
|
// Current goto is a mailbox, save to rcpt_final_mailboxes if not a duplicate
|
||||||
if (!in_array($username, $rcpt_final_mailboxes)) {
|
if (!in_array($username, $rcpt_final_mailboxes)) {
|
||||||
$rcpt_final_mailboxes[] = $username;
|
$rcpt_final_mailboxes[] = $username;
|
||||||
@@ -172,21 +167,21 @@ foreach (json_decode($rcpts, true) as $rcpt) {
|
|||||||
else {
|
else {
|
||||||
$parsed_goto = parse_email($goto);
|
$parsed_goto = parse_email($goto);
|
||||||
if (!$redis->hGet('DOMAIN_MAP', $parsed_goto['domain'])) {
|
if (!$redis->hGet('DOMAIN_MAP', $parsed_goto['domain'])) {
|
||||||
error_log("RCPT RESOVLER:" . $goto . " is not a mailcow handled mailbox or alias address" . PHP_EOL);
|
error_log("RCPT RESOLVER:" . $goto . " is not a mailcow handled mailbox or alias address" . PHP_EOL);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
$stmt = $pdo->prepare("SELECT `goto` FROM `alias` WHERE `address` = :goto AND `active` = '1'");
|
$stmt = $pdo->prepare("SELECT `goto` FROM `alias` WHERE `address` = :goto AND `active` = '1'");
|
||||||
$stmt->execute(array(':goto' => $goto));
|
$stmt->execute(array(':goto' => $goto));
|
||||||
$goto_branch = $stmt->fetch(PDO::FETCH_ASSOC)['goto'];
|
$goto_branch = $stmt->fetch(PDO::FETCH_ASSOC)['goto'];
|
||||||
if ($goto_branch) {
|
if ($goto_branch) {
|
||||||
error_log("RCPT RESOVLER: http pipe: goto address " . $goto . " is an alias branch for " . $goto_branch . PHP_EOL);
|
error_log("RCPT RESOLVER: http pipe: goto address " . $goto . " is an alias branch for " . $goto_branch . PHP_EOL);
|
||||||
$goto_branch_array = explode(',', $goto_branch);
|
$goto_branch_array = explode(',', $goto_branch);
|
||||||
} else {
|
} else {
|
||||||
$stmt = $pdo->prepare("SELECT `target_domain` FROM `alias_domain` WHERE `alias_domain` = :domain AND `active` = '1'");
|
$stmt = $pdo->prepare("SELECT `target_domain` FROM `alias_domain` WHERE `alias_domain` = :domain AND `active` = '1'");
|
||||||
$stmt->execute(array(':domain' => $parsed_goto['domain']));
|
$stmt->execute(array(':domain' => $parsed_goto['domain']));
|
||||||
$goto_branch = $stmt->fetch(PDO::FETCH_ASSOC)['target_domain'];
|
$goto_branch = $stmt->fetch(PDO::FETCH_ASSOC)['target_domain'];
|
||||||
if ($goto_branch) {
|
if ($goto_branch) {
|
||||||
error_log("RCPT RESOVLER: http pipe: goto domain " . $parsed_goto['domain'] . " is a domain alias branch for " . $goto_branch . PHP_EOL);
|
error_log("RCPT RESOLVER: http pipe: goto domain " . $parsed_goto['domain'] . " is a domain alias branch for " . $goto_branch . PHP_EOL);
|
||||||
$goto_branch_array = array($parsed_goto['local'] . '@' . $goto_branch);
|
$goto_branch_array = array($parsed_goto['local'] . '@' . $goto_branch);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -208,11 +203,11 @@ foreach (json_decode($rcpts, true) as $rcpt) {
|
|||||||
// Force exit if loop cannot be solved
|
// Force exit if loop cannot be solved
|
||||||
// Postfix does not allow for alias loops, so this should never happen.
|
// Postfix does not allow for alias loops, so this should never happen.
|
||||||
$loop_c++;
|
$loop_c++;
|
||||||
error_log("RCPT RESOVLER: http pipe: goto array count on loop #". $loop_c . " is " . count($gotos_array) . PHP_EOL);
|
error_log("RCPT RESOLVER: http pipe: goto array count on loop #". $loop_c . " is " . count($gotos_array) . PHP_EOL);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (PDOException $e) {
|
catch (PDOException $e) {
|
||||||
error_log("RCPT RESOVLER: " . $e->getMessage() . PHP_EOL);
|
error_log("RCPT RESOLVER: " . $e->getMessage() . PHP_EOL);
|
||||||
http_response_code(502);
|
http_response_code(502);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,50 +32,46 @@ function parse_email($email) {
|
|||||||
$a = strrpos($email, '@');
|
$a = strrpos($email, '@');
|
||||||
return array('local' => substr($email, 0, $a), 'domain' => substr(substr($email, $a), 1));
|
return array('local' => substr($email, 0, $a), 'domain' => substr(substr($email, $a), 1));
|
||||||
}
|
}
|
||||||
if (!function_exists('getallheaders')) {
|
// rspamd metadata_exporter (multipart formatter): metadata JSON arrives as $_POST['metadata'].
|
||||||
function getallheaders() {
|
if (empty($_POST['metadata'])) {
|
||||||
if (!is_array($_SERVER)) {
|
error_log("NOTIFY: missing metadata part from rspamd" . PHP_EOL);
|
||||||
return array();
|
http_response_code(400);
|
||||||
}
|
exit;
|
||||||
$headers = array();
|
|
||||||
foreach ($_SERVER as $name => $value) {
|
|
||||||
if (substr($name, 0, 5) == 'HTTP_') {
|
|
||||||
$headers[str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($name, 5)))))] = $value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return $headers;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$headers = getallheaders();
|
$meta = json_decode($_POST['metadata'], true);
|
||||||
$json_body = json_decode(file_get_contents('php://input'));
|
if (!is_array($meta)) {
|
||||||
|
error_log("NOTIFY: cannot decode metadata JSON" . PHP_EOL);
|
||||||
|
http_response_code(400);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
$qid = $headers['X-Rspamd-Qid'];
|
$qid = $meta['qid'] ?? 'unknown';
|
||||||
$rcpts = $headers['X-Rspamd-Rcpt'];
|
$rcpts = $meta['rcpt'] ?? array();
|
||||||
$sender = $headers['X-Rspamd-From'];
|
$sender = $meta['from'] ?? '';
|
||||||
$ip = $headers['X-Rspamd-Ip'];
|
$ip = $meta['ip'] ?? 'unknown';
|
||||||
$subject = iconv_mime_decode($headers['X-Rspamd-Subject']);
|
$subject = $meta['subject'] ?? '';
|
||||||
$messageid= $json_body->message_id;
|
$messageid= $meta['message_id'] ?? '';
|
||||||
$priority = 0;
|
$priority = 0;
|
||||||
|
|
||||||
$symbols_array = json_decode($headers['X-Rspamd-Symbols'], true);
|
$symbols_array = $meta['symbols'] ?? array();
|
||||||
if (is_array($symbols_array)) {
|
if (is_array($symbols_array)) {
|
||||||
foreach ($symbols_array as $symbol) {
|
foreach ($symbols_array as $symbol) {
|
||||||
if ($symbol['name'] == 'HAS_X_PRIO_ONE') {
|
if (($symbol['name'] ?? null) == 'HAS_X_PRIO_ONE') {
|
||||||
$priority = 1;
|
$priority = 1;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$sender_address = $json_body->header_from[0];
|
$sender_address = $meta['header_from'][0] ?? '';
|
||||||
$sender_name = '-';
|
$sender_name = '-';
|
||||||
if (preg_match('/(?<name>.*?)<(?<address>.*?)>/i', $sender_address, $matches)) {
|
if (preg_match('/(?<name>.*?)<(?<address>.*?)>/i', $sender_address, $matches)) {
|
||||||
$sender_address = $matches['address'];
|
$sender_address = $matches['address'];
|
||||||
$sender_name = trim($matches['name'], '"\' ');
|
$sender_name = trim($matches['name'], '"\' ');
|
||||||
}
|
}
|
||||||
|
|
||||||
$to_address = $json_body->header_to[0];
|
$to_address = $meta['header_to'][0] ?? '';
|
||||||
$to_name = '-';
|
$to_name = '-';
|
||||||
if (preg_match('/(?<name>.*?)<(?<address>.*?)>/i', $to_address, $matches)) {
|
if (preg_match('/(?<name>.*?)<(?<address>.*?)>/i', $to_address, $matches)) {
|
||||||
$to_address = $matches['address'];
|
$to_address = $matches['address'];
|
||||||
@@ -85,7 +81,7 @@ if (preg_match('/(?<name>.*?)<(?<address>.*?)>/i', $to_address, $matches)) {
|
|||||||
$rcpt_final_mailboxes = array();
|
$rcpt_final_mailboxes = array();
|
||||||
|
|
||||||
// Loop through all rcpts
|
// Loop through all rcpts
|
||||||
foreach (json_decode($rcpts, true) as $rcpt) {
|
foreach ($rcpts as $rcpt) {
|
||||||
// Remove tag
|
// Remove tag
|
||||||
$rcpt = preg_replace('/^(.*?)\+.*(@.*)$/', '$1$2', $rcpt);
|
$rcpt = preg_replace('/^(.*?)\+.*(@.*)$/', '$1$2', $rcpt);
|
||||||
|
|
||||||
@@ -143,12 +139,12 @@ foreach (json_decode($rcpts, true) as $rcpt) {
|
|||||||
|
|
||||||
// Loop through all found gotos
|
// Loop through all found gotos
|
||||||
foreach ($gotos_array as $index => &$goto) {
|
foreach ($gotos_array as $index => &$goto) {
|
||||||
error_log("RCPT RESOVLER: http pipe: query " . $goto . " as username from mailbox" . PHP_EOL);
|
error_log("RCPT RESOLVER: http pipe: query " . $goto . " as username from mailbox" . PHP_EOL);
|
||||||
$stmt = $pdo->prepare("SELECT `username` FROM `mailbox` WHERE `username` = :goto AND (`active`= '1' OR `active`= '2');");
|
$stmt = $pdo->prepare("SELECT `username` FROM `mailbox` WHERE `username` = :goto AND (`active`= '1' OR `active`= '2');");
|
||||||
$stmt->execute(array(':goto' => $goto));
|
$stmt->execute(array(':goto' => $goto));
|
||||||
$username = $stmt->fetch(PDO::FETCH_ASSOC)['username'];
|
$username = $stmt->fetch(PDO::FETCH_ASSOC)['username'];
|
||||||
if (!empty($username)) {
|
if (!empty($username)) {
|
||||||
error_log("RCPT RESOVLER: http pipe: mailbox found: " . $username . PHP_EOL);
|
error_log("RCPT RESOLVER: http pipe: mailbox found: " . $username . PHP_EOL);
|
||||||
// Current goto is a mailbox, save to rcpt_final_mailboxes if not a duplicate
|
// Current goto is a mailbox, save to rcpt_final_mailboxes if not a duplicate
|
||||||
if (!in_array($username, $rcpt_final_mailboxes)) {
|
if (!in_array($username, $rcpt_final_mailboxes)) {
|
||||||
$rcpt_final_mailboxes[] = $username;
|
$rcpt_final_mailboxes[] = $username;
|
||||||
@@ -157,21 +153,21 @@ foreach (json_decode($rcpts, true) as $rcpt) {
|
|||||||
else {
|
else {
|
||||||
$parsed_goto = parse_email($goto);
|
$parsed_goto = parse_email($goto);
|
||||||
if (!$redis->hGet('DOMAIN_MAP', $parsed_goto['domain'])) {
|
if (!$redis->hGet('DOMAIN_MAP', $parsed_goto['domain'])) {
|
||||||
error_log("RCPT RESOVLER:" . $goto . " is not a mailcow handled mailbox or alias address" . PHP_EOL);
|
error_log("RCPT RESOLVER:" . $goto . " is not a mailcow handled mailbox or alias address" . PHP_EOL);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
$stmt = $pdo->prepare("SELECT `goto` FROM `alias` WHERE `address` = :goto AND `active` = '1'");
|
$stmt = $pdo->prepare("SELECT `goto` FROM `alias` WHERE `address` = :goto AND `active` = '1'");
|
||||||
$stmt->execute(array(':goto' => $goto));
|
$stmt->execute(array(':goto' => $goto));
|
||||||
$goto_branch = $stmt->fetch(PDO::FETCH_ASSOC)['goto'];
|
$goto_branch = $stmt->fetch(PDO::FETCH_ASSOC)['goto'];
|
||||||
if ($goto_branch) {
|
if ($goto_branch) {
|
||||||
error_log("RCPT RESOVLER: http pipe: goto address " . $goto . " is an alias branch for " . $goto_branch . PHP_EOL);
|
error_log("RCPT RESOLVER: http pipe: goto address " . $goto . " is an alias branch for " . $goto_branch . PHP_EOL);
|
||||||
$goto_branch_array = explode(',', $goto_branch);
|
$goto_branch_array = explode(',', $goto_branch);
|
||||||
} else {
|
} else {
|
||||||
$stmt = $pdo->prepare("SELECT `target_domain` FROM `alias_domain` WHERE `alias_domain` = :domain AND `active` = '1'");
|
$stmt = $pdo->prepare("SELECT `target_domain` FROM `alias_domain` WHERE `alias_domain` = :domain AND `active` = '1'");
|
||||||
$stmt->execute(array(':domain' => $parsed_goto['domain']));
|
$stmt->execute(array(':domain' => $parsed_goto['domain']));
|
||||||
$goto_branch = $stmt->fetch(PDO::FETCH_ASSOC)['target_domain'];
|
$goto_branch = $stmt->fetch(PDO::FETCH_ASSOC)['target_domain'];
|
||||||
if ($goto_branch) {
|
if ($goto_branch) {
|
||||||
error_log("RCPT RESOVLER: http pipe: goto domain " . $parsed_goto['domain'] . " is a domain alias branch for " . $goto_branch . PHP_EOL);
|
error_log("RCPT RESOLVER: http pipe: goto domain " . $parsed_goto['domain'] . " is a domain alias branch for " . $goto_branch . PHP_EOL);
|
||||||
$goto_branch_array = array($parsed_goto['local'] . '@' . $goto_branch);
|
$goto_branch_array = array($parsed_goto['local'] . '@' . $goto_branch);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -193,11 +189,11 @@ foreach (json_decode($rcpts, true) as $rcpt) {
|
|||||||
// Force exit if loop cannot be solved
|
// Force exit if loop cannot be solved
|
||||||
// Postfix does not allow for alias loops, so this should never happen.
|
// Postfix does not allow for alias loops, so this should never happen.
|
||||||
$loop_c++;
|
$loop_c++;
|
||||||
error_log("RCPT RESOVLER: http pipe: goto array count on loop #". $loop_c . " is " . count($gotos_array) . PHP_EOL);
|
error_log("RCPT RESOLVER: http pipe: goto array count on loop #". $loop_c . " is " . count($gotos_array) . PHP_EOL);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (PDOException $e) {
|
catch (PDOException $e) {
|
||||||
error_log("RCPT RESOVLER: " . $e->getMessage() . PHP_EOL);
|
error_log("RCPT RESOLVER: " . $e->getMessage() . PHP_EOL);
|
||||||
http_response_code(502);
|
http_response_code(502);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,18 +2,7 @@
|
|||||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
|
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
|
||||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.admin.inc.php';
|
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.admin.inc.php';
|
||||||
|
|
||||||
if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'domainadmin') {
|
protect_route(['admin']);
|
||||||
header('Location: /domainadmin/mailbox');
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user') {
|
|
||||||
header('Location: /user');
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
elseif (!isset($_SESSION['mailcow_cc_role']) || $_SESSION['mailcow_cc_role'] != "admin") {
|
|
||||||
header('Location: /admin');
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
|
|
||||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php';
|
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php';
|
||||||
$_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
|
$_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
|
||||||
|
|||||||
@@ -3,8 +3,11 @@ require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
|
|||||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.admin.inc.php';
|
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.admin.inc.php';
|
||||||
|
|
||||||
if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'admin') {
|
if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'admin') {
|
||||||
header('Location: /admin/dashboard');
|
// Only redirect to dashboard if NO pending actions
|
||||||
exit();
|
if (empty($_SESSION['pending_tfa_setup']) && empty($_SESSION['pending_pw_update'])) {
|
||||||
|
header('Location: /admin/dashboard');
|
||||||
|
exit();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'domainadmin') {
|
elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'domainadmin') {
|
||||||
header('Location: /domainadmin/mailbox');
|
header('Location: /domainadmin/mailbox');
|
||||||
|
|||||||
@@ -2,18 +2,7 @@
|
|||||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
|
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
|
||||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.admin.inc.php';
|
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.admin.inc.php';
|
||||||
|
|
||||||
if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'domainadmin') {
|
protect_route(['admin']);
|
||||||
header('Location: /domainadmin/mailbox');
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user') {
|
|
||||||
header('Location: /user');
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
elseif (!isset($_SESSION['mailcow_cc_role']) || $_SESSION['mailcow_cc_role'] != "admin") {
|
|
||||||
header('Location: /admin');
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
|
|
||||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php';
|
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php';
|
||||||
$_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
|
$_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
|
||||||
|
|||||||
@@ -2,19 +2,7 @@
|
|||||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
|
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
|
||||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.admin.inc.php';
|
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.admin.inc.php';
|
||||||
|
|
||||||
if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'domainadmin') {
|
protect_route(['admin']);
|
||||||
header('Location: /domainadmin/mailbox');
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user') {
|
|
||||||
header('Location: /user');
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
elseif (!isset($_SESSION['mailcow_cc_role']) || $_SESSION['mailcow_cc_role'] != "admin") {
|
|
||||||
header('Location: /admin');
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php';
|
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php';
|
||||||
$js_minifier->add('/web/js/site/queue.js');
|
$js_minifier->add('/web/js/site/queue.js');
|
||||||
|
|||||||
@@ -2,18 +2,7 @@
|
|||||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
|
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
|
||||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.admin.inc.php';
|
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.admin.inc.php';
|
||||||
|
|
||||||
if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'domainadmin') {
|
protect_route(['admin']);
|
||||||
header('Location: /domainadmin/mailbox');
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user') {
|
|
||||||
header('Location: /user');
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
elseif (!isset($_SESSION['mailcow_cc_role']) || $_SESSION['mailcow_cc_role'] != "admin") {
|
|
||||||
header('Location: /admin');
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
|
|
||||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php';
|
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php';
|
||||||
$_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
|
$_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
|
||||||
@@ -126,6 +115,8 @@ $template_data = [
|
|||||||
'logo_dark_specs' => customize('get', 'main_logo_dark_specs'),
|
'logo_dark_specs' => customize('get', 'main_logo_dark_specs'),
|
||||||
'ip_check' => customize('get', 'ip_check'),
|
'ip_check' => customize('get', 'ip_check'),
|
||||||
'custom_login' => customize('get', 'custom_login'),
|
'custom_login' => customize('get', 'custom_login'),
|
||||||
|
'ui_themes' => customize('get', 'ui_themes'),
|
||||||
|
'ui_theme' => customize('get', 'ui_theme'),
|
||||||
'password_complexity' => password_complexity('get'),
|
'password_complexity' => password_complexity('get'),
|
||||||
'show_rspamd_global_filters' => @$_SESSION['show_rspamd_global_filters'],
|
'show_rspamd_global_filters' => @$_SESSION['show_rspamd_global_filters'],
|
||||||
'cors_settings' => $cors_settings,
|
'cors_settings' => $cors_settings,
|
||||||
|
|||||||
+112
-2
@@ -185,6 +185,9 @@ paths:
|
|||||||
example:
|
example:
|
||||||
username: info@domain.tld
|
username: info@domain.tld
|
||||||
domain: domain.tld
|
domain: domain.tld
|
||||||
|
description: my time limited alias
|
||||||
|
validity: 8760
|
||||||
|
permanent: false
|
||||||
properties:
|
properties:
|
||||||
username:
|
username:
|
||||||
description: 'the mailbox an alias should be created for'
|
description: 'the mailbox an alias should be created for'
|
||||||
@@ -192,6 +195,15 @@ paths:
|
|||||||
domain:
|
domain:
|
||||||
description: "the domain"
|
description: "the domain"
|
||||||
type: string
|
type: string
|
||||||
|
description:
|
||||||
|
description: "a description for the alias, defaults to an empty string"
|
||||||
|
type: string
|
||||||
|
validity:
|
||||||
|
description: "how many hours the alias stays valid, 1 to 87600, defaults to 8760 (one year)"
|
||||||
|
type: integer
|
||||||
|
permanent:
|
||||||
|
description: "keep the alias after it expired, defaults to false"
|
||||||
|
type: boolean
|
||||||
type: object
|
type: object
|
||||||
summary: Create time limited alias
|
summary: Create time limited alias
|
||||||
/api/v1/add/app-passwd:
|
/api/v1/add/app-passwd:
|
||||||
@@ -1072,6 +1084,7 @@ paths:
|
|||||||
password2: "*"
|
password2: "*"
|
||||||
quota: "3072"
|
quota: "3072"
|
||||||
force_pw_update: "1"
|
force_pw_update: "1"
|
||||||
|
force_tfa: "1"
|
||||||
tls_enforce_in: "1"
|
tls_enforce_in: "1"
|
||||||
tls_enforce_out: "1"
|
tls_enforce_out: "1"
|
||||||
tags: ["tag1", "tag2"]
|
tags: ["tag1", "tag2"]
|
||||||
@@ -1118,6 +1131,7 @@ paths:
|
|||||||
password2: atedismonsin
|
password2: atedismonsin
|
||||||
quota: "3072"
|
quota: "3072"
|
||||||
force_pw_update: "1"
|
force_pw_update: "1"
|
||||||
|
force_tfa: "1"
|
||||||
tls_enforce_in: "1"
|
tls_enforce_in: "1"
|
||||||
tls_enforce_out: "1"
|
tls_enforce_out: "1"
|
||||||
tags: ["tag1", "tag2"]
|
tags: ["tag1", "tag2"]
|
||||||
@@ -1151,6 +1165,9 @@ paths:
|
|||||||
force_pw_update:
|
force_pw_update:
|
||||||
description: forces the user to update its password on first login
|
description: forces the user to update its password on first login
|
||||||
type: boolean
|
type: boolean
|
||||||
|
force_tfa:
|
||||||
|
description: force 2FA enrollment at login
|
||||||
|
type: boolean
|
||||||
tls_enforce_in:
|
tls_enforce_in:
|
||||||
description: force inbound email tls encryption
|
description: force inbound email tls encryption
|
||||||
type: boolean
|
type: boolean
|
||||||
@@ -2454,6 +2471,90 @@ paths:
|
|||||||
type: object
|
type: object
|
||||||
type: object
|
type: object
|
||||||
summary: Delete mails in Quarantine
|
summary: Delete mails in Quarantine
|
||||||
|
/api/v1/edit/qitem:
|
||||||
|
post:
|
||||||
|
responses:
|
||||||
|
"401":
|
||||||
|
$ref: "#/components/responses/Unauthorized"
|
||||||
|
"200":
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
examples:
|
||||||
|
release:
|
||||||
|
value:
|
||||||
|
- log:
|
||||||
|
- quarantine
|
||||||
|
- edit
|
||||||
|
- id:
|
||||||
|
- "33"
|
||||||
|
action: release
|
||||||
|
msg:
|
||||||
|
- item_released
|
||||||
|
- "33"
|
||||||
|
type: success
|
||||||
|
learnham:
|
||||||
|
value:
|
||||||
|
- log:
|
||||||
|
- quarantine
|
||||||
|
- edit
|
||||||
|
- id:
|
||||||
|
- "34"
|
||||||
|
action: learnham
|
||||||
|
msg:
|
||||||
|
- item_learned
|
||||||
|
- "34"
|
||||||
|
type: success
|
||||||
|
schema:
|
||||||
|
properties:
|
||||||
|
log:
|
||||||
|
description: contains request object
|
||||||
|
items: {}
|
||||||
|
type: array
|
||||||
|
msg:
|
||||||
|
items: {}
|
||||||
|
type: array
|
||||||
|
type:
|
||||||
|
enum:
|
||||||
|
- success
|
||||||
|
- danger
|
||||||
|
- error
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
description: OK
|
||||||
|
headers: {}
|
||||||
|
tags:
|
||||||
|
- Quarantine
|
||||||
|
description: >-
|
||||||
|
Using this endpoint you can perform actions on quarantine items. It is possible to release
|
||||||
|
emails from quarantine into to the inbox, or learn them as ham to improve Rspamd filtering.
|
||||||
|
You must provide the quarantine item IDs. You can get the IDs using the GET method.
|
||||||
|
operationId: Edit mails in Quarantine
|
||||||
|
requestBody:
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
example:
|
||||||
|
items:
|
||||||
|
- "33"
|
||||||
|
- "34"
|
||||||
|
attr:
|
||||||
|
action: release
|
||||||
|
properties:
|
||||||
|
items:
|
||||||
|
description: contains list of quarantine item IDs to release or learn as ham
|
||||||
|
type: object
|
||||||
|
attr:
|
||||||
|
description: attributes for the action
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
action:
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- release
|
||||||
|
- learnham
|
||||||
|
description: "release - return email to inbox; learnham - learn as ham to improve filtering"
|
||||||
|
type: object
|
||||||
|
summary: Edit mails in Quarantine
|
||||||
/api/v1/delete/recipient_map:
|
/api/v1/delete/recipient_map:
|
||||||
post:
|
post:
|
||||||
responses:
|
responses:
|
||||||
@@ -3330,6 +3431,7 @@ paths:
|
|||||||
- mailbox
|
- mailbox
|
||||||
- active: "1"
|
- active: "1"
|
||||||
force_pw_update: "0"
|
force_pw_update: "0"
|
||||||
|
force_tfa: "0"
|
||||||
name: Full name
|
name: Full name
|
||||||
password: "*"
|
password: "*"
|
||||||
password2: "*"
|
password2: "*"
|
||||||
@@ -3380,6 +3482,7 @@ paths:
|
|||||||
attr:
|
attr:
|
||||||
active: "1"
|
active: "1"
|
||||||
force_pw_update: "0"
|
force_pw_update: "0"
|
||||||
|
force_tfa: "0"
|
||||||
name: Full name
|
name: Full name
|
||||||
authsource: mailcow
|
authsource: mailcow
|
||||||
password: ""
|
password: ""
|
||||||
@@ -3403,6 +3506,9 @@ paths:
|
|||||||
force_pw_update:
|
force_pw_update:
|
||||||
description: force user to change password on next login
|
description: force user to change password on next login
|
||||||
type: boolean
|
type: boolean
|
||||||
|
force_tfa:
|
||||||
|
description: force 2FA enrollment at login
|
||||||
|
type: boolean
|
||||||
name:
|
name:
|
||||||
description: Full name of the mailbox user
|
description: Full name of the mailbox user
|
||||||
type: string
|
type: string
|
||||||
@@ -4797,6 +4903,7 @@ paths:
|
|||||||
- active: "1"
|
- active: "1"
|
||||||
attributes:
|
attributes:
|
||||||
force_pw_update: "0"
|
force_pw_update: "0"
|
||||||
|
force_tfa: "0"
|
||||||
mailbox_format: "maildir:"
|
mailbox_format: "maildir:"
|
||||||
quarantine_notification: never
|
quarantine_notification: never
|
||||||
sogo_access: "1"
|
sogo_access: "1"
|
||||||
@@ -4813,6 +4920,7 @@ paths:
|
|||||||
quota: 3221225472
|
quota: 3221225472
|
||||||
quota_used: 0
|
quota_used: 0
|
||||||
rl: false
|
rl: false
|
||||||
|
sender_acl: ["otherbox@doman3.tld", "@aliasdomain.tld"]
|
||||||
spam_aliases: 0
|
spam_aliases: 0
|
||||||
username: info@doman3.tld
|
username: info@doman3.tld
|
||||||
tags: ["tag1", "tag2"]
|
tags: ["tag1", "tag2"]
|
||||||
@@ -5721,6 +5829,7 @@ paths:
|
|||||||
- active: "1"
|
- active: "1"
|
||||||
attributes:
|
attributes:
|
||||||
force_pw_update: "0"
|
force_pw_update: "0"
|
||||||
|
force_tfa: "0"
|
||||||
mailbox_format: "maildir:"
|
mailbox_format: "maildir:"
|
||||||
quarantine_notification: never
|
quarantine_notification: never
|
||||||
sogo_access: "1"
|
sogo_access: "1"
|
||||||
@@ -5738,6 +5847,7 @@ paths:
|
|||||||
quota: 3221225472
|
quota: 3221225472
|
||||||
quota_used: 0
|
quota_used: 0
|
||||||
rl: false
|
rl: false
|
||||||
|
sender_acl: ["otherbox@domain3.tld", "@aliasdomain.tld"]
|
||||||
spam_aliases: 0
|
spam_aliases: 0
|
||||||
username: info@domain3.tld
|
username: info@domain3.tld
|
||||||
tags: ["tag1", "tag2"]
|
tags: ["tag1", "tag2"]
|
||||||
@@ -5760,7 +5870,7 @@ paths:
|
|||||||
response:
|
response:
|
||||||
value:
|
value:
|
||||||
- type: "success"
|
- type: "success"
|
||||||
log: ["cors", "edit", {"allowed_origins": ["*", "mail.mailcow.tld"], "allowed_methods": ["POST", "GET", "DELETE", "PUT"]}]
|
log: ["cors", "edit", {"allowed_origins": ["*", "https://mail.mailcow.tld"], "allowed_methods": ["POST", "GET", "DELETE", "PUT"]}]
|
||||||
msg: "cors_headers_edited"
|
msg: "cors_headers_edited"
|
||||||
description: OK
|
description: OK
|
||||||
headers: { }
|
headers: { }
|
||||||
@@ -5777,7 +5887,7 @@ paths:
|
|||||||
schema:
|
schema:
|
||||||
example:
|
example:
|
||||||
attr:
|
attr:
|
||||||
allowed_origins: ["*", "mail.mailcow.tld"]
|
allowed_origins: ["*", "https://mail.mailcow.tld"]
|
||||||
allowed_methods: ["POST", "GET", "DELETE", "PUT"]
|
allowed_methods: ["POST", "GET", "DELETE", "PUT"]
|
||||||
properties:
|
properties:
|
||||||
attr:
|
attr:
|
||||||
|
|||||||
@@ -29,8 +29,8 @@ header('Content-Type: application/xml');
|
|||||||
<clientConfig version="1.1">
|
<clientConfig version="1.1">
|
||||||
<emailProvider id="<?=$mailcow_hostname; ?>">
|
<emailProvider id="<?=$mailcow_hostname; ?>">
|
||||||
<domain>%EMAILDOMAIN%</domain>
|
<domain>%EMAILDOMAIN%</domain>
|
||||||
<displayName>A mailcow mail server</displayName>
|
<displayName><?=$autodiscover_config['displayName']; ?></displayName>
|
||||||
<displayShortName>mail server</displayShortName>
|
<displayShortName><?=$autodiscover_config['displayShortName']; ?></displayShortName>
|
||||||
|
|
||||||
<incomingServer type="imap">
|
<incomingServer type="imap">
|
||||||
<hostname><?=$autodiscover_config['imap']['server']; ?></hostname>
|
<hostname><?=$autodiscover_config['imap']['server']; ?></hostname>
|
||||||
|
|||||||
+148
-85
@@ -60,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(
|
||||||
@@ -163,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>
|
||||||
@@ -238,6 +304,3 @@ if ($login_role === "user") {
|
|||||||
}
|
}
|
||||||
?>
|
?>
|
||||||
</Autodiscover>
|
</Autodiscover>
|
||||||
<?php
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
/*!
|
/*!
|
||||||
* Bootstrap-select v1.14.0-beta2 (https://developer.snapappointments.com/bootstrap-select)
|
* Bootstrap-select v1.14.0-beta3 (https://developer.snapappointments.com/bootstrap-select)
|
||||||
*
|
*
|
||||||
* Copyright 2012-2021 SnapAppointments, LLC
|
* Copyright 2012-2022 SnapAppointments, LLC
|
||||||
* Licensed under MIT (https://github.com/snapappointments/bootstrap-select/blob/master/LICENSE)
|
* Licensed under MIT (https://github.com/snapappointments/bootstrap-select/blob/master/LICENSE)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@-webkit-keyframes bs-notify-fadeOut {
|
@-webkit-keyframes bs-notify-fadeOut {
|
||||||
0% {
|
0% {
|
||||||
opacity: 0.9;
|
opacity: 0.9;
|
||||||
@@ -35,8 +35,7 @@ select.selectpicker {
|
|||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
.bootstrap-select {
|
.bootstrap-select {
|
||||||
width: 220px \0;
|
width: 220px;
|
||||||
/*IE9 and below*/
|
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
.bootstrap-select > .dropdown-toggle {
|
.bootstrap-select > .dropdown-toggle {
|
||||||
|
|||||||
@@ -17,15 +17,15 @@ table.dataTable>tbody>tr.child ul.dtr-details>li {
|
|||||||
}
|
}
|
||||||
table.dataTable.dtr-inline.collapsed>tbody>tr>td.dtr-control:before:hover,
|
table.dataTable.dtr-inline.collapsed>tbody>tr>td.dtr-control:before:hover,
|
||||||
table.dataTable.dtr-inline.collapsed>tbody>tr>th.dtr-control:before:hover {
|
table.dataTable.dtr-inline.collapsed>tbody>tr>th.dtr-control:before:hover {
|
||||||
background-color: #5e5e5e;
|
background-color: var(--mc-dtr-hover-bg);
|
||||||
}
|
}
|
||||||
table.dataTable.dtr-inline.collapsed>tbody>tr>td.dtr-control:before,
|
table.dataTable.dtr-inline.collapsed>tbody>tr>td.dtr-control:before,
|
||||||
table.dataTable.dtr-inline.collapsed>tbody>tr>th.dtr-control:before,
|
table.dataTable.dtr-inline.collapsed>tbody>tr>th.dtr-control:before,
|
||||||
table.dataTable td.dt-control:before {
|
table.dataTable td.dt-control:before {
|
||||||
background-color: #979797 !important;
|
background-color: var(--mc-dtr-bg) !important;
|
||||||
border: 1.5px solid #616161 !important;
|
border: 1.5px solid var(--mc-dtr-border) !important;
|
||||||
border-radius: 2px !important;
|
border-radius: 2px !important;
|
||||||
color: #fff;
|
color: var(--mc-dtr-color);
|
||||||
height: 1em;
|
height: 1em;
|
||||||
width: 1em;
|
width: 1em;
|
||||||
line-height: 1.25em;
|
line-height: 1.25em;
|
||||||
@@ -37,7 +37,7 @@ table.dataTable td.dt-control:before {
|
|||||||
table.dataTable.dtr-inline.collapsed>tbody>tr.parent>td.dtr-control:before,
|
table.dataTable.dtr-inline.collapsed>tbody>tr.parent>td.dtr-control:before,
|
||||||
table.dataTable.dtr-inline.collapsed>tbody>tr.parent>th.dtr-control:before,
|
table.dataTable.dtr-inline.collapsed>tbody>tr.parent>th.dtr-control:before,
|
||||||
table.dataTable td.dt-control:before {
|
table.dataTable td.dt-control:before {
|
||||||
background-color: #979797 !important;
|
background-color: var(--mc-dtr-active-bg) !important;
|
||||||
}
|
}
|
||||||
table.dataTable.table-striped>tbody>tr>td {
|
table.dataTable.table-striped>tbody>tr>td {
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
|
|||||||
@@ -34,11 +34,44 @@
|
|||||||
url('/fonts/noto-sans-v12-latin_greek_cyrillic-700italic.woff') format('woff');
|
url('/fonts/noto-sans-v12-latin_greek_cyrillic-700italic.woff') format('woff');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* theme tokens, override in css/themes/<name>/theme.css */
|
||||||
|
:root {
|
||||||
|
--mc-font-family: "Noto Sans","Helvetica Neue",Helvetica,Arial,sans-serif;
|
||||||
|
--mc-font-size: 10.5pt;
|
||||||
|
--mc-body-bg: #fbfbfb;
|
||||||
|
--mc-primary: #0F688D;
|
||||||
|
--mc-primary-border: #0d526d;
|
||||||
|
--mc-info: #148DBC;
|
||||||
|
--mc-info-border: #127ea8;
|
||||||
|
--mc-border: #dfdfdf;
|
||||||
|
--mc-surface: #fff;
|
||||||
|
--mc-surface-alt: #f7f7f7;
|
||||||
|
--mc-muted: #959595;
|
||||||
|
--mc-hr: #ccc;
|
||||||
|
--mc-progress-bg: #d5d5d5;
|
||||||
|
--mc-placeholder: rgb(137 137 137);
|
||||||
|
--mc-btn-outline-color: #000000;
|
||||||
|
--mc-btn-outline-border: #cfcfcf;
|
||||||
|
--mc-btn-outline-hover-bg: #f0f0f0;
|
||||||
|
--mc-btn-outline-active-bg: #f0f0f0;
|
||||||
|
--mc-btn-light-active-color: #fff;
|
||||||
|
--mc-btn-light-active-bg: #555;
|
||||||
|
--mc-btn-light-active-border: #4d4d4d;
|
||||||
|
--mc-dtr-bg: #979797;
|
||||||
|
--mc-dtr-border: #616161;
|
||||||
|
--mc-dtr-color: #fff;
|
||||||
|
--mc-dtr-hover-bg: #5e5e5e;
|
||||||
|
--mc-dtr-active-bg: #979797;
|
||||||
|
--mc-login-user: #28b62c;
|
||||||
|
--mc-login-domainadmin: #ffc107;
|
||||||
|
--mc-login-admin: #ff4136;
|
||||||
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
background-color: #fbfbfb;
|
background-color: var(--mc-body-bg);
|
||||||
}
|
}
|
||||||
#maxmsgsize { min-width: 80px; }
|
#maxmsgsize { min-width: 80px; }
|
||||||
#slider1 .slider-selection {
|
#slider1 .slider-selection {
|
||||||
@@ -86,17 +119,17 @@ body {
|
|||||||
max-height: 1000px
|
max-height: 1000px
|
||||||
}
|
}
|
||||||
.nav-tabs .nav-link, .nav-tabs .nav-link.disabled, .nav-tabs .nav-link.disabled:hover, .nav-tabs .nav-link.disabled:focus {
|
.nav-tabs .nav-link, .nav-tabs .nav-link.disabled, .nav-tabs .nav-link.disabled:hover, .nav-tabs .nav-link.disabled:focus {
|
||||||
border-color: #dfdfdf;
|
border-color: var(--mc-border);
|
||||||
}
|
}
|
||||||
.nav-tabs .nav-link.active, .nav-tabs .nav-item.show .nav-link {
|
.nav-tabs .nav-link.active, .nav-tabs .nav-item.show .nav-link {
|
||||||
border-color: #dfdfdf;
|
border-color: var(--mc-border);
|
||||||
border-bottom: 1px solid #ffffff;
|
border-bottom: 1px solid var(--mc-surface);
|
||||||
}
|
}
|
||||||
.nav-tabs .nav-link:hover, .nav-tabs .nav-link:focus {
|
.nav-tabs .nav-link:hover, .nav-tabs .nav-link:focus {
|
||||||
border-color: #dfdfdf;
|
border-color: var(--mc-border);
|
||||||
}
|
}
|
||||||
.nav-tabs {
|
.nav-tabs {
|
||||||
border-bottom: 1px solid #dfdfdf;
|
border-bottom: 1px solid var(--mc-border);
|
||||||
}
|
}
|
||||||
.bi {
|
.bi {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
@@ -164,13 +197,13 @@ body.modal-open {
|
|||||||
padding-right: inherit !important;
|
padding-right: inherit !important;
|
||||||
}
|
}
|
||||||
body {
|
body {
|
||||||
font-family: "Noto Sans","Helvetica Neue",Helvetica,Arial,sans-serif;
|
font-family: var(--mc-font-family);
|
||||||
font-size: 10.5pt;
|
font-size: var(--mc-font-size);
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
html {
|
html {
|
||||||
font-family: "Noto Sans","Helvetica Neue",Helvetica,Arial,sans-serif;
|
font-family: var(--mc-font-family);
|
||||||
font-size: 10.5pt;
|
font-size: var(--mc-font-size);
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
#mailcow-alert {
|
#mailcow-alert {
|
||||||
@@ -206,7 +239,7 @@ legend {
|
|||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
.overlay {
|
.overlay {
|
||||||
background: #fff;
|
background: var(--mc-surface);
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 10000;
|
z-index: 10000;
|
||||||
top: 0; right: 0; bottom: 0; left: 0;
|
top: 0; right: 0; bottom: 0; left: 0;
|
||||||
@@ -215,9 +248,6 @@ legend {
|
|||||||
.bootstrap-select.btn-group .no-results {
|
.bootstrap-select.btn-group .no-results {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-secondary {
|
|
||||||
color: rgb(197, 197, 197) !important;
|
|
||||||
}
|
|
||||||
.haveibeenpwned {
|
.haveibeenpwned {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
-webkit-user-select: none;
|
-webkit-user-select: none;
|
||||||
@@ -239,7 +269,7 @@ legend {
|
|||||||
.footer {
|
.footer {
|
||||||
margin-top: 27px;
|
margin-top: 27px;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
color: #959595;
|
color: var(--mc-muted);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
@@ -337,7 +367,7 @@ code {
|
|||||||
overflow-x: auto!important;
|
overflow-x: auto!important;
|
||||||
}
|
}
|
||||||
.well {
|
.well {
|
||||||
border: 1px solid #dfdfdf;
|
border: 1px solid var(--mc-border);
|
||||||
background-color: #f9f9f9;
|
background-color: #f9f9f9;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
}
|
}
|
||||||
@@ -356,47 +386,47 @@ button[aria-expanded='true'] > .caret {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.list-group-details {
|
.list-group-details {
|
||||||
background: #fff;
|
background: var(--mc-surface);
|
||||||
}
|
}
|
||||||
.list-group-header {
|
.list-group-header {
|
||||||
background: #f7f7f7;
|
background: var(--mc-surface-alt);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.bg-primary, .alert-primary, .btn-primary {
|
.bg-primary, .alert-primary, .btn-primary {
|
||||||
background-color: #0F688D !important;
|
background-color: var(--mc-primary) !important;
|
||||||
border-color: #0d526d !important;
|
border-color: var(--mc-primary-border) !important;
|
||||||
}
|
}
|
||||||
.bg-info, .alert-info, .btn-info {
|
.bg-info, .alert-info, .btn-info {
|
||||||
background-color: #148DBC !important;
|
background-color: var(--mc-info) !important;
|
||||||
border-color: #127ea8 !important;
|
border-color: var(--mc-info-border) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-secondary {
|
.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-secondary {
|
||||||
color: rgb(137 137 137)!important;
|
color: var(--mc-placeholder) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.progress {
|
.progress {
|
||||||
height: 16px;
|
height: 16px;
|
||||||
background-color: #d5d5d5;
|
background-color: var(--mc-progress-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.btn-outline-secondary:hover {
|
.btn-outline-secondary:hover {
|
||||||
background-color: #f0f0f0;
|
background-color: var(--mc-btn-outline-hover-bg);
|
||||||
}
|
}
|
||||||
.btn.btn-outline-secondary {
|
.btn.btn-outline-secondary {
|
||||||
color: #000000 !important;
|
color: var(--mc-btn-outline-color) !important;
|
||||||
border-color: #cfcfcf !important;
|
border-color: var(--mc-btn-outline-border) !important;
|
||||||
}
|
}
|
||||||
.btn-check:checked+.btn-outline-secondary, .btn-check:active+.btn-outline-secondary, .btn-outline-secondary:active, .btn-outline-secondary.active, .btn-outline-secondary.dropdown-toggle.show {
|
.btn-check:checked+.btn-outline-secondary, .btn-check:active+.btn-outline-secondary, .btn-outline-secondary:active, .btn-outline-secondary.active, .btn-outline-secondary.dropdown-toggle.show {
|
||||||
background-color: #f0f0f0 !important;
|
background-color: var(--mc-btn-outline-active-bg) !important;
|
||||||
}
|
}
|
||||||
.btn-check:checked+.btn-light, .btn-check:active+.btn-light, .btn-light:active, .btn-light.active, .show>.btn-light.dropdown-toggle {
|
.btn-check:checked+.btn-light, .btn-check:active+.btn-light, .btn-light:active, .btn-light.active, .show>.btn-light.dropdown-toggle {
|
||||||
color: #fff;
|
color: var(--mc-btn-light-active-color);
|
||||||
background-color: #555;
|
background-color: var(--mc-btn-light-active-bg);
|
||||||
background-image: none;
|
background-image: none;
|
||||||
border-color: #4d4d4d;
|
border-color: var(--mc-btn-light-active-border);
|
||||||
}
|
}
|
||||||
.btn-check:checked+.btn-light:focus, .btn-check:active+.btn-light:focus, .btn-light:active:focus, .btn-light.active:focus, .show>.btn-light.dropdown-toggle:focus,
|
.btn-check:checked+.btn-light:focus, .btn-check:active+.btn-light:focus, .btn-light:active:focus, .btn-light.active:focus, .show>.btn-light.dropdown-toggle:focus,
|
||||||
.btn-check:focus+.btn-light, .btn-light:focus {
|
.btn-check:focus+.btn-light, .btn-light:focus {
|
||||||
@@ -412,6 +442,26 @@ button[aria-expanded='true'] > .caret {
|
|||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* role marker on login pages, header icons differ too (color blindness) */
|
||||||
|
.login-card::before {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
height: 5px;
|
||||||
|
background: var(--mc-login-color);
|
||||||
|
border-radius: inherit;
|
||||||
|
border-bottom-left-radius: 0;
|
||||||
|
border-bottom-right-radius: 0;
|
||||||
|
}
|
||||||
|
.login-user {
|
||||||
|
--mc-login-color: var(--mc-login-user);
|
||||||
|
}
|
||||||
|
.login-domainadmin {
|
||||||
|
--mc-login-color: var(--mc-login-domainadmin);
|
||||||
|
}
|
||||||
|
.login-admin {
|
||||||
|
--mc-login-color: var(--mc-login-admin);
|
||||||
|
}
|
||||||
|
|
||||||
.hr-title {
|
.hr-title {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -423,7 +473,7 @@ button[aria-expanded='true'] > .caret {
|
|||||||
.hr-title::after {
|
.hr-title::after {
|
||||||
content: "";
|
content: "";
|
||||||
flex: 1;
|
flex: 1;
|
||||||
border-bottom: 1px solid #ccc;
|
border-bottom: 1px solid var(--mc-hr);
|
||||||
}
|
}
|
||||||
|
|
||||||
.hr-title:not(:empty)::before {
|
.hr-title:not(:empty)::before {
|
||||||
|
|||||||
+3750
-2935
File diff suppressed because it is too large.
Load diff
@@ -1,62 +1,103 @@
|
|||||||
|
/* theme tokens, override in css/themes/<name>/dark.css */
|
||||||
|
:root {
|
||||||
|
--mc-body-bg: #1c1c1e;
|
||||||
|
--mc-body-color: #f2f2f7;
|
||||||
|
--mc-surface: #2c2c2e;
|
||||||
|
--mc-surface-alt: #282828;
|
||||||
|
--mc-text: #ccc;
|
||||||
|
--mc-text-soft: #bbb;
|
||||||
|
--mc-muted: #8e8e93;
|
||||||
|
--mc-border: #555;
|
||||||
|
--mc-tab-border: #444444;
|
||||||
|
--mc-link: #6fc7e9;
|
||||||
|
--mc-link-hover: #3daedb;
|
||||||
|
--mc-btn-bg: #5e5e5e;
|
||||||
|
--mc-btn-border: #4c4c4e;
|
||||||
|
--mc-input-bg: #424242;
|
||||||
|
--mc-input-color: #e2e2e2;
|
||||||
|
--mc-input-border: #999;
|
||||||
|
--mc-dropdown-bg: #424242;
|
||||||
|
--mc-table-bg: #3a3a3a;
|
||||||
|
--mc-table-stripe: #282828;
|
||||||
|
--mc-table-stripe-row: #424242;
|
||||||
|
--mc-table-hover: #343434;
|
||||||
|
--mc-table-border: #5c5c5c;
|
||||||
|
--mc-progress-bg: #555;
|
||||||
|
--mc-placeholder: #d4d4d4;
|
||||||
|
--mc-btn-outline-color: #e0e0e0;
|
||||||
|
--mc-btn-outline-border: #7a7a7a;
|
||||||
|
--mc-btn-outline-hover-bg: #5c5c5c;
|
||||||
|
--mc-btn-outline-active-bg: #7a7a7a;
|
||||||
|
--mc-btn-light-active-color: #f2f2f7;
|
||||||
|
--mc-btn-light-active-bg: #242424;
|
||||||
|
--mc-btn-light-active-border: #1c1c1e;
|
||||||
|
--mc-btn-dark-bg: #242424;
|
||||||
|
--mc-input-disabled-bg: #6e6e6e;
|
||||||
|
--mc-input-disabled-color: #a8a8a8;
|
||||||
|
--mc-list-header-bg: #444;
|
||||||
|
--mc-list-details-bg: #555;
|
||||||
|
--mc-chip-bg: #444;
|
||||||
|
--mc-table-child-bg: #414141;
|
||||||
|
--mc-dtr-bg: #7a7a7a;
|
||||||
|
--mc-dtr-border: #5c5c5c;
|
||||||
|
--mc-dtr-color: #e0e0e0;
|
||||||
|
--mc-dtr-hover-bg: #7a7a7a;
|
||||||
|
--mc-dtr-active-bg: #949494;
|
||||||
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
background-color: #1c1c1e;
|
background-color: var(--mc-body-bg);
|
||||||
color: #f2f2f7;
|
color: var(--mc-body-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.card {
|
.card {
|
||||||
border: 1px solid #2c2c2e;
|
border: 1px solid var(--mc-surface);
|
||||||
background-color: #2c2c2e;
|
background-color: var(--mc-surface);
|
||||||
}
|
}
|
||||||
|
|
||||||
legend {
|
legend {
|
||||||
color: #f2f2f7;
|
color: var(--mc-body-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-header {
|
.card-header {
|
||||||
color: #8e8e93;
|
color: var(--mc-muted);
|
||||||
background-color: #1c1c1e;
|
background-color: var(--mc-body-bg);
|
||||||
border-color: transparent;
|
border-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-body {
|
.card-body {
|
||||||
--bs-card-color: #bbb;
|
--bs-card-color: var(--mc-text-soft);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-secondary, .paginate_button, .page-link, .btn-light {
|
.btn-secondary, .paginate_button, .page-link, .btn-light {
|
||||||
color: #f2f2f7 !important;
|
color: var(--mc-body-color) !important;
|
||||||
background-color: #5e5e5e !important;
|
background-color: var(--mc-btn-bg) !important;
|
||||||
border-color: #4c4c4e !important;
|
border-color: var(--mc-btn-border) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-dark {
|
.btn-dark {
|
||||||
color: #f2f2f7 !important;
|
color: var(--mc-body-color) !important;
|
||||||
background-color: #242424 !important;
|
background-color: var(--mc-btn-dark-bg) !important;
|
||||||
border-color: #1c1c1e !important;
|
border-color: var(--mc-body-bg) !important;
|
||||||
}
|
|
||||||
|
|
||||||
.btn-secondary:focus, .btn-secondary:hover, .btn-group.open .dropdown-toggle.btn-secondary {
|
|
||||||
background-color: #444444;
|
|
||||||
border-color: #4c4c4e !important;
|
|
||||||
color: #f2f2f7;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-check:checked+.btn-secondary, .btn-check:active+.btn-secondary, .btn-secondary:active, .btn-secondary.active, .show>.btn-secondary.dropdown-toggle {
|
.btn-check:checked+.btn-secondary, .btn-check:active+.btn-secondary, .btn-secondary:active, .btn-secondary.active, .show>.btn-secondary.dropdown-toggle {
|
||||||
border-color: #5e5e5e !important;
|
border-color: var(--mc-btn-bg) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.alert-secondary {
|
.alert-secondary {
|
||||||
color: #f2f2f7 !important;
|
color: var(--mc-body-color) !important;
|
||||||
background-color: #5e5e5e !important;
|
background-color: var(--mc-btn-bg) !important;
|
||||||
border-color: #4c4c4e !important;
|
border-color: var(--mc-btn-border) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bg-secondary {
|
.bg-secondary {
|
||||||
color: #f2f2f7 !important;
|
color: var(--mc-body-color) !important;
|
||||||
background-color: #5e5e5e !important;
|
background-color: var(--mc-btn-bg) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.alert-secondary, .alert-secondary a, .alert-secondary .alert-link {
|
.alert-secondary, .alert-secondary a, .alert-secondary .alert-link {
|
||||||
color: #f2f2f7;
|
color: var(--mc-body-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-item.active .page-link {
|
.page-item.active .page-link {
|
||||||
@@ -65,18 +106,18 @@ legend {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.btn-secondary:focus, .btn-secondary:hover, .btn-group.open .dropdown-toggle.btn-secondary {
|
.btn-secondary:focus, .btn-secondary:hover, .btn-group.open .dropdown-toggle.btn-secondary {
|
||||||
background-color: #5e5e5e;
|
background-color: var(--mc-btn-bg);
|
||||||
border-color: #4c4c4e !important;
|
border-color: var(--mc-btn-border) !important;
|
||||||
color: #f2f2f7;
|
color: var(--mc-body-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-secondary:disabled, .btn-secondary.disabled {
|
.btn-secondary:disabled, .btn-secondary.disabled {
|
||||||
border-color: #5e5e5e !important;
|
border-color: var(--mc-btn-bg) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-content {
|
.modal-content {
|
||||||
--bs-modal-color: #bbb;
|
--bs-modal-color: var(--mc-text-soft);
|
||||||
background-color: #2c2c2e;
|
background-color: var(--mc-surface);
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-header {
|
.modal-header {
|
||||||
@@ -84,7 +125,7 @@ legend {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.modal-title {
|
.modal-title {
|
||||||
color: #bbb;
|
color: var(--mc-text-soft);
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal .btn-close {
|
.modal .btn-close {
|
||||||
@@ -92,12 +133,12 @@ legend {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.navbar.bg-light {
|
.navbar.bg-light {
|
||||||
background-color: #1c1c1e !important;
|
background-color: var(--mc-body-bg) !important;
|
||||||
border-color: #2c2c2e;
|
border-color: var(--mc-surface);
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-link {
|
.nav-link {
|
||||||
color: #8e8e93 !important;
|
color: var(--mc-muted) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-tabs .nav-link.active, .nav-tabs .nav-item.show .nav-link {
|
.nav-tabs .nav-link.active, .nav-tabs .nav-item.show .nav-link {
|
||||||
@@ -105,23 +146,23 @@ legend {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.nav-tabs, .nav-tabs .nav-link {
|
.nav-tabs, .nav-tabs .nav-link {
|
||||||
border-color: #444444 !important;
|
border-color: var(--mc-tab-border) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-tabs .nav-link:not(.disabled):hover, .nav-tabs .nav-link:not(.disabled):focus, .nav-tabs .nav-link.active {
|
.nav-tabs .nav-link:not(.disabled):hover, .nav-tabs .nav-link:not(.disabled):focus, .nav-tabs .nav-link.active {
|
||||||
border-bottom-color: #1c1c1e !important;
|
border-bottom-color: var(--mc-body-bg) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card .nav-tabs .nav-link:not(.disabled):hover, .card .nav-tabs .nav-link:not(.disabled):focus, .card .nav-tabs .nav-link.active {
|
.card .nav-tabs .nav-link:not(.disabled):hover, .card .nav-tabs .nav-link:not(.disabled):focus, .card .nav-tabs .nav-link.active {
|
||||||
border-bottom-color: #2c2c2e !important;
|
border-bottom-color: var(--mc-surface) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table, .table-striped>tbody>tr:nth-of-type(odd)>*, tbody tr {
|
.table, .table-striped>tbody>tr:nth-of-type(odd)>*, tbody tr {
|
||||||
color: #f2f2f7 !important;
|
color: var(--mc-body-color) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dropdown-menu {
|
.dropdown-menu {
|
||||||
background-color: #424242;
|
background-color: var(--mc-dropdown-bg);
|
||||||
border: 1px solid #282828;
|
border: 1px solid #282828;
|
||||||
}
|
}
|
||||||
.dropdown-menu>li>a:focus, .dropdown-menu>li>a:hover {
|
.dropdown-menu>li>a:focus, .dropdown-menu>li>a:hover {
|
||||||
@@ -132,79 +173,79 @@ legend {
|
|||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
.bootstrap-select>.dropdown-toggle.bs-placeholder, .bootstrap-select>.dropdown-toggle.bs-placeholder.btn-secondary {
|
.bootstrap-select>.dropdown-toggle.bs-placeholder, .bootstrap-select>.dropdown-toggle.bs-placeholder.btn-secondary {
|
||||||
color: #d4d4d4 !important;
|
color: var(--mc-placeholder) !important;
|
||||||
}
|
}
|
||||||
tbody tr {
|
tbody tr {
|
||||||
color: #ccc;
|
color: var(--mc-text);
|
||||||
}
|
}
|
||||||
.navbar-default .navbar-nav>.open>a, .navbar-default .navbar-nav>.open>a:focus, .navbar-default .navbar-nav>.open>a:hover {
|
.navbar-default .navbar-nav>.open>a, .navbar-default .navbar-nav>.open>a:focus, .navbar-default .navbar-nav>.open>a:hover {
|
||||||
color: #ccc;
|
color: var(--mc-text);
|
||||||
}
|
}
|
||||||
.navbar-default .navbar-nav>.active>a, .navbar-default .navbar-nav>.active>a:focus, .navbar-default .navbar-nav>.active>a:hover {
|
.navbar-default .navbar-nav>.active>a, .navbar-default .navbar-nav>.active>a:focus, .navbar-default .navbar-nav>.active>a:hover {
|
||||||
color: #ccc;
|
color: var(--mc-text);
|
||||||
}
|
}
|
||||||
.list-group-item {
|
.list-group-item {
|
||||||
background-color: #282828;
|
background-color: var(--mc-surface-alt);
|
||||||
border: 1px solid #555;
|
border: 1px solid var(--mc-border);
|
||||||
}
|
}
|
||||||
.table-striped>tbody>tr:nth-of-type(odd) {
|
.table-striped>tbody>tr:nth-of-type(odd) {
|
||||||
background-color: #424242;
|
background-color: var(--mc-table-stripe-row);
|
||||||
}
|
}
|
||||||
table.dataTable>tbody>tr.child ul.dtr-details>li {
|
table.dataTable>tbody>tr.child ul.dtr-details>li {
|
||||||
border-bottom: 1px solid rgba(255, 255, 255, 0.13);
|
border-bottom: 1px solid rgba(255, 255, 255, 0.13);
|
||||||
}
|
}
|
||||||
.label.label-last-login {
|
.label.label-last-login {
|
||||||
color: #ccc !important;
|
color: var(--mc-text) !important;
|
||||||
background-color: #555 !important;
|
background-color: #555 !important;
|
||||||
}
|
}
|
||||||
.progress {
|
.progress {
|
||||||
background-color: #555;
|
background-color: var(--mc-progress-bg);
|
||||||
}
|
}
|
||||||
.h1, .h2, .h3, .h4, .h5, .h6, h1, h2, h3, h4, h5, h6 {
|
.h1, .h2, .h3, .h4, .h5, .h6, h1, h2, h3, h4, h5, h6 {
|
||||||
color: #ccc;
|
color: var(--mc-text);
|
||||||
}
|
}
|
||||||
div.numberedtextarea-number {
|
div.numberedtextarea-number {
|
||||||
color: #999;
|
color: #999;
|
||||||
}
|
}
|
||||||
.well {
|
.well {
|
||||||
border: 1px solid #555;
|
border: 1px solid var(--mc-border);
|
||||||
background-color: #282828;
|
background-color: var(--mc-surface-alt);
|
||||||
}
|
}
|
||||||
pre {
|
pre {
|
||||||
color: #ccc;
|
color: var(--mc-text);
|
||||||
background-color: #282828;
|
background-color: var(--mc-surface-alt);
|
||||||
border: 1px solid #555;
|
border: 1px solid var(--mc-border);
|
||||||
}
|
}
|
||||||
.form-control {
|
.form-control {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
input.form-control, textarea.form-control {
|
input.form-control, textarea.form-control {
|
||||||
color: #e2e2e2 !important;
|
color: var(--mc-input-color) !important;
|
||||||
background-color: #424242 !important;
|
background-color: var(--mc-input-bg) !important;
|
||||||
border: 1px solid #999;
|
border: 1px solid var(--mc-input-border);
|
||||||
}
|
}
|
||||||
input.form-control:focus, textarea.form-control {
|
input.form-control:focus, textarea.form-control {
|
||||||
background-color: #424242 !important;
|
background-color: var(--mc-input-bg) !important;
|
||||||
}
|
}
|
||||||
input.form-control:disabled, textarea.form-disabled {
|
input.form-control:disabled, textarea.form-disabled {
|
||||||
color: #a8a8a8 !important;
|
color: var(--mc-input-disabled-color) !important;
|
||||||
background-color: #6e6e6e !important;
|
background-color: var(--mc-input-disabled-bg) !important;
|
||||||
}
|
}
|
||||||
.input-group-addon {
|
.input-group-addon {
|
||||||
color: #ccc;
|
color: var(--mc-text);
|
||||||
background-color: #424242 !important;
|
background-color: var(--mc-input-bg) !important;
|
||||||
border: 1px solid #999;
|
border: 1px solid var(--mc-input-border);
|
||||||
}
|
}
|
||||||
.input-group-text {
|
.input-group-text {
|
||||||
color: #ccc;
|
color: var(--mc-text);
|
||||||
background-color: #1c1c1c;
|
background-color: #1c1c1c;
|
||||||
}
|
}
|
||||||
|
|
||||||
.list-group-item {
|
.list-group-item {
|
||||||
color: #ccc;
|
color: var(--mc-text);
|
||||||
}
|
}
|
||||||
.dropdown-item {
|
.dropdown-item {
|
||||||
color: #ccc;
|
color: var(--mc-text);
|
||||||
}
|
}
|
||||||
.dropdown-item:hover {
|
.dropdown-item:hover {
|
||||||
color: #616161 !important;
|
color: #616161 !important;
|
||||||
@@ -214,9 +255,9 @@ input.form-control:disabled, textarea.form-disabled {
|
|||||||
background-color: #007aff;
|
background-color: #007aff;
|
||||||
}
|
}
|
||||||
.form-select {
|
.form-select {
|
||||||
color: #e2e2e2!important;
|
color: var(--mc-input-color)!important;
|
||||||
background-color: #424242!important;
|
background-color: var(--mc-input-bg)!important;
|
||||||
border: 1px solid #999;
|
border: 1px solid var(--mc-input-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
.responsive-tabs .card-header button[data-bs-toggle="collapse"] {
|
.responsive-tabs .card-header button[data-bs-toggle="collapse"] {
|
||||||
@@ -240,15 +281,15 @@ input.form-control:disabled, textarea.form-disabled {
|
|||||||
|
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: #6fc7e9;
|
color: var(--mc-link);
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
a:hover {
|
a:hover {
|
||||||
color: #3daedb;
|
color: var(--mc-link-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
.breadcrumb-item.active {
|
.breadcrumb-item.active {
|
||||||
color: #ccc;
|
color: var(--mc-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -273,11 +314,11 @@ a:hover {
|
|||||||
background-color: #3a3a3a;
|
background-color: #3a3a3a;
|
||||||
}
|
}
|
||||||
.accordion-button {
|
.accordion-button {
|
||||||
color: #bbb;
|
color: var(--mc-text-soft);
|
||||||
background-color: #2c2c2c;
|
background-color: #2c2c2c;
|
||||||
}
|
}
|
||||||
.accordion-button:not(.collapsed) {
|
.accordion-button:not(.collapsed) {
|
||||||
color: #6fc7e9;
|
color: var(--mc-link);
|
||||||
background-color: #2c2c2c;
|
background-color: #2c2c2c;
|
||||||
}
|
}
|
||||||
.accordion-button::after {
|
.accordion-button::after {
|
||||||
@@ -300,15 +341,15 @@ a:hover {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.tag-box {
|
.tag-box {
|
||||||
background-color: #282828;
|
background-color: var(--mc-surface-alt);
|
||||||
border: 1px solid #555;
|
border: 1px solid var(--mc-border);
|
||||||
}
|
}
|
||||||
.tag-input {
|
.tag-input {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
background-color: #282828;
|
background-color: var(--mc-surface-alt);
|
||||||
}
|
}
|
||||||
.tag-add {
|
.tag-add {
|
||||||
color: #ccc;
|
color: var(--mc-text);
|
||||||
}
|
}
|
||||||
.tag-add:hover {
|
.tag-add:hover {
|
||||||
color: #d1d1d1;
|
color: #d1d1d1;
|
||||||
@@ -318,19 +359,19 @@ a:hover {
|
|||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
.btn-outline-secondary:hover {
|
.btn-outline-secondary:hover {
|
||||||
background-color: #5c5c5c;
|
background-color: var(--mc-btn-outline-hover-bg);
|
||||||
}
|
}
|
||||||
.btn.btn-outline-secondary {
|
.btn.btn-outline-secondary {
|
||||||
color: #e0e0e0 !important;
|
color: var(--mc-btn-outline-color) !important;
|
||||||
border-color: #7a7a7a !important;
|
border-color: var(--mc-btn-outline-border) !important;
|
||||||
}
|
}
|
||||||
.btn-check:checked+.btn-outline-secondary, .btn-check:active+.btn-outline-secondary, .btn-outline-secondary:active, .btn-outline-secondary.active, .btn-outline-secondary.dropdown-toggle.show {
|
.btn-check:checked+.btn-outline-secondary, .btn-check:active+.btn-outline-secondary, .btn-outline-secondary:active, .btn-outline-secondary.active, .btn-outline-secondary.dropdown-toggle.show {
|
||||||
background-color: #7a7a7a !important;
|
background-color: var(--mc-btn-outline-active-bg) !important;
|
||||||
}
|
}
|
||||||
.btn-check:checked+.btn-light, .btn-check:active+.btn-light, .btn-light:active, .btn-light.active, .show>.btn-light.dropdown-toggle {
|
.btn-check:checked+.btn-light, .btn-check:active+.btn-light, .btn-light:active, .btn-light.active, .show>.btn-light.dropdown-toggle {
|
||||||
color: #f2f2f7 !important;
|
color: var(--mc-btn-light-active-color) !important;
|
||||||
background-color: #242424 !important;
|
background-color: var(--mc-btn-light-active-bg) !important;
|
||||||
border-color: #1c1c1e !important;
|
border-color: var(--mc-btn-light-active-border) !important;
|
||||||
}
|
}
|
||||||
.btn-input-missing,
|
.btn-input-missing,
|
||||||
.btn-input-missing:hover,
|
.btn-input-missing:hover,
|
||||||
@@ -348,16 +389,16 @@ a:hover {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.list-group-details {
|
.list-group-details {
|
||||||
background: #555;
|
background: var(--mc-list-details-bg);
|
||||||
}
|
}
|
||||||
.list-group-header {
|
.list-group-header {
|
||||||
background: #444;
|
background: var(--mc-list-header-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
span.mail-address-item {
|
span.mail-address-item {
|
||||||
background-color: #444;
|
background-color: var(--mc-chip-bg);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
border: 1px solid #555;
|
border: 1px solid var(--mc-border);
|
||||||
padding: 2px 7px;
|
padding: 2px 7px;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin: 2px 6px 2px 0;
|
margin: 2px 6px 2px 0;
|
||||||
@@ -365,52 +406,52 @@ span.mail-address-item {
|
|||||||
|
|
||||||
table.dataTable.dtr-inline.collapsed>tbody>tr>td.dtr-control:before:hover,
|
table.dataTable.dtr-inline.collapsed>tbody>tr>td.dtr-control:before:hover,
|
||||||
table.dataTable.dtr-inline.collapsed>tbody>tr>th.dtr-control:before:hover {
|
table.dataTable.dtr-inline.collapsed>tbody>tr>th.dtr-control:before:hover {
|
||||||
background-color: #7a7a7a !important;
|
background-color: var(--mc-dtr-hover-bg) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
table.dataTable.dtr-inline.collapsed>tbody>tr>td.dtr-control:before,
|
table.dataTable.dtr-inline.collapsed>tbody>tr>td.dtr-control:before,
|
||||||
table.dataTable.dtr-inline.collapsed>tbody>tr>th.dtr-control:before {
|
table.dataTable.dtr-inline.collapsed>tbody>tr>th.dtr-control:before {
|
||||||
background-color: #7a7a7a !important;
|
background-color: var(--mc-dtr-bg) !important;
|
||||||
border: 1.5px solid #5c5c5c !important;
|
border: 1.5px solid var(--mc-dtr-border) !important;
|
||||||
color: #e0e0e0 !important;
|
color: var(--mc-dtr-color) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
table.dataTable.dtr-inline.collapsed>tbody>tr.parent>td.dtr-control:before,
|
table.dataTable.dtr-inline.collapsed>tbody>tr.parent>td.dtr-control:before,
|
||||||
table.dataTable.dtr-inline.collapsed>tbody>tr.parent>th.dtr-control:before {
|
table.dataTable.dtr-inline.collapsed>tbody>tr.parent>th.dtr-control:before {
|
||||||
background-color: #949494;
|
background-color: var(--mc-dtr-active-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
table.dataTable.dtr-inline.collapsed>tbody>tr>td.child,
|
table.dataTable.dtr-inline.collapsed>tbody>tr>td.child,
|
||||||
table.dataTable.dtr-inline.collapsed>tbody>tr>th.child,
|
table.dataTable.dtr-inline.collapsed>tbody>tr>th.child,
|
||||||
table.dataTable.dtr-inline.collapsed>tbody>tr>td.dataTables_empty {
|
table.dataTable.dtr-inline.collapsed>tbody>tr>td.dataTables_empty {
|
||||||
background-color: #414141;
|
background-color: var(--mc-table-child-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
table.table, .table-striped>tbody>tr:nth-of-type(odd)>*, tbody tr {
|
table.table, .table-striped>tbody>tr:nth-of-type(odd)>*, tbody tr {
|
||||||
color: #ccc !important;
|
color: var(--mc-text) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-secondary {
|
.table-secondary {
|
||||||
--bs-table-bg: #282828;
|
--bs-table-bg: #282828;
|
||||||
--bs-table-striped-bg: #343434;
|
--bs-table-striped-bg: #343434;
|
||||||
--bs-table-striped-color: #f2f2f7;
|
--bs-table-striped-color: var(--mc-body-color);
|
||||||
--bs-table-active-bg: #4c4c4c;
|
--bs-table-active-bg: #4c4c4c;
|
||||||
--bs-table-active-color: #f2f2f7;
|
--bs-table-active-color: var(--mc-body-color);
|
||||||
--bs-table-hover-bg: #3a3a3a;
|
--bs-table-hover-bg: #3a3a3a;
|
||||||
--bs-table-hover-color: #f2f2f7;
|
--bs-table-hover-color: var(--mc-body-color);
|
||||||
color: #ccc;
|
color: var(--mc-text);
|
||||||
border-color: #3a3a3a;
|
border-color: #3a3a3a;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-light {
|
.table-light {
|
||||||
--bs-table-bg: #3a3a3a;
|
--bs-table-bg: #3a3a3a;
|
||||||
--bs-table-striped-bg: #444444;
|
--bs-table-striped-bg: #444444;
|
||||||
--bs-table-striped-color: #f2f2f7;
|
--bs-table-striped-color: var(--mc-body-color);
|
||||||
--bs-table-active-bg: #5c5c5c;
|
--bs-table-active-bg: #5c5c5c;
|
||||||
--bs-table-active-color: #f2f2f7;
|
--bs-table-active-color: var(--mc-body-color);
|
||||||
--bs-table-hover-bg: #4c4c4c;
|
--bs-table-hover-bg: #4c4c4c;
|
||||||
--bs-table-hover-color: #f2f2f7;
|
--bs-table-hover-color: var(--mc-body-color);
|
||||||
color: #ccc;
|
color: var(--mc-text);
|
||||||
border-color: #4c4c4c;
|
border-color: #4c4c4c;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -430,23 +471,23 @@ table.table, .table-striped>tbody>tr:nth-of-type(odd)>*, tbody tr {
|
|||||||
|
|
||||||
.table-striped>tbody>tr:nth-of-type(odd)>td,
|
.table-striped>tbody>tr:nth-of-type(odd)>td,
|
||||||
.table-striped>tbody>tr:nth-of-type(odd)>th {
|
.table-striped>tbody>tr:nth-of-type(odd)>th {
|
||||||
background-color: #282828;
|
background-color: var(--mc-table-stripe);
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-hover>tbody>tr:hover {
|
.table-hover>tbody>tr:hover {
|
||||||
background-color: #343434;
|
background-color: var(--mc-table-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
.table>:not(caption)>*>* {
|
.table>:not(caption)>*>* {
|
||||||
border-color: #5c5c5c;
|
border-color: var(--mc-table-border);
|
||||||
--bs-table-color-state:#bbb;
|
--bs-table-color-state:var(--mc-text-soft);
|
||||||
--bs-table-bg: #3a3a3a;
|
--bs-table-bg: var(--mc-table-bg);
|
||||||
}
|
}
|
||||||
.text-muted {
|
.text-muted {
|
||||||
--bs-secondary-color: #8e8e93;
|
--bs-secondary-color: var(--mc-muted);
|
||||||
}
|
}
|
||||||
input::placeholder {
|
input::placeholder {
|
||||||
color: #8e8e93 !important;
|
color: var(--mc-muted) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-select {
|
.form-select {
|
||||||
|
|||||||
@@ -0,0 +1,212 @@
|
|||||||
|
/* servercow theme, dark mode, bundled after mailcow core dark mode */
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--sc-brown-hover: #806b55;
|
||||||
|
--sc-brown-active: #8a745c;
|
||||||
|
--sc-brown-soft: rgba(201, 180, 154, .18);
|
||||||
|
--sc-brown-tint: rgba(201, 180, 154, .08);
|
||||||
|
--sc-link: #c9b49a;
|
||||||
|
--sc-danger-text: #ff8a8a;
|
||||||
|
--sc-ink: #ece7e1;
|
||||||
|
--sc-muted: #a89f94;
|
||||||
|
--sc-placeholder: #8a8177;
|
||||||
|
--sc-bg: #171513;
|
||||||
|
--sc-surface: #211e1b;
|
||||||
|
--sc-cap: #2a2622;
|
||||||
|
--sc-addon: #2a2622;
|
||||||
|
--sc-line: #463f38;
|
||||||
|
--sc-divider: #34302b;
|
||||||
|
--sc-cap-line: #3d3732;
|
||||||
|
--sc-stripe: rgba(255, 255, 255, .025);
|
||||||
|
--sc-hover: rgba(201, 180, 154, .08);
|
||||||
|
--sc-focus-border: #8a7864;
|
||||||
|
--sc-focus: rgba(201, 180, 154, .25);
|
||||||
|
--sc-shadow: 0 0 10px 0 rgba(0, 0, 0, .5);
|
||||||
|
--bs-body-color: #ece7e1;
|
||||||
|
--bs-body-bg: #171513;
|
||||||
|
/* core dark tokens, see css/themes/mailcow-darkmode.css */
|
||||||
|
--mc-body-bg: var(--sc-bg);
|
||||||
|
--mc-body-color: var(--sc-ink);
|
||||||
|
--mc-surface: var(--sc-surface);
|
||||||
|
--mc-surface-alt: var(--sc-cap);
|
||||||
|
--mc-text: var(--sc-ink);
|
||||||
|
--mc-text-soft: var(--sc-ink);
|
||||||
|
--mc-muted: var(--sc-muted);
|
||||||
|
--mc-border: var(--sc-divider);
|
||||||
|
--mc-tab-border: var(--sc-divider);
|
||||||
|
--mc-link: var(--sc-link);
|
||||||
|
--mc-link-hover: #e0cfba;
|
||||||
|
--mc-btn-bg: var(--sc-cap);
|
||||||
|
--mc-btn-border: var(--sc-line);
|
||||||
|
--mc-input-bg: var(--sc-bg);
|
||||||
|
--mc-input-color: var(--sc-ink);
|
||||||
|
--mc-input-border: var(--sc-line);
|
||||||
|
--mc-dropdown-bg: var(--sc-surface);
|
||||||
|
--mc-table-bg: transparent;
|
||||||
|
--mc-table-stripe: var(--sc-stripe);
|
||||||
|
--mc-table-stripe-row: transparent;
|
||||||
|
--mc-table-hover: var(--sc-hover);
|
||||||
|
--mc-table-border: var(--sc-divider);
|
||||||
|
--mc-progress-bg: var(--sc-cap);
|
||||||
|
--mc-placeholder: var(--sc-placeholder);
|
||||||
|
--mc-hr: var(--sc-divider);
|
||||||
|
--mc-btn-outline-color: var(--sc-ink);
|
||||||
|
--mc-btn-outline-border: var(--sc-line);
|
||||||
|
--mc-btn-outline-hover-bg: var(--sc-brown-tint);
|
||||||
|
--mc-btn-outline-active-bg: var(--sc-brown);
|
||||||
|
--mc-btn-light-active-color: #fff;
|
||||||
|
--mc-btn-light-active-bg: var(--sc-brown);
|
||||||
|
--mc-btn-light-active-border: var(--sc-brown);
|
||||||
|
--mc-btn-dark-bg: var(--sc-brown);
|
||||||
|
--mc-input-disabled-bg: var(--sc-cap);
|
||||||
|
--mc-input-disabled-color: var(--sc-muted);
|
||||||
|
--mc-list-header-bg: var(--sc-cap);
|
||||||
|
--mc-list-details-bg: var(--sc-surface);
|
||||||
|
--mc-chip-bg: var(--sc-cap);
|
||||||
|
--mc-table-child-bg: var(--sc-surface);
|
||||||
|
--mc-dtr-bg: var(--sc-cap);
|
||||||
|
--mc-dtr-border: var(--sc-line);
|
||||||
|
--mc-dtr-color: var(--sc-link);
|
||||||
|
--mc-dtr-hover-bg: var(--sc-hover);
|
||||||
|
--mc-dtr-active-bg: var(--sc-brown-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* navbar */
|
||||||
|
.navbar.bg-light {
|
||||||
|
background-color: var(--sc-surface) !important;
|
||||||
|
border-bottom-color: var(--sc-green);
|
||||||
|
}
|
||||||
|
.navbar-light .navbar-nav .nav-link {
|
||||||
|
color: var(--sc-ink) !important;
|
||||||
|
}
|
||||||
|
.navbar-light .navbar-nav .nav-link:hover,
|
||||||
|
.navbar-light .navbar-nav .nav-link:focus,
|
||||||
|
.navbar-light .navbar-nav .nav-link.active,
|
||||||
|
.navbar-light .navbar-nav .show > .nav-link {
|
||||||
|
color: var(--sc-link) !important;
|
||||||
|
}
|
||||||
|
.dropdown-menu {
|
||||||
|
border-color: var(--sc-cap-line);
|
||||||
|
}
|
||||||
|
.dropdown-item:hover, .dropdown-item:focus {
|
||||||
|
color: var(--sc-link) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* cards and modals */
|
||||||
|
.card-header {
|
||||||
|
color: var(--sc-ink);
|
||||||
|
background-color: var(--sc-cap);
|
||||||
|
border-color: var(--sc-cap-line);
|
||||||
|
}
|
||||||
|
.modal-content {
|
||||||
|
background-color: var(--sc-surface);
|
||||||
|
}
|
||||||
|
.modal-header {
|
||||||
|
border-color: var(--sc-cap-line);
|
||||||
|
}
|
||||||
|
.input-group-text {
|
||||||
|
background-color: var(--sc-addon);
|
||||||
|
}
|
||||||
|
.form-control::file-selector-button {
|
||||||
|
color: var(--sc-ink);
|
||||||
|
background-color: var(--sc-cap);
|
||||||
|
border-color: var(--sc-line);
|
||||||
|
}
|
||||||
|
.form-control:hover:not(:disabled):not([readonly])::file-selector-button {
|
||||||
|
color: var(--sc-link);
|
||||||
|
background-color: var(--sc-line);
|
||||||
|
}
|
||||||
|
.form-check-input:not(:checked) {
|
||||||
|
background-color: var(--sc-bg);
|
||||||
|
border-color: var(--sc-line);
|
||||||
|
}
|
||||||
|
kbd {
|
||||||
|
color: var(--sc-ink);
|
||||||
|
background-color: var(--sc-cap);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* buttons */
|
||||||
|
.btn-secondary:hover, .btn-secondary:focus, .btn-light:hover, .btn-light:focus, .page-link:hover {
|
||||||
|
color: var(--sc-link) !important;
|
||||||
|
background-color: var(--sc-line) !important;
|
||||||
|
}
|
||||||
|
.btn-check:checked + .btn-secondary, .btn-secondary.active, .show > .btn-secondary.dropdown-toggle,
|
||||||
|
.page-item.active .page-link {
|
||||||
|
color: #fff !important;
|
||||||
|
background-color: var(--sc-brown) !important;
|
||||||
|
border-color: var(--sc-brown) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* tabs */
|
||||||
|
.nav-tabs .nav-link {
|
||||||
|
color: var(--sc-ink) !important;
|
||||||
|
border-bottom-color: transparent !important;
|
||||||
|
}
|
||||||
|
.nav-tabs .nav-link:not(.disabled):hover, .nav-tabs .nav-link:not(.disabled):focus,
|
||||||
|
.card .nav-tabs .nav-link:not(.disabled):hover, .card .nav-tabs .nav-link:not(.disabled):focus {
|
||||||
|
color: var(--sc-link) !important;
|
||||||
|
border-bottom-color: transparent !important;
|
||||||
|
}
|
||||||
|
.nav-tabs .nav-link.active, .card .nav-tabs .nav-link.active {
|
||||||
|
color: var(--sc-link) !important;
|
||||||
|
border-bottom-color: var(--sc-link) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* semantic text */
|
||||||
|
.text-success {
|
||||||
|
color: var(--sc-green) !important;
|
||||||
|
}
|
||||||
|
.text-warning {
|
||||||
|
color: var(--sc-warning) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* alerts, opaque dark tints (also used for floating notifications) */
|
||||||
|
.alert-primary {
|
||||||
|
color: #e6d9c9 !important;
|
||||||
|
background-color: #3c342b !important;
|
||||||
|
border-color: #5e4f3e !important;
|
||||||
|
}
|
||||||
|
.alert-info {
|
||||||
|
color: #cfe9f6 !important;
|
||||||
|
background-color: #2f3334 !important;
|
||||||
|
border-color: #44535a !important;
|
||||||
|
}
|
||||||
|
.alert-success {
|
||||||
|
color: #cdeea0;
|
||||||
|
background-color: #2e3419;
|
||||||
|
border-color: #405515;
|
||||||
|
}
|
||||||
|
.alert-danger {
|
||||||
|
color: #f9c4c4;
|
||||||
|
background-color: #402a28;
|
||||||
|
border-color: #693a38;
|
||||||
|
}
|
||||||
|
.alert-warning {
|
||||||
|
color: #f5d496;
|
||||||
|
background-color: #3c2f19;
|
||||||
|
border-color: #654815;
|
||||||
|
}
|
||||||
|
.alert-secondary, .alert-light {
|
||||||
|
color: var(--sc-ink) !important;
|
||||||
|
background-color: var(--sc-cap) !important;
|
||||||
|
border-color: var(--sc-divider) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert .btn-close {
|
||||||
|
filter: invert(1) grayscale(100%) brightness(200%);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* slider */
|
||||||
|
.noUi-target {
|
||||||
|
background: var(--sc-bg);
|
||||||
|
border-color: var(--sc-line);
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
.noUi-handle {
|
||||||
|
background: var(--sc-cap);
|
||||||
|
border-color: var(--sc-line);
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
.noUi-handle::before, .noUi-handle::after {
|
||||||
|
background: var(--sc-line);
|
||||||
|
}
|
||||||
@@ -0,0 +1,202 @@
|
|||||||
|
|
||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright [yyyy] [name of copyright owner]
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
Copyright 2020 The Poppins Project Authors (https://github.com/itfoundry/Poppins)
|
||||||
|
|
||||||
|
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||||
|
This license is copied below, and is also available with a FAQ at:
|
||||||
|
http://scripts.sil.org/OFL
|
||||||
|
|
||||||
|
|
||||||
|
-----------------------------------------------------------
|
||||||
|
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||||
|
-----------------------------------------------------------
|
||||||
|
|
||||||
|
PREAMBLE
|
||||||
|
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||||
|
development of collaborative font projects, to support the font creation
|
||||||
|
efforts of academic and linguistic communities, and to provide a free and
|
||||||
|
open framework in which fonts may be shared and improved in partnership
|
||||||
|
with others.
|
||||||
|
|
||||||
|
The OFL allows the licensed fonts to be used, studied, modified and
|
||||||
|
redistributed freely as long as they are not sold by themselves. The
|
||||||
|
fonts, including any derivative works, can be bundled, embedded,
|
||||||
|
redistributed and/or sold with any software provided that any reserved
|
||||||
|
names are not used by derivative works. The fonts and derivatives,
|
||||||
|
however, cannot be released under any other type of license. The
|
||||||
|
requirement for fonts to remain under this license does not apply
|
||||||
|
to any document created using the fonts or their derivatives.
|
||||||
|
|
||||||
|
DEFINITIONS
|
||||||
|
"Font Software" refers to the set of files released by the Copyright
|
||||||
|
Holder(s) under this license and clearly marked as such. This may
|
||||||
|
include source files, build scripts and documentation.
|
||||||
|
|
||||||
|
"Reserved Font Name" refers to any names specified as such after the
|
||||||
|
copyright statement(s).
|
||||||
|
|
||||||
|
"Original Version" refers to the collection of Font Software components as
|
||||||
|
distributed by the Copyright Holder(s).
|
||||||
|
|
||||||
|
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||||
|
or substituting -- in part or in whole -- any of the components of the
|
||||||
|
Original Version, by changing formats or by porting the Font Software to a
|
||||||
|
new environment.
|
||||||
|
|
||||||
|
"Author" refers to any designer, engineer, programmer, technical
|
||||||
|
writer or other person who contributed to the Font Software.
|
||||||
|
|
||||||
|
PERMISSION & CONDITIONS
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||||
|
redistribute, and sell modified and unmodified copies of the Font
|
||||||
|
Software, subject to the following conditions:
|
||||||
|
|
||||||
|
1) Neither the Font Software nor any of its individual components,
|
||||||
|
in Original or Modified Versions, may be sold by itself.
|
||||||
|
|
||||||
|
2) Original or Modified Versions of the Font Software may be bundled,
|
||||||
|
redistributed and/or sold with any software, provided that each copy
|
||||||
|
contains the above copyright notice and this license. These can be
|
||||||
|
included either as stand-alone text files, human-readable headers or
|
||||||
|
in the appropriate machine-readable metadata fields within text or
|
||||||
|
binary files as long as those fields can be easily viewed by the user.
|
||||||
|
|
||||||
|
3) No Modified Version of the Font Software may use the Reserved Font
|
||||||
|
Name(s) unless explicit written permission is granted by the corresponding
|
||||||
|
Copyright Holder. This restriction only applies to the primary font name as
|
||||||
|
presented to the users.
|
||||||
|
|
||||||
|
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||||
|
Software shall not be used to promote, endorse or advertise any
|
||||||
|
Modified Version, except to acknowledge the contribution(s) of the
|
||||||
|
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||||
|
permission.
|
||||||
|
|
||||||
|
5) The Font Software, modified or unmodified, in part or in whole,
|
||||||
|
must be distributed entirely under this license, and must not be
|
||||||
|
distributed under any other license. The requirement for fonts to
|
||||||
|
remain under this license does not apply to any document created
|
||||||
|
using the Font Software.
|
||||||
|
|
||||||
|
TERMINATION
|
||||||
|
This license becomes null and void if any of the above conditions are
|
||||||
|
not met.
|
||||||
|
|
||||||
|
DISCLAIMER
|
||||||
|
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||||
|
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||||
|
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||||
|
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||||
|
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
@@ -0,0 +1,584 @@
|
|||||||
|
/* servercow theme for mailcow, based on the servercow customer portal */
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Poppins';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: local(''),
|
||||||
|
url('/css/themes/servercow/fonts/poppins-v24-latin_latin-ext-regular.woff2') format('woff2'),
|
||||||
|
url('/css/themes/servercow/fonts/poppins-v24-latin_latin-ext-regular.woff') format('woff');
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Poppins';
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: local(''),
|
||||||
|
url('/css/themes/servercow/fonts/poppins-v24-latin_latin-ext-italic.woff2') format('woff2'),
|
||||||
|
url('/css/themes/servercow/fonts/poppins-v24-latin_latin-ext-italic.woff') format('woff');
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Poppins';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 500;
|
||||||
|
font-display: swap;
|
||||||
|
src: local(''),
|
||||||
|
url('/css/themes/servercow/fonts/poppins-v24-latin_latin-ext-500.woff2') format('woff2'),
|
||||||
|
url('/css/themes/servercow/fonts/poppins-v24-latin_latin-ext-500.woff') format('woff');
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Poppins';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 600;
|
||||||
|
font-display: swap;
|
||||||
|
src: local(''),
|
||||||
|
url('/css/themes/servercow/fonts/poppins-v24-latin_latin-ext-600.woff2') format('woff2'),
|
||||||
|
url('/css/themes/servercow/fonts/poppins-v24-latin_latin-ext-600.woff') format('woff');
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Roboto Slab';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: local(''),
|
||||||
|
url('/css/themes/servercow/fonts/roboto-slab-v36-cyrillic_greek_latin-regular.woff2') format('woff2'),
|
||||||
|
url('/css/themes/servercow/fonts/roboto-slab-v36-cyrillic_greek_latin-regular.woff') format('woff');
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Roboto Slab';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 500;
|
||||||
|
font-display: swap;
|
||||||
|
src: local(''),
|
||||||
|
url('/css/themes/servercow/fonts/roboto-slab-v36-cyrillic_greek_latin-500.woff2') format('woff2'),
|
||||||
|
url('/css/themes/servercow/fonts/roboto-slab-v36-cyrillic_greek_latin-500.woff') format('woff');
|
||||||
|
}
|
||||||
|
|
||||||
|
/* tokens */
|
||||||
|
:root {
|
||||||
|
--sc-brown: #6F5D49;
|
||||||
|
--sc-brown-hover: #5e4f3e;
|
||||||
|
--sc-brown-active: #594a3a;
|
||||||
|
--sc-brown-soft: rgba(111, 93, 73, .2);
|
||||||
|
--sc-brown-tint: rgba(111, 93, 73, .08);
|
||||||
|
--sc-link: #6F5D49;
|
||||||
|
--sc-green: #8AD608;
|
||||||
|
--sc-blue: #96D0EC;
|
||||||
|
--sc-warning: #e3950b;
|
||||||
|
--sc-danger: #ef6f6f;
|
||||||
|
--sc-danger-hover: #e85e5e;
|
||||||
|
--sc-danger-text: #c43c3c;
|
||||||
|
--sc-ink: #212529;
|
||||||
|
--sc-muted: #6c757d;
|
||||||
|
--sc-placeholder: #6c757d;
|
||||||
|
--sc-bg: #F6F8F8;
|
||||||
|
--sc-surface: #ffffff;
|
||||||
|
--sc-cap: #f7f7f7;
|
||||||
|
--sc-addon: #e9ecef;
|
||||||
|
--sc-line: #ced4da;
|
||||||
|
--sc-divider: #dee2e6;
|
||||||
|
--sc-cap-line: rgba(0, 0, 0, .175);
|
||||||
|
--sc-stripe: rgba(111, 93, 73, .04);
|
||||||
|
--sc-hover: rgba(111, 93, 73, .08);
|
||||||
|
--sc-focus-border: #b7aea4;
|
||||||
|
--sc-focus: rgba(111, 93, 73, .25);
|
||||||
|
--sc-shadow: 0 0 10px 0 rgba(134, 134, 134, .3);
|
||||||
|
--sc-font: 'Poppins', system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||||
|
--sc-font-head: 'Roboto Slab', Georgia, serif;
|
||||||
|
--bs-body-color: #212529;
|
||||||
|
--bs-body-bg: #F6F8F8;
|
||||||
|
--bs-body-font-family: var(--sc-font);
|
||||||
|
/* core tokens, see css/build/014-mailcow.css */
|
||||||
|
--mc-font-family: var(--sc-font);
|
||||||
|
--mc-font-size: 10pt;
|
||||||
|
--mc-body-bg: var(--sc-bg);
|
||||||
|
--mc-primary: var(--sc-brown);
|
||||||
|
--mc-primary-border: var(--sc-brown);
|
||||||
|
--mc-info: var(--sc-blue);
|
||||||
|
--mc-info-border: var(--sc-blue);
|
||||||
|
--mc-border: var(--sc-divider);
|
||||||
|
--mc-surface: var(--sc-surface);
|
||||||
|
--mc-surface-alt: var(--sc-cap);
|
||||||
|
--mc-muted: var(--sc-muted);
|
||||||
|
--mc-hr: var(--sc-divider);
|
||||||
|
--mc-progress-bg: var(--sc-addon);
|
||||||
|
--mc-placeholder: var(--sc-placeholder);
|
||||||
|
--mc-btn-outline-color: var(--sc-ink);
|
||||||
|
--mc-btn-outline-border: var(--sc-line);
|
||||||
|
--mc-btn-outline-hover-bg: var(--sc-brown-tint);
|
||||||
|
--mc-btn-outline-active-bg: var(--sc-brown);
|
||||||
|
--mc-btn-light-active-color: #fff;
|
||||||
|
--mc-btn-light-active-bg: var(--sc-brown);
|
||||||
|
--mc-btn-light-active-border: var(--sc-brown);
|
||||||
|
--mc-dtr-bg: var(--sc-surface);
|
||||||
|
--mc-dtr-border: var(--sc-line);
|
||||||
|
--mc-dtr-color: var(--sc-brown);
|
||||||
|
--mc-dtr-hover-bg: var(--sc-brown-tint);
|
||||||
|
--mc-dtr-active-bg: var(--sc-brown-soft);
|
||||||
|
--mc-login-user: var(--sc-green);
|
||||||
|
--mc-login-domainadmin: #f5c400;
|
||||||
|
--mc-login-admin: var(--sc-danger);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* base */
|
||||||
|
body {
|
||||||
|
color: var(--sc-ink);
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
}
|
||||||
|
h1, h2, h3, h4, h5, h6, .h1, .h2, .h3, .h4, .h5, .h6, legend, .modal-title {
|
||||||
|
font-family: var(--sc-font-head);
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--sc-ink);
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
color: var(--sc-link);
|
||||||
|
}
|
||||||
|
a:hover {
|
||||||
|
color: var(--sc-brown-hover);
|
||||||
|
}
|
||||||
|
.text-muted, .text-secondary {
|
||||||
|
color: var(--sc-muted) !important;
|
||||||
|
}
|
||||||
|
::selection {
|
||||||
|
background: var(--sc-brown-soft);
|
||||||
|
}
|
||||||
|
hr {
|
||||||
|
border-color: var(--sc-divider);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
::placeholder {
|
||||||
|
color: var(--sc-placeholder) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* navbar, white with green line */
|
||||||
|
.navbar.bg-light {
|
||||||
|
background-color: var(--sc-surface) !important;
|
||||||
|
border: 0;
|
||||||
|
border-bottom: 5px solid var(--sc-green);
|
||||||
|
}
|
||||||
|
.navbar-light .navbar-nav .nav-link {
|
||||||
|
color: var(--sc-ink);
|
||||||
|
}
|
||||||
|
.navbar-light .navbar-nav .nav-link:hover,
|
||||||
|
.navbar-light .navbar-nav .nav-link:focus,
|
||||||
|
.navbar-light .navbar-nav .nav-link.active,
|
||||||
|
.navbar-light .navbar-nav .show > .nav-link {
|
||||||
|
color: var(--sc-brown);
|
||||||
|
}
|
||||||
|
@media (min-width: 992px) {
|
||||||
|
.navbar-nav .nav-link {
|
||||||
|
height: 60px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.dropdown-menu {
|
||||||
|
border: 1px solid var(--sc-cap-line);
|
||||||
|
border-radius: 0;
|
||||||
|
box-shadow: var(--sc-shadow);
|
||||||
|
}
|
||||||
|
.dropdown-item {
|
||||||
|
color: var(--sc-ink);
|
||||||
|
}
|
||||||
|
.dropdown-item:hover, .dropdown-item:focus {
|
||||||
|
color: var(--sc-brown) !important;
|
||||||
|
background-color: var(--sc-brown-soft);
|
||||||
|
}
|
||||||
|
.dropdown-item.active, .dropdown-item:active {
|
||||||
|
color: #fff;
|
||||||
|
background-color: var(--sc-brown);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* cards, white with soft shadow */
|
||||||
|
.card {
|
||||||
|
border: 0 !important;
|
||||||
|
border-radius: 0;
|
||||||
|
background-color: var(--sc-surface);
|
||||||
|
box-shadow: var(--sc-shadow);
|
||||||
|
}
|
||||||
|
.card-header, .card-header:first-child {
|
||||||
|
font-family: var(--sc-font-head);
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
color: var(--sc-ink);
|
||||||
|
background-color: var(--sc-cap);
|
||||||
|
border-bottom: 1px solid var(--sc-cap-line);
|
||||||
|
border-radius: 0;
|
||||||
|
padding: .5rem 1rem;
|
||||||
|
}
|
||||||
|
.card-header .btn, .card-header .form-control, .card-header .form-select {
|
||||||
|
font-family: var(--sc-font);
|
||||||
|
font-size: .875rem;
|
||||||
|
}
|
||||||
|
.card-footer {
|
||||||
|
background-color: var(--sc-cap);
|
||||||
|
border-top: 1px solid var(--sc-cap-line);
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
.card .card {
|
||||||
|
box-shadow: none;
|
||||||
|
border: 1px solid var(--sc-divider) !important;
|
||||||
|
}
|
||||||
|
/* colored headers become a top line */
|
||||||
|
.card:has(> .card-header.bg-danger) {
|
||||||
|
border-top: 4px solid var(--sc-danger) !important;
|
||||||
|
}
|
||||||
|
.card-header.bg-danger, .card-header.bg-success, .card-header.bg-primary, .card-header.bg-info {
|
||||||
|
color: var(--sc-ink) !important;
|
||||||
|
background-color: var(--sc-cap) !important;
|
||||||
|
}
|
||||||
|
.list-group-item {
|
||||||
|
border-color: var(--sc-divider);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* modals */
|
||||||
|
.modal-content {
|
||||||
|
border: 0;
|
||||||
|
border-radius: 0;
|
||||||
|
box-shadow: 0 8px 40px rgba(0, 0, 0, .2);
|
||||||
|
}
|
||||||
|
.modal-header {
|
||||||
|
background-color: var(--sc-cap);
|
||||||
|
border-bottom: 1px solid var(--sc-cap-line);
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
.modal-footer {
|
||||||
|
border-top: 1px solid var(--sc-divider);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* buttons, flat and square */
|
||||||
|
.btn {
|
||||||
|
border-width: 1px;
|
||||||
|
border-radius: 0;
|
||||||
|
font-weight: 400;
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
.btn:not(.disabled):hover,
|
||||||
|
.btn:not(.disabled):active {
|
||||||
|
margin-top: 0;
|
||||||
|
border-bottom-width: 1px;
|
||||||
|
}
|
||||||
|
.btn:focus, .btn-check:focus + .btn {
|
||||||
|
box-shadow: 0 0 0 .2rem var(--sc-focus);
|
||||||
|
}
|
||||||
|
.btn-primary, .btn-primary:hover, .btn-primary:focus, .btn-primary:active {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.btn-primary:hover, .btn-primary:focus, .btn-primary:active {
|
||||||
|
background-color: var(--sc-brown-hover) !important;
|
||||||
|
border-color: var(--sc-brown-active) !important;
|
||||||
|
}
|
||||||
|
/* main call to action, portal primary */
|
||||||
|
.btn-success, .btn-success:not(.disabled):hover, .btn-success:not(.disabled):active, .btn-success:focus,
|
||||||
|
.btn-success:disabled, .btn-success.disabled {
|
||||||
|
color: #fff;
|
||||||
|
background-color: var(--sc-brown);
|
||||||
|
border-color: var(--sc-brown);
|
||||||
|
}
|
||||||
|
.btn-success:hover, .btn-success:active, .btn-success:focus {
|
||||||
|
background-color: var(--sc-brown-hover);
|
||||||
|
border-color: var(--sc-brown-active);
|
||||||
|
}
|
||||||
|
.btn-danger, .btn-danger:not(.disabled):hover, .btn-danger:not(.disabled):active, .btn-danger:focus,
|
||||||
|
.btn-danger:disabled, .btn-danger.disabled {
|
||||||
|
color: #000;
|
||||||
|
background-color: var(--sc-danger);
|
||||||
|
border-color: var(--sc-danger);
|
||||||
|
}
|
||||||
|
.btn-danger:hover, .btn-danger:active, .btn-danger:focus {
|
||||||
|
background-color: var(--sc-danger-hover);
|
||||||
|
border-color: var(--sc-danger-hover);
|
||||||
|
}
|
||||||
|
.btn-warning, .btn-warning:not(.disabled):hover, .btn-warning:not(.disabled):active, .btn-warning:focus {
|
||||||
|
color: #000;
|
||||||
|
background-color: var(--sc-warning);
|
||||||
|
border-color: var(--sc-warning);
|
||||||
|
}
|
||||||
|
.bg-info, .alert-info, .btn-info, .badge.bg-info > a {
|
||||||
|
color: #000 !important;
|
||||||
|
}
|
||||||
|
.btn-secondary, .btn-secondary:not(.disabled):hover, .btn-secondary:not(.disabled):active, .btn-secondary:focus,
|
||||||
|
.btn-light, .btn-light:not(.disabled):hover, .btn-light:not(.disabled):active, .btn-light:focus {
|
||||||
|
color: var(--sc-ink);
|
||||||
|
background-color: var(--sc-surface);
|
||||||
|
border-color: var(--sc-line);
|
||||||
|
}
|
||||||
|
.btn-secondary:hover, .btn-secondary:active, .btn-secondary:focus,
|
||||||
|
.btn-light:hover, .btn-light:focus {
|
||||||
|
color: var(--sc-brown);
|
||||||
|
background-color: var(--sc-brown-tint);
|
||||||
|
border-color: var(--sc-line);
|
||||||
|
}
|
||||||
|
.btn-check:checked + .btn-secondary, .btn-secondary.active, .show > .btn-secondary.dropdown-toggle {
|
||||||
|
color: #fff;
|
||||||
|
background-color: var(--sc-brown);
|
||||||
|
border-color: var(--sc-brown);
|
||||||
|
}
|
||||||
|
.btn-check:checked + .btn-outline-secondary, .btn-outline-secondary.active {
|
||||||
|
color: #fff !important;
|
||||||
|
border-color: var(--sc-brown) !important;
|
||||||
|
}
|
||||||
|
.btn-outline-primary {
|
||||||
|
color: var(--sc-brown);
|
||||||
|
border-color: var(--sc-brown);
|
||||||
|
}
|
||||||
|
.btn-outline-primary:hover {
|
||||||
|
color: #fff;
|
||||||
|
background-color: var(--sc-brown);
|
||||||
|
border-color: var(--sc-brown);
|
||||||
|
}
|
||||||
|
/* active toggle, e.g. tagging options */
|
||||||
|
.btn-dark, .btn-dark:hover, .btn-dark:focus, .btn-dark:active {
|
||||||
|
color: #fff;
|
||||||
|
background-color: var(--sc-brown);
|
||||||
|
border-color: var(--sc-brown);
|
||||||
|
}
|
||||||
|
.btn-xs, .btn-group-xs > .btn {
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* forms */
|
||||||
|
.form-control, .form-select {
|
||||||
|
border-color: var(--sc-line);
|
||||||
|
border-radius: 0;
|
||||||
|
box-shadow: none;
|
||||||
|
color: var(--sc-ink);
|
||||||
|
background-color: var(--sc-surface);
|
||||||
|
}
|
||||||
|
.form-control:focus, .form-select:focus {
|
||||||
|
border-color: var(--sc-focus-border);
|
||||||
|
box-shadow: 0 0 0 .25rem var(--sc-focus);
|
||||||
|
}
|
||||||
|
.form-control::file-selector-button {
|
||||||
|
color: var(--sc-ink);
|
||||||
|
background-color: var(--sc-addon);
|
||||||
|
border-color: var(--sc-line);
|
||||||
|
}
|
||||||
|
.form-control:hover:not(:disabled):not([readonly])::file-selector-button {
|
||||||
|
color: var(--sc-brown);
|
||||||
|
background-color: var(--sc-brown-soft);
|
||||||
|
}
|
||||||
|
.input-group-text {
|
||||||
|
color: var(--sc-ink);
|
||||||
|
background-color: var(--sc-addon);
|
||||||
|
border-color: var(--sc-line);
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
.input-group > :not(:first-child), .input-group > :not(:last-child) {
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
.form-check-input {
|
||||||
|
border-color: var(--sc-line);
|
||||||
|
}
|
||||||
|
.form-check-input:checked {
|
||||||
|
background-color: var(--sc-brown);
|
||||||
|
border-color: var(--sc-brown);
|
||||||
|
}
|
||||||
|
.form-check-input:focus {
|
||||||
|
border-color: var(--sc-focus-border);
|
||||||
|
box-shadow: 0 0 0 .25rem var(--sc-focus);
|
||||||
|
}
|
||||||
|
.bootstrap-select > .dropdown-toggle {
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
.bootstrap-select > .dropdown-toggle.bs-placeholder,
|
||||||
|
.bootstrap-select > .dropdown-toggle.bs-placeholder:hover,
|
||||||
|
.bootstrap-select > .dropdown-toggle.bs-placeholder:focus,
|
||||||
|
.bootstrap-select > .dropdown-toggle.bs-placeholder:active {
|
||||||
|
color: var(--mc-placeholder);
|
||||||
|
}
|
||||||
|
.bootstrap-select .dropdown-menu li a.active, .bootstrap-select .dropdown-menu li a:active {
|
||||||
|
color: #fff;
|
||||||
|
background-color: var(--sc-brown);
|
||||||
|
}
|
||||||
|
div.numberedtextarea-number {
|
||||||
|
color: var(--sc-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* nav tabs */
|
||||||
|
.nav-tabs {
|
||||||
|
border-bottom: 1px solid var(--sc-divider);
|
||||||
|
}
|
||||||
|
.nav-tabs .nav-link,
|
||||||
|
.nav-tabs .nav-link.disabled, .nav-tabs .nav-link.disabled:hover, .nav-tabs .nav-link.disabled:focus {
|
||||||
|
margin-top: 0;
|
||||||
|
color: var(--sc-ink);
|
||||||
|
border: 0;
|
||||||
|
border-bottom: 3px solid transparent;
|
||||||
|
border-radius: 0;
|
||||||
|
background: none;
|
||||||
|
}
|
||||||
|
.nav-tabs .nav-link:not(.disabled):hover, .nav-tabs .nav-link:not(.disabled):focus {
|
||||||
|
padding-bottom: .5rem;
|
||||||
|
color: var(--sc-brown);
|
||||||
|
}
|
||||||
|
.nav-tabs .nav-link.active, .nav-tabs .nav-item.show .nav-link,
|
||||||
|
.nav-tabs .nav-link:not(.disabled).active {
|
||||||
|
padding-bottom: .5rem;
|
||||||
|
color: var(--sc-brown);
|
||||||
|
background: none;
|
||||||
|
border-bottom: 3px solid var(--sc-brown);
|
||||||
|
}
|
||||||
|
.nav-pills .nav-link {
|
||||||
|
border-radius: 0;
|
||||||
|
color: var(--sc-ink);
|
||||||
|
}
|
||||||
|
.nav-pills .nav-link.active, .nav-pills .show > .nav-link {
|
||||||
|
background-color: var(--sc-brown);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* tables */
|
||||||
|
.table {
|
||||||
|
--bs-table-bg: transparent;
|
||||||
|
--bs-table-striped-bg: var(--sc-stripe);
|
||||||
|
--bs-table-striped-color: var(--sc-ink);
|
||||||
|
--bs-table-hover-color: var(--sc-ink);
|
||||||
|
--bs-table-hover-bg: var(--sc-hover);
|
||||||
|
color: var(--sc-ink);
|
||||||
|
border-color: var(--sc-divider);
|
||||||
|
}
|
||||||
|
.table > thead > tr > th, table.dataTable thead th {
|
||||||
|
color: var(--sc-ink);
|
||||||
|
font-weight: 600;
|
||||||
|
border-bottom: 1px solid var(--sc-line);
|
||||||
|
}
|
||||||
|
table.dataTable.table-striped > tbody > tr.odd > * {
|
||||||
|
box-shadow: inset 0 0 0 9999px var(--sc-stripe);
|
||||||
|
}
|
||||||
|
table.dataTable.table-hover > tbody > tr:hover > * {
|
||||||
|
box-shadow: inset 0 0 0 9999px var(--sc-hover);
|
||||||
|
}
|
||||||
|
.table > :not(:first-child) {
|
||||||
|
border-top: 0;
|
||||||
|
}
|
||||||
|
.dataTables_wrapper .dataTables_filter input,
|
||||||
|
.dataTables_wrapper .dataTables_length select {
|
||||||
|
border: 1px solid var(--sc-line);
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
.pagination > li > a, .pagination > li > span, .pagination .page-link,
|
||||||
|
.pagination > li > a:hover, .pagination > li > a:focus, .pagination > li > a:active {
|
||||||
|
top: 0;
|
||||||
|
color: var(--sc-brown);
|
||||||
|
font-weight: 400;
|
||||||
|
text-transform: none;
|
||||||
|
background-color: var(--sc-surface);
|
||||||
|
border: 1px solid var(--sc-divider);
|
||||||
|
border-radius: 0 !important;
|
||||||
|
}
|
||||||
|
.pagination > li > a:hover, .pagination > li > a:focus {
|
||||||
|
background-color: var(--sc-brown-tint);
|
||||||
|
}
|
||||||
|
.pagination > .disabled > a, .pagination > .disabled > a:hover, .pagination > .disabled > a:active,
|
||||||
|
.page-item.disabled .page-link {
|
||||||
|
color: var(--sc-muted);
|
||||||
|
background-color: var(--sc-surface);
|
||||||
|
border-width: 1px;
|
||||||
|
border-color: var(--sc-divider);
|
||||||
|
}
|
||||||
|
.page-item.active .page-link {
|
||||||
|
color: #fff;
|
||||||
|
background-color: var(--sc-brown);
|
||||||
|
border-color: var(--sc-brown);
|
||||||
|
}
|
||||||
|
.breadcrumb {
|
||||||
|
border: 1px solid var(--sc-divider);
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* alerts like the portal */
|
||||||
|
.alert {
|
||||||
|
border: 1px solid;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
.alert-primary {
|
||||||
|
color: #43382c !important;
|
||||||
|
background-color: #e2dfdb !important;
|
||||||
|
border-color: #d4cec8 !important;
|
||||||
|
}
|
||||||
|
.alert-info {
|
||||||
|
color: #3c535e !important;
|
||||||
|
background-color: #eaf6fb !important;
|
||||||
|
border-color: #e0f1f9 !important;
|
||||||
|
}
|
||||||
|
.alert-success {
|
||||||
|
color: #375603;
|
||||||
|
background-color: #e8f7ce;
|
||||||
|
border-color: #dcf3b5;
|
||||||
|
}
|
||||||
|
.alert-danger {
|
||||||
|
color: #8f4343;
|
||||||
|
background-color: #fce2e2;
|
||||||
|
border-color: #fad4d4;
|
||||||
|
}
|
||||||
|
.alert-warning {
|
||||||
|
color: #885907;
|
||||||
|
background-color: #f9eace;
|
||||||
|
border-color: #f7dfb6;
|
||||||
|
}
|
||||||
|
.alert-secondary, .alert-light {
|
||||||
|
color: var(--sc-ink);
|
||||||
|
background-color: var(--sc-cap);
|
||||||
|
border-color: var(--sc-divider);
|
||||||
|
}
|
||||||
|
.alert .alert-link {
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
.alert .btn-close {
|
||||||
|
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath d='M.293.293a1 1 0 0 1 1.414 0L8 6.586 14.293.293a1 1 0 1 1 1.414 1.414L9.414 8l6.293 6.293a1 1 0 0 1-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 0 1-1.414-1.414L6.586 8 .293 1.707a1 1 0 0 1 0-1.414z'/%3e%3c/svg%3e");
|
||||||
|
opacity: .5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* badges and semantic colors, readable text on top */
|
||||||
|
.badge {
|
||||||
|
font-weight: 500;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
.bg-success {
|
||||||
|
color: #000 !important;
|
||||||
|
background-color: var(--sc-green) !important;
|
||||||
|
}
|
||||||
|
.bg-warning {
|
||||||
|
color: #000 !important;
|
||||||
|
background-color: var(--sc-warning) !important;
|
||||||
|
}
|
||||||
|
.bg-danger {
|
||||||
|
color: #000 !important;
|
||||||
|
background-color: var(--sc-danger) !important;
|
||||||
|
}
|
||||||
|
.badge.bg-secondary, .badge.bg-light {
|
||||||
|
color: var(--sc-ink);
|
||||||
|
background-color: var(--sc-addon) !important;
|
||||||
|
}
|
||||||
|
.text-success {
|
||||||
|
color: #4f8a0b !important;
|
||||||
|
}
|
||||||
|
.text-danger {
|
||||||
|
color: var(--sc-danger-text) !important;
|
||||||
|
}
|
||||||
|
.text-warning {
|
||||||
|
color: #9a6100 !important;
|
||||||
|
}
|
||||||
|
.progress {
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
.progress-bar {
|
||||||
|
color: #000;
|
||||||
|
background-color: var(--sc-green);
|
||||||
|
}
|
||||||
|
.progress-bar.bg-danger, .progress-bar-danger {
|
||||||
|
color: #000;
|
||||||
|
background-color: var(--sc-danger) !important;
|
||||||
|
}
|
||||||
|
.progress-bar-warning {
|
||||||
|
color: #000;
|
||||||
|
background-color: var(--sc-warning) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* footer */
|
||||||
|
.footer, .footer a {
|
||||||
|
color: var(--sc-muted);
|
||||||
|
}
|
||||||
|
.footer hr {
|
||||||
|
border-color: var(--sc-divider);
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"name": "servercow",
|
||||||
|
"description": "Based on the servercow theme"
|
||||||
|
}
|
||||||
@@ -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');
|
||||||
|
|||||||
+3
-5
@@ -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';
|
||||||
|
|||||||
@@ -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'));
|
||||||
|
|||||||
@@ -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,6 +64,8 @@ $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']),
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -233,6 +233,38 @@ function customize($_action, $_item, $_data = null) {
|
|||||||
'msg' => 'custom_login_modified'
|
'msg' => 'custom_login_modified'
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
case 'ui_theme':
|
||||||
|
$ui_theme = is_array($_data['ui_theme']) ? (string)reset($_data['ui_theme']) : (string)$_data['ui_theme'];
|
||||||
|
if ($ui_theme !== '' && !array_key_exists($ui_theme, customize('get', 'ui_themes'))) {
|
||||||
|
$_SESSION['return'][] = array(
|
||||||
|
'type' => 'danger',
|
||||||
|
'log' => array(__FUNCTION__, $_action, $_item, $_data),
|
||||||
|
'msg' => 'ui_theme_invalid'
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if ($ui_theme === '') {
|
||||||
|
$redis->del('UI_THEME');
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$redis->set('UI_THEME', $ui_theme);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (RedisException $e) {
|
||||||
|
$_SESSION['return'][] = array(
|
||||||
|
'type' => 'danger',
|
||||||
|
'log' => array(__FUNCTION__, $_action, $_item, $_data),
|
||||||
|
'msg' => array('redis_error', $e)
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$_SESSION['return'][] = array(
|
||||||
|
'type' => 'success',
|
||||||
|
'log' => array(__FUNCTION__, $_action, $_item, $_data),
|
||||||
|
'msg' => 'ui_theme_modified'
|
||||||
|
);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'delete':
|
case 'delete':
|
||||||
@@ -402,6 +434,42 @@ function customize($_action, $_item, $_data = null) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case 'ui_themes':
|
||||||
|
// theme folders in css/themes/<name>/ with a theme.css
|
||||||
|
$themes = array();
|
||||||
|
foreach (glob('/web/css/themes/*/theme.css') as $theme_css) {
|
||||||
|
$theme_dir = dirname($theme_css);
|
||||||
|
$theme = basename($theme_dir);
|
||||||
|
if (!preg_match('/^[a-zA-Z0-9_-]+$/', $theme)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$meta = json_decode(@file_get_contents($theme_dir . '/theme.json'), true);
|
||||||
|
$themes[$theme] = array(
|
||||||
|
'name' => (!empty($meta['name'])) ? $meta['name'] : $theme,
|
||||||
|
'description' => (!empty($meta['description'])) ? $meta['description'] : '',
|
||||||
|
'dark' => file_exists($theme_dir . '/dark.css')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
ksort($themes);
|
||||||
|
return $themes;
|
||||||
|
break;
|
||||||
|
case 'ui_theme':
|
||||||
|
try {
|
||||||
|
$ui_theme = $redis->get('UI_THEME');
|
||||||
|
}
|
||||||
|
catch (RedisException $e) {
|
||||||
|
$_SESSION['return'][] = array(
|
||||||
|
'type' => 'danger',
|
||||||
|
'log' => array(__FUNCTION__, $_action, $_item, $_data),
|
||||||
|
'msg' => array('redis_error', $e)
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (empty($ui_theme) || !preg_match('/^[a-zA-Z0-9_-]+$/', $ui_theme) || !file_exists('/web/css/themes/' . $ui_theme . '/theme.css')) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return $ui_theme;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
+292
-61
@@ -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'];
|
||||||
@@ -1148,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"]) {
|
||||||
@@ -1244,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),
|
||||||
@@ -1257,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),
|
||||||
@@ -1285,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),
|
||||||
@@ -1292,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(
|
||||||
@@ -1544,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'");
|
||||||
@@ -1576,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'])) {
|
||||||
@@ -2164,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),
|
||||||
@@ -2175,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;
|
||||||
@@ -2223,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');
|
||||||
|
|
||||||
@@ -3378,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;
|
||||||
|
|||||||
@@ -9,6 +9,10 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
|||||||
$_data_log = $_data;
|
$_data_log = $_data;
|
||||||
!isset($_data_log['password']) ?: $_data_log['password'] = '*';
|
!isset($_data_log['password']) ?: $_data_log['password'] = '*';
|
||||||
!isset($_data_log['password2']) ?: $_data_log['password2'] = '*';
|
!isset($_data_log['password2']) ?: $_data_log['password2'] = '*';
|
||||||
|
|
||||||
|
// Track mailboxes affected by alias operations for incremental SOGo updates
|
||||||
|
$update_sogo_mailboxes = array();
|
||||||
|
|
||||||
switch ($_action) {
|
switch ($_action) {
|
||||||
case 'add':
|
case 'add':
|
||||||
switch ($_type) {
|
switch ($_type) {
|
||||||
@@ -37,13 +41,15 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
|||||||
else {
|
else {
|
||||||
$username = $_SESSION['mailcow_cc_username'];
|
$username = $_SESSION['mailcow_cc_username'];
|
||||||
}
|
}
|
||||||
if (isset($_data["validity"]) && !filter_var($_data["validity"], FILTER_VALIDATE_INT, array('options' => array('min_range' => 1, 'max_range' => 87600)))) {
|
if (isset($_data["validity"])) {
|
||||||
$_SESSION['return'][] = array(
|
if (!filter_var($_data["validity"], FILTER_VALIDATE_INT, array('options' => array('min_range' => 1, 'max_range' => 87600)))) {
|
||||||
'type' => 'danger',
|
$_SESSION['return'][] = array(
|
||||||
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
'type' => 'danger',
|
||||||
'msg' => 'validity_missing'
|
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
||||||
);
|
'msg' => 'validity_missing'
|
||||||
return false;
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// Default to 1 yr
|
// Default to 1 yr
|
||||||
@@ -56,7 +62,8 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
|||||||
$permanent = 0;
|
$permanent = 0;
|
||||||
}
|
}
|
||||||
$domain = $_data['domain'];
|
$domain = $_data['domain'];
|
||||||
$description = $_data['description'];
|
// spamalias.description is NOT NULL, an absent description must not become a null insert
|
||||||
|
$description = $_data['description'] ?? '';
|
||||||
$valid_domains[] = mailbox('get', 'mailbox_details', $username)['domain'];
|
$valid_domains[] = mailbox('get', 'mailbox_details', $username)['domain'];
|
||||||
$valid_alias_domains = user_get_alias_details($username)['alias_domains'];
|
$valid_alias_domains = user_get_alias_details($username)['alias_domains'];
|
||||||
if (!empty($valid_alias_domains)) {
|
if (!empty($valid_alias_domains)) {
|
||||||
@@ -695,6 +702,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
|||||||
$gotos = array_map('trim', preg_split( "/( |,|;|\n)/", $_data['goto']));
|
$gotos = array_map('trim', preg_split( "/( |,|;|\n)/", $_data['goto']));
|
||||||
$internal = intval($_data['internal']);
|
$internal = intval($_data['internal']);
|
||||||
$active = intval($_data['active']);
|
$active = intval($_data['active']);
|
||||||
|
$sender_allowed = intval($_data['sender_allowed']);
|
||||||
$sogo_visible = intval($_data['sogo_visible']);
|
$sogo_visible = intval($_data['sogo_visible']);
|
||||||
$goto_null = intval($_data['goto_null']);
|
$goto_null = intval($_data['goto_null']);
|
||||||
$goto_spam = intval($_data['goto_spam']);
|
$goto_spam = intval($_data['goto_spam']);
|
||||||
@@ -742,6 +750,18 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
|||||||
$goto_domain = idn_to_ascii(substr(strstr($goto, '@'), 1), 0, INTL_IDNA_VARIANT_UTS46);
|
$goto_domain = idn_to_ascii(substr(strstr($goto, '@'), 1), 0, INTL_IDNA_VARIANT_UTS46);
|
||||||
$goto_local_part = strstr($goto, '@', true);
|
$goto_local_part = strstr($goto, '@', true);
|
||||||
$goto = $goto_local_part.'@'.$goto_domain;
|
$goto = $goto_local_part.'@'.$goto_domain;
|
||||||
|
// Deny external goto domains: global switch (all roles) overrides the per-DA ACL
|
||||||
|
if (($GLOBALS['ALIAS_DISABLE_EXTERNAL_DOMAINS'] === true ||
|
||||||
|
(isset($_SESSION['acl']['alias_external_goto']) && $_SESSION['acl']['alias_external_goto'] != "1")) &&
|
||||||
|
!is_local_mailcow_domain($goto_domain)) {
|
||||||
|
$_SESSION['return'][] = array(
|
||||||
|
'type' => 'danger',
|
||||||
|
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
||||||
|
'msg' => array('external_goto_denied', htmlspecialchars($goto))
|
||||||
|
);
|
||||||
|
unset($gotos[$i]);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
$stmt = $pdo->prepare("SELECT `username` FROM `mailbox`
|
$stmt = $pdo->prepare("SELECT `username` FROM `mailbox`
|
||||||
WHERE `kind` REGEXP 'location|thing|group'
|
WHERE `kind` REGEXP 'location|thing|group'
|
||||||
AND `username`= :goto");
|
AND `username`= :goto");
|
||||||
@@ -850,8 +870,8 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
|||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
$stmt = $pdo->prepare("INSERT INTO `alias` (`address`, `public_comment`, `private_comment`, `goto`, `domain`, `sogo_visible`, `internal`, `active`)
|
$stmt = $pdo->prepare("INSERT INTO `alias` (`address`, `public_comment`, `private_comment`, `goto`, `domain`, `sogo_visible`, `internal`, `sender_allowed`, `active`)
|
||||||
VALUES (:address, :public_comment, :private_comment, :goto, :domain, :sogo_visible, :internal, :active)");
|
VALUES (:address, :public_comment, :private_comment, :goto, :domain, :sogo_visible, :internal, :sender_allowed, :active)");
|
||||||
if (!filter_var($address, FILTER_VALIDATE_EMAIL) === true) {
|
if (!filter_var($address, FILTER_VALIDATE_EMAIL) === true) {
|
||||||
$stmt->execute(array(
|
$stmt->execute(array(
|
||||||
':address' => '@'.$domain,
|
':address' => '@'.$domain,
|
||||||
@@ -862,6 +882,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
|||||||
':domain' => $domain,
|
':domain' => $domain,
|
||||||
':sogo_visible' => $sogo_visible,
|
':sogo_visible' => $sogo_visible,
|
||||||
':internal' => $internal,
|
':internal' => $internal,
|
||||||
|
':sender_allowed' => $sender_allowed,
|
||||||
':active' => $active
|
':active' => $active
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
@@ -874,6 +895,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
|||||||
':domain' => $domain,
|
':domain' => $domain,
|
||||||
':sogo_visible' => $sogo_visible,
|
':sogo_visible' => $sogo_visible,
|
||||||
':internal' => $internal,
|
':internal' => $internal,
|
||||||
|
':sender_allowed' => $sender_allowed,
|
||||||
':active' => $active
|
':active' => $active
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
@@ -883,6 +905,17 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
|||||||
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
||||||
'msg' => array('alias_added', $address, $id)
|
'msg' => array('alias_added', $address, $id)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Track affected mailboxes for SOGo update
|
||||||
|
if (!empty($goto)) {
|
||||||
|
$gotos = array_map('trim', explode(',', $goto));
|
||||||
|
foreach ($gotos as $g) {
|
||||||
|
if (filter_var($g, FILTER_VALIDATE_EMAIL) &&
|
||||||
|
!in_array($g, array('null@localhost', 'spam@localhost', 'ham@localhost'))) {
|
||||||
|
$update_sogo_mailboxes[] = $g;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'alias_domain':
|
case 'alias_domain':
|
||||||
@@ -1075,9 +1108,12 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
|||||||
$_data['pop3_access'] = (in_array('pop3', $_data['protocol_access'])) ? 1 : 0;
|
$_data['pop3_access'] = (in_array('pop3', $_data['protocol_access'])) ? 1 : 0;
|
||||||
$_data['smtp_access'] = (in_array('smtp', $_data['protocol_access'])) ? 1 : 0;
|
$_data['smtp_access'] = (in_array('smtp', $_data['protocol_access'])) ? 1 : 0;
|
||||||
$_data['sieve_access'] = (in_array('sieve', $_data['protocol_access'])) ? 1 : 0;
|
$_data['sieve_access'] = (in_array('sieve', $_data['protocol_access'])) ? 1 : 0;
|
||||||
|
$_data['eas_access'] = (in_array('eas', $_data['protocol_access'])) ? 1 : 0;
|
||||||
|
$_data['dav_access'] = (in_array('dav', $_data['protocol_access'])) ? 1 : 0;
|
||||||
}
|
}
|
||||||
$active = (isset($_data['active'])) ? intval($_data['active']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['active']);
|
$active = (isset($_data['active'])) ? intval($_data['active']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['active']);
|
||||||
$force_pw_update = (isset($_data['force_pw_update'])) ? intval($_data['force_pw_update']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['force_pw_update']);
|
$force_pw_update = (isset($_data['force_pw_update'])) ? intval($_data['force_pw_update']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['force_pw_update']);
|
||||||
|
$force_tfa = (isset($_data['force_tfa'])) ? intval($_data['force_tfa']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['force_tfa']);
|
||||||
$tls_enforce_in = (isset($_data['tls_enforce_in'])) ? intval($_data['tls_enforce_in']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['tls_enforce_in']);
|
$tls_enforce_in = (isset($_data['tls_enforce_in'])) ? intval($_data['tls_enforce_in']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['tls_enforce_in']);
|
||||||
$tls_enforce_out = (isset($_data['tls_enforce_out'])) ? intval($_data['tls_enforce_out']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['tls_enforce_out']);
|
$tls_enforce_out = (isset($_data['tls_enforce_out'])) ? intval($_data['tls_enforce_out']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['tls_enforce_out']);
|
||||||
$sogo_access = (isset($_data['sogo_access'])) ? intval($_data['sogo_access']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['sogo_access']);
|
$sogo_access = (isset($_data['sogo_access'])) ? intval($_data['sogo_access']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['sogo_access']);
|
||||||
@@ -1085,17 +1121,32 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
|||||||
$pop3_access = (isset($_data['pop3_access'])) ? intval($_data['pop3_access']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['pop3_access']);
|
$pop3_access = (isset($_data['pop3_access'])) ? intval($_data['pop3_access']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['pop3_access']);
|
||||||
$smtp_access = (isset($_data['smtp_access'])) ? intval($_data['smtp_access']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['smtp_access']);
|
$smtp_access = (isset($_data['smtp_access'])) ? intval($_data['smtp_access']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['smtp_access']);
|
||||||
$sieve_access = (isset($_data['sieve_access'])) ? intval($_data['sieve_access']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['sieve_access']);
|
$sieve_access = (isset($_data['sieve_access'])) ? intval($_data['sieve_access']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['sieve_access']);
|
||||||
|
$eas_access = (isset($_data['eas_access'])) ? intval($_data['eas_access']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['eas_access']);
|
||||||
|
$dav_access = (isset($_data['dav_access'])) ? intval($_data['dav_access']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['dav_access']);
|
||||||
$relayhost = (isset($_data['relayhost'])) ? intval($_data['relayhost']) : 0;
|
$relayhost = (isset($_data['relayhost'])) ? intval($_data['relayhost']) : 0;
|
||||||
$quarantine_notification = (isset($_data['quarantine_notification'])) ? strval($_data['quarantine_notification']) : strval($MAILBOX_DEFAULT_ATTRIBUTES['quarantine_notification']);
|
$quarantine_notification = (isset($_data['quarantine_notification'])) ? strval($_data['quarantine_notification']) : strval($MAILBOX_DEFAULT_ATTRIBUTES['quarantine_notification']);
|
||||||
$quarantine_category = (isset($_data['quarantine_category'])) ? strval($_data['quarantine_category']) : strval($MAILBOX_DEFAULT_ATTRIBUTES['quarantine_category']);
|
$quarantine_category = (isset($_data['quarantine_category'])) ? strval($_data['quarantine_category']) : strval($MAILBOX_DEFAULT_ATTRIBUTES['quarantine_category']);
|
||||||
|
// Validate quarantine_category
|
||||||
|
if (!in_array($quarantine_category, array('add_header', 'reject', 'all'))) {
|
||||||
|
$_SESSION['return'][] = array(
|
||||||
|
'type' => 'danger',
|
||||||
|
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
||||||
|
'msg' => 'quarantine_category_invalid'
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
$quota_b = ($quota_m * 1048576);
|
$quota_b = ($quota_m * 1048576);
|
||||||
$attribute_hash = (!empty($_data['attribute_hash'])) ? $_data['attribute_hash'] : '';
|
$attribute_hash = (!empty($_data['attribute_hash'])) ? $_data['attribute_hash'] : '';
|
||||||
if (in_array($authsource, array('keycloak', 'generic-oidc', 'ldap'))){
|
if (in_array($authsource, array('keycloak', 'generic-oidc', 'ldap'))){
|
||||||
$force_pw_update = 0;
|
$force_pw_update = 0;
|
||||||
}
|
}
|
||||||
|
if ($authsource == 'generic-oidc'){
|
||||||
|
$force_tfa = 0;
|
||||||
|
}
|
||||||
$mailbox_attrs = json_encode(
|
$mailbox_attrs = json_encode(
|
||||||
array(
|
array(
|
||||||
'force_pw_update' => strval($force_pw_update),
|
'force_pw_update' => strval($force_pw_update),
|
||||||
|
'force_tfa' => strval($force_tfa),
|
||||||
'tls_enforce_in' => strval($tls_enforce_in),
|
'tls_enforce_in' => strval($tls_enforce_in),
|
||||||
'tls_enforce_out' => strval($tls_enforce_out),
|
'tls_enforce_out' => strval($tls_enforce_out),
|
||||||
'sogo_access' => strval($sogo_access),
|
'sogo_access' => strval($sogo_access),
|
||||||
@@ -1103,6 +1154,8 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
|||||||
'pop3_access' => strval($pop3_access),
|
'pop3_access' => strval($pop3_access),
|
||||||
'smtp_access' => strval($smtp_access),
|
'smtp_access' => strval($smtp_access),
|
||||||
'sieve_access' => strval($sieve_access),
|
'sieve_access' => strval($sieve_access),
|
||||||
|
'eas_access' => strval($eas_access),
|
||||||
|
'dav_access' => strval($dav_access),
|
||||||
'relayhost' => strval($relayhost),
|
'relayhost' => strval($relayhost),
|
||||||
'passwd_update' => time(),
|
'passwd_update' => time(),
|
||||||
'mailbox_format' => strval($MAILBOX_DEFAULT_ATTRIBUTES['mailbox_format']),
|
'mailbox_format' => strval($MAILBOX_DEFAULT_ATTRIBUTES['mailbox_format']),
|
||||||
@@ -1356,15 +1409,8 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
|||||||
), $_extra);
|
), $_extra);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
// Track affected mailboxes for SOGo update
|
||||||
update_sogo_static_view($username);
|
$update_sogo_mailboxes[] = $username;
|
||||||
} catch (PDOException $e) {
|
|
||||||
$_SESSION['return'][] = array(
|
|
||||||
'type' => 'danger',
|
|
||||||
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
|
||||||
'msg' => $e->getMessage()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
$_SESSION['return'][] = array(
|
$_SESSION['return'][] = array(
|
||||||
'type' => 'success',
|
'type' => 'success',
|
||||||
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
||||||
@@ -1595,6 +1641,9 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
|||||||
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
||||||
'msg' => array('resource_added', htmlspecialchars($name))
|
'msg' => array('resource_added', htmlspecialchars($name))
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Track affected mailboxes for SOGo update
|
||||||
|
$update_sogo_mailboxes[] = $name;
|
||||||
break;
|
break;
|
||||||
case 'domain_templates':
|
case 'domain_templates':
|
||||||
if ($_SESSION['mailcow_cc_role'] != "admin") {
|
if ($_SESSION['mailcow_cc_role'] != "admin") {
|
||||||
@@ -1708,9 +1757,19 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
|||||||
$attr["tagged_mail_handler"] = (!empty($_data['tagged_mail_handler'])) ? $_data['tagged_mail_handler'] : strval($MAILBOX_DEFAULT_ATTRIBUTES['tagged_mail_handler']);
|
$attr["tagged_mail_handler"] = (!empty($_data['tagged_mail_handler'])) ? $_data['tagged_mail_handler'] : strval($MAILBOX_DEFAULT_ATTRIBUTES['tagged_mail_handler']);
|
||||||
$attr["quarantine_notification"] = (!empty($_data['quarantine_notification'])) ? $_data['quarantine_notification'] : strval($MAILBOX_DEFAULT_ATTRIBUTES['quarantine_notification']);
|
$attr["quarantine_notification"] = (!empty($_data['quarantine_notification'])) ? $_data['quarantine_notification'] : strval($MAILBOX_DEFAULT_ATTRIBUTES['quarantine_notification']);
|
||||||
$attr["quarantine_category"] = (!empty($_data['quarantine_category'])) ? $_data['quarantine_category'] : strval($MAILBOX_DEFAULT_ATTRIBUTES['quarantine_category']);
|
$attr["quarantine_category"] = (!empty($_data['quarantine_category'])) ? $_data['quarantine_category'] : strval($MAILBOX_DEFAULT_ATTRIBUTES['quarantine_category']);
|
||||||
|
// Validate quarantine_category
|
||||||
|
if (!in_array($attr["quarantine_category"], array('add_header', 'reject', 'all'))) {
|
||||||
|
$_SESSION['return'][] = array(
|
||||||
|
'type' => 'danger',
|
||||||
|
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_extra),
|
||||||
|
'msg' => 'quarantine_category_invalid'
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
$attr["rl_frame"] = (!empty($_data['rl_frame'])) ? $_data['rl_frame'] : "s";
|
$attr["rl_frame"] = (!empty($_data['rl_frame'])) ? $_data['rl_frame'] : "s";
|
||||||
$attr["rl_value"] = (!empty($_data['rl_value'])) ? $_data['rl_value'] : "";
|
$attr["rl_value"] = (!empty($_data['rl_value'])) ? $_data['rl_value'] : "";
|
||||||
$attr["force_pw_update"] = isset($_data['force_pw_update']) ? intval($_data['force_pw_update']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['force_pw_update']);
|
$attr["force_pw_update"] = isset($_data['force_pw_update']) ? intval($_data['force_pw_update']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['force_pw_update']);
|
||||||
|
$attr["force_tfa"] = isset($_data['force_tfa']) ? intval($_data['force_tfa']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['force_tfa']);
|
||||||
$attr["sogo_access"] = isset($_data['sogo_access']) ? intval($_data['sogo_access']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['sogo_access']);
|
$attr["sogo_access"] = isset($_data['sogo_access']) ? intval($_data['sogo_access']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['sogo_access']);
|
||||||
$attr["active"] = isset($_data['active']) ? intval($_data['active']) : 1;
|
$attr["active"] = isset($_data['active']) ? intval($_data['active']) : 1;
|
||||||
$attr["tls_enforce_in"] = isset($_data['tls_enforce_in']) ? intval($_data['tls_enforce_in']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['tls_enforce_in']);
|
$attr["tls_enforce_in"] = isset($_data['tls_enforce_in']) ? intval($_data['tls_enforce_in']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['tls_enforce_in']);
|
||||||
@@ -1721,12 +1780,16 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
|||||||
$attr['pop3_access'] = (in_array('pop3', $_data['protocol_access'])) ? 1 : 0;
|
$attr['pop3_access'] = (in_array('pop3', $_data['protocol_access'])) ? 1 : 0;
|
||||||
$attr['smtp_access'] = (in_array('smtp', $_data['protocol_access'])) ? 1 : 0;
|
$attr['smtp_access'] = (in_array('smtp', $_data['protocol_access'])) ? 1 : 0;
|
||||||
$attr['sieve_access'] = (in_array('sieve', $_data['protocol_access'])) ? 1 : 0;
|
$attr['sieve_access'] = (in_array('sieve', $_data['protocol_access'])) ? 1 : 0;
|
||||||
|
$attr['eas_access'] = (in_array('eas', $_data['protocol_access'])) ? 1 : 0;
|
||||||
|
$attr['dav_access'] = (in_array('dav', $_data['protocol_access'])) ? 1 : 0;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
$attr['imap_access'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['imap_access']);
|
$attr['imap_access'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['imap_access']);
|
||||||
$attr['pop3_access'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['pop3_access']);
|
$attr['pop3_access'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['pop3_access']);
|
||||||
$attr['smtp_access'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['smtp_access']);
|
$attr['smtp_access'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['smtp_access']);
|
||||||
$attr['sieve_access'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['sieve_access']);
|
$attr['sieve_access'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['sieve_access']);
|
||||||
|
$attr['eas_access'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['eas_access']);
|
||||||
|
$attr['dav_access'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['dav_access']);
|
||||||
}
|
}
|
||||||
if (isset($_data['acl'])) {
|
if (isset($_data['acl'])) {
|
||||||
$_data['acl'] = (array)$_data['acl'];
|
$_data['acl'] = (array)$_data['acl'];
|
||||||
@@ -2032,6 +2095,14 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
foreach ($usernames as $username) {
|
foreach ($usernames as $username) {
|
||||||
|
if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $username)) {
|
||||||
|
$_SESSION['return'][] = array(
|
||||||
|
'type' => 'danger',
|
||||||
|
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
||||||
|
'msg' => 'access_denied'
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if ($_data['spam_score'] == "default") {
|
if ($_data['spam_score'] == "default") {
|
||||||
$stmt = $pdo->prepare("DELETE FROM `filterconf` WHERE `object` = :username
|
$stmt = $pdo->prepare("DELETE FROM `filterconf` WHERE `object` = :username
|
||||||
AND (`option` = 'lowspamlevel' OR `option` = 'highspamlevel')");
|
AND (`option` = 'lowspamlevel' OR `option` = 'highspamlevel')");
|
||||||
@@ -2501,6 +2572,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
|||||||
if (!empty($is_now)) {
|
if (!empty($is_now)) {
|
||||||
$internal = (isset($_data['internal'])) ? intval($_data['internal']) : $is_now['internal'];
|
$internal = (isset($_data['internal'])) ? intval($_data['internal']) : $is_now['internal'];
|
||||||
$active = (isset($_data['active'])) ? intval($_data['active']) : $is_now['active'];
|
$active = (isset($_data['active'])) ? intval($_data['active']) : $is_now['active'];
|
||||||
|
$sender_allowed = (isset($_data['sender_allowed'])) ? intval($_data['sender_allowed']) : $is_now['sender_allowed'];
|
||||||
$sogo_visible = (isset($_data['sogo_visible'])) ? intval($_data['sogo_visible']) : $is_now['sogo_visible'];
|
$sogo_visible = (isset($_data['sogo_visible'])) ? intval($_data['sogo_visible']) : $is_now['sogo_visible'];
|
||||||
$goto_null = (isset($_data['goto_null'])) ? intval($_data['goto_null']) : 0;
|
$goto_null = (isset($_data['goto_null'])) ? intval($_data['goto_null']) : 0;
|
||||||
$goto_spam = (isset($_data['goto_spam'])) ? intval($_data['goto_spam']) : 0;
|
$goto_spam = (isset($_data['goto_spam'])) ? intval($_data['goto_spam']) : 0;
|
||||||
@@ -2655,6 +2727,19 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
|||||||
unset($gotos[$i]);
|
unset($gotos[$i]);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
// Deny external goto domains: global switch (all roles) overrides the per-DA ACL
|
||||||
|
$goto_domain = idn_to_ascii(substr(strstr($goto, '@'), 1), 0, INTL_IDNA_VARIANT_UTS46);
|
||||||
|
if (($GLOBALS['ALIAS_DISABLE_EXTERNAL_DOMAINS'] === true ||
|
||||||
|
(isset($_SESSION['acl']['alias_external_goto']) && $_SESSION['acl']['alias_external_goto'] != "1")) &&
|
||||||
|
!is_local_mailcow_domain($goto_domain)) {
|
||||||
|
$_SESSION['return'][] = array(
|
||||||
|
'type' => 'danger',
|
||||||
|
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
||||||
|
'msg' => array('external_goto_denied', htmlspecialchars($goto))
|
||||||
|
);
|
||||||
|
unset($gotos[$i]);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if ($goto == $address) {
|
if ($goto == $address) {
|
||||||
$_SESSION['return'][] = array(
|
$_SESSION['return'][] = array(
|
||||||
'type' => 'danger',
|
'type' => 'danger',
|
||||||
@@ -2686,6 +2771,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
|||||||
`goto` = :goto,
|
`goto` = :goto,
|
||||||
`sogo_visible`= :sogo_visible,
|
`sogo_visible`= :sogo_visible,
|
||||||
`internal`= :internal,
|
`internal`= :internal,
|
||||||
|
`sender_allowed`= :sender_allowed,
|
||||||
`active`= :active
|
`active`= :active
|
||||||
WHERE `id` = :id");
|
WHERE `id` = :id");
|
||||||
$stmt->execute(array(
|
$stmt->execute(array(
|
||||||
@@ -2696,6 +2782,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
|||||||
':goto' => $goto,
|
':goto' => $goto,
|
||||||
':sogo_visible' => $sogo_visible,
|
':sogo_visible' => $sogo_visible,
|
||||||
':internal' => $internal,
|
':internal' => $internal,
|
||||||
|
':sender_allowed' => $sender_allowed,
|
||||||
':active' => $active,
|
':active' => $active,
|
||||||
':id' => $is_now['id']
|
':id' => $is_now['id']
|
||||||
));
|
));
|
||||||
@@ -2705,6 +2792,28 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
|||||||
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
||||||
'msg' => array('alias_modified', htmlspecialchars($address))
|
'msg' => array('alias_modified', htmlspecialchars($address))
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Track affected mailboxes for SOGo update (both old and new goto addresses)
|
||||||
|
// Old goto: to remove alias from their view
|
||||||
|
if (!empty($is_now['goto'])) {
|
||||||
|
$old_gotos = array_map('trim', explode(',', $is_now['goto']));
|
||||||
|
foreach ($old_gotos as $g) {
|
||||||
|
if (filter_var($g, FILTER_VALIDATE_EMAIL) &&
|
||||||
|
!in_array($g, array('null@localhost', 'spam@localhost', 'ham@localhost'))) {
|
||||||
|
$update_sogo_mailboxes[] = $g;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// New goto: to add alias to their view
|
||||||
|
if (!empty($goto)) {
|
||||||
|
$new_gotos = array_map('trim', explode(',', $goto));
|
||||||
|
foreach ($new_gotos as $g) {
|
||||||
|
if (filter_var($g, FILTER_VALIDATE_EMAIL) &&
|
||||||
|
!in_array($g, array('null@localhost', 'spam@localhost', 'ham@localhost'))) {
|
||||||
|
$update_sogo_mailboxes[] = $g;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'domain':
|
case 'domain':
|
||||||
@@ -3043,15 +3152,20 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
|||||||
$_data['pop3_access'] = (in_array('pop3', $_data['protocol_access'])) ? 1 : 0;
|
$_data['pop3_access'] = (in_array('pop3', $_data['protocol_access'])) ? 1 : 0;
|
||||||
$_data['smtp_access'] = (in_array('smtp', $_data['protocol_access'])) ? 1 : 0;
|
$_data['smtp_access'] = (in_array('smtp', $_data['protocol_access'])) ? 1 : 0;
|
||||||
$_data['sieve_access'] = (in_array('sieve', $_data['protocol_access'])) ? 1 : 0;
|
$_data['sieve_access'] = (in_array('sieve', $_data['protocol_access'])) ? 1 : 0;
|
||||||
|
$_data['eas_access'] = (in_array('eas', $_data['protocol_access'])) ? 1 : 0;
|
||||||
|
$_data['dav_access'] = (in_array('dav', $_data['protocol_access'])) ? 1 : 0;
|
||||||
}
|
}
|
||||||
if (!empty($is_now)) {
|
if (!empty($is_now)) {
|
||||||
$active = (isset($_data['active'])) ? intval($_data['active']) : $is_now['active'];
|
$active = (isset($_data['active'])) ? intval($_data['active']) : $is_now['active'];
|
||||||
(int)$force_pw_update = (isset($_data['force_pw_update'])) ? intval($_data['force_pw_update']) : intval($is_now['attributes']['force_pw_update']);
|
(int)$force_pw_update = (isset($_data['force_pw_update'])) ? intval($_data['force_pw_update']) : intval($is_now['attributes']['force_pw_update']);
|
||||||
|
(int)$force_tfa = (isset($_data['force_tfa'])) ? intval($_data['force_tfa']) : intval($is_now['attributes']['force_tfa']);
|
||||||
(int)$sogo_access = (isset($_data['sogo_access']) && hasACLAccess("sogo_access")) ? intval($_data['sogo_access']) : intval($is_now['attributes']['sogo_access']);
|
(int)$sogo_access = (isset($_data['sogo_access']) && hasACLAccess("sogo_access")) ? intval($_data['sogo_access']) : intval($is_now['attributes']['sogo_access']);
|
||||||
(int)$imap_access = (isset($_data['imap_access']) && hasACLAccess("protocol_access")) ? intval($_data['imap_access']) : intval($is_now['attributes']['imap_access']);
|
(int)$imap_access = (isset($_data['imap_access']) && hasACLAccess("protocol_access")) ? intval($_data['imap_access']) : intval($is_now['attributes']['imap_access']);
|
||||||
(int)$pop3_access = (isset($_data['pop3_access']) && hasACLAccess("protocol_access")) ? intval($_data['pop3_access']) : intval($is_now['attributes']['pop3_access']);
|
(int)$pop3_access = (isset($_data['pop3_access']) && hasACLAccess("protocol_access")) ? intval($_data['pop3_access']) : intval($is_now['attributes']['pop3_access']);
|
||||||
(int)$smtp_access = (isset($_data['smtp_access']) && hasACLAccess("protocol_access")) ? intval($_data['smtp_access']) : intval($is_now['attributes']['smtp_access']);
|
(int)$smtp_access = (isset($_data['smtp_access']) && hasACLAccess("protocol_access")) ? intval($_data['smtp_access']) : intval($is_now['attributes']['smtp_access']);
|
||||||
(int)$sieve_access = (isset($_data['sieve_access']) && hasACLAccess("protocol_access")) ? intval($_data['sieve_access']) : intval($is_now['attributes']['sieve_access']);
|
(int)$sieve_access = (isset($_data['sieve_access']) && hasACLAccess("protocol_access")) ? intval($_data['sieve_access']) : intval($is_now['attributes']['sieve_access']);
|
||||||
|
(int)$eas_access = (isset($_data['eas_access']) && hasACLAccess("protocol_access")) ? intval($_data['eas_access']) : intval($is_now['attributes']['eas_access']);
|
||||||
|
(int)$dav_access = (isset($_data['dav_access']) && hasACLAccess("protocol_access")) ? intval($_data['dav_access']) : intval($is_now['attributes']['dav_access']);
|
||||||
(int)$relayhost = (isset($_data['relayhost']) && hasACLAccess("mailbox_relayhost")) ? intval($_data['relayhost']) : intval($is_now['attributes']['relayhost']);
|
(int)$relayhost = (isset($_data['relayhost']) && hasACLAccess("mailbox_relayhost")) ? intval($_data['relayhost']) : intval($is_now['attributes']['relayhost']);
|
||||||
(int)$quota_m = (isset_has_content($_data['quota'])) ? intval($_data['quota']) : ($is_now['quota'] / 1048576);
|
(int)$quota_m = (isset_has_content($_data['quota'])) ? intval($_data['quota']) : ($is_now['quota'] / 1048576);
|
||||||
$name = (!empty($_data['name'])) ? ltrim(rtrim($_data['name'], '>'), '<') : $is_now['name'];
|
$name = (!empty($_data['name'])) ? ltrim(rtrim($_data['name'], '>'), '<') : $is_now['name'];
|
||||||
@@ -3069,6 +3183,9 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
|||||||
if (in_array($authsource, array('keycloak', 'generic-oidc', 'ldap'))){
|
if (in_array($authsource, array('keycloak', 'generic-oidc', 'ldap'))){
|
||||||
$force_pw_update = 0;
|
$force_pw_update = 0;
|
||||||
}
|
}
|
||||||
|
if ($authsource == 'generic-oidc'){
|
||||||
|
$force_tfa = 0;
|
||||||
|
}
|
||||||
$pw_recovery_email = (isset($_data['pw_recovery_email']) && $authsource == 'mailcow') ? $_data['pw_recovery_email'] : $is_now['attributes']['recovery_email'];
|
$pw_recovery_email = (isset($_data['pw_recovery_email']) && $authsource == 'mailcow') ? $_data['pw_recovery_email'] : $is_now['attributes']['recovery_email'];
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -3185,9 +3302,10 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
|||||||
}
|
}
|
||||||
if (isset($_data['sender_acl'])) {
|
if (isset($_data['sender_acl'])) {
|
||||||
// Get sender_acl items set by admin
|
// Get sender_acl items set by admin
|
||||||
|
$current_sender_acls = mailbox('get', 'sender_acl_handles', $username);
|
||||||
$sender_acl_admin = array_merge(
|
$sender_acl_admin = array_merge(
|
||||||
mailbox('get', 'sender_acl_handles', $username)['sender_acl_domains']['ro'],
|
$current_sender_acls['sender_acl_domains']['ro'],
|
||||||
mailbox('get', 'sender_acl_handles', $username)['sender_acl_addresses']['ro']
|
$current_sender_acls['sender_acl_addresses']['ro']
|
||||||
);
|
);
|
||||||
// Get sender_acl items from POST array
|
// Get sender_acl items from POST array
|
||||||
// Set sender_acl_domain_admin to empty array if sender_acl contains "default" to trigger a reset
|
// Set sender_acl_domain_admin to empty array if sender_acl contains "default" to trigger a reset
|
||||||
@@ -3275,16 +3393,25 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
|||||||
$stmt->execute(array(
|
$stmt->execute(array(
|
||||||
':username' => $username
|
':username' => $username
|
||||||
));
|
));
|
||||||
$fixed_sender_aliases = mailbox('get', 'sender_acl_handles', $username)['fixed_sender_aliases'];
|
$sender_acl_handles = mailbox('get', 'sender_acl_handles', $username);
|
||||||
|
$fixed_sender_aliases_allowed = $sender_acl_handles['fixed_sender_aliases_allowed'];
|
||||||
|
$fixed_sender_aliases_blocked = $sender_acl_handles['fixed_sender_aliases_blocked'];
|
||||||
|
|
||||||
foreach ($sender_acl_merged as $sender_acl) {
|
foreach ($sender_acl_merged as $sender_acl) {
|
||||||
$domain = ltrim($sender_acl, '@');
|
$domain = ltrim($sender_acl, '@');
|
||||||
if (is_valid_domain_name($domain)) {
|
if (is_valid_domain_name($domain)) {
|
||||||
$sender_acl = '@' . $domain;
|
$sender_acl = '@' . $domain;
|
||||||
}
|
}
|
||||||
// Don't add if allowed by alias
|
|
||||||
if (in_array($sender_acl, $fixed_sender_aliases)) {
|
// Always add to sender_acl table to create explicit permission
|
||||||
|
// Skip only if it's in allowed list (would be redundant)
|
||||||
|
// But DO add if it's in blocked list (creates override)
|
||||||
|
if (in_array($sender_acl, $fixed_sender_aliases_allowed)) {
|
||||||
|
// Skip: already allowed by sender_allowed=1, no need for sender_acl entry
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add to sender_acl (either override for blocked aliases, or grant for selectable ones)
|
||||||
$stmt = $pdo->prepare("INSERT INTO `sender_acl` (`send_as`, `logged_in_as`)
|
$stmt = $pdo->prepare("INSERT INTO `sender_acl` (`send_as`, `logged_in_as`)
|
||||||
VALUES (:sender_acl, :username)");
|
VALUES (:sender_acl, :username)");
|
||||||
$stmt->execute(array(
|
$stmt->execute(array(
|
||||||
@@ -3329,12 +3456,15 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
|||||||
`quota` = :quota_b,
|
`quota` = :quota_b,
|
||||||
`authsource` = :authsource,
|
`authsource` = :authsource,
|
||||||
`attributes` = JSON_SET(`attributes`, '$.force_pw_update', :force_pw_update),
|
`attributes` = JSON_SET(`attributes`, '$.force_pw_update', :force_pw_update),
|
||||||
|
`attributes` = JSON_SET(`attributes`, '$.force_tfa', :force_tfa),
|
||||||
`attributes` = JSON_SET(`attributes`, '$.sogo_access', :sogo_access),
|
`attributes` = JSON_SET(`attributes`, '$.sogo_access', :sogo_access),
|
||||||
`attributes` = JSON_SET(`attributes`, '$.imap_access', :imap_access),
|
`attributes` = JSON_SET(`attributes`, '$.imap_access', :imap_access),
|
||||||
`attributes` = JSON_SET(`attributes`, '$.sieve_access', :sieve_access),
|
`attributes` = JSON_SET(`attributes`, '$.sieve_access', :sieve_access),
|
||||||
`attributes` = JSON_SET(`attributes`, '$.pop3_access', :pop3_access),
|
`attributes` = JSON_SET(`attributes`, '$.pop3_access', :pop3_access),
|
||||||
`attributes` = JSON_SET(`attributes`, '$.relayhost', :relayhost),
|
`attributes` = JSON_SET(`attributes`, '$.relayhost', :relayhost),
|
||||||
`attributes` = JSON_SET(`attributes`, '$.smtp_access', :smtp_access),
|
`attributes` = JSON_SET(`attributes`, '$.smtp_access', :smtp_access),
|
||||||
|
`attributes` = JSON_SET(`attributes`, '$.eas_access', :eas_access),
|
||||||
|
`attributes` = JSON_SET(`attributes`, '$.dav_access', :dav_access),
|
||||||
`attributes` = JSON_SET(`attributes`, '$.recovery_email', :recovery_email),
|
`attributes` = JSON_SET(`attributes`, '$.recovery_email', :recovery_email),
|
||||||
`attributes` = JSON_SET(`attributes`, '$.attribute_hash', :attribute_hash)
|
`attributes` = JSON_SET(`attributes`, '$.attribute_hash', :attribute_hash)
|
||||||
WHERE `username` = :username");
|
WHERE `username` = :username");
|
||||||
@@ -3344,11 +3474,14 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
|||||||
':quota_b' => $quota_b,
|
':quota_b' => $quota_b,
|
||||||
':attribute_hash' => $attribute_hash,
|
':attribute_hash' => $attribute_hash,
|
||||||
':force_pw_update' => $force_pw_update,
|
':force_pw_update' => $force_pw_update,
|
||||||
|
':force_tfa' => $force_tfa,
|
||||||
':sogo_access' => $sogo_access,
|
':sogo_access' => $sogo_access,
|
||||||
':imap_access' => $imap_access,
|
':imap_access' => $imap_access,
|
||||||
':pop3_access' => $pop3_access,
|
':pop3_access' => $pop3_access,
|
||||||
':sieve_access' => $sieve_access,
|
':sieve_access' => $sieve_access,
|
||||||
':smtp_access' => $smtp_access,
|
':smtp_access' => $smtp_access,
|
||||||
|
':eas_access' => $eas_access,
|
||||||
|
':dav_access' => $dav_access,
|
||||||
':recovery_email' => $pw_recovery_email,
|
':recovery_email' => $pw_recovery_email,
|
||||||
':relayhost' => $relayhost,
|
':relayhost' => $relayhost,
|
||||||
':username' => $username,
|
':username' => $username,
|
||||||
@@ -3397,17 +3530,9 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
|||||||
'msg' => array('mailbox_modified', $username)
|
'msg' => array('mailbox_modified', $username)
|
||||||
);
|
);
|
||||||
|
|
||||||
try {
|
// Track affected mailboxes for SOGo update
|
||||||
update_sogo_static_view($username);
|
$update_sogo_mailboxes[] = $username;
|
||||||
} catch (PDOException $e) {
|
|
||||||
$_SESSION['return'][] = array(
|
|
||||||
'type' => 'danger',
|
|
||||||
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
|
||||||
'msg' => $e->getMessage()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return true;
|
|
||||||
break;
|
break;
|
||||||
case 'mailbox_rename':
|
case 'mailbox_rename':
|
||||||
$domain = $_data['domain'];
|
$domain = $_data['domain'];
|
||||||
@@ -3718,9 +3843,19 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
|||||||
$attr["tagged_mail_handler"] = (!empty($_data['tagged_mail_handler'])) ? $_data['tagged_mail_handler'] : $is_now['tagged_mail_handler'];
|
$attr["tagged_mail_handler"] = (!empty($_data['tagged_mail_handler'])) ? $_data['tagged_mail_handler'] : $is_now['tagged_mail_handler'];
|
||||||
$attr["quarantine_notification"] = (!empty($_data['quarantine_notification'])) ? $_data['quarantine_notification'] : $is_now['quarantine_notification'];
|
$attr["quarantine_notification"] = (!empty($_data['quarantine_notification'])) ? $_data['quarantine_notification'] : $is_now['quarantine_notification'];
|
||||||
$attr["quarantine_category"] = (!empty($_data['quarantine_category'])) ? $_data['quarantine_category'] : $is_now['quarantine_category'];
|
$attr["quarantine_category"] = (!empty($_data['quarantine_category'])) ? $_data['quarantine_category'] : $is_now['quarantine_category'];
|
||||||
|
// Validate quarantine_category
|
||||||
|
if (!in_array($attr["quarantine_category"], array('add_header', 'reject', 'all'))) {
|
||||||
|
$_SESSION['return'][] = array(
|
||||||
|
'type' => 'danger',
|
||||||
|
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_extra),
|
||||||
|
'msg' => 'quarantine_category_invalid'
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
$attr["rl_frame"] = (!empty($_data['rl_frame'])) ? $_data['rl_frame'] : $is_now['rl_frame'];
|
$attr["rl_frame"] = (!empty($_data['rl_frame'])) ? $_data['rl_frame'] : $is_now['rl_frame'];
|
||||||
$attr["rl_value"] = (!empty($_data['rl_value'])) ? $_data['rl_value'] : $is_now['rl_value'];
|
$attr["rl_value"] = (!empty($_data['rl_value'])) ? $_data['rl_value'] : $is_now['rl_value'];
|
||||||
$attr["force_pw_update"] = isset($_data['force_pw_update']) ? intval($_data['force_pw_update']) : $is_now['force_pw_update'];
|
$attr["force_pw_update"] = isset($_data['force_pw_update']) ? intval($_data['force_pw_update']) : $is_now['force_pw_update'];
|
||||||
|
$attr["force_tfa"] = isset($_data['force_tfa']) ? intval($_data['force_tfa']) : $is_now['force_tfa'];
|
||||||
$attr["sogo_access"] = isset($_data['sogo_access']) ? intval($_data['sogo_access']) : $is_now['sogo_access'];
|
$attr["sogo_access"] = isset($_data['sogo_access']) ? intval($_data['sogo_access']) : $is_now['sogo_access'];
|
||||||
$attr["active"] = isset($_data['active']) ? intval($_data['active']) : $is_now['active'];
|
$attr["active"] = isset($_data['active']) ? intval($_data['active']) : $is_now['active'];
|
||||||
$attr["tls_enforce_in"] = isset($_data['tls_enforce_in']) ? intval($_data['tls_enforce_in']) : $is_now['tls_enforce_in'];
|
$attr["tls_enforce_in"] = isset($_data['tls_enforce_in']) ? intval($_data['tls_enforce_in']) : $is_now['tls_enforce_in'];
|
||||||
@@ -3731,6 +3866,8 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
|||||||
$attr['pop3_access'] = (in_array('pop3', $_data['protocol_access'])) ? 1 : 0;
|
$attr['pop3_access'] = (in_array('pop3', $_data['protocol_access'])) ? 1 : 0;
|
||||||
$attr['smtp_access'] = (in_array('smtp', $_data['protocol_access'])) ? 1 : 0;
|
$attr['smtp_access'] = (in_array('smtp', $_data['protocol_access'])) ? 1 : 0;
|
||||||
$attr['sieve_access'] = (in_array('sieve', $_data['protocol_access'])) ? 1 : 0;
|
$attr['sieve_access'] = (in_array('sieve', $_data['protocol_access'])) ? 1 : 0;
|
||||||
|
$attr['eas_access'] = (in_array('eas', $_data['protocol_access'])) ? 1 : 0;
|
||||||
|
$attr['dav_access'] = (in_array('dav', $_data['protocol_access'])) ? 1 : 0;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
foreach ($is_now as $key => $value){
|
foreach ($is_now as $key => $value){
|
||||||
@@ -4032,6 +4169,9 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
|||||||
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
||||||
'msg' => array('resource_modified', htmlspecialchars($name))
|
'msg' => array('resource_modified', htmlspecialchars($name))
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Track affected mailboxes for SOGo update
|
||||||
|
$update_sogo_mailboxes[] = $name;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'domain_wide_footer':
|
case 'domain_wide_footer':
|
||||||
@@ -4160,13 +4300,22 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
|||||||
$data['sender_acl_addresses']['rw'] = array();
|
$data['sender_acl_addresses']['rw'] = array();
|
||||||
$data['sender_acl_addresses']['selectable'] = array();
|
$data['sender_acl_addresses']['selectable'] = array();
|
||||||
$data['fixed_sender_aliases'] = array();
|
$data['fixed_sender_aliases'] = array();
|
||||||
|
$data['fixed_sender_aliases_allowed'] = array();
|
||||||
|
$data['fixed_sender_aliases_blocked'] = array();
|
||||||
$data['external_sender_aliases'] = array();
|
$data['external_sender_aliases'] = array();
|
||||||
// Fixed addresses
|
// Fixed addresses - split by sender_allowed status
|
||||||
$stmt = $pdo->prepare("SELECT `address` FROM `alias` WHERE `goto` REGEXP :goto AND `address` NOT LIKE '@%'");
|
$stmt = $pdo->prepare("SELECT `address`, `sender_allowed` FROM `alias` WHERE `goto` REGEXP :goto AND `address` NOT LIKE '@%'");
|
||||||
$stmt->execute(array(':goto' => '(^|,)'.preg_quote($_data, '/').'($|,)'));
|
$stmt->execute(array(':goto' => '(^|,)'.preg_quote($_data, '/').'($|,)'));
|
||||||
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
while ($row = array_shift($rows)) {
|
while ($row = array_shift($rows)) {
|
||||||
|
// Keep old array for backward compatibility
|
||||||
$data['fixed_sender_aliases'][] = $row['address'];
|
$data['fixed_sender_aliases'][] = $row['address'];
|
||||||
|
// Split into allowed/blocked for proper display
|
||||||
|
if ($row['sender_allowed'] == '1') {
|
||||||
|
$data['fixed_sender_aliases_allowed'][] = $row['address'];
|
||||||
|
} else {
|
||||||
|
$data['fixed_sender_aliases_blocked'][] = $row['address'];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
$stmt = $pdo->prepare("SELECT CONCAT(`local_part`, '@', `alias_domain`.`alias_domain`) AS `alias_domain_alias` FROM `mailbox`, `alias_domain`
|
$stmt = $pdo->prepare("SELECT CONCAT(`local_part`, '@', `alias_domain`.`alias_domain`) AS `alias_domain_alias` FROM `mailbox`, `alias_domain`
|
||||||
WHERE `alias_domain`.`target_domain` = `mailbox`.`domain`
|
WHERE `alias_domain`.`target_domain` = `mailbox`.`domain`
|
||||||
@@ -4726,6 +4875,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
|||||||
`internal`,
|
`internal`,
|
||||||
`active`,
|
`active`,
|
||||||
`sogo_visible`,
|
`sogo_visible`,
|
||||||
|
`sender_allowed`,
|
||||||
`created`,
|
`created`,
|
||||||
`modified`
|
`modified`
|
||||||
FROM `alias`
|
FROM `alias`
|
||||||
@@ -4759,6 +4909,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
|||||||
$aliasdata['active_int'] = $row['active'];
|
$aliasdata['active_int'] = $row['active'];
|
||||||
$aliasdata['sogo_visible'] = $row['sogo_visible'];
|
$aliasdata['sogo_visible'] = $row['sogo_visible'];
|
||||||
$aliasdata['sogo_visible_int'] = $row['sogo_visible'];
|
$aliasdata['sogo_visible_int'] = $row['sogo_visible'];
|
||||||
|
$aliasdata['sender_allowed'] = $row['sender_allowed'];
|
||||||
$aliasdata['created'] = $row['created'];
|
$aliasdata['created'] = $row['created'];
|
||||||
$aliasdata['modified'] = $row['modified'];
|
$aliasdata['modified'] = $row['modified'];
|
||||||
if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $aliasdata['domain'])) {
|
if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $aliasdata['domain'])) {
|
||||||
@@ -5197,6 +5348,14 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
|||||||
$mailboxdata['rl_scope'] = 'domain';
|
$mailboxdata['rl_scope'] = 'domain';
|
||||||
}
|
}
|
||||||
$mailboxdata['is_relayed'] = $row['backupmx'];
|
$mailboxdata['is_relayed'] = $row['backupmx'];
|
||||||
|
// Internal send-as ACL for this mailbox, mirrors the sender_acl field accepted by edit/mailbox
|
||||||
|
$mailboxdata['sender_acl'] = array();
|
||||||
|
$stmt = $pdo->prepare("SELECT `send_as` FROM `sender_acl` WHERE `logged_in_as` = :username AND `external` = '0'");
|
||||||
|
$stmt->execute(array(':username' => $_data));
|
||||||
|
$sender_acl_rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
foreach ($sender_acl_rows as $sender_acl_row) {
|
||||||
|
$mailboxdata['sender_acl'][] = $sender_acl_row['send_as'];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
$stmt = $pdo->prepare("SELECT `tag_name`
|
$stmt = $pdo->prepare("SELECT `tag_name`
|
||||||
FROM `tags_mailbox` WHERE `username`= :username");
|
FROM `tags_mailbox` WHERE `username`= :username");
|
||||||
@@ -5524,8 +5683,11 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
|||||||
$stmt->execute(array(
|
$stmt->execute(array(
|
||||||
':username' => $username
|
':username' => $username
|
||||||
));
|
));
|
||||||
$stmt = $pdo->prepare("DELETE FROM `sogo_acl` WHERE `c_object` LIKE '%/" . $username . "/%' OR `c_uid` = :username");
|
// bind LIKE pattern + escape LIKE wildcards so the value matches literally (no SQLi, no over-match)
|
||||||
|
$c_object_like = '%/' . addcslashes($username, '\\%_') . '/%';
|
||||||
|
$stmt = $pdo->prepare("DELETE FROM `sogo_acl` WHERE `c_object` LIKE :c_object_like OR `c_uid` = :username");
|
||||||
$stmt->execute(array(
|
$stmt->execute(array(
|
||||||
|
':c_object_like' => $c_object_like,
|
||||||
':username' => $username
|
':username' => $username
|
||||||
));
|
));
|
||||||
$stmt = $pdo->prepare("DELETE FROM `sogo_store` WHERE `c_folder_id` IN (SELECT `c_folder_id` FROM `sogo_folder_info` WHERE `c_path2` = :username)");
|
$stmt = $pdo->prepare("DELETE FROM `sogo_store` WHERE `c_folder_id` IN (SELECT `c_folder_id` FROM `sogo_folder_info` WHERE `c_path2` = :username)");
|
||||||
@@ -5725,6 +5887,18 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
|||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Track affected mailboxes for SOGo update (capture before deletion)
|
||||||
|
if (!empty($alias_data['goto'])) {
|
||||||
|
$gotos = array_map('trim', explode(',', $alias_data['goto']));
|
||||||
|
foreach ($gotos as $g) {
|
||||||
|
if (filter_var($g, FILTER_VALIDATE_EMAIL) &&
|
||||||
|
!in_array($g, array('null@localhost', 'spam@localhost', 'ham@localhost'))) {
|
||||||
|
$update_sogo_mailboxes[] = $g;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$stmt = $pdo->prepare("DELETE FROM `alias` WHERE `id` = :id");
|
$stmt = $pdo->prepare("DELETE FROM `alias` WHERE `id` = :id");
|
||||||
$stmt->execute(array(
|
$stmt->execute(array(
|
||||||
':id' => $alias_data['id']
|
':id' => $alias_data['id']
|
||||||
@@ -5909,8 +6083,11 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
|||||||
$stmt->execute(array(
|
$stmt->execute(array(
|
||||||
':username' => $username
|
':username' => $username
|
||||||
));
|
));
|
||||||
$stmt = $pdo->prepare("DELETE FROM `sogo_acl` WHERE `c_object` LIKE '%/" . str_replace('%', '\%', $username) . "/%' OR `c_uid` = :username");
|
// bind LIKE pattern + escape LIKE wildcards so the value matches literally (no SQLi, no over-match)
|
||||||
|
$c_object_like = '%/' . addcslashes($username, '\\%_') . '/%';
|
||||||
|
$stmt = $pdo->prepare("DELETE FROM `sogo_acl` WHERE `c_object` LIKE :c_object_like OR `c_uid` = :username");
|
||||||
$stmt->execute(array(
|
$stmt->execute(array(
|
||||||
|
':c_object_like' => $c_object_like,
|
||||||
':username' => $username
|
':username' => $username
|
||||||
));
|
));
|
||||||
$stmt = $pdo->prepare("DELETE FROM `sogo_store` WHERE `c_folder_id` IN (SELECT `c_folder_id` FROM `sogo_folder_info` WHERE `c_path2` = :username)");
|
$stmt = $pdo->prepare("DELETE FROM `sogo_store` WHERE `c_folder_id` IN (SELECT `c_folder_id` FROM `sogo_folder_info` WHERE `c_path2` = :username)");
|
||||||
@@ -5983,22 +6160,15 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
|
||||||
update_sogo_static_view($username);
|
|
||||||
}catch (PDOException $e) {
|
|
||||||
$_SESSION['return'][] = array(
|
|
||||||
'type' => 'success',
|
|
||||||
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
|
||||||
'msg' => $e->getMessage()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
$_SESSION['return'][] = array(
|
$_SESSION['return'][] = array(
|
||||||
'type' => 'success',
|
'type' => 'success',
|
||||||
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
||||||
'msg' => array('mailbox_removed', htmlspecialchars($username))
|
'msg' => array('mailbox_removed', htmlspecialchars($username))
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Track affected mailboxes for SOGo update
|
||||||
|
$update_sogo_mailboxes[] = $username;
|
||||||
}
|
}
|
||||||
return true;
|
|
||||||
break;
|
break;
|
||||||
case 'mailbox_templates':
|
case 'mailbox_templates':
|
||||||
if ($_SESSION['mailcow_cc_role'] != "admin") {
|
if ($_SESSION['mailcow_cc_role'] != "admin") {
|
||||||
@@ -6073,8 +6243,11 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
|||||||
$stmt->execute(array(
|
$stmt->execute(array(
|
||||||
':username' => $name
|
':username' => $name
|
||||||
));
|
));
|
||||||
$stmt = $pdo->prepare("DELETE FROM `sogo_acl` WHERE `c_object` LIKE '%/" . $name . "/%' OR `c_uid` = :username");
|
// bind LIKE pattern + escape LIKE wildcards so the value matches literally (no SQLi, no over-match)
|
||||||
|
$c_object_like = '%/' . addcslashes($name, '\\%_') . '/%';
|
||||||
|
$stmt = $pdo->prepare("DELETE FROM `sogo_acl` WHERE `c_object` LIKE :c_object_like OR `c_uid` = :username");
|
||||||
$stmt->execute(array(
|
$stmt->execute(array(
|
||||||
|
':c_object_like' => $c_object_like,
|
||||||
':username' => $name
|
':username' => $name
|
||||||
));
|
));
|
||||||
$stmt = $pdo->prepare("DELETE FROM `sogo_store` WHERE `c_folder_id` IN (SELECT `c_folder_id` FROM `sogo_folder_info` WHERE `c_path2` = :username)");
|
$stmt = $pdo->prepare("DELETE FROM `sogo_store` WHERE `c_folder_id` IN (SELECT `c_folder_id` FROM `sogo_folder_info` WHERE `c_path2` = :username)");
|
||||||
@@ -6098,6 +6271,9 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
|||||||
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
||||||
'msg' => array('resource_removed', htmlspecialchars($name))
|
'msg' => array('resource_removed', htmlspecialchars($name))
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Track affected mailboxes for SOGo update
|
||||||
|
$update_sogo_mailboxes[] = $name;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'tags_domain':
|
case 'tags_domain':
|
||||||
@@ -6204,9 +6380,21 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if ($_action != 'get' && in_array($_type, array('domain', 'alias', 'alias_domain', 'resource')) && getenv('SKIP_SOGO') != "y") {
|
if ($_action != 'get' && in_array($_type, array('domain', 'alias', 'alias_domain', 'resource', 'mailbox')) && getenv('SKIP_SOGO') != "y") {
|
||||||
try {
|
try {
|
||||||
update_sogo_static_view();
|
if (($_type == 'alias' || $_type == 'resource' || $_type == 'mailbox') && !empty($update_sogo_mailboxes)) {
|
||||||
|
// INCREMENTAL UPDATE: Update only affected mailboxes/resources
|
||||||
|
$update_sogo_mailboxes = array_unique($update_sogo_mailboxes);
|
||||||
|
foreach ($update_sogo_mailboxes as $mailbox) {
|
||||||
|
update_sogo_static_view($mailbox);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// FULL REBUILD: For domain and alias_domain operations or if no tracked mailboxes
|
||||||
|
// Domain operations affect all mailboxes
|
||||||
|
// Alias_domain operations affect entire target domain
|
||||||
|
update_sogo_static_view();
|
||||||
|
}
|
||||||
}catch (PDOException $e) {
|
}catch (PDOException $e) {
|
||||||
$_SESSION['return'][] = array(
|
$_SESSION['return'][] = array(
|
||||||
'type' => 'success',
|
'type' => 'success',
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ function quarantine($_action, $_data = null) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
$stmt = $pdo->prepare('SELECT `id` FROM `quarantine` LEFT OUTER JOIN `user_acl` ON `user_acl`.`username` = `rcpt`
|
$stmt = $pdo->prepare('SELECT `id` FROM `quarantine` LEFT OUTER JOIN `user_acl` ON `user_acl`.`username` = `rcpt`
|
||||||
WHERE `qhash` = :hash
|
WHERE `qhash` = :hash
|
||||||
AND user_acl.quarantine = 1
|
AND user_acl.quarantine = 1
|
||||||
AND rcpt IN (SELECT username FROM mailbox)');
|
AND rcpt IN (SELECT username FROM mailbox)');
|
||||||
$stmt->execute(array(':hash' => $hash));
|
$stmt->execute(array(':hash' => $hash));
|
||||||
@@ -65,7 +65,7 @@ function quarantine($_action, $_data = null) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
$stmt = $pdo->prepare('SELECT `id` FROM `quarantine` LEFT OUTER JOIN `user_acl` ON `user_acl`.`username` = `rcpt`
|
$stmt = $pdo->prepare('SELECT `id` FROM `quarantine` LEFT OUTER JOIN `user_acl` ON `user_acl`.`username` = `rcpt`
|
||||||
WHERE `qhash` = :hash
|
WHERE `qhash` = :hash
|
||||||
AND `user_acl`.`quarantine` = 1
|
AND `user_acl`.`quarantine` = 1
|
||||||
AND `username` IN (SELECT `username` FROM `mailbox`)');
|
AND `username` IN (SELECT `username` FROM `mailbox`)');
|
||||||
$stmt->execute(array(':hash' => $hash));
|
$stmt->execute(array(':hash' => $hash));
|
||||||
@@ -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)),
|
||||||
@@ -180,7 +182,7 @@ function quarantine($_action, $_data = null) {
|
|||||||
array('221', '')
|
array('221', '')
|
||||||
);
|
);
|
||||||
// Thanks to https://stackoverflow.com/questions/6632399/given-an-email-as-raw-text-how-can-i-send-it-using-php
|
// Thanks to https://stackoverflow.com/questions/6632399/given-an-email-as-raw-text-how-can-i-send-it-using-php
|
||||||
$smtp_connection = fsockopen($postfix, 590, $errno, $errstr, 1);
|
$smtp_connection = fsockopen($postfix, 590, $errno, $errstr, 1);
|
||||||
if (!$smtp_connection) {
|
if (!$smtp_connection) {
|
||||||
logger(array('return' => array(
|
logger(array('return' => array(
|
||||||
array(
|
array(
|
||||||
@@ -192,7 +194,7 @@ function quarantine($_action, $_data = null) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
for ($i=0; $i < count($postfix_talk); $i++) {
|
for ($i=0; $i < count($postfix_talk); $i++) {
|
||||||
$smtp_resource = fgets($smtp_connection, 256);
|
$smtp_resource = fgets($smtp_connection, 256);
|
||||||
if (substr($smtp_resource, 0, 3) !== $postfix_talk[$i][0]) {
|
if (substr($smtp_resource, 0, 3) !== $postfix_talk[$i][0]) {
|
||||||
$ret = substr($smtp_resource, 0, 3);
|
$ret = substr($smtp_resource, 0, 3);
|
||||||
$ret = (empty($ret)) ? '-' : $ret;
|
$ret = (empty($ret)) ? '-' : $ret;
|
||||||
@@ -465,17 +467,19 @@ 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', '')
|
||||||
);
|
);
|
||||||
// Thanks to https://stackoverflow.com/questions/6632399/given-an-email-as-raw-text-how-can-i-send-it-using-php
|
// Thanks to https://stackoverflow.com/questions/6632399/given-an-email-as-raw-text-how-can-i-send-it-using-php
|
||||||
$smtp_connection = fsockopen($postfix, 590, $errno, $errstr, 1);
|
$smtp_connection = fsockopen($postfix, 590, $errno, $errstr, 1);
|
||||||
if (!$smtp_connection) {
|
if (!$smtp_connection) {
|
||||||
$_SESSION['return'][] = array(
|
$_SESSION['return'][] = array(
|
||||||
'type' => 'warning',
|
'type' => 'warning',
|
||||||
@@ -485,7 +489,7 @@ function quarantine($_action, $_data = null) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
for ($i=0; $i < count($postfix_talk); $i++) {
|
for ($i=0; $i < count($postfix_talk); $i++) {
|
||||||
$smtp_resource = fgets($smtp_connection, 256);
|
$smtp_resource = fgets($smtp_connection, 256);
|
||||||
if (substr($smtp_resource, 0, 3) !== $postfix_talk[$i][0]) {
|
if (substr($smtp_resource, 0, 3) !== $postfix_talk[$i][0]) {
|
||||||
$ret = substr($smtp_resource, 0, 3);
|
$ret = substr($smtp_resource, 0, 3);
|
||||||
$ret = (empty($ret)) ? '-' : $ret;
|
$ret = (empty($ret)) ? '-' : $ret;
|
||||||
@@ -833,7 +837,7 @@ function quarantine($_action, $_data = null) {
|
|||||||
)));
|
)));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
$stmt = $pdo->prepare('SELECT * FROM `quarantine` WHERE `qhash` = :hash');
|
$stmt = $pdo->prepare('SELECT * FROM `quarantine` WHERE `qhash` = :hash');
|
||||||
$stmt->execute(array(':hash' => $hash));
|
$stmt->execute(array(':hash' => $hash));
|
||||||
return $stmt->fetch(PDO::FETCH_ASSOC);
|
return $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -22,6 +22,9 @@ if (preg_match("/debug/i", $_SERVER['REQUEST_URI'])) {
|
|||||||
if ($_SERVER['REQUEST_URI'] == '/') {
|
if ($_SERVER['REQUEST_URI'] == '/') {
|
||||||
$css_minifier->add('/web/css/site/index.css');
|
$css_minifier->add('/web/css/site/index.css');
|
||||||
}
|
}
|
||||||
|
if (file_exists('/web/css/themes/'.$UI_THEME.'/theme.css')) {
|
||||||
|
$css_minifier->add('/web/css/themes/'.$UI_THEME.'/theme.css');
|
||||||
|
}
|
||||||
|
|
||||||
$hash = $css_minifier->getDataHash();
|
$hash = $css_minifier->getDataHash();
|
||||||
$CSSPath = '/tmp/' . $hash . '.css';
|
$CSSPath = '/tmp/' . $hash . '.css';
|
||||||
@@ -30,6 +33,17 @@ if(!file_exists($CSSPath)) {
|
|||||||
cleanupCSS($hash);
|
cleanupCSS($hash);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// dark mode bundle, core dark mode + theme dark.css
|
||||||
|
$dark_minifier = new CSSminifierExtended('/web/css/themes/mailcow-darkmode.css');
|
||||||
|
if (file_exists('/web/css/themes/'.$UI_THEME.'/dark.css')) {
|
||||||
|
$dark_minifier->add('/web/css/themes/'.$UI_THEME.'/dark.css');
|
||||||
|
}
|
||||||
|
$DarkCSSPath = '/tmp/' . $hash . '-' . $dark_minifier->getDataHash() . '.css';
|
||||||
|
if(!file_exists($DarkCSSPath)) {
|
||||||
|
$dark_minifier->minify($DarkCSSPath);
|
||||||
|
}
|
||||||
|
$dark_css_path = '/cache/' . basename($DarkCSSPath);
|
||||||
|
|
||||||
$mailcow_apps_processed = $MAILCOW_APPS;
|
$mailcow_apps_processed = $MAILCOW_APPS;
|
||||||
$app_links = customize('get', 'app_links');
|
$app_links = customize('get', 'app_links');
|
||||||
$app_links_processed = $app_links;
|
$app_links_processed = $app_links;
|
||||||
@@ -77,6 +91,7 @@ $globalVariables = [
|
|||||||
'dual_login' => @$_SESSION['dual-login'],
|
'dual_login' => @$_SESSION['dual-login'],
|
||||||
'ui_texts' => $UI_TEXTS,
|
'ui_texts' => $UI_TEXTS,
|
||||||
'css_path' => '/cache/'.basename($CSSPath),
|
'css_path' => '/cache/'.basename($CSSPath),
|
||||||
|
'dark_css_path' => $dark_css_path,
|
||||||
'logo' => customize('get', 'main_logo'),
|
'logo' => customize('get', 'main_logo'),
|
||||||
'logo_dark' => customize('get', 'main_logo_dark'),
|
'logo_dark' => customize('get', 'main_logo_dark'),
|
||||||
'available_languages' => $AVAILABLE_LANGUAGES,
|
'available_languages' => $AVAILABLE_LANGUAGES,
|
||||||
@@ -89,7 +104,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) {
|
||||||
|
|||||||
Loaded 100 of 505 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user