Compare commits
351 Commits
Author | SHA1 | Date |
---|---|---|
|
d6c3c58f42 | |
|
b050cb9864 | |
|
e176724775 | |
|
8f9ed9e0df | |
|
003eecf131 | |
|
180b9fc8d2 | |
|
5d3491c801 | |
|
c45684b986 | |
|
5c886d2f4e | |
|
9f39af46aa | |
|
7cda9f063f | |
|
5e7583c5e6 | |
|
a1fb962215 | |
|
57d849a51b | |
|
3000da6b88 | |
|
db75cbbcb0 | |
|
22acbb6b57 | |
|
31cb0f7db1 | |
|
6d17b9f504 | |
|
0f337971ff | |
|
6cf2775e7e | |
|
dabf9104ed | |
|
952ddb18fd | |
|
34d990a800 | |
|
020cb21b35 | |
|
525364ba65 | |
|
731fabef58 | |
|
c10be77a1b | |
|
a8bc4e3f37 | |
|
815572f200 | |
|
23fc54f2cf | |
|
11407973b1 | |
|
b9867e3fe0 | |
|
3814c3294f | |
|
9c44b5e546 | |
|
cd635ec813 | |
|
03831149f8 | |
|
521120a448 | |
|
ec8d298c36 | |
|
03580cbf39 | |
|
2b009c71c1 | |
|
b903cf3888 | |
|
cf239dd6b2 | |
|
a0723f60d2 | |
|
da8e496430 | |
|
722134e474 | |
|
cb1a11e551 | |
|
8984509f58 | |
|
0f0d43b253 | |
|
0f6956572e | |
|
29892dc694 | |
|
14265f3de8 | |
|
0863bffdd2 | |
|
3b748a30cc | |
|
5619175108 | |
|
6e9c024b3c | |
|
8cd4ae1e34 | |
|
689856b186 | |
|
7b645303d6 | |
|
408381bddb | |
|
380cdab6fc | |
|
03b7a8d639 | |
|
bf6a61fa2d | |
|
1de47072f8 | |
|
c0c46b7cf5 | |
|
42a91af7ac | |
|
6e1ee638ff | |
|
61c8afa088 | |
|
c873a14127 | |
|
06cce79806 | |
|
0927c5df57 | |
|
e691d2c782 | |
|
67510adb9e | |
|
490d553dfc | |
|
70aab7568e | |
|
f82aba3e26 | |
|
f80940efdc | |
|
6f875398c0 | |
|
7a582afbdc | |
|
38cd376228 | |
|
74bcec45f1 | |
|
9700b3251f | |
|
88b8d50cd5 | |
|
55b0191050 | |
|
33c97fb318 | |
|
23d33ad5a8 | |
|
bd6c98047a | |
|
73d6a29ae1 | |
|
173e39c859 | |
|
c0745c5cde | |
|
1a6f93327e | |
|
3c68a53170 | |
|
e38c27ed67 | |
|
8eaf8bbbde | |
|
e015c7dbca | |
|
58452abcdf | |
|
2cbf0da137 | |
|
97a492b891 | |
|
aabcd10539 | |
|
ee607dc3cc | |
|
1265302a8e | |
|
b5acf56e20 | |
|
9752313d24 | |
|
fe4a418af4 | |
|
e5f03e8526 | |
|
fb60c4a150 | |
|
6b82284a41 | |
|
192f67cd41 | |
|
fd203abd47 | |
|
6b65f0fc74 | |
|
856b3b62f2 | |
|
0372a2150d | |
|
f3322c0577 | |
|
c2bcc4e086 | |
|
6e79c48640 | |
|
d7dfa95e1b | |
|
cf1cc24e33 | |
|
6824a5650f | |
|
73570cc8b5 | |
|
959dcb9980 | |
|
8f28666916 | |
|
3eaa5a626c | |
|
8c79056a94 | |
|
ed076dc23e | |
|
be2286c11c | |
|
0e24c3d300 | |
|
e1d8df6580 | |
|
04a08a7d69 | |
|
3c0c8aa01f | |
|
026b278357 | |
|
4121509ceb | |
|
00ac61f0a4 | |
|
4bb0dbb2f7 | |
|
13b6df74af | |
|
5c025bf865 | |
|
20fc9eaf84 | |
|
22a0479fab | |
|
3510d5617d | |
|
236d627fbd | |
|
99739eada0 | |
|
7bfef57894 | |
|
d9dfe15253 | |
|
3fe8aaa719 | |
|
78a8fac6af | |
|
6986e7758f | |
|
b4a9df76b8 | |
|
d9d958356a | |
|
96f954a4e2 | |
|
44585e1c15 | |
|
c737ff4180 | |
|
025279009d | |
|
a9dc13d567 | |
|
c3ed01c9b5 | |
|
bd0b4a521e | |
|
800a0ace71 | |
|
db97869472 | |
|
f681fcf154 | |
|
db1b5956fc | |
|
bdb07061ed | |
|
428b917579 | |
|
469f959e96 | |
|
b68e189d97 | |
|
028ef22878 | |
|
80dacc015a | |
|
0194c39bd5 | |
|
f53ca24bb0 | |
|
ae46a877d3 | |
|
400939faf6 | |
|
fd0205aafd | |
|
e367a8ce24 | |
|
096e2a41e9 | |
|
e010f08143 | |
|
3d2483ca37 | |
|
535dd23509 | |
|
4336a99c6a | |
|
4cd5f93cdf | |
|
67955779b0 | |
|
26c34b484a | |
|
4021613059 | |
|
e891bf8411 | |
|
f7798d1aac | |
|
d11f00261b | |
|
22cd12f37b | |
|
db2fb12837 | |
|
e808e595eb | |
|
ce6742c676 | |
|
cf3dc584d0 | |
|
62f3603588 | |
|
9fd4aa93e9 | |
|
5bc3d93545 | |
|
c28a6b89f0 | |
|
1233613bea | |
|
0206e0886c | |
|
f6d135fbad | |
|
f7da314dcf | |
|
e6ce5e88f7 | |
|
e5e6418be8 | |
|
6507b53bbb | |
|
0f59d4952b | |
|
7225bd2f55 | |
|
deb2b80352 | |
|
ad9dee92be | |
|
f36bc16ca7 | |
|
bda5f0ed4a | |
|
cbe1c97a82 | |
|
81fcbdd104 | |
|
1a9294b58f | |
|
310c01aac2 | |
|
229303c1f8 | |
|
fc075bc6b7 | |
|
d04f0257c2 | |
|
d11d356803 | |
|
c54750ef8b | |
|
510ef5196b | |
|
04e46f9f5b | |
|
6c0a5028c0 | |
|
791bbeeb39 | |
|
a5b8f1b7f7 | |
|
af267ff706 | |
|
46cc022590 | |
|
f77c65411d | |
|
1052e13af8 | |
|
11e1502b12 | |
|
02afc45a15 | |
|
3e1cfe0d08 | |
|
d20df7d73e | |
|
a8c61daeaf | |
|
1a4f11209a | |
|
04403aaf70 | |
|
7f0dd7d0d7 | |
|
cd29ad883e | |
|
e1cd719a17 | |
|
15bb331a7d | |
|
6f3179bb8d | |
|
29e5b87207 | |
|
4403bc2d18 | |
|
63e92e0897 | |
|
aa4d8b1f47 | |
|
9054ca18be | |
|
38291d123f | |
|
ca64ff2c0b | |
|
dc85f49961 | |
|
5dca4dac81 | |
|
df8775d4c9 | |
|
2bc663dcd5 | |
|
1071bb8230 | |
|
e437810eca | |
|
e8fd34d31f | |
|
6aebb8352e | |
|
d684e0efc0 | |
|
64ac6a8891 | |
|
72e8180c6b | |
|
d62c275004 | |
|
aa7f562761 | |
|
a1f033e4c1 | |
|
58ddc31db6 | |
|
5bf62481d5 | |
|
6ff3f3f044 | |
|
640f535e99 | |
|
05d1a974eb | |
|
99e38d81b1 | |
|
ed7b384e24 | |
|
5439ea1010 | |
|
b719982504 | |
|
8281d3fa55 | |
|
9ba65a572e | |
|
afddcf7f3b | |
|
294569f5c9 | |
|
ef6452cf55 | |
|
9af40eba10 | |
|
1b3a13ca19 | |
|
71cc607de6 | |
|
2ebd8345df | |
|
f5baeb31c1 | |
|
5abda44bc6 | |
|
520d070081 | |
|
86beba6f5a | |
|
f0d9948aee | |
|
8e3d2f7010 | |
|
fc1c5a505d | |
|
18cb06fbc7 | |
|
1af785a94f | |
|
7626becb38 | |
|
5d5e959729 | |
|
49bbdd064e | |
|
9279ee2e76 | |
|
a76e6b32f7 | |
|
4c6f8c4f60 | |
|
826d32413b | |
|
b6799d9fcb | |
|
8782304e8d | |
|
9c55d46bc6 | |
|
099db33e44 | |
|
5c57df4669 | |
|
152431a7d7 | |
|
36fa5dc633 | |
|
814f4aed15 | |
|
e990856629 | |
|
c97afbfa0b | |
|
93b3e0302a | |
|
27c87de4ed | |
|
028ad4ceb9 | |
|
e501642b8e | |
|
7966f010a2 | |
|
b22f74cb59 | |
|
c928948b15 | |
|
606eaad8f7 | |
|
c44281f62d | |
|
1e98784eee | |
|
dd9296ffc2 | |
|
fc0e6b6efb | |
|
68f5fbf65c | |
|
9727e4084f | |
|
5c2f48e94c | |
|
cb098df743 | |
|
b3c54ed07a | |
|
c601eca25d | |
|
48a13255f3 | |
|
08f93c7d58 | |
|
e5c9752681 | |
|
afa1ed1eff | |
|
072cbe62de | |
|
9fe8bfadf3 | |
|
75e4953070 | |
|
de30650dc7 | |
|
690c34bc1d | |
|
4d2e32ee40 | |
|
02b2988beb | |
|
3f1a5af88b | |
|
850fd85d4d | |
|
24acd42589 | |
|
eaa0dea63b | |
|
dd50bbca9b | |
|
f3f5471ef7 | |
|
516c8ea66c | |
|
48310034e5 | |
|
be35a88f8c | |
|
e67b512499 | |
|
0cf59159cd | |
|
e7a929a947 | |
|
dabf4d4383 | |
|
13bdd4ad0b | |
|
3281b97ea9 | |
|
8070db96e9 | |
|
82c80a9682 | |
|
136cc2e3ff | |
|
eefce62f01 | |
|
240b2c63f6 | |
|
1606658cb1 | |
|
54ba66733e | |
|
f6847e6f8c |
|
@ -1,13 +1,31 @@
|
|||
{
|
||||
"enabled": true,
|
||||
"timezone": "Europe/Berlin",
|
||||
"dependencyDashboard": false,
|
||||
"dependencyDashboard": true,
|
||||
"dependencyDashboardTitle": "Renovate Dashboard",
|
||||
"commitBody": "Signed-off-by: milkmaker <milkmaker@mailcow.de>",
|
||||
"rebaseWhen": "auto",
|
||||
"labels": ["renovate"],
|
||||
"assignees": [
|
||||
"@magiccc"
|
||||
],
|
||||
"baseBranches": ["staging"],
|
||||
"enabledManagers": ["github-actions"]
|
||||
"enabledManagers": ["github-actions", "regex", "docker-compose"],
|
||||
"ignorePaths": [
|
||||
"data\/web\/inc\/lib\/vendor\/matthiasmullie\/minify\/**"
|
||||
],
|
||||
"regexManagers": [
|
||||
{
|
||||
"fileMatch": ["^helper-scripts\/nextcloud.sh$"],
|
||||
"matchStrings": [
|
||||
"#\\srenovate:\\sdatasource=(?<datasource>.*?) depName=(?<depName>.*?)( versioning=(?<versioning>.*?))?( extractVersion=(?<extractVersion>.*?))?\\s.*?_VERSION=(?<currentValue>.*)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"fileMatch": ["(^|/)Dockerfile[^/]*$"],
|
||||
"matchStrings": [
|
||||
"#\\srenovate:\\sdatasource=(?<datasource>.*?) depName=(?<depName>.*?)( versioning=(?<versioning>.*?))?\\s(ENV|ARG) .*?_VERSION=(?<currentValue>.*)\\s"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@ jobs:
|
|||
if: github.event.pull_request.base.ref != 'staging' #check if the target branch is not staging
|
||||
steps:
|
||||
- name: Send message
|
||||
uses: thollander/actions-comment-pull-request@main
|
||||
uses: thollander/actions-comment-pull-request@v2.4.0
|
||||
with:
|
||||
GITHUB_TOKEN: ${{ secrets.CHECKIFPRISSTAGING_ACTION_PAT }}
|
||||
message: |
|
||||
|
|
|
@ -14,7 +14,7 @@ jobs:
|
|||
pull-requests: write
|
||||
steps:
|
||||
- name: Mark/Close Stale Issues and Pull Requests 🗑️
|
||||
uses: actions/stale@v6.0.1
|
||||
uses: actions/stale@v8.0.0
|
||||
with:
|
||||
repo-token: ${{ secrets.STALE_ACTION_PAT }}
|
||||
days-before-stale: 60
|
||||
|
|
|
@ -1,63 +0,0 @@
|
|||
name: mailcow Integration Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "master", "staging" ]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
integration_tests:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Setup Ansible
|
||||
run: |
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
sudo apt-get update
|
||||
sudo apt-get install python3 python3-pip git
|
||||
sudo pip3 install ansible
|
||||
- name: Prepair Test Environment
|
||||
run: |
|
||||
git clone https://github.com/mailcow/mailcow-integration-tests.git --branch $(curl -sL https://api.github.com/repos/mailcow/mailcow-integration-tests/releases/latest | jq -r '.tag_name') --single-branch .
|
||||
./fork_check.sh
|
||||
./ci.sh
|
||||
./ci-pip-requirements.sh
|
||||
env:
|
||||
VAULT_PW: ${{ secrets.MAILCOW_TESTS_VAULT_PW }}
|
||||
VAULT_FILE: ${{ secrets.MAILCOW_TESTS_VAULT_FILE }}
|
||||
- name: Start Integration Test Server
|
||||
run: |
|
||||
./fork_check.sh
|
||||
ansible-playbook mailcow-start-server.yml --diff
|
||||
env:
|
||||
PY_COLORS: '1'
|
||||
ANSIBLE_FORCE_COLOR: '1'
|
||||
ANSIBLE_HOST_KEY_CHECKING: 'false'
|
||||
- name: Setup Integration Test Server
|
||||
run: |
|
||||
./fork_check.sh
|
||||
sleep 30
|
||||
ansible-playbook mailcow-setup-server.yml --private-key id_ssh_rsa --diff
|
||||
env:
|
||||
PY_COLORS: '1'
|
||||
ANSIBLE_FORCE_COLOR: '1'
|
||||
ANSIBLE_HOST_KEY_CHECKING: 'false'
|
||||
- name: Run Integration Tests
|
||||
run: |
|
||||
./fork_check.sh
|
||||
ansible-playbook mailcow-integration-tests.yml --private-key id_ssh_rsa --diff
|
||||
env:
|
||||
PY_COLORS: '1'
|
||||
ANSIBLE_FORCE_COLOR: '1'
|
||||
ANSIBLE_HOST_KEY_CHECKING: 'false'
|
||||
- name: Delete Integration Test Server
|
||||
if: always()
|
||||
run: |
|
||||
./fork_check.sh
|
||||
ansible-playbook mailcow-delete-server.yml --diff
|
||||
env:
|
||||
PY_COLORS: '1'
|
||||
ANSIBLE_FORCE_COLOR: '1'
|
||||
ANSIBLE_HOST_KEY_CHECKING: 'false'
|
|
@ -12,7 +12,7 @@ jobs:
|
|||
with:
|
||||
fetch-depth: 0
|
||||
- name: Run the Action
|
||||
uses: devops-infra/action-pull-request@v0.5.3
|
||||
uses: devops-infra/action-pull-request@v0.5.5
|
||||
with:
|
||||
github_token: ${{ secrets.PRTONIGHTLY_ACTION_PAT }}
|
||||
title: Automatic PR to nightly from ${{ github.event.repository.updated_at}}
|
||||
|
|
|
@ -26,7 +26,7 @@ jobs:
|
|||
password: ${{ secrets.BACKUPIMAGEBUILD_ACTION_DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v3
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
file: data/Dockerfiles/backup/Dockerfile
|
||||
|
|
|
@ -1,20 +0,0 @@
|
|||
name: "Tweet trigger release"
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
tweet:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: "Get Release Tag"
|
||||
run: |
|
||||
RELEASE_TAG=$(curl https://api.github.com/repos/mailcow/mailcow-dockerized/releases/latest | jq -r '.tag_name')
|
||||
- name: Tweet-trigger-publish-release
|
||||
uses: mugi111/tweet-trigger-release@v1.1
|
||||
with:
|
||||
consumer_key: ${{ secrets.CONSUMER_KEY }}
|
||||
consumer_secret: ${{ secrets.CONSUMER_SECRET }}
|
||||
access_token_key: ${{ secrets.ACCESS_TOKEN_KEY }}
|
||||
access_token_secret: ${{ secrets.ACCESS_TOKEN_SECRET }}
|
||||
tweet_body: 'A new mailcow update has just been released! Checkout the GitHub Page for changelog and more informations: https://github.com/mailcow/mailcow-dockerized/releases/latest'
|
|
@ -0,0 +1,39 @@
|
|||
name: Update postscreen_access.cidr
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Monthly
|
||||
- cron: "0 0 1 * *"
|
||||
workflow_dispatch: # Allow to run workflow manually
|
||||
|
||||
permissions:
|
||||
contents: read # to fetch code (actions/checkout)
|
||||
|
||||
|
||||
jobs:
|
||||
Update-postscreen_access_cidr:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Generate postscreen_access.cidr
|
||||
run: |
|
||||
bash helper-scripts/update_postscreen_whitelist.sh
|
||||
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v5
|
||||
with:
|
||||
token: ${{ secrets.mailcow_action_Update_postscreen_access_cidr_pat }}
|
||||
commit-message: update postscreen_access.cidr
|
||||
committer: milkmaker <milkmaker@mailcow.de>
|
||||
author: milkmaker <milkmaker@mailcow.de>
|
||||
signoff: false
|
||||
branch: update/postscreen_access.cidr
|
||||
base: staging
|
||||
delete-branch: true
|
||||
add-paths: |
|
||||
data/conf/postfix/postscreen_access.cidr
|
||||
title: '[Postfix] update postscreen_access.cidr'
|
||||
body: |
|
||||
This PR updates the postscreen_access.cidr using GitHub Actions and [helper-scripts/update_postscreen_whitelist.sh](https://github.com/mailcow/mailcow-dockerized/blob/master/helper-scripts/update_postscreen_whitelist.sh)
|
|
@ -36,6 +36,8 @@ data/conf/postfix/extra.cf
|
|||
data/conf/postfix/sni.map
|
||||
data/conf/postfix/sni.map.db
|
||||
data/conf/postfix/sql
|
||||
data/conf/postfix/dns_blocklists.cf
|
||||
data/conf/postfix/dnsbl_reply.map
|
||||
data/conf/rspamd/custom/*
|
||||
data/conf/rspamd/local.d/*
|
||||
data/conf/rspamd/override.d/*
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
# mailcow: dockerized - 🐮 + 🐋 = 💕
|
||||
|
||||
[](https://github.com/mailcow/mailcow-dockerized/actions/workflows/integration_tests.yml)
|
||||
[](https://translate.mailcow.email/engage/mailcow-dockerized/)
|
||||
[](https://twitter.com/mailcow_email)
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
FROM alpine:3.17
|
||||
|
||||
LABEL maintainer "Andre Peters <andre.peters@servercow.de>"
|
||||
LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>"
|
||||
|
||||
RUN apk upgrade --no-cache \
|
||||
&& apk add --update --no-cache \
|
||||
|
|
|
@ -213,11 +213,13 @@ while true; do
|
|||
done
|
||||
ADDITIONAL_WC_ARR+=('autodiscover' 'autoconfig')
|
||||
|
||||
if [[ ${SKIP_IP_CHECK} != "y" ]]; then
|
||||
# Start IP detection
|
||||
log_f "Detecting IP addresses..."
|
||||
IPV4=$(get_ipv4)
|
||||
IPV6=$(get_ipv6)
|
||||
log_f "OK: ${IPV4}, ${IPV6:-"0000:0000:0000:0000:0000:0000:0000:0000"}"
|
||||
fi
|
||||
|
||||
#########################################
|
||||
# IP and webroot challenge verification #
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
FROM clamav/clamav:1.0_base
|
||||
FROM clamav/clamav:1.0.1-1_base
|
||||
|
||||
LABEL maintainer "André Peters <andre.peters@servercow.de>"
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
FROM alpine:3.17
|
||||
|
||||
LABEL maintainer "Andre Peters <andre.peters@servercow.de>"
|
||||
LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>"
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
|
@ -13,9 +13,13 @@ RUN apk add --update --no-cache python3 \
|
|||
fastapi \
|
||||
uvicorn \
|
||||
aiodocker \
|
||||
redis
|
||||
docker \
|
||||
aioredis
|
||||
RUN mkdir /app/modules
|
||||
|
||||
COPY docker-entrypoint.sh /app/
|
||||
COPY dockerapi.py /app/
|
||||
COPY main.py /app/main.py
|
||||
COPY modules/ /app/modules/
|
||||
|
||||
ENTRYPOINT ["/bin/sh", "/app/docker-entrypoint.sh"]
|
||||
CMD exec python main.py
|
|
@ -6,4 +6,4 @@
|
|||
-subj /CN=dockerapi/O=mailcow \
|
||||
-addext subjectAltName=DNS:dockerapi`
|
||||
|
||||
`uvicorn --host 0.0.0.0 --port 443 --ssl-certfile=/app/dockerapi_cert.pem --ssl-keyfile=/app/dockerapi_key.pem dockerapi:app`
|
||||
exec "$@"
|
||||
|
|
|
@ -1,623 +0,0 @@
|
|||
from fastapi import FastAPI, Response, Request
|
||||
import aiodocker
|
||||
import psutil
|
||||
import sys
|
||||
import re
|
||||
import time
|
||||
import os
|
||||
import json
|
||||
import asyncio
|
||||
import redis
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
containerIds_to_update = []
|
||||
host_stats_isUpdating = False
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
@app.get("/host/stats")
|
||||
async def get_host_update_stats():
|
||||
global host_stats_isUpdating
|
||||
|
||||
if host_stats_isUpdating == False:
|
||||
print("start host stats task")
|
||||
asyncio.create_task(get_host_stats())
|
||||
host_stats_isUpdating = True
|
||||
|
||||
while True:
|
||||
if redis_client.exists('host_stats'):
|
||||
break
|
||||
print("wait for host_stats results")
|
||||
await asyncio.sleep(1.5)
|
||||
|
||||
|
||||
print("host stats pulled")
|
||||
stats = json.loads(redis_client.get('host_stats'))
|
||||
return Response(content=json.dumps(stats, indent=4), media_type="application/json")
|
||||
|
||||
@app.get("/containers/{container_id}/json")
|
||||
async def get_container(container_id : str):
|
||||
if container_id and container_id.isalnum():
|
||||
try:
|
||||
for container in (await async_docker_client.containers.list()):
|
||||
if container._id == container_id:
|
||||
container_info = await container.show()
|
||||
return Response(content=json.dumps(container_info, indent=4), media_type="application/json")
|
||||
|
||||
res = {
|
||||
"type": "danger",
|
||||
"msg": "no container found"
|
||||
}
|
||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||
except Exception as e:
|
||||
res = {
|
||||
"type": "danger",
|
||||
"msg": str(e)
|
||||
}
|
||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||
else:
|
||||
res = {
|
||||
"type": "danger",
|
||||
"msg": "no or invalid id defined"
|
||||
}
|
||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||
|
||||
@app.get("/containers/json")
|
||||
async def get_containers():
|
||||
containers = {}
|
||||
try:
|
||||
for container in (await async_docker_client.containers.list()):
|
||||
container_info = await container.show()
|
||||
containers.update({container_info['Id']: container_info})
|
||||
return Response(content=json.dumps(containers, indent=4), media_type="application/json")
|
||||
except Exception as e:
|
||||
res = {
|
||||
"type": "danger",
|
||||
"msg": str(e)
|
||||
}
|
||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||
|
||||
@app.post("/containers/{container_id}/{post_action}")
|
||||
async def post_containers(container_id : str, post_action : str, request: Request):
|
||||
try :
|
||||
request_json = await request.json()
|
||||
except Exception as err:
|
||||
request_json = {}
|
||||
|
||||
if container_id and container_id.isalnum() and post_action:
|
||||
try:
|
||||
"""Dispatch container_post api call"""
|
||||
if post_action == 'exec':
|
||||
if not request_json or not 'cmd' in request_json:
|
||||
res = {
|
||||
"type": "danger",
|
||||
"msg": "cmd is missing"
|
||||
}
|
||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||
if not request_json or not 'task' in request_json:
|
||||
res = {
|
||||
"type": "danger",
|
||||
"msg": "task is missing"
|
||||
}
|
||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||
|
||||
api_call_method_name = '__'.join(['container_post', str(post_action), str(request_json['cmd']), str(request_json['task']) ])
|
||||
else:
|
||||
api_call_method_name = '__'.join(['container_post', str(post_action) ])
|
||||
|
||||
docker_utils = DockerUtils(async_docker_client)
|
||||
api_call_method = getattr(docker_utils, api_call_method_name, lambda container_id: Response(content=json.dumps({'type': 'danger', 'msg':'container_post - unknown api call' }, indent=4), media_type="application/json"))
|
||||
|
||||
|
||||
print("api call: %s, container_id: %s" % (api_call_method_name, container_id))
|
||||
return await api_call_method(container_id, request_json)
|
||||
except Exception as e:
|
||||
print("error - container_post: %s" % str(e))
|
||||
res = {
|
||||
"type": "danger",
|
||||
"msg": str(e)
|
||||
}
|
||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||
|
||||
else:
|
||||
res = {
|
||||
"type": "danger",
|
||||
"msg": "invalid container id or missing action"
|
||||
}
|
||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||
|
||||
@app.post("/container/{container_id}/stats/update")
|
||||
async def post_container_update_stats(container_id : str):
|
||||
global containerIds_to_update
|
||||
|
||||
# start update task for container if no task is running
|
||||
if container_id not in containerIds_to_update:
|
||||
asyncio.create_task(get_container_stats(container_id))
|
||||
containerIds_to_update.append(container_id)
|
||||
|
||||
while True:
|
||||
if redis_client.exists(container_id + '_stats'):
|
||||
break
|
||||
await asyncio.sleep(1.5)
|
||||
|
||||
stats = json.loads(redis_client.get(container_id + '_stats'))
|
||||
return Response(content=json.dumps(stats, indent=4), media_type="application/json")
|
||||
|
||||
|
||||
|
||||
|
||||
class DockerUtils:
|
||||
def __init__(self, docker_client):
|
||||
self.docker_client = docker_client
|
||||
|
||||
# api call: container_post - post_action: stop
|
||||
async def container_post__stop(self, container_id, request_json):
|
||||
for container in (await self.docker_client.containers.list()):
|
||||
if container._id == container_id:
|
||||
await container.stop()
|
||||
res = {
|
||||
'type': 'success',
|
||||
'msg': 'command completed successfully'
|
||||
}
|
||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||
|
||||
# api call: container_post - post_action: start
|
||||
async def container_post__start(self, container_id, request_json):
|
||||
for container in (await self.docker_client.containers.list()):
|
||||
if container._id == container_id:
|
||||
await container.start()
|
||||
res = {
|
||||
'type': 'success',
|
||||
'msg': 'command completed successfully'
|
||||
}
|
||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||
|
||||
|
||||
# api call: container_post - post_action: restart
|
||||
async def container_post__restart(self, container_id, request_json):
|
||||
for container in (await self.docker_client.containers.list()):
|
||||
if container._id == container_id:
|
||||
await container.restart()
|
||||
res = {
|
||||
'type': 'success',
|
||||
'msg': 'command completed successfully'
|
||||
}
|
||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||
|
||||
|
||||
# api call: container_post - post_action: top
|
||||
async def container_post__top(self, container_id, request_json):
|
||||
for container in (await self.docker_client.containers.list()):
|
||||
if container._id == container_id:
|
||||
ps_exec = await container.exec("ps")
|
||||
async with ps_exec.start(detach=False) as stream:
|
||||
ps_return = await stream.read_out()
|
||||
|
||||
exec_details = await ps_exec.inspect()
|
||||
if exec_details["ExitCode"] == None or exec_details["ExitCode"] == 0:
|
||||
res = {
|
||||
'type': 'success',
|
||||
'msg': ps_return.data.decode('utf-8')
|
||||
}
|
||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||
else:
|
||||
res = {
|
||||
'type': 'danger',
|
||||
'msg': ''
|
||||
}
|
||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||
|
||||
|
||||
# api call: container_post - post_action: exec - cmd: mailq - task: delete
|
||||
async def container_post__exec__mailq__delete(self, container_id, request_json):
|
||||
if 'items' in request_json:
|
||||
r = re.compile("^[0-9a-fA-F]+$")
|
||||
filtered_qids = filter(r.match, request_json['items'])
|
||||
if filtered_qids:
|
||||
flagged_qids = ['-d %s' % i for i in filtered_qids]
|
||||
sanitized_string = str(' '.join(flagged_qids))
|
||||
|
||||
for container in (await self.docker_client.containers.list()):
|
||||
if container._id == container_id:
|
||||
postsuper_r_exec = await container.exec(["/bin/bash", "-c", "/usr/sbin/postsuper " + sanitized_string])
|
||||
return await exec_run_handler('generic', postsuper_r_exec)
|
||||
|
||||
# api call: container_post - post_action: exec - cmd: mailq - task: hold
|
||||
async def container_post__exec__mailq__hold(self, container_id, request_json):
|
||||
if 'items' in request_json:
|
||||
r = re.compile("^[0-9a-fA-F]+$")
|
||||
filtered_qids = filter(r.match, request_json['items'])
|
||||
if filtered_qids:
|
||||
flagged_qids = ['-h %s' % i for i in filtered_qids]
|
||||
sanitized_string = str(' '.join(flagged_qids))
|
||||
|
||||
for container in (await self.docker_client.containers.list()):
|
||||
if container._id == container_id:
|
||||
postsuper_r_exec = await container.exec(["/bin/bash", "-c", "/usr/sbin/postsuper " + sanitized_string])
|
||||
return await exec_run_handler('generic', postsuper_r_exec)
|
||||
|
||||
# api call: container_post - post_action: exec - cmd: mailq - task: cat
|
||||
async def container_post__exec__mailq__cat(self, container_id, request_json):
|
||||
if 'items' in request_json:
|
||||
r = re.compile("^[0-9a-fA-F]+$")
|
||||
filtered_qids = filter(r.match, request_json['items'])
|
||||
if filtered_qids:
|
||||
sanitized_string = str(' '.join(filtered_qids))
|
||||
|
||||
for container in (await self.docker_client.containers.list()):
|
||||
if container._id == container_id:
|
||||
postcat_exec = await container.exec(["/bin/bash", "-c", "/usr/sbin/postcat -q " + sanitized_string], user='postfix')
|
||||
return await exec_run_handler('utf8_text_only', postcat_exec)
|
||||
|
||||
# api call: container_post - post_action: exec - cmd: mailq - task: unhold
|
||||
async def container_post__exec__mailq__unhold(self, container_id, request_json):
|
||||
if 'items' in request_json:
|
||||
r = re.compile("^[0-9a-fA-F]+$")
|
||||
filtered_qids = filter(r.match, request_json['items'])
|
||||
if filtered_qids:
|
||||
flagged_qids = ['-H %s' % i for i in filtered_qids]
|
||||
sanitized_string = str(' '.join(flagged_qids))
|
||||
|
||||
for container in (await self.docker_client.containers.list()):
|
||||
if container._id == container_id:
|
||||
postsuper_r_exec = await container.exec(["/bin/bash", "-c", "/usr/sbin/postsuper " + sanitized_string])
|
||||
return await exec_run_handler('generic', postsuper_r_exec)
|
||||
|
||||
|
||||
# api call: container_post - post_action: exec - cmd: mailq - task: deliver
|
||||
async def container_post__exec__mailq__deliver(self, container_id, request_json):
|
||||
if 'items' in request_json:
|
||||
r = re.compile("^[0-9a-fA-F]+$")
|
||||
filtered_qids = filter(r.match, request_json['items'])
|
||||
if filtered_qids:
|
||||
flagged_qids = ['-i %s' % i for i in filtered_qids]
|
||||
|
||||
for container in (await self.docker_client.containers.list()):
|
||||
if container._id == container_id:
|
||||
for i in flagged_qids:
|
||||
postsuper_r_exec = await container.exec(["/bin/bash", "-c", "/usr/sbin/postqueue " + i], user='postfix')
|
||||
async with postsuper_r_exec.start(detach=False) as stream:
|
||||
postsuper_r_return = await stream.read_out()
|
||||
# todo: check each exit code
|
||||
res = {
|
||||
'type': 'success',
|
||||
'msg': 'Scheduled immediate delivery'
|
||||
}
|
||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||
|
||||
|
||||
# api call: container_post - post_action: exec - cmd: mailq - task: list
|
||||
async def container_post__exec__mailq__list(self, container_id, request_json):
|
||||
for container in (await self.docker_client.containers.list()):
|
||||
if container._id == container_id:
|
||||
mailq_exec = await container.exec(["/usr/sbin/postqueue", "-j"], user='postfix')
|
||||
return await exec_run_handler('utf8_text_only', mailq_exec)
|
||||
|
||||
|
||||
# api call: container_post - post_action: exec - cmd: mailq - task: flush
|
||||
async def container_post__exec__mailq__flush(self, container_id, request_json):
|
||||
for container in (await self.docker_client.containers.list()):
|
||||
if container._id == container_id:
|
||||
postsuper_r_exec = await container.exec(["/usr/sbin/postqueue", "-f"], user='postfix')
|
||||
return await exec_run_handler('generic', postsuper_r_exec)
|
||||
|
||||
|
||||
# api call: container_post - post_action: exec - cmd: mailq - task: super_delete
|
||||
async def container_post__exec__mailq__super_delete(self, container_id, request_json):
|
||||
for container in (await self.docker_client.containers.list()):
|
||||
if container._id == container_id:
|
||||
postsuper_r_exec = await container.exec(["/usr/sbin/postsuper", "-d", "ALL"])
|
||||
return await exec_run_handler('generic', postsuper_r_exec)
|
||||
|
||||
|
||||
# api call: container_post - post_action: exec - cmd: system - task: fts_rescan
|
||||
async def container_post__exec__system__fts_rescan(self, container_id, request_json):
|
||||
if 'username' in request_json:
|
||||
for container in (await self.docker_client.containers.list()):
|
||||
if container._id == container_id:
|
||||
rescan_exec = await container.exec(["/bin/bash", "-c", "/usr/bin/doveadm fts rescan -u '" + request_json['username'].replace("'", "'\\''") + "'"], user='vmail')
|
||||
async with rescan_exec.start(detach=False) as stream:
|
||||
rescan_return = await stream.read_out()
|
||||
|
||||
exec_details = await rescan_exec.inspect()
|
||||
if exec_details["ExitCode"] == None or exec_details["ExitCode"] == 0:
|
||||
res = {
|
||||
'type': 'success',
|
||||
'msg': 'fts_rescan: rescan triggered'
|
||||
}
|
||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||
else:
|
||||
res = {
|
||||
'type': 'warning',
|
||||
'msg': 'fts_rescan error'
|
||||
}
|
||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||
|
||||
if 'all' in request_json:
|
||||
for container in (await self.docker_client.containers.list()):
|
||||
if container._id == container_id:
|
||||
rescan_exec = await container.exec(["/bin/bash", "-c", "/usr/bin/doveadm fts rescan -A"], user='vmail')
|
||||
async with rescan_exec.start(detach=False) as stream:
|
||||
rescan_return = await stream.read_out()
|
||||
|
||||
exec_details = await rescan_exec.inspect()
|
||||
if exec_details["ExitCode"] == None or exec_details["ExitCode"] == 0:
|
||||
res = {
|
||||
'type': 'success',
|
||||
'msg': 'fts_rescan: rescan triggered'
|
||||
}
|
||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||
else:
|
||||
res = {
|
||||
'type': 'warning',
|
||||
'msg': 'fts_rescan error'
|
||||
}
|
||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||
|
||||
|
||||
# api call: container_post - post_action: exec - cmd: system - task: df
|
||||
async def container_post__exec__system__df(self, container_id, request_json):
|
||||
if 'dir' in request_json:
|
||||
for container in (await self.docker_client.containers.list()):
|
||||
if container._id == container_id:
|
||||
df_exec = await container.exec(["/bin/bash", "-c", "/bin/df -H '" + request_json['dir'].replace("'", "'\\''") + "' | /usr/bin/tail -n1 | /usr/bin/tr -s [:blank:] | /usr/bin/tr ' ' ','"], user='nobody')
|
||||
async with df_exec.start(detach=False) as stream:
|
||||
df_return = await stream.read_out()
|
||||
|
||||
print(df_return)
|
||||
print(await df_exec.inspect())
|
||||
exec_details = await df_exec.inspect()
|
||||
if exec_details["ExitCode"] == None or exec_details["ExitCode"] == 0:
|
||||
return df_return.data.decode('utf-8').rstrip()
|
||||
else:
|
||||
return "0,0,0,0,0,0"
|
||||
|
||||
|
||||
# api call: container_post - post_action: exec - cmd: system - task: mysql_upgrade
|
||||
async def container_post__exec__system__mysql_upgrade(self, container_id, request_json):
|
||||
for container in (await self.docker_client.containers.list()):
|
||||
if container._id == container_id:
|
||||
sql_exec = await container.exec(["/bin/bash", "-c", "/usr/bin/mysql_upgrade -uroot -p'" + os.environ['DBROOT'].replace("'", "'\\''") + "'\n"], user='mysql')
|
||||
async with sql_exec.start(detach=False) as stream:
|
||||
sql_return = await stream.read_out()
|
||||
|
||||
exec_details = await sql_exec.inspect()
|
||||
if exec_details["ExitCode"] == None or exec_details["ExitCode"] == 0:
|
||||
matched = False
|
||||
for line in sql_return.data.decode('utf-8').split("\n"):
|
||||
if 'is already upgraded to' in line:
|
||||
matched = True
|
||||
if matched:
|
||||
res = {
|
||||
'type': 'success',
|
||||
'msg': 'mysql_upgrade: already upgraded',
|
||||
'text': sql_return.data.decode('utf-8')
|
||||
}
|
||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||
else:
|
||||
await container.restart()
|
||||
res = {
|
||||
'type': 'warning',
|
||||
'msg': 'mysql_upgrade: upgrade was applied',
|
||||
'text': sql_return.data.decode('utf-8')
|
||||
}
|
||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||
else:
|
||||
res = {
|
||||
'type': 'error',
|
||||
'msg': 'mysql_upgrade: error running command',
|
||||
'text': sql_return.data.decode('utf-8')
|
||||
}
|
||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||
|
||||
# api call: container_post - post_action: exec - cmd: system - task: mysql_tzinfo_to_sql
|
||||
async def container_post__exec__system__mysql_tzinfo_to_sql(self, container_id, request_json):
|
||||
for container in (await self.docker_client.containers.list()):
|
||||
if container._id == container_id:
|
||||
sql_exec = await container.exec(["/bin/bash", "-c", "/usr/bin/mysql_tzinfo_to_sql /usr/share/zoneinfo | /bin/sed 's/Local time zone must be set--see zic manual page/FCTY/' | /usr/bin/mysql -uroot -p'" + os.environ['DBROOT'].replace("'", "'\\''") + "' mysql \n"], user='mysql')
|
||||
async with sql_exec.start(detach=False) as stream:
|
||||
sql_return = await stream.read_out()
|
||||
|
||||
exec_details = await sql_exec.inspect()
|
||||
if exec_details["ExitCode"] == None or exec_details["ExitCode"] == 0:
|
||||
res = {
|
||||
'type': 'info',
|
||||
'msg': 'mysql_tzinfo_to_sql: command completed successfully',
|
||||
'text': sql_return.data.decode('utf-8')
|
||||
}
|
||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||
else:
|
||||
res = {
|
||||
'type': 'error',
|
||||
'msg': 'mysql_tzinfo_to_sql: error running command',
|
||||
'text': sql_return.data.decode('utf-8')
|
||||
}
|
||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||
|
||||
# api call: container_post - post_action: exec - cmd: reload - task: dovecot
|
||||
async def container_post__exec__reload__dovecot(self, container_id, request_json):
|
||||
for container in (await self.docker_client.containers.list()):
|
||||
if container._id == container_id:
|
||||
reload_exec = await container.exec(["/bin/bash", "-c", "/usr/sbin/dovecot reload"])
|
||||
return await exec_run_handler('generic', reload_exec)
|
||||
|
||||
|
||||
# api call: container_post - post_action: exec - cmd: reload - task: postfix
|
||||
async def container_post__exec__reload__postfix(self, container_id, request_json):
|
||||
for container in (await self.docker_client.containers.list()):
|
||||
if container._id == container_id:
|
||||
reload_exec = await container.exec(["/bin/bash", "-c", "/usr/sbin/postfix reload"])
|
||||
return await exec_run_handler('generic', reload_exec)
|
||||
|
||||
|
||||
# api call: container_post - post_action: exec - cmd: reload - task: nginx
|
||||
async def container_post__exec__reload__nginx(self, container_id, request_json):
|
||||
for container in (await self.docker_client.containers.list()):
|
||||
if container._id == container_id:
|
||||
reload_exec = await container.exec(["/bin/sh", "-c", "/usr/sbin/nginx -s reload"])
|
||||
return await exec_run_handler('generic', reload_exec)
|
||||
|
||||
|
||||
# api call: container_post - post_action: exec - cmd: sieve - task: list
|
||||
async def container_post__exec__sieve__list(self, container_id, request_json):
|
||||
if 'username' in request_json:
|
||||
for container in (await self.docker_client.containers.list()):
|
||||
if container._id == container_id:
|
||||
sieve_exec = await container.exec(["/bin/bash", "-c", "/usr/bin/doveadm sieve list -u '" + request_json['username'].replace("'", "'\\''") + "'"])
|
||||
return await exec_run_handler('utf8_text_only', sieve_exec)
|
||||
|
||||
|
||||
# api call: container_post - post_action: exec - cmd: sieve - task: print
|
||||
async def container_post__exec__sieve__print(self, container_id, request_json):
|
||||
if 'username' in request_json and 'script_name' in request_json:
|
||||
for container in (await self.docker_client.containers.list()):
|
||||
if container._id == container_id:
|
||||
cmd = ["/bin/bash", "-c", "/usr/bin/doveadm sieve get -u '" + request_json['username'].replace("'", "'\\''") + "' '" + request_json['script_name'].replace("'", "'\\''") + "'"]
|
||||
sieve_exec = await container.exec(cmd)
|
||||
return await exec_run_handler('utf8_text_only', sieve_exec)
|
||||
|
||||
|
||||
# api call: container_post - post_action: exec - cmd: maildir - task: cleanup
|
||||
async def container_post__exec__maildir__cleanup(self, container_id, request_json):
|
||||
if 'maildir' in request_json:
|
||||
for container in (await self.docker_client.containers.list()):
|
||||
if container._id == container_id:
|
||||
sane_name = re.sub(r'\W+', '', request_json['maildir'])
|
||||
cmd = ["/bin/bash", "-c", "if [[ -d '/var/vmail/" + request_json['maildir'].replace("'", "'\\''") + "' ]]; then /bin/mv '/var/vmail/" + request_json['maildir'].replace("'", "'\\''") + "' '/var/vmail/_garbage/" + str(int(time.time())) + "_" + sane_name + "'; fi"]
|
||||
maildir_cleanup_exec = await container.exec(cmd, user='vmail')
|
||||
return await exec_run_handler('generic', maildir_cleanup_exec)
|
||||
|
||||
# api call: container_post - post_action: exec - cmd: rspamd - task: worker_password
|
||||
async def container_post__exec__rspamd__worker_password(self, container_id, request_json):
|
||||
if 'raw' in request_json:
|
||||
for container in (await self.docker_client.containers.list()):
|
||||
if container._id == container_id:
|
||||
|
||||
cmd = "./set_worker_password.sh '" + request_json['raw'].replace("'", "'\\''") + "' 2> /dev/null"
|
||||
rspamd_password_exec = await container.exec(cmd, user='_rspamd')
|
||||
async with rspamd_password_exec.start(detach=False) as stream:
|
||||
rspamd_password_return = await stream.read_out()
|
||||
|
||||
matched = False
|
||||
if "OK" in rspamd_password_return.data.decode('utf-8'):
|
||||
matched = True
|
||||
await container.restart()
|
||||
|
||||
if matched:
|
||||
res = {
|
||||
'type': 'success',
|
||||
'msg': 'command completed successfully'
|
||||
}
|
||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||
else:
|
||||
res = {
|
||||
'type': 'danger',
|
||||
'msg': 'command did not complete'
|
||||
}
|
||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||
|
||||
|
||||
|
||||
async def exec_run_handler(type, exec_obj):
|
||||
async with exec_obj.start(detach=False) as stream:
|
||||
exec_return = await stream.read_out()
|
||||
|
||||
if exec_return == None:
|
||||
exec_return = ""
|
||||
else:
|
||||
exec_return = exec_return.data.decode('utf-8')
|
||||
|
||||
if type == 'generic':
|
||||
exec_details = await exec_obj.inspect()
|
||||
if exec_details["ExitCode"] == None or exec_details["ExitCode"] == 0:
|
||||
res = {
|
||||
"type": "success",
|
||||
"msg": "command completed successfully"
|
||||
}
|
||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||
else:
|
||||
res = {
|
||||
"type": "success",
|
||||
"msg": "'command failed: " + exec_return
|
||||
}
|
||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||
if type == 'utf8_text_only':
|
||||
return Response(content=exec_return, media_type="text/plain")
|
||||
|
||||
async def get_host_stats(wait=5):
|
||||
global host_stats_isUpdating
|
||||
|
||||
try:
|
||||
system_time = datetime.now()
|
||||
host_stats = {
|
||||
"cpu": {
|
||||
"cores": psutil.cpu_count(),
|
||||
"usage": psutil.cpu_percent()
|
||||
},
|
||||
"memory": {
|
||||
"total": psutil.virtual_memory().total,
|
||||
"usage": psutil.virtual_memory().percent,
|
||||
"swap": psutil.swap_memory()
|
||||
},
|
||||
"uptime": time.time() - psutil.boot_time(),
|
||||
"system_time": system_time.strftime("%d.%m.%Y %H:%M:%S")
|
||||
}
|
||||
|
||||
redis_client.set('host_stats', json.dumps(host_stats), ex=10)
|
||||
except Exception as e:
|
||||
res = {
|
||||
"type": "danger",
|
||||
"msg": str(e)
|
||||
}
|
||||
print(json.dumps(res, indent=4))
|
||||
|
||||
await asyncio.sleep(wait)
|
||||
host_stats_isUpdating = False
|
||||
|
||||
|
||||
async def get_container_stats(container_id, wait=5, stop=False):
|
||||
global containerIds_to_update
|
||||
|
||||
if container_id and container_id.isalnum():
|
||||
try:
|
||||
for container in (await async_docker_client.containers.list()):
|
||||
if container._id == container_id:
|
||||
res = await container.stats(stream=False)
|
||||
|
||||
if redis_client.exists(container_id + '_stats'):
|
||||
stats = json.loads(redis_client.get(container_id + '_stats'))
|
||||
else:
|
||||
stats = []
|
||||
stats.append(res[0])
|
||||
if len(stats) > 3:
|
||||
del stats[0]
|
||||
redis_client.set(container_id + '_stats', json.dumps(stats), ex=60)
|
||||
except Exception as e:
|
||||
res = {
|
||||
"type": "danger",
|
||||
"msg": str(e)
|
||||
}
|
||||
print(json.dumps(res, indent=4))
|
||||
else:
|
||||
res = {
|
||||
"type": "danger",
|
||||
"msg": "no or invalid id defined"
|
||||
}
|
||||
print(json.dumps(res, indent=4))
|
||||
|
||||
await asyncio.sleep(wait)
|
||||
if stop == True:
|
||||
# update task was called second time, stop
|
||||
containerIds_to_update.remove(container_id)
|
||||
else:
|
||||
# call update task a second time
|
||||
await get_container_stats(container_id, wait=0, stop=True)
|
||||
|
||||
|
||||
if os.environ['REDIS_SLAVEOF_IP'] != "":
|
||||
redis_client = redis.Redis(host=os.environ['REDIS_SLAVEOF_IP'], port=os.environ['REDIS_SLAVEOF_PORT'], db=0)
|
||||
else:
|
||||
redis_client = redis.Redis(host='redis-mailcow', port=6379, db=0)
|
||||
|
||||
async_docker_client = aiodocker.Docker(url='unix:///var/run/docker.sock')
|
|
@ -0,0 +1,260 @@
|
|||
import os
|
||||
import sys
|
||||
import uvicorn
|
||||
import json
|
||||
import uuid
|
||||
import async_timeout
|
||||
import asyncio
|
||||
import aioredis
|
||||
import aiodocker
|
||||
import docker
|
||||
import logging
|
||||
from logging.config import dictConfig
|
||||
from fastapi import FastAPI, Response, Request
|
||||
from modules.DockerApi import DockerApi
|
||||
|
||||
dockerapi = None
|
||||
app = FastAPI()
|
||||
|
||||
# Define Routes
|
||||
@app.get("/host/stats")
|
||||
async def get_host_update_stats():
|
||||
global dockerapi
|
||||
|
||||
if dockerapi.host_stats_isUpdating == False:
|
||||
asyncio.create_task(dockerapi.get_host_stats())
|
||||
dockerapi.host_stats_isUpdating = True
|
||||
|
||||
while True:
|
||||
if await dockerapi.redis_client.exists('host_stats'):
|
||||
break
|
||||
await asyncio.sleep(1.5)
|
||||
|
||||
stats = json.loads(await dockerapi.redis_client.get('host_stats'))
|
||||
return Response(content=json.dumps(stats, indent=4), media_type="application/json")
|
||||
|
||||
@app.get("/containers/{container_id}/json")
|
||||
async def get_container(container_id : str):
|
||||
global dockerapi
|
||||
|
||||
if container_id and container_id.isalnum():
|
||||
try:
|
||||
for container in (await dockerapi.async_docker_client.containers.list()):
|
||||
if container._id == container_id:
|
||||
container_info = await container.show()
|
||||
return Response(content=json.dumps(container_info, indent=4), media_type="application/json")
|
||||
|
||||
res = {
|
||||
"type": "danger",
|
||||
"msg": "no container found"
|
||||
}
|
||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||
except Exception as e:
|
||||
res = {
|
||||
"type": "danger",
|
||||
"msg": str(e)
|
||||
}
|
||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||
else:
|
||||
res = {
|
||||
"type": "danger",
|
||||
"msg": "no or invalid id defined"
|
||||
}
|
||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||
|
||||
@app.get("/containers/json")
|
||||
async def get_containers():
|
||||
global dockerapi
|
||||
|
||||
containers = {}
|
||||
try:
|
||||
for container in (await dockerapi.async_docker_client.containers.list()):
|
||||
container_info = await container.show()
|
||||
containers.update({container_info['Id']: container_info})
|
||||
return Response(content=json.dumps(containers, indent=4), media_type="application/json")
|
||||
except Exception as e:
|
||||
res = {
|
||||
"type": "danger",
|
||||
"msg": str(e)
|
||||
}
|
||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||
|
||||
@app.post("/containers/{container_id}/{post_action}")
|
||||
async def post_containers(container_id : str, post_action : str, request: Request):
|
||||
global dockerapi
|
||||
|
||||
try :
|
||||
request_json = await request.json()
|
||||
except Exception as err:
|
||||
request_json = {}
|
||||
|
||||
if container_id and container_id.isalnum() and post_action:
|
||||
try:
|
||||
"""Dispatch container_post api call"""
|
||||
if post_action == 'exec':
|
||||
if not request_json or not 'cmd' in request_json:
|
||||
res = {
|
||||
"type": "danger",
|
||||
"msg": "cmd is missing"
|
||||
}
|
||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||
if not request_json or not 'task' in request_json:
|
||||
res = {
|
||||
"type": "danger",
|
||||
"msg": "task is missing"
|
||||
}
|
||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||
|
||||
api_call_method_name = '__'.join(['container_post', str(post_action), str(request_json['cmd']), str(request_json['task']) ])
|
||||
else:
|
||||
api_call_method_name = '__'.join(['container_post', str(post_action) ])
|
||||
|
||||
api_call_method = getattr(dockerapi, api_call_method_name, lambda container_id: Response(content=json.dumps({'type': 'danger', 'msg':'container_post - unknown api call' }, indent=4), media_type="application/json"))
|
||||
|
||||
dockerapi.logger.info("api call: %s, container_id: %s" % (api_call_method_name, container_id))
|
||||
return api_call_method(request_json, container_id=container_id)
|
||||
except Exception as e:
|
||||
dockerapi.logger.error("error - container_post: %s" % str(e))
|
||||
res = {
|
||||
"type": "danger",
|
||||
"msg": str(e)
|
||||
}
|
||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||
|
||||
else:
|
||||
res = {
|
||||
"type": "danger",
|
||||
"msg": "invalid container id or missing action"
|
||||
}
|
||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||
|
||||
@app.post("/container/{container_id}/stats/update")
|
||||
async def post_container_update_stats(container_id : str):
|
||||
global dockerapi
|
||||
|
||||
# start update task for container if no task is running
|
||||
if container_id not in dockerapi.containerIds_to_update:
|
||||
asyncio.create_task(dockerapi.get_container_stats(container_id))
|
||||
dockerapi.containerIds_to_update.append(container_id)
|
||||
|
||||
while True:
|
||||
if await dockerapi.redis_client.exists(container_id + '_stats'):
|
||||
break
|
||||
await asyncio.sleep(1.5)
|
||||
|
||||
stats = json.loads(await dockerapi.redis_client.get(container_id + '_stats'))
|
||||
return Response(content=json.dumps(stats, indent=4), media_type="application/json")
|
||||
|
||||
# Events
|
||||
@app.on_event("startup")
|
||||
async def startup_event():
|
||||
global dockerapi
|
||||
|
||||
# Initialize a custom logger
|
||||
logger = logging.getLogger("dockerapi")
|
||||
logger.setLevel(logging.INFO)
|
||||
# Configure the logger to output logs to the terminal
|
||||
handler = logging.StreamHandler()
|
||||
handler.setLevel(logging.INFO)
|
||||
formatter = logging.Formatter("%(levelname)s: %(message)s")
|
||||
handler.setFormatter(formatter)
|
||||
logger.addHandler(handler)
|
||||
|
||||
logger.info("Init APP")
|
||||
|
||||
# Init redis client
|
||||
if os.environ['REDIS_SLAVEOF_IP'] != "":
|
||||
redis_client = redis = await aioredis.from_url(f"redis://{os.environ['REDIS_SLAVEOF_IP']}:{os.environ['REDIS_SLAVEOF_PORT']}/0")
|
||||
else:
|
||||
redis_client = redis = await aioredis.from_url("redis://redis-mailcow:6379/0")
|
||||
|
||||
# Init docker clients
|
||||
sync_docker_client = docker.DockerClient(base_url='unix://var/run/docker.sock', version='auto')
|
||||
async_docker_client = aiodocker.Docker(url='unix:///var/run/docker.sock')
|
||||
|
||||
dockerapi = DockerApi(redis_client, sync_docker_client, async_docker_client, logger)
|
||||
|
||||
logger.info("Subscribe to redis channel")
|
||||
# Subscribe to redis channel
|
||||
dockerapi.pubsub = redis.pubsub()
|
||||
await dockerapi.pubsub.subscribe("MC_CHANNEL")
|
||||
asyncio.create_task(handle_pubsub_messages(dockerapi.pubsub))
|
||||
|
||||
@app.on_event("shutdown")
|
||||
async def shutdown_event():
|
||||
global dockerapi
|
||||
|
||||
# Close docker connections
|
||||
dockerapi.sync_docker_client.close()
|
||||
await dockerapi.async_docker_client.close()
|
||||
|
||||
# Close redis
|
||||
await dockerapi.pubsub.unsubscribe("MC_CHANNEL")
|
||||
await dockerapi.redis_client.close()
|
||||
|
||||
# PubSub Handler
|
||||
async def handle_pubsub_messages(channel: aioredis.client.PubSub):
|
||||
global dockerapi
|
||||
|
||||
while True:
|
||||
try:
|
||||
async with async_timeout.timeout(1):
|
||||
message = await channel.get_message(ignore_subscribe_messages=True)
|
||||
if message is not None:
|
||||
# Parse message
|
||||
data_json = json.loads(message['data'].decode('utf-8'))
|
||||
dockerapi.logger.info(f"PubSub Received - {json.dumps(data_json)}")
|
||||
|
||||
# Handle api_call
|
||||
if 'api_call' in data_json:
|
||||
# api_call: container_post
|
||||
if data_json['api_call'] == "container_post":
|
||||
if 'post_action' in data_json and 'container_name' in data_json:
|
||||
try:
|
||||
"""Dispatch container_post api call"""
|
||||
request_json = {}
|
||||
if data_json['post_action'] == 'exec':
|
||||
if 'request' in data_json:
|
||||
request_json = data_json['request']
|
||||
if 'cmd' in request_json:
|
||||
if 'task' in request_json:
|
||||
api_call_method_name = '__'.join(['container_post', str(data_json['post_action']), str(request_json['cmd']), str(request_json['task']) ])
|
||||
else:
|
||||
dockerapi.logger.error("api call: task missing")
|
||||
else:
|
||||
dockerapi.logger.error("api call: cmd missing")
|
||||
else:
|
||||
dockerapi.logger.error("api call: request missing")
|
||||
else:
|
||||
api_call_method_name = '__'.join(['container_post', str(data_json['post_action'])])
|
||||
|
||||
if api_call_method_name:
|
||||
api_call_method = getattr(dockerapi, api_call_method_name)
|
||||
if api_call_method:
|
||||
dockerapi.logger.info("api call: %s, container_name: %s" % (api_call_method_name, data_json['container_name']))
|
||||
api_call_method(request_json, container_name=data_json['container_name'])
|
||||
else:
|
||||
dockerapi.logger.error("api call not found: %s, container_name: %s" % (api_call_method_name, data_json['container_name']))
|
||||
except Exception as e:
|
||||
dockerapi.logger.error("container_post: %s" % str(e))
|
||||
else:
|
||||
dockerapi.logger.error("api call: missing container_name, post_action or request")
|
||||
else:
|
||||
dockerapi.logger.error("Unknwon PubSub recieved - %s" % json.dumps(data_json))
|
||||
else:
|
||||
dockerapi.logger.error("Unknwon PubSub recieved - %s" % json.dumps(data_json))
|
||||
|
||||
await asyncio.sleep(0.01)
|
||||
except asyncio.TimeoutError:
|
||||
pass
|
||||
|
||||
if __name__ == '__main__':
|
||||
uvicorn.run(
|
||||
app,
|
||||
host="0.0.0.0",
|
||||
port=443,
|
||||
ssl_certfile="/app/dockerapi_cert.pem",
|
||||
ssl_keyfile="/app/dockerapi_key.pem",
|
||||
log_level="info",
|
||||
loop="none"
|
||||
)
|
|
@ -0,0 +1,487 @@
|
|||
import psutil
|
||||
import sys
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
import json
|
||||
import asyncio
|
||||
import platform
|
||||
from datetime import datetime
|
||||
from fastapi import FastAPI, Response, Request
|
||||
|
||||
class DockerApi:
|
||||
def __init__(self, redis_client, sync_docker_client, async_docker_client, logger):
|
||||
self.redis_client = redis_client
|
||||
self.sync_docker_client = sync_docker_client
|
||||
self.async_docker_client = async_docker_client
|
||||
self.logger = logger
|
||||
|
||||
self.host_stats_isUpdating = False
|
||||
self.containerIds_to_update = []
|
||||
|
||||
# api call: container_post - post_action: stop
|
||||
def container_post__stop(self, request_json, **kwargs):
|
||||
if 'container_id' in kwargs:
|
||||
filters = {"id": kwargs['container_id']}
|
||||
elif 'container_name' in kwargs:
|
||||
filters = {"name": kwargs['container_name']}
|
||||
|
||||
for container in self.sync_docker_client.containers.list(all=True, filters=filters):
|
||||
container.stop()
|
||||
|
||||
res = { 'type': 'success', 'msg': 'command completed successfully'}
|
||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||
# api call: container_post - post_action: start
|
||||
def container_post__start(self, request_json, **kwargs):
|
||||
if 'container_id' in kwargs:
|
||||
filters = {"id": kwargs['container_id']}
|
||||
elif 'container_name' in kwargs:
|
||||
filters = {"name": kwargs['container_name']}
|
||||
|
||||
for container in self.sync_docker_client.containers.list(all=True, filters=filters):
|
||||
container.start()
|
||||
|
||||
res = { 'type': 'success', 'msg': 'command completed successfully'}
|
||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||
# api call: container_post - post_action: restart
|
||||
def container_post__restart(self, request_json, **kwargs):
|
||||
if 'container_id' in kwargs:
|
||||
filters = {"id": kwargs['container_id']}
|
||||
elif 'container_name' in kwargs:
|
||||
filters = {"name": kwargs['container_name']}
|
||||
|
||||
for container in self.sync_docker_client.containers.list(all=True, filters=filters):
|
||||
container.restart()
|
||||
|
||||
res = { 'type': 'success', 'msg': 'command completed successfully'}
|
||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||
# api call: container_post - post_action: top
|
||||
def container_post__top(self, request_json, **kwargs):
|
||||
if 'container_id' in kwargs:
|
||||
filters = {"id": kwargs['container_id']}
|
||||
elif 'container_name' in kwargs:
|
||||
filters = {"name": kwargs['container_name']}
|
||||
|
||||
for container in self.sync_docker_client.containers.list(all=True, filters=filters):
|
||||
res = { 'type': 'success', 'msg': container.top()}
|
||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||
# api call: container_post - post_action: stats
|
||||
def container_post__stats(self, request_json, **kwargs):
|
||||
if 'container_id' in kwargs:
|
||||
filters = {"id": kwargs['container_id']}
|
||||
elif 'container_name' in kwargs:
|
||||
filters = {"name": kwargs['container_name']}
|
||||
|
||||
for container in self.sync_docker_client.containers.list(all=True, filters=filters):
|
||||
for stat in container.stats(decode=True, stream=True):
|
||||
res = { 'type': 'success', 'msg': stat}
|
||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||
# api call: container_post - post_action: exec - cmd: mailq - task: delete
|
||||
def container_post__exec__mailq__delete(self, request_json, **kwargs):
|
||||
if 'container_id' in kwargs:
|
||||
filters = {"id": kwargs['container_id']}
|
||||
elif 'container_name' in kwargs:
|
||||
filters = {"name": kwargs['container_name']}
|
||||
|
||||
if 'items' in request_json:
|
||||
r = re.compile("^[0-9a-fA-F]+$")
|
||||
filtered_qids = filter(r.match, request_json['items'])
|
||||
if filtered_qids:
|
||||
flagged_qids = ['-d %s' % i for i in filtered_qids]
|
||||
sanitized_string = str(' '.join(flagged_qids))
|
||||
for container in self.sync_docker_client.containers.list(filters=filters):
|
||||
postsuper_r = container.exec_run(["/bin/bash", "-c", "/usr/sbin/postsuper " + sanitized_string])
|
||||
return self.exec_run_handler('generic', postsuper_r)
|
||||
# api call: container_post - post_action: exec - cmd: mailq - task: hold
|
||||
def container_post__exec__mailq__hold(self, request_json, **kwargs):
|
||||
if 'container_id' in kwargs:
|
||||
filters = {"id": kwargs['container_id']}
|
||||
elif 'container_name' in kwargs:
|
||||
filters = {"name": kwargs['container_name']}
|
||||
|
||||
if 'items' in request_json:
|
||||
r = re.compile("^[0-9a-fA-F]+$")
|
||||
filtered_qids = filter(r.match, request_json['items'])
|
||||
if filtered_qids:
|
||||
flagged_qids = ['-h %s' % i for i in filtered_qids]
|
||||
sanitized_string = str(' '.join(flagged_qids))
|
||||
for container in self.sync_docker_client.containers.list(filters=filters):
|
||||
postsuper_r = container.exec_run(["/bin/bash", "-c", "/usr/sbin/postsuper " + sanitized_string])
|
||||
return self.exec_run_handler('generic', postsuper_r)
|
||||
# api call: container_post - post_action: exec - cmd: mailq - task: cat
|
||||
def container_post__exec__mailq__cat(self, request_json, **kwargs):
|
||||
if 'container_id' in kwargs:
|
||||
filters = {"id": kwargs['container_id']}
|
||||
elif 'container_name' in kwargs:
|
||||
filters = {"name": kwargs['container_name']}
|
||||
|
||||
if 'items' in request_json:
|
||||
r = re.compile("^[0-9a-fA-F]+$")
|
||||
filtered_qids = filter(r.match, request_json['items'])
|
||||
if filtered_qids:
|
||||
sanitized_string = str(' '.join(filtered_qids))
|
||||
|
||||
for container in self.sync_docker_client.containers.list(filters=filters):
|
||||
postcat_return = container.exec_run(["/bin/bash", "-c", "/usr/sbin/postcat -q " + sanitized_string], user='postfix')
|
||||
if not postcat_return:
|
||||
postcat_return = 'err: invalid'
|
||||
return self.exec_run_handler('utf8_text_only', postcat_return)
|
||||
# api call: container_post - post_action: exec - cmd: mailq - task: unhold
|
||||
def container_post__exec__mailq__unhold(self, request_json, **kwargs):
|
||||
if 'container_id' in kwargs:
|
||||
filters = {"id": kwargs['container_id']}
|
||||
elif 'container_name' in kwargs:
|
||||
filters = {"name": kwargs['container_name']}
|
||||
|
||||
if 'items' in request_json:
|
||||
r = re.compile("^[0-9a-fA-F]+$")
|
||||
filtered_qids = filter(r.match, request_json['items'])
|
||||
if filtered_qids:
|
||||
flagged_qids = ['-H %s' % i for i in filtered_qids]
|
||||
sanitized_string = str(' '.join(flagged_qids))
|
||||
for container in self.sync_docker_client.containers.list(filters=filters):
|
||||
postsuper_r = container.exec_run(["/bin/bash", "-c", "/usr/sbin/postsuper " + sanitized_string])
|
||||
return self.exec_run_handler('generic', postsuper_r)
|
||||
# api call: container_post - post_action: exec - cmd: mailq - task: deliver
|
||||
def container_post__exec__mailq__deliver(self, request_json, **kwargs):
|
||||
if 'container_id' in kwargs:
|
||||
filters = {"id": kwargs['container_id']}
|
||||
elif 'container_name' in kwargs:
|
||||
filters = {"name": kwargs['container_name']}
|
||||
|
||||
if 'items' in request_json:
|
||||
r = re.compile("^[0-9a-fA-F]+$")
|
||||
filtered_qids = filter(r.match, request_json['items'])
|
||||
if filtered_qids:
|
||||
flagged_qids = ['-i %s' % i for i in filtered_qids]
|
||||
for container in self.sync_docker_client.containers.list(filters=filters):
|
||||
for i in flagged_qids:
|
||||
postqueue_r = container.exec_run(["/bin/bash", "-c", "/usr/sbin/postqueue " + i], user='postfix')
|
||||
# todo: check each exit code
|
||||
res = { 'type': 'success', 'msg': 'Scheduled immediate delivery'}
|
||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||
# api call: container_post - post_action: exec - cmd: mailq - task: list
|
||||
def container_post__exec__mailq__list(self, request_json, **kwargs):
|
||||
if 'container_id' in kwargs:
|
||||
filters = {"id": kwargs['container_id']}
|
||||
elif 'container_name' in kwargs:
|
||||
filters = {"name": kwargs['container_name']}
|
||||
|
||||
for container in self.sync_docker_client.containers.list(filters=filters):
|
||||
mailq_return = container.exec_run(["/usr/sbin/postqueue", "-j"], user='postfix')
|
||||
return self.exec_run_handler('utf8_text_only', mailq_return)
|
||||
# api call: container_post - post_action: exec - cmd: mailq - task: flush
|
||||
def container_post__exec__mailq__flush(self, request_json, **kwargs):
|
||||
if 'container_id' in kwargs:
|
||||
filters = {"id": kwargs['container_id']}
|
||||
elif 'container_name' in kwargs:
|
||||
filters = {"name": kwargs['container_name']}
|
||||
|
||||
for container in self.sync_docker_client.containers.list(filters=filters):
|
||||
postqueue_r = container.exec_run(["/usr/sbin/postqueue", "-f"], user='postfix')
|
||||
return self.exec_run_handler('generic', postqueue_r)
|
||||
# api call: container_post - post_action: exec - cmd: mailq - task: super_delete
|
||||
def container_post__exec__mailq__super_delete(self, request_json, **kwargs):
|
||||
if 'container_id' in kwargs:
|
||||
filters = {"id": kwargs['container_id']}
|
||||
elif 'container_name' in kwargs:
|
||||
filters = {"name": kwargs['container_name']}
|
||||
|
||||
for container in self.sync_docker_client.containers.list(filters=filters):
|
||||
postsuper_r = container.exec_run(["/usr/sbin/postsuper", "-d", "ALL"])
|
||||
return self.exec_run_handler('generic', postsuper_r)
|
||||
# api call: container_post - post_action: exec - cmd: system - task: fts_rescan
|
||||
def container_post__exec__system__fts_rescan(self, request_json, **kwargs):
|
||||
if 'container_id' in kwargs:
|
||||
filters = {"id": kwargs['container_id']}
|
||||
elif 'container_name' in kwargs:
|
||||
filters = {"name": kwargs['container_name']}
|
||||
|
||||
if 'username' in request_json:
|
||||
for container in self.sync_docker_client.containers.list(filters=filters):
|
||||
rescan_return = container.exec_run(["/bin/bash", "-c", "/usr/bin/doveadm fts rescan -u '" + request_json['username'].replace("'", "'\\''") + "'"], user='vmail')
|
||||
if rescan_return.exit_code == 0:
|
||||
res = { 'type': 'success', 'msg': 'fts_rescan: rescan triggered'}
|
||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||
else:
|
||||
res = { 'type': 'warning', 'msg': 'fts_rescan error'}
|
||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||
if 'all' in request_json:
|
||||
for container in self.sync_docker_client.containers.list(filters=filters):
|
||||
rescan_return = container.exec_run(["/bin/bash", "-c", "/usr/bin/doveadm fts rescan -A"], user='vmail')
|
||||
if rescan_return.exit_code == 0:
|
||||
res = { 'type': 'success', 'msg': 'fts_rescan: rescan triggered'}
|
||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||
else:
|
||||
res = { 'type': 'warning', 'msg': 'fts_rescan error'}
|
||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||
# api call: container_post - post_action: exec - cmd: system - task: df
|
||||
def container_post__exec__system__df(self, request_json, **kwargs):
|
||||
if 'container_id' in kwargs:
|
||||
filters = {"id": kwargs['container_id']}
|
||||
elif 'container_name' in kwargs:
|
||||
filters = {"name": kwargs['container_name']}
|
||||
|
||||
if 'dir' in request_json:
|
||||
for container in self.sync_docker_client.containers.list(filters=filters):
|
||||
df_return = container.exec_run(["/bin/bash", "-c", "/bin/df -H '" + request_json['dir'].replace("'", "'\\''") + "' | /usr/bin/tail -n1 | /usr/bin/tr -s [:blank:] | /usr/bin/tr ' ' ','"], user='nobody')
|
||||
if df_return.exit_code == 0:
|
||||
return df_return.output.decode('utf-8').rstrip()
|
||||
else:
|
||||
return "0,0,0,0,0,0"
|
||||
# api call: container_post - post_action: exec - cmd: system - task: mysql_upgrade
|
||||
def container_post__exec__system__mysql_upgrade(self, request_json, **kwargs):
|
||||
if 'container_id' in kwargs:
|
||||
filters = {"id": kwargs['container_id']}
|
||||
elif 'container_name' in kwargs:
|
||||
filters = {"name": kwargs['container_name']}
|
||||
|
||||
for container in self.sync_docker_client.containers.list(filters=filters):
|
||||
sql_return = container.exec_run(["/bin/bash", "-c", "/usr/bin/mysql_upgrade -uroot -p'" + os.environ['DBROOT'].replace("'", "'\\''") + "'\n"], user='mysql')
|
||||
if sql_return.exit_code == 0:
|
||||
matched = False
|
||||
for line in sql_return.output.decode('utf-8').split("\n"):
|
||||
if 'is already upgraded to' in line:
|
||||
matched = True
|
||||
if matched:
|
||||
res = { 'type': 'success', 'msg':'mysql_upgrade: already upgraded', 'text': sql_return.output.decode('utf-8')}
|
||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||
else:
|
||||
container.restart()
|
||||
res = { 'type': 'warning', 'msg':'mysql_upgrade: upgrade was applied', 'text': sql_return.output.decode('utf-8')}
|
||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||
else:
|
||||
res = { 'type': 'error', 'msg': 'mysql_upgrade: error running command', 'text': sql_return.output.decode('utf-8')}
|
||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||
# api call: container_post - post_action: exec - cmd: system - task: mysql_tzinfo_to_sql
|
||||
def container_post__exec__system__mysql_tzinfo_to_sql(self, request_json, **kwargs):
|
||||
if 'container_id' in kwargs:
|
||||
filters = {"id": kwargs['container_id']}
|
||||
elif 'container_name' in kwargs:
|
||||
filters = {"name": kwargs['container_name']}
|
||||
|
||||
for container in self.sync_docker_client.containers.list(filters=filters):
|
||||
sql_return = container.exec_run(["/bin/bash", "-c", "/usr/bin/mysql_tzinfo_to_sql /usr/share/zoneinfo | /bin/sed 's/Local time zone must be set--see zic manual page/FCTY/' | /usr/bin/mysql -uroot -p'" + os.environ['DBROOT'].replace("'", "'\\''") + "' mysql \n"], user='mysql')
|
||||
if sql_return.exit_code == 0:
|
||||
res = { 'type': 'info', 'msg': 'mysql_tzinfo_to_sql: command completed successfully', 'text': sql_return.output.decode('utf-8')}
|
||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||
else:
|
||||
res = { 'type': 'error', 'msg': 'mysql_tzinfo_to_sql: error running command', 'text': sql_return.output.decode('utf-8')}
|
||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||
# api call: container_post - post_action: exec - cmd: reload - task: dovecot
|
||||
def container_post__exec__reload__dovecot(self, request_json, **kwargs):
|
||||
if 'container_id' in kwargs:
|
||||
filters = {"id": kwargs['container_id']}
|
||||
elif 'container_name' in kwargs:
|
||||
filters = {"name": kwargs['container_name']}
|
||||
|
||||
for container in self.sync_docker_client.containers.list(filters=filters):
|
||||
reload_return = container.exec_run(["/bin/bash", "-c", "/usr/sbin/dovecot reload"])
|
||||
return self.exec_run_handler('generic', reload_return)
|
||||
# api call: container_post - post_action: exec - cmd: reload - task: postfix
|
||||
def container_post__exec__reload__postfix(self, request_json, **kwargs):
|
||||
if 'container_id' in kwargs:
|
||||
filters = {"id": kwargs['container_id']}
|
||||
elif 'container_name' in kwargs:
|
||||
filters = {"name": kwargs['container_name']}
|
||||
|
||||
for container in self.sync_docker_client.containers.list(filters=filters):
|
||||
reload_return = container.exec_run(["/bin/bash", "-c", "/usr/sbin/postfix reload"])
|
||||
return self.exec_run_handler('generic', reload_return)
|
||||
# api call: container_post - post_action: exec - cmd: reload - task: nginx
|
||||
def container_post__exec__reload__nginx(self, request_json, **kwargs):
|
||||
if 'container_id' in kwargs:
|
||||
filters = {"id": kwargs['container_id']}
|
||||
elif 'container_name' in kwargs:
|
||||
filters = {"name": kwargs['container_name']}
|
||||
|
||||
for container in self.sync_docker_client.containers.list(filters=filters):
|
||||
reload_return = container.exec_run(["/bin/sh", "-c", "/usr/sbin/nginx -s reload"])
|
||||
return self.exec_run_handler('generic', reload_return)
|
||||
# api call: container_post - post_action: exec - cmd: sieve - task: list
|
||||
def container_post__exec__sieve__list(self, request_json, **kwargs):
|
||||
if 'container_id' in kwargs:
|
||||
filters = {"id": kwargs['container_id']}
|
||||
elif 'container_name' in kwargs:
|
||||
filters = {"name": kwargs['container_name']}
|
||||
|
||||
if 'username' in request_json:
|
||||
for container in self.sync_docker_client.containers.list(filters=filters):
|
||||
sieve_return = container.exec_run(["/bin/bash", "-c", "/usr/bin/doveadm sieve list -u '" + request_json['username'].replace("'", "'\\''") + "'"])
|
||||
return self.exec_run_handler('utf8_text_only', sieve_return)
|
||||
# api call: container_post - post_action: exec - cmd: sieve - task: print
|
||||
def container_post__exec__sieve__print(self, request_json, **kwargs):
|
||||
if 'container_id' in kwargs:
|
||||
filters = {"id": kwargs['container_id']}
|
||||
elif 'container_name' in kwargs:
|
||||
filters = {"name": kwargs['container_name']}
|
||||
|
||||
if 'username' in request_json and 'script_name' in request_json:
|
||||
for container in self.sync_docker_client.containers.list(filters=filters):
|
||||
cmd = ["/bin/bash", "-c", "/usr/bin/doveadm sieve get -u '" + request_json['username'].replace("'", "'\\''") + "' '" + request_json['script_name'].replace("'", "'\\''") + "'"]
|
||||
sieve_return = container.exec_run(cmd)
|
||||
return self.exec_run_handler('utf8_text_only', sieve_return)
|
||||
# api call: container_post - post_action: exec - cmd: maildir - task: cleanup
|
||||
def container_post__exec__maildir__cleanup(self, request_json, **kwargs):
|
||||
if 'container_id' in kwargs:
|
||||
filters = {"id": kwargs['container_id']}
|
||||
elif 'container_name' in kwargs:
|
||||
filters = {"name": kwargs['container_name']}
|
||||
|
||||
if 'maildir' in request_json:
|
||||
for container in self.sync_docker_client.containers.list(filters=filters):
|
||||
sane_name = re.sub(r'\W+', '', request_json['maildir'])
|
||||
vmail_name = request_json['maildir'].replace("'", "'\\''")
|
||||
cmd_vmail = "if [[ -d '/var/vmail/" + vmail_name + "' ]]; then /bin/mv '/var/vmail/" + vmail_name + "' '/var/vmail/_garbage/" + str(int(time.time())) + "_" + sane_name + "'; fi"
|
||||
index_name = request_json['maildir'].split("/")
|
||||
if len(index_name) > 1:
|
||||
index_name = index_name[1].replace("'", "'\\''") + "@" + index_name[0].replace("'", "'\\''")
|
||||
cmd_vmail_index = "if [[ -d '/var/vmail_index/" + index_name + "' ]]; then /bin/mv '/var/vmail_index/" + index_name + "' '/var/vmail/_garbage/" + str(int(time.time())) + "_" + sane_name + "_index'; fi"
|
||||
cmd = ["/bin/bash", "-c", cmd_vmail + " && " + cmd_vmail_index]
|
||||
else:
|
||||
cmd = ["/bin/bash", "-c", cmd_vmail]
|
||||
maildir_cleanup = container.exec_run(cmd, user='vmail')
|
||||
return self.exec_run_handler('generic', maildir_cleanup)
|
||||
# api call: container_post - post_action: exec - cmd: rspamd - task: worker_password
|
||||
def container_post__exec__rspamd__worker_password(self, request_json, **kwargs):
|
||||
if 'container_id' in kwargs:
|
||||
filters = {"id": kwargs['container_id']}
|
||||
elif 'container_name' in kwargs:
|
||||
filters = {"name": kwargs['container_name']}
|
||||
|
||||
if 'raw' in request_json:
|
||||
for container in self.sync_docker_client.containers.list(filters=filters):
|
||||
cmd = "/usr/bin/rspamadm pw -e -p '" + request_json['raw'].replace("'", "'\\''") + "' 2> /dev/null"
|
||||
cmd_response = self.exec_cmd_container(container, cmd, user="_rspamd")
|
||||
|
||||
matched = False
|
||||
for line in cmd_response.split("\n"):
|
||||
if '$2$' in line:
|
||||
hash = line.strip()
|
||||
hash_out = re.search('\$2\$.+$', hash).group(0)
|
||||
rspamd_passphrase_hash = re.sub('[^0-9a-zA-Z\$]+', '', hash_out.rstrip())
|
||||
rspamd_password_filename = "/etc/rspamd/override.d/worker-controller-password.inc"
|
||||
cmd = '''/bin/echo 'enable_password = "%s";' > %s && cat %s''' % (rspamd_passphrase_hash, rspamd_password_filename, rspamd_password_filename)
|
||||
cmd_response = self.exec_cmd_container(container, cmd, user="_rspamd")
|
||||
if rspamd_passphrase_hash.startswith("$2$") and rspamd_passphrase_hash in cmd_response:
|
||||
container.restart()
|
||||
matched = True
|
||||
if matched:
|
||||
res = { 'type': 'success', 'msg': 'command completed successfully' }
|
||||
self.logger.info('success changing Rspamd password')
|
||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||
else:
|
||||
self.logger.error('failed changing Rspamd password')
|
||||
res = { 'type': 'danger', 'msg': 'command did not complete' }
|
||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||
|
||||
# Collect host stats
|
||||
async def get_host_stats(self, wait=5):
|
||||
try:
|
||||
system_time = datetime.now()
|
||||
host_stats = {
|
||||
"cpu": {
|
||||
"cores": psutil.cpu_count(),
|
||||
"usage": psutil.cpu_percent()
|
||||
},
|
||||
"memory": {
|
||||
"total": psutil.virtual_memory().total,
|
||||
"usage": psutil.virtual_memory().percent,
|
||||
"swap": psutil.swap_memory()
|
||||
},
|
||||
"uptime": time.time() - psutil.boot_time(),
|
||||
"system_time": system_time.strftime("%d.%m.%Y %H:%M:%S"),
|
||||
"architecture": platform.machine()
|
||||
}
|
||||
|
||||
await self.redis_client.set('host_stats', json.dumps(host_stats), ex=10)
|
||||
except Exception as e:
|
||||
res = {
|
||||
"type": "danger",
|
||||
"msg": str(e)
|
||||
}
|
||||
|
||||
await asyncio.sleep(wait)
|
||||
self.host_stats_isUpdating = False
|
||||
# Collect container stats
|
||||
async def get_container_stats(self, container_id, wait=5, stop=False):
|
||||
if container_id and container_id.isalnum():
|
||||
try:
|
||||
for container in (await self.async_docker_client.containers.list()):
|
||||
if container._id == container_id:
|
||||
res = await container.stats(stream=False)
|
||||
|
||||
if await self.redis_client.exists(container_id + '_stats'):
|
||||
stats = json.loads(await self.redis_client.get(container_id + '_stats'))
|
||||
else:
|
||||
stats = []
|
||||
stats.append(res[0])
|
||||
if len(stats) > 3:
|
||||
del stats[0]
|
||||
await self.redis_client.set(container_id + '_stats', json.dumps(stats), ex=60)
|
||||
except Exception as e:
|
||||
res = {
|
||||
"type": "danger",
|
||||
"msg": str(e)
|
||||
}
|
||||
else:
|
||||
res = {
|
||||
"type": "danger",
|
||||
"msg": "no or invalid id defined"
|
||||
}
|
||||
|
||||
await asyncio.sleep(wait)
|
||||
if stop == True:
|
||||
# update task was called second time, stop
|
||||
self.containerIds_to_update.remove(container_id)
|
||||
else:
|
||||
# call update task a second time
|
||||
await self.get_container_stats(container_id, wait=0, stop=True)
|
||||
|
||||
def exec_cmd_container(self, container, cmd, user, timeout=2, shell_cmd="/bin/bash"):
|
||||
def recv_socket_data(c_socket, timeout):
|
||||
c_socket.setblocking(0)
|
||||
total_data=[]
|
||||
data=''
|
||||
begin=time.time()
|
||||
while True:
|
||||
if total_data and time.time()-begin > timeout:
|
||||
break
|
||||
elif time.time()-begin > timeout*2:
|
||||
break
|
||||
try:
|
||||
data = c_socket.recv(8192)
|
||||
if data:
|
||||
total_data.append(data.decode('utf-8'))
|
||||
#change the beginning time for measurement
|
||||
begin=time.time()
|
||||
else:
|
||||
#sleep for sometime to indicate a gap
|
||||
time.sleep(0.1)
|
||||
break
|
||||
except:
|
||||
pass
|
||||
return ''.join(total_data)
|
||||
|
||||
try :
|
||||
socket = container.exec_run([shell_cmd], stdin=True, socket=True, user=user).output._sock
|
||||
if not cmd.endswith("\n"):
|
||||
cmd = cmd + "\n"
|
||||
socket.send(cmd.encode('utf-8'))
|
||||
data = recv_socket_data(socket, timeout)
|
||||
socket.close()
|
||||
return data
|
||||
except Exception as e:
|
||||
self.logger.error("error - exec_cmd_container: %s" % str(e))
|
||||
traceback.print_exc(file=sys.stdout)
|
||||
|
||||
def exec_run_handler(self, type, output):
|
||||
if type == 'generic':
|
||||
if output.exit_code == 0:
|
||||
res = { 'type': 'success', 'msg': 'command completed successfully' }
|
||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||
else:
|
||||
res = { 'type': 'danger', 'msg': 'command failed: ' + output.output.decode('utf-8') }
|
||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||
if type == 'utf8_text_only':
|
||||
return Response(content=output.output.decode('utf-8'), media_type="text/plain")
|
|
@ -1,10 +1,13 @@
|
|||
FROM debian:bullseye-slim
|
||||
LABEL maintainer "Andre Peters <andre.peters@servercow.de>"
|
||||
LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>"
|
||||
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
ARG DOVECOT=2.3.19.1
|
||||
# renovate: datasource=github-tags depName=dovecot/core versioning=semver-coerced
|
||||
ARG DOVECOT=2.3.20
|
||||
# renovate: datasource=github-releases depName=tianon/gosu versioning=semver-coerced
|
||||
ARG GOSU_VERSION=1.16
|
||||
ENV LC_ALL C
|
||||
ENV GOSU_VERSION 1.14
|
||||
|
||||
|
||||
# Add groups and users before installing Dovecot to not break compatibility
|
||||
RUN groupadd -g 5000 vmail \
|
||||
|
@ -18,6 +21,7 @@ RUN groupadd -g 5000 vmail \
|
|||
&& touch /etc/default/locale \
|
||||
&& apt-get update \
|
||||
&& apt-get -y --no-install-recommends install \
|
||||
build-essential \
|
||||
apt-transport-https \
|
||||
ca-certificates \
|
||||
cpanminus \
|
||||
|
@ -58,6 +62,7 @@ RUN groupadd -g 5000 vmail \
|
|||
libproc-processtable-perl \
|
||||
libreadonly-perl \
|
||||
libregexp-common-perl \
|
||||
libssl-dev \
|
||||
libsys-meminfo-perl \
|
||||
libterm-readkey-perl \
|
||||
libtest-deep-perl \
|
||||
|
@ -107,6 +112,8 @@ RUN groupadd -g 5000 vmail \
|
|||
&& apt-get autoclean \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& rm -rf /tmp/* /var/tmp/* /root/.cache/
|
||||
# imapsync dependencies
|
||||
RUN cpan Crypt::OpenSSL::PKCS12
|
||||
|
||||
COPY trim_logs.sh /usr/local/bin/trim_logs.sh
|
||||
COPY clean_q_aged.sh /usr/local/bin/clean_q_aged.sh
|
||||
|
|
|
@ -159,7 +159,7 @@ function auth_password_verify(req, pass)
|
|||
VALUES ("%s", 0, "%s", "%s")]], con:escape(req.service), con:escape(req.user), con:escape(req.real_rip)))
|
||||
cur:close()
|
||||
con:close()
|
||||
return dovecot.auth.PASSDB_RESULT_OK, "password=" .. pass
|
||||
return dovecot.auth.PASSDB_RESULT_OK, ""
|
||||
end
|
||||
row = cur:fetch (row, "a")
|
||||
end
|
||||
|
@ -180,13 +180,13 @@ function auth_password_verify(req, pass)
|
|||
if tostring(req.real_rip) == "__IPV4_SOGO__" then
|
||||
cur:close()
|
||||
con:close()
|
||||
return dovecot.auth.PASSDB_RESULT_OK, "password=" .. pass
|
||||
return dovecot.auth.PASSDB_RESULT_OK, ""
|
||||
elseif row.has_prot_access == "1" then
|
||||
con:execute(string.format([[REPLACE INTO sasl_log (service, app_password, username, real_rip)
|
||||
VALUES ("%s", %d, "%s", "%s")]], con:escape(req.service), row.id, con:escape(req.user), con:escape(req.real_rip)))
|
||||
cur:close()
|
||||
con:close()
|
||||
return dovecot.auth.PASSDB_RESULT_OK, "password=" .. pass
|
||||
return dovecot.auth.PASSDB_RESULT_OK, ""
|
||||
end
|
||||
end
|
||||
row = cur:fetch (row, "a")
|
||||
|
|
|
@ -8492,6 +8492,7 @@ sub xoauth2
|
|||
require HTML::Entities ;
|
||||
require JSON ;
|
||||
require JSON::WebToken::Crypt::RSA ;
|
||||
require Crypt::OpenSSL::PKCS12;
|
||||
require Crypt::OpenSSL::RSA ;
|
||||
require Encode::Byte ;
|
||||
require IO::Socket::SSL ;
|
||||
|
@ -8532,8 +8533,9 @@ sub xoauth2
|
|||
|
||||
$sync->{ debug } and myprint( "Service account: $iss\nKey file: $keyfile\nKey password: $keypass\n");
|
||||
|
||||
# Get private key from p12 file (would be better in perl...)
|
||||
$key = `openssl pkcs12 -in "$keyfile" -nodes -nocerts -passin pass:$keypass -nomacver`;
|
||||
# Get private key from p12 file
|
||||
my $pkcs12 = Crypt::OpenSSL::PKCS12->new_from_file($keyfile);
|
||||
$key = $pkcs12->private_key($keypass);
|
||||
|
||||
$sync->{ debug } and myprint( "Private key:\n$key\n");
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
FROM alpine:3.17
|
||||
LABEL maintainer "Andre Peters <andre.peters@servercow.de>"
|
||||
LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>"
|
||||
|
||||
ENV XTABLES_LIBDIR /usr/lib/xtables
|
||||
ENV PYTHON_IPTABLES_XTABLES_VERSION 12
|
||||
|
|
|
@ -64,28 +64,40 @@ def refreshF2boptions():
|
|||
global f2boptions
|
||||
global quit_now
|
||||
global exit_code
|
||||
|
||||
f2boptions = {}
|
||||
|
||||
if not r.get('F2B_OPTIONS'):
|
||||
f2boptions = {}
|
||||
f2boptions['ban_time'] = int
|
||||
f2boptions['max_attempts'] = int
|
||||
f2boptions['retry_window'] = int
|
||||
f2boptions['netban_ipv4'] = int
|
||||
f2boptions['netban_ipv6'] = int
|
||||
f2boptions['ban_time'] = r.get('F2B_BAN_TIME') or 1800
|
||||
f2boptions['max_attempts'] = r.get('F2B_MAX_ATTEMPTS') or 10
|
||||
f2boptions['retry_window'] = r.get('F2B_RETRY_WINDOW') or 600
|
||||
f2boptions['netban_ipv4'] = r.get('F2B_NETBAN_IPV4') or 32
|
||||
f2boptions['netban_ipv6'] = r.get('F2B_NETBAN_IPV6') or 128
|
||||
r.set('F2B_OPTIONS', json.dumps(f2boptions, ensure_ascii=False))
|
||||
f2boptions['ban_time'] = r.get('F2B_BAN_TIME')
|
||||
f2boptions['max_ban_time'] = r.get('F2B_MAX_BAN_TIME')
|
||||
f2boptions['ban_time_increment'] = r.get('F2B_BAN_TIME_INCREMENT')
|
||||
f2boptions['max_attempts'] = r.get('F2B_MAX_ATTEMPTS')
|
||||
f2boptions['retry_window'] = r.get('F2B_RETRY_WINDOW')
|
||||
f2boptions['netban_ipv4'] = r.get('F2B_NETBAN_IPV4')
|
||||
f2boptions['netban_ipv6'] = r.get('F2B_NETBAN_IPV6')
|
||||
else:
|
||||
try:
|
||||
f2boptions = {}
|
||||
f2boptions = json.loads(r.get('F2B_OPTIONS'))
|
||||
except ValueError:
|
||||
print('Error loading F2B options: F2B_OPTIONS is not json')
|
||||
quit_now = True
|
||||
exit_code = 2
|
||||
|
||||
verifyF2boptions(f2boptions)
|
||||
r.set('F2B_OPTIONS', json.dumps(f2boptions, ensure_ascii=False))
|
||||
|
||||
def verifyF2boptions(f2boptions):
|
||||
verifyF2boption(f2boptions,'ban_time', 1800)
|
||||
verifyF2boption(f2boptions,'max_ban_time', 10000)
|
||||
verifyF2boption(f2boptions,'ban_time_increment', True)
|
||||
verifyF2boption(f2boptions,'max_attempts', 10)
|
||||
verifyF2boption(f2boptions,'retry_window', 600)
|
||||
verifyF2boption(f2boptions,'netban_ipv4', 32)
|
||||
verifyF2boption(f2boptions,'netban_ipv6', 128)
|
||||
|
||||
def verifyF2boption(f2boptions, f2boption, f2bdefault):
|
||||
f2boptions[f2boption] = f2boptions[f2boption] if f2boption in f2boptions and f2boptions[f2boption] is not None else f2bdefault
|
||||
|
||||
def refreshF2bregex():
|
||||
global f2bregex
|
||||
global quit_now
|
||||
|
@ -147,6 +159,7 @@ def ban(address):
|
|||
global lock
|
||||
refreshF2boptions()
|
||||
BAN_TIME = int(f2boptions['ban_time'])
|
||||
BAN_TIME_INCREMENT = bool(f2boptions['ban_time_increment'])
|
||||
MAX_ATTEMPTS = int(f2boptions['max_attempts'])
|
||||
RETRY_WINDOW = int(f2boptions['retry_window'])
|
||||
NETBAN_IPV4 = '/' + str(f2boptions['netban_ipv4'])
|
||||
|
@ -174,20 +187,16 @@ def ban(address):
|
|||
net = ipaddress.ip_network((address + (NETBAN_IPV4 if type(ip) is ipaddress.IPv4Address else NETBAN_IPV6)), strict=False)
|
||||
net = str(net)
|
||||
|
||||
if not net in bans or time.time() - bans[net]['last_attempt'] > RETRY_WINDOW:
|
||||
bans[net] = { 'attempts': 0 }
|
||||
active_window = RETRY_WINDOW
|
||||
else:
|
||||
active_window = time.time() - bans[net]['last_attempt']
|
||||
if not net in bans:
|
||||
bans[net] = {'attempts': 0, 'last_attempt': 0, 'ban_counter': 0}
|
||||
|
||||
bans[net]['attempts'] += 1
|
||||
bans[net]['last_attempt'] = time.time()
|
||||
|
||||
active_window = time.time() - bans[net]['last_attempt']
|
||||
|
||||
if bans[net]['attempts'] >= MAX_ATTEMPTS:
|
||||
cur_time = int(round(time.time()))
|
||||
logCrit('Banning %s for %d minutes' % (net, BAN_TIME / 60))
|
||||
NET_BAN_TIME = BAN_TIME if not BAN_TIME_INCREMENT else BAN_TIME * 2 ** bans[net]['ban_counter']
|
||||
logCrit('Banning %s for %d minutes' % (net, NET_BAN_TIME / 60 ))
|
||||
if type(ip) is ipaddress.IPv4Address:
|
||||
with lock:
|
||||
chain = iptc.Chain(iptc.Table(iptc.Table.FILTER), 'MAILCOW')
|
||||
|
@ -206,7 +215,7 @@ def ban(address):
|
|||
rule.target = target
|
||||
if rule not in chain.rules:
|
||||
chain.insert_rule(rule)
|
||||
r.hset('F2B_ACTIVE_BANS', '%s' % net, cur_time + BAN_TIME)
|
||||
r.hset('F2B_ACTIVE_BANS', '%s' % net, cur_time + NET_BAN_TIME)
|
||||
else:
|
||||
logWarn('%d more attempts in the next %d seconds until %s is banned' % (MAX_ATTEMPTS - bans[net]['attempts'], RETRY_WINDOW, net))
|
||||
|
||||
|
@ -238,7 +247,8 @@ def unban(net):
|
|||
r.hdel('F2B_ACTIVE_BANS', '%s' % net)
|
||||
r.hdel('F2B_QUEUE_UNBAN', '%s' % net)
|
||||
if net in bans:
|
||||
del bans[net]
|
||||
bans[net]['attempts'] = 0
|
||||
bans[net]['ban_counter'] += 1
|
||||
|
||||
def permBan(net, unban=False):
|
||||
global lock
|
||||
|
@ -332,7 +342,7 @@ def watch():
|
|||
logWarn('%s matched rule id %s (%s)' % (addr, rule_id, item['data']))
|
||||
ban(addr)
|
||||
except Exception as ex:
|
||||
logWarn('Error reading log line from pubsub')
|
||||
logWarn('Error reading log line from pubsub: %s' % ex)
|
||||
quit_now = True
|
||||
exit_code = 2
|
||||
|
||||
|
@ -359,21 +369,30 @@ def snat4(snat_target):
|
|||
chain = iptc.Chain(table, 'POSTROUTING')
|
||||
table.autocommit = False
|
||||
new_rule = get_snat4_rule()
|
||||
for position, rule in enumerate(chain.rules):
|
||||
match = all((
|
||||
new_rule.get_src() == rule.get_src(),
|
||||
new_rule.get_dst() == rule.get_dst(),
|
||||
new_rule.target.parameters == rule.target.parameters,
|
||||
new_rule.target.name == rule.target.name
|
||||
))
|
||||
if position == 0:
|
||||
if not match:
|
||||
logInfo(f'Added POSTROUTING rule for source network {new_rule.src} to SNAT target {snat_target}')
|
||||
chain.insert_rule(new_rule)
|
||||
else:
|
||||
if match:
|
||||
logInfo(f'Remove rule for source network {new_rule.src} to SNAT target {snat_target} from POSTROUTING chain at position {position}')
|
||||
chain.delete_rule(rule)
|
||||
|
||||
if not chain.rules:
|
||||
# if there are no rules in the chain, insert the new rule directly
|
||||
logInfo(f'Added POSTROUTING rule for source network {new_rule.src} to SNAT target {snat_target}')
|
||||
chain.insert_rule(new_rule)
|
||||
else:
|
||||
for position, rule in enumerate(chain.rules):
|
||||
if not hasattr(rule.target, 'parameter'):
|
||||
continue
|
||||
match = all((
|
||||
new_rule.get_src() == rule.get_src(),
|
||||
new_rule.get_dst() == rule.get_dst(),
|
||||
new_rule.target.parameters == rule.target.parameters,
|
||||
new_rule.target.name == rule.target.name
|
||||
))
|
||||
if position == 0:
|
||||
if not match:
|
||||
logInfo(f'Added POSTROUTING rule for source network {new_rule.src} to SNAT target {snat_target}')
|
||||
chain.insert_rule(new_rule)
|
||||
else:
|
||||
if match:
|
||||
logInfo(f'Remove rule for source network {new_rule.src} to SNAT target {snat_target} from POSTROUTING chain at position {position}')
|
||||
chain.delete_rule(rule)
|
||||
|
||||
table.commit()
|
||||
table.autocommit = True
|
||||
except:
|
||||
|
@ -418,6 +437,8 @@ def autopurge():
|
|||
time.sleep(10)
|
||||
refreshF2boptions()
|
||||
BAN_TIME = int(f2boptions['ban_time'])
|
||||
MAX_BAN_TIME = int(f2boptions['max_ban_time'])
|
||||
BAN_TIME_INCREMENT = bool(f2boptions['ban_time_increment'])
|
||||
MAX_ATTEMPTS = int(f2boptions['max_attempts'])
|
||||
QUEUE_UNBAN = r.hgetall('F2B_QUEUE_UNBAN')
|
||||
if QUEUE_UNBAN:
|
||||
|
@ -425,7 +446,9 @@ def autopurge():
|
|||
unban(str(net))
|
||||
for net in bans.copy():
|
||||
if bans[net]['attempts'] >= MAX_ATTEMPTS:
|
||||
if time.time() - bans[net]['last_attempt'] > BAN_TIME:
|
||||
NET_BAN_TIME = BAN_TIME if not BAN_TIME_INCREMENT else BAN_TIME * 2 ** bans[net]['ban_counter']
|
||||
TIME_SINCE_LAST_ATTEMPT = time.time() - bans[net]['last_attempt']
|
||||
if TIME_SINCE_LAST_ATTEMPT > NET_BAN_TIME or TIME_SINCE_LAST_ATTEMPT > MAX_BAN_TIME:
|
||||
unban(net)
|
||||
|
||||
def isIpNetwork(address):
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
FROM alpine:3.17
|
||||
LABEL maintainer "Andre Peters <andre.peters@servercow.de>"
|
||||
LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>"
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
|
|
|
@ -1,12 +1,18 @@
|
|||
FROM php:8.1-fpm-alpine3.17
|
||||
LABEL maintainer "Andre Peters <andre.peters@servercow.de>"
|
||||
FROM php:8.2-fpm-alpine3.17
|
||||
LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>"
|
||||
|
||||
ENV APCU_PECL 5.1.22
|
||||
ENV IMAGICK_PECL 3.7.0
|
||||
ENV MAILPARSE_PECL 3.1.4
|
||||
ENV MEMCACHED_PECL 3.2.0
|
||||
ENV REDIS_PECL 5.3.7
|
||||
ENV COMPOSER 2.4.4
|
||||
# renovate: datasource=github-tags depName=krakjoe/apcu versioning=semver-coerced
|
||||
ARG APCU_PECL_VERSION=5.1.22
|
||||
# renovate: datasource=github-tags depName=Imagick/imagick versioning=semver-coerced
|
||||
ARG IMAGICK_PECL_VERSION=3.7.0
|
||||
# renovate: datasource=github-tags depName=php/pecl-mail-mailparse versioning=semver-coerced
|
||||
ARG MAILPARSE_PECL_VERSION=3.1.4
|
||||
# renovate: datasource=github-tags depName=php-memcached-dev/php-memcached versioning=semver-coerced
|
||||
ARG MEMCACHED_PECL_VERSION=3.2.0
|
||||
# renovate: datasource=github-tags depName=phpredis/phpredis versioning=semver-coerced
|
||||
ARG REDIS_PECL_VERSION=5.3.7
|
||||
# renovate: datasource=github-tags depName=composer/composer versioning=semver-coerced
|
||||
ARG COMPOSER_VERSION=2.5.5
|
||||
|
||||
RUN apk add -U --no-cache autoconf \
|
||||
aspell-dev \
|
||||
|
@ -46,6 +52,7 @@ RUN apk add -U --no-cache autoconf \
|
|||
libxpm-dev \
|
||||
libzip \
|
||||
libzip-dev \
|
||||
linux-headers \
|
||||
make \
|
||||
mysql-client \
|
||||
openldap-dev \
|
||||
|
@ -55,11 +62,11 @@ RUN apk add -U --no-cache autoconf \
|
|||
samba-client \
|
||||
zlib-dev \
|
||||
tzdata \
|
||||
&& pecl install mailparse-${MAILPARSE_PECL} \
|
||||
&& pecl install redis-${REDIS_PECL} \
|
||||
&& pecl install memcached-${MEMCACHED_PECL} \
|
||||
&& pecl install APCu-${APCU_PECL} \
|
||||
&& pecl install imagick-${IMAGICK_PECL} \
|
||||
&& pecl install APCu-${APCU_PECL_VERSION} \
|
||||
&& pecl install imagick-${IMAGICK_PECL_VERSION} \
|
||||
&& pecl install mailparse-${MAILPARSE_PECL_VERSION} \
|
||||
&& pecl install memcached-${MEMCACHED_PECL_VERSION} \
|
||||
&& pecl install redis-${REDIS_PECL_VERSION} \
|
||||
&& docker-php-ext-enable apcu imagick memcached mailparse redis \
|
||||
&& pecl clear-cache \
|
||||
&& docker-php-ext-configure intl \
|
||||
|
@ -69,10 +76,10 @@ RUN apk add -U --no-cache autoconf \
|
|||
--with-webp \
|
||||
--with-xpm \
|
||||
--with-avif \
|
||||
&& docker-php-ext-install -j 4 exif gd gettext intl ldap opcache pcntl pdo pdo_mysql pspell soap sockets zip bcmath gmp \
|
||||
&& docker-php-ext-install -j 4 exif gd gettext intl ldap opcache pcntl pdo pdo_mysql pspell soap sockets sysvsem zip bcmath gmp \
|
||||
&& docker-php-ext-configure imap --with-imap --with-imap-ssl \
|
||||
&& docker-php-ext-install -j 4 imap \
|
||||
&& curl --silent --show-error https://getcomposer.org/installer | php -- --version=${COMPOSER} \
|
||||
&& curl --silent --show-error https://getcomposer.org/installer | php -- --version=${COMPOSER_VERSION} \
|
||||
&& mv composer.phar /usr/local/bin/composer \
|
||||
&& chmod +x /usr/local/bin/composer \
|
||||
&& apk del --purge autoconf \
|
||||
|
@ -93,6 +100,7 @@ RUN apk add -U --no-cache autoconf \
|
|||
libxml2-dev \
|
||||
libxpm-dev \
|
||||
libzip-dev \
|
||||
linux-headers \
|
||||
make \
|
||||
openldap-dev \
|
||||
pcre-dev \
|
||||
|
@ -102,4 +110,4 @@ COPY ./docker-entrypoint.sh /
|
|||
|
||||
ENTRYPOINT ["/docker-entrypoint.sh"]
|
||||
|
||||
CMD ["php-fpm"]
|
||||
CMD ["php-fpm"]
|
|
@ -172,6 +172,24 @@ BEGIN
|
|||
END;
|
||||
//
|
||||
DELIMITER ;
|
||||
DROP EVENT IF EXISTS clean_sasl_log;
|
||||
DELIMITER //
|
||||
CREATE EVENT clean_sasl_log
|
||||
ON SCHEDULE EVERY 1 DAY DO
|
||||
BEGIN
|
||||
DELETE sasl_log.* FROM sasl_log
|
||||
LEFT JOIN (
|
||||
SELECT username, service, MAX(datetime) AS lastdate
|
||||
FROM sasl_log
|
||||
GROUP BY username, service
|
||||
) AS last ON sasl_log.username = last.username AND sasl_log.service = last.service
|
||||
WHERE datetime < DATE_SUB(NOW(), INTERVAL 31 DAY) AND datetime < lastdate;
|
||||
DELETE FROM sasl_log
|
||||
WHERE username NOT IN (SELECT username FROM mailbox) AND
|
||||
datetime < DATE_SUB(NOW(), INTERVAL 31 DAY);
|
||||
END;
|
||||
//
|
||||
DELIMITER ;
|
||||
EOF
|
||||
fi
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
FROM debian:bullseye-slim
|
||||
LABEL maintainer "Andre Peters <andre.peters@servercow.de>"
|
||||
LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>"
|
||||
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
ENV LC_ALL C
|
||||
|
@ -17,10 +17,10 @@ RUN groupadd -g 102 postfix \
|
|||
ca-certificates \
|
||||
curl \
|
||||
dirmngr \
|
||||
dnsutils \
|
||||
dnsutils \
|
||||
gnupg \
|
||||
libsasl2-modules \
|
||||
mariadb-client \
|
||||
mariadb-client \
|
||||
perl \
|
||||
postfix \
|
||||
postfix-mysql \
|
||||
|
@ -32,7 +32,7 @@ RUN groupadd -g 102 postfix \
|
|||
syslog-ng \
|
||||
syslog-ng-core \
|
||||
syslog-ng-mod-redis \
|
||||
tzdata \
|
||||
tzdata \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& touch /etc/default/locale \
|
||||
&& printf '#!/bin/bash\n/usr/sbin/postconf -c /opt/postfix/conf "$@"' > /usr/local/sbin/postconf \
|
||||
|
|
|
@ -393,12 +393,101 @@ query = SELECT goto FROM spamalias
|
|||
AND validity >= UNIX_TIMESTAMP()
|
||||
EOF
|
||||
|
||||
sed -i '/User overrides/q' /opt/postfix/conf/main.cf
|
||||
if [ ! -f /opt/postfix/conf/dns_blocklists.cf ]; then
|
||||
cat <<EOF > /opt/postfix/conf/dns_blocklists.cf
|
||||
# This file can be edited.
|
||||
# Delete this file and restart postfix container to revert any changes.
|
||||
postscreen_dnsbl_sites = wl.mailspike.net=127.0.0.[18;19;20]*-2
|
||||
hostkarma.junkemailfilter.com=127.0.0.1*-2
|
||||
list.dnswl.org=127.0.[0..255].0*-2
|
||||
list.dnswl.org=127.0.[0..255].1*-4
|
||||
list.dnswl.org=127.0.[0..255].2*-6
|
||||
list.dnswl.org=127.0.[0..255].3*-8
|
||||
ix.dnsbl.manitu.net*2
|
||||
bl.spamcop.net*2
|
||||
bl.suomispam.net*2
|
||||
hostkarma.junkemailfilter.com=127.0.0.2*3
|
||||
hostkarma.junkemailfilter.com=127.0.0.4*2
|
||||
hostkarma.junkemailfilter.com=127.0.1.2*1
|
||||
backscatter.spameatingmonkey.net*2
|
||||
bl.ipv6.spameatingmonkey.net*2
|
||||
bl.spameatingmonkey.net*2
|
||||
b.barracudacentral.org=127.0.0.2*7
|
||||
bl.mailspike.net=127.0.0.2*5
|
||||
bl.mailspike.net=127.0.0.[10;11;12]*4
|
||||
dnsbl.sorbs.net=127.0.0.10*8
|
||||
dnsbl.sorbs.net=127.0.0.5*6
|
||||
dnsbl.sorbs.net=127.0.0.7*3
|
||||
dnsbl.sorbs.net=127.0.0.8*2
|
||||
dnsbl.sorbs.net=127.0.0.6*2
|
||||
dnsbl.sorbs.net=127.0.0.9*2
|
||||
EOF
|
||||
fi
|
||||
DNSBL_CONFIG=$(grep -v '^#' /opt/postfix/conf/dns_blocklists.cf | grep '\S')
|
||||
|
||||
if [ ! -z "$DNSBL_CONFIG" ]; then
|
||||
echo -e "\e[33mChecking if ASN for your IP is listed for Spamhaus Bad ASN List...\e[0m"
|
||||
if [ -n "$SPAMHAUS_DQS_KEY" ]; then
|
||||
echo -e "\e[32mDetected SPAMHAUS_DQS_KEY variable from mailcow.conf...\e[0m"
|
||||
echo -e "\e[33mUsing DQS Blocklists from Spamhaus!\e[0m"
|
||||
SPAMHAUS_DNSBL_CONFIG=$(cat <<EOF
|
||||
${SPAMHAUS_DQS_KEY}.zen.dq.spamhaus.net=127.0.0.[4..7]*6
|
||||
${SPAMHAUS_DQS_KEY}.zen.dq.spamhaus.net=127.0.0.[10;11]*8
|
||||
${SPAMHAUS_DQS_KEY}.zen.dq.spamhaus.net=127.0.0.3*4
|
||||
${SPAMHAUS_DQS_KEY}.zen.dq.spamhaus.net=127.0.0.2*3
|
||||
postscreen_dnsbl_reply_map = texthash:/opt/postfix/conf/dnsbl_reply.map
|
||||
EOF
|
||||
|
||||
cat <<EOF > /opt/postfix/conf/dnsbl_reply.map
|
||||
# Autogenerated by mailcow, using Spamhaus DQS reply domains
|
||||
${SPAMHAUS_DQS_KEY}.sbl.dq.spamhaus.net sbl.spamhaus.org
|
||||
${SPAMHAUS_DQS_KEY}.xbl.dq.spamhaus.net xbl.spamhaus.org
|
||||
${SPAMHAUS_DQS_KEY}.pbl.dq.spamhaus.net pbl.spamhaus.org
|
||||
${SPAMHAUS_DQS_KEY}.zen.dq.spamhaus.net zen.spamhaus.org
|
||||
${SPAMHAUS_DQS_KEY}.dbl.dq.spamhaus.net dbl.spamhaus.org
|
||||
${SPAMHAUS_DQS_KEY}.zrd.dq.spamhaus.net zrd.spamhaus.org
|
||||
EOF
|
||||
)
|
||||
else
|
||||
if [ -f "/opt/postfix/conf/dnsbl_reply.map" ]; then
|
||||
rm /opt/postfix/conf/dnsbl_reply.map
|
||||
fi
|
||||
response=$(curl --connect-timeout 15 --max-time 30 -s -o /dev/null -w "%{http_code}" "https://asn-check.mailcow.email")
|
||||
if [ "$response" -eq 503 ]; then
|
||||
echo -e "\e[31mThe AS of your IP is listed as a banned AS from Spamhaus!\e[0m"
|
||||
echo -e "\e[33mNo SPAMHAUS_DQS_KEY found... Skipping Spamhaus blocklists entirely!\e[0m"
|
||||
SPAMHAUS_DNSBL_CONFIG=""
|
||||
elif [ "$response" -eq 200 ]; then
|
||||
echo -e "\e[32mThe AS of your IP is NOT listed as a banned AS from Spamhaus!\e[0m"
|
||||
echo -e "\e[33mUsing the open Spamhaus blocklists.\e[0m"
|
||||
SPAMHAUS_DNSBL_CONFIG=$(cat <<EOF
|
||||
zen.spamhaus.org=127.0.0.[10;11]*8
|
||||
zen.spamhaus.org=127.0.0.[4..7]*6
|
||||
zen.spamhaus.org=127.0.0.3*4
|
||||
zen.spamhaus.org=127.0.0.2*3
|
||||
EOF
|
||||
)
|
||||
|
||||
else
|
||||
echo -e "\e[31mWe couldn't determine your AS... (maybe DNS/Network issue?) Response Code: $response\e[0m"
|
||||
echo -e "\e[33mDeactivating Spamhaus DNS Blocklists to be on the safe site!\e[0m"
|
||||
SPAMHAUS_DNSBL_CONFIG=""
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# Reset main.cf
|
||||
sed -i '/Overrides/q' /opt/postfix/conf/main.cf
|
||||
echo >> /opt/postfix/conf/main.cf
|
||||
# Append postscreen dnsbl sites to main.cf
|
||||
if [ ! -z "$DNSBL_CONFIG" ]; then
|
||||
echo -e "${DNSBL_CONFIG}\n${SPAMHAUS_DNSBL_CONFIG}" >> /opt/postfix/conf/main.cf
|
||||
fi
|
||||
# Append user overrides
|
||||
echo -e "\n# User Overrides" >> /opt/postfix/conf/main.cf
|
||||
touch /opt/postfix/conf/extra.cf
|
||||
sed -i '/myhostname/d' /opt/postfix/conf/extra.cf
|
||||
echo -e "myhostname = ${MAILCOW_HOSTNAME}\n$(cat /opt/postfix/conf/extra.cf)" > /opt/postfix/conf/extra.cf
|
||||
|
||||
cat /opt/postfix/conf/extra.cf >> /opt/postfix/conf/main.cf
|
||||
|
||||
if [ ! -f /opt/postfix/conf/custom_transport.pcre ]; then
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
FROM debian:bullseye-slim
|
||||
LABEL maintainer "Andre Peters <andre.peters@tinc.gmbh>"
|
||||
LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>"
|
||||
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
ARG CODENAME=bullseye
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
FROM debian:bullseye-slim
|
||||
LABEL maintainer "Andre Peters <andre.peters@servercow.de>"
|
||||
LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>"
|
||||
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
ARG SOGO_DEBIAN_REPOSITORY=http://packages.sogo.nu/nightly/5/debian/
|
||||
# renovate: datasource=github-releases depName=tianon/gosu versioning=semver-coerced
|
||||
ARG GOSU_VERSION=1.16
|
||||
ENV LC_ALL C
|
||||
ENV GOSU_VERSION 1.14
|
||||
|
||||
# Prerequisites
|
||||
RUN echo "Building from repository $SOGO_DEBIAN_REPOSITORY" \
|
||||
|
|
|
@ -2,7 +2,8 @@ FROM solr:7.7-slim
|
|||
|
||||
USER root
|
||||
|
||||
ENV GOSU_VERSION 1.11
|
||||
# renovate: datasource=github-releases depName=tianon/gosu versioning=semver-coerced
|
||||
ARG GOSU_VERSION=1.16
|
||||
|
||||
COPY solr.sh /
|
||||
COPY solr-config-7.7.0.xml /
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
FROM alpine:3.17
|
||||
|
||||
LABEL maintainer "Andre Peters <andre.peters@servercow.de>"
|
||||
LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>"
|
||||
|
||||
RUN apk add --update --no-cache \
|
||||
curl \
|
||||
|
|
|
@ -24,7 +24,7 @@ server {
|
|||
add_header X-Download-Options "noopen" always;
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
add_header X-Permitted-Cross-Domain-Policies "none" always;
|
||||
add_header X-Robots-Tag "none" always;
|
||||
add_header X-Robots-Tag "noindex, nofollow" always;
|
||||
add_header X-XSS-Protection "1; mode=block" always;
|
||||
|
||||
fastcgi_hide_header X-Powered-By;
|
||||
|
|
|
@ -24,6 +24,11 @@ mail_plugins = </etc/dovecot/mail_plugins
|
|||
mail_attachment_fs = crypt:set_prefix=mail_crypt_global:posix:
|
||||
mail_attachment_dir = /var/attachments
|
||||
mail_attachment_min_size = 128k
|
||||
# Significantly speeds up very large mailboxes, but is only safe to enable if
|
||||
# you do not manually modify the files in the `cur` directories in
|
||||
# mailcowdockerized_vmail-vol-1.
|
||||
# https://docs.mailcow.email/manual-guides/Dovecot/u_e-dovecot-performance/
|
||||
maildir_very_dirty_syncs = yes
|
||||
|
||||
# Dovecot 2.2
|
||||
#ssl_protocols = !SSLv3
|
||||
|
|
|
@ -114,7 +114,7 @@
|
|||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_redirect off;
|
||||
error_page 403 /_rspamderror.php;
|
||||
error_page 401 /_rspamderror.php;
|
||||
}
|
||||
proxy_pass http://rspamd:11334/;
|
||||
proxy_set_header Host $http_host;
|
||||
|
|
|
@ -40,34 +40,6 @@ postscreen_blacklist_action = drop
|
|||
postscreen_cache_cleanup_interval = 24h
|
||||
postscreen_cache_map = proxy:btree:$data_directory/postscreen_cache
|
||||
postscreen_dnsbl_action = enforce
|
||||
postscreen_dnsbl_sites = wl.mailspike.net=127.0.0.[18;19;20]*-2
|
||||
hostkarma.junkemailfilter.com=127.0.0.1*-2
|
||||
list.dnswl.org=127.0.[0..255].0*-2
|
||||
list.dnswl.org=127.0.[0..255].1*-4
|
||||
list.dnswl.org=127.0.[0..255].2*-6
|
||||
list.dnswl.org=127.0.[0..255].3*-8
|
||||
ix.dnsbl.manitu.net*2
|
||||
bl.spamcop.net*2
|
||||
bl.suomispam.net*2
|
||||
hostkarma.junkemailfilter.com=127.0.0.2*3
|
||||
hostkarma.junkemailfilter.com=127.0.0.4*2
|
||||
hostkarma.junkemailfilter.com=127.0.1.2*1
|
||||
backscatter.spameatingmonkey.net*2
|
||||
bl.ipv6.spameatingmonkey.net*2
|
||||
bl.spameatingmonkey.net*2
|
||||
b.barracudacentral.org=127.0.0.2*7
|
||||
bl.mailspike.net=127.0.0.2*5
|
||||
bl.mailspike.net=127.0.0.[10;11;12]*4
|
||||
dnsbl.sorbs.net=127.0.0.10*8
|
||||
dnsbl.sorbs.net=127.0.0.5*6
|
||||
dnsbl.sorbs.net=127.0.0.7*3
|
||||
dnsbl.sorbs.net=127.0.0.8*2
|
||||
dnsbl.sorbs.net=127.0.0.6*2
|
||||
dnsbl.sorbs.net=127.0.0.9*2
|
||||
zen.spamhaus.org=127.0.0.[10;11]*8
|
||||
zen.spamhaus.org=127.0.0.[4..7]*6
|
||||
zen.spamhaus.org=127.0.0.3*4
|
||||
zen.spamhaus.org=127.0.0.2*3
|
||||
postscreen_dnsbl_threshold = 6
|
||||
postscreen_dnsbl_ttl = 5m
|
||||
postscreen_greet_action = enforce
|
||||
|
@ -197,4 +169,4 @@ smtps_smtpd_tls_mandatory_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1
|
|||
parent_domain_matches_subdomains = debug_peer_list,fast_flush_domains,mynetworks,qmqpd_authorized_clients
|
||||
|
||||
# DO NOT EDIT ANYTHING BELOW #
|
||||
# User overrides #
|
||||
# Overrides #
|
||||
|
|
|
@ -1,15 +1,20 @@
|
|||
# Whitelist generated by Postwhite v3.4 on Mon 21 Mar 2022 06:50:26 PM CET
|
||||
# Whitelist generated by Postwhite v3.4 on Mon Jul 31 10:06:06 UTC 2023
|
||||
# https://github.com/stevejenkins/postwhite/
|
||||
# 1898 total rules
|
||||
# 2043 total rules
|
||||
2a00:1450:4000::/36 permit
|
||||
2a01:111:f400::/48 permit
|
||||
2a01:111:f403::/48 permit
|
||||
2a01:4180:4050:0400::/64 permit
|
||||
2a01:4180:4050:0800::/64 permit
|
||||
2a01:4180:4051:0400::/64 permit
|
||||
2a01:4180:4051:0800::/64 permit
|
||||
2a01:111:f403:8000::/50 permit
|
||||
2a01:111:f403::/49 permit
|
||||
2a01:111:f403:c000::/51 permit
|
||||
2a01:111:f403:f000::/52 permit
|
||||
2a02:a60:0:5::/64 permit
|
||||
2c0f:fb50:4000::/36 permit
|
||||
2.207.151.53 permit
|
||||
3.14.230.16 permit
|
||||
3.70.123.177 permit
|
||||
3.93.157.0/24 permit
|
||||
3.129.120.190 permit
|
||||
3.210.190.0/24 permit
|
||||
8.20.114.31 permit
|
||||
8.25.194.0/23 permit
|
||||
8.25.196.0/23 permit
|
||||
|
@ -19,41 +24,53 @@
|
|||
13.70.32.43 permit
|
||||
13.72.50.45 permit
|
||||
13.74.143.28 permit
|
||||
13.77.161.179 permit
|
||||
13.78.233.182 permit
|
||||
13.92.31.129 permit
|
||||
13.110.208.0/21 permit
|
||||
13.110.209.0/24 permit
|
||||
13.110.216.0/22 permit
|
||||
13.110.224.0/20 permit
|
||||
13.111.0.0/16 permit
|
||||
17.41.0.0/16 permit
|
||||
15.200.21.50 permit
|
||||
15.200.44.248 permit
|
||||
15.200.201.185 permit
|
||||
17.57.155.0/24 permit
|
||||
17.57.156.0/24 permit
|
||||
17.58.0.0/16 permit
|
||||
17.110.0.0/15 permit
|
||||
17.142.0.0/15 permit
|
||||
17.162.0.0/15 permit
|
||||
17.164.0.0/16 permit
|
||||
17.171.37.0/24 permit
|
||||
17.172.0.0/16 permit
|
||||
17.179.168.0/23 permit
|
||||
18.156.89.250 permit
|
||||
18.157.243.190 permit
|
||||
18.194.95.56 permit
|
||||
18.198.96.88 permit
|
||||
20.47.149.138 permit
|
||||
20.48.0.0/12 permit
|
||||
18.208.124.128/25 permit
|
||||
18.216.232.154 permit
|
||||
18.234.1.244 permit
|
||||
18.236.40.242 permit
|
||||
20.51.6.32/30 permit
|
||||
20.52.52.2 permit
|
||||
20.52.128.133 permit
|
||||
20.59.80.4/30 permit
|
||||
20.63.210.192/28 permit
|
||||
20.64.0.0/10 permit
|
||||
20.69.8.108/30 permit
|
||||
20.70.246.20 permit
|
||||
20.76.201.171 permit
|
||||
20.83.222.104/30 permit
|
||||
20.88.157.184/30 permit
|
||||
20.94.180.64/28 permit
|
||||
20.97.34.220/30 permit
|
||||
20.98.148.156/30 permit
|
||||
20.98.194.68/30 permit
|
||||
20.105.209.76/30 permit
|
||||
20.107.239.64/30 permit
|
||||
20.112.250.133 permit
|
||||
20.118.139.208/30 permit
|
||||
20.185.213.160/27 permit
|
||||
20.185.213.224/27 permit
|
||||
20.185.214.0/27 permit
|
||||
20.185.214.2 permit
|
||||
20.185.214.32/27 permit
|
||||
20.185.214.64/27 permit
|
||||
20.192.0.0/10 permit
|
||||
23.100.85.1 permit
|
||||
20.231.239.246 permit
|
||||
20.236.44.162 permit
|
||||
23.103.224.0/19 permit
|
||||
23.249.208.0/20 permit
|
||||
23.251.224.0/19 permit
|
||||
|
@ -78,46 +95,38 @@
|
|||
27.123.206.56/29 permit
|
||||
27.123.206.76/30 permit
|
||||
27.123.206.80/28 permit
|
||||
34.194.25.167 permit
|
||||
34.194.144.120 permit
|
||||
31.25.48.222 permit
|
||||
34.195.217.107 permit
|
||||
34.202.239.6 permit
|
||||
34.212.163.75 permit
|
||||
34.215.104.144 permit
|
||||
34.225.212.172 permit
|
||||
34.247.168.44 permit
|
||||
35.161.32.253 permit
|
||||
35.167.93.243 permit
|
||||
35.176.132.251 permit
|
||||
35.190.247.0/24 permit
|
||||
35.191.0.0/16 permit
|
||||
37.188.97.188 permit
|
||||
37.218.248.47 permit
|
||||
37.218.249.47 permit
|
||||
37.218.251.62 permit
|
||||
39.156.163.64/29 permit
|
||||
40.71.187.0/24 permit
|
||||
40.76.4.15 permit
|
||||
40.77.102.222 permit
|
||||
40.92.0.0/15 permit
|
||||
40.97.116.82 permit
|
||||
40.97.128.194 permit
|
||||
40.97.148.226 permit
|
||||
40.97.153.146 permit
|
||||
40.97.156.114 permit
|
||||
40.97.160.2 permit
|
||||
40.97.161.50 permit
|
||||
40.97.164.146 permit
|
||||
40.107.0.0/16 permit
|
||||
40.112.65.63 permit
|
||||
40.112.72.205 permit
|
||||
40.113.200.201 permit
|
||||
40.117.80.0/24 permit
|
||||
40.121.71.46 permit
|
||||
41.74.192.0/22 permit
|
||||
41.74.196.0/22 permit
|
||||
41.74.200.0/23 permit
|
||||
41.74.204.0/23 permit
|
||||
41.74.206.0/24 permit
|
||||
42.159.163.81 permit
|
||||
42.159.163.82 permit
|
||||
42.159.163.83 permit
|
||||
43.228.184.0/22 permit
|
||||
44.206.138.57 permit
|
||||
44.209.42.157 permit
|
||||
44.236.56.93 permit
|
||||
44.238.220.251 permit
|
||||
46.19.168.0/23 permit
|
||||
46.226.48.0/21 permit
|
||||
46.228.36.37 permit
|
||||
46.228.36.38/31 permit
|
||||
|
@ -167,6 +176,8 @@
|
|||
46.243.88.175 permit
|
||||
46.243.88.176 permit
|
||||
46.243.88.177 permit
|
||||
46.243.95.179 permit
|
||||
46.243.95.180 permit
|
||||
50.18.45.249 permit
|
||||
50.18.121.236 permit
|
||||
50.18.121.248 permit
|
||||
|
@ -178,11 +189,6 @@
|
|||
50.31.32.0/19 permit
|
||||
50.31.156.96/27 permit
|
||||
50.31.205.0/24 permit
|
||||
51.4.71.62 permit
|
||||
51.4.72.0/24 permit
|
||||
51.4.80.0/27 permit
|
||||
51.5.72.0/24 permit
|
||||
51.5.80.0/27 permit
|
||||
51.137.58.21 permit
|
||||
51.140.75.55 permit
|
||||
51.144.100.179 permit
|
||||
|
@ -191,17 +197,28 @@
|
|||
52.5.230.59 permit
|
||||
52.27.5.72 permit
|
||||
52.27.28.47 permit
|
||||
52.33.191.91 permit
|
||||
52.28.63.81 permit
|
||||
52.36.138.31 permit
|
||||
52.37.142.146 permit
|
||||
52.38.191.253 permit
|
||||
52.41.64.145 permit
|
||||
52.58.216.183 permit
|
||||
52.59.143.3 permit
|
||||
52.60.41.5 permit
|
||||
52.60.115.116 permit
|
||||
52.61.91.9 permit
|
||||
52.71.0.205 permit
|
||||
52.82.172.0/22 permit
|
||||
52.94.124.0/28 permit
|
||||
52.95.48.152/29 permit
|
||||
52.95.49.88/29 permit
|
||||
52.96.91.34 permit
|
||||
52.96.111.82 permit
|
||||
52.96.172.98 permit
|
||||
52.96.214.50 permit
|
||||
52.96.222.194 permit
|
||||
52.96.222.226 permit
|
||||
52.96.223.2 permit
|
||||
52.96.228.130 permit
|
||||
52.96.229.242 permit
|
||||
52.100.0.0/14 permit
|
||||
52.119.213.144/28 permit
|
||||
52.160.39.140 permit
|
||||
|
@ -214,23 +231,29 @@
|
|||
52.222.73.83 permit
|
||||
52.222.73.120 permit
|
||||
52.222.75.85 permit
|
||||
52.222.89.228 permit
|
||||
52.234.172.96/28 permit
|
||||
52.236.28.240/28 permit
|
||||
52.237.141.173 permit
|
||||
52.244.206.214 permit
|
||||
52.247.53.144 permit
|
||||
52.250.107.196 permit
|
||||
52.250.126.174 permit
|
||||
52.251.55.143 permit
|
||||
54.90.148.255 permit
|
||||
54.156.255.69 permit
|
||||
54.172.97.247 permit
|
||||
54.174.52.0/24 permit
|
||||
54.174.53.128/30 permit
|
||||
54.174.57.0/24 permit
|
||||
54.174.59.0/24 permit
|
||||
54.174.60.0/23 permit
|
||||
54.174.63.0/24 permit
|
||||
54.186.193.102 permit
|
||||
54.191.223.5 permit
|
||||
54.191.223.56 permit
|
||||
54.194.61.95 permit
|
||||
54.195.113.45 permit
|
||||
54.213.20.246 permit
|
||||
54.214.39.184 permit
|
||||
54.216.77.168 permit
|
||||
54.221.227.204 permit
|
||||
54.240.0.0/18 permit
|
||||
54.240.64.0/19 permit
|
||||
54.240.96.0/19 permit
|
||||
|
@ -238,7 +261,9 @@
|
|||
54.244.54.130 permit
|
||||
54.244.242.0/24 permit
|
||||
54.246.232.180 permit
|
||||
54.255.61.23 permit
|
||||
62.13.128.0/24 permit
|
||||
62.13.128.150 permit
|
||||
62.13.129.128/25 permit
|
||||
62.13.136.0/22 permit
|
||||
62.13.140.0/22 permit
|
||||
|
@ -249,22 +274,29 @@
|
|||
62.17.146.128/26 permit
|
||||
62.140.7.0/24 permit
|
||||
62.140.10.21 permit
|
||||
62.179.121.0/24 permit
|
||||
62.201.172.0/27 permit
|
||||
62.201.172.32/27 permit
|
||||
62.253.227.114 permit
|
||||
63.32.13.159 permit
|
||||
63.80.14.0/23 permit
|
||||
63.111.28.137 permit
|
||||
63.128.21.0/24 permit
|
||||
63.143.57.128/25 permit
|
||||
63.143.59.128/25 permit
|
||||
64.18.0.0/20 permit
|
||||
64.20.241.45 permit
|
||||
64.34.47.128/27 permit
|
||||
64.34.57.192/26 permit
|
||||
64.69.212.0/24 permit
|
||||
64.71.149.160/28 permit
|
||||
64.79.155.0/24 permit
|
||||
64.79.155.192 permit
|
||||
64.79.155.193 permit
|
||||
64.79.155.205 permit
|
||||
64.79.155.206 permit
|
||||
64.89.44.85 permit
|
||||
64.89.45.80 permit
|
||||
64.89.45.194 permit
|
||||
64.89.45.196 permit
|
||||
64.95.144.196 permit
|
||||
64.127.115.252 permit
|
||||
64.132.88.0/23 permit
|
||||
64.132.92.0/24 permit
|
||||
|
@ -290,6 +322,7 @@
|
|||
64.207.219.71 permit
|
||||
64.207.219.72 permit
|
||||
64.207.219.73 permit
|
||||
64.207.219.75 permit
|
||||
64.207.219.77 permit
|
||||
64.207.219.78 permit
|
||||
64.207.219.79 permit
|
||||
|
@ -300,9 +333,6 @@
|
|||
64.207.219.142 permit
|
||||
64.207.219.143 permit
|
||||
64.233.160.0/19 permit
|
||||
65.38.115.76 permit
|
||||
65.38.115.84 permit
|
||||
65.39.215.0/24 permit
|
||||
65.52.80.137 permit
|
||||
65.54.51.64/26 permit
|
||||
65.54.61.64/26 permit
|
||||
|
@ -342,6 +372,10 @@
|
|||
66.111.4.225 permit
|
||||
66.111.4.229 permit
|
||||
66.111.4.230 permit
|
||||
66.119.150.192/26 permit
|
||||
66.135.202.0/27 permit
|
||||
66.135.215.0/24 permit
|
||||
66.135.222.1 permit
|
||||
66.162.193.226/31 permit
|
||||
66.163.184.0/21 permit
|
||||
66.163.184.0/24 permit
|
||||
|
@ -373,7 +407,8 @@
|
|||
66.196.81.234 permit
|
||||
66.211.168.230/31 permit
|
||||
66.211.170.86/31 permit
|
||||
66.211.170.88/30 permit
|
||||
66.211.170.88/29 permit
|
||||
66.211.184.0/23 permit
|
||||
66.218.74.64/30 permit
|
||||
66.218.74.68/31 permit
|
||||
66.218.75.112/30 permit
|
||||
|
@ -445,6 +480,8 @@
|
|||
68.142.230.72/30 permit
|
||||
68.142.230.76/31 permit
|
||||
68.142.230.78 permit
|
||||
68.232.140.138 permit
|
||||
68.232.157.143 permit
|
||||
68.232.192.0/20 permit
|
||||
69.63.178.128/25 permit
|
||||
69.63.181.0/24 permit
|
||||
|
@ -452,6 +489,10 @@
|
|||
69.65.42.195 permit
|
||||
69.65.49.192/29 permit
|
||||
69.72.32.0/20 permit
|
||||
69.72.40.93 permit
|
||||
69.72.40.94/31 permit
|
||||
69.72.40.96/30 permit
|
||||
69.72.47.205 permit
|
||||
69.147.84.227 permit
|
||||
69.162.98.0/24 permit
|
||||
69.169.224.0/20 permit
|
||||
|
@ -460,7 +501,7 @@
|
|||
70.37.151.128/25 permit
|
||||
70.42.149.0/24 permit
|
||||
70.42.149.35 permit
|
||||
72.3.185.0/24 permit
|
||||
72.3.237.64/28 permit
|
||||
72.14.192.0/18 permit
|
||||
72.21.192.0/19 permit
|
||||
72.21.217.142 permit
|
||||
|
@ -522,15 +563,11 @@
|
|||
72.30.239.228/31 permit
|
||||
72.30.239.244/30 permit
|
||||
72.30.239.248/31 permit
|
||||
72.32.154.0/24 permit
|
||||
72.32.217.0/24 permit
|
||||
72.32.243.0/24 permit
|
||||
72.34.168.76 permit
|
||||
72.34.168.80 permit
|
||||
72.34.168.85 permit
|
||||
72.34.168.86 permit
|
||||
72.52.72.32/28 permit
|
||||
72.52.72.36 permit
|
||||
74.6.128.0/21 permit
|
||||
74.6.128.0/24 permit
|
||||
74.6.129.0/24 permit
|
||||
|
@ -558,8 +595,11 @@
|
|||
74.112.67.243 permit
|
||||
74.125.0.0/16 permit
|
||||
74.202.227.40 permit
|
||||
74.208.4.192/26 permit
|
||||
74.208.5.64/26 permit
|
||||
74.208.122.0/26 permit
|
||||
74.209.250.0/24 permit
|
||||
74.209.250.12 permit
|
||||
76.223.128.0/19 permit
|
||||
76.223.176.0/20 permit
|
||||
77.238.176.0/22 permit
|
||||
77.238.176.0/24 permit
|
||||
|
@ -583,7 +623,13 @@
|
|||
77.238.189.146/31 permit
|
||||
77.238.189.148/30 permit
|
||||
81.223.46.0/27 permit
|
||||
84.16.77.1 permit
|
||||
82.165.159.0/24 permit
|
||||
82.165.159.0/26 permit
|
||||
82.165.229.31 permit
|
||||
82.165.229.130 permit
|
||||
82.165.230.21 permit
|
||||
82.165.230.22 permit
|
||||
84.116.36.0/24 permit
|
||||
85.158.136.0/21 permit
|
||||
86.61.88.25 permit
|
||||
87.198.219.130 permit
|
||||
|
@ -624,11 +670,11 @@
|
|||
87.248.117.201 permit
|
||||
87.248.117.202 permit
|
||||
87.248.117.205 permit
|
||||
87.252.219.254 permit
|
||||
87.253.232.0/21 permit
|
||||
89.22.108.0/24 permit
|
||||
91.194.248.0/23 permit
|
||||
91.211.240.0/22 permit
|
||||
91.220.42.0/24 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
|
||||
|
@ -638,6 +684,7 @@
|
|||
96.43.148.64/28 permit
|
||||
96.43.148.64/31 permit
|
||||
96.43.151.64/28 permit
|
||||
98.97.248.0/21 permit
|
||||
98.136.44.181 permit
|
||||
98.136.44.182/31 permit
|
||||
98.136.44.184 permit
|
||||
|
@ -1142,23 +1189,21 @@
|
|||
98.139.245.212/31 permit
|
||||
99.78.197.208/28 permit
|
||||
103.2.140.0/22 permit
|
||||
103.9.8.121 permit
|
||||
103.9.8.122 permit
|
||||
103.9.8.123 permit
|
||||
103.9.96.0/22 permit
|
||||
103.13.69.0/24 permit
|
||||
103.28.42.0/24 permit
|
||||
103.47.204.0/22 permit
|
||||
103.96.21.0/24 permit
|
||||
103.96.22.0/24 permit
|
||||
103.96.23.0/24 permit
|
||||
103.151.192.0/23 permit
|
||||
103.237.104.0/22 permit
|
||||
103.168.172.128/27 permit
|
||||
104.43.243.237 permit
|
||||
104.44.112.128/25 permit
|
||||
104.47.0.0/17 permit
|
||||
104.130.96.0/28 permit
|
||||
104.130.122.0/23 permit
|
||||
104.214.25.77 permit
|
||||
104.215.148.63 permit
|
||||
104.215.186.3 permit
|
||||
104.245.209.192/26 permit
|
||||
106.10.144.64/27 permit
|
||||
106.10.144.100/31 permit
|
||||
|
@ -1320,6 +1365,8 @@
|
|||
117.120.16.0/21 permit
|
||||
119.42.242.52/31 permit
|
||||
119.42.242.156 permit
|
||||
121.244.91.48 permit
|
||||
122.15.156.182 permit
|
||||
123.126.78.64/29 permit
|
||||
124.47.150.0/24 permit
|
||||
124.47.189.0/24 permit
|
||||
|
@ -1335,20 +1382,35 @@
|
|||
128.127.70.0/26 permit
|
||||
128.245.0.0/20 permit
|
||||
128.245.64.0/20 permit
|
||||
128.245.176.0/20 permit
|
||||
128.245.242.0/24 permit
|
||||
128.245.242.16 permit
|
||||
128.245.242.17 permit
|
||||
128.245.242.18 permit
|
||||
128.245.243.0/24 permit
|
||||
128.245.244.0/24 permit
|
||||
128.245.245.0/24 permit
|
||||
128.245.246.0/24 permit
|
||||
128.245.247.0/24 permit
|
||||
129.41.77.70 permit
|
||||
129.41.169.249 permit
|
||||
129.80.5.164 permit
|
||||
129.80.67.121 permit
|
||||
129.146.88.28 permit
|
||||
129.146.147.105 permit
|
||||
129.146.236.58 permit
|
||||
129.151.67.221 permit
|
||||
129.153.62.216 permit
|
||||
129.153.104.71 permit
|
||||
129.153.168.146 permit
|
||||
129.153.190.200 permit
|
||||
129.153.194.228 permit
|
||||
129.159.87.137 permit
|
||||
129.213.195.191 permit
|
||||
130.61.9.72 permit
|
||||
130.211.0.0/22 permit
|
||||
130.248.172.0/24 permit
|
||||
130.248.173.0/24 permit
|
||||
131.107.0.0/16 permit
|
||||
131.253.30.0/24 permit
|
||||
131.253.121.0/26 permit
|
||||
131.253.121.20 permit
|
||||
131.253.121.52 permit
|
||||
132.145.13.209 permit
|
||||
132.226.26.225 permit
|
||||
132.226.49.32 permit
|
||||
|
@ -1358,9 +1420,13 @@
|
|||
134.170.141.64/26 permit
|
||||
134.170.143.0/24 permit
|
||||
134.170.174.0/24 permit
|
||||
135.84.80.192/26 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
|
||||
136.143.160.0/24 permit
|
||||
136.143.161.0/24 permit
|
||||
136.143.182.0/23 permit
|
||||
136.143.184.0/24 permit
|
||||
136.143.188.0/24 permit
|
||||
|
@ -1369,34 +1435,53 @@
|
|||
136.147.176.0/20 permit
|
||||
136.147.176.0/24 permit
|
||||
136.147.182.0/24 permit
|
||||
136.179.50.206 permit
|
||||
138.91.172.26 permit
|
||||
139.60.152.0/22 permit
|
||||
139.178.64.159 permit
|
||||
139.178.64.195 permit
|
||||
139.138.35.44 permit
|
||||
139.138.46.121 permit
|
||||
139.138.46.176 permit
|
||||
139.138.46.219 permit
|
||||
139.138.57.55 permit
|
||||
139.138.58.119 permit
|
||||
139.180.17.0/24 permit
|
||||
141.148.159.229 permit
|
||||
141.193.32.0/23 permit
|
||||
143.55.224.0/21 permit
|
||||
143.55.232.0/22 permit
|
||||
143.55.236.0/22 permit
|
||||
143.244.80.0/20 permit
|
||||
144.24.6.140 permit
|
||||
144.34.8.247 permit
|
||||
144.34.9.247 permit
|
||||
144.34.32.247 permit
|
||||
144.34.33.247 permit
|
||||
144.178.36.0/24 permit
|
||||
144.178.38.0/24 permit
|
||||
145.253.228.160/29 permit
|
||||
145.253.239.128/29 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.182 permit
|
||||
146.88.28.0/24 permit
|
||||
146.101.78.0/24 permit
|
||||
147.75.65.173 permit
|
||||
147.75.65.174 permit
|
||||
147.75.98.190 permit
|
||||
147.28.36.0/24 permit
|
||||
147.160.158.0/24 permit
|
||||
147.243.1.47 permit
|
||||
147.243.1.48 permit
|
||||
147.243.1.153 permit
|
||||
147.243.128.24 permit
|
||||
147.243.128.26 permit
|
||||
148.105.0.14 permit
|
||||
148.105.0.0/16 permit
|
||||
148.105.8.0/21 permit
|
||||
149.72.0.0/16 permit
|
||||
149.97.173.180 permit
|
||||
150.230.98.160 permit
|
||||
152.67.105.195 permit
|
||||
152.69.200.236 permit
|
||||
155.248.208.51 permit
|
||||
157.55.0.192/26 permit
|
||||
157.55.1.128/26 permit
|
||||
157.55.2.0/25 permit
|
||||
|
@ -1412,32 +1497,43 @@
|
|||
157.56.232.0/21 permit
|
||||
157.56.240.0/20 permit
|
||||
157.56.248.0/21 permit
|
||||
157.58.30.128/25 permit
|
||||
157.58.196.96/29 permit
|
||||
157.58.249.3 permit
|
||||
157.151.208.65 permit
|
||||
157.255.1.64/29 permit
|
||||
158.101.211.207 permit
|
||||
158.120.80.0/21 permit
|
||||
158.247.16.0/20 permit
|
||||
159.92.157.0/24 permit
|
||||
159.92.157.16 permit
|
||||
159.92.157.17 permit
|
||||
159.92.157.18 permit
|
||||
159.92.158.0/24 permit
|
||||
159.92.159.0/24 permit
|
||||
159.92.160.0/24 permit
|
||||
159.92.161.0/24 permit
|
||||
159.92.162.0/24 permit
|
||||
159.112.240.0/20 permit
|
||||
159.112.242.162 permit
|
||||
159.135.132.128/25 permit
|
||||
159.135.140.80/29 permit
|
||||
159.135.224.0/20 permit
|
||||
159.135.228.10 permit
|
||||
159.183.0.0/16 permit
|
||||
160.1.62.192 permit
|
||||
161.38.192.0/20 permit
|
||||
161.38.204.0/22 permit
|
||||
161.71.32.0/19 permit
|
||||
161.71.64.0/20 permit
|
||||
162.208.119.181 permit
|
||||
162.247.216.0/22 permit
|
||||
163.47.180.0/22 permit
|
||||
163.47.180.0/23 permit
|
||||
163.114.130.16 permit
|
||||
163.114.132.120 permit
|
||||
165.173.128.0/24 permit
|
||||
166.78.68.0/22 permit
|
||||
166.78.68.221 permit
|
||||
166.78.69.146 permit
|
||||
166.78.69.169 permit
|
||||
166.78.69.170 permit
|
||||
166.78.71.131 permit
|
||||
|
@ -1457,10 +1553,13 @@
|
|||
167.216.129.210 permit
|
||||
167.216.131.180 permit
|
||||
167.220.67.232/29 permit
|
||||
167.220.67.238 permit
|
||||
168.138.5.36 permit
|
||||
168.138.73.51 permit
|
||||
168.245.0.0/17 permit
|
||||
169.148.129.0/24 permit
|
||||
169.148.131.0/24 permit
|
||||
170.10.68.0/22 permit
|
||||
170.10.128.0/24 permit
|
||||
170.10.129.0/24 permit
|
||||
170.10.133.0/24 permit
|
||||
172.217.0.0/19 permit
|
||||
|
@ -1475,10 +1574,8 @@
|
|||
173.194.0.0/16 permit
|
||||
173.203.79.182 permit
|
||||
173.203.81.39 permit
|
||||
173.224.160.128/25 permit
|
||||
173.224.160.188 permit
|
||||
173.224.161.128/25 permit
|
||||
173.228.155.0/24 permit
|
||||
173.224.165.0/26 permit
|
||||
174.36.84.8/29 permit
|
||||
174.36.84.16/29 permit
|
||||
174.36.84.32/29 permit
|
||||
|
@ -1491,6 +1588,7 @@
|
|||
174.36.114.152/29 permit
|
||||
174.37.67.28/30 permit
|
||||
174.129.203.189 permit
|
||||
175.41.215.51 permit
|
||||
176.32.105.0/24 permit
|
||||
176.32.127.0/24 permit
|
||||
178.236.10.128/26 permit
|
||||
|
@ -1498,8 +1596,9 @@
|
|||
182.50.76.0/22 permit
|
||||
182.50.78.64/28 permit
|
||||
183.240.219.64/29 permit
|
||||
185.4.120.0/23 permit
|
||||
185.4.122.0/24 permit
|
||||
185.12.80.0/22 permit
|
||||
185.28.196.0/22 permit
|
||||
185.58.84.93 permit
|
||||
185.58.85.0/24 permit
|
||||
185.58.86.0/24 permit
|
||||
|
@ -1509,9 +1608,13 @@
|
|||
185.80.93.204 permit
|
||||
185.80.93.227 permit
|
||||
185.80.95.31 permit
|
||||
185.90.20.0/22 permit
|
||||
185.189.236.0/22 permit
|
||||
185.211.120.0/22 permit
|
||||
185.250.236.0/22 permit
|
||||
185.250.239.148 permit
|
||||
185.250.239.168 permit
|
||||
185.250.239.190 permit
|
||||
188.125.68.132 permit
|
||||
188.125.68.152/31 permit
|
||||
188.125.68.156 permit
|
||||
|
@ -1563,7 +1666,7 @@
|
|||
188.125.85.238 permit
|
||||
188.172.128.0/20 permit
|
||||
192.0.64.0/18 permit
|
||||
192.28.128.0/18 permit
|
||||
192.18.139.154 permit
|
||||
192.30.252.0/22 permit
|
||||
192.64.236.0/24 permit
|
||||
192.64.237.0/24 permit
|
||||
|
@ -1579,16 +1682,21 @@
|
|||
192.254.113.10 permit
|
||||
192.254.113.101 permit
|
||||
192.254.114.176 permit
|
||||
192.254.118.63 permit
|
||||
193.7.206.0/25 permit
|
||||
193.7.207.0/25 permit
|
||||
193.109.254.0/23 permit
|
||||
193.122.128.100 permit
|
||||
194.64.234.128/27 permit
|
||||
194.64.234.129 permit
|
||||
194.104.109.0/24 permit
|
||||
194.104.110.21 permit
|
||||
194.104.110.240/28 permit
|
||||
194.104.111.0/24 permit
|
||||
194.106.220.0/23 permit
|
||||
194.113.24.0/22 permit
|
||||
194.154.193.192/27 permit
|
||||
195.4.92.0/23 permit
|
||||
195.54.172.0/23 permit
|
||||
195.130.217.0/24 permit
|
||||
195.234.109.226 permit
|
||||
195.245.230.0/23 permit
|
||||
|
@ -1605,19 +1713,23 @@
|
|||
198.37.144.0/20 permit
|
||||
198.37.152.186 permit
|
||||
198.61.254.0/23 permit
|
||||
198.61.254.21 permit
|
||||
198.61.254.231 permit
|
||||
198.74.56.28 permit
|
||||
198.178.234.57 permit
|
||||
198.244.48.0/20 permit
|
||||
198.244.60.0/22 permit
|
||||
198.245.80.0/20 permit
|
||||
198.245.81.0/24 permit
|
||||
199.15.176.173 permit
|
||||
199.15.212.0/22 permit
|
||||
199.15.213.187 permit
|
||||
199.15.226.37 permit
|
||||
199.16.156.0/22 permit
|
||||
199.33.145.1 permit
|
||||
199.33.145.32 permit
|
||||
199.59.148.0/22 permit
|
||||
199.67.84.0/24 permit
|
||||
199.67.86.0/24 permit
|
||||
199.67.88.0/24 permit
|
||||
199.101.161.130 permit
|
||||
199.101.162.0/25 permit
|
||||
199.122.120.0/21 permit
|
||||
|
@ -1630,8 +1742,10 @@
|
|||
202.177.148.110 permit
|
||||
203.31.36.0/22 permit
|
||||
203.32.4.25 permit
|
||||
203.55.21.0/24 permit
|
||||
203.81.17.0/24 permit
|
||||
203.122.32.250 permit
|
||||
203.145.57.160/27 permit
|
||||
203.188.194.32 permit
|
||||
203.188.194.151 permit
|
||||
203.188.194.203 permit
|
||||
|
@ -1666,28 +1780,31 @@
|
|||
203.209.230.76/31 permit
|
||||
204.11.168.0/21 permit
|
||||
204.13.11.48/29 permit
|
||||
204.13.11.48/30 permit
|
||||
204.14.232.0/21 permit
|
||||
204.14.232.64/28 permit
|
||||
204.14.234.64/28 permit
|
||||
204.29.186.0/23 permit
|
||||
204.75.142.0/24 permit
|
||||
204.79.197.212 permit
|
||||
204.92.114.187 permit
|
||||
204.92.114.203 permit
|
||||
204.92.114.204/31 permit
|
||||
204.141.32.0/23 permit
|
||||
204.141.42.0/23 permit
|
||||
204.153.121.0/24 permit
|
||||
204.232.168.0/24 permit
|
||||
205.139.110.0/24 permit
|
||||
205.201.128.0/20 permit
|
||||
205.201.131.128/25 permit
|
||||
205.201.134.128/25 permit
|
||||
205.201.136.0/23 permit
|
||||
205.201.137.229 permit
|
||||
205.201.139.0/24 permit
|
||||
205.207.104.0/22 permit
|
||||
205.207.104.108 permit
|
||||
205.220.167.17 permit
|
||||
205.220.167.98 permit
|
||||
205.220.179.17 permit
|
||||
205.220.179.98 permit
|
||||
205.251.233.32 permit
|
||||
205.251.233.36 permit
|
||||
206.25.247.143 permit
|
||||
|
@ -1723,6 +1840,7 @@
|
|||
207.211.31.0/25 permit
|
||||
207.211.41.113 permit
|
||||
207.218.90.0/24 permit
|
||||
207.218.90.122 permit
|
||||
207.250.68.0/24 permit
|
||||
208.40.232.70 permit
|
||||
208.43.21.28/30 permit
|
||||
|
@ -1758,8 +1876,10 @@
|
|||
208.71.42.212/31 permit
|
||||
208.71.42.214 permit
|
||||
208.72.249.240/29 permit
|
||||
208.74.204.0/22 permit
|
||||
208.74.204.9 permit
|
||||
208.75.120.0/22 permit
|
||||
208.75.121.246 permit
|
||||
208.75.122.246 permit
|
||||
208.82.237.96/29 permit
|
||||
208.82.237.104/31 permit
|
||||
|
@ -1773,14 +1893,13 @@
|
|||
209.46.117.168 permit
|
||||
209.46.117.179 permit
|
||||
209.61.151.0/24 permit
|
||||
209.61.151.236 permit
|
||||
209.61.151.249 permit
|
||||
209.61.151.251 permit
|
||||
209.67.98.46 permit
|
||||
209.67.98.59 permit
|
||||
209.85.128.0/17 permit
|
||||
212.4.136.0/26 permit
|
||||
212.25.240.80 permit
|
||||
212.25.240.83 permit
|
||||
212.25.240.84/31 permit
|
||||
212.25.240.88 permit
|
||||
212.82.96.0/24 permit
|
||||
212.82.96.32/27 permit
|
||||
212.82.96.64/29 permit
|
||||
|
@ -1821,6 +1940,12 @@
|
|||
212.82.111.228/31 permit
|
||||
212.82.111.230 permit
|
||||
212.123.28.40 permit
|
||||
212.227.15.0/24 permit
|
||||
212.227.15.0/25 permit
|
||||
212.227.17.0/27 permit
|
||||
212.227.126.128/25 permit
|
||||
213.46.255.0/24 permit
|
||||
213.165.64.0/23 permit
|
||||
213.167.75.0/25 permit
|
||||
213.167.81.0/25 permit
|
||||
213.199.128.139 permit
|
||||
|
@ -1861,6 +1986,10 @@
|
|||
216.46.168.0/24 permit
|
||||
216.58.192.0/19 permit
|
||||
216.66.217.240/29 permit
|
||||
216.71.138.33 permit
|
||||
216.71.152.207 permit
|
||||
216.71.154.29 permit
|
||||
216.71.155.89 permit
|
||||
216.74.162.13 permit
|
||||
216.74.162.14 permit
|
||||
216.82.240.0/20 permit
|
||||
|
@ -1870,33 +1999,49 @@
|
|||
216.109.114.0/24 permit
|
||||
216.109.114.32/27 permit
|
||||
216.109.114.64/29 permit
|
||||
216.113.160.0/24 permit
|
||||
216.113.172.0/25 permit
|
||||
216.113.175.0/24 permit
|
||||
216.128.126.97 permit
|
||||
216.136.162.65 permit
|
||||
216.136.162.120/29 permit
|
||||
216.136.168.80/28 permit
|
||||
216.145.217.0/24 permit
|
||||
216.145.221.0/24 permit
|
||||
216.198.0.0/18 permit
|
||||
216.203.30.55 permit
|
||||
216.203.33.178/31 permit
|
||||
216.205.24.0/24 permit
|
||||
216.239.32.0/19 permit
|
||||
217.72.192.64/26 permit
|
||||
217.72.192.248/29 permit
|
||||
217.72.207.0/27 permit
|
||||
217.77.141.52 permit
|
||||
217.77.141.59 permit
|
||||
217.175.194.0/24 permit
|
||||
222.73.195.64/29 permit
|
||||
223.165.113.0/24 permit
|
||||
223.165.115.0/24 permit
|
||||
223.165.118.0/23 permit
|
||||
223.165.120.0/23 permit
|
||||
2001:0868:0100:0600::/64 permit
|
||||
2001:4860:4000::/36 permit
|
||||
2001:748:100:40::2:0/112 permit
|
||||
2404:6800:4000::/36 permit
|
||||
2603:1010:3:3::5b permit
|
||||
2603:1020:201:10::10f permit
|
||||
2603:1030:20e:3::23c permit
|
||||
2603:1030:b:3::152 permit
|
||||
2603:1030:c02:8::14 permit
|
||||
2607:f8b0:4000::/36 permit
|
||||
2620:109:c003:104::215 permit
|
||||
2620:109:c003:104::/64 permit
|
||||
2620:109:c006:104::215 permit
|
||||
2620:109:c003:104::215 permit
|
||||
2620:109:c006:104::/64 permit
|
||||
2620:109:c006:104::215 permit
|
||||
2620:109:c00d:104::/64 permit
|
||||
2620:10d:c090:450::120 permit
|
||||
2620:10d:c091:450::16 permit
|
||||
2620:119:50c0:207::215 permit
|
||||
2620:10d:c091:400::8:1 permit
|
||||
2620:119:50c0:207::/64 permit
|
||||
2620:119:50c0:207::215 permit
|
||||
2800:3f0:4000::/36 permit
|
||||
194.25.134.0/24 permit # t-online.de
|
||||
|
|
|
@ -27,4 +27,5 @@
|
|||
#197518 2 #Rackmarkt SL, Spain
|
||||
#197695 2 #Domain names registrar REG.RU Ltd, Russia
|
||||
#198068 2 #P.A.G.M. OU, Estonia
|
||||
#201942 5 #Soltia Consulting SL, Spain
|
||||
#201942 5 #Soltia Consulting SL, Spain
|
||||
#213373 4 #IP Connect Inc
|
|
@ -8,7 +8,7 @@ VIRUS_FOUND {
|
|||
}
|
||||
# Bad policy from free mail providers
|
||||
FREEMAIL_POLICY_FAILURE {
|
||||
expression = "-g+:policies & !DMARC_POLICY_ALLOW & !MAILLIST & ( FREEMAIL_ENVFROM | FREEMAIL_FROM ) & !WHITELISTED_FWD_HOST";
|
||||
expression = "FREEMAIL_FROM & !DMARC_POLICY_ALLOW & !MAILLIST& !WHITELISTED_FWD_HOST & -g+:policies";
|
||||
score = 16.0;
|
||||
}
|
||||
# Applies to freemail with undisclosed recipients
|
||||
|
@ -68,3 +68,39 @@ WL_FWD_HOST {
|
|||
ENCRYPTED_CHAT {
|
||||
expression = "CHAT_VERSION_HEADER & ENCRYPTED_PGP";
|
||||
}
|
||||
|
||||
CLAMD_SPAM_FOUND {
|
||||
expression = "CLAM_SECI_SPAM & !MAILCOW_WHITE";
|
||||
description = "Probably Spam, Securite Spam Flag set through ClamAV";
|
||||
score = 5;
|
||||
}
|
||||
|
||||
CLAMD_BAD_PDF {
|
||||
expression = "CLAM_SECI_PDF & !MAILCOW_WHITE";
|
||||
description = "Bad PDF Found, Securite bad PDF Flag set through ClamAV";
|
||||
score = 8;
|
||||
}
|
||||
|
||||
CLAMD_BAD_JPG {
|
||||
expression = "CLAM_SECI_JPG & !MAILCOW_WHITE";
|
||||
description = "Bad JPG Found, Securite bad JPG Flag set through ClamAV";
|
||||
score = 8;
|
||||
}
|
||||
|
||||
CLAMD_ASCII_MALWARE {
|
||||
expression = "CLAM_SECI_ASCII & !MAILCOW_WHITE";
|
||||
description = "ASCII malware found, Securite ASCII malware Flag set through ClamAV";
|
||||
score = 8;
|
||||
}
|
||||
|
||||
CLAMD_HTML_MALWARE {
|
||||
expression = "CLAM_SECI_HTML & !MAILCOW_WHITE";
|
||||
description = "HTML malware found, Securite HTML malware Flag set through ClamAV";
|
||||
score = 8;
|
||||
}
|
||||
|
||||
CLAMD_JS_MALWARE {
|
||||
expression = "CLAM_SECI_JS & !MAILCOW_WHITE";
|
||||
description = "JS malware found, Securite JS malware Flag set through ClamAV";
|
||||
score = 8;
|
||||
}
|
|
@ -159,8 +159,8 @@ BAZAAR_ABUSE_CH {
|
|||
}
|
||||
|
||||
URLHAUS_ABUSE_CH {
|
||||
type = "url";
|
||||
filter = "full";
|
||||
type = "selector";
|
||||
selector = "urls";
|
||||
map = "https://urlhaus.abuse.ch/downloads/text_online/";
|
||||
score = 10.0;
|
||||
}
|
||||
|
@ -175,7 +175,7 @@ BAD_SUBJECT_00 {
|
|||
type = "header";
|
||||
header = "subject";
|
||||
regexp = true;
|
||||
map = "http://nullnull.org/bad-subject-regex.txt";
|
||||
map = "http://fuzzy.mailcow.email/bad-subject-regex.txt";
|
||||
score = 6.0;
|
||||
symbols_set = ["BAD_SUBJECT_00"];
|
||||
}
|
||||
|
|
|
@ -340,6 +340,10 @@ rspamd_config:register_symbol({
|
|||
if not bcc_dest then
|
||||
return -- stop
|
||||
end
|
||||
-- dot stuff content before sending
|
||||
local email_content = tostring(task:get_content())
|
||||
email_content = string.gsub(email_content, "\r\n%.", "\r\n..")
|
||||
-- send mail
|
||||
lua_smtp.sendmail({
|
||||
task = task,
|
||||
host = os.getenv("IPV4_NETWORK") .. '.253',
|
||||
|
@ -347,8 +351,8 @@ rspamd_config:register_symbol({
|
|||
from = task:get_from(stp)[1].addr,
|
||||
recipients = bcc_dest,
|
||||
helo = 'bcc',
|
||||
timeout = 10,
|
||||
}, task:get_content(), sendmail_cb)
|
||||
timeout = 20,
|
||||
}, email_content, sendmail_cb)
|
||||
end
|
||||
|
||||
-- determine from
|
||||
|
|
|
@ -62,7 +62,7 @@
|
|||
SOGoFirstDayOfWeek = "1";
|
||||
|
||||
SOGoSieveFolderEncoding = "UTF-8";
|
||||
SOGoPasswordChangeEnabled = YES;
|
||||
SOGoPasswordChangeEnabled = NO;
|
||||
SOGoSentFolderName = "Sent";
|
||||
SOGoMailShowSubscribedFoldersOnly = NO;
|
||||
NGImap4ConnectionStringSeparator = "/";
|
||||
|
|
|
@ -10,9 +10,6 @@ require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php';
|
|||
$_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
|
||||
$tfa_data = get_tfa();
|
||||
$fido2_data = fido2(array("action" => "get_friendly_names"));
|
||||
if (!isset($_SESSION['gal']) && $license_cache = $redis->Get('LICENSE_STATUS_CACHE')) {
|
||||
$_SESSION['gal'] = json_decode($license_cache, true);
|
||||
}
|
||||
|
||||
$js_minifier->add('/web/js/site/admin.js');
|
||||
$js_minifier->add('/web/js/presets/rspamd.js');
|
||||
|
@ -83,13 +80,17 @@ foreach ($RSPAMD_MAPS['regex'] as $rspamd_regex_desc => $rspamd_regex_map) {
|
|||
];
|
||||
}
|
||||
|
||||
// cors settings
|
||||
$cors_settings = cors('get');
|
||||
$cors_settings['allowed_origins'] = str_replace(", ", "\n", $cors_settings['allowed_origins']);
|
||||
$cors_settings['allowed_methods'] = explode(", ", $cors_settings['allowed_methods']);
|
||||
|
||||
$template = 'admin.twig';
|
||||
$template_data = [
|
||||
'tfa_data' => $tfa_data,
|
||||
'tfa_id' => @$_SESSION['tfa_id'],
|
||||
'fido2_cid' => @$_SESSION['fido2_cid'],
|
||||
'fido2_data' => $fido2_data,
|
||||
'gal' => @$_SESSION['gal'],
|
||||
'api' => [
|
||||
'ro' => admin_api('ro', 'get'),
|
||||
'rw' => admin_api('rw', 'get'),
|
||||
|
@ -107,8 +108,10 @@ $template_data = [
|
|||
'rsettings' => $rsettings,
|
||||
'rspamd_regex_maps' => $rspamd_regex_maps,
|
||||
'logo_specs' => customize('get', 'main_logo_specs'),
|
||||
'ip_check' => customize('get', 'ip_check'),
|
||||
'password_complexity' => password_complexity('get'),
|
||||
'show_rspamd_global_filters' => @$_SESSION['show_rspamd_global_filters'],
|
||||
'cors_settings' => $cors_settings,
|
||||
'lang_admin' => json_encode($lang['admin']),
|
||||
'lang_datatables' => json_encode($lang['datatables'])
|
||||
];
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
openapi: 3.0.0
|
||||
openapi: 3.1.0
|
||||
info:
|
||||
description: >-
|
||||
mailcow is complete e-mailing solution with advanced antispam, antivirus,
|
||||
|
@ -699,6 +699,38 @@ paths:
|
|||
type: string
|
||||
type: object
|
||||
summary: Create Domain Admin user
|
||||
/api/v1/add/sso/domain-admin:
|
||||
post:
|
||||
responses:
|
||||
"401":
|
||||
$ref: "#/components/responses/Unauthorized"
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
examples:
|
||||
response:
|
||||
value:
|
||||
token: "591F6D-5C3DD2-7455CD-DAF1C1-AA4FCC"
|
||||
description: OK
|
||||
headers: { }
|
||||
tags:
|
||||
- Single Sign-On
|
||||
description: >-
|
||||
Using this endpoint you can issue a token for Domain Admin user. This token can be used for
|
||||
autologin Domain Admin user by using query_string var sso_token={token}. Token expiration time is 30s
|
||||
operationId: Issue Domain Admin SSO token
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
example:
|
||||
username: testadmin
|
||||
properties:
|
||||
username:
|
||||
description: the username for the admin user
|
||||
type: object
|
||||
type: object
|
||||
summary: Issue Domain Admin SSO token
|
||||
/api/v1/edit/da-acl:
|
||||
post:
|
||||
responses:
|
||||
|
@ -1999,7 +2031,7 @@ paths:
|
|||
- domain.tld
|
||||
- domain2.tld
|
||||
properties:
|
||||
items:
|
||||
items:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
|
@ -2993,7 +3025,7 @@ paths:
|
|||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
log:
|
||||
|
@ -3144,8 +3176,10 @@ paths:
|
|||
example:
|
||||
attr:
|
||||
ban_time: "86400"
|
||||
ban_time_increment: "1"
|
||||
blacklist: "10.100.6.5/32,10.100.8.4/32"
|
||||
max_attempts: "5"
|
||||
max_ban_time: "86400"
|
||||
netban_ipv4: "24"
|
||||
netban_ipv6: "64"
|
||||
retry_window: "600"
|
||||
|
@ -3159,11 +3193,17 @@ paths:
|
|||
description: the backlisted ips or hostnames separated by comma
|
||||
type: string
|
||||
ban_time:
|
||||
description: the time a ip should be banned
|
||||
description: the time an ip should be banned
|
||||
type: number
|
||||
ban_time_increment:
|
||||
description: if the time of the ban should increase each time
|
||||
type: boolean
|
||||
max_attempts:
|
||||
description: the maximum numbe of wrong logins before a ip is banned
|
||||
type: number
|
||||
max_ban_time:
|
||||
description: the maximum time an ip should be banned
|
||||
type: number
|
||||
netban_ipv4:
|
||||
description: the networks mask to ban for ipv4
|
||||
type: number
|
||||
|
@ -4081,10 +4121,12 @@ paths:
|
|||
response:
|
||||
value:
|
||||
ban_time: 604800
|
||||
ban_time_increment: 1
|
||||
blacklist: |-
|
||||
45.82.153.37/32
|
||||
92.118.38.52/32
|
||||
max_attempts: 1
|
||||
max_ban_time: 604800
|
||||
netban_ipv4: 32
|
||||
netban_ipv6: 128
|
||||
perm_bans:
|
||||
|
@ -5560,6 +5602,50 @@ paths:
|
|||
description: You can list all mailboxes existing in system for a specific domain.
|
||||
operationId: Get mailboxes of a domain
|
||||
summary: Get mailboxes of a domain
|
||||
/api/v1/edit/cors:
|
||||
post:
|
||||
responses:
|
||||
"401":
|
||||
$ref: "#/components/responses/Unauthorized"
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
examples:
|
||||
response:
|
||||
value:
|
||||
- type: "success"
|
||||
log: ["cors", "edit", {"allowed_origins": ["*", "mail.mailcow.tld"], "allowed_methods": ["POST", "GET", "DELETE", "PUT"]}]
|
||||
msg: "cors_headers_edited"
|
||||
description: OK
|
||||
headers: { }
|
||||
tags:
|
||||
- Cross-Origin Resource Sharing (CORS)
|
||||
description: >-
|
||||
This endpoint allows you to manage Cross-Origin Resource Sharing (CORS) settings for the API.
|
||||
CORS is a security feature implemented by web browsers to prevent unauthorized cross-origin requests.
|
||||
By editing the CORS settings, you can specify which domains and which methods are permitted to access the API resources from outside the mailcow domain.
|
||||
operationId: Edit Cross-Origin Resource Sharing (CORS) settings
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
example:
|
||||
attr:
|
||||
allowed_origins: ["*", "mail.mailcow.tld"]
|
||||
allowed_methods: ["POST", "GET", "DELETE", "PUT"]
|
||||
properties:
|
||||
attr:
|
||||
type: object
|
||||
properties:
|
||||
allowed_origins:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
allowed_methods:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
summary: Edit Cross-Origin Resource Sharing (CORS) settings
|
||||
|
||||
tags:
|
||||
- name: Domains
|
||||
|
@ -5586,6 +5672,8 @@ tags:
|
|||
description: Manage DKIM keys
|
||||
- name: Domain admin
|
||||
description: Create or udpdate domain admin users
|
||||
- name: Single Sign-On
|
||||
description: Issue tokens for users
|
||||
- name: Address Rewriting
|
||||
description: Create BCC maps or recipient maps
|
||||
- name: Outgoing TLS Policy Map Overrides
|
||||
|
@ -5602,3 +5690,5 @@ tags:
|
|||
description: Get the status of your cow
|
||||
- name: Ratelimits
|
||||
description: Edit domain ratelimits
|
||||
- name: Cross-Origin Resource Sharing (CORS)
|
||||
description: Manage Cross-Origin Resource Sharing (CORS) settings
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
window.onload = function() {
|
||||
// Begin Swagger UI call region
|
||||
const ui = SwaggerUIBundle({
|
||||
window.ui = SwaggerUIBundle({
|
||||
urls: [{url: "/api/openapi.yaml", name: "mailcow API"}],
|
||||
dom_id: '#swagger-ui',
|
||||
deepLinking: true,
|
||||
|
@ -15,5 +15,4 @@ window.onload = function() {
|
|||
});
|
||||
// End Swagger UI call region
|
||||
|
||||
window.ui = ui;
|
||||
};
|
||||
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -4,10 +4,10 @@
|
|||
*
|
||||
* To rebuild or modify this file with the latest versions of the included
|
||||
* software please visit:
|
||||
* https://datatables.net/download/#bs5/dt-1.12.0/r-2.3.0/sl-1.4.0
|
||||
* https://datatables.net/download/#bs5/dt-1.13.1/r-2.4.0/sl-1.5.0
|
||||
*
|
||||
* Included libraries:
|
||||
* DataTables 1.12.0, Responsive 2.3.0, Select 1.4.0
|
||||
* DataTables 1.13.1, Responsive 2.4.0, Select 1.5.0
|
||||
*/
|
||||
|
||||
@charset "UTF-8";
|
||||
|
@ -63,7 +63,7 @@ table.dataTable thead > tr > td.sorting_desc_disabled:after {
|
|||
opacity: 0.125;
|
||||
right: 10px;
|
||||
line-height: 9px;
|
||||
font-size: 0.9em;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
table.dataTable thead > tr > th.sorting:before, table.dataTable thead > tr > th.sorting_asc:before, table.dataTable thead > tr > th.sorting_desc:before, table.dataTable thead > tr > th.sorting_asc_disabled:before, table.dataTable thead > tr > th.sorting_desc_disabled:before,
|
||||
table.dataTable thead > tr > td.sorting:before,
|
||||
|
@ -72,7 +72,7 @@ table.dataTable thead > tr > td.sorting_desc:before,
|
|||
table.dataTable thead > tr > td.sorting_asc_disabled:before,
|
||||
table.dataTable thead > tr > td.sorting_desc_disabled:before {
|
||||
bottom: 50%;
|
||||
content: "▴";
|
||||
content: "▲";
|
||||
}
|
||||
table.dataTable thead > tr > th.sorting:after, table.dataTable thead > tr > th.sorting_asc:after, table.dataTable thead > tr > th.sorting_desc:after, table.dataTable thead > tr > th.sorting_asc_disabled:after, table.dataTable thead > tr > th.sorting_desc_disabled:after,
|
||||
table.dataTable thead > tr > td.sorting:after,
|
||||
|
@ -81,7 +81,7 @@ table.dataTable thead > tr > td.sorting_desc:after,
|
|||
table.dataTable thead > tr > td.sorting_asc_disabled:after,
|
||||
table.dataTable thead > tr > td.sorting_desc_disabled:after {
|
||||
top: 50%;
|
||||
content: "▾";
|
||||
content: "▼";
|
||||
}
|
||||
table.dataTable thead > tr > th.sorting_asc:before, table.dataTable thead > tr > th.sorting_desc:after,
|
||||
table.dataTable thead > tr > td.sorting_asc:before,
|
||||
|
@ -287,6 +287,9 @@ table.dataTable > tbody > tr.selected > * {
|
|||
box-shadow: inset 0 0 0 9999px rgba(13, 110, 253, 0.9);
|
||||
color: white;
|
||||
}
|
||||
table.dataTable > tbody > tr.selected a {
|
||||
color: #090a0b;
|
||||
}
|
||||
table.dataTable.table-striped > tbody > tr.odd > * {
|
||||
box-shadow: inset 0 0 0 9999px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
@ -335,6 +338,13 @@ div.dataTables_wrapper div.dataTables_paginate ul.pagination {
|
|||
white-space: nowrap;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
div.dataTables_wrapper div.dt-row {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
div.dataTables_wrapper span.sorting-value {
|
||||
display: none;
|
||||
}
|
||||
|
||||
div.dataTables_scrollHead table.dataTable {
|
||||
margin-bottom: 0 !important;
|
||||
|
@ -380,17 +390,6 @@ div.dataTables_wrapper div.dataTables_paginate {
|
|||
table.dataTable.table-sm > thead > tr > th:not(.sorting_disabled) {
|
||||
padding-right: 20px;
|
||||
}
|
||||
table.dataTable.table-sm .sorting:before,
|
||||
table.dataTable.table-sm .sorting_asc:before,
|
||||
table.dataTable.table-sm .sorting_desc:before {
|
||||
top: 5px;
|
||||
right: 0.85em;
|
||||
}
|
||||
table.dataTable.table-sm .sorting:after,
|
||||
table.dataTable.table-sm .sorting_asc:after,
|
||||
table.dataTable.table-sm .sorting_desc:after {
|
||||
top: 5px;
|
||||
}
|
||||
|
||||
table.table-bordered.dataTable {
|
||||
border-right-width: 0;
|
||||
|
@ -629,13 +628,13 @@ table.dataTable > tbody > tr > .selected {
|
|||
background-color: rgba(13, 110, 253, 0.9);
|
||||
color: white;
|
||||
}
|
||||
table.dataTable tbody td.select-checkbox,
|
||||
table.dataTable tbody th.select-checkbox {
|
||||
table.dataTable > tbody > tr > td.select-checkbox,
|
||||
table.dataTable > tbody > tr > th.select-checkbox {
|
||||
position: relative;
|
||||
}
|
||||
table.dataTable tbody td.select-checkbox:before, table.dataTable tbody td.select-checkbox:after,
|
||||
table.dataTable tbody th.select-checkbox:before,
|
||||
table.dataTable tbody th.select-checkbox:after {
|
||||
table.dataTable > tbody > tr > td.select-checkbox:before, table.dataTable > tbody > tr > td.select-checkbox:after,
|
||||
table.dataTable > tbody > tr > th.select-checkbox:before,
|
||||
table.dataTable > tbody > tr > th.select-checkbox:after {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 1.2em;
|
||||
|
@ -644,20 +643,20 @@ table.dataTable tbody th.select-checkbox:after {
|
|||
height: 12px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
table.dataTable tbody td.select-checkbox:before,
|
||||
table.dataTable tbody th.select-checkbox:before {
|
||||
table.dataTable > tbody > tr > td.select-checkbox:before,
|
||||
table.dataTable > tbody > tr > th.select-checkbox:before {
|
||||
content: " ";
|
||||
margin-top: -5px;
|
||||
margin-left: -6px;
|
||||
border: 1px solid black;
|
||||
border-radius: 3px;
|
||||
}
|
||||
table.dataTable tr.selected td.select-checkbox:before,
|
||||
table.dataTable tr.selected th.select-checkbox:before {
|
||||
table.dataTable > tbody > tr.selected > td.select-checkbox:before,
|
||||
table.dataTable > tbody > tr.selected > th.select-checkbox:before {
|
||||
border: 1px solid white;
|
||||
}
|
||||
table.dataTable tr.selected td.select-checkbox:after,
|
||||
table.dataTable tr.selected th.select-checkbox:after {
|
||||
table.dataTable > tbody > tr.selected > td.select-checkbox:after,
|
||||
table.dataTable > tbody > tr.selected > th.select-checkbox:after {
|
||||
content: "✓";
|
||||
font-size: 20px;
|
||||
margin-top: -19px;
|
||||
|
@ -665,12 +664,12 @@ table.dataTable tr.selected th.select-checkbox:after {
|
|||
text-align: center;
|
||||
text-shadow: 1px 1px #B0BED9, -1px -1px #B0BED9, 1px -1px #B0BED9, -1px 1px #B0BED9;
|
||||
}
|
||||
table.dataTable.compact tbody td.select-checkbox:before,
|
||||
table.dataTable.compact tbody th.select-checkbox:before {
|
||||
table.dataTable.compact > tbody > tr > td.select-checkbox:before,
|
||||
table.dataTable.compact > tbody > tr > th.select-checkbox:before {
|
||||
margin-top: -12px;
|
||||
}
|
||||
table.dataTable.compact tr.selected td.select-checkbox:after,
|
||||
table.dataTable.compact tr.selected th.select-checkbox:after {
|
||||
table.dataTable.compact > tbody > tr.selected > td.select-checkbox:after,
|
||||
table.dataTable.compact > tbody > tr.selected > th.select-checkbox:after {
|
||||
margin-top: -16px;
|
||||
}
|
||||
|
||||
|
@ -690,4 +689,3 @@ table.dataTable.table-sm tbody td.select-checkbox::before {
|
|||
margin-top: -9px;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -77,4 +77,22 @@ li .dtr-data {
|
|||
table.dataTable>tbody>tr.child span.dtr-title {
|
||||
width: 30%;
|
||||
max-width: 250px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
div.dataTables_wrapper div.dataTables_filter {
|
||||
text-align: left;
|
||||
}
|
||||
div.dataTables_wrapper div.dataTables_length {
|
||||
text-align: right;
|
||||
}
|
||||
.dataTables_paginate, .dataTables_length, .dataTables_filter {
|
||||
margin: 10px 0!important;
|
||||
}
|
||||
|
||||
td.dt-text-right {
|
||||
text-align: end !important;
|
||||
}
|
||||
th.dt-text-right {
|
||||
text-align: end !important;
|
||||
}
|
|
@ -199,6 +199,13 @@
|
|||
display: none !important;
|
||||
}
|
||||
|
||||
div.dataTables_wrapper div.dataTables_length {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.senders-mw220 {
|
||||
max-width: 100% !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 350px) {
|
|
@ -66,4 +66,6 @@ table tbody tr td input[type="checkbox"] {
|
|||
padding: .2em .4em .3em !important;
|
||||
background-color: #ececec!important;
|
||||
}
|
||||
|
||||
.badge.bg-info .bi {
|
||||
font-size: inherit;
|
||||
}
|
||||
|
|
|
@ -1,102 +1,104 @@
|
|||
.pagination a {
|
||||
text-decoration: none !important;
|
||||
}
|
||||
|
||||
.panel.panel-default {
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
.table-responsive {
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
.table-responsive {
|
||||
overflow-x: scroll !important;
|
||||
}
|
||||
|
||||
.footer-add-item {
|
||||
display: block;
|
||||
text-align: center;
|
||||
font-style: italic;
|
||||
padding: 10px;
|
||||
background: #F5F5F5;
|
||||
}
|
||||
|
||||
@media (min-width: 992px) {
|
||||
.container {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
@media (min-width: 1920px) {
|
||||
.container {
|
||||
width: 80%;
|
||||
}
|
||||
}
|
||||
|
||||
.mass-actions-quarantine {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.inputMissingAttr {
|
||||
border-color: #FF4136;
|
||||
}
|
||||
|
||||
.modal#qidDetailModal p {
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
span#qid_detail_score {
|
||||
font-weight: 700;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
span.rspamd-symbol {
|
||||
display: inline-block;
|
||||
margin: 2px 6px 2px 0;
|
||||
border-radius: 4px;
|
||||
padding: 0 7px;
|
||||
}
|
||||
|
||||
span.rspamd-symbol.positive {
|
||||
background: #4CAF50;
|
||||
border: 1px solid #4CAF50;
|
||||
color: white;
|
||||
}
|
||||
|
||||
span.rspamd-symbol.negative {
|
||||
background: #ff4136;
|
||||
border: 1px solid #ff4136;
|
||||
color: white;
|
||||
}
|
||||
|
||||
span.rspamd-symbol.neutral {
|
||||
background: #f5f5f5;
|
||||
color: #333;
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
|
||||
span.rspamd-symbol span.score {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
span.mail-address-item {
|
||||
background-color: #f5f5f5;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #ccc;
|
||||
padding: 2px 7px;
|
||||
display: inline-block;
|
||||
margin: 2px 6px 2px 0;
|
||||
}
|
||||
|
||||
table tbody tr {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
table tbody tr td input[type="checkbox"] {
|
||||
cursor: pointer;
|
||||
}
|
||||
.label-rspamd-action {
|
||||
font-size:110%;
|
||||
margin:20px;
|
||||
}
|
||||
|
||||
.pagination a {
|
||||
text-decoration: none !important;
|
||||
}
|
||||
|
||||
.panel.panel-default {
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
.table-responsive {
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
.table-responsive {
|
||||
overflow-x: scroll !important;
|
||||
}
|
||||
|
||||
.footer-add-item {
|
||||
display: block;
|
||||
text-align: center;
|
||||
font-style: italic;
|
||||
padding: 10px;
|
||||
background: #F5F5F5;
|
||||
}
|
||||
|
||||
@media (min-width: 992px) {
|
||||
.container {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
@media (min-width: 1920px) {
|
||||
.container {
|
||||
width: 80%;
|
||||
}
|
||||
}
|
||||
|
||||
.mass-actions-quarantine {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.inputMissingAttr {
|
||||
border-color: #FF4136;
|
||||
}
|
||||
|
||||
.modal#qidDetailModal p {
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
span#qid_detail_score {
|
||||
font-weight: 700;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
span.rspamd-symbol {
|
||||
display: inline-block;
|
||||
margin: 2px 6px 2px 0;
|
||||
border-radius: 4px;
|
||||
padding: 0 7px;
|
||||
}
|
||||
|
||||
span.rspamd-symbol.positive {
|
||||
background: #4CAF50;
|
||||
border: 1px solid #4CAF50;
|
||||
color: white;
|
||||
}
|
||||
|
||||
span.rspamd-symbol.negative {
|
||||
background: #ff4136;
|
||||
border: 1px solid #ff4136;
|
||||
color: white;
|
||||
}
|
||||
|
||||
span.rspamd-symbol.neutral {
|
||||
background: #f5f5f5;
|
||||
color: #333;
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
|
||||
span.rspamd-symbol span.score {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
span.mail-address-item {
|
||||
background-color: #f5f5f5;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #ccc;
|
||||
padding: 2px 7px;
|
||||
display: inline-block;
|
||||
margin: 2px 6px 2px 0;
|
||||
}
|
||||
|
||||
table tbody tr {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
table tbody tr td input[type="checkbox"] {
|
||||
cursor: pointer;
|
||||
}
|
||||
.label-rspamd-action {
|
||||
font-size:110%;
|
||||
margin:20px;
|
||||
}
|
||||
.senders-mw220 {
|
||||
max-width: 220px;
|
||||
}
|
||||
|
|
|
@ -11,7 +11,86 @@
|
|||
* Copyright 2011-2021 Twitter, Inc.
|
||||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
|
||||
*/
|
||||
@import url("https://fonts.googleapis.com/css2?family=Source+Sans+Pro:ital,wght@0,300;0,400;0,700;1,400&display=swap");
|
||||
|
||||
/* source-sans-pro-300 - latin */
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
src: url('/fonts/source-sans-pro-v21-latin-300.eot'); /* IE9 Compat Modes */
|
||||
src: local(''),
|
||||
url('/fonts/source-sans-pro-v21-latin-300.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */
|
||||
url('/fonts/source-sans-pro-v21-latin-300.woff2') format('woff2'), /* Super Modern Browsers */
|
||||
url('/fonts/source-sans-pro-v21-latin-300.woff') format('woff'), /* Modern Browsers */
|
||||
url('/fonts/source-sans-pro-v21-latin-300.ttf') format('truetype'), /* Safari, Android, iOS */
|
||||
url('/fonts/source-sans-pro-v21-latin-300.svg#SourceSansPro') format('svg'); /* Legacy iOS */
|
||||
}
|
||||
/* source-sans-pro-300italic - latin */
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: italic;
|
||||
font-weight: 300;
|
||||
src: url('/fonts/source-sans-pro-v21-latin-300italic.eot'); /* IE9 Compat Modes */
|
||||
src: local(''),
|
||||
url('/fonts/source-sans-pro-v21-latin-300italic.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */
|
||||
url('/fonts/source-sans-pro-v21-latin-300italic.woff2') format('woff2'), /* Super Modern Browsers */
|
||||
url('/fonts/source-sans-pro-v21-latin-300italic.woff') format('woff'), /* Modern Browsers */
|
||||
url('/fonts/source-sans-pro-v21-latin-300italic.ttf') format('truetype'), /* Safari, Android, iOS */
|
||||
url('/fonts/source-sans-pro-v21-latin-300italic.svg#SourceSansPro') format('svg'); /* Legacy iOS */
|
||||
}
|
||||
/* source-sans-pro-regular - latin */
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url('/fonts/source-sans-pro-v21-latin-regular.eot'); /* IE9 Compat Modes */
|
||||
src: local(''),
|
||||
url('/fonts/source-sans-pro-v21-latin-regular.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */
|
||||
url('/fonts/source-sans-pro-v21-latin-regular.woff2') format('woff2'), /* Super Modern Browsers */
|
||||
url('/fonts/source-sans-pro-v21-latin-regular.woff') format('woff'), /* Modern Browsers */
|
||||
url('/fonts/source-sans-pro-v21-latin-regular.ttf') format('truetype'), /* Safari, Android, iOS */
|
||||
url('/fonts/source-sans-pro-v21-latin-regular.svg#SourceSansPro') format('svg'); /* Legacy iOS */
|
||||
}
|
||||
/* source-sans-pro-italic - latin */
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
src: url('/fonts/source-sans-pro-v21-latin-italic.eot'); /* IE9 Compat Modes */
|
||||
src: local(''),
|
||||
url('/fonts/source-sans-pro-v21-latin-italic.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */
|
||||
url('/fonts/source-sans-pro-v21-latin-italic.woff2') format('woff2'), /* Super Modern Browsers */
|
||||
url('/fonts/source-sans-pro-v21-latin-italic.woff') format('woff'), /* Modern Browsers */
|
||||
url('/fonts/source-sans-pro-v21-latin-italic.ttf') format('truetype'), /* Safari, Android, iOS */
|
||||
url('/fonts/source-sans-pro-v21-latin-italic.svg#SourceSansPro') format('svg'); /* Legacy iOS */
|
||||
}
|
||||
/* source-sans-pro-700 - latin */
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
src: url('/fonts/source-sans-pro-v21-latin-700.eot'); /* IE9 Compat Modes */
|
||||
src: local(''),
|
||||
url('/fonts/source-sans-pro-v21-latin-700.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */
|
||||
url('/fonts/source-sans-pro-v21-latin-700.woff2') format('woff2'), /* Super Modern Browsers */
|
||||
url('/fonts/source-sans-pro-v21-latin-700.woff') format('woff'), /* Modern Browsers */
|
||||
url('/fonts/source-sans-pro-v21-latin-700.ttf') format('truetype'), /* Safari, Android, iOS */
|
||||
url('/fonts/source-sans-pro-v21-latin-700.svg#SourceSansPro') format('svg'); /* Legacy iOS */
|
||||
}
|
||||
/* source-sans-pro-700italic - latin */
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: italic;
|
||||
font-weight: 700;
|
||||
src: url('/fonts/source-sans-pro-v21-latin-700italic.eot'); /* IE9 Compat Modes */
|
||||
src: local(''),
|
||||
url('/fonts/source-sans-pro-v21-latin-700italic.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */
|
||||
url('/fonts/source-sans-pro-v21-latin-700italic.woff2') format('woff2'), /* Super Modern Browsers */
|
||||
url('/fonts/source-sans-pro-v21-latin-700italic.woff') format('woff'), /* Modern Browsers */
|
||||
url('/fonts/source-sans-pro-v21-latin-700italic.ttf') format('truetype'), /* Safari, Android, iOS */
|
||||
url('/fonts/source-sans-pro-v21-latin-700italic.svg#SourceSansPro') format('svg'); /* Legacy iOS */
|
||||
}
|
||||
|
||||
:root {
|
||||
--bs-blue: #158cba;
|
||||
--bs-indigo: #6610f2;
|
||||
|
|
|
@ -20,6 +20,11 @@ legend {
|
|||
background-color: #7a7a7a !important;
|
||||
border-color: #5c5c5c !important;
|
||||
}
|
||||
.btn-dark {
|
||||
color: #000 !important;;
|
||||
background-color: #f6f6f6 !important;;
|
||||
border-color: #ddd !important;;
|
||||
}
|
||||
.btn-check:checked+.btn-secondary, .btn-check:active+.btn-secondary, .btn-secondary:active, .btn-secondary.active, .show>.btn-secondary.dropdown-toggle {
|
||||
border-color: #7a7a7a !important;
|
||||
}
|
||||
|
@ -299,22 +304,22 @@ a:hover {
|
|||
}
|
||||
|
||||
|
||||
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 {
|
||||
background-color: #7a7a7a !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 {
|
||||
background-color: #7a7a7a !important;
|
||||
border: 1.5px solid #5c5c5c !important;
|
||||
color: #fff !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 {
|
||||
background-color: #949494;
|
||||
}
|
||||
table.dataTable.dtr-inline.collapsed>tbody>tr>td.child,
|
||||
table.dataTable.dtr-inline.collapsed>tbody>tr>th.child,
|
||||
table.dataTable.dtr-inline.collapsed>tbody>tr>td.child,
|
||||
table.dataTable.dtr-inline.collapsed>tbody>tr>th.child,
|
||||
table.dataTable.dtr-inline.collapsed>tbody>tr>td.dataTables_empty {
|
||||
background-color: #444444;
|
||||
}
|
||||
|
@ -327,7 +332,7 @@ table.dataTable.dtr-inline.collapsed>tbody>tr>td.dataTables_empty {
|
|||
}
|
||||
.btn.btn-outline-secondary {
|
||||
color: #fff !important;
|
||||
border-color: #7a7a7a !important;
|
||||
border-color: #7a7a7a !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 {
|
||||
background-color: #9b9b9b !important;
|
||||
|
@ -358,3 +363,11 @@ table.dataTable.dtr-inline.collapsed>tbody>tr>td.dataTables_empty {
|
|||
background: #333;
|
||||
}
|
||||
|
||||
span.mail-address-item {
|
||||
background-color: #333;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #555;
|
||||
padding: 2px 7px;
|
||||
display: inline-block;
|
||||
margin: 2px 6px 2px 0;
|
||||
}
|
||||
|
|
|
@ -11,6 +11,11 @@ $_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
|
|||
$solr_status = (preg_match("/^([yY][eE][sS]|[yY])+$/", $_ENV["SKIP_SOLR"])) ? false : solr_status();
|
||||
$clamd_status = (preg_match("/^([yY][eE][sS]|[yY])+$/", $_ENV["SKIP_CLAMD"])) ? false : true;
|
||||
|
||||
|
||||
if (!isset($_SESSION['gal']) && $license_cache = $redis->Get('LICENSE_STATUS_CACHE')) {
|
||||
$_SESSION['gal'] = json_decode($license_cache, true);
|
||||
}
|
||||
|
||||
$js_minifier->add('/web/js/site/debug.js');
|
||||
|
||||
// vmail df
|
||||
|
@ -54,11 +59,13 @@ $template_data = [
|
|||
'vmail_df' => $vmail_df,
|
||||
'hostname' => $hostname,
|
||||
'timezone' => $timezone,
|
||||
'gal' => @$_SESSION['gal'],
|
||||
'license_guid' => license('guid'),
|
||||
'solr_status' => $solr_status,
|
||||
'solr_uptime' => round($solr_status['status']['dovecot-fts']['uptime'] / 1000 / 60 / 60),
|
||||
'clamd_status' => $clamd_status,
|
||||
'containers' => $containers,
|
||||
'ip_check' => customize('get', 'ip_check'),
|
||||
'lang_admin' => json_encode($lang['admin']),
|
||||
'lang_debug' => json_encode($lang['debug']),
|
||||
'lang_datatables' => json_encode($lang['datatables']),
|
||||
|
|
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.
Binary file not shown.
|
@ -49,7 +49,9 @@ function bcc($_action, $_data = null, $_attr = null) {
|
|||
}
|
||||
elseif (filter_var($local_dest, FILTER_VALIDATE_EMAIL)) {
|
||||
$mailbox = mailbox('get', 'mailbox_details', $local_dest);
|
||||
if ($mailbox === false && array_key_exists($local_dest, array_merge($direct_aliases, $shared_aliases)) === false) {
|
||||
$shared_aliases = mailbox('get', 'shared_aliases');
|
||||
$direct_aliases = mailbox('get', 'direct_aliases');
|
||||
if ($mailbox === false && in_array($local_dest, array_merge($direct_aliases, $shared_aliases)) === false) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data, $_attr),
|
||||
|
|
|
@ -160,6 +160,25 @@ function customize($_action, $_item, $_data = null) {
|
|||
'msg' => 'ui_texts'
|
||||
);
|
||||
break;
|
||||
case 'ip_check':
|
||||
$ip_check = ($_data['ip_check_opt_in'] == "1") ? 1 : 0;
|
||||
try {
|
||||
$redis->set('IP_CHECK', $ip_check);
|
||||
}
|
||||
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' => 'ip_check_opt_in_modified'
|
||||
);
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case 'delete':
|
||||
|
@ -276,6 +295,20 @@ function customize($_action, $_item, $_data = null) {
|
|||
return false;
|
||||
}
|
||||
break;
|
||||
case 'ip_check':
|
||||
try {
|
||||
$ip_check = ($ip_check = $redis->get('IP_CHECK')) ? $ip_check : 0;
|
||||
return $ip_check;
|
||||
}
|
||||
catch (RedisException $e) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_item, $_data),
|
||||
'msg' => array('redis_error', $e)
|
||||
);
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
|
|
@ -192,5 +192,16 @@ function docker($action, $service_name = null, $attr1 = null, $attr2 = null, $ex
|
|||
}
|
||||
return false;
|
||||
break;
|
||||
case 'broadcast':
|
||||
$request = array(
|
||||
"api_call" => "container_post",
|
||||
"container_name" => $service_name,
|
||||
"post_action" => $attr1,
|
||||
"request" => $attr2
|
||||
);
|
||||
|
||||
$redis->publish("MC_CHANNEL", json_encode($request));
|
||||
return true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,407 +1,468 @@
|
|||
<?php
|
||||
function domain_admin($_action, $_data = null) {
|
||||
global $pdo;
|
||||
global $lang;
|
||||
$_data_log = $_data;
|
||||
!isset($_data_log['password']) ?: $_data_log['password'] = '*';
|
||||
!isset($_data_log['password2']) ?: $_data_log['password2'] = '*';
|
||||
!isset($_data_log['user_old_pass']) ?: $_data_log['user_old_pass'] = '*';
|
||||
!isset($_data_log['user_new_pass']) ?: $_data_log['user_new_pass'] = '*';
|
||||
!isset($_data_log['user_new_pass2']) ?: $_data_log['user_new_pass2'] = '*';
|
||||
switch ($_action) {
|
||||
case 'add':
|
||||
$username = strtolower(trim($_data['username']));
|
||||
$password = $_data['password'];
|
||||
$password2 = $_data['password2'];
|
||||
$domains = (array)$_data['domains'];
|
||||
$active = intval($_data['active']);
|
||||
if ($_SESSION['mailcow_cc_role'] != "admin") {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if (empty($domains)) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => 'domain_invalid'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if (!ctype_alnum(str_replace(array('_', '.', '-'), '', $username)) || empty ($username) || $username == 'API') {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('username_invalid', $username)
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare("SELECT `username` FROM `mailbox`
|
||||
WHERE `username` = :username");
|
||||
$stmt->execute(array(':username' => $username));
|
||||
$num_results[] = count($stmt->fetchAll(PDO::FETCH_ASSOC));
|
||||
|
||||
$stmt = $pdo->prepare("SELECT `username` FROM `admin`
|
||||
WHERE `username` = :username");
|
||||
$stmt->execute(array(':username' => $username));
|
||||
$num_results[] = count($stmt->fetchAll(PDO::FETCH_ASSOC));
|
||||
|
||||
$stmt = $pdo->prepare("SELECT `username` FROM `domain_admins`
|
||||
WHERE `username` = :username");
|
||||
$stmt->execute(array(':username' => $username));
|
||||
$num_results[] = count($stmt->fetchAll(PDO::FETCH_ASSOC));
|
||||
|
||||
foreach ($num_results as $num_results_each) {
|
||||
if ($num_results_each != 0) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('object_exists', htmlspecialchars($username))
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (password_check($password, $password2) !== true) {
|
||||
continue;
|
||||
}
|
||||
$password_hashed = hash_password($password);
|
||||
$valid_domains = 0;
|
||||
foreach ($domains as $domain) {
|
||||
if (!is_valid_domain_name($domain) || mailbox('get', 'domain_details', $domain) === false) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('domain_invalid', htmlspecialchars($domain))
|
||||
);
|
||||
continue;
|
||||
}
|
||||
$valid_domains++;
|
||||
$stmt = $pdo->prepare("INSERT INTO `domain_admins` (`username`, `domain`, `created`, `active`)
|
||||
VALUES (:username, :domain, :created, :active)");
|
||||
$stmt->execute(array(
|
||||
':username' => $username,
|
||||
':domain' => $domain,
|
||||
':created' => date('Y-m-d H:i:s'),
|
||||
':active' => $active
|
||||
));
|
||||
}
|
||||
if ($valid_domains != 0) {
|
||||
$stmt = $pdo->prepare("INSERT INTO `admin` (`username`, `password`, `superadmin`, `active`)
|
||||
VALUES (:username, :password_hashed, '0', :active)");
|
||||
$stmt->execute(array(
|
||||
':username' => $username,
|
||||
':password_hashed' => $password_hashed,
|
||||
':active' => $active
|
||||
));
|
||||
}
|
||||
$stmt = $pdo->prepare("INSERT INTO `da_acl` (`username`) VALUES (:username)");
|
||||
$stmt->execute(array(
|
||||
':username' => $username
|
||||
));
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('domain_admin_added', htmlspecialchars($username))
|
||||
);
|
||||
break;
|
||||
case 'edit':
|
||||
if ($_SESSION['mailcow_cc_role'] != "admin" && $_SESSION['mailcow_cc_role'] != "domainadmin") {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
// Administrator
|
||||
if ($_SESSION['mailcow_cc_role'] == "admin") {
|
||||
if (!is_array($_data['username'])) {
|
||||
$usernames = array();
|
||||
$usernames[] = $_data['username'];
|
||||
}
|
||||
else {
|
||||
$usernames = $_data['username'];
|
||||
}
|
||||
foreach ($usernames as $username) {
|
||||
$is_now = domain_admin('details', $username);
|
||||
$domains = (isset($_data['domains'])) ? (array)$_data['domains'] : null;
|
||||
if (!empty($is_now)) {
|
||||
$active = (isset($_data['active'])) ? intval($_data['active']) : $is_now['active'];
|
||||
$domains = (!empty($domains)) ? $domains : $is_now['selected_domains'];
|
||||
$username_new = (!empty($_data['username_new'])) ? $_data['username_new'] : $is_now['username'];
|
||||
}
|
||||
else {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
continue;
|
||||
}
|
||||
$password = $_data['password'];
|
||||
$password2 = $_data['password2'];
|
||||
if (!empty($domains)) {
|
||||
foreach ($domains as $domain) {
|
||||
if (!is_valid_domain_name($domain) || mailbox('get', 'domain_details', $domain) === false) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('domain_invalid', htmlspecialchars($domain))
|
||||
);
|
||||
continue 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!ctype_alnum(str_replace(array('_', '.', '-'), '', $username_new))) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('username_invalid', $username_new)
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if ($username_new != $username) {
|
||||
if (!empty(domain_admin('details', $username_new)['username'])) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('username_invalid', $username_new)
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
$stmt = $pdo->prepare("DELETE FROM `domain_admins` WHERE `username` = :username");
|
||||
$stmt->execute(array(
|
||||
':username' => $username,
|
||||
));
|
||||
$stmt = $pdo->prepare("UPDATE `da_acl` SET `username` = :username_new WHERE `username` = :username");
|
||||
$stmt->execute(array(
|
||||
':username_new' => $username_new,
|
||||
':username' => $username
|
||||
));
|
||||
if (!empty($domains)) {
|
||||
foreach ($domains as $domain) {
|
||||
$stmt = $pdo->prepare("INSERT INTO `domain_admins` (`username`, `domain`, `created`, `active`)
|
||||
VALUES (:username_new, :domain, :created, :active)");
|
||||
$stmt->execute(array(
|
||||
':username_new' => $username_new,
|
||||
':domain' => $domain,
|
||||
':created' => date('Y-m-d H:i:s'),
|
||||
':active' => $active
|
||||
));
|
||||
}
|
||||
}
|
||||
if (!empty($password)) {
|
||||
if (password_check($password, $password2) !== true) {
|
||||
return false;
|
||||
}
|
||||
$password_hashed = hash_password($password);
|
||||
$stmt = $pdo->prepare("UPDATE `admin` SET `username` = :username_new, `active` = :active, `password` = :password_hashed WHERE `username` = :username");
|
||||
$stmt->execute(array(
|
||||
':password_hashed' => $password_hashed,
|
||||
':username_new' => $username_new,
|
||||
':username' => $username,
|
||||
':active' => $active
|
||||
));
|
||||
if (isset($_data['disable_tfa'])) {
|
||||
$stmt = $pdo->prepare("UPDATE `tfa` SET `active` = '0' WHERE `username` = :username");
|
||||
$stmt->execute(array(':username' => $username));
|
||||
}
|
||||
else {
|
||||
$stmt = $pdo->prepare("UPDATE `tfa` SET `username` = :username_new WHERE `username` = :username");
|
||||
$stmt->execute(array(':username_new' => $username_new, ':username' => $username));
|
||||
}
|
||||
}
|
||||
else {
|
||||
$stmt = $pdo->prepare("UPDATE `admin` SET `username` = :username_new, `active` = :active WHERE `username` = :username");
|
||||
$stmt->execute(array(
|
||||
':username_new' => $username_new,
|
||||
':username' => $username,
|
||||
':active' => $active
|
||||
));
|
||||
if (isset($_data['disable_tfa'])) {
|
||||
$stmt = $pdo->prepare("UPDATE `tfa` SET `active` = '0' WHERE `username` = :username");
|
||||
$stmt->execute(array(':username' => $username));
|
||||
}
|
||||
else {
|
||||
$stmt = $pdo->prepare("UPDATE `tfa` SET `username` = :username_new WHERE `username` = :username");
|
||||
$stmt->execute(array(':username_new' => $username_new, ':username' => $username));
|
||||
}
|
||||
}
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('domain_admin_modified', htmlspecialchars($username))
|
||||
);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
// Domain administrator
|
||||
// Can only edit itself
|
||||
elseif ($_SESSION['mailcow_cc_role'] == "domainadmin") {
|
||||
$username = $_SESSION['mailcow_cc_username'];
|
||||
$password_old = $_data['user_old_pass'];
|
||||
$password_new = $_data['user_new_pass'];
|
||||
$password_new2 = $_data['user_new_pass2'];
|
||||
|
||||
$stmt = $pdo->prepare("SELECT `password` FROM `admin`
|
||||
WHERE `username` = :user");
|
||||
$stmt->execute(array(':user' => $username));
|
||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
if (!verify_hash($row['password'], $password_old)) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if (password_check($password_new, $password_new2) !== true) {
|
||||
return false;
|
||||
}
|
||||
$password_hashed = hash_password($password_new);
|
||||
$stmt = $pdo->prepare("UPDATE `admin` SET `password` = :password_hashed WHERE `username` = :username");
|
||||
$stmt->execute(array(
|
||||
':password_hashed' => $password_hashed,
|
||||
':username' => $username
|
||||
));
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('domain_admin_modified', htmlspecialchars($username))
|
||||
);
|
||||
}
|
||||
break;
|
||||
case 'delete':
|
||||
if ($_SESSION['mailcow_cc_role'] != "admin") {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
$usernames = (array)$_data['username'];
|
||||
foreach ($usernames as $username) {
|
||||
if (empty(domain_admin('details', $username))) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('username_invalid', $username)
|
||||
);
|
||||
continue;
|
||||
}
|
||||
$stmt = $pdo->prepare("DELETE FROM `domain_admins` WHERE `username` = :username");
|
||||
$stmt->execute(array(
|
||||
':username' => $username,
|
||||
));
|
||||
$stmt = $pdo->prepare("DELETE FROM `admin` WHERE `username` = :username");
|
||||
$stmt->execute(array(
|
||||
':username' => $username,
|
||||
));
|
||||
$stmt = $pdo->prepare("DELETE FROM `da_acl` WHERE `username` = :username");
|
||||
$stmt->execute(array(
|
||||
':username' => $username,
|
||||
));
|
||||
$stmt = $pdo->prepare("DELETE FROM `tfa` WHERE `username` = :username");
|
||||
$stmt->execute(array(
|
||||
':username' => $username,
|
||||
));
|
||||
$stmt = $pdo->prepare("DELETE FROM `fido2` WHERE `username` = :username");
|
||||
$stmt->execute(array(
|
||||
':username' => $username,
|
||||
));
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('domain_admin_removed', htmlspecialchars($username))
|
||||
);
|
||||
}
|
||||
break;
|
||||
case 'get':
|
||||
$domainadmins = array();
|
||||
if ($_SESSION['mailcow_cc_role'] != "admin") {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
$stmt = $pdo->query("SELECT DISTINCT
|
||||
`username`
|
||||
FROM `domain_admins`
|
||||
WHERE `username` IN (
|
||||
SELECT `username` FROM `admin`
|
||||
WHERE `superadmin`!='1'
|
||||
)");
|
||||
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
while ($row = array_shift($rows)) {
|
||||
$domainadmins[] = $row['username'];
|
||||
}
|
||||
return $domainadmins;
|
||||
break;
|
||||
case 'details':
|
||||
$domainadmindata = array();
|
||||
if ($_SESSION['mailcow_cc_role'] == "domainadmin" && $_data != $_SESSION['mailcow_cc_username']) {
|
||||
return false;
|
||||
}
|
||||
elseif ($_SESSION['mailcow_cc_role'] != "admin" || !isset($_data)) {
|
||||
return false;
|
||||
}
|
||||
if (!ctype_alnum(str_replace(array('_', '.', '-'), '', $_data))) {
|
||||
return false;
|
||||
}
|
||||
$stmt = $pdo->prepare("SELECT
|
||||
`tfa`.`active` AS `tfa_active`,
|
||||
`domain_admins`.`username`,
|
||||
`domain_admins`.`created`,
|
||||
`domain_admins`.`active` AS `active`
|
||||
FROM `domain_admins`
|
||||
LEFT OUTER JOIN `tfa` ON `tfa`.`username`=`domain_admins`.`username`
|
||||
WHERE `domain_admins`.`username`= :domain_admin");
|
||||
$stmt->execute(array(
|
||||
':domain_admin' => $_data
|
||||
));
|
||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
if (empty($row)) {
|
||||
return false;
|
||||
}
|
||||
$domainadmindata['username'] = $row['username'];
|
||||
$domainadmindata['tfa_active'] = (is_null($row['tfa_active'])) ? 0 : $row['tfa_active'];
|
||||
$domainadmindata['tfa_active_int'] = (is_null($row['tfa_active'])) ? 0 : $row['tfa_active'];
|
||||
$domainadmindata['active'] = $row['active'];
|
||||
$domainadmindata['active_int'] = $row['active'];
|
||||
$domainadmindata['created'] = $row['created'];
|
||||
// GET SELECTED
|
||||
$stmt = $pdo->prepare("SELECT `domain` FROM `domain`
|
||||
WHERE `domain` IN (
|
||||
SELECT `domain` FROM `domain_admins`
|
||||
WHERE `username`= :domain_admin)");
|
||||
$stmt->execute(array(':domain_admin' => $_data));
|
||||
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
while($row = array_shift($rows)) {
|
||||
$domainadmindata['selected_domains'][] = $row['domain'];
|
||||
}
|
||||
// GET UNSELECTED
|
||||
$stmt = $pdo->prepare("SELECT `domain` FROM `domain`
|
||||
WHERE `domain` NOT IN (
|
||||
SELECT `domain` FROM `domain_admins`
|
||||
WHERE `username`= :domain_admin)");
|
||||
$stmt->execute(array(':domain_admin' => $_data));
|
||||
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
while($row = array_shift($rows)) {
|
||||
$domainadmindata['unselected_domains'][] = $row['domain'];
|
||||
}
|
||||
if (!isset($domainadmindata['unselected_domains'])) {
|
||||
$domainadmindata['unselected_domains'] = "";
|
||||
}
|
||||
|
||||
return $domainadmindata;
|
||||
break;
|
||||
}
|
||||
}
|
||||
<?php
|
||||
function domain_admin($_action, $_data = null) {
|
||||
global $pdo;
|
||||
global $lang;
|
||||
$_data_log = $_data;
|
||||
!isset($_data_log['password']) ?: $_data_log['password'] = '*';
|
||||
!isset($_data_log['password2']) ?: $_data_log['password2'] = '*';
|
||||
!isset($_data_log['user_old_pass']) ?: $_data_log['user_old_pass'] = '*';
|
||||
!isset($_data_log['user_new_pass']) ?: $_data_log['user_new_pass'] = '*';
|
||||
!isset($_data_log['user_new_pass2']) ?: $_data_log['user_new_pass2'] = '*';
|
||||
switch ($_action) {
|
||||
case 'add':
|
||||
$username = strtolower(trim($_data['username']));
|
||||
$password = $_data['password'];
|
||||
$password2 = $_data['password2'];
|
||||
$domains = (array)$_data['domains'];
|
||||
$active = intval($_data['active']);
|
||||
if ($_SESSION['mailcow_cc_role'] != "admin") {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if (empty($domains)) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => 'domain_invalid'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if (!ctype_alnum(str_replace(array('_', '.', '-'), '', $username)) || empty ($username) || $username == 'API') {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('username_invalid', $username)
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare("SELECT `username` FROM `mailbox`
|
||||
WHERE `username` = :username");
|
||||
$stmt->execute(array(':username' => $username));
|
||||
$num_results[] = count($stmt->fetchAll(PDO::FETCH_ASSOC));
|
||||
|
||||
$stmt = $pdo->prepare("SELECT `username` FROM `admin`
|
||||
WHERE `username` = :username");
|
||||
$stmt->execute(array(':username' => $username));
|
||||
$num_results[] = count($stmt->fetchAll(PDO::FETCH_ASSOC));
|
||||
|
||||
$stmt = $pdo->prepare("SELECT `username` FROM `domain_admins`
|
||||
WHERE `username` = :username");
|
||||
$stmt->execute(array(':username' => $username));
|
||||
$num_results[] = count($stmt->fetchAll(PDO::FETCH_ASSOC));
|
||||
|
||||
foreach ($num_results as $num_results_each) {
|
||||
if ($num_results_each != 0) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('object_exists', htmlspecialchars($username))
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (password_check($password, $password2) !== true) {
|
||||
continue;
|
||||
}
|
||||
$password_hashed = hash_password($password);
|
||||
$valid_domains = 0;
|
||||
foreach ($domains as $domain) {
|
||||
if (!is_valid_domain_name($domain) || mailbox('get', 'domain_details', $domain) === false) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('domain_invalid', htmlspecialchars($domain))
|
||||
);
|
||||
continue;
|
||||
}
|
||||
$valid_domains++;
|
||||
$stmt = $pdo->prepare("INSERT INTO `domain_admins` (`username`, `domain`, `created`, `active`)
|
||||
VALUES (:username, :domain, :created, :active)");
|
||||
$stmt->execute(array(
|
||||
':username' => $username,
|
||||
':domain' => $domain,
|
||||
':created' => date('Y-m-d H:i:s'),
|
||||
':active' => $active
|
||||
));
|
||||
}
|
||||
if ($valid_domains != 0) {
|
||||
$stmt = $pdo->prepare("INSERT INTO `admin` (`username`, `password`, `superadmin`, `active`)
|
||||
VALUES (:username, :password_hashed, '0', :active)");
|
||||
$stmt->execute(array(
|
||||
':username' => $username,
|
||||
':password_hashed' => $password_hashed,
|
||||
':active' => $active
|
||||
));
|
||||
}
|
||||
$stmt = $pdo->prepare("INSERT INTO `da_acl` (`username`) VALUES (:username)");
|
||||
$stmt->execute(array(
|
||||
':username' => $username
|
||||
));
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('domain_admin_added', htmlspecialchars($username))
|
||||
);
|
||||
break;
|
||||
case 'edit':
|
||||
if ($_SESSION['mailcow_cc_role'] != "admin" && $_SESSION['mailcow_cc_role'] != "domainadmin") {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
// Administrator
|
||||
if ($_SESSION['mailcow_cc_role'] == "admin") {
|
||||
if (!is_array($_data['username'])) {
|
||||
$usernames = array();
|
||||
$usernames[] = $_data['username'];
|
||||
}
|
||||
else {
|
||||
$usernames = $_data['username'];
|
||||
}
|
||||
foreach ($usernames as $username) {
|
||||
$is_now = domain_admin('details', $username);
|
||||
$domains = (isset($_data['domains'])) ? (array)$_data['domains'] : null;
|
||||
if (!empty($is_now)) {
|
||||
$active = (isset($_data['active'])) ? intval($_data['active']) : $is_now['active'];
|
||||
$domains = (!empty($domains)) ? $domains : $is_now['selected_domains'];
|
||||
$username_new = (!empty($_data['username_new'])) ? $_data['username_new'] : $is_now['username'];
|
||||
}
|
||||
else {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
continue;
|
||||
}
|
||||
$password = $_data['password'];
|
||||
$password2 = $_data['password2'];
|
||||
if (!empty($domains)) {
|
||||
foreach ($domains as $domain) {
|
||||
if (!is_valid_domain_name($domain) || mailbox('get', 'domain_details', $domain) === false) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('domain_invalid', htmlspecialchars($domain))
|
||||
);
|
||||
continue 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!ctype_alnum(str_replace(array('_', '.', '-'), '', $username_new))) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('username_invalid', $username_new)
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if ($username_new != $username) {
|
||||
if (!empty(domain_admin('details', $username_new)['username'])) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('username_invalid', $username_new)
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
$stmt = $pdo->prepare("DELETE FROM `domain_admins` WHERE `username` = :username");
|
||||
$stmt->execute(array(
|
||||
':username' => $username,
|
||||
));
|
||||
$stmt = $pdo->prepare("UPDATE `da_acl` SET `username` = :username_new WHERE `username` = :username");
|
||||
$stmt->execute(array(
|
||||
':username_new' => $username_new,
|
||||
':username' => $username
|
||||
));
|
||||
if (!empty($domains)) {
|
||||
foreach ($domains as $domain) {
|
||||
$stmt = $pdo->prepare("INSERT INTO `domain_admins` (`username`, `domain`, `created`, `active`)
|
||||
VALUES (:username_new, :domain, :created, :active)");
|
||||
$stmt->execute(array(
|
||||
':username_new' => $username_new,
|
||||
':domain' => $domain,
|
||||
':created' => date('Y-m-d H:i:s'),
|
||||
':active' => $active
|
||||
));
|
||||
}
|
||||
}
|
||||
if (!empty($password)) {
|
||||
if (password_check($password, $password2) !== true) {
|
||||
return false;
|
||||
}
|
||||
$password_hashed = hash_password($password);
|
||||
$stmt = $pdo->prepare("UPDATE `admin` SET `username` = :username_new, `active` = :active, `password` = :password_hashed WHERE `username` = :username");
|
||||
$stmt->execute(array(
|
||||
':password_hashed' => $password_hashed,
|
||||
':username_new' => $username_new,
|
||||
':username' => $username,
|
||||
':active' => $active
|
||||
));
|
||||
if (isset($_data['disable_tfa'])) {
|
||||
$stmt = $pdo->prepare("UPDATE `tfa` SET `active` = '0' WHERE `username` = :username");
|
||||
$stmt->execute(array(':username' => $username));
|
||||
}
|
||||
else {
|
||||
$stmt = $pdo->prepare("UPDATE `tfa` SET `username` = :username_new WHERE `username` = :username");
|
||||
$stmt->execute(array(':username_new' => $username_new, ':username' => $username));
|
||||
}
|
||||
}
|
||||
else {
|
||||
$stmt = $pdo->prepare("UPDATE `admin` SET `username` = :username_new, `active` = :active WHERE `username` = :username");
|
||||
$stmt->execute(array(
|
||||
':username_new' => $username_new,
|
||||
':username' => $username,
|
||||
':active' => $active
|
||||
));
|
||||
if (isset($_data['disable_tfa'])) {
|
||||
$stmt = $pdo->prepare("UPDATE `tfa` SET `active` = '0' WHERE `username` = :username");
|
||||
$stmt->execute(array(':username' => $username));
|
||||
}
|
||||
else {
|
||||
$stmt = $pdo->prepare("UPDATE `tfa` SET `username` = :username_new WHERE `username` = :username");
|
||||
$stmt->execute(array(':username_new' => $username_new, ':username' => $username));
|
||||
}
|
||||
}
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('domain_admin_modified', htmlspecialchars($username))
|
||||
);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
// Domain administrator
|
||||
// Can only edit itself
|
||||
elseif ($_SESSION['mailcow_cc_role'] == "domainadmin") {
|
||||
$username = $_SESSION['mailcow_cc_username'];
|
||||
$password_old = $_data['user_old_pass'];
|
||||
$password_new = $_data['user_new_pass'];
|
||||
$password_new2 = $_data['user_new_pass2'];
|
||||
|
||||
$stmt = $pdo->prepare("SELECT `password` FROM `admin`
|
||||
WHERE `username` = :user");
|
||||
$stmt->execute(array(':user' => $username));
|
||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
if (!verify_hash($row['password'], $password_old)) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if (password_check($password_new, $password_new2) !== true) {
|
||||
return false;
|
||||
}
|
||||
$password_hashed = hash_password($password_new);
|
||||
$stmt = $pdo->prepare("UPDATE `admin` SET `password` = :password_hashed WHERE `username` = :username");
|
||||
$stmt->execute(array(
|
||||
':password_hashed' => $password_hashed,
|
||||
':username' => $username
|
||||
));
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('domain_admin_modified', htmlspecialchars($username))
|
||||
);
|
||||
}
|
||||
break;
|
||||
case 'delete':
|
||||
if ($_SESSION['mailcow_cc_role'] != "admin") {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
$usernames = (array)$_data['username'];
|
||||
foreach ($usernames as $username) {
|
||||
if (empty(domain_admin('details', $username))) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('username_invalid', $username)
|
||||
);
|
||||
continue;
|
||||
}
|
||||
$stmt = $pdo->prepare("DELETE FROM `domain_admins` WHERE `username` = :username");
|
||||
$stmt->execute(array(
|
||||
':username' => $username,
|
||||
));
|
||||
$stmt = $pdo->prepare("DELETE FROM `admin` WHERE `username` = :username");
|
||||
$stmt->execute(array(
|
||||
':username' => $username,
|
||||
));
|
||||
$stmt = $pdo->prepare("DELETE FROM `da_acl` WHERE `username` = :username");
|
||||
$stmt->execute(array(
|
||||
':username' => $username,
|
||||
));
|
||||
$stmt = $pdo->prepare("DELETE FROM `tfa` WHERE `username` = :username");
|
||||
$stmt->execute(array(
|
||||
':username' => $username,
|
||||
));
|
||||
$stmt = $pdo->prepare("DELETE FROM `fido2` WHERE `username` = :username");
|
||||
$stmt->execute(array(
|
||||
':username' => $username,
|
||||
));
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('domain_admin_removed', htmlspecialchars($username))
|
||||
);
|
||||
}
|
||||
break;
|
||||
case 'get':
|
||||
$domainadmins = array();
|
||||
if ($_SESSION['mailcow_cc_role'] != "admin") {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
$stmt = $pdo->query("SELECT DISTINCT
|
||||
`username`
|
||||
FROM `domain_admins`
|
||||
WHERE `username` IN (
|
||||
SELECT `username` FROM `admin`
|
||||
WHERE `superadmin`!='1'
|
||||
)");
|
||||
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
while ($row = array_shift($rows)) {
|
||||
$domainadmins[] = $row['username'];
|
||||
}
|
||||
return $domainadmins;
|
||||
break;
|
||||
case 'details':
|
||||
$domainadmindata = array();
|
||||
if ($_SESSION['mailcow_cc_role'] == "domainadmin" && $_data != $_SESSION['mailcow_cc_username']) {
|
||||
return false;
|
||||
}
|
||||
elseif ($_SESSION['mailcow_cc_role'] != "admin" || !isset($_data)) {
|
||||
return false;
|
||||
}
|
||||
if (!ctype_alnum(str_replace(array('_', '.', '-'), '', $_data))) {
|
||||
return false;
|
||||
}
|
||||
$stmt = $pdo->prepare("SELECT
|
||||
`tfa`.`active` AS `tfa_active`,
|
||||
`domain_admins`.`username`,
|
||||
`domain_admins`.`created`,
|
||||
`domain_admins`.`active` AS `active`
|
||||
FROM `domain_admins`
|
||||
LEFT OUTER JOIN `tfa` ON `tfa`.`username`=`domain_admins`.`username`
|
||||
WHERE `domain_admins`.`username`= :domain_admin");
|
||||
$stmt->execute(array(
|
||||
':domain_admin' => $_data
|
||||
));
|
||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
if (empty($row)) {
|
||||
return false;
|
||||
}
|
||||
$domainadmindata['username'] = $row['username'];
|
||||
$domainadmindata['tfa_active'] = (is_null($row['tfa_active'])) ? 0 : $row['tfa_active'];
|
||||
$domainadmindata['tfa_active_int'] = (is_null($row['tfa_active'])) ? 0 : $row['tfa_active'];
|
||||
$domainadmindata['active'] = $row['active'];
|
||||
$domainadmindata['active_int'] = $row['active'];
|
||||
$domainadmindata['created'] = $row['created'];
|
||||
// GET SELECTED
|
||||
$stmt = $pdo->prepare("SELECT `domain` FROM `domain`
|
||||
WHERE `domain` IN (
|
||||
SELECT `domain` FROM `domain_admins`
|
||||
WHERE `username`= :domain_admin)");
|
||||
$stmt->execute(array(':domain_admin' => $_data));
|
||||
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
while($row = array_shift($rows)) {
|
||||
$domainadmindata['selected_domains'][] = $row['domain'];
|
||||
}
|
||||
// GET UNSELECTED
|
||||
$stmt = $pdo->prepare("SELECT `domain` FROM `domain`
|
||||
WHERE `domain` NOT IN (
|
||||
SELECT `domain` FROM `domain_admins`
|
||||
WHERE `username`= :domain_admin)");
|
||||
$stmt->execute(array(':domain_admin' => $_data));
|
||||
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
while($row = array_shift($rows)) {
|
||||
$domainadmindata['unselected_domains'][] = $row['domain'];
|
||||
}
|
||||
if (!isset($domainadmindata['unselected_domains'])) {
|
||||
$domainadmindata['unselected_domains'] = "";
|
||||
}
|
||||
|
||||
return $domainadmindata;
|
||||
break;
|
||||
}
|
||||
}
|
||||
function domain_admin_sso($_action, $_data) {
|
||||
global $pdo;
|
||||
|
||||
switch ($_action) {
|
||||
case 'check':
|
||||
$token = $_data;
|
||||
|
||||
$stmt = $pdo->prepare("SELECT `t1`.`username` FROM `da_sso` AS `t1` JOIN `admin` AS `t2` ON `t1`.`username` = `t2`.`username` WHERE `t1`.`token` = :token AND `t1`.`created` > DATE_SUB(NOW(), INTERVAL '30' SECOND) AND `t2`.`active` = 1 AND `t2`.`superadmin` = 0;");
|
||||
$stmt->execute(array(
|
||||
':token' => preg_replace('/[^a-zA-Z0-9-]/', '', $token)
|
||||
));
|
||||
$return = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
return empty($return['username']) ? false : $return['username'];
|
||||
case 'issue':
|
||||
if ($_SESSION['mailcow_cc_role'] != "admin") {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
$username = $_data['username'];
|
||||
|
||||
$stmt = $pdo->prepare("SELECT `username` FROM `domain_admins`
|
||||
WHERE `username` = :username");
|
||||
$stmt->execute(array(':username' => $username));
|
||||
$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
|
||||
|
||||
if ($num_results < 1) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data),
|
||||
'msg' => array('object_doesnt_exist', htmlspecialchars($username))
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
$token = implode('-', array(
|
||||
strtoupper(bin2hex(random_bytes(3))),
|
||||
strtoupper(bin2hex(random_bytes(3))),
|
||||
strtoupper(bin2hex(random_bytes(3))),
|
||||
strtoupper(bin2hex(random_bytes(3))),
|
||||
strtoupper(bin2hex(random_bytes(3)))
|
||||
));
|
||||
|
||||
$stmt = $pdo->prepare("INSERT INTO `da_sso` (`username`, `token`)
|
||||
VALUES (:username, :token)");
|
||||
$stmt->execute(array(
|
||||
':username' => $username,
|
||||
':token' => $token
|
||||
));
|
||||
|
||||
// perform cleanup
|
||||
$pdo->query("DELETE FROM `da_sso` WHERE created < DATE_SUB(NOW(), INTERVAL '30' SECOND);");
|
||||
|
||||
return ['token' => $token];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -239,7 +239,9 @@ function fail2ban($_action, $_data = null) {
|
|||
$is_now = fail2ban('get');
|
||||
if (!empty($is_now)) {
|
||||
$ban_time = intval((isset($_data['ban_time'])) ? $_data['ban_time'] : $is_now['ban_time']);
|
||||
$ban_time_increment = (isset($_data['ban_time_increment']) && $_data['ban_time_increment'] == "1") ? 1 : 0;
|
||||
$max_attempts = intval((isset($_data['max_attempts'])) ? $_data['max_attempts'] : $is_now['max_attempts']);
|
||||
$max_ban_time = intval((isset($_data['max_ban_time'])) ? $_data['max_ban_time'] : $is_now['max_ban_time']);
|
||||
$retry_window = intval((isset($_data['retry_window'])) ? $_data['retry_window'] : $is_now['retry_window']);
|
||||
$netban_ipv4 = intval((isset($_data['netban_ipv4'])) ? $_data['netban_ipv4'] : $is_now['netban_ipv4']);
|
||||
$netban_ipv6 = intval((isset($_data['netban_ipv6'])) ? $_data['netban_ipv6'] : $is_now['netban_ipv6']);
|
||||
|
@ -256,6 +258,8 @@ function fail2ban($_action, $_data = null) {
|
|||
}
|
||||
$f2b_options = array();
|
||||
$f2b_options['ban_time'] = ($ban_time < 60) ? 60 : $ban_time;
|
||||
$f2b_options['ban_time_increment'] = ($ban_time_increment == 1) ? true : false;
|
||||
$f2b_options['max_ban_time'] = ($max_ban_time < 60) ? 60 : $max_ban_time;
|
||||
$f2b_options['netban_ipv4'] = ($netban_ipv4 < 8) ? 8 : $netban_ipv4;
|
||||
$f2b_options['netban_ipv6'] = ($netban_ipv6 < 8) ? 8 : $netban_ipv6;
|
||||
$f2b_options['netban_ipv4'] = ($netban_ipv4 > 32) ? 32 : $netban_ipv4;
|
||||
|
|
|
@ -526,8 +526,9 @@ function logger($_data = false) {
|
|||
':remote' => get_remote_ip()
|
||||
));
|
||||
}
|
||||
catch (Exception $e) {
|
||||
// Do nothing
|
||||
catch (PDOException $e) {
|
||||
# handle the exception here, as the exception handler function results in a white page
|
||||
error_log($e->getMessage(), 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1015,20 +1016,58 @@ function formatBytes($size, $precision = 2) {
|
|||
}
|
||||
return round(pow(1024, $base - floor($base)), $precision) . $suffixes[floor($base)];
|
||||
}
|
||||
function update_sogo_static_view() {
|
||||
function update_sogo_static_view($mailbox = null) {
|
||||
if (getenv('SKIP_SOGO') == "y") {
|
||||
return true;
|
||||
}
|
||||
global $pdo;
|
||||
global $lang;
|
||||
$stmt = $pdo->query("SELECT 'OK' FROM INFORMATION_SCHEMA.TABLES
|
||||
WHERE TABLE_NAME = 'sogo_view'");
|
||||
$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
|
||||
if ($num_results != 0) {
|
||||
$stmt = $pdo->query("REPLACE INTO _sogo_static_view (`c_uid`, `domain`, `c_name`, `c_password`, `c_cn`, `mail`, `aliases`, `ad_aliases`, `ext_acl`, `kind`, `multiple_bookings`)
|
||||
SELECT `c_uid`, `domain`, `c_name`, `c_password`, `c_cn`, `mail`, `aliases`, `ad_aliases`, `ext_acl`, `kind`, `multiple_bookings` from sogo_view");
|
||||
$stmt = $pdo->query("DELETE FROM _sogo_static_view WHERE `c_uid` NOT IN (SELECT `username` FROM `mailbox` WHERE `active` = '1');");
|
||||
|
||||
$mailbox_exists = false;
|
||||
if ($mailbox !== null) {
|
||||
// Check if the mailbox exists
|
||||
$stmt = $pdo->prepare("SELECT username FROM mailbox WHERE username = :mailbox AND active = '1'");
|
||||
$stmt->execute(array(':mailbox' => $mailbox));
|
||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
if ($row){
|
||||
$mailbox_exists = true;
|
||||
}
|
||||
}
|
||||
|
||||
$query = "REPLACE INTO _sogo_static_view (`c_uid`, `domain`, `c_name`, `c_password`, `c_cn`, `mail`, `aliases`, `ad_aliases`, `ext_acl`, `kind`, `multiple_bookings`)
|
||||
SELECT
|
||||
mailbox.username,
|
||||
mailbox.domain,
|
||||
mailbox.username,
|
||||
IF(JSON_UNQUOTE(JSON_VALUE(attributes, '$.force_pw_update')) = '0',
|
||||
IF(JSON_UNQUOTE(JSON_VALUE(attributes, '$.sogo_access')) = 1, password, '{SSHA256}A123A123A321A321A321B321B321B123B123B321B432F123E321123123321321'),
|
||||
'{SSHA256}A123A123A321A321A321B321B321B123B123B321B432F123E321123123321321'),
|
||||
mailbox.name,
|
||||
mailbox.username,
|
||||
IFNULL(GROUP_CONCAT(ga.aliases ORDER BY ga.aliases SEPARATOR ' '), ''),
|
||||
IFNULL(gda.ad_alias, ''),
|
||||
IFNULL(external_acl.send_as_acl, ''),
|
||||
mailbox.kind,
|
||||
mailbox.multiple_bookings
|
||||
FROM
|
||||
mailbox
|
||||
LEFT OUTER JOIN grouped_mail_aliases ga ON ga.username REGEXP CONCAT('(^|,)', mailbox.username, '($|,)')
|
||||
LEFT OUTER JOIN grouped_domain_alias_address gda ON gda.username = mailbox.username
|
||||
LEFT OUTER JOIN grouped_sender_acl_external external_acl ON external_acl.username = mailbox.username
|
||||
WHERE
|
||||
mailbox.active = '1'";
|
||||
|
||||
if ($mailbox_exists) {
|
||||
$query .= " AND mailbox.username = :mailbox";
|
||||
$stmt = $pdo->prepare($query);
|
||||
$stmt->execute(array(':mailbox' => $mailbox));
|
||||
} else {
|
||||
$query .= " GROUP BY mailbox.username";
|
||||
$stmt = $pdo->query($query);
|
||||
}
|
||||
|
||||
$stmt = $pdo->query("DELETE FROM _sogo_static_view WHERE `c_uid` NOT IN (SELECT `username` FROM `mailbox` WHERE `active` = '1');");
|
||||
|
||||
flush_memcached();
|
||||
}
|
||||
function edit_user_account($_data) {
|
||||
|
@ -1739,7 +1778,7 @@ function verify_tfa_login($username, $_data) {
|
|||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $username, '*'),
|
||||
'msg' => array('webauthn_verification_failed', 'authenticator not found')
|
||||
'msg' => array('webauthn_authenticator_failed')
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
@ -1748,11 +1787,20 @@ function verify_tfa_login($username, $_data) {
|
|||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $username, '*'),
|
||||
'msg' => array('webauthn_verification_failed', 'publicKey not found')
|
||||
'msg' => array('webauthn_publickey_failed')
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($process_webauthn['username'] != $_SESSION['pending_mailcow_cc_username']){
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $username, '*'),
|
||||
'msg' => array('webauthn_username_failed')
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
$WebAuthn->processGet($clientDataJSON, $authenticatorData, $signature, $process_webauthn['publicKey'], $challenge, null, $GLOBALS['WEBAUTHN_UV_FLAG_LOGIN'], $GLOBALS['WEBAUTHN_USER_PRESENT_FLAG']);
|
||||
}
|
||||
|
@ -1784,21 +1832,12 @@ function verify_tfa_login($username, $_data) {
|
|||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $username, '*'),
|
||||
'msg' => array('webauthn_verification_failed', 'could not determine user role')
|
||||
'msg' => array('webauthn_role_failed')
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if ($process_webauthn['username'] != $_SESSION['pending_mailcow_cc_username']){
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $username, '*'),
|
||||
'msg' => array('webauthn_verification_failed', 'user who requests does not match with sql entry')
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
$_SESSION["mailcow_cc_username"] = $process_webauthn['username'];
|
||||
$_SESSION['tfa_id'] = $process_webauthn['id'];
|
||||
$_SESSION['authReq'] = null;
|
||||
|
@ -2093,6 +2132,120 @@ function rspamd_ui($action, $data = null) {
|
|||
break;
|
||||
}
|
||||
}
|
||||
function cors($action, $data = null) {
|
||||
global $redis;
|
||||
|
||||
switch ($action) {
|
||||
case "edit":
|
||||
if ($_SESSION['mailcow_cc_role'] != "admin") {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $action, $data),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
$allowed_origins = isset($data['allowed_origins']) ? $data['allowed_origins'] : array($_SERVER['SERVER_NAME']);
|
||||
$allowed_origins = !is_array($allowed_origins) ? array_filter(array_map('trim', explode("\n", $allowed_origins))) : $allowed_origins;
|
||||
foreach ($allowed_origins as $origin) {
|
||||
if (!filter_var($origin, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME) && $origin != '*') {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $action, $data),
|
||||
'msg' => 'cors_invalid_origin'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
$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;
|
||||
$available_methods = array('GET', 'POST', 'PUT', 'DELETE');
|
||||
foreach ($allowed_methods as $method) {
|
||||
if (!in_array($method, $available_methods)) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $action, $data),
|
||||
'msg' => 'cors_invalid_method'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
$redis->hMSet('CORS_SETTINGS', array(
|
||||
'allowed_origins' => implode(', ', $allowed_origins),
|
||||
'allowed_methods' => implode(', ', $allowed_methods)
|
||||
));
|
||||
} catch (RedisException $e) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $action, $data),
|
||||
'msg' => array('redis_error', $e)
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $action, $data),
|
||||
'msg' => 'cors_headers_edited'
|
||||
);
|
||||
return true;
|
||||
break;
|
||||
case "get":
|
||||
try {
|
||||
$cors_settings = $redis->hMGet('CORS_SETTINGS', array('allowed_origins', 'allowed_methods'));
|
||||
} catch (RedisException $e) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $action, $data),
|
||||
'msg' => array('redis_error', $e)
|
||||
);
|
||||
}
|
||||
|
||||
$cors_settings = !$cors_settings ? array('allowed_origins' => $_SERVER['SERVER_NAME'], 'allowed_methods' => 'GET, POST, PUT, DELETE') : $cors_settings;
|
||||
$cors_settings['allowed_origins'] = empty($cors_settings['allowed_origins']) ? $_SERVER['SERVER_NAME'] : $cors_settings['allowed_origins'];
|
||||
$cors_settings['allowed_methods'] = empty($cors_settings['allowed_methods']) ? 'GET, POST, PUT, DELETE, OPTION' : $cors_settings['allowed_methods'];
|
||||
|
||||
return $cors_settings;
|
||||
break;
|
||||
case "set_headers":
|
||||
$cors_settings = cors('get');
|
||||
// check if requested origin is in allowed origins
|
||||
$allowed_origins = explode(', ', $cors_settings['allowed_origins']);
|
||||
$cors_settings['allowed_origins'] = $allowed_origins[0];
|
||||
if (in_array('*', $allowed_origins)){
|
||||
$cors_settings['allowed_origins'] = '*';
|
||||
} else if (in_array($_SERVER['HTTP_ORIGIN'], $allowed_origins)) {
|
||||
$cors_settings['allowed_origins'] = $_SERVER['HTTP_ORIGIN'];
|
||||
}
|
||||
// always allow OPTIONS for preflight request
|
||||
$cors_settings["allowed_methods"] = empty($cors_settings["allowed_methods"]) ? 'OPTIONS' : $cors_settings["allowed_methods"] . ', ' . 'OPTIONS';
|
||||
|
||||
header('Access-Control-Allow-Origin: ' . $cors_settings['allowed_origins']);
|
||||
header('Access-Control-Allow-Methods: '. $cors_settings['allowed_methods']);
|
||||
header('Access-Control-Allow-Headers: Accept, Content-Type, X-Api-Key, Origin');
|
||||
|
||||
// Access-Control settings requested, this is just a preflight request
|
||||
if ($_SERVER['REQUEST_METHOD'] == 'OPTIONS' &&
|
||||
isset($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_METHOD']) &&
|
||||
isset($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_HEADERS'])) {
|
||||
|
||||
$allowed_methods = explode(', ', $cors_settings["allowed_methods"]);
|
||||
if (in_array($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_METHOD'], $allowed_methods, true))
|
||||
// method allowed send 200 OK
|
||||
http_response_code(200);
|
||||
else
|
||||
// method not allowed send 405 METHOD NOT ALLOWED
|
||||
http_response_code(405);
|
||||
|
||||
exit;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function get_logs($application, $lines = false) {
|
||||
if ($lines === false) {
|
||||
|
|
|
@ -1264,11 +1264,13 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
|||
));
|
||||
}
|
||||
|
||||
update_sogo_static_view($username);
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
||||
'msg' => array('mailbox_added', htmlspecialchars($username))
|
||||
);
|
||||
return true;
|
||||
break;
|
||||
case 'resource':
|
||||
$domain = idn_to_ascii(strtolower(trim($_data['domain'])), 0, INTL_IDNA_VARIANT_UTS46);
|
||||
|
@ -2879,67 +2881,68 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
|||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
||||
'msg' => 'access_denied'
|
||||
'msg' => 'extended_sender_acl_denied'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
$extra_acls = array_map('trim', preg_split( "/( |,|;|\n)/", $_data['extended_sender_acl']));
|
||||
foreach ($extra_acls as $i => &$extra_acl) {
|
||||
if (empty($extra_acl)) {
|
||||
continue;
|
||||
}
|
||||
if (substr($extra_acl, 0, 1) === "@") {
|
||||
$extra_acl = ltrim($extra_acl, '@');
|
||||
}
|
||||
if (!filter_var($extra_acl, FILTER_VALIDATE_EMAIL) && !is_valid_domain_name($extra_acl)) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
||||
'msg' => array('extra_acl_invalid', htmlspecialchars($extra_acl))
|
||||
);
|
||||
unset($extra_acls[$i]);
|
||||
continue;
|
||||
}
|
||||
$domains = array_merge(mailbox('get', 'domains'), mailbox('get', 'alias_domains'));
|
||||
if (filter_var($extra_acl, FILTER_VALIDATE_EMAIL)) {
|
||||
$extra_acl_domain = idn_to_ascii(substr(strstr($extra_acl, '@'), 1), 0, INTL_IDNA_VARIANT_UTS46);
|
||||
if (in_array($extra_acl_domain, $domains)) {
|
||||
else {
|
||||
$extra_acls = array_map('trim', preg_split( "/( |,|;|\n)/", $_data['extended_sender_acl']));
|
||||
foreach ($extra_acls as $i => &$extra_acl) {
|
||||
if (empty($extra_acl)) {
|
||||
continue;
|
||||
}
|
||||
if (substr($extra_acl, 0, 1) === "@") {
|
||||
$extra_acl = ltrim($extra_acl, '@');
|
||||
}
|
||||
if (!filter_var($extra_acl, FILTER_VALIDATE_EMAIL) && !is_valid_domain_name($extra_acl)) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
||||
'msg' => array('extra_acl_invalid_domain', $extra_acl_domain)
|
||||
'msg' => array('extra_acl_invalid', htmlspecialchars($extra_acl))
|
||||
);
|
||||
unset($extra_acls[$i]);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (in_array($extra_acl, $domains)) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
||||
'msg' => array('extra_acl_invalid_domain', $extra_acl_domain)
|
||||
);
|
||||
unset($extra_acls[$i]);
|
||||
continue;
|
||||
$domains = array_merge(mailbox('get', 'domains'), mailbox('get', 'alias_domains'));
|
||||
if (filter_var($extra_acl, FILTER_VALIDATE_EMAIL)) {
|
||||
$extra_acl_domain = idn_to_ascii(substr(strstr($extra_acl, '@'), 1), 0, INTL_IDNA_VARIANT_UTS46);
|
||||
if (in_array($extra_acl_domain, $domains)) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
||||
'msg' => array('extra_acl_invalid_domain', $extra_acl_domain)
|
||||
);
|
||||
unset($extra_acls[$i]);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (in_array($extra_acl, $domains)) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
||||
'msg' => array('extra_acl_invalid_domain', $extra_acl_domain)
|
||||
);
|
||||
unset($extra_acls[$i]);
|
||||
continue;
|
||||
}
|
||||
$extra_acl = '@' . $extra_acl;
|
||||
}
|
||||
$extra_acl = '@' . $extra_acl;
|
||||
}
|
||||
}
|
||||
$extra_acls = array_filter($extra_acls);
|
||||
$extra_acls = array_values($extra_acls);
|
||||
$extra_acls = array_unique($extra_acls);
|
||||
$stmt = $pdo->prepare("DELETE FROM `sender_acl` WHERE `external` = 1 AND `logged_in_as` = :username");
|
||||
$stmt->execute(array(
|
||||
':username' => $username
|
||||
));
|
||||
foreach ($extra_acls as $sender_acl_external) {
|
||||
$stmt = $pdo->prepare("INSERT INTO `sender_acl` (`send_as`, `logged_in_as`, `external`)
|
||||
VALUES (:sender_acl, :username, 1)");
|
||||
$extra_acls = array_filter($extra_acls);
|
||||
$extra_acls = array_values($extra_acls);
|
||||
$extra_acls = array_unique($extra_acls);
|
||||
$stmt = $pdo->prepare("DELETE FROM `sender_acl` WHERE `external` = 1 AND `logged_in_as` = :username");
|
||||
$stmt->execute(array(
|
||||
':sender_acl' => $sender_acl_external,
|
||||
':username' => $username
|
||||
));
|
||||
foreach ($extra_acls as $sender_acl_external) {
|
||||
$stmt = $pdo->prepare("INSERT INTO `sender_acl` (`send_as`, `logged_in_as`, `external`)
|
||||
VALUES (:sender_acl, :username, 1)");
|
||||
$stmt->execute(array(
|
||||
':sender_acl' => $sender_acl_external,
|
||||
':username' => $username
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isset($_data['sender_acl'])) {
|
||||
|
@ -3129,7 +3132,10 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
|||
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
||||
'msg' => array('mailbox_modified', $username)
|
||||
);
|
||||
|
||||
update_sogo_static_view($username);
|
||||
}
|
||||
return true;
|
||||
break;
|
||||
case 'mailbox_templates':
|
||||
if ($_SESSION['mailcow_cc_role'] != "admin") {
|
||||
|
@ -3959,6 +3965,39 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
|||
}
|
||||
return $aliasdomaindata;
|
||||
break;
|
||||
case 'shared_aliases':
|
||||
$shared_aliases = array();
|
||||
$stmt = $pdo->query("SELECT `address` FROM `alias`
|
||||
WHERE `goto` REGEXP ','
|
||||
AND `address` NOT LIKE '@%'
|
||||
AND `goto` != `address`");
|
||||
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
while($row = array_shift($rows)) {
|
||||
$domain = explode("@", $row['address'])[1];
|
||||
if (hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $domain)) {
|
||||
$shared_aliases[] = $row['address'];
|
||||
}
|
||||
}
|
||||
|
||||
return $shared_aliases;
|
||||
break;
|
||||
case 'direct_aliases':
|
||||
$direct_aliases = array();
|
||||
$stmt = $pdo->query("SELECT `address` FROM `alias`
|
||||
WHERE `goto` NOT LIKE '%,%'
|
||||
AND `address` NOT LIKE '@%'
|
||||
AND `goto` != `address`");
|
||||
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
while($row = array_shift($rows)) {
|
||||
$domain = explode("@", $row['address'])[1];
|
||||
if (hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $domain)) {
|
||||
$direct_aliases[] = $row['address'];
|
||||
}
|
||||
}
|
||||
|
||||
return $direct_aliases;
|
||||
break;
|
||||
case 'domains':
|
||||
$domains = array();
|
||||
if ($_SESSION['mailcow_cc_role'] != "admin" && $_SESSION['mailcow_cc_role'] != "domainadmin") {
|
||||
|
@ -4891,13 +4930,19 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
|||
if (!empty($mailbox_details['domain']) && !empty($mailbox_details['local_part'])) {
|
||||
$maildir = $mailbox_details['domain'] . '/' . $mailbox_details['local_part'];
|
||||
$exec_fields = array('cmd' => 'maildir', 'task' => 'cleanup', 'maildir' => $maildir);
|
||||
$maildir_gc = json_decode(docker('post', 'dovecot-mailcow', 'exec', $exec_fields), true);
|
||||
if ($maildir_gc['type'] != 'success') {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'warning',
|
||||
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
||||
'msg' => 'Could not move maildir to garbage collector: ' . $maildir_gc['msg']
|
||||
);
|
||||
|
||||
if (getenv("CLUSTERMODE") == "replication") {
|
||||
// broadcast to each dovecot container
|
||||
docker('broadcast', 'dovecot-mailcow', 'exec', $exec_fields);
|
||||
} else {
|
||||
$maildir_gc = json_decode(docker('post', 'dovecot-mailcow', 'exec', $exec_fields), true);
|
||||
if ($maildir_gc['type'] != 'success') {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'warning',
|
||||
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
||||
'msg' => 'Could not move maildir to garbage collector: ' . $maildir_gc['msg']
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
|
@ -4950,9 +4995,10 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
|||
$stmt->execute(array(
|
||||
':username' => $username
|
||||
));
|
||||
$stmt = $pdo->prepare("DELETE FROM `sender_acl` WHERE `logged_in_as` = :username");
|
||||
$stmt = $pdo->prepare("DELETE FROM `sender_acl` WHERE `logged_in_as` = :logged_in_as OR `send_as` = :send_as");
|
||||
$stmt->execute(array(
|
||||
':username' => $username
|
||||
':logged_in_as' => $username,
|
||||
':send_as' => $username
|
||||
));
|
||||
// fk, better safe than sorry
|
||||
$stmt = $pdo->prepare("DELETE FROM `user_acl` WHERE `username` = :username");
|
||||
|
@ -5052,12 +5098,15 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
|||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
update_sogo_static_view($username);
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
||||
'msg' => array('mailbox_removed', htmlspecialchars($username))
|
||||
);
|
||||
}
|
||||
return true;
|
||||
break;
|
||||
case 'mailbox_templates':
|
||||
if ($_SESSION['mailcow_cc_role'] != "admin") {
|
||||
|
@ -5170,15 +5219,6 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
|||
$tags = $_data['tags'];
|
||||
if (!is_array($tags)) $tags = array();
|
||||
|
||||
|
||||
if ($_SESSION['mailcow_cc_role'] != "admin") {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
$wasModified = false;
|
||||
foreach ($domains as $domain) {
|
||||
|
@ -5190,7 +5230,15 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
|||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $domain)) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach($tags as $tag){
|
||||
// delete tag
|
||||
$wasModified = true;
|
||||
|
@ -5264,7 +5312,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
|||
}
|
||||
break;
|
||||
}
|
||||
if ($_action != 'get' && in_array($_type, array('domain', 'alias', 'alias_domain', 'mailbox', 'resource'))) {
|
||||
if ($_action != 'get' && in_array($_type, array('domain', 'alias', 'alias_domain', 'resource')) && getenv('SKIP_SOGO') != "y") {
|
||||
update_sogo_static_view();
|
||||
}
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -234,7 +234,7 @@ if (!isset($_SESSION['mailcow_locale']) && !isset($_COOKIE['mailcow_locale'])) {
|
|||
|
||||
// Try suggest match
|
||||
// e.g. suggest en-gb when only en-us is provided
|
||||
if (!isset($_COOKIE['mailcow_locale'])) {
|
||||
if (!isset($_SESSION['mailcow_locale'])) {
|
||||
foreach ($lang2pref as $lang => $q) {
|
||||
if (array_key_exists(substr($lang, 0, 2), $AVAILABLE_BASE_LANGUAGES)) {
|
||||
$_SESSION['mailcow_locale'] = $AVAILABLE_BASE_LANGUAGES[substr($lang, 0, 2)];
|
||||
|
|
|
@ -1,140 +1,140 @@
|
|||
<?php
|
||||
// Start session
|
||||
if (session_status() !== PHP_SESSION_ACTIVE) {
|
||||
ini_set("session.cookie_httponly", 1);
|
||||
ini_set('session.gc_maxlifetime', $SESSION_LIFETIME);
|
||||
}
|
||||
|
||||
if (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) &&
|
||||
strtolower($_SERVER['HTTP_X_FORWARDED_PROTO']) == "https") {
|
||||
if (session_status() !== PHP_SESSION_ACTIVE) {
|
||||
ini_set("session.cookie_secure", 1);
|
||||
}
|
||||
$IS_HTTPS = true;
|
||||
}
|
||||
elseif (isset($_SERVER['HTTPS'])) {
|
||||
if (session_status() !== PHP_SESSION_ACTIVE) {
|
||||
ini_set("session.cookie_secure", 1);
|
||||
}
|
||||
$IS_HTTPS = true;
|
||||
}
|
||||
else {
|
||||
$IS_HTTPS = false;
|
||||
}
|
||||
|
||||
if (session_status() !== PHP_SESSION_ACTIVE) {
|
||||
session_start();
|
||||
}
|
||||
|
||||
if (!isset($_SESSION['CSRF']['TOKEN'])) {
|
||||
$_SESSION['CSRF']['TOKEN'] = bin2hex(random_bytes(32));
|
||||
}
|
||||
|
||||
// Set session UA
|
||||
if (!isset($_SESSION['SESS_REMOTE_UA'])) {
|
||||
$_SESSION['SESS_REMOTE_UA'] = $_SERVER['HTTP_USER_AGENT'];
|
||||
}
|
||||
|
||||
// Keep session active
|
||||
if (isset($_SESSION['LAST_ACTIVITY']) && (time() - $_SESSION['LAST_ACTIVITY'] > $SESSION_LIFETIME)) {
|
||||
session_unset();
|
||||
session_destroy();
|
||||
}
|
||||
$_SESSION['LAST_ACTIVITY'] = time();
|
||||
|
||||
// API
|
||||
if (!empty($_SERVER['HTTP_X_API_KEY'])) {
|
||||
$stmt = $pdo->prepare("SELECT * FROM `api` WHERE `api_key` = :api_key AND `active` = '1';");
|
||||
$stmt->execute(array(
|
||||
':api_key' => preg_replace('/[^a-zA-Z0-9-]/', '', $_SERVER['HTTP_X_API_KEY'])
|
||||
));
|
||||
$api_return = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
if (!empty($api_return['api_key'])) {
|
||||
$skip_ip_check = ($api_return['skip_ip_check'] == 1);
|
||||
$remote = get_remote_ip(false);
|
||||
$allow_from = array_map('trim', preg_split( "/( |,|;|\n)/", $api_return['allow_from']));
|
||||
if ($skip_ip_check === true || ip_acl($remote, $allow_from)) {
|
||||
$_SESSION['mailcow_cc_username'] = 'API';
|
||||
$_SESSION['mailcow_cc_role'] = 'admin';
|
||||
$_SESSION['mailcow_cc_api'] = true;
|
||||
if ($api_return['access'] == 'rw') {
|
||||
$_SESSION['mailcow_cc_api_access'] = 'rw';
|
||||
}
|
||||
else {
|
||||
$_SESSION['mailcow_cc_api_access'] = 'ro';
|
||||
}
|
||||
}
|
||||
else {
|
||||
$redis->publish("F2B_CHANNEL", "mailcow UI: Invalid password for API_USER by " . $_SERVER['REMOTE_ADDR']);
|
||||
error_log("mailcow UI: Invalid password for " . $user . " by " . $_SERVER['REMOTE_ADDR']);
|
||||
http_response_code(401);
|
||||
echo json_encode(array(
|
||||
'type' => 'error',
|
||||
'msg' => 'api access denied for ip ' . $_SERVER['REMOTE_ADDR']
|
||||
));
|
||||
unset($_POST);
|
||||
exit();
|
||||
}
|
||||
}
|
||||
else {
|
||||
$redis->publish("F2B_CHANNEL", "mailcow UI: Invalid password for API_USER by " . $_SERVER['REMOTE_ADDR']);
|
||||
error_log("mailcow UI: Invalid password for " . $user . " by " . $_SERVER['REMOTE_ADDR']);
|
||||
http_response_code(401);
|
||||
echo json_encode(array(
|
||||
'type' => 'error',
|
||||
'msg' => 'authentication failed'
|
||||
));
|
||||
unset($_POST);
|
||||
exit();
|
||||
}
|
||||
}
|
||||
|
||||
// Handle logouts
|
||||
if (isset($_POST["logout"])) {
|
||||
if (isset($_SESSION["dual-login"])) {
|
||||
$_SESSION["mailcow_cc_username"] = $_SESSION["dual-login"]["username"];
|
||||
$_SESSION["mailcow_cc_role"] = $_SESSION["dual-login"]["role"];
|
||||
unset($_SESSION["dual-login"]);
|
||||
header("Location: /mailbox");
|
||||
exit();
|
||||
}
|
||||
else {
|
||||
session_regenerate_id(true);
|
||||
session_unset();
|
||||
session_destroy();
|
||||
session_write_close();
|
||||
header("Location: /");
|
||||
}
|
||||
}
|
||||
|
||||
// Check session
|
||||
function session_check() {
|
||||
if (isset($_SESSION['mailcow_cc_api']) && $_SESSION['mailcow_cc_api'] === true) {
|
||||
return true;
|
||||
}
|
||||
if (!isset($_SESSION['SESS_REMOTE_UA']) || ($_SESSION['SESS_REMOTE_UA'] != $_SERVER['HTTP_USER_AGENT'])) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'warning',
|
||||
'msg' => 'session_ua'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if (!empty($_POST)) {
|
||||
if ($_SESSION['CSRF']['TOKEN'] != $_POST['csrf_token']) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'warning',
|
||||
'msg' => 'session_token'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
unset($_POST['csrf_token']);
|
||||
$_SESSION['CSRF']['TOKEN'] = bin2hex(random_bytes(32));
|
||||
$_SESSION['CSRF']['TIME'] = time();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (isset($_SESSION['mailcow_cc_role']) && session_check() === false) {
|
||||
$_POST = array();
|
||||
$_FILES = array();
|
||||
}
|
||||
<?php
|
||||
// Start session
|
||||
if (session_status() !== PHP_SESSION_ACTIVE) {
|
||||
ini_set("session.cookie_httponly", 1);
|
||||
ini_set('session.gc_maxlifetime', $SESSION_LIFETIME);
|
||||
}
|
||||
|
||||
if (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) &&
|
||||
strtolower($_SERVER['HTTP_X_FORWARDED_PROTO']) == "https") {
|
||||
if (session_status() !== PHP_SESSION_ACTIVE) {
|
||||
ini_set("session.cookie_secure", 1);
|
||||
}
|
||||
$IS_HTTPS = true;
|
||||
}
|
||||
elseif (isset($_SERVER['HTTPS'])) {
|
||||
if (session_status() !== PHP_SESSION_ACTIVE) {
|
||||
ini_set("session.cookie_secure", 1);
|
||||
}
|
||||
$IS_HTTPS = true;
|
||||
}
|
||||
else {
|
||||
$IS_HTTPS = false;
|
||||
}
|
||||
|
||||
if (session_status() !== PHP_SESSION_ACTIVE) {
|
||||
session_start();
|
||||
}
|
||||
|
||||
if (!isset($_SESSION['CSRF']['TOKEN'])) {
|
||||
$_SESSION['CSRF']['TOKEN'] = bin2hex(random_bytes(32));
|
||||
}
|
||||
|
||||
// Set session UA
|
||||
if (!isset($_SESSION['SESS_REMOTE_UA'])) {
|
||||
$_SESSION['SESS_REMOTE_UA'] = $_SERVER['HTTP_USER_AGENT'];
|
||||
}
|
||||
|
||||
// Keep session active
|
||||
if (isset($_SESSION['LAST_ACTIVITY']) && (time() - $_SESSION['LAST_ACTIVITY'] > $SESSION_LIFETIME)) {
|
||||
session_unset();
|
||||
session_destroy();
|
||||
}
|
||||
$_SESSION['LAST_ACTIVITY'] = time();
|
||||
|
||||
// API
|
||||
if (!empty($_SERVER['HTTP_X_API_KEY'])) {
|
||||
$stmt = $pdo->prepare("SELECT * FROM `api` WHERE `api_key` = :api_key AND `active` = '1';");
|
||||
$stmt->execute(array(
|
||||
':api_key' => preg_replace('/[^a-zA-Z0-9-]/', '', $_SERVER['HTTP_X_API_KEY'])
|
||||
));
|
||||
$api_return = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
if (!empty($api_return['api_key'])) {
|
||||
$skip_ip_check = ($api_return['skip_ip_check'] == 1);
|
||||
$remote = get_remote_ip(false);
|
||||
$allow_from = array_map('trim', preg_split( "/( |,|;|\n)/", $api_return['allow_from']));
|
||||
if ($skip_ip_check === true || ip_acl($remote, $allow_from)) {
|
||||
$_SESSION['mailcow_cc_username'] = 'API';
|
||||
$_SESSION['mailcow_cc_role'] = 'admin';
|
||||
$_SESSION['mailcow_cc_api'] = true;
|
||||
if ($api_return['access'] == 'rw') {
|
||||
$_SESSION['mailcow_cc_api_access'] = 'rw';
|
||||
}
|
||||
else {
|
||||
$_SESSION['mailcow_cc_api_access'] = 'ro';
|
||||
}
|
||||
}
|
||||
else {
|
||||
$redis->publish("F2B_CHANNEL", "mailcow UI: Invalid password for API_USER by " . $_SERVER['REMOTE_ADDR']);
|
||||
error_log("mailcow UI: Invalid password for " . $user . " by " . $_SERVER['REMOTE_ADDR']);
|
||||
http_response_code(401);
|
||||
echo json_encode(array(
|
||||
'type' => 'error',
|
||||
'msg' => 'api access denied for ip ' . $_SERVER['REMOTE_ADDR']
|
||||
));
|
||||
unset($_POST);
|
||||
exit();
|
||||
}
|
||||
}
|
||||
else {
|
||||
$redis->publish("F2B_CHANNEL", "mailcow UI: Invalid password for API_USER by " . $_SERVER['REMOTE_ADDR']);
|
||||
error_log("mailcow UI: Invalid password for " . $user . " by " . $_SERVER['REMOTE_ADDR']);
|
||||
http_response_code(401);
|
||||
echo json_encode(array(
|
||||
'type' => 'error',
|
||||
'msg' => 'authentication failed'
|
||||
));
|
||||
unset($_POST);
|
||||
exit();
|
||||
}
|
||||
}
|
||||
|
||||
// Handle logouts
|
||||
if (isset($_POST["logout"])) {
|
||||
if (isset($_SESSION["dual-login"])) {
|
||||
$_SESSION["mailcow_cc_username"] = $_SESSION["dual-login"]["username"];
|
||||
$_SESSION["mailcow_cc_role"] = $_SESSION["dual-login"]["role"];
|
||||
unset($_SESSION["dual-login"]);
|
||||
header("Location: /mailbox");
|
||||
exit();
|
||||
}
|
||||
else {
|
||||
session_regenerate_id(true);
|
||||
session_unset();
|
||||
session_destroy();
|
||||
session_write_close();
|
||||
header("Location: /");
|
||||
}
|
||||
}
|
||||
|
||||
// Check session
|
||||
function session_check() {
|
||||
if (isset($_SESSION['mailcow_cc_api']) && $_SESSION['mailcow_cc_api'] === true) {
|
||||
return true;
|
||||
}
|
||||
if (!isset($_SESSION['SESS_REMOTE_UA']) || ($_SESSION['SESS_REMOTE_UA'] != $_SERVER['HTTP_USER_AGENT'])) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'warning',
|
||||
'msg' => 'session_ua'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if (!empty($_POST)) {
|
||||
if ($_SESSION['CSRF']['TOKEN'] != $_POST['csrf_token']) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'warning',
|
||||
'msg' => 'session_token'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
unset($_POST['csrf_token']);
|
||||
$_SESSION['CSRF']['TOKEN'] = bin2hex(random_bytes(32));
|
||||
$_SESSION['CSRF']['TIME'] = time();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (isset($_SESSION['mailcow_cc_role']) && session_check() === false) {
|
||||
$_POST = array();
|
||||
$_FILES = array();
|
||||
}
|
||||
|
|
|
@ -1,4 +1,15 @@
|
|||
<?php
|
||||
// SSO Domain Admin
|
||||
if (!empty($_GET['sso_token'])) {
|
||||
$username = domain_admin_sso('check', $_GET['sso_token']);
|
||||
|
||||
if ($username !== false) {
|
||||
$_SESSION['mailcow_cc_username'] = $username;
|
||||
$_SESSION['mailcow_cc_role'] = 'domainadmin';
|
||||
header('Location: /mailbox');
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($_POST["verify_tfa_login"])) {
|
||||
if (verify_tfa_login($_SESSION['pending_mailcow_cc_username'], $_POST)) {
|
||||
$_SESSION['mailcow_cc_username'] = $_SESSION['pending_mailcow_cc_username'];
|
||||
|
@ -6,7 +17,7 @@ if (isset($_POST["verify_tfa_login"])) {
|
|||
unset($_SESSION['pending_mailcow_cc_username']);
|
||||
unset($_SESSION['pending_mailcow_cc_role']);
|
||||
unset($_SESSION['pending_tfa_methods']);
|
||||
|
||||
|
||||
header("Location: /user");
|
||||
} else {
|
||||
unset($_SESSION['pending_mailcow_cc_username']);
|
||||
|
@ -34,7 +45,7 @@ if (isset($_POST["quick_delete"])) {
|
|||
if (isset($_POST["login_user"]) && isset($_POST["pass_user"])) {
|
||||
$login_user = strtolower(trim($_POST["login_user"]));
|
||||
$as = check_login($login_user, $_POST["pass_user"]);
|
||||
|
||||
|
||||
if ($as == "admin") {
|
||||
$_SESSION['mailcow_cc_username'] = $login_user;
|
||||
$_SESSION['mailcow_cc_role'] = "admin";
|
||||
|
@ -52,7 +63,7 @@ if (isset($_POST["login_user"]) && isset($_POST["pass_user"])) {
|
|||
unset($_SESSION['index_query_string']);
|
||||
if (in_array('mobileconfig', $http_parameters)) {
|
||||
if (in_array('only_email', $http_parameters)) {
|
||||
header("Location: /mobileconfig.php?email_only");
|
||||
header("Location: /mobileconfig.php?only_email");
|
||||
die();
|
||||
}
|
||||
header("Location: /mobileconfig.php");
|
||||
|
|
|
@ -124,7 +124,7 @@ $MAILCOW_APPS = array(
|
|||
);
|
||||
|
||||
// Rows until pagination begins
|
||||
$PAGINATION_SIZE = 20;
|
||||
$PAGINATION_SIZE = 25;
|
||||
|
||||
// Default number of rows/lines to display (log table)
|
||||
$LOG_LINES = 1000;
|
||||
|
|
|
@ -4,20 +4,20 @@
|
|||
*
|
||||
* To rebuild or modify this file with the latest versions of the included
|
||||
* software please visit:
|
||||
* https://datatables.net/download/#bs5/dt-1.12.0/r-2.3.0/sl-1.4.0
|
||||
* https://datatables.net/download/#bs5/dt-1.13.1/r-2.4.0/sl-1.5.0
|
||||
*
|
||||
* Included libraries:
|
||||
* DataTables 1.12.0, Responsive 2.3.0, Select 1.4.0
|
||||
* DataTables 1.13.1, Responsive 2.4.0, Select 1.5.0
|
||||
*/
|
||||
|
||||
/*! DataTables 1.12.0
|
||||
/*! DataTables 1.13.1
|
||||
* ©2008-2022 SpryMedia Ltd - datatables.net/license
|
||||
*/
|
||||
|
||||
/**
|
||||
* @summary DataTables
|
||||
* @description Paginate, search and order HTML tables
|
||||
* @version 1.12.0
|
||||
* @version 1.13.1
|
||||
* @author SpryMedia Ltd
|
||||
* @contact www.datatables.net
|
||||
* @copyright SpryMedia Ltd.
|
||||
|
@ -1162,6 +1162,10 @@
|
|||
$( rowOne[0] ).children('th, td').each( function (i, cell) {
|
||||
var col = oSettings.aoColumns[i];
|
||||
|
||||
if (! col) {
|
||||
_fnLog( oSettings, 0, 'Incorrect column count', 18 );
|
||||
}
|
||||
|
||||
if ( col.mData === i ) {
|
||||
var sort = a( cell, 'sort' ) || a( cell, 'order' );
|
||||
var filter = a( cell, 'filter' ) || a( cell, 'search' );
|
||||
|
@ -3166,6 +3170,11 @@
|
|||
create = nTrIn ? false : true;
|
||||
|
||||
nTd = create ? document.createElement( oCol.sCellType ) : anTds[i];
|
||||
|
||||
if (! nTd) {
|
||||
_fnLog( oSettings, 0, 'Incorrect column count', 18 );
|
||||
}
|
||||
|
||||
nTd._DT_CellIndex = {
|
||||
row: iRow,
|
||||
column: i
|
||||
|
@ -3316,10 +3325,16 @@
|
|||
|
||||
for ( i=0, ien=cells.length ; i<ien ; i++ ) {
|
||||
column = columns[i];
|
||||
column.nTf = cells[i].cell;
|
||||
|
||||
if ( column.sClass ) {
|
||||
$(column.nTf).addClass( column.sClass );
|
||||
if (column) {
|
||||
column.nTf = cells[i].cell;
|
||||
|
||||
if ( column.sClass ) {
|
||||
$(column.nTf).addClass( column.sClass );
|
||||
}
|
||||
}
|
||||
else {
|
||||
_fnLog( oSettings, 0, 'Incorrect column count', 18 );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5079,6 +5094,10 @@
|
|||
_fnDraw( settings );
|
||||
}
|
||||
}
|
||||
else {
|
||||
// No change event - paging was called, but no change
|
||||
_fnCallbackFire( settings, null, 'page-nc', [settings] );
|
||||
}
|
||||
|
||||
return changed;
|
||||
}
|
||||
|
@ -5348,6 +5367,7 @@
|
|||
footerCopy = footer.clone().prependTo( table );
|
||||
footerTrgEls = footer.find('tr'); // the original tfoot is in its own table and must be sized
|
||||
footerSrcEls = footerCopy.find('tr');
|
||||
footerCopy.find('[id]').removeAttr('id');
|
||||
}
|
||||
|
||||
// Clone the current header and footer elements and then place it into the inner table
|
||||
|
@ -5355,6 +5375,7 @@
|
|||
headerTrgEls = header.find('tr'); // original header is in its own table
|
||||
headerSrcEls = headerCopy.find('tr');
|
||||
headerCopy.find('th, td').removeAttr('tabindex');
|
||||
headerCopy.find('[id]').removeAttr('id');
|
||||
|
||||
|
||||
/*
|
||||
|
@ -8332,8 +8353,12 @@
|
|||
|
||||
$(document).on('plugin-init.dt', function (e, context) {
|
||||
var api = new _Api( context );
|
||||
|
||||
const namespace = 'on-plugin-init';
|
||||
const stateSaveParamsEvent = `stateSaveParams.${namespace}`;
|
||||
const destroyEvent = `destroy.${namespace}`;
|
||||
|
||||
api.on( 'stateSaveParams', function ( e, settings, d ) {
|
||||
api.on( stateSaveParamsEvent, function ( e, settings, d ) {
|
||||
// This could be more compact with the API, but it is a lot faster as a simple
|
||||
// internal loop
|
||||
var idFn = settings.rowIdFn;
|
||||
|
@ -8347,7 +8372,11 @@
|
|||
}
|
||||
|
||||
d.childRows = ids;
|
||||
})
|
||||
});
|
||||
|
||||
api.on( destroyEvent, function () {
|
||||
api.off(`${stateSaveParamsEvent} ${destroyEvent}`);
|
||||
});
|
||||
|
||||
var loaded = api.state.loaded();
|
||||
|
||||
|
@ -9668,7 +9697,7 @@
|
|||
* @type string
|
||||
* @default Version number
|
||||
*/
|
||||
DataTable.version = "1.12.0";
|
||||
DataTable.version = "1.13.1";
|
||||
|
||||
/**
|
||||
* Private data store, containing all of the settings objects that are
|
||||
|
@ -14092,7 +14121,7 @@
|
|||
*
|
||||
* @type string
|
||||
*/
|
||||
build:"bs5/dt-1.12.0/r-2.3.0/sl-1.4.0",
|
||||
build:"bs5/dt-1.13.1/r-2.4.0/sl-1.5.0",
|
||||
|
||||
|
||||
/**
|
||||
|
@ -14730,7 +14759,7 @@
|
|||
var classes = settings.oClasses;
|
||||
var lang = settings.oLanguage.oPaginate;
|
||||
var aria = settings.oLanguage.oAria.paginate || {};
|
||||
var btnDisplay, btnClass, counter=0;
|
||||
var btnDisplay, btnClass;
|
||||
|
||||
var attach = function( container, buttons ) {
|
||||
var i, ien, node, button, tabIndex;
|
||||
|
@ -14805,7 +14834,7 @@
|
|||
'class': classes.sPageButton+' '+btnClass,
|
||||
'aria-controls': settings.sTableId,
|
||||
'aria-label': aria[ button ],
|
||||
'data-dt-idx': counter,
|
||||
'data-dt-idx': button,
|
||||
'tabindex': tabIndex,
|
||||
'id': idx === 0 && typeof button === 'string' ?
|
||||
settings.sTableId +'_'+ button :
|
||||
|
@ -14817,8 +14846,6 @@
|
|||
_fnBindAction(
|
||||
node, {action: button}, clickHandler
|
||||
);
|
||||
|
||||
counter++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -15163,7 +15190,7 @@
|
|||
}
|
||||
}
|
||||
else if (window.luxon) {
|
||||
dt = format
|
||||
dt = format && typeof d === 'string'
|
||||
? window.luxon.DateTime.fromFormat( d, format )
|
||||
: window.luxon.DateTime.fromISO( d );
|
||||
|
||||
|
@ -15303,14 +15330,26 @@
|
|||
}
|
||||
|
||||
// Based on locale, determine standard number formatting
|
||||
var __thousands = '';
|
||||
var __decimal = '';
|
||||
// Fallback for legacy browsers is US English
|
||||
var __thousands = ',';
|
||||
var __decimal = '.';
|
||||
|
||||
if (Intl) {
|
||||
var num = new Intl.NumberFormat().formatToParts(1000.1);
|
||||
|
||||
__thousands = num[1].value;
|
||||
__decimal = num[3].value;
|
||||
try {
|
||||
var num = new Intl.NumberFormat().formatToParts(100000.1);
|
||||
|
||||
for (var i=0 ; i<num.length ; i++) {
|
||||
if (num[i].type === 'group') {
|
||||
__thousands = num[i].value;
|
||||
}
|
||||
else if (num[i].type === 'decimal') {
|
||||
__decimal = num[i].value;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
// noop
|
||||
}
|
||||
}
|
||||
|
||||
// Formatted date time detection - use by declaring the formats you are going to use
|
||||
|
@ -15379,6 +15418,10 @@
|
|||
return d;
|
||||
}
|
||||
|
||||
if (d === '' || d === null) {
|
||||
return d;
|
||||
}
|
||||
|
||||
var negative = d < 0 ? '-' : '';
|
||||
var flo = parseFloat( d );
|
||||
|
||||
|
@ -15569,7 +15612,7 @@
|
|||
$.each( DataTable, function ( prop, val ) {
|
||||
$.fn.DataTable[ prop ] = val;
|
||||
} );
|
||||
|
||||
|
||||
return DataTable;
|
||||
}));
|
||||
|
||||
|
@ -15578,14 +15621,6 @@
|
|||
* 2020 SpryMedia Ltd - datatables.net/license
|
||||
*/
|
||||
|
||||
/**
|
||||
* DataTables integration for Bootstrap 4. This requires Bootstrap 5 and
|
||||
* DataTables 1.10 or newer.
|
||||
*
|
||||
* This file sets the defaults and adds options to DataTables to style its
|
||||
* controls using Bootstrap. See http://datatables.net/manual/styling/bootstrap
|
||||
* for further information.
|
||||
*/
|
||||
(function( factory ){
|
||||
if ( typeof define === 'function' && define.amd ) {
|
||||
// AMD
|
||||
|
@ -15597,16 +15632,22 @@
|
|||
// CommonJS
|
||||
module.exports = function (root, $) {
|
||||
if ( ! root ) {
|
||||
// CommonJS environments without a window global must pass a
|
||||
// root. This will give an error otherwise
|
||||
root = window;
|
||||
}
|
||||
|
||||
if ( ! $ || ! $.fn.dataTable ) {
|
||||
// Require DataTables, which attaches to jQuery, including
|
||||
// jQuery if needed and have a $ property so we can access the
|
||||
// jQuery object that is used
|
||||
$ = require('datatables.net')(root, $).$;
|
||||
if ( ! $ ) {
|
||||
$ = typeof window !== 'undefined' ? // jQuery's factory checks for a global window
|
||||
require('jquery') :
|
||||
require('jquery')( root );
|
||||
}
|
||||
|
||||
if ( ! $.fn.dataTable ) {
|
||||
require('datatables.net')(root, $);
|
||||
}
|
||||
|
||||
|
||||
return factory( $, root, root.document );
|
||||
};
|
||||
}
|
||||
|
@ -15619,11 +15660,21 @@
|
|||
var DataTable = $.fn.dataTable;
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* DataTables integration for Bootstrap 5. This requires Bootstrap 5 and
|
||||
* DataTables 1.10 or newer.
|
||||
*
|
||||
* This file sets the defaults and adds options to DataTables to style its
|
||||
* controls using Bootstrap. See http://datatables.net/manual/styling/bootstrap
|
||||
* for further information.
|
||||
*/
|
||||
|
||||
/* Set the defaults for DataTables initialisation */
|
||||
$.extend( true, DataTable.defaults, {
|
||||
dom:
|
||||
"<'row'<'col-sm-12 col-md-6'l><'col-sm-12 col-md-6'f>>" +
|
||||
"<'row'<'col-sm-12'tr>>" +
|
||||
"<'row dt-row'<'col-sm-12'tr>>" +
|
||||
"<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
|
||||
renderer: 'bootstrap'
|
||||
} );
|
||||
|
@ -15645,7 +15696,7 @@ DataTable.ext.renderer.pageButton.bootstrap = function ( settings, host, idx, bu
|
|||
var classes = settings.oClasses;
|
||||
var lang = settings.oLanguage.oPaginate;
|
||||
var aria = settings.oLanguage.oAria.paginate || {};
|
||||
var btnDisplay, btnClass, counter=0;
|
||||
var btnDisplay, btnClass;
|
||||
|
||||
var attach = function( container, buttons ) {
|
||||
var i, ien, node, button;
|
||||
|
@ -15714,7 +15765,7 @@ DataTable.ext.renderer.pageButton.bootstrap = function ( settings, host, idx, bu
|
|||
'href': '#',
|
||||
'aria-controls': settings.sTableId,
|
||||
'aria-label': aria[ button ],
|
||||
'data-dt-idx': counter,
|
||||
'data-dt-idx': button,
|
||||
'tabindex': settings.iTabIndex,
|
||||
'class': 'page-link'
|
||||
} )
|
||||
|
@ -15725,13 +15776,12 @@ DataTable.ext.renderer.pageButton.bootstrap = function ( settings, host, idx, bu
|
|||
settings.oApi._fnBindAction(
|
||||
node, {action: button}, clickHandler
|
||||
);
|
||||
|
||||
counter++;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var hostEl = $(host);
|
||||
// IE9 throws an 'unknown error' if document.activeElement is used
|
||||
// inside an iframe or frame.
|
||||
var activeEl;
|
||||
|
@ -15741,17 +15791,26 @@ DataTable.ext.renderer.pageButton.bootstrap = function ( settings, host, idx, bu
|
|||
// elements, focus is lost on the select button which is bad for
|
||||
// accessibility. So we want to restore focus once the draw has
|
||||
// completed
|
||||
activeEl = $(host).find(document.activeElement).data('dt-idx');
|
||||
activeEl = hostEl.find(document.activeElement).data('dt-idx');
|
||||
}
|
||||
catch (e) {}
|
||||
|
||||
var paginationEl = hostEl.children('ul.pagination');
|
||||
|
||||
if (paginationEl.length) {
|
||||
paginationEl.empty();
|
||||
}
|
||||
else {
|
||||
paginationEl = hostEl.html('<ul/>').children('ul').addClass('pagination');
|
||||
}
|
||||
|
||||
attach(
|
||||
$(host).empty().html('<ul class="pagination"/>').children('ul'),
|
||||
paginationEl,
|
||||
buttons
|
||||
);
|
||||
|
||||
if ( activeEl !== undefined ) {
|
||||
$(host).find( '[data-dt-idx='+activeEl+']' ).trigger('focus');
|
||||
hostEl.find('[data-dt-idx='+activeEl+']').trigger('focus');
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -15760,14 +15819,54 @@ return DataTable;
|
|||
}));
|
||||
|
||||
|
||||
/*! Responsive 2.3.0
|
||||
/*! Responsive 2.4.0
|
||||
* 2014-2022 SpryMedia Ltd - datatables.net/license
|
||||
*/
|
||||
|
||||
(function( factory ){
|
||||
if ( typeof define === 'function' && define.amd ) {
|
||||
// AMD
|
||||
define( ['jquery', 'datatables.net'], function ( $ ) {
|
||||
return factory( $, window, document );
|
||||
} );
|
||||
}
|
||||
else if ( typeof exports === 'object' ) {
|
||||
// CommonJS
|
||||
module.exports = function (root, $) {
|
||||
if ( ! root ) {
|
||||
// CommonJS environments without a window global must pass a
|
||||
// root. This will give an error otherwise
|
||||
root = window;
|
||||
}
|
||||
|
||||
if ( ! $ ) {
|
||||
$ = typeof window !== 'undefined' ? // jQuery's factory checks for a global window
|
||||
require('jquery') :
|
||||
require('jquery')( root );
|
||||
}
|
||||
|
||||
if ( ! $.fn.dataTable ) {
|
||||
require('datatables.net')(root, $);
|
||||
}
|
||||
|
||||
|
||||
return factory( $, root, root.document );
|
||||
};
|
||||
}
|
||||
else {
|
||||
// Browser
|
||||
factory( jQuery, window, document );
|
||||
}
|
||||
}(function( $, window, document, undefined ) {
|
||||
'use strict';
|
||||
var DataTable = $.fn.dataTable;
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* @summary Responsive
|
||||
* @description Responsive tables plug-in for DataTables
|
||||
* @version 2.3.0
|
||||
* @version 2.4.0
|
||||
* @author SpryMedia Ltd (www.sprymedia.co.uk)
|
||||
* @contact www.sprymedia.co.uk/contact
|
||||
* @copyright SpryMedia Ltd.
|
||||
|
@ -15781,35 +15880,6 @@ return DataTable;
|
|||
*
|
||||
* For details please refer to: http://www.datatables.net
|
||||
*/
|
||||
(function( factory ){
|
||||
if ( typeof define === 'function' && define.amd ) {
|
||||
// AMD
|
||||
define( ['jquery', 'datatables.net'], function ( $ ) {
|
||||
return factory( $, window, document );
|
||||
} );
|
||||
}
|
||||
else if ( typeof exports === 'object' ) {
|
||||
// CommonJS
|
||||
module.exports = function (root, $) {
|
||||
if ( ! root ) {
|
||||
root = window;
|
||||
}
|
||||
|
||||
if ( ! $ || ! $.fn.dataTable ) {
|
||||
$ = require('datatables.net')(root, $).$;
|
||||
}
|
||||
|
||||
return factory( $, root, root.document );
|
||||
};
|
||||
}
|
||||
else {
|
||||
// Browser
|
||||
factory( jQuery, window, document );
|
||||
}
|
||||
}(function( $, window, document, undefined ) {
|
||||
'use strict';
|
||||
var DataTable = $.fn.dataTable;
|
||||
|
||||
|
||||
/**
|
||||
* Responsive is a plug-in for the DataTables library that makes use of
|
||||
|
@ -15863,9 +15933,10 @@ var Responsive = function ( settings, opts ) {
|
|||
}
|
||||
|
||||
this.s = {
|
||||
dt: new DataTable.Api( settings ),
|
||||
childNodeStore: {},
|
||||
columns: [],
|
||||
current: []
|
||||
current: [],
|
||||
dt: new DataTable.Api( settings )
|
||||
};
|
||||
|
||||
// Check if responsive has already been initialised on this table
|
||||
|
@ -16070,6 +16141,63 @@ $.extend( Responsive.prototype, {
|
|||
* Private methods
|
||||
*/
|
||||
|
||||
/**
|
||||
* Get and store nodes from a cell - use for node moving renderers
|
||||
*
|
||||
* @param {*} dt DT instance
|
||||
* @param {*} row Row index
|
||||
* @param {*} col Column index
|
||||
*/
|
||||
_childNodes: function( dt, row, col ) {
|
||||
var name = row+'-'+col;
|
||||
|
||||
if ( this.s.childNodeStore[ name ] ) {
|
||||
return this.s.childNodeStore[ name ];
|
||||
}
|
||||
|
||||
// https://jsperf.com/childnodes-array-slice-vs-loop
|
||||
var nodes = [];
|
||||
var children = dt.cell( row, col ).node().childNodes;
|
||||
for ( var i=0, ien=children.length ; i<ien ; i++ ) {
|
||||
nodes.push( children[i] );
|
||||
}
|
||||
|
||||
this.s.childNodeStore[ name ] = nodes;
|
||||
|
||||
return nodes;
|
||||
},
|
||||
|
||||
/**
|
||||
* Restore nodes from the cache to a table cell
|
||||
*
|
||||
* @param {*} dt DT instance
|
||||
* @param {*} row Row index
|
||||
* @param {*} col Column index
|
||||
*/
|
||||
_childNodesRestore: function( dt, row, col ) {
|
||||
var name = row+'-'+col;
|
||||
|
||||
if ( ! this.s.childNodeStore[ name ] ) {
|
||||
return;
|
||||
}
|
||||
|
||||
var node = dt.cell( row, col ).node();
|
||||
var store = this.s.childNodeStore[ name ];
|
||||
var parent = store[0].parentNode;
|
||||
var parentChildren = parent.childNodes;
|
||||
var a = [];
|
||||
|
||||
for ( var i=0, ien=parentChildren.length ; i<ien ; i++ ) {
|
||||
a.push( parentChildren[i] );
|
||||
}
|
||||
|
||||
for ( var j=0, jen=a.length ; j<jen ; j++ ) {
|
||||
node.appendChild( a[j] );
|
||||
}
|
||||
|
||||
this.s.childNodeStore[ name ] = undefined;
|
||||
},
|
||||
|
||||
/**
|
||||
* Calculate the visibility for the columns in a table for a given
|
||||
* breakpoint. The result is pre-determined based on the class logic if
|
||||
|
@ -16399,8 +16527,8 @@ $.extend( Responsive.prototype, {
|
|||
: details.renderer;
|
||||
|
||||
var res = details.display( row, update, function () {
|
||||
return renderer(
|
||||
dt, row[0], that._detailsObj(row[0])
|
||||
return renderer.call(
|
||||
that, dt, row[0], that._detailsObj(row[0])
|
||||
);
|
||||
} );
|
||||
|
||||
|
@ -16622,9 +16750,11 @@ $.extend( Responsive.prototype, {
|
|||
}
|
||||
} );
|
||||
|
||||
if ( changed ) {
|
||||
this._redrawChildren();
|
||||
// Always need to update the display, regardless of if it has changed or not, so nodes
|
||||
// can be re-inserted for listHiddenNodes
|
||||
this._redrawChildren();
|
||||
|
||||
if ( changed ) {
|
||||
// Inform listeners of the change
|
||||
$(dt.table().node()).trigger( 'responsive-resize.dt', [dt, this.s.current] );
|
||||
|
||||
|
@ -16650,6 +16780,7 @@ $.extend( Responsive.prototype, {
|
|||
{
|
||||
var dt = this.s.dt;
|
||||
var columns = this.s.columns;
|
||||
var that = this;
|
||||
|
||||
// Are we allowed to do auto sizing?
|
||||
if ( ! this.c.auto ) {
|
||||
|
@ -16663,11 +16794,11 @@ $.extend( Responsive.prototype, {
|
|||
}
|
||||
|
||||
// Need to restore all children. They will be reinstated by a re-render
|
||||
if ( ! $.isEmptyObject( _childNodeStore ) ) {
|
||||
$.each( _childNodeStore, function ( key ) {
|
||||
if ( ! $.isEmptyObject( this.s.childNodeStore ) ) {
|
||||
$.each( this.s.childNodeStore, function ( key ) {
|
||||
var idx = key.split('-');
|
||||
|
||||
_childNodesRestore( dt, idx[0]*1, idx[1]*1 );
|
||||
that._childNodesRestore( dt, idx[0]*1, idx[1]*1 );
|
||||
} );
|
||||
}
|
||||
|
||||
|
@ -16787,6 +16918,7 @@ $.extend( Responsive.prototype, {
|
|||
*/
|
||||
_setColumnVis: function ( col, showHide )
|
||||
{
|
||||
var that = this;
|
||||
var dt = this.s.dt;
|
||||
var display = showHide ? '' : 'none'; // empty string will remove the attr
|
||||
|
||||
|
@ -16803,9 +16935,9 @@ $.extend( Responsive.prototype, {
|
|||
.toggleClass('dtr-hidden', !showHide);
|
||||
|
||||
// If the are child nodes stored, we might need to reinsert them
|
||||
if ( ! $.isEmptyObject( _childNodeStore ) ) {
|
||||
if ( ! $.isEmptyObject( this.s.childNodeStore ) ) {
|
||||
dt.cells( null, col ).indexes().each( function (idx) {
|
||||
_childNodesRestore( dt, idx.row, idx.column );
|
||||
that._childNodesRestore( dt, idx.row, idx.column );
|
||||
} );
|
||||
}
|
||||
},
|
||||
|
@ -16972,52 +17104,6 @@ Responsive.display = {
|
|||
};
|
||||
|
||||
|
||||
var _childNodeStore = {};
|
||||
|
||||
function _childNodes( dt, row, col ) {
|
||||
var name = row+'-'+col;
|
||||
|
||||
if ( _childNodeStore[ name ] ) {
|
||||
return _childNodeStore[ name ];
|
||||
}
|
||||
|
||||
// https://jsperf.com/childnodes-array-slice-vs-loop
|
||||
var nodes = [];
|
||||
var children = dt.cell( row, col ).node().childNodes;
|
||||
for ( var i=0, ien=children.length ; i<ien ; i++ ) {
|
||||
nodes.push( children[i] );
|
||||
}
|
||||
|
||||
_childNodeStore[ name ] = nodes;
|
||||
|
||||
return nodes;
|
||||
}
|
||||
|
||||
function _childNodesRestore( dt, row, col ) {
|
||||
var name = row+'-'+col;
|
||||
|
||||
if ( ! _childNodeStore[ name ] ) {
|
||||
return;
|
||||
}
|
||||
|
||||
var node = dt.cell( row, col ).node();
|
||||
var store = _childNodeStore[ name ];
|
||||
var parent = store[0].parentNode;
|
||||
var parentChildren = parent.childNodes;
|
||||
var a = [];
|
||||
|
||||
for ( var i=0, ien=parentChildren.length ; i<ien ; i++ ) {
|
||||
a.push( parentChildren[i] );
|
||||
}
|
||||
|
||||
for ( var j=0, jen=a.length ; j<jen ; j++ ) {
|
||||
node.appendChild( a[j] );
|
||||
}
|
||||
|
||||
_childNodeStore[ name ] = undefined;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Display methods - functions which define how the hidden data should be shown
|
||||
* in the table.
|
||||
|
@ -17029,6 +17115,7 @@ function _childNodesRestore( dt, row, col ) {
|
|||
Responsive.renderer = {
|
||||
listHiddenNodes: function () {
|
||||
return function ( api, rowIdx, columns ) {
|
||||
var that = this;
|
||||
var ul = $('<ul data-dtr-index="'+rowIdx+'" class="dtr-details"/>');
|
||||
var found = false;
|
||||
|
||||
|
@ -17045,7 +17132,7 @@ Responsive.renderer = {
|
|||
'</span> '+
|
||||
'</li>'
|
||||
)
|
||||
.append( $('<span class="dtr-data"/>').append( _childNodes( api, col.rowIndex, col.columnIndex ) ) )// api.cell( col.rowIndex, col.columnIndex ).node().childNodes ) )
|
||||
.append( $('<span class="dtr-data"/>').append( that._childNodes( api, col.rowIndex, col.columnIndex ) ) )// api.cell( col.rowIndex, col.columnIndex ).node().childNodes ) )
|
||||
.appendTo( ul );
|
||||
|
||||
found = true;
|
||||
|
@ -17229,7 +17316,7 @@ Api.registerPlural( 'columns().responsiveHidden()', 'column().responsiveHidden()
|
|||
* @name Responsive.version
|
||||
* @static
|
||||
*/
|
||||
Responsive.version = '2.3.0';
|
||||
Responsive.version = '2.4.0';
|
||||
|
||||
|
||||
$.fn.dataTable.Responsive = Responsive;
|
||||
|
@ -17256,12 +17343,12 @@ $(document).on( 'preInit.dt.dtr', function (e, settings, json) {
|
|||
} );
|
||||
|
||||
|
||||
return Responsive;
|
||||
return DataTable;
|
||||
}));
|
||||
|
||||
|
||||
/*! Bootstrap 5 integration for DataTables' Responsive
|
||||
* ©2021 SpryMedia Ltd - datatables.net/license
|
||||
* © SpryMedia Ltd - datatables.net/license
|
||||
*/
|
||||
|
||||
(function( factory ){
|
||||
|
@ -17275,17 +17362,26 @@ return Responsive;
|
|||
// CommonJS
|
||||
module.exports = function (root, $) {
|
||||
if ( ! root ) {
|
||||
// CommonJS environments without a window global must pass a
|
||||
// root. This will give an error otherwise
|
||||
root = window;
|
||||
}
|
||||
|
||||
if ( ! $ || ! $.fn.dataTable ) {
|
||||
$ = require('datatables.net-bs5')(root, $).$;
|
||||
if ( ! $ ) {
|
||||
$ = typeof window !== 'undefined' ? // jQuery's factory checks for a global window
|
||||
require('jquery') :
|
||||
require('jquery')( root );
|
||||
}
|
||||
|
||||
if ( ! $.fn.dataTable.Responsive ) {
|
||||
if ( ! $.fn.dataTable ) {
|
||||
require('datatables.net-bs5')(root, $);
|
||||
}
|
||||
|
||||
if ( ! $.fn.dataTable ) {
|
||||
require('datatables.net-responsive')(root, $);
|
||||
}
|
||||
|
||||
|
||||
return factory( $, root, root.document );
|
||||
};
|
||||
}
|
||||
|
@ -17298,6 +17394,7 @@ return Responsive;
|
|||
var DataTable = $.fn.dataTable;
|
||||
|
||||
|
||||
|
||||
var _display = DataTable.Responsive.display;
|
||||
var _original = _display.modal;
|
||||
var _modal = $(
|
||||
|
@ -17359,33 +17456,14 @@ _display.modal = function ( options ) {
|
|||
};
|
||||
|
||||
|
||||
return DataTable.Responsive;
|
||||
return DataTable;
|
||||
}));
|
||||
|
||||
|
||||
/*! Select for DataTables 1.4.0
|
||||
/*! Select for DataTables 1.5.0
|
||||
* 2015-2021 SpryMedia Ltd - datatables.net/license/mit
|
||||
*/
|
||||
|
||||
/**
|
||||
* @summary Select for DataTables
|
||||
* @description A collection of API methods, events and buttons for DataTables
|
||||
* that provides selection options of the items in a DataTable
|
||||
* @version 1.4.0
|
||||
* @file dataTables.select.js
|
||||
* @author SpryMedia Ltd (www.sprymedia.co.uk)
|
||||
* @contact datatables.net/forums
|
||||
* @copyright Copyright 2015-2021 SpryMedia Ltd.
|
||||
*
|
||||
* This source file is free software, available under the following license:
|
||||
* MIT license - http://datatables.net/license/mit
|
||||
*
|
||||
* This source file is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the license files for details.
|
||||
*
|
||||
* For details please refer to: http://www.datatables.net/extensions/select
|
||||
*/
|
||||
(function( factory ){
|
||||
if ( typeof define === 'function' && define.amd ) {
|
||||
// AMD
|
||||
|
@ -17397,13 +17475,22 @@ return DataTable.Responsive;
|
|||
// CommonJS
|
||||
module.exports = function (root, $) {
|
||||
if ( ! root ) {
|
||||
// CommonJS environments without a window global must pass a
|
||||
// root. This will give an error otherwise
|
||||
root = window;
|
||||
}
|
||||
|
||||
if ( ! $ || ! $.fn.dataTable ) {
|
||||
$ = require('datatables.net')(root, $).$;
|
||||
if ( ! $ ) {
|
||||
$ = typeof window !== 'undefined' ? // jQuery's factory checks for a global window
|
||||
require('jquery') :
|
||||
require('jquery')( root );
|
||||
}
|
||||
|
||||
if ( ! $.fn.dataTable ) {
|
||||
require('datatables.net')(root, $);
|
||||
}
|
||||
|
||||
|
||||
return factory( $, root, root.document );
|
||||
};
|
||||
}
|
||||
|
@ -17416,10 +17503,11 @@ return DataTable.Responsive;
|
|||
var DataTable = $.fn.dataTable;
|
||||
|
||||
|
||||
|
||||
// Version information for debugger
|
||||
DataTable.select = {};
|
||||
|
||||
DataTable.select.version = '1.4.0';
|
||||
DataTable.select.version = '1.5.0';
|
||||
|
||||
DataTable.select.init = function ( dt ) {
|
||||
var ctx = dt.settings()[0];
|
||||
|
@ -18688,7 +18776,6 @@ $(document).on( 'preInit.dt.dtSelect', function (e, ctx) {
|
|||
} );
|
||||
|
||||
|
||||
return DataTable.select;
|
||||
return DataTable;
|
||||
}));
|
||||
|
||||
|
File diff suppressed because one or more lines are too long
|
@ -1,70 +0,0 @@
|
|||
/**
|
||||
* This plug-in for DataTables represents the ultimate option in extensibility
|
||||
* for sorting date / time strings correctly. It uses
|
||||
* [Moment.js](http://momentjs.com) to create automatic type detection and
|
||||
* sorting plug-ins for DataTables based on a given format. This way, DataTables
|
||||
* will automatically detect your temporal information and sort it correctly.
|
||||
*
|
||||
* For usage instructions, please see the DataTables blog
|
||||
* post that [introduces it](//datatables.net/blog/2014-12-18).
|
||||
*
|
||||
* @name Ultimate Date / Time sorting
|
||||
* @summary Sort date and time in any format using Moment.js
|
||||
* @author [Allan Jardine](//datatables.net)
|
||||
* @depends DataTables 1.10+, Moment.js 1.7+
|
||||
*
|
||||
* @example
|
||||
* $.fn.dataTable.moment( 'HH:mm MMM D, YY' );
|
||||
* $.fn.dataTable.moment( 'dddd, MMMM Do, YYYY' );
|
||||
*
|
||||
* $('#example').DataTable();
|
||||
*/
|
||||
|
||||
(function (factory) {
|
||||
if (typeof define === "function" && define.amd) {
|
||||
define(["jquery", "moment", "datatables.net"], factory);
|
||||
} else {
|
||||
factory(jQuery, moment);
|
||||
}
|
||||
}(function ($, moment) {
|
||||
|
||||
function strip (d) {
|
||||
if ( typeof d === 'string' ) {
|
||||
// Strip HTML tags and newline characters if possible
|
||||
d = d.replace(/(<.*?>)|(\r?\n|\r)/g, '');
|
||||
|
||||
// Strip out surrounding white space
|
||||
d = d.trim();
|
||||
}
|
||||
|
||||
return d;
|
||||
}
|
||||
|
||||
$.fn.dataTable.moment = function ( format, locale, reverseEmpties ) {
|
||||
var types = $.fn.dataTable.ext.type;
|
||||
|
||||
// Add type detection
|
||||
types.detect.unshift( function ( d ) {
|
||||
d = strip(d);
|
||||
|
||||
// Null and empty values are acceptable
|
||||
if ( d === '' || d === null ) {
|
||||
return 'moment-'+format;
|
||||
}
|
||||
|
||||
return moment( d, format, locale, true ).isValid() ?
|
||||
'moment-'+format :
|
||||
null;
|
||||
} );
|
||||
|
||||
// Add sorting method - use an integer for the sorting
|
||||
types.order[ 'moment-'+format+'-pre' ] = function ( d ) {
|
||||
d = strip(d);
|
||||
|
||||
return !moment(d, format, locale, true).isValid() ?
|
||||
(reverseEmpties ? -Infinity : Infinity) :
|
||||
parseInt( moment( d, format, locale, true ).format( 'x' ), 10 );
|
||||
};
|
||||
};
|
||||
|
||||
}));
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue