Compare commits
273 Commits
2022-12
...
feat/mailb
Author | SHA1 | Date | |
---|---|---|---|
|
eb33166f3e | ||
|
06cce79806 | ||
|
67510adb9e | ||
|
70aab7568e | ||
|
f82aba3e26 | ||
|
f80940efdc | ||
|
38cd376228 | ||
|
9700b3251f | ||
|
88b8d50cd5 | ||
|
55b0191050 | ||
|
33c97fb318 | ||
|
23d33ad5a8 | ||
|
bd6c98047a | ||
|
73d6a29ae1 | ||
|
173e39c859 | ||
|
c0745c5cde | ||
|
1a6f93327e | ||
|
3c68a53170 | ||
|
e38c27ed67 | ||
|
8eaf8bbbde | ||
|
e015c7dbca | ||
|
58452abcdf | ||
|
2cbf0da137 | ||
|
aabcd10539 | ||
|
ee607dc3cc | ||
|
1265302a8e | ||
|
b5acf56e20 | ||
|
fe4a418af4 | ||
|
e5f03e8526 | ||
|
fb60c4a150 | ||
|
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 |
22
.github/renovate.json
vendored
22
.github/renovate.json
vendored
@@ -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.3.1
|
||||
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
|
||||
|
63
.github/workflows/integration_tests.yml
vendored
63
.github/workflows/integration_tests.yml
vendored
@@ -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'
|
2
.github/workflows/pr_to_nightly.yml
vendored
2
.github/workflows/pr_to_nightly.yml
vendored
@@ -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}}
|
||||
|
2
.github/workflows/rebuild_backup_image.yml
vendored
2
.github/workflows/rebuild_backup_image.yml
vendored
@@ -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'
|
39
.github/workflows/update_postscreen_access_list.yml
vendored
Normal file
39
.github/workflows/update_postscreen_access_list.yml
vendored
Normal file
@@ -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)
|
@@ -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')
|
260
data/Dockerfiles/dockerapi/main.py
Normal file
260
data/Dockerfiles/dockerapi/main.py
Normal file
@@ -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"
|
||||
)
|
486
data/Dockerfiles/dockerapi/modules/DockerApi.py
Normal file
486
data/Dockerfiles/dockerapi/modules/DockerApi.py
Normal file
@@ -0,0 +1,486 @@
|
||||
import psutil
|
||||
import sys
|
||||
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")
|
0
data/Dockerfiles/dockerapi/modules/__init__.py
Normal file
0
data/Dockerfiles/dockerapi/modules/__init__.py
Normal file
@@ -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")
|
||||
@@ -315,8 +315,14 @@ remote ${IPV4_NETWORK}.248 {
|
||||
}
|
||||
EOF
|
||||
|
||||
# Create random master Password for SOGo SSO
|
||||
RAND_PASS=$(cat /dev/urandom | tr -dc 'a-z0-9' | fold -w 32 | head -n 1)
|
||||
# Set SOGo SSO master Password
|
||||
if [ -z "$SOGO_SSO_PASS" ]; then
|
||||
# Set from env var
|
||||
RAND_PASS=$SOGO_SSO_PASS
|
||||
else
|
||||
# Create random master Password
|
||||
RAND_PASS=$(cat /dev/urandom | tr -dc 'a-z0-9' | fold -w 32 | head -n 1)
|
||||
fi
|
||||
echo -n ${RAND_PASS} > /etc/phpfpm/sogo-sso.pass
|
||||
cat <<EOF > /etc/dovecot/sogo-sso.conf
|
||||
# Autogenerated by mailcow
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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"];
|
||||
}
|
||||
|
@@ -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');
|
||||
@@ -89,7 +86,6 @@ $template_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,6 +103,7 @@ $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'],
|
||||
'lang_admin' => json_encode($lang['admin']),
|
||||
|
@@ -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:
|
||||
@@ -5586,6 +5628,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
|
||||
|
@@ -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']),
|
||||
|
BIN
data/web/fonts/source-sans-pro-v21-latin-300.woff
Normal file
BIN
data/web/fonts/source-sans-pro-v21-latin-300.woff
Normal file
Binary file not shown.
BIN
data/web/fonts/source-sans-pro-v21-latin-300.woff2
Normal file
BIN
data/web/fonts/source-sans-pro-v21-latin-300.woff2
Normal file
Binary file not shown.
BIN
data/web/fonts/source-sans-pro-v21-latin-300italic.woff
Normal file
BIN
data/web/fonts/source-sans-pro-v21-latin-300italic.woff
Normal file
Binary file not shown.
BIN
data/web/fonts/source-sans-pro-v21-latin-300italic.woff2
Normal file
BIN
data/web/fonts/source-sans-pro-v21-latin-300italic.woff2
Normal file
Binary file not shown.
BIN
data/web/fonts/source-sans-pro-v21-latin-700.woff
Normal file
BIN
data/web/fonts/source-sans-pro-v21-latin-700.woff
Normal file
Binary file not shown.
BIN
data/web/fonts/source-sans-pro-v21-latin-700.woff2
Normal file
BIN
data/web/fonts/source-sans-pro-v21-latin-700.woff2
Normal file
Binary file not shown.
BIN
data/web/fonts/source-sans-pro-v21-latin-700italic.woff
Normal file
BIN
data/web/fonts/source-sans-pro-v21-latin-700italic.woff
Normal file
Binary file not shown.
BIN
data/web/fonts/source-sans-pro-v21-latin-700italic.woff2
Normal file
BIN
data/web/fonts/source-sans-pro-v21-latin-700italic.woff2
Normal file
Binary file not shown.
BIN
data/web/fonts/source-sans-pro-v21-latin-italic.woff
Normal file
BIN
data/web/fonts/source-sans-pro-v21-latin-italic.woff
Normal file
Binary file not shown.
BIN
data/web/fonts/source-sans-pro-v21-latin-italic.woff2
Normal file
BIN
data/web/fonts/source-sans-pro-v21-latin-italic.woff2
Normal file
Binary file not shown.
BIN
data/web/fonts/source-sans-pro-v21-latin-regular.woff
Normal file
BIN
data/web/fonts/source-sans-pro-v21-latin-regular.woff
Normal file
Binary file not shown.
BIN
data/web/fonts/source-sans-pro-v21-latin-regular.woff2
Normal file
BIN
data/web/fonts/source-sans-pro-v21-latin-regular.woff2
Normal file
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;
|
||||
|
@@ -1015,20 +1015,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 +1777,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 +1786,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 +1831,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;
|
||||
|
@@ -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,14 +4930,7 @@ 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']
|
||||
);
|
||||
}
|
||||
docker('broadcast', 'dovecot-mailcow', 'exec', $exec_fields);
|
||||
}
|
||||
else {
|
||||
$_SESSION['return'][] = array(
|
||||
@@ -4950,9 +4982,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 +5085,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 +5206,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 +5217,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 +5299,68 @@ 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();
|
||||
}
|
||||
}
|
||||
function mailbox_sso($_action, $_data) {
|
||||
global $pdo;
|
||||
|
||||
switch ($_action) {
|
||||
case 'check':
|
||||
$token = $_data;
|
||||
|
||||
$stmt = $pdo->prepare("SELECT `t1`.`username` FROM `mailbox_sso` AS `t1` JOIN `mailbox` AS `t2` ON `t1`.`username` = `t2`.`username` WHERE `t1`.`token` = :token AND `t1`.`created` > DATE_SUB(NOW(), INTERVAL '30' SECOND) AND `t2`.`active` = 1;");
|
||||
$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 `mailbox`
|
||||
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 `mailbox_sso` (`username`, `token`)
|
||||
VALUES (:username, :token)");
|
||||
$stmt->execute(array(
|
||||
':username' => $username,
|
||||
':token' => $token
|
||||
));
|
||||
|
||||
// perform cleanup
|
||||
$pdo->query("DELETE FROM `mailbox_sso` WHERE created < DATE_SUB(NOW(), INTERVAL '30' SECOND);");
|
||||
|
||||
return ['token' => $token];
|
||||
break;
|
||||
}
|
||||
}
|
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,22 @@
|
||||
<?php
|
||||
if (!empty($_GET['sso_token'])) {
|
||||
// SSO Domain Admin
|
||||
$username = domain_admin_sso('check', $_GET['sso_token']);
|
||||
if ($username !== false) {
|
||||
$_SESSION['mailcow_cc_username'] = $username;
|
||||
$_SESSION['mailcow_cc_role'] = 'domainadmin';
|
||||
header('Location: /mailbox');
|
||||
}
|
||||
|
||||
// SSO Mailbox User
|
||||
$username = mailbox_sso('check', $_GET['sso_token']);
|
||||
if ($username !== false) {
|
||||
$_SESSION['mailcow_cc_username'] = $username;
|
||||
$_SESSION['mailcow_cc_role'] = 'user';
|
||||
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 +24,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 +52,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 +70,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;
|
||||
}));
|
||||
|
||||
|
7
data/web/js/build/004-moment.min.js
vendored
7
data/web/js/build/004-moment.min.js
vendored
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 );
|
||||
};
|
||||
};
|
||||
|
||||
}));
|
@@ -1,354 +1,373 @@
|
||||
$(document).ready(function() {
|
||||
// mailcow alert box generator
|
||||
window.mailcow_alert_box = function(message, type) {
|
||||
msg = $('<span/>').text(message).text();
|
||||
if (type == 'danger' || type == 'info') {
|
||||
auto_hide = 0;
|
||||
$('#' + localStorage.getItem("add_modal")).modal('show');
|
||||
localStorage.removeItem("add_modal");
|
||||
} else {
|
||||
auto_hide = 5000;
|
||||
}
|
||||
$.notify({message: msg},{z_index: 20000, delay: auto_hide, type: type,placement: {from: "bottom",align: "right"},animate: {enter: 'animated fadeInUp',exit: 'animated fadeOutDown'}});
|
||||
}
|
||||
|
||||
$(".generate_password").click(function( event ) {
|
||||
event.preventDefault();
|
||||
$('[data-hibp]').trigger('input');
|
||||
if (typeof($(this).closest("form").data('pwgen-length')) == "number") {
|
||||
var random_passwd = GPW.pronounceable($(this).closest("form").data('pwgen-length'))
|
||||
}
|
||||
else {
|
||||
var random_passwd = GPW.pronounceable(8)
|
||||
}
|
||||
$(this).closest("form").find('[data-pwgen-field]').attr('type', 'text');
|
||||
$(this).closest("form").find('[data-pwgen-field]').val(random_passwd);
|
||||
});
|
||||
function str_rot13(str) {
|
||||
return (str + '').replace(/[a-z]/gi, function(s){
|
||||
return String.fromCharCode(s.charCodeAt(0) + (s.toLowerCase() < 'n' ? 13 : -13))
|
||||
})
|
||||
}
|
||||
$(".rot-enc").html(function(){
|
||||
return str_rot13($(this).html())
|
||||
});
|
||||
// https://stackoverflow.com/questions/4399005/implementing-jquerys-shake-effect-with-animate
|
||||
function shake(div,interval,distance,times) {
|
||||
if(typeof interval === 'undefined') {
|
||||
interval = 100;
|
||||
}
|
||||
if(typeof distance === 'undefined') {
|
||||
distance = 10;
|
||||
}
|
||||
if(typeof times === 'undefined') {
|
||||
times = 4;
|
||||
}
|
||||
$(div).css('position','relative');
|
||||
for(var iter=0;iter<(times+1);iter++){
|
||||
$(div).animate({ left: ((iter%2==0 ? distance : distance*-1))}, interval);
|
||||
}
|
||||
$(div).animate({ left: 0},interval);
|
||||
}
|
||||
|
||||
// form cache
|
||||
$('[data-cached-form="true"]').formcache({key: $(this).data('id')});
|
||||
|
||||
// tooltips
|
||||
$(function () {
|
||||
$('[data-bs-toggle="tooltip"]').tooltip()
|
||||
});
|
||||
|
||||
// remember last navigation pill
|
||||
(function () {
|
||||
'use strict';
|
||||
// remember desktop tabs
|
||||
$('button[data-bs-toggle="tab"]').on('click', function (e) {
|
||||
if ($(this).data('dont-remember') == 1) {
|
||||
return true;
|
||||
}
|
||||
var id = $(this).parents('[role="tablist"]').attr('id');
|
||||
var key = 'lastTag';
|
||||
if (id) {
|
||||
key += ':' + id;
|
||||
}
|
||||
|
||||
var tab_id = $(e.target).attr('data-bs-target').substring(1);
|
||||
localStorage.setItem(key, tab_id);
|
||||
});
|
||||
// remember mobile tabs
|
||||
$('button[data-bs-target^="#collapse-tab-"]').on('click', function (e) {
|
||||
// only remember tab if its being opened
|
||||
if ($(this).hasClass('collapsed')) return false;
|
||||
var tab_id = $(this).closest('div[role="tabpanel"]').attr('id');
|
||||
|
||||
if ($(this).data('dont-remember') == 1) {
|
||||
return true;
|
||||
}
|
||||
var id = $(this).parents('[role="tablist"]').attr('id');;
|
||||
var key = 'lastTag';
|
||||
if (id) {
|
||||
key += ':' + id;
|
||||
}
|
||||
|
||||
localStorage.setItem(key, tab_id);
|
||||
});
|
||||
// open last tab
|
||||
$('[role="tablist"]').each(function (idx, elem) {
|
||||
var id = $(elem).attr('id');
|
||||
var key = 'lastTag';
|
||||
if (id) {
|
||||
key += ':' + id;
|
||||
}
|
||||
var lastTab = localStorage.getItem(key);
|
||||
if (lastTab) {
|
||||
$('[data-bs-target="#' + lastTab + '"]').click();
|
||||
var tab = $('[id^="' + lastTab + '"]');
|
||||
$(tab).find('.card-body.collapse').collapse('show');
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
||||
// IE fix to hide scrollbars when table body is empty
|
||||
$('tbody').filter(function (index) {
|
||||
return $(this).children().length < 1;
|
||||
}).remove();
|
||||
|
||||
// selectpicker
|
||||
$('select').selectpicker({
|
||||
'styleBase': 'btn btn-xs-lg',
|
||||
'noneSelectedText': lang_footer.nothing_selected
|
||||
});
|
||||
|
||||
// haveibeenpwned and passwd policy
|
||||
$.ajax({
|
||||
url: '/api/v1/get/passwordpolicy/html',
|
||||
type: 'GET',
|
||||
success: function(res) {
|
||||
$(".hibp-out").after(res);
|
||||
}
|
||||
});
|
||||
$('[data-hibp]').after('<p class="small haveibeenpwned"><i class="bi bi-shield-fill-exclamation"></i> ' + lang_footer.hibp_check + '</p><span class="hibp-out"></span>');
|
||||
$('[data-hibp]').on('input', function() {
|
||||
out_field = $(this).next('.haveibeenpwned').next('.hibp-out').text('').attr('class', 'hibp-out');
|
||||
});
|
||||
$('.haveibeenpwned:not(.task-running)').on('click', function() {
|
||||
var hibp_field = $(this)
|
||||
$(hibp_field).addClass('task-running');
|
||||
var hibp_result = $(hibp_field).next('.hibp-out')
|
||||
var password_field = $(this).prev('[data-hibp]')
|
||||
if ($(password_field).val() == '') {
|
||||
shake(password_field);
|
||||
}
|
||||
else {
|
||||
$(hibp_result).attr('class', 'hibp-out badge fs-5 bg-info');
|
||||
$(hibp_result).text(lang_footer.loading);
|
||||
var password_digest = $.sha1($(password_field).val())
|
||||
var digest_five = password_digest.substring(0, 5).toUpperCase();
|
||||
var queryURL = "https://api.pwnedpasswords.com/range/" + digest_five;
|
||||
var compl_digest = password_digest.substring(5, 41).toUpperCase();
|
||||
$.ajax({
|
||||
url: queryURL,
|
||||
type: 'GET',
|
||||
success: function(res) {
|
||||
if (res.search(compl_digest) > -1){
|
||||
$(hibp_result).removeClass('badge fs-5 bg-info').addClass('badge fs-5 bg-danger');
|
||||
$(hibp_result).text(lang_footer.hibp_nok)
|
||||
} else {
|
||||
$(hibp_result).removeClass('badge fs-5 bg-info').addClass('badge fs-5 bg-success');
|
||||
$(hibp_result).text(lang_footer.hibp_ok)
|
||||
}
|
||||
$(hibp_field).removeClass('task-running');
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
$(hibp_result).removeClass('badge fs-5 bg-info').addClass('badge fs-5 bg-warning');
|
||||
$(hibp_result).text('API error: ' + xhr.responseText)
|
||||
$(hibp_field).removeClass('task-running');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Disable disallowed inputs
|
||||
$('[data-acl="0"]').each(function(event){
|
||||
if ($(this).is("a")) {
|
||||
$(this).removeAttr("data-bs-toggle");
|
||||
$(this).removeAttr("data-bs-target");
|
||||
$(this).removeAttr("data-action");
|
||||
$(this).click(function(event) {
|
||||
event.preventDefault();
|
||||
});
|
||||
}
|
||||
if ($(this).is("select")) {
|
||||
$(this).selectpicker('destroy');
|
||||
$(this).replaceWith(function() {
|
||||
return '<label class="control-label"><b>' + this.innerText + '</b></label>';
|
||||
});
|
||||
}
|
||||
if ($(this).hasClass('btn-group')) {
|
||||
$(this).find('a').each(function(){
|
||||
$(this).removeClass('dropdown-toggle')
|
||||
.removeAttr('data-bs-toggle')
|
||||
.removeAttr('data-bs-target')
|
||||
.removeAttr('data-action')
|
||||
.removeAttr('id')
|
||||
.attr("disabled", true);
|
||||
$(this).click(function(event) {
|
||||
event.preventDefault();
|
||||
return;
|
||||
});
|
||||
});
|
||||
$(this).find('button').each(function() {
|
||||
$(this).attr("disabled", true);
|
||||
});
|
||||
} else if ($(this).hasClass('input-group')) {
|
||||
$(this).find('input').each(function() {
|
||||
$(this).removeClass('dropdown-toggle')
|
||||
.removeAttr('data-bs-toggle')
|
||||
.attr("disabled", true);
|
||||
$(this).click(function(event) {
|
||||
event.preventDefault();
|
||||
});
|
||||
});
|
||||
$(this).find('button').each(function() {
|
||||
$(this).attr("disabled", true);
|
||||
});
|
||||
} else if ($(this).hasClass('form-group')) {
|
||||
$(this).find('input').each(function() {
|
||||
$(this).attr("disabled", true);
|
||||
});
|
||||
} else if ($(this).hasClass('btn')) {
|
||||
$(this).attr("disabled", true);
|
||||
} else if ($(this).attr('data-provide') == 'slider') {
|
||||
$(this).attr('disabled', true);
|
||||
} else if ($(this).is(':checkbox')) {
|
||||
$(this).attr("disabled", true);
|
||||
}
|
||||
$(this).data("toggle", "tooltip");
|
||||
$(this).attr("title", lang_acl.prohibited);
|
||||
$(this).tooltip();
|
||||
});
|
||||
|
||||
// disable submit after submitting form (not API driven buttons)
|
||||
$('form').submit(function() {
|
||||
if ($('form button[type="submit"]').data('submitted') == '1') {
|
||||
return false;
|
||||
} else {
|
||||
$(this).find('button[type="submit"]').first().text(lang_footer.loading);
|
||||
$('form button[type="submit"]').attr('data-submitted', '1');
|
||||
function disableF5(e) { if ((e.which || e.keyCode) == 116 || (e.which || e.keyCode) == 82) e.preventDefault(); };
|
||||
$(document).on("keydown", disableF5);
|
||||
}
|
||||
});
|
||||
// Textarea line numbers
|
||||
$(".textarea-code").numberedtextarea({allowTabChar: true});
|
||||
// trigger container restart
|
||||
$('#RestartContainer').on('show.bs.modal', function(e) {
|
||||
var container = $(e.relatedTarget).data('container');
|
||||
$('#containerName').text(container);
|
||||
$('#triggerRestartContainer').click(function(){
|
||||
$(this).prop("disabled",true);
|
||||
$(this).html('<div class="spinner-border text-white" role="status"><span class="visually-hidden">Loading...</span></div>');
|
||||
$('#statusTriggerRestartContainer').html(lang_footer.restarting_container);
|
||||
$.ajax({
|
||||
method: 'get',
|
||||
url: '/inc/ajax/container_ctrl.php',
|
||||
timeout: docker_timeout,
|
||||
data: {
|
||||
'service': container,
|
||||
'action': 'restart'
|
||||
}
|
||||
})
|
||||
.always( function (data, status) {
|
||||
$('#statusTriggerRestartContainer').append(data);
|
||||
var htmlResponse = $.parseHTML(data)
|
||||
if ($(htmlResponse).find('span').hasClass('text-success')) {
|
||||
$('#triggerRestartContainer').html('<i class="bi bi-check-lg"></i> ');
|
||||
setTimeout(function(){
|
||||
$('#RestartContainer').modal('toggle');
|
||||
window.location = window.location.href.split("#")[0];
|
||||
}, 1200);
|
||||
} else {
|
||||
$('#triggerRestartContainer').html('<i class="bi bi-slash-lg"></i> ');
|
||||
}
|
||||
})
|
||||
});
|
||||
})
|
||||
|
||||
// Jquery Datatables, enable responsive plugin and date sort plugin
|
||||
$.extend($.fn.dataTable.defaults, {
|
||||
responsive: true
|
||||
});
|
||||
$.fn.dataTable.moment('dd:mm:YYYY');
|
||||
|
||||
// tag boxes
|
||||
$('.tag-box .tag-add').click(function(){
|
||||
addTag(this);
|
||||
});
|
||||
$(".tag-box .tag-input").keydown(function (e) {
|
||||
if (e.which == 13){
|
||||
e.preventDefault();
|
||||
addTag(this);
|
||||
}
|
||||
});
|
||||
|
||||
// Dark Mode Loader
|
||||
$('#dark-mode-toggle').click(toggleDarkMode);
|
||||
if ($('#dark-mode-theme').length) {
|
||||
$('#dark-mode-toggle').prop('checked', true);
|
||||
if ($('#rspamd_logo').length) $('#rspamd_logo').attr('src', '/img/rspamd_logo_light.png');
|
||||
if ($('#rspamd_logo_sm').length) $('#rspamd_logo_sm').attr('src', '/img/rspamd_logo_light.png');
|
||||
}
|
||||
function toggleDarkMode(){
|
||||
if($('#dark-mode-theme').length){
|
||||
$('#dark-mode-theme').remove();
|
||||
$('#dark-mode-toggle').prop('checked', false);
|
||||
if ($('#rspamd_logo').length) $('#rspamd_logo').attr('src', '/img/rspamd_logo_dark.png');
|
||||
if ($('#rspamd_logo_sm').length) $('#rspamd_logo_sm').attr('src', '/img/rspamd_logo_dark.png');
|
||||
localStorage.setItem('theme', 'light');
|
||||
}else{
|
||||
$('head').append('<link id="dark-mode-theme" rel="stylesheet" type="text/css" href="/css/themes/mailcow-darkmode.css">');
|
||||
$('#dark-mode-toggle').prop('checked', true);
|
||||
if ($('#rspamd_logo').length) $('#rspamd_logo').attr('src', '/img/rspamd_logo_light.png');
|
||||
if ($('#rspamd_logo_sm').length) $('#rspamd_logo_sm').attr('src', '/img/rspamd_logo_light.png');
|
||||
localStorage.setItem('theme', 'dark');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// https://stackoverflow.com/questions/24816/escaping-html-strings-with-jquery
|
||||
function escapeHtml(n){var entityMap={"&":"&","<":"<",">":">",'"':""","'":"'","/":"/","`":"`","=":"="}; return String(n).replace(/[&<>"'`=\/]/g,function(n){return entityMap[n]})}
|
||||
function unescapeHtml(t){var n={"&":"&","<":"<",">":">",""":'"',"'":"'","/":"/","`":"`","=":"="};return String(t).replace(/&|<|>|"|'|/|`|=/g,function(t){return n[t]})}
|
||||
|
||||
function addTag(tagAddElem, tag = null){
|
||||
var tagboxElem = $(tagAddElem).parent();
|
||||
var tagInputElem = $(tagboxElem).find(".tag-input")[0];
|
||||
var tagValuesElem = $(tagboxElem).find(".tag-values")[0];
|
||||
|
||||
if (!tag)
|
||||
tag = $(tagInputElem).val();
|
||||
if (!tag) return;
|
||||
var value_tags = [];
|
||||
try {
|
||||
value_tags = JSON.parse($(tagValuesElem).val());
|
||||
} catch {}
|
||||
if (!Array.isArray(value_tags)) value_tags = [];
|
||||
if (value_tags.includes(tag)) return;
|
||||
|
||||
$('<span class="badge bg-primary tag-badge btn-badge"><i class="bi bi-tag-fill"></i> ' + escapeHtml(tag) + '</span>').insertBefore('.tag-input').click(function(){
|
||||
var del_tag = unescapeHtml($(this).text());
|
||||
var del_tags = [];
|
||||
try {
|
||||
del_tags = JSON.parse($(tagValuesElem).val());
|
||||
} catch {}
|
||||
if (Array.isArray(del_tags)){
|
||||
del_tags.splice(del_tags.indexOf(del_tag), 1);
|
||||
$(tagValuesElem).val(JSON.stringify(del_tags));
|
||||
}
|
||||
$(this).remove();
|
||||
});
|
||||
|
||||
value_tags.push(tag);
|
||||
$(tagValuesElem).val(JSON.stringify(value_tags));
|
||||
$(tagInputElem).val('');
|
||||
}
|
||||
const LOCALE = undefined;
|
||||
const DATETIME_FORMAT = {
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit"
|
||||
};
|
||||
|
||||
$(document).ready(function() {
|
||||
// mailcow alert box generator
|
||||
window.mailcow_alert_box = function(message, type) {
|
||||
msg = $('<span/>').text(message).text();
|
||||
if (type == 'danger' || type == 'info') {
|
||||
auto_hide = 0;
|
||||
$('#' + localStorage.getItem("add_modal")).modal('show');
|
||||
localStorage.removeItem("add_modal");
|
||||
} else {
|
||||
auto_hide = 5000;
|
||||
}
|
||||
$.notify({message: msg},{z_index: 20000, delay: auto_hide, type: type,placement: {from: "bottom",align: "right"},animate: {enter: 'animated fadeInUp',exit: 'animated fadeOutDown'}});
|
||||
}
|
||||
|
||||
$(".generate_password").click(async function( event ) {
|
||||
try {
|
||||
var password_policy = await window.fetch("/api/v1/get/passwordpolicy", { method:'GET', cache:'no-cache' });
|
||||
var password_policy = await password_policy.json();
|
||||
random_passwd_length = password_policy.length;
|
||||
} catch(err) {
|
||||
var random_passwd_length = 8;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
$('[data-hibp]').trigger('input');
|
||||
if (typeof($(this).closest("form").data('pwgen-length')) == "number") {
|
||||
var random_passwd = GPW.pronounceable($(this).closest("form").data('pwgen-length'))
|
||||
}
|
||||
else {
|
||||
var random_passwd = GPW.pronounceable(random_passwd_length)
|
||||
}
|
||||
$(this).closest("form").find('[data-pwgen-field]').attr('type', 'text');
|
||||
$(this).closest("form").find('[data-pwgen-field]').val(random_passwd);
|
||||
});
|
||||
function str_rot13(str) {
|
||||
return (str + '').replace(/[a-z]/gi, function(s){
|
||||
return String.fromCharCode(s.charCodeAt(0) + (s.toLowerCase() < 'n' ? 13 : -13))
|
||||
})
|
||||
}
|
||||
$(".rot-enc").html(function(){
|
||||
return str_rot13($(this).html())
|
||||
});
|
||||
// https://stackoverflow.com/questions/4399005/implementing-jquerys-shake-effect-with-animate
|
||||
function shake(div,interval,distance,times) {
|
||||
if(typeof interval === 'undefined') {
|
||||
interval = 100;
|
||||
}
|
||||
if(typeof distance === 'undefined') {
|
||||
distance = 10;
|
||||
}
|
||||
if(typeof times === 'undefined') {
|
||||
times = 4;
|
||||
}
|
||||
$(div).css('position','relative');
|
||||
for(var iter=0;iter<(times+1);iter++){
|
||||
$(div).animate({ left: ((iter%2==0 ? distance : distance*-1))}, interval);
|
||||
}
|
||||
$(div).animate({ left: 0},interval);
|
||||
}
|
||||
|
||||
// form cache
|
||||
$('[data-cached-form="true"]').formcache({key: $(this).data('id')});
|
||||
|
||||
// tooltips
|
||||
$(function () {
|
||||
$('[data-bs-toggle="tooltip"]').tooltip()
|
||||
});
|
||||
|
||||
// remember last navigation pill
|
||||
(function () {
|
||||
'use strict';
|
||||
// remember desktop tabs
|
||||
$('button[data-bs-toggle="tab"]').on('click', function (e) {
|
||||
if ($(this).data('dont-remember') == 1) {
|
||||
return true;
|
||||
}
|
||||
var id = $(this).parents('[role="tablist"]').attr('id');
|
||||
var key = 'lastTag';
|
||||
if (id) {
|
||||
key += ':' + id;
|
||||
}
|
||||
|
||||
var tab_id = $(e.target).attr('data-bs-target').substring(1);
|
||||
localStorage.setItem(key, tab_id);
|
||||
});
|
||||
// remember mobile tabs
|
||||
$('button[data-bs-target^="#collapse-tab-"]').on('click', function (e) {
|
||||
// only remember tab if its being opened
|
||||
if ($(this).hasClass('collapsed')) return false;
|
||||
var tab_id = $(this).closest('div[role="tabpanel"]').attr('id');
|
||||
|
||||
if ($(this).data('dont-remember') == 1) {
|
||||
return true;
|
||||
}
|
||||
var id = $(this).parents('[role="tablist"]').attr('id');;
|
||||
var key = 'lastTag';
|
||||
if (id) {
|
||||
key += ':' + id;
|
||||
}
|
||||
|
||||
localStorage.setItem(key, tab_id);
|
||||
});
|
||||
// open last tab
|
||||
$('[role="tablist"]').each(function (idx, elem) {
|
||||
var id = $(elem).attr('id');
|
||||
var key = 'lastTag';
|
||||
if (id) {
|
||||
key += ':' + id;
|
||||
}
|
||||
var lastTab = localStorage.getItem(key);
|
||||
if (lastTab) {
|
||||
$('[data-bs-target="#' + lastTab + '"]').click();
|
||||
var tab = $('[id^="' + lastTab + '"]');
|
||||
$(tab).find('.card-body.collapse').collapse('show');
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
||||
// IE fix to hide scrollbars when table body is empty
|
||||
$('tbody').filter(function (index) {
|
||||
return $(this).children().length < 1;
|
||||
}).remove();
|
||||
|
||||
// selectpicker
|
||||
$('select').selectpicker({
|
||||
'styleBase': 'btn btn-xs-lg',
|
||||
'noneSelectedText': lang_footer.nothing_selected
|
||||
});
|
||||
|
||||
// haveibeenpwned and passwd policy
|
||||
$.ajax({
|
||||
url: '/api/v1/get/passwordpolicy/html',
|
||||
type: 'GET',
|
||||
success: function(res) {
|
||||
$(".hibp-out").after(res);
|
||||
}
|
||||
});
|
||||
$('[data-hibp]').after('<p class="small haveibeenpwned"><i class="bi bi-shield-fill-exclamation"></i> ' + lang_footer.hibp_check + '</p><span class="hibp-out"></span>');
|
||||
$('[data-hibp]').on('input', function() {
|
||||
out_field = $(this).next('.haveibeenpwned').next('.hibp-out').text('').attr('class', 'hibp-out');
|
||||
});
|
||||
$('.haveibeenpwned:not(.task-running)').on('click', function() {
|
||||
var hibp_field = $(this)
|
||||
$(hibp_field).addClass('task-running');
|
||||
var hibp_result = $(hibp_field).next('.hibp-out')
|
||||
var password_field = $(this).prev('[data-hibp]')
|
||||
if ($(password_field).val() == '') {
|
||||
shake(password_field);
|
||||
}
|
||||
else {
|
||||
$(hibp_result).attr('class', 'hibp-out badge fs-5 bg-info');
|
||||
$(hibp_result).text(lang_footer.loading);
|
||||
var password_digest = $.sha1($(password_field).val())
|
||||
var digest_five = password_digest.substring(0, 5).toUpperCase();
|
||||
var queryURL = "https://api.pwnedpasswords.com/range/" + digest_five;
|
||||
var compl_digest = password_digest.substring(5, 41).toUpperCase();
|
||||
$.ajax({
|
||||
url: queryURL,
|
||||
type: 'GET',
|
||||
success: function(res) {
|
||||
if (res.search(compl_digest) > -1){
|
||||
$(hibp_result).removeClass('badge fs-5 bg-info').addClass('badge fs-5 bg-danger');
|
||||
$(hibp_result).text(lang_footer.hibp_nok)
|
||||
} else {
|
||||
$(hibp_result).removeClass('badge fs-5 bg-info').addClass('badge fs-5 bg-success');
|
||||
$(hibp_result).text(lang_footer.hibp_ok)
|
||||
}
|
||||
$(hibp_field).removeClass('task-running');
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
$(hibp_result).removeClass('badge fs-5 bg-info').addClass('badge fs-5 bg-warning');
|
||||
$(hibp_result).text('API error: ' + xhr.responseText)
|
||||
$(hibp_field).removeClass('task-running');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Disable disallowed inputs
|
||||
$('[data-acl="0"]').each(function(event){
|
||||
if ($(this).is("a")) {
|
||||
$(this).removeAttr("data-bs-toggle");
|
||||
$(this).removeAttr("data-bs-target");
|
||||
$(this).removeAttr("data-action");
|
||||
$(this).click(function(event) {
|
||||
event.preventDefault();
|
||||
});
|
||||
}
|
||||
if ($(this).is("select")) {
|
||||
$(this).selectpicker('destroy');
|
||||
$(this).replaceWith(function() {
|
||||
return '<label class="control-label"><b>' + this.innerText + '</b></label>';
|
||||
});
|
||||
}
|
||||
if ($(this).hasClass('btn-group')) {
|
||||
$(this).find('a').each(function(){
|
||||
$(this).removeClass('dropdown-toggle')
|
||||
.removeAttr('data-bs-toggle')
|
||||
.removeAttr('data-bs-target')
|
||||
.removeAttr('data-action')
|
||||
.removeAttr('id')
|
||||
.attr("disabled", true);
|
||||
$(this).click(function(event) {
|
||||
event.preventDefault();
|
||||
return;
|
||||
});
|
||||
});
|
||||
$(this).find('button').each(function() {
|
||||
$(this).attr("disabled", true);
|
||||
});
|
||||
} else if ($(this).hasClass('input-group')) {
|
||||
$(this).find('input').each(function() {
|
||||
$(this).removeClass('dropdown-toggle')
|
||||
.removeAttr('data-bs-toggle')
|
||||
.attr("disabled", true);
|
||||
$(this).click(function(event) {
|
||||
event.preventDefault();
|
||||
});
|
||||
});
|
||||
$(this).find('button').each(function() {
|
||||
$(this).attr("disabled", true);
|
||||
});
|
||||
} else if ($(this).hasClass('form-group')) {
|
||||
$(this).find('input').each(function() {
|
||||
$(this).attr("disabled", true);
|
||||
});
|
||||
} else if ($(this).hasClass('btn')) {
|
||||
$(this).attr("disabled", true);
|
||||
} else if ($(this).attr('data-provide') == 'slider') {
|
||||
$(this).attr('disabled', true);
|
||||
} else if ($(this).is(':checkbox')) {
|
||||
$(this).attr("disabled", true);
|
||||
}
|
||||
$(this).data("toggle", "tooltip");
|
||||
$(this).attr("title", lang_acl.prohibited);
|
||||
$(this).tooltip();
|
||||
});
|
||||
|
||||
// disable submit after submitting form (not API driven buttons)
|
||||
$('form').submit(function() {
|
||||
if ($('form button[type="submit"]').data('submitted') == '1') {
|
||||
return false;
|
||||
} else {
|
||||
$(this).find('button[type="submit"]').first().text(lang_footer.loading);
|
||||
$('form button[type="submit"]').attr('data-submitted', '1');
|
||||
function disableF5(e) { if ((e.which || e.keyCode) == 116 || (e.which || e.keyCode) == 82) e.preventDefault(); };
|
||||
$(document).on("keydown", disableF5);
|
||||
}
|
||||
});
|
||||
// Textarea line numbers
|
||||
$(".textarea-code").numberedtextarea({allowTabChar: true});
|
||||
// trigger container restart
|
||||
$('#RestartContainer').on('show.bs.modal', function(e) {
|
||||
var container = $(e.relatedTarget).data('container');
|
||||
$('#containerName').text(container);
|
||||
$('#triggerRestartContainer').click(function(){
|
||||
$(this).prop("disabled",true);
|
||||
$(this).html('<div class="spinner-border text-white" role="status"><span class="visually-hidden">Loading...</span></div>');
|
||||
$('#statusTriggerRestartContainer').html(lang_footer.restarting_container);
|
||||
$.ajax({
|
||||
method: 'get',
|
||||
url: '/inc/ajax/container_ctrl.php',
|
||||
timeout: docker_timeout,
|
||||
data: {
|
||||
'service': container,
|
||||
'action': 'restart'
|
||||
}
|
||||
})
|
||||
.always( function (data, status) {
|
||||
$('#statusTriggerRestartContainer').append(data);
|
||||
var htmlResponse = $.parseHTML(data)
|
||||
if ($(htmlResponse).find('span').hasClass('text-success')) {
|
||||
$('#triggerRestartContainer').html('<i class="bi bi-check-lg"></i> ');
|
||||
setTimeout(function(){
|
||||
$('#RestartContainer').modal('toggle');
|
||||
window.location = window.location.href.split("#")[0];
|
||||
}, 1200);
|
||||
} else {
|
||||
$('#triggerRestartContainer').html('<i class="bi bi-slash-lg"></i> ');
|
||||
}
|
||||
})
|
||||
});
|
||||
})
|
||||
|
||||
// Jquery Datatables, enable responsive plugin
|
||||
$.extend($.fn.dataTable.defaults, {
|
||||
responsive: true
|
||||
});
|
||||
// disable default datatable click listener
|
||||
$(document).off('click', 'tbody>tr');
|
||||
|
||||
// tag boxes
|
||||
$('.tag-box .tag-add').click(function(){
|
||||
addTag(this);
|
||||
});
|
||||
$(".tag-box .tag-input").keydown(function (e) {
|
||||
if (e.which == 13){
|
||||
e.preventDefault();
|
||||
addTag(this);
|
||||
}
|
||||
});
|
||||
|
||||
// Dark Mode Loader
|
||||
$('#dark-mode-toggle').click(toggleDarkMode);
|
||||
if ($('#dark-mode-theme').length) {
|
||||
$('#dark-mode-toggle').prop('checked', true);
|
||||
if ($('#rspamd_logo').length) $('#rspamd_logo').attr('src', '/img/rspamd_logo_light.png');
|
||||
if ($('#rspamd_logo_sm').length) $('#rspamd_logo_sm').attr('src', '/img/rspamd_logo_light.png');
|
||||
}
|
||||
function toggleDarkMode(){
|
||||
if($('#dark-mode-theme').length){
|
||||
$('#dark-mode-theme').remove();
|
||||
$('#dark-mode-toggle').prop('checked', false);
|
||||
if ($('#rspamd_logo').length) $('#rspamd_logo').attr('src', '/img/rspamd_logo_dark.png');
|
||||
if ($('#rspamd_logo_sm').length) $('#rspamd_logo_sm').attr('src', '/img/rspamd_logo_dark.png');
|
||||
localStorage.setItem('theme', 'light');
|
||||
}else{
|
||||
$('head').append('<link id="dark-mode-theme" rel="stylesheet" type="text/css" href="/css/themes/mailcow-darkmode.css">');
|
||||
$('#dark-mode-toggle').prop('checked', true);
|
||||
if ($('#rspamd_logo').length) $('#rspamd_logo').attr('src', '/img/rspamd_logo_light.png');
|
||||
if ($('#rspamd_logo_sm').length) $('#rspamd_logo_sm').attr('src', '/img/rspamd_logo_light.png');
|
||||
localStorage.setItem('theme', 'dark');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// https://stackoverflow.com/questions/24816/escaping-html-strings-with-jquery
|
||||
function escapeHtml(n){var entityMap={"&":"&","<":"<",">":">",'"':""","'":"'","/":"/","`":"`","=":"="}; return String(n).replace(/[&<>"'`=\/]/g,function(n){return entityMap[n]})}
|
||||
function unescapeHtml(t){var n={"&":"&","<":"<",">":">",""":'"',"'":"'","/":"/","`":"`","=":"="};return String(t).replace(/&|<|>|"|'|/|`|=/g,function(t){return n[t]})}
|
||||
|
||||
function addTag(tagAddElem, tag = null){
|
||||
var tagboxElem = $(tagAddElem).parent();
|
||||
var tagInputElem = $(tagboxElem).find(".tag-input")[0];
|
||||
var tagValuesElem = $(tagboxElem).find(".tag-values")[0];
|
||||
|
||||
if (!tag)
|
||||
tag = $(tagInputElem).val();
|
||||
if (!tag) return;
|
||||
var value_tags = [];
|
||||
try {
|
||||
value_tags = JSON.parse($(tagValuesElem).val());
|
||||
} catch {}
|
||||
if (!Array.isArray(value_tags)) value_tags = [];
|
||||
if (value_tags.includes(tag)) return;
|
||||
|
||||
$('<span class="badge bg-primary tag-badge btn-badge"><i class="bi bi-tag-fill"></i> ' + escapeHtml(tag) + '</span>').insertBefore('.tag-input').click(function(){
|
||||
var del_tag = unescapeHtml($(this).text());
|
||||
var del_tags = [];
|
||||
try {
|
||||
del_tags = JSON.parse($(tagValuesElem).val());
|
||||
} catch {}
|
||||
if (Array.isArray(del_tags)){
|
||||
del_tags.splice(del_tags.indexOf(del_tag), 1);
|
||||
$(tagValuesElem).val(JSON.stringify(del_tags));
|
||||
}
|
||||
$(this).remove();
|
||||
});
|
||||
|
||||
value_tags.push(tag);
|
||||
$(tagValuesElem).val(JSON.stringify(value_tags));
|
||||
$(tagInputElem).val('');
|
||||
}
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,210 +1,222 @@
|
||||
$(document).ready(function() {
|
||||
$(".arrow-toggle").on('click', function(e) { e.preventDefault(); $(this).find('.arrow').toggleClass("animation"); });
|
||||
$("#pushover_delete").click(function() { return confirm(lang.delete_ays); });
|
||||
$(".goto_checkbox").click(function( event ) {
|
||||
$("form[data-id='editalias'] .goto_checkbox").not(this).prop('checked', false);
|
||||
if ($("form[data-id='editalias'] .goto_checkbox:checked").length > 0) {
|
||||
$('#textarea_alias_goto').prop('disabled', true);
|
||||
}
|
||||
else {
|
||||
$("#textarea_alias_goto").removeAttr('disabled');
|
||||
}
|
||||
});
|
||||
$("#disable_sender_check").click(function( event ) {
|
||||
if ($("form[data-id='editmailbox'] #disable_sender_check:checked").length > 0) {
|
||||
$('#editSelectSenderACL').prop('disabled', true);
|
||||
$('#editSelectSenderACL').selectpicker('refresh');
|
||||
}
|
||||
else {
|
||||
$('#editSelectSenderACL').prop('disabled', false);
|
||||
$('#editSelectSenderACL').selectpicker('refresh');
|
||||
}
|
||||
});
|
||||
if ($("form[data-id='editalias'] .goto_checkbox:checked").length > 0) {
|
||||
$('#textarea_alias_goto').prop('disabled', true);
|
||||
}
|
||||
|
||||
$("#mailbox-password-warning-close").click(function( event ) {
|
||||
$('#mailbox-passwd-hidden-info').addClass('hidden');
|
||||
$('#mailbox-passwd-form-groups').removeClass('hidden');
|
||||
});
|
||||
// Sender ACL
|
||||
if ($("#editSelectSenderACL option[value='\*']:selected").length > 0){
|
||||
$("#sender_acl_disabled").show();
|
||||
}
|
||||
$('#editSelectSenderACL').change(function() {
|
||||
if ($("#editSelectSenderACL option[value='\*']:selected").length > 0){
|
||||
$("#sender_acl_disabled").show();
|
||||
}
|
||||
else {
|
||||
$("#sender_acl_disabled").hide();
|
||||
}
|
||||
});
|
||||
// Resources
|
||||
if ($("#editSelectMultipleBookings").val() == "custom") {
|
||||
$("#multiple_bookings_custom_div").show();
|
||||
$('input[name=multiple_bookings]').val($("#multiple_bookings_custom").val());
|
||||
}
|
||||
$("#editSelectMultipleBookings").change(function() {
|
||||
$('input[name=multiple_bookings]').val($("#editSelectMultipleBookings").val());
|
||||
if ($('input[name=multiple_bookings]').val() == "custom") {
|
||||
$("#multiple_bookings_custom_div").show();
|
||||
}
|
||||
else {
|
||||
$("#multiple_bookings_custom_div").hide();
|
||||
}
|
||||
});
|
||||
$("#multiple_bookings_custom").bind("change keypress keyup blur", function() {
|
||||
$('input[name=multiple_bookings]').val($("#multiple_bookings_custom").val());
|
||||
});
|
||||
|
||||
// load tags
|
||||
if ($('#tags').length){
|
||||
var tagsEl = $('#tags').parent().find('.tag-values')[0];
|
||||
console.log($(tagsEl).val())
|
||||
var tags = JSON.parse($(tagsEl).val());
|
||||
$(tagsEl).val("");
|
||||
|
||||
for (var i = 0; i < tags.length; i++)
|
||||
addTag($('#tags'), tags[i]);
|
||||
}
|
||||
});
|
||||
|
||||
jQuery(function($){
|
||||
// http://stackoverflow.com/questions/46155/validate-email-address-in-javascript
|
||||
function validateEmail(email) {
|
||||
var re = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
|
||||
return re.test(email);
|
||||
}
|
||||
function draw_wl_policy_domain_table() {
|
||||
$('#wl_policy_domain_table').DataTable({
|
||||
processing: true,
|
||||
serverSide: false,
|
||||
language: lang_datatables,
|
||||
ajax: {
|
||||
type: "GET",
|
||||
url: '/api/v1/get/policy_wl_domain/' + table_for_domain,
|
||||
dataSrc: function(data){
|
||||
$.each(data, function (i, item) {
|
||||
if (!validateEmail(item.object)) {
|
||||
item.chkbox = '<input type="checkbox" data-id="policy_wl_domain" name="multi_select" value="' + item.prefid + '" />';
|
||||
}
|
||||
else {
|
||||
item.chkbox = '<input type="checkbox" disabled title="' + lang_user.spamfilter_table_domain_policy + '" />';
|
||||
}
|
||||
});
|
||||
|
||||
return data;
|
||||
}
|
||||
},
|
||||
columns: [
|
||||
{
|
||||
// placeholder, so checkbox will not block child row toggle
|
||||
title: '',
|
||||
data: null,
|
||||
searchable: false,
|
||||
orderable: false,
|
||||
defaultContent: ''
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
data: 'chkbox',
|
||||
searchable: false,
|
||||
orderable: false,
|
||||
defaultContent: ''
|
||||
},
|
||||
{
|
||||
title: 'ID',
|
||||
data: 'prefid',
|
||||
defaultContent: ''
|
||||
},
|
||||
{
|
||||
title: lang_user.spamfilter_table_rule,
|
||||
data: 'value',
|
||||
defaultContent: ''
|
||||
},
|
||||
{
|
||||
title: 'Scope',
|
||||
data: 'object',
|
||||
defaultContent: ''
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
function draw_bl_policy_domain_table() {
|
||||
$('#bl_policy_domain_table').DataTable({
|
||||
processing: true,
|
||||
serverSide: false,
|
||||
language: lang_datatables,
|
||||
ajax: {
|
||||
type: "GET",
|
||||
url: '/api/v1/get/policy_bl_domain/' + table_for_domain,
|
||||
dataSrc: function(data){
|
||||
$.each(data, function (i, item) {
|
||||
if (!validateEmail(item.object)) {
|
||||
item.chkbox = '<input type="checkbox" data-id="policy_bl_domain" name="multi_select" value="' + item.prefid + '" />';
|
||||
}
|
||||
else {
|
||||
item.chkbox = '<input type="checkbox" disabled tooltip="' + lang_user.spamfilter_table_domain_policy + '" />';
|
||||
}
|
||||
});
|
||||
|
||||
return data;
|
||||
}
|
||||
},
|
||||
columns: [
|
||||
{
|
||||
// placeholder, so checkbox will not block child row toggle
|
||||
title: '',
|
||||
data: null,
|
||||
searchable: false,
|
||||
orderable: false,
|
||||
defaultContent: ''
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
data: 'chkbox',
|
||||
searchable: false,
|
||||
orderable: false,
|
||||
defaultContent: ''
|
||||
},
|
||||
{
|
||||
title: 'ID',
|
||||
data: 'prefid',
|
||||
defaultContent: ''
|
||||
},
|
||||
{
|
||||
title: lang_user.spamfilter_table_rule,
|
||||
data: 'value',
|
||||
defaultContent: ''
|
||||
},
|
||||
{
|
||||
title: 'Scope',
|
||||
data: 'object',
|
||||
defaultContent: ''
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// detect element visibility changes
|
||||
function onVisible(element, callback) {
|
||||
$(document).ready(function() {
|
||||
element_object = document.querySelector(element);
|
||||
if (element_object === null) return;
|
||||
|
||||
new IntersectionObserver((entries, observer) => {
|
||||
entries.forEach(entry => {
|
||||
if(entry.intersectionRatio > 0) {
|
||||
callback(element_object);
|
||||
observer.disconnect();
|
||||
}
|
||||
});
|
||||
}).observe(element_object);
|
||||
});
|
||||
}
|
||||
// Draw Table if tab is active
|
||||
onVisible("[id^=wl_policy_domain_table]", () => draw_wl_policy_domain_table());
|
||||
onVisible("[id^=bl_policy_domain_table]", () => draw_bl_policy_domain_table());
|
||||
});
|
||||
$(document).ready(function() {
|
||||
$(".arrow-toggle").on('click', function(e) { e.preventDefault(); $(this).find('.arrow').toggleClass("animation"); });
|
||||
$("#pushover_delete").click(function() { return confirm(lang.delete_ays); });
|
||||
$(".goto_checkbox").click(function( event ) {
|
||||
$("form[data-id='editalias'] .goto_checkbox").not(this).prop('checked', false);
|
||||
if ($("form[data-id='editalias'] .goto_checkbox:checked").length > 0) {
|
||||
$('#textarea_alias_goto').prop('disabled', true);
|
||||
}
|
||||
else {
|
||||
$("#textarea_alias_goto").removeAttr('disabled');
|
||||
}
|
||||
});
|
||||
$("#disable_sender_check").click(function( event ) {
|
||||
if ($("form[data-id='editmailbox'] #disable_sender_check:checked").length > 0) {
|
||||
$('#editSelectSenderACL').prop('disabled', true);
|
||||
$('#editSelectSenderACL').selectpicker('refresh');
|
||||
}
|
||||
else {
|
||||
$('#editSelectSenderACL').prop('disabled', false);
|
||||
$('#editSelectSenderACL').selectpicker('refresh');
|
||||
}
|
||||
});
|
||||
if ($("form[data-id='editalias'] .goto_checkbox:checked").length > 0) {
|
||||
$('#textarea_alias_goto').prop('disabled', true);
|
||||
}
|
||||
|
||||
$("#mailbox-password-warning-close").click(function( event ) {
|
||||
$('#mailbox-passwd-hidden-info').addClass('hidden');
|
||||
$('#mailbox-passwd-form-groups').removeClass('hidden');
|
||||
});
|
||||
// Sender ACL
|
||||
if ($("#editSelectSenderACL option[value='\*']:selected").length > 0){
|
||||
$("#sender_acl_disabled").show();
|
||||
}
|
||||
$('#editSelectSenderACL').change(function() {
|
||||
if ($("#editSelectSenderACL option[value='\*']:selected").length > 0){
|
||||
$("#sender_acl_disabled").show();
|
||||
}
|
||||
else {
|
||||
$("#sender_acl_disabled").hide();
|
||||
}
|
||||
});
|
||||
// Resources
|
||||
if ($("#editSelectMultipleBookings").val() == "custom") {
|
||||
$("#multiple_bookings_custom_div").show();
|
||||
$('input[name=multiple_bookings]').val($("#multiple_bookings_custom").val());
|
||||
}
|
||||
$("#editSelectMultipleBookings").change(function() {
|
||||
$('input[name=multiple_bookings]').val($("#editSelectMultipleBookings").val());
|
||||
if ($('input[name=multiple_bookings]').val() == "custom") {
|
||||
$("#multiple_bookings_custom_div").show();
|
||||
}
|
||||
else {
|
||||
$("#multiple_bookings_custom_div").hide();
|
||||
}
|
||||
});
|
||||
$("#multiple_bookings_custom").bind("change keypress keyup blur", function() {
|
||||
$('input[name=multiple_bookings]').val($("#multiple_bookings_custom").val());
|
||||
});
|
||||
|
||||
// load tags
|
||||
if ($('#tags').length){
|
||||
var tagsEl = $('#tags').parent().find('.tag-values')[0];
|
||||
console.log($(tagsEl).val())
|
||||
var tags = JSON.parse($(tagsEl).val());
|
||||
$(tagsEl).val("");
|
||||
|
||||
for (var i = 0; i < tags.length; i++)
|
||||
addTag($('#tags'), tags[i]);
|
||||
}
|
||||
});
|
||||
|
||||
jQuery(function($){
|
||||
// http://stackoverflow.com/questions/46155/validate-email-address-in-javascript
|
||||
function validateEmail(email) {
|
||||
var re = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
|
||||
return re.test(email);
|
||||
}
|
||||
function draw_wl_policy_domain_table() {
|
||||
$('#wl_policy_domain_table').DataTable({
|
||||
responsive: true,
|
||||
processing: true,
|
||||
serverSide: false,
|
||||
stateSave: true,
|
||||
pageLength: pagination_size,
|
||||
dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" +
|
||||
"tr" +
|
||||
"<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
|
||||
language: lang_datatables,
|
||||
ajax: {
|
||||
type: "GET",
|
||||
url: '/api/v1/get/policy_wl_domain/' + table_for_domain,
|
||||
dataSrc: function(data){
|
||||
$.each(data, function (i, item) {
|
||||
if (!validateEmail(item.object)) {
|
||||
item.chkbox = '<input type="checkbox" data-id="policy_wl_domain" name="multi_select" value="' + item.prefid + '" />';
|
||||
}
|
||||
else {
|
||||
item.chkbox = '<input type="checkbox" disabled title="' + lang_user.spamfilter_table_domain_policy + '" />';
|
||||
}
|
||||
});
|
||||
|
||||
return data;
|
||||
}
|
||||
},
|
||||
columns: [
|
||||
{
|
||||
// placeholder, so checkbox will not block child row toggle
|
||||
title: '',
|
||||
data: null,
|
||||
searchable: false,
|
||||
orderable: false,
|
||||
defaultContent: ''
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
data: 'chkbox',
|
||||
searchable: false,
|
||||
orderable: false,
|
||||
defaultContent: ''
|
||||
},
|
||||
{
|
||||
title: 'ID',
|
||||
data: 'prefid',
|
||||
defaultContent: ''
|
||||
},
|
||||
{
|
||||
title: lang_user.spamfilter_table_rule,
|
||||
data: 'value',
|
||||
defaultContent: ''
|
||||
},
|
||||
{
|
||||
title: 'Scope',
|
||||
data: 'object',
|
||||
defaultContent: ''
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
function draw_bl_policy_domain_table() {
|
||||
$('#bl_policy_domain_table').DataTable({
|
||||
responsive: true,
|
||||
processing: true,
|
||||
serverSide: false,
|
||||
stateSave: true,
|
||||
pageLength: pagination_size,
|
||||
dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" +
|
||||
"tr" +
|
||||
"<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
|
||||
language: lang_datatables,
|
||||
ajax: {
|
||||
type: "GET",
|
||||
url: '/api/v1/get/policy_bl_domain/' + table_for_domain,
|
||||
dataSrc: function(data){
|
||||
$.each(data, function (i, item) {
|
||||
if (!validateEmail(item.object)) {
|
||||
item.chkbox = '<input type="checkbox" data-id="policy_bl_domain" name="multi_select" value="' + item.prefid + '" />';
|
||||
}
|
||||
else {
|
||||
item.chkbox = '<input type="checkbox" disabled tooltip="' + lang_user.spamfilter_table_domain_policy + '" />';
|
||||
}
|
||||
});
|
||||
|
||||
return data;
|
||||
}
|
||||
},
|
||||
columns: [
|
||||
{
|
||||
// placeholder, so checkbox will not block child row toggle
|
||||
title: '',
|
||||
data: null,
|
||||
searchable: false,
|
||||
orderable: false,
|
||||
defaultContent: ''
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
data: 'chkbox',
|
||||
searchable: false,
|
||||
orderable: false,
|
||||
defaultContent: ''
|
||||
},
|
||||
{
|
||||
title: 'ID',
|
||||
data: 'prefid',
|
||||
defaultContent: ''
|
||||
},
|
||||
{
|
||||
title: lang_user.spamfilter_table_rule,
|
||||
data: 'value',
|
||||
defaultContent: ''
|
||||
},
|
||||
{
|
||||
title: 'Scope',
|
||||
data: 'object',
|
||||
defaultContent: ''
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// detect element visibility changes
|
||||
function onVisible(element, callback) {
|
||||
$(document).ready(function() {
|
||||
element_object = document.querySelector(element);
|
||||
if (element_object === null) return;
|
||||
|
||||
new IntersectionObserver((entries, observer) => {
|
||||
entries.forEach(entry => {
|
||||
if(entry.intersectionRatio > 0) {
|
||||
callback(element_object);
|
||||
observer.disconnect();
|
||||
}
|
||||
});
|
||||
}).observe(element_object);
|
||||
});
|
||||
}
|
||||
// Draw Table if tab is active
|
||||
onVisible("[id^=wl_policy_domain_table]", () => draw_wl_policy_domain_table());
|
||||
onVisible("[id^=bl_policy_domain_table]", () => draw_bl_policy_domain_table());
|
||||
});
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -1,71 +1,71 @@
|
||||
jQuery(function($){
|
||||
var qitem = $('legend').data('hash');
|
||||
var qError = $("#qid_error");
|
||||
$.ajax({
|
||||
url: '/inc/ajax/qitem_details.php',
|
||||
data: { hash: qitem },
|
||||
dataType: 'json',
|
||||
success: function(data){
|
||||
$('[data-id="qitems_single"]').each(function(index) {
|
||||
$(this).attr("data-item", qitem);
|
||||
});
|
||||
$('#qid_detail_subj').text(data.subject);
|
||||
$('#qid_detail_hfrom').text(data.header_from);
|
||||
$('#qid_detail_efrom').text(data.env_from);
|
||||
$('#qid_detail_score').html('');
|
||||
$('#qid_detail_symbols').html('');
|
||||
$('#qid_detail_recipients').html('');
|
||||
$('#qid_detail_fuzzy').html('');
|
||||
if (typeof data.fuzzy_hashes === 'object' && data.fuzzy_hashes !== null && data.fuzzy_hashes.length !== 0) {
|
||||
$.each(data.fuzzy_hashes, function (index, value) {
|
||||
$('#qid_detail_fuzzy').append('<p style="font-family:monospace">' + value + '</p>');
|
||||
});
|
||||
} else {
|
||||
$('#qid_detail_fuzzy').append('-');
|
||||
}
|
||||
if (typeof data.symbols !== 'undefined') {
|
||||
data.symbols.sort(function (a, b) {
|
||||
if (a.score === 0) return 1
|
||||
if (b.score === 0) return -1
|
||||
if (b.score < 0 && a.score < 0) {
|
||||
return a.score - b.score
|
||||
}
|
||||
if (b.score > 0 && a.score > 0) {
|
||||
return b.score - a.score
|
||||
}
|
||||
return b.score - a.score
|
||||
})
|
||||
$.each(data.symbols, function (index, value) {
|
||||
var highlightClass = ''
|
||||
if (value.score > 0) highlightClass = 'negative'
|
||||
else if (value.score < 0) highlightClass = 'positive'
|
||||
else highlightClass = 'neutral'
|
||||
$('#qid_detail_symbols').append('<span data-bs-toggle="tooltip" class="rspamd-symbol ' + highlightClass + '" title="' + (value.options ? value.options.join(', ') : '') + '">' + value.name + ' (<span class="score">' + value.score + '</span>)</span>');
|
||||
});
|
||||
$('[data-bs-toggle="tooltip"]').tooltip()
|
||||
}
|
||||
if (typeof data.score !== 'undefined' && typeof data.action !== 'undefined') {
|
||||
if (data.action === "add header") {
|
||||
$('#qid_detail_score').append('<span class="label-rspamd-action badge fs-6 bg-warning"><b>' + data.score + '</b> - ' + lang.junk_folder + '</span>');
|
||||
} else if (data.action === "reject") {
|
||||
$('#qid_detail_score').append('<span class="label-rspamd-action badge fs-6 bg-danger"><b>' + data.score + '</b> - ' + lang.rejected + '</span>');
|
||||
} else if (data.action === "rewrite subject") {
|
||||
$('#qid_detail_score').append('<span class="label-rspamd-action badge fs-6 bg-warning"><b>' + data.score + '</b> - ' + lang.rewrite_subject + '</span>');
|
||||
}
|
||||
}
|
||||
if (typeof data.recipients !== 'undefined') {
|
||||
$.each(data.recipients, function(index, value) {
|
||||
var elem = $('<span class="mail-address-item"></span>');
|
||||
elem.text(value.address + ' (' + value.type.toUpperCase() + ')');
|
||||
$('#qid_detail_recipients').append(elem);
|
||||
});
|
||||
}
|
||||
},
|
||||
error: function(data){
|
||||
if (typeof data.error !== 'undefined') {
|
||||
qError.text("Error loading quarantine item");
|
||||
qError.show();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
jQuery(function($){
|
||||
var qitem = $('legend').data('hash');
|
||||
var qError = $("#qid_error");
|
||||
$.ajax({
|
||||
url: '/inc/ajax/qitem_details.php',
|
||||
data: { hash: qitem },
|
||||
dataType: 'json',
|
||||
success: function(data){
|
||||
$('[data-id="qitems_single"]').each(function(index) {
|
||||
$(this).attr("data-item", qitem);
|
||||
});
|
||||
$('#qid_detail_subj').text(data.subject);
|
||||
$('#qid_detail_hfrom').text(data.header_from);
|
||||
$('#qid_detail_efrom').text(data.env_from);
|
||||
$('#qid_detail_score').html('');
|
||||
$('#qid_detail_symbols').html('');
|
||||
$('#qid_detail_recipients').html('');
|
||||
$('#qid_detail_fuzzy').html('');
|
||||
if (typeof data.fuzzy_hashes === 'object' && data.fuzzy_hashes !== null && data.fuzzy_hashes.length !== 0) {
|
||||
$.each(data.fuzzy_hashes, function (index, value) {
|
||||
$('#qid_detail_fuzzy').append('<p style="font-family:monospace">' + value + '</p>');
|
||||
});
|
||||
} else {
|
||||
$('#qid_detail_fuzzy').append('-');
|
||||
}
|
||||
if (typeof data.symbols !== 'undefined') {
|
||||
data.symbols.sort(function (a, b) {
|
||||
if (a.score === 0) return 1;
|
||||
if (b.score === 0) return -1;
|
||||
if (b.score < 0 && a.score < 0) {
|
||||
return a.score - b.score;
|
||||
}
|
||||
if (b.score > 0 && a.score > 0) {
|
||||
return b.score - a.score;
|
||||
}
|
||||
return b.score - a.score;
|
||||
})
|
||||
$.each(data.symbols, function (index, value) {
|
||||
var highlightClass = '';
|
||||
if (value.score > 0) highlightClass = 'negative';
|
||||
else if (value.score < 0) highlightClass = 'positive';
|
||||
else highlightClass = 'neutral';
|
||||
$('#qid_detail_symbols').append('<span data-bs-toggle="tooltip" class="rspamd-symbol ' + highlightClass + '" title="' + (value.options ? value.options.join(', ') : '') + '">' + value.name + ' (<span class="score">' + value.score + '</span>)</span>');
|
||||
});
|
||||
$('[data-bs-toggle="tooltip"]').tooltip();
|
||||
}
|
||||
if (typeof data.score !== 'undefined' && typeof data.action !== 'undefined') {
|
||||
if (data.action === "add header") {
|
||||
$('#qid_detail_score').append('<span class="label-rspamd-action badge fs-6 bg-warning"><b>' + data.score + '</b> - ' + lang.junk_folder + '</span>');
|
||||
} else if (data.action === "reject") {
|
||||
$('#qid_detail_score').append('<span class="label-rspamd-action badge fs-6 bg-danger"><b>' + data.score + '</b> - ' + lang.rejected + '</span>');
|
||||
} else if (data.action === "rewrite subject") {
|
||||
$('#qid_detail_score').append('<span class="label-rspamd-action badge fs-6 bg-warning"><b>' + data.score + '</b> - ' + lang.rewrite_subject + '</span>');
|
||||
}
|
||||
}
|
||||
if (typeof data.recipients !== 'undefined') {
|
||||
$.each(data.recipients, function(index, value) {
|
||||
var elem = $('<span class="mail-address-item"></span>');
|
||||
elem.text(value.address + ' (' + value.type.toUpperCase() + ')');
|
||||
$('#qid_detail_recipients').append(elem);
|
||||
});
|
||||
}
|
||||
},
|
||||
error: function(data){
|
||||
if (typeof data.error !== 'undefined') {
|
||||
qError.text("Error loading quarantine item");
|
||||
qError.show();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
@@ -1,260 +1,297 @@
|
||||
// Base64 functions
|
||||
var Base64={_keyStr:"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",encode:function(r){var t,e,o,a,h,n,c,d="",C=0;for(r=Base64._utf8_encode(r);C<r.length;)a=(t=r.charCodeAt(C++))>>2,h=(3&t)<<4|(e=r.charCodeAt(C++))>>4,n=(15&e)<<2|(o=r.charCodeAt(C++))>>6,c=63&o,isNaN(e)?n=c=64:isNaN(o)&&(c=64),d=d+this._keyStr.charAt(a)+this._keyStr.charAt(h)+this._keyStr.charAt(n)+this._keyStr.charAt(c);return d},decode:function(r){var t,e,o,a,h,n,c="",d=0;for(r=r.replace(/[^A-Za-z0-9\+\/\=]/g,"");d<r.length;)t=this._keyStr.indexOf(r.charAt(d++))<<2|(a=this._keyStr.indexOf(r.charAt(d++)))>>4,e=(15&a)<<4|(h=this._keyStr.indexOf(r.charAt(d++)))>>2,o=(3&h)<<6|(n=this._keyStr.indexOf(r.charAt(d++))),c+=String.fromCharCode(t),64!=h&&(c+=String.fromCharCode(e)),64!=n&&(c+=String.fromCharCode(o));return c=Base64._utf8_decode(c)},_utf8_encode:function(r){r=r.replace(/\r\n/g,"\n");for(var t="",e=0;e<r.length;e++){var o=r.charCodeAt(e);o<128?t+=String.fromCharCode(o):o>127&&o<2048?(t+=String.fromCharCode(o>>6|192),t+=String.fromCharCode(63&o|128)):(t+=String.fromCharCode(o>>12|224),t+=String.fromCharCode(o>>6&63|128),t+=String.fromCharCode(63&o|128))}return t},_utf8_decode:function(r){for(var t="",e=0,o=c1=c2=0;e<r.length;)(o=r.charCodeAt(e))<128?(t+=String.fromCharCode(o),e++):o>191&&o<224?(c2=r.charCodeAt(e+1),t+=String.fromCharCode((31&o)<<6|63&c2),e+=2):(c2=r.charCodeAt(e+1),c3=r.charCodeAt(e+2),t+=String.fromCharCode((15&o)<<12|(63&c2)<<6|63&c3),e+=3);return t}};
|
||||
|
||||
jQuery(function($){
|
||||
acl_data = JSON.parse(acl);
|
||||
// http://stackoverflow.com/questions/24816/escaping-html-strings-with-jquery
|
||||
var entityMap={"&":"&","<":"<",">":">",'"':""","'":"'","/":"/","`":"`","=":"="};
|
||||
function escapeHtml(n){return String(n).replace(/[&<>"'`=\/]/g,function(n){return entityMap[n]})}
|
||||
function humanFileSize(i){if(Math.abs(i)<1024)return i+" B";var B=["KiB","MiB","GiB","TiB","PiB","EiB","ZiB","YiB"],e=-1;do{i/=1024,++e}while(Math.abs(i)>=1024&&e<B.length-1);return i.toFixed(1)+" "+B[e]}
|
||||
$(".refresh_table").on('click', function(e) {
|
||||
e.preventDefault();
|
||||
var table_name = $(this).data('table');
|
||||
$('#' + table_name).DataTable().ajax.reload();
|
||||
});
|
||||
function draw_quarantine_table() {
|
||||
$('#quarantinetable').DataTable({
|
||||
processing: true,
|
||||
serverSide: false,
|
||||
language: lang_datatables,
|
||||
ajax: {
|
||||
type: "GET",
|
||||
url: "/api/v1/get/quarantine/all",
|
||||
dataSrc: function(data){
|
||||
$.each(data, function (i, item) {
|
||||
if (item.subject === null) {
|
||||
item.subject = '';
|
||||
} else {
|
||||
item.subject = escapeHtml(item.subject);
|
||||
}
|
||||
if (item.score === null) {
|
||||
item.score = '-';
|
||||
}
|
||||
if (item.virus_flag > 0) {
|
||||
item.virus = '<span class="badge fs-6 bg-danger">' + lang.high_danger + '</span>';
|
||||
} else {
|
||||
item.virus = '<span class="badge fs-6 bg-secondary">' + lang.neutral_danger + '</span>';
|
||||
}
|
||||
if (item.action === "reject") {
|
||||
item.rspamdaction = '<span class="badge fs-6 bg-danger">' + lang.rejected + '</span>';
|
||||
} else if (item.action === "add header") {
|
||||
item.rspamdaction = '<span class="badge fs-6 bg-warning">' + lang.junk_folder + '</span>';
|
||||
} else if (item.action === "rewrite subject") {
|
||||
item.rspamdaction = '<span class="badge fs-6 bg-warning">' + lang.rewrite_subject + '</span>';
|
||||
}
|
||||
if(item.notified > 0) {
|
||||
item.notified = '✔';
|
||||
} else {
|
||||
item.notified = '✖';
|
||||
}
|
||||
if (acl_data.login_as === 1) {
|
||||
item.action = '<div class="btn-group">' +
|
||||
'<a href="#" data-item="' + encodeURI(item.id) + '" class="btn btn-xs btn-xs-half btn-info show_qid_info"><i class="bi bi-box-arrow-up-right"></i> ' + lang.show_item + '</a>' +
|
||||
'<a href="#" data-action="delete_selected" data-id="del-single-qitem" data-api-url="delete/qitem" data-item="' + encodeURI(item.id) + '" class="btn btn-xs btn-xs-half btn-danger"><i class="bi bi-trash"></i> ' + lang.remove + '</a>' +
|
||||
'</div>';
|
||||
}
|
||||
else {
|
||||
item.action = '<div class="btn-group">' +
|
||||
'<a href="#" data-item="' + encodeURI(item.id) + '" class="btn btn-xs btn-info show_qid_info"><i class="bi bi-file-earmark-text"></i> ' + lang.show_item + '</a>' +
|
||||
'</div>';
|
||||
}
|
||||
item.chkbox = '<input type="checkbox" data-id="qitems" name="multi_select" value="' + item.id + '" />';
|
||||
});
|
||||
|
||||
return data;
|
||||
}
|
||||
},
|
||||
columns: [
|
||||
{
|
||||
// placeholder, so checkbox will not block child row toggle
|
||||
title: '',
|
||||
data: null,
|
||||
searchable: false,
|
||||
orderable: false,
|
||||
defaultContent: ''
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
data: 'chkbox',
|
||||
searchable: false,
|
||||
orderable: false,
|
||||
defaultContent: ''
|
||||
},
|
||||
{
|
||||
title: 'ID',
|
||||
data: 'id',
|
||||
defaultContent: ''
|
||||
},
|
||||
{
|
||||
title: lang.qid,
|
||||
data: 'qid',
|
||||
defaultContent: ''
|
||||
},
|
||||
{
|
||||
title: lang.sender,
|
||||
data: 'sender',
|
||||
defaultContent: ''
|
||||
},
|
||||
{
|
||||
title: lang.subj,
|
||||
data: 'sender',
|
||||
defaultContent: ''
|
||||
},
|
||||
{
|
||||
title: lang.rspamd_result,
|
||||
data: 'rspamdaction',
|
||||
defaultContent: ''
|
||||
},
|
||||
{
|
||||
title: lang.rcpt,
|
||||
data: 'rcpt',
|
||||
defaultContent: ''
|
||||
},
|
||||
{
|
||||
title: lang.danger,
|
||||
data: 'virus',
|
||||
defaultContent: ''
|
||||
},
|
||||
{
|
||||
title: lang.spam_score,
|
||||
data: 'score',
|
||||
defaultContent: ''
|
||||
},
|
||||
{
|
||||
title: lang.notified,
|
||||
data: 'notified',
|
||||
defaultContent: ''
|
||||
},
|
||||
{
|
||||
title: lang.received,
|
||||
data: 'created',
|
||||
defaultContent: '',
|
||||
render: function (data,type) {
|
||||
var date = new Date(data ? data * 1000 : 0);
|
||||
return date.toLocaleDateString(undefined, {year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit"});
|
||||
}
|
||||
},
|
||||
{
|
||||
title: lang.action,
|
||||
data: 'action',
|
||||
className: 'text-md-end dt-sm-head-hidden dt-body-right',
|
||||
defaultContent: ''
|
||||
},
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
$('body').on('click', '.show_qid_info', function (e) {
|
||||
e.preventDefault();
|
||||
var qitem = $(this).attr('data-item');
|
||||
var qError = $("#qid_error");
|
||||
|
||||
$('#qidDetailModal').modal('show');
|
||||
qError.hide();
|
||||
|
||||
$.ajax({
|
||||
url: '/inc/ajax/qitem_details.php',
|
||||
data: { id: qitem },
|
||||
dataType: 'json',
|
||||
success: function(data){
|
||||
|
||||
$('[data-id="qitems_single"]').each(function(index) {
|
||||
$(this).attr("data-item", qitem);
|
||||
});
|
||||
|
||||
$("#quick_download_link").attr("onclick", "window.open('/inc/ajax/qitem_details.php?id=" + qitem + "&eml', '_blank')");
|
||||
$("#quick_release_link").attr("onclick", "window.open('/inc/ajax/qitem_details.php?id=" + qitem + "&quick_release', '_blank')");
|
||||
$("#quick_delete_link").attr("onclick", "window.open('/inc/ajax/qitem_details.php?id=" + qitem + "&quick_delete', '_blank')");
|
||||
|
||||
$('#qid_detail_subj').text(data.subject);
|
||||
$('#qid_detail_hfrom').text(data.header_from);
|
||||
$('#qid_detail_efrom').text(data.env_from);
|
||||
$('#qid_detail_score').html('');
|
||||
$('#qid_detail_recipients').html('');
|
||||
$('#qid_detail_symbols').html('');
|
||||
$('#qid_detail_fuzzy').html('');
|
||||
if (typeof data.symbols !== 'undefined') {
|
||||
data.symbols.sort(function (a, b) {
|
||||
if (a.score === 0) return 1
|
||||
if (b.score === 0) return -1
|
||||
if (b.score < 0 && a.score < 0) {
|
||||
return a.score - b.score
|
||||
}
|
||||
if (b.score > 0 && a.score > 0) {
|
||||
return b.score - a.score
|
||||
}
|
||||
return b.score - a.score
|
||||
})
|
||||
$.each(data.symbols, function (index, value) {
|
||||
var highlightClass = ''
|
||||
if (value.score > 0) highlightClass = 'negative'
|
||||
else if (value.score < 0) highlightClass = 'positive'
|
||||
else highlightClass = 'neutral'
|
||||
$('#qid_detail_symbols').append('<span data-bs-toggle="tooltip" class="rspamd-symbol ' + highlightClass + '" title="' + (value.options ? value.options.join(', ') : '') + '">' + value.name + ' (<span class="score">' + value.score + '</span>)</span>');
|
||||
});
|
||||
$('[data-bs-toggle="tooltip"]').tooltip()
|
||||
}
|
||||
if (typeof data.fuzzy_hashes === 'object' && data.fuzzy_hashes !== null && data.fuzzy_hashes.length !== 0) {
|
||||
$.each(data.fuzzy_hashes, function (index, value) {
|
||||
$('#qid_detail_fuzzy').append('<p style="font-family:monospace">' + value + '</p>');
|
||||
});
|
||||
} else {
|
||||
$('#qid_detail_fuzzy').append('-');
|
||||
}
|
||||
if (typeof data.score !== 'undefined' && typeof data.action !== 'undefined') {
|
||||
if (data.action == "add header") {
|
||||
$('#qid_detail_score').append('<span class="label-rspamd-action badge fs-6 bg-warning"><b>' + data.score + '</b> - ' + lang.junk_folder + '</span>');
|
||||
} else if (data.action == "reject") {
|
||||
$('#qid_detail_score').append('<span class="label-rspamd-action badge fs-6 bg-danger"><b>' + data.score + '</b> - ' + lang.rejected + '</span>');
|
||||
} else if (data.action == "rewrite subject") {
|
||||
$('#qid_detail_score').append('<span class="label-rspamd-action badge fs-6 bg-warning"><b>' + data.score + '</b> - ' + lang.rewrite_subject + '</span>');
|
||||
}
|
||||
}
|
||||
if (typeof data.recipients !== 'undefined') {
|
||||
$.each(data.recipients, function(index, value) {
|
||||
var elem = $('<span class="mail-address-item"></span>');
|
||||
elem.text(value.address + ' (' + value.type.toUpperCase() + ')');
|
||||
$('#qid_detail_recipients').append(elem);
|
||||
});
|
||||
}
|
||||
$('#qid_detail_text').text(data.text_plain);
|
||||
$('#qid_detail_text_from_html').text(data.text_html);
|
||||
var qAtts = $("#qid_detail_atts");
|
||||
if (typeof data.attachments !== 'undefined') {
|
||||
qAtts.text('');
|
||||
$.each(data.attachments, function(index, value) {
|
||||
qAtts.append(
|
||||
'<p><a href="/inc/ajax/qitem_details.php?id=' + qitem + '&att=' + index + '" target="_blank">' + value[0] + '</a> (' + value[1] + ')' +
|
||||
' - <small><a href="' + value[3] + '" target="_blank">' + lang.check_hash + '</a></small></p>'
|
||||
);
|
||||
});
|
||||
}
|
||||
else {
|
||||
qAtts.text('-');
|
||||
}
|
||||
},
|
||||
error: function(data){
|
||||
if (typeof data.error !== 'undefined') {
|
||||
$('#qid_detail_subj').text('-');
|
||||
$('#qid_detail_hfrom').text('-');
|
||||
$('#qid_detail_efrom').text('-');
|
||||
$('#qid_detail_score').html('-');
|
||||
$('#qid_detail_recipients').html('-');
|
||||
$('#qid_detail_symbols').html('-');
|
||||
$('#qid_detail_fuzzy').html('-');
|
||||
$('#qid_detail_text').text('-');
|
||||
$('#qid_detail_text_from_html').text('-');
|
||||
qError.text("Error loading quarantine item");
|
||||
qError.show();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$('body').on('click', 'span.footable-toggle', function () {
|
||||
event.stopPropagation();
|
||||
})
|
||||
|
||||
// Initial table drawings
|
||||
draw_quarantine_table();
|
||||
});
|
||||
// Base64 functions
|
||||
var Base64={_keyStr:"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",encode:function(r){var t,e,o,a,h,n,c,d="",C=0;for(r=Base64._utf8_encode(r);C<r.length;)a=(t=r.charCodeAt(C++))>>2,h=(3&t)<<4|(e=r.charCodeAt(C++))>>4,n=(15&e)<<2|(o=r.charCodeAt(C++))>>6,c=63&o,isNaN(e)?n=c=64:isNaN(o)&&(c=64),d=d+this._keyStr.charAt(a)+this._keyStr.charAt(h)+this._keyStr.charAt(n)+this._keyStr.charAt(c);return d},decode:function(r){var t,e,o,a,h,n,c="",d=0;for(r=r.replace(/[^A-Za-z0-9\+\/\=]/g,"");d<r.length;)t=this._keyStr.indexOf(r.charAt(d++))<<2|(a=this._keyStr.indexOf(r.charAt(d++)))>>4,e=(15&a)<<4|(h=this._keyStr.indexOf(r.charAt(d++)))>>2,o=(3&h)<<6|(n=this._keyStr.indexOf(r.charAt(d++))),c+=String.fromCharCode(t),64!=h&&(c+=String.fromCharCode(e)),64!=n&&(c+=String.fromCharCode(o));return c=Base64._utf8_decode(c)},_utf8_encode:function(r){r=r.replace(/\r\n/g,"\n");for(var t="",e=0;e<r.length;e++){var o=r.charCodeAt(e);o<128?t+=String.fromCharCode(o):o>127&&o<2048?(t+=String.fromCharCode(o>>6|192),t+=String.fromCharCode(63&o|128)):(t+=String.fromCharCode(o>>12|224),t+=String.fromCharCode(o>>6&63|128),t+=String.fromCharCode(63&o|128))}return t},_utf8_decode:function(r){for(var t="",e=0,o=c1=c2=0;e<r.length;)(o=r.charCodeAt(e))<128?(t+=String.fromCharCode(o),e++):o>191&&o<224?(c2=r.charCodeAt(e+1),t+=String.fromCharCode((31&o)<<6|63&c2),e+=2):(c2=r.charCodeAt(e+1),c3=r.charCodeAt(e+2),t+=String.fromCharCode((15&o)<<12|(63&c2)<<6|63&c3),e+=3);return t}};
|
||||
|
||||
jQuery(function($){
|
||||
acl_data = JSON.parse(acl);
|
||||
// http://stackoverflow.com/questions/24816/escaping-html-strings-with-jquery
|
||||
var entityMap={"&":"&","<":"<",">":">",'"':""","'":"'","/":"/","`":"`","=":"="};
|
||||
function escapeHtml(n){return String(n).replace(/[&<>"'`=\/]/g,function(n){return entityMap[n]})}
|
||||
function humanFileSize(i){if(Math.abs(i)<1024)return i+" B";var B=["KiB","MiB","GiB","TiB","PiB","EiB","ZiB","YiB"],e=-1;do{i/=1024,++e}while(Math.abs(i)>=1024&&e<B.length-1);return i.toFixed(1)+" "+B[e]}
|
||||
$(".refresh_table").on('click', function(e) {
|
||||
e.preventDefault();
|
||||
var table_name = $(this).data('table');
|
||||
$('#' + table_name).DataTable().ajax.reload();
|
||||
});
|
||||
function draw_quarantine_table() {
|
||||
var table = $('#quarantinetable').DataTable({
|
||||
responsive: true,
|
||||
processing: true,
|
||||
serverSide: false,
|
||||
stateSave: true,
|
||||
pageLength: pagination_size,
|
||||
order: [[2, 'desc']],
|
||||
lengthMenu: [
|
||||
[10, 25, 50, 100, -1],
|
||||
[10, 25, 50, 100, 'all']
|
||||
],
|
||||
pagingType: 'first_last_numbers',
|
||||
aColumns: [
|
||||
{ sWidth: '8.25%' },
|
||||
{ sClass: 'classDataTable' }
|
||||
],
|
||||
dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" +
|
||||
"tr" +
|
||||
"<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
|
||||
language: lang_datatables,
|
||||
initComplete: function(){
|
||||
hideTableExpandCollapseBtn('#quarantinetable');
|
||||
},
|
||||
ajax: {
|
||||
type: "GET",
|
||||
url: "/api/v1/get/quarantine/all",
|
||||
dataSrc: function(data){
|
||||
$.each(data, function (i, item) {
|
||||
if (item.subject === null) {
|
||||
item.subject = '';
|
||||
} else {
|
||||
item.subject = escapeHtml(item.subject);
|
||||
}
|
||||
if (item.score === null) {
|
||||
item.score = '-';
|
||||
}
|
||||
if (item.virus_flag > 0) {
|
||||
item.virus = '<span class="badge fs-6 bg-danger">' + lang.high_danger + '</span>';
|
||||
} else {
|
||||
item.virus = '<span class="badge fs-6 bg-secondary">' + lang.neutral_danger + '</span>';
|
||||
}
|
||||
if (item.action === "reject") {
|
||||
item.rspamdaction = '<span class="badge fs-6 bg-danger">' + lang.rejected + '</span>';
|
||||
} else if (item.action === "add header") {
|
||||
item.rspamdaction = '<span class="badge fs-6 bg-warning">' + lang.junk_folder + '</span>';
|
||||
} else if (item.action === "rewrite subject") {
|
||||
item.rspamdaction = '<span class="badge fs-6 bg-warning">' + lang.rewrite_subject + '</span>';
|
||||
}
|
||||
if(item.notified > 0) {
|
||||
item.notified = '✔';
|
||||
} else {
|
||||
item.notified = '✖';
|
||||
}
|
||||
if (acl_data.login_as === 1) {
|
||||
item.action = '<div class="btn-group">' +
|
||||
'<a href="#" data-item="' + encodeURI(item.id) + '" class="btn btn-xs btn-xs-half btn-info show_qid_info"><i class="bi bi-box-arrow-up-right"></i> ' + lang.show_item + '</a>' +
|
||||
'<a href="#" data-action="delete_selected" data-id="del-single-qitem" data-api-url="delete/qitem" data-item="' + encodeURI(item.id) + '" class="btn btn-xs btn-xs-half btn-danger"><i class="bi bi-trash"></i> ' + lang.remove + '</a>' +
|
||||
'</div>';
|
||||
}
|
||||
else {
|
||||
item.action = '<div class="btn-group">' +
|
||||
'<a href="#" data-item="' + encodeURI(item.id) + '" class="btn btn-xs btn-info show_qid_info"><i class="bi bi-file-earmark-text"></i> ' + lang.show_item + '</a>' +
|
||||
'</div>';
|
||||
}
|
||||
item.chkbox = '<input type="checkbox" data-id="qitems" name="multi_select" value="' + item.id + '" />';
|
||||
});
|
||||
|
||||
return data;
|
||||
}
|
||||
},
|
||||
columns: [
|
||||
{
|
||||
// placeholder, so checkbox will not block child row toggle
|
||||
title: '',
|
||||
data: null,
|
||||
searchable: false,
|
||||
orderable: false,
|
||||
defaultContent: ''
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
data: 'chkbox',
|
||||
searchable: false,
|
||||
orderable: false,
|
||||
defaultContent: ''
|
||||
},
|
||||
{
|
||||
title: 'ID',
|
||||
data: 'id',
|
||||
defaultContent: ''
|
||||
},
|
||||
{
|
||||
title: lang.qid,
|
||||
data: 'qid',
|
||||
defaultContent: ''
|
||||
},
|
||||
{
|
||||
title: lang.sender,
|
||||
data: 'sender',
|
||||
className: 'senders-mw220',
|
||||
defaultContent: ''
|
||||
},
|
||||
{
|
||||
title: lang.subj,
|
||||
data: 'subject',
|
||||
defaultContent: ''
|
||||
},
|
||||
{
|
||||
title: lang.rspamd_result,
|
||||
data: 'rspamdaction',
|
||||
defaultContent: ''
|
||||
},
|
||||
{
|
||||
title: lang.rcpt,
|
||||
data: 'rcpt',
|
||||
defaultContent: ''
|
||||
},
|
||||
{
|
||||
title: lang.danger,
|
||||
data: 'virus',
|
||||
defaultContent: ''
|
||||
},
|
||||
{
|
||||
title: lang.spam_score,
|
||||
data: 'score',
|
||||
defaultContent: ''
|
||||
},
|
||||
{
|
||||
title: lang.notified,
|
||||
data: 'notified',
|
||||
defaultContent: ''
|
||||
},
|
||||
{
|
||||
title: lang.received,
|
||||
data: 'created',
|
||||
defaultContent: '',
|
||||
createdCell: function(td, cellData) {
|
||||
$(td).attr({
|
||||
"data-order": cellData,
|
||||
"data-sort": cellData
|
||||
});
|
||||
|
||||
var date = new Date(cellData ? cellData * 1000 : 0);
|
||||
var dateString = date.toLocaleDateString(undefined, {year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit"});
|
||||
$(td).html(dateString);
|
||||
}
|
||||
},
|
||||
{
|
||||
title: lang.action,
|
||||
data: 'action',
|
||||
className: 'dt-text-right dt-sm-head-hidden',
|
||||
defaultContent: ''
|
||||
},
|
||||
]
|
||||
});
|
||||
|
||||
table.on('responsive-resize', function (e, datatable, columns){
|
||||
hideTableExpandCollapseBtn('#quarantinetable');
|
||||
});
|
||||
}
|
||||
|
||||
$('body').on('click', '.show_qid_info', function (e) {
|
||||
e.preventDefault();
|
||||
var qitem = $(this).attr('data-item');
|
||||
var qError = $("#qid_error");
|
||||
|
||||
$('#qidDetailModal').modal('show');
|
||||
qError.hide();
|
||||
|
||||
$.ajax({
|
||||
url: '/inc/ajax/qitem_details.php',
|
||||
data: { id: qitem },
|
||||
dataType: 'json',
|
||||
success: function(data){
|
||||
|
||||
$('[data-id="qitems_single"]').each(function(index) {
|
||||
$(this).attr("data-item", qitem);
|
||||
});
|
||||
|
||||
$("#quick_download_link").attr("onclick", "window.open('/inc/ajax/qitem_details.php?id=" + qitem + "&eml', '_blank')");
|
||||
$("#quick_release_link").attr("onclick", "window.open('/inc/ajax/qitem_details.php?id=" + qitem + "&quick_release', '_blank')");
|
||||
$("#quick_delete_link").attr("onclick", "window.open('/inc/ajax/qitem_details.php?id=" + qitem + "&quick_delete', '_blank')");
|
||||
|
||||
$('#qid_detail_subj').text(data.subject);
|
||||
$('#qid_detail_hfrom').text(data.header_from);
|
||||
$('#qid_detail_efrom').text(data.env_from);
|
||||
$('#qid_detail_score').html('');
|
||||
$('#qid_detail_recipients').html('');
|
||||
$('#qid_detail_symbols').html('');
|
||||
$('#qid_detail_fuzzy').html('');
|
||||
if (typeof data.symbols !== 'undefined') {
|
||||
data.symbols.sort(function (a, b) {
|
||||
if (a.score === 0) return 1;
|
||||
if (b.score === 0) return -1;
|
||||
if (b.score < 0 && a.score < 0) {
|
||||
return a.score - b.score;
|
||||
}
|
||||
if (b.score > 0 && a.score > 0) {
|
||||
return b.score - a.score;
|
||||
}
|
||||
return b.score - a.score;
|
||||
})
|
||||
$.each(data.symbols, function (index, value) {
|
||||
var highlightClass = '';
|
||||
if (value.score > 0) highlightClass = 'negative';
|
||||
else if (value.score < 0) highlightClass = 'positive';
|
||||
else highlightClass = 'neutral';
|
||||
$('#qid_detail_symbols').append('<span data-bs-toggle="tooltip" class="rspamd-symbol ' + highlightClass + '" title="' + (value.options ? value.options.join(', ') : '') + '">' + value.name + ' (<span class="score">' + value.score + '</span>)</span>');
|
||||
});
|
||||
$('[data-bs-toggle="tooltip"]').tooltip();
|
||||
}
|
||||
if (typeof data.fuzzy_hashes === 'object' && data.fuzzy_hashes !== null && data.fuzzy_hashes.length !== 0) {
|
||||
$.each(data.fuzzy_hashes, function (index, value) {
|
||||
$('#qid_detail_fuzzy').append('<p style="font-family:monospace">' + value + '</p>');
|
||||
});
|
||||
} else {
|
||||
$('#qid_detail_fuzzy').append('-');
|
||||
}
|
||||
if (typeof data.score !== 'undefined' && typeof data.action !== 'undefined') {
|
||||
if (data.action == "add header") {
|
||||
$('#qid_detail_score').append('<span class="label-rspamd-action badge fs-6 bg-warning"><b>' + data.score + '</b> - ' + lang.junk_folder + '</span>');
|
||||
} else if (data.action == "reject") {
|
||||
$('#qid_detail_score').append('<span class="label-rspamd-action badge fs-6 bg-danger"><b>' + data.score + '</b> - ' + lang.rejected + '</span>');
|
||||
} else if (data.action == "rewrite subject") {
|
||||
$('#qid_detail_score').append('<span class="label-rspamd-action badge fs-6 bg-warning"><b>' + data.score + '</b> - ' + lang.rewrite_subject + '</span>');
|
||||
}
|
||||
}
|
||||
if (typeof data.recipients !== 'undefined') {
|
||||
$.each(data.recipients, function(index, value) {
|
||||
var elem = $('<span class="mail-address-item"></span>');
|
||||
elem.text(value.address + ' (' + value.type.toUpperCase() + ')');
|
||||
$('#qid_detail_recipients').append(elem);
|
||||
});
|
||||
}
|
||||
$('#qid_detail_text').text(data.text_plain);
|
||||
$('#qid_detail_text_from_html').text(data.text_html);
|
||||
var qAtts = $("#qid_detail_atts");
|
||||
if (typeof data.attachments !== 'undefined') {
|
||||
qAtts.text('');
|
||||
$.each(data.attachments, function(index, value) {
|
||||
qAtts.append(
|
||||
'<p><a href="/inc/ajax/qitem_details.php?id=' + qitem + '&att=' + index + '" target="_blank">' + value[0] + '</a> (' + value[1] + ')' +
|
||||
' - <small><a href="' + value[3] + '" target="_blank">' + lang.check_hash + '</a></small></p>'
|
||||
);
|
||||
});
|
||||
}
|
||||
else {
|
||||
qAtts.text('-');
|
||||
}
|
||||
},
|
||||
error: function(data){
|
||||
if (typeof data.error !== 'undefined') {
|
||||
$('#qid_detail_subj').text('-');
|
||||
$('#qid_detail_hfrom').text('-');
|
||||
$('#qid_detail_efrom').text('-');
|
||||
$('#qid_detail_score').html('-');
|
||||
$('#qid_detail_recipients').html('-');
|
||||
$('#qid_detail_symbols').html('-');
|
||||
$('#qid_detail_fuzzy').html('-');
|
||||
$('#qid_detail_text').text('-');
|
||||
$('#qid_detail_text_from_html').text('-');
|
||||
qError.text("Error loading quarantine item");
|
||||
qError.show();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$('body').on('click', 'span.footable-toggle', function () {
|
||||
event.stopPropagation();
|
||||
})
|
||||
|
||||
// Initial table drawings
|
||||
draw_quarantine_table();
|
||||
|
||||
function hideTableExpandCollapseBtn(table){
|
||||
if ($(table).hasClass('collapsed'))
|
||||
$(".table_collapse_option").show();
|
||||
else
|
||||
$(".table_collapse_option").hide();
|
||||
}
|
||||
});
|
||||
|
@@ -1,123 +1,128 @@
|
||||
jQuery(function($){
|
||||
|
||||
$(".refresh_table").on('click', function(e) {
|
||||
e.preventDefault();
|
||||
var table_name = $(this).data('table');
|
||||
$('#' + table_name).DataTable().ajax.reload();
|
||||
});
|
||||
$(".refresh_table").on('click', function(e) {
|
||||
e.preventDefault();
|
||||
var table_name = $(this).data('table');
|
||||
$('#' + table_name).DataTable().ajax.reload();
|
||||
});
|
||||
|
||||
|
||||
function humanFileSize(i){if(Math.abs(i)<1024)return i+" B";var B=["KiB","MiB","GiB","TiB","PiB","EiB","ZiB","YiB"],e=-1;do{i/=1024,++e}while(Math.abs(i)>=1024&&e<B.length-1);return i.toFixed(1)+" "+B[e]}
|
||||
function humanFileSize(i){if(Math.abs(i)<1024)return i+" B";var B=["KiB","MiB","GiB","TiB","PiB","EiB","ZiB","YiB"],e=-1;do{i/=1024,++e}while(Math.abs(i)>=1024&&e<B.length-1);return i.toFixed(1)+" "+B[e]}
|
||||
|
||||
// Queue item
|
||||
$('#showQueuedMsg').on('show.bs.modal', function (e) {
|
||||
$('#queue_msg_content').text(lang.loading);
|
||||
button = $(e.relatedTarget)
|
||||
if (button != null) {
|
||||
$('#queue_id').text(button.data('queue-id'));
|
||||
}
|
||||
$.ajax({
|
||||
type: 'GET',
|
||||
url: '/api/v1/get/postcat/' + button.data('queue-id'),
|
||||
dataType: 'text',
|
||||
complete: function (data) {
|
||||
console.log(data);
|
||||
$('#queue_msg_content').text(data.responseText);
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
function draw_queue() {
|
||||
// just recalc width if instance already exists
|
||||
if ($.fn.DataTable.isDataTable('#queuetable') ) {
|
||||
$('#queuetable').DataTable().columns.adjust().responsive.recalc();
|
||||
return;
|
||||
// Queue item
|
||||
$('#showQueuedMsg').on('show.bs.modal', function (e) {
|
||||
$('#queue_msg_content').text(lang.loading);
|
||||
button = $(e.relatedTarget)
|
||||
if (button != null) {
|
||||
$('#queue_id').text(button.data('queue-id'));
|
||||
}
|
||||
$.ajax({
|
||||
type: 'GET',
|
||||
url: '/api/v1/get/postcat/' + button.data('queue-id'),
|
||||
dataType: 'text',
|
||||
complete: function (data) {
|
||||
$('#queue_msg_content').text(data.responseText);
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
$('#queuetable').DataTable({
|
||||
processing: true,
|
||||
serverSide: false,
|
||||
language: lang_datatables,
|
||||
ajax: {
|
||||
type: "GET",
|
||||
url: "/api/v1/get/mailq/all",
|
||||
dataSrc: function(data){
|
||||
$.each(data, function (i, item) {
|
||||
item.chkbox = '<input type="checkbox" data-id="mailqitems" name="multi_select" value="' + item.queue_id + '" />';
|
||||
rcpts = $.map(item.recipients, function(i) {
|
||||
return escapeHtml(i);
|
||||
});
|
||||
item.recipients = rcpts.join('<hr style="margin:1px!important">');
|
||||
item.action = '<div class="btn-group">' +
|
||||
'<a href="#" data-bs-toggle="modal" data-bs-target="#showQueuedMsg" data-queue-id="' + encodeURI(item.queue_id) + '" class="btn btn-xs btn-secondary">' + lang.queue_show_message + '</a>' +
|
||||
function draw_queue() {
|
||||
// just recalc width if instance already exists
|
||||
if ($.fn.DataTable.isDataTable('#queuetable') ) {
|
||||
$('#queuetable').DataTable().columns.adjust().responsive.recalc();
|
||||
return;
|
||||
}
|
||||
|
||||
$('#queuetable').DataTable({
|
||||
responsive: true,
|
||||
processing: true,
|
||||
serverSide: false,
|
||||
stateSave: true,
|
||||
pageLength: pagination_size,
|
||||
dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" +
|
||||
"tr" +
|
||||
"<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
|
||||
language: lang_datatables,
|
||||
ajax: {
|
||||
type: "GET",
|
||||
url: "/api/v1/get/mailq/all",
|
||||
dataSrc: function(data){
|
||||
$.each(data, function (i, item) {
|
||||
item.chkbox = '<input type="checkbox" data-id="mailqitems" name="multi_select" value="' + item.queue_id + '" />';
|
||||
rcpts = $.map(item.recipients, function(i) {
|
||||
return escapeHtml(i);
|
||||
});
|
||||
item.recipients = rcpts.join('<hr style="margin:1px!important">');
|
||||
item.action = '<div class="btn-group">' +
|
||||
'<a href="#" data-bs-toggle="modal" data-bs-target="#showQueuedMsg" data-queue-id="' + encodeURI(item.queue_id) + '" class="btn btn-xs btn-secondary">' + lang.show_message + '</a>' +
|
||||
'</div>';
|
||||
});
|
||||
return data;
|
||||
}
|
||||
},
|
||||
columns: [
|
||||
{
|
||||
// placeholder, so checkbox will not block child row toggle
|
||||
title: '',
|
||||
data: null,
|
||||
searchable: false,
|
||||
orderable: false,
|
||||
defaultContent: ''
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
data: 'chkbox',
|
||||
searchable: false,
|
||||
orderable: false,
|
||||
defaultContent: ''
|
||||
},
|
||||
{
|
||||
title: 'QID',
|
||||
data: 'queue_id',
|
||||
defaultContent: ''
|
||||
},
|
||||
{
|
||||
title: 'Queue',
|
||||
data: 'queue_name',
|
||||
defaultContent: ''
|
||||
},
|
||||
{
|
||||
title: lang_admin.arrival_time,
|
||||
data: 'arrival_time',
|
||||
defaultContent: '',
|
||||
render: function (data, type){
|
||||
var date = new Date(data ? data * 1000 : 0);
|
||||
return date.toLocaleDateString(undefined, {year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit"});
|
||||
}
|
||||
},
|
||||
{
|
||||
title: lang_admin.message_size,
|
||||
data: 'message_size',
|
||||
defaultContent: '',
|
||||
render: function (data, type){
|
||||
return humanFileSize(data);
|
||||
}
|
||||
},
|
||||
{
|
||||
title: lang_admin.sender,
|
||||
data: 'sender',
|
||||
defaultContent: ''
|
||||
},
|
||||
{
|
||||
title: lang_admin.recipients,
|
||||
data: 'recipients',
|
||||
defaultContent: ''
|
||||
},
|
||||
{
|
||||
title: lang_admin.action,
|
||||
data: 'action',
|
||||
className: 'text-md-end dt-sm-head-hidden dt-body-right',
|
||||
defaultContent: ''
|
||||
},
|
||||
{
|
||||
// placeholder, so checkbox will not block child row toggle
|
||||
title: '',
|
||||
data: null,
|
||||
searchable: false,
|
||||
orderable: false,
|
||||
defaultContent: ''
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
data: 'chkbox',
|
||||
searchable: false,
|
||||
orderable: false,
|
||||
defaultContent: ''
|
||||
},
|
||||
{
|
||||
title: 'QID',
|
||||
data: 'queue_id',
|
||||
defaultContent: ''
|
||||
},
|
||||
{
|
||||
title: 'Queue',
|
||||
data: 'queue_name',
|
||||
defaultContent: ''
|
||||
},
|
||||
{
|
||||
title: lang_admin.arrival_time,
|
||||
data: 'arrival_time',
|
||||
defaultContent: '',
|
||||
render: function (data, type){
|
||||
var date = new Date(data ? data * 1000 : 0);
|
||||
return date.toLocaleDateString(undefined, {year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit"});
|
||||
}
|
||||
},
|
||||
{
|
||||
title: lang_admin.message_size,
|
||||
data: 'message_size',
|
||||
defaultContent: '',
|
||||
render: function (data, type){
|
||||
return humanFileSize(data);
|
||||
}
|
||||
},
|
||||
{
|
||||
title: lang_admin.sender,
|
||||
data: 'sender',
|
||||
defaultContent: ''
|
||||
},
|
||||
{
|
||||
title: lang_admin.recipients,
|
||||
data: 'recipients',
|
||||
defaultContent: ''
|
||||
},
|
||||
{
|
||||
title: lang_admin.action,
|
||||
data: 'action',
|
||||
className: 'dt-sm-head-hidden dt-text-right',
|
||||
defaultContent: ''
|
||||
},
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
draw_queue();
|
||||
|
||||
})
|
||||
})
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -288,6 +288,26 @@ if (isset($_GET['query'])) {
|
||||
case "domain-admin":
|
||||
process_add_return(domain_admin('add', $attr));
|
||||
break;
|
||||
case "sso":
|
||||
switch ($object) {
|
||||
case "domain-admin":
|
||||
$data = domain_admin_sso('issue', $attr);
|
||||
if($data) {
|
||||
echo json_encode($data);
|
||||
exit(0);
|
||||
}
|
||||
process_add_return($data);
|
||||
break;
|
||||
case "mailbox":
|
||||
$data = mailbox_sso('issue', $attr);
|
||||
if($data) {
|
||||
echo json_encode($data);
|
||||
exit(0);
|
||||
}
|
||||
process_add_return($data);
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case "admin":
|
||||
process_add_return(admin('add', $attr));
|
||||
break;
|
||||
@@ -561,6 +581,15 @@ if (isset($_GET['query'])) {
|
||||
echo '{}';
|
||||
}
|
||||
break;
|
||||
default:
|
||||
$password_complexity_rules = password_complexity('get');
|
||||
if ($password_complexity_rules !== false) {
|
||||
process_get_return($password_complexity_rules);
|
||||
}
|
||||
else {
|
||||
echo '{}';
|
||||
}
|
||||
break;
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -1544,14 +1573,15 @@ if (isset($_GET['query'])) {
|
||||
}
|
||||
else if ($extra == "ip") {
|
||||
// get public ips
|
||||
|
||||
$curl = curl_init();
|
||||
curl_setopt($curl, CURLOPT_URL, 'http://ipv4.mailcow.email');
|
||||
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
|
||||
curl_setopt($curl, CURLOPT_POST, 0);
|
||||
curl_setopt($curl, CURLOPT_CONNECTTIMEOUT, 10);
|
||||
curl_setopt($curl, CURLOPT_TIMEOUT, 15);
|
||||
curl_setopt($curl, CURLOPT_URL, 'http://ipv4.mailcow.email');
|
||||
$ipv4 = curl_exec($curl);
|
||||
curl_setopt($curl, CURLOPT_URL, 'http://ipv6.mailcow.email');
|
||||
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
|
||||
curl_setopt($curl, CURLOPT_POST, 0);
|
||||
$ipv6 = curl_exec($curl);
|
||||
$ips = array(
|
||||
"ipv4" => $ipv4,
|
||||
@@ -1913,6 +1943,9 @@ if (isset($_GET['query'])) {
|
||||
case "ui_texts":
|
||||
process_edit_return(customize('edit', 'ui_texts', $attr));
|
||||
break;
|
||||
case "ip_check":
|
||||
process_edit_return(customize('edit', 'ip_check', $attr));
|
||||
break;
|
||||
case "self":
|
||||
if ($_SESSION['mailcow_cc_role'] == "domainadmin") {
|
||||
process_edit_return(domain_admin('edit', $attr));
|
||||
|
@@ -393,7 +393,6 @@
|
||||
"toggle_all": "Marcar tots"
|
||||
},
|
||||
"queue": {
|
||||
"queue_command_success": "Queue command completed successfully",
|
||||
"queue_manager": "Queue Manager"
|
||||
},
|
||||
"start": {
|
||||
|
@@ -105,7 +105,8 @@
|
||||
"timeout2": "Časový limit pro připojení k lokálnímu serveru",
|
||||
"username": "Uživatelské jméno",
|
||||
"validate": "Ověřit",
|
||||
"validation_success": "Úspěšně ověřeno"
|
||||
"validation_success": "Úspěšně ověřeno",
|
||||
"tags": "Štítky"
|
||||
},
|
||||
"admin": {
|
||||
"access": "Přístupy",
|
||||
@@ -333,7 +334,11 @@
|
||||
"username": "Uživatelské jméno",
|
||||
"validate_license_now": "Ověřit GUID na licenčním serveru",
|
||||
"verify": "Ověřit",
|
||||
"yes": "✓"
|
||||
"yes": "✓",
|
||||
"f2b_ban_time_increment": "Délka banu je prodlužována s každým dalším banem",
|
||||
"f2b_max_ban_time": "Maximální délka banu (s)",
|
||||
"ip_check": "Kontrola IP",
|
||||
"ip_check_disabled": "Kontrola IP je vypnuta. Můžete ji zapnout v <br> <strong>System > Nastavení > Options > Přizpůsobení</strong>"
|
||||
},
|
||||
"danger": {
|
||||
"access_denied": "Přístup odepřen nebo jsou neplatná data ve formuláři",
|
||||
@@ -536,7 +541,7 @@
|
||||
"inactive": "Neaktivní",
|
||||
"kind": "Druh",
|
||||
"last_modified": "Naposledy změněn",
|
||||
"lookup_mx": "Cíl je regulární výraz který se shoduje s MX záznamem (<code>.*google\\.com</code> směřuje veškerou poštu na MX které jsou cílem pro google.com přes tento skok)",
|
||||
"lookup_mx": "Cíl je regulární výraz který se shoduje s MX záznamem (<code>.*\\.google\\.com</code> směřuje veškerou poštu na MX které jsou cílem pro google.com přes tento skok)",
|
||||
"mailbox": "Úprava mailové schránky",
|
||||
"mailbox_quota_def": "Výchozí kvóta schránky",
|
||||
"mailbox_relayhost_info": "Aplikované jen na uživatelskou schránku a přímé aliasy, přepisuje předávající server domény.",
|
||||
@@ -650,7 +655,7 @@
|
||||
},
|
||||
"login": {
|
||||
"delayed": "Přihlášení zpožděno o %s sekund.",
|
||||
"fido2_webauthn": "FIDO2/WebAuthn",
|
||||
"fido2_webauthn": "FIDO2/WebAuthn Login",
|
||||
"login": "Přihlásit",
|
||||
"mobileconfig_info": "Ke stažení profilového souboru se přihlaste jako uživatel schránky.",
|
||||
"other_logins": "Přihlášení klíčem",
|
||||
@@ -889,7 +894,6 @@
|
||||
"type": "Typ"
|
||||
},
|
||||
"queue": {
|
||||
"queue_deliver_mail": "Doručit",
|
||||
"queue_manager": "Správce fronty"
|
||||
},
|
||||
"ratelimit": {
|
||||
|
@@ -1,18 +1,18 @@
|
||||
{
|
||||
"acl": {
|
||||
"alias_domains": "Tilføj kældenavn domæner",
|
||||
"alias_domains": "Tilføj domænealias",
|
||||
"app_passwds": "Administrer app-adgangskoder",
|
||||
"bcc_maps": "BCC kort",
|
||||
"delimiter_action": "Afgrænsning handling",
|
||||
"eas_reset": "Nulstil EAS endheder",
|
||||
"eas_reset": "Nulstil EAS enheder",
|
||||
"extend_sender_acl": "Tillad at udvide afsenderens ACL med eksterne adresser",
|
||||
"filters": "Filtre",
|
||||
"login_as": "Login som mailboks bruger",
|
||||
"prohibited": "Forbudt af ACL",
|
||||
"protocol_access": "Ændre protokol adgang",
|
||||
"prohibited": "Nægtet af ACL",
|
||||
"protocol_access": "Skift protokol adgang",
|
||||
"pushover": "Pushover",
|
||||
"quarantine": "Karantæneaktioner",
|
||||
"quarantine_attachments": "Karantæne vedhæftede filer",
|
||||
"quarantine": "Karantænehandlinger",
|
||||
"quarantine_attachments": "Karantænevedhæftede filer",
|
||||
"quarantine_notification": "Skift karantænemeddelelser",
|
||||
"ratelimit": "Satsgrænse",
|
||||
"recipient_maps": "Modtagerkort",
|
||||
@@ -20,12 +20,15 @@
|
||||
"sogo_access": "Tillad styring af SOGo-adgang",
|
||||
"sogo_profile_reset": "Nulstil SOGo-profil",
|
||||
"spam_alias": "Midlertidige aliasser",
|
||||
"spam_policy": "Sortliste / hvidliste",
|
||||
"spam_policy": "Sortliste/hvidliste",
|
||||
"spam_score": "Spam-score",
|
||||
"syncjobs": "Synkroniser job",
|
||||
"syncjobs": "Synkroniserings job",
|
||||
"tls_policy": "TLS politik",
|
||||
"unlimited_quota": "Ubegrænset quote for mailbokse",
|
||||
"domain_desc": "Skift domæne beskrivelse"
|
||||
"unlimited_quota": "Ubegrænset plads for mailbokse",
|
||||
"domain_desc": "Skift domæne beskrivelse",
|
||||
"domain_relayhost": "Skift relæ host for et domæne",
|
||||
"mailbox_relayhost": "Skift relæ-host for en postkasse",
|
||||
"quarantine_category": "Skift kategorien for karantænemeddelelse"
|
||||
},
|
||||
"add": {
|
||||
"activate_filter_warn": "Alle andre filtre deaktiveres, når aktiv er markeret.",
|
||||
@@ -33,7 +36,7 @@
|
||||
"add": "Tilføj",
|
||||
"add_domain_only": "Tilføj kun domæne",
|
||||
"add_domain_restart": "Tilføj domæne og genstart SOGo",
|
||||
"alias_address": "Alias adresse (r)",
|
||||
"alias_address": "Alias adresse(r)",
|
||||
"alias_address_info": "<small>Fuld e-mail-adresse eller @ eksempel.com for at fange alle beskeder til et domæne (kommasepareret). <b> kun mailcow-domæner</b>.</small>",
|
||||
"alias_domain": "Alias-domæne",
|
||||
"alias_domain_info": "<small>Kun gyldige domænenavne (kommasepareret).</small>",
|
||||
@@ -59,7 +62,7 @@
|
||||
"gal": "Global adresseliste",
|
||||
"gal_info": "GAL indeholder alle objekter i et domæne og kan ikke redigeres af nogen bruger. Information om ledig / optaget i SOGo mangler, hvis deaktiveret! <b> Genstart SOGo for at anvende ændringer. </b>",
|
||||
"generate": "generere",
|
||||
"goto_ham": "Lær som <span class=\"text-success\"><b>ham</b></span>",
|
||||
"goto_ham": "Lær som <span class=\"text-success\"><b>ønsket</b></span>",
|
||||
"goto_null": "Kassér e-mail i stilhed",
|
||||
"goto_spam": "Lær som <span class=\"text-danger\"><b>spam</b></span>",
|
||||
"hostname": "Vært",
|
||||
@@ -80,7 +83,7 @@
|
||||
"private_comment": "Privat kommentar",
|
||||
"public_comment": "Offentlig kommentar",
|
||||
"quota_mb": "Kvota (Mb)",
|
||||
"relay_all": "Send alle modtagere videre",
|
||||
"relay_all": "Besvar alle modtager",
|
||||
"relay_all_info": "↪ Hvis du vælger <b> ikke </b> at videresende alle modtagere, skal du tilføje et (\"blind\") postkasse til hver enkelt modtager, der skal videresendes.",
|
||||
"relay_domain": "Send dette domæne videre",
|
||||
"relay_transport_info": "<div class=\"badge fs-6 bg-info\">Info</div> Du kan definere transportkort til en tilpasset destination for dette domæne. Hvis ikke indstillet, foretages der et MX-opslag.",
|
||||
@@ -101,7 +104,10 @@
|
||||
"timeout2": "Timeout for forbindelse til lokal vært",
|
||||
"username": "Brugernavn",
|
||||
"validate": "Bekræft",
|
||||
"validation_success": "Valideret med succes"
|
||||
"validation_success": "Valideret med succes",
|
||||
"bcc_dest_format": "BCC-destination skal være en enkelt gyldig e-mail-adresse.<br>Hvis du har brug for at sende en kopi til flere adresser, kan du oprette et alias og bruge det her.",
|
||||
"app_passwd_protocols": "Tilladte protokoller for app adgangskode",
|
||||
"tags": "Tag's"
|
||||
},
|
||||
"admin": {
|
||||
"access": "Adgang",
|
||||
@@ -308,7 +314,10 @@
|
||||
"username": "Brugernavn",
|
||||
"validate_license_now": "Valider GUID mod licensserver",
|
||||
"verify": "Verificere",
|
||||
"yes": "✓"
|
||||
"yes": "✓",
|
||||
"ip_check_opt_in": "Opt-In for brug af tredjepartstjeneste <strong>ipv4.mailcow.email</strong> og <strong>ipv6.mailcow.email</strong> til at finde eksterne IP-adresser.",
|
||||
"queue_unban": "unban",
|
||||
"admins": "Administratorer"
|
||||
},
|
||||
"danger": {
|
||||
"access_denied": "Adgang nægtet eller ugyldig formular data",
|
||||
@@ -425,7 +434,8 @@
|
||||
"username_invalid": "Brugernavn %s kan ikke bruges",
|
||||
"validity_missing": "Tildel venligst en gyldighedsperiode",
|
||||
"value_missing": "Angiv alle værdier",
|
||||
"yotp_verification_failed": "Yubico OTP verifikationen mislykkedes: %s"
|
||||
"yotp_verification_failed": "Yubico OTP verifikationen mislykkedes: %s",
|
||||
"webauthn_publickey_failed": "Der er ikke gemt nogen offentlig nøgle for den valgte autentifikator"
|
||||
},
|
||||
"debug": {
|
||||
"chart_this_server": "Diagram (denne server)",
|
||||
@@ -442,7 +452,8 @@
|
||||
"solr_status": "Solr-status",
|
||||
"started_on": "Startede den",
|
||||
"static_logs": "Statiske logfiler",
|
||||
"system_containers": "System og Beholdere"
|
||||
"system_containers": "System og Beholdere",
|
||||
"error_show_ip": "Kunne ikke finde de offentlige IP-adresser"
|
||||
},
|
||||
"diagnostics": {
|
||||
"cname_from_a": "Værdi afledt af A / AAAA-post. Dette understøttes, så længe posten peger på den korrekte ressource.",
|
||||
@@ -553,7 +564,11 @@
|
||||
"title": "Rediger objekt",
|
||||
"unchanged_if_empty": "Lad være tomt, hvis uændret",
|
||||
"username": "Brugernavn",
|
||||
"validate_save": "Valider og gem"
|
||||
"validate_save": "Valider og gem",
|
||||
"admin": "Rediger administrator",
|
||||
"lookup_mx": "Destination er et regulært udtryk, der matcher MX-navnet (<code>.*google\\.dk</code> for at dirigere al e-mail, der er målrettet til en MX, der ender på google.dk, over dette hop)",
|
||||
"mailbox_relayhost_info": "Anvendt på postkassen og kun direkte aliasser, og overskriver et domæne relæ-host.",
|
||||
"quota_warning_bcc": "Kvoteadvarsel BCC"
|
||||
},
|
||||
"footer": {
|
||||
"cancel": "Afbestille",
|
||||
@@ -571,7 +586,7 @@
|
||||
"header": {
|
||||
"administration": "Konfiguration og detailer",
|
||||
"apps": "Apps",
|
||||
"debug": "Systemoplysninger",
|
||||
"debug": "Information",
|
||||
"email": "E-Mail",
|
||||
"mailcow_config": "Konfiguration",
|
||||
"quarantine": "Karantæne",
|
||||
@@ -586,7 +601,7 @@
|
||||
},
|
||||
"login": {
|
||||
"delayed": "Login blev forsinket med% s sekunder.",
|
||||
"fido2_webauthn": "FIDO2/WebAuthn",
|
||||
"fido2_webauthn": "FIDO2/WebAuthn Login",
|
||||
"login": "Login",
|
||||
"mobileconfig_info": "Log ind som postkassebruger for at downloade den anmodede Apple-forbindelsesprofil.",
|
||||
"other_logins": "Nøgle login",
|
||||
@@ -739,7 +754,10 @@
|
||||
"username": "Brugernavn",
|
||||
"waiting": "Venter",
|
||||
"weekly": "Ugentlig",
|
||||
"yes": "✓"
|
||||
"yes": "✓",
|
||||
"goto_ham": "Lær som <b>ønsket</b>",
|
||||
"catch_all": "Fang-alt",
|
||||
"open_logs": "Åben logfiler"
|
||||
},
|
||||
"oauth2": {
|
||||
"access_denied": "Log ind som mailboks ejer for at give adgang via OAuth2.",
|
||||
@@ -803,7 +821,6 @@
|
||||
"toggle_all": "Skift alt"
|
||||
},
|
||||
"queue": {
|
||||
"queue_deliver_mail": "Aflevere",
|
||||
"queue_manager": "Køadministrator"
|
||||
},
|
||||
"start": {
|
||||
@@ -1031,7 +1048,7 @@
|
||||
"spamfilter_table_empty": "Intet data at vise",
|
||||
"spamfilter_table_remove": "slet",
|
||||
"spamfilter_table_rule": "Regl",
|
||||
"spamfilter_wl": "Hvisliste",
|
||||
"spamfilter_wl": "Hvidliste",
|
||||
"spamfilter_wl_desc": "Hvidlistede e-mail-adresser til <b>aldrig</b> at klassificeres som spam. Wildcards kan bruges. Et filter anvendes kun på direkte aliaser (aliaser med en enkelt målpostkasse) eksklusive catch-aliaser og selve en postkasse.",
|
||||
"spamfilter_yellow": "Gul: denne besked kan være spam, vil blive tagget som spam og flyttes til din junk-mappe",
|
||||
"status": "Status",
|
||||
@@ -1067,5 +1084,11 @@
|
||||
"quota_exceeded_scope": "Domænekvote overskredet: Kun ubegrænsede postkasser kan oprettes i dette domæneomfang.",
|
||||
"session_token": "Form nøgle ugyldig: Nøgle passer ikke",
|
||||
"session_ua": "Form nøgle ugyldig: Bruger-Agent gyldighedskontrols fejl"
|
||||
},
|
||||
"datatables": {
|
||||
"lengthMenu": "Vis _MENU_ poster",
|
||||
"paginate": {
|
||||
"first": "Først"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -175,10 +175,12 @@
|
||||
"empty": "Keine Einträge vorhanden",
|
||||
"excludes": "Diese Empfänger ausschließen",
|
||||
"f2b_ban_time": "Bannzeit in Sekunden",
|
||||
"f2b_ban_time_increment": "Bannzeit erhöht sich mit jedem Bann",
|
||||
"f2b_blacklist": "Blacklist für Netzwerke und Hosts",
|
||||
"f2b_filter": "Regex-Filter",
|
||||
"f2b_list_info": "Ein Host oder Netzwerk auf der Blacklist wird immer eine Whitelist-Einheit überwiegen. <b>Die Aktualisierung der Liste dauert einige Sekunden.</b>",
|
||||
"f2b_max_attempts": "Max. Versuche",
|
||||
"f2b_max_ban_time": "Maximale Bannzeit in Sekunden",
|
||||
"f2b_netban_ipv4": "Netzbereich für IPv4-Banns (8-32)",
|
||||
"f2b_netban_ipv6": "Netzbereich für IPv6-Banns (8-128)",
|
||||
"f2b_parameters": "Fail2ban-Parameter",
|
||||
@@ -204,6 +206,9 @@
|
||||
"include_exclude": "Ein- und Ausschlüsse",
|
||||
"include_exclude_info": "Ohne Auswahl werden <b>alle Mailboxen</b> adressiert.",
|
||||
"includes": "Diese Empfänger einschließen",
|
||||
"ip_check": "IP Check",
|
||||
"ip_check_disabled": "IP check ist deaktiviert. Unter dem angegebenen Pfad kann es aktiviert werden<br> <strong>System > Konfiguration > Einstellungen > UI-Anpassung</strong>",
|
||||
"ip_check_opt_in": "Opt-In für die Nutzung der Drittanbieter-Dienste <strong>ipv4.mailcow.email</strong> und <strong>ipv6.mailcow.email</strong> zur Auflösung externer IP-Adressen.",
|
||||
"is_mx_based": "MX-basiert",
|
||||
"last_applied": "Zuletzt angewendet",
|
||||
"license_info": "Eine Lizenz ist nicht erforderlich, hilft jedoch der Entwicklung mailcows.<br><a href=\"https://www.servercow.de/mailcow#sal\" target=\"_blank\" alt=\"SAL Bestellung\">Hier kann die mailcow-GUID registriert werden.</a> Alternativ ist <a href=\"https://www.servercow.de/mailcow#support\" target=\"_blank\" alt=\"SAL Bestellung\">die Bestellung von Support-Paketen möglich</a>.",
|
||||
@@ -211,7 +216,7 @@
|
||||
"loading": "Bitte warten...",
|
||||
"login_time": "Zeit",
|
||||
"logo_info": "Die hochgeladene Grafik wird für die Navigationsleiste auf eine Höhe von 40px skaliert. Für die Darstellung auf der Login-Maske beträgt die skalierte Breite maximal 250px. Eine frei skalierbare Grafik (etwa SVG) wird empfohlen.",
|
||||
"lookup_mx": "Ziel mit MX vergleichen (Regex, etwa <code>.*google\\.com</code>, um alle Ziele mit MX *google.com zu routen)",
|
||||
"lookup_mx": "Ziel mit MX vergleichen (Regex, etwa <code>.*\\.google\\.com</code>, um alle Ziele mit MX *google.com zu routen)",
|
||||
"main_name": "\"mailcow UI\" Name",
|
||||
"merged_vars_hint": "Ausgegraute Reihen wurden aus der Datei <code>vars.(local.)inc.php</code> gelesen und können hier nicht verändert werden.",
|
||||
"message": "Nachricht",
|
||||
@@ -336,7 +341,8 @@
|
||||
"oauth2_add_client": "Füge OAuth2 Client hinzu",
|
||||
"api_read_only": "Schreibgeschützter Zugriff",
|
||||
"api_read_write": "Lese-Schreib-Zugriff",
|
||||
"oauth2_apps": "OAuth2 Apps"
|
||||
"oauth2_apps": "OAuth2 Apps",
|
||||
"queue_unban": "entsperren"
|
||||
},
|
||||
"danger": {
|
||||
"access_denied": "Zugriff verweigert oder unvollständige/ungültige Daten",
|
||||
@@ -363,6 +369,7 @@
|
||||
"domain_not_empty": "Domain %s ist nicht leer",
|
||||
"domain_not_found": "Domain %s nicht gefunden",
|
||||
"domain_quota_m_in_use": "Domain-Speicherplatzlimit muss größer oder gleich %d MiB sein",
|
||||
"extended_sender_acl_denied": "Keine Rechte zum Setzen von externen Absenderadressen",
|
||||
"extra_acl_invalid": "Externe Absenderadresse \"%s\" ist ungültig",
|
||||
"extra_acl_invalid_domain": "Externe Absenderadresse \"%s\" verwendet eine ungültige Domain",
|
||||
"fido2_verification_failed": "FIDO2-Verifizierung fehlgeschlagen: %s",
|
||||
@@ -450,49 +457,58 @@
|
||||
"totp_verification_failed": "TOTP-Verifizierung fehlgeschlagen",
|
||||
"transport_dest_exists": "Transport-Maps-Ziel \"%s\" existiert bereits",
|
||||
"webauthn_verification_failed": "WebAuthn-Verifizierung fehlgeschlagen: %s",
|
||||
"webauthn_authenticator_failed": "Der ausgewählte Authenticator wurde nicht gefunden",
|
||||
"webauthn_publickey_failed": "Zu dem ausgewählten Authenticator wurde kein Publickey hinterlegt",
|
||||
"webauthn_username_failed": "Der ausgewählte Authenticator gehört zu einem anderen Konto",
|
||||
"unknown": "Ein unbekannter Fehler trat auf",
|
||||
"unknown_tfa_method": "Unbekannte TFA-Methode",
|
||||
"unlimited_quota_acl": "Unendliche Quota untersagt durch ACL",
|
||||
"username_invalid": "Benutzername %s kann nicht verwendet werden",
|
||||
"validity_missing": "Bitte geben Sie eine Gültigkeitsdauer an",
|
||||
"value_missing": "Bitte alle Felder ausfüllen",
|
||||
"yotp_verification_failed": "Yubico OTP-Verifizierung fehlgeschlagen: %s"
|
||||
"yotp_verification_failed": "Yubico OTP-Verifizierung fehlgeschlagen: %s",
|
||||
"template_exists": "Vorlage %s existiert bereits",
|
||||
"template_id_invalid": "Vorlagen-ID %s ungültig",
|
||||
"template_name_invalid": "Name der Vorlage ungültig"
|
||||
},
|
||||
"datatables": {
|
||||
"collapse_all": "Alle Einklappen",
|
||||
"decimal": "",
|
||||
"emptyTable": "Keine Daten in der Tabelle vorhanden",
|
||||
"expand_all": "Alle Ausklappen",
|
||||
"info": "_START_ bis _END_ von _TOTAL_ Einträgen",
|
||||
"infoEmpty": "0 bis 0 von 0 Einträgen",
|
||||
"infoFiltered": "(gefiltert von _MAX_ Einträgen)",
|
||||
"infoPostFix": "",
|
||||
"thousands": ".",
|
||||
"lengthMenu": "_MENU_ Einträge anzeigen",
|
||||
"loadingRecords": "Wird geladen...",
|
||||
"processing": "Bitte warten...",
|
||||
"search": "Suchen",
|
||||
"zeroRecords": "Keine Einträge vorhanden.",
|
||||
"paginate": {
|
||||
"first": "Erste",
|
||||
"previous": "Zurück",
|
||||
"next": "Nächste",
|
||||
"last": "Letzte"
|
||||
},
|
||||
"aria": {
|
||||
"sortAscending": ": aktivieren, um Spalte aufsteigend zu sortieren",
|
||||
"sortDescending": ": aktivieren, um Spalte absteigend zu sortieren"
|
||||
}
|
||||
"collapse_all": "Alle Einklappen",
|
||||
"decimal": ",",
|
||||
"emptyTable": "Keine Daten in der Tabelle vorhanden",
|
||||
"expand_all": "Alle Ausklappen",
|
||||
"info": "_START_ bis _END_ von _TOTAL_ Einträgen",
|
||||
"infoEmpty": "0 bis 0 von 0 Einträgen",
|
||||
"infoFiltered": "(gefiltert von _MAX_ Einträgen)",
|
||||
"infoPostFix": "",
|
||||
"thousands": ".",
|
||||
"lengthMenu": "_MENU_ Einträge anzeigen",
|
||||
"loadingRecords": "Wird geladen...",
|
||||
"processing": "Bitte warten...",
|
||||
"search": "Suchen",
|
||||
"zeroRecords": "Keine Einträge vorhanden.",
|
||||
"paginate": {
|
||||
"first": "Erste",
|
||||
"previous": "Zurück",
|
||||
"next": "Nächste",
|
||||
"last": "Letzte"
|
||||
},
|
||||
"aria": {
|
||||
"sortAscending": ": aktivieren, um Spalte aufsteigend zu sortieren",
|
||||
"sortDescending": ": aktivieren, um Spalte absteigend zu sortieren"
|
||||
}
|
||||
},
|
||||
"debug": {
|
||||
"architecture": "Architektur",
|
||||
"chart_this_server": "Chart (dieser Server)",
|
||||
"containers_info": "Container-Information",
|
||||
"container_running": "Läuft",
|
||||
"container_disabled": "Container gestoppt oder deaktiviert",
|
||||
"container_stopped": "Angehalten",
|
||||
"cores": "Kerne",
|
||||
"current_time": "Systemzeit",
|
||||
"disk_usage": "Festplattennutzung",
|
||||
"docs": "Dokumente",
|
||||
"error_show_ip": "Konnte die öffentlichen IP Adressen nicht auflösen",
|
||||
"external_logs": "Externe Logs",
|
||||
"history_all_servers": "History (alle Server)",
|
||||
"in_memory_logs": "In-memory Logs",
|
||||
@@ -505,6 +521,7 @@
|
||||
"online_users": "Benutzer online",
|
||||
"restart_container": "Neustart",
|
||||
"service": "Dienst",
|
||||
"show_ip": "Zeige öffentliche IP",
|
||||
"size": "Größe",
|
||||
"solr_dead": "Solr startet, ist deaktiviert oder temporär nicht erreichbar.",
|
||||
"solr_status": "Solr Status",
|
||||
@@ -518,7 +535,8 @@
|
||||
"update_available": "Es ist ein Update verfügbar",
|
||||
"no_update_available": "Das System ist auf aktuellem Stand",
|
||||
"update_failed": "Es konnte nicht nach einem Update gesucht werden",
|
||||
"username": "Benutzername"
|
||||
"username": "Benutzername",
|
||||
"wip": "Aktuell noch in Arbeit"
|
||||
},
|
||||
"diagnostics": {
|
||||
"cname_from_a": "Wert abgeleitet von A/AAAA-Eintrag. Wird unterstützt, sofern der Eintrag auf die korrekte Ressource zeigt.",
|
||||
@@ -577,7 +595,7 @@
|
||||
"inactive": "Inaktiv",
|
||||
"kind": "Art",
|
||||
"last_modified": "Zuletzt geändert",
|
||||
"lookup_mx": "Ziel mit MX vergleichen (Regex, etwa <code>.*google\\.com</code>, um alle Ziele mit MX *google.com zu routen)",
|
||||
"lookup_mx": "Ziel mit MX vergleichen (Regex, etwa <code>.*\\.google\\.com</code>, um alle Ziele mit MX *google.com zu routen)",
|
||||
"mailbox": "Mailbox bearbeiten",
|
||||
"mailbox_quota_def": "Standard-Quota einer Mailbox",
|
||||
"mailbox_relayhost_info": "Wird auf eine Mailbox und direkte Alias-Adressen angewendet. Überschreibt die Einstellung einer Domain.",
|
||||
@@ -644,7 +662,8 @@
|
||||
"title": "Objekt bearbeiten",
|
||||
"unchanged_if_empty": "Unverändert, wenn leer",
|
||||
"username": "Benutzername",
|
||||
"validate_save": "Validieren und speichern"
|
||||
"validate_save": "Validieren und speichern",
|
||||
"pushover_sound": "Ton"
|
||||
},
|
||||
"fido2": {
|
||||
"confirm": "Bestätigen",
|
||||
@@ -685,7 +704,8 @@
|
||||
"quarantine": "Quarantäne",
|
||||
"restart_netfilter": "Netfilter neustarten",
|
||||
"restart_sogo": "SOGo neustarten",
|
||||
"user_settings": "Benutzereinstellungen"
|
||||
"user_settings": "Benutzereinstellungen",
|
||||
"mailcow_system": "System"
|
||||
},
|
||||
"info": {
|
||||
"awaiting_tfa_confirmation": "Warte auf TFA-Verifizierung",
|
||||
@@ -694,7 +714,7 @@
|
||||
},
|
||||
"login": {
|
||||
"delayed": "Login wurde zur Sicherheit um %s Sekunde/n verzögert.",
|
||||
"fido2_webauthn": "FIDO2/WebAuthn",
|
||||
"fido2_webauthn": "FIDO2/WebAuthn Login",
|
||||
"login": "Anmelden",
|
||||
"mobileconfig_info": "Bitte als Mailbox-Benutzer einloggen, um das Verbindungsprofil herunterzuladen.",
|
||||
"other_logins": "Key Login",
|
||||
@@ -941,7 +961,7 @@
|
||||
"queue": {
|
||||
"delete": "Queue löschen",
|
||||
"flush": "Queue flushen",
|
||||
"info" : "In der Mailqueue befinden sich alle E-Mails, welche auf eine Zustellung warten. Sollte eine E-Mail eine längere Zeit innerhalb der Mailqueue stecken wird diese automatisch vom System gelöscht.<br>Die Fehlermeldung der jeweiligen Mail gibt aufschluss darüber, warum diese nicht zugestellt werden konnte",
|
||||
"info": "In der Mailqueue befinden sich alle E-Mails, welche auf eine Zustellung warten. Sollte eine E-Mail eine längere Zeit innerhalb der Mailqueue stecken wird diese automatisch vom System gelöscht.<br>Die Fehlermeldung der jeweiligen Mail gibt aufschluss darüber, warum diese nicht zugestellt werden konnte",
|
||||
"legend": "Funktionen der Mailqueue Aktionen:",
|
||||
"ays": "Soll die derzeitige Queue wirklich komplett bereinigt werden?",
|
||||
"deliver_mail": "Ausliefern",
|
||||
@@ -1000,6 +1020,7 @@
|
||||
"forwarding_host_removed": "Weiterleitungs-Host %s wurde entfernt",
|
||||
"global_filter_written": "Filterdatei wurde erfolgreich geschrieben",
|
||||
"hash_deleted": "Hash wurde gelöscht",
|
||||
"ip_check_opt_in_modified": "IP Check wurde erfolgreich gespeichert",
|
||||
"item_deleted": "Objekt %s wurde entfernt",
|
||||
"item_released": "Objekt %s freigegeben",
|
||||
"items_deleted": "Objekt(e) %s wurde(n) erfolgreich entfernt",
|
||||
@@ -1228,7 +1249,8 @@
|
||||
"syncjob_EXIT_CONNECTION_FAILURE": "Verbindungsproblem",
|
||||
"syncjob_EXIT_TLS_FAILURE": "Problem mit verschlüsselter Verbindung",
|
||||
"syncjob_EXIT_AUTHENTICATION_FAILURE": "Authentifizierungsproblem",
|
||||
"syncjob_EXIT_AUTHENTICATION_FAILURE_USER1": "Falscher Benutzername oder Passwort"
|
||||
"syncjob_EXIT_AUTHENTICATION_FAILURE_USER1": "Falscher Benutzername oder Passwort",
|
||||
"pushover_sound": "Ton"
|
||||
},
|
||||
"warning": {
|
||||
"cannot_delete_self": "Kann derzeit eingeloggten Benutzer nicht entfernen",
|
||||
|
@@ -177,10 +177,12 @@
|
||||
"empty": "No results",
|
||||
"excludes": "Excludes these recipients",
|
||||
"f2b_ban_time": "Ban time (s)",
|
||||
"f2b_ban_time_increment": "Ban time is incremented with each ban",
|
||||
"f2b_blacklist": "Blacklisted networks/hosts",
|
||||
"f2b_filter": "Regex filters",
|
||||
"f2b_list_info": "A blacklisted host or network will always outweigh a whitelist entity. <b>List updates will take a few seconds to be applied.</b>",
|
||||
"f2b_max_attempts": "Max. attempts",
|
||||
"f2b_max_ban_time": "Max. ban time (s)",
|
||||
"f2b_netban_ipv4": "IPv4 subnet size to apply ban on (8-32)",
|
||||
"f2b_netban_ipv6": "IPv6 subnet size to apply ban on (8-128)",
|
||||
"f2b_parameters": "Fail2ban parameters",
|
||||
@@ -206,6 +208,9 @@
|
||||
"include_exclude": "Include/Exclude",
|
||||
"include_exclude_info": "By default - with no selection - <b>all mailboxes</b> are addressed",
|
||||
"includes": "Include these recipients",
|
||||
"ip_check": "IP Check",
|
||||
"ip_check_disabled": "IP check is disabled. You can enable it under<br> <strong>System > Configuration > Options > Customize</strong>",
|
||||
"ip_check_opt_in": "Opt-In for using third party service <strong>ipv4.mailcow.email</strong> and <strong>ipv6.mailcow.email</strong> to resolve external IP addresses.",
|
||||
"is_mx_based": "MX based",
|
||||
"last_applied": "Last applied",
|
||||
"license_info": "A license is not required but helps further development.<br><a href=\"https://www.servercow.de/mailcow?lang=en#sal\" target=\"_blank\" alt=\"SAL order\">Register your GUID here</a> or <a href=\"https://www.servercow.de/mailcow?lang=en#support\" target=\"_blank\" alt=\"Support order\">buy support for your mailcow installation.</a>",
|
||||
@@ -213,7 +218,7 @@
|
||||
"loading": "Please wait...",
|
||||
"login_time": "Login time",
|
||||
"logo_info": "Your image will be scaled to a height of 40px for the top navigation bar and a max. width of 250px for the start page. A scalable graphic is highly recommended.",
|
||||
"lookup_mx": "Destination is a regular expression to match against MX name (<code>.*google\\.com</code> to route all mail targeted to a MX ending in google.com over this hop)",
|
||||
"lookup_mx": "Destination is a regular expression to match against MX name (<code>.*\\.google\\.com</code> to route all mail targeted to a MX ending in google.com over this hop)",
|
||||
"main_name": "\"mailcow UI\" name",
|
||||
"merged_vars_hint": "Greyed out rows were merged from <code>vars.(local.)inc.php</code> and cannot be modified.",
|
||||
"message": "Message",
|
||||
@@ -264,6 +269,7 @@
|
||||
"quota_notifications": "Quota notifications",
|
||||
"quota_notifications_info": "Quota notifications are sent to users once when crossing 80% and once when crossing 95% usage.",
|
||||
"quota_notifications_vars": "{{percent}} equals the current quota of the user<br>{{username}} is the mailbox name",
|
||||
"queue_unban": "unban",
|
||||
"r_active": "Active restrictions",
|
||||
"r_inactive": "Inactive restrictions",
|
||||
"r_info": "Greyed out/disabled elements on the list of active restrictions are not known as valid restrictions to mailcow and cannot be moved. Unknown restrictions will be set in order of appearance anyway. <br>You can add new elements in <code>inc/vars.local.inc.php</code> to be able to toggle them.",
|
||||
@@ -363,6 +369,7 @@
|
||||
"domain_not_empty": "Cannot remove non-empty domain %s",
|
||||
"domain_not_found": "Domain %s not found",
|
||||
"domain_quota_m_in_use": "Domain quota must be greater or equal to %s MiB",
|
||||
"extended_sender_acl_denied": "missing ACL to set external sender addresses",
|
||||
"extra_acl_invalid": "External sender address \"%s\" is invalid",
|
||||
"extra_acl_invalid_domain": "External sender \"%s\" uses an invalid domain",
|
||||
"fido2_verification_failed": "FIDO2 verification failed: %s",
|
||||
@@ -453,6 +460,9 @@
|
||||
"totp_verification_failed": "TOTP verification failed",
|
||||
"transport_dest_exists": "Transport destination \"%s\" exists",
|
||||
"webauthn_verification_failed": "WebAuthn verification failed: %s",
|
||||
"webauthn_authenticator_failed": "The selected authenticator was not found",
|
||||
"webauthn_publickey_failed": "No public key was stored for the selected authenticator",
|
||||
"webauthn_username_failed": "The selected authenticator belongs to another account",
|
||||
"unknown": "An unknown error occurred",
|
||||
"unknown_tfa_method": "Unknown TFA method",
|
||||
"unlimited_quota_acl": "Unlimited quota prohibited by ACL",
|
||||
@@ -462,40 +472,43 @@
|
||||
"yotp_verification_failed": "Yubico OTP verification failed: %s"
|
||||
},
|
||||
"datatables": {
|
||||
"collapse_all": "Collapse All",
|
||||
"decimal": "",
|
||||
"emptyTable": "No data available in table",
|
||||
"expand_all": "Expand All",
|
||||
"info": "Showing _START_ to _END_ of _TOTAL_ entries",
|
||||
"infoEmpty": "Showing 0 to 0 of 0 entries",
|
||||
"infoFiltered": "(filtered from _MAX_ total entries)",
|
||||
"infoPostFix": "",
|
||||
"thousands": ",",
|
||||
"lengthMenu": "Show _MENU_ entries",
|
||||
"loadingRecords": "Loading...",
|
||||
"processing": "Please wait...",
|
||||
"search": "Search:",
|
||||
"zeroRecords": "No matching records found",
|
||||
"paginate": {
|
||||
"first": "First",
|
||||
"last": "Last",
|
||||
"next": "Next",
|
||||
"previous": "Previous"
|
||||
},
|
||||
"aria": {
|
||||
"sortAscending": ": activate to sort column ascending",
|
||||
"sortDescending": ": activate to sort column descending"
|
||||
}
|
||||
"collapse_all": "Collapse All",
|
||||
"decimal": ".",
|
||||
"emptyTable": "No data available in table",
|
||||
"expand_all": "Expand All",
|
||||
"info": "Showing _START_ to _END_ of _TOTAL_ entries",
|
||||
"infoEmpty": "Showing 0 to 0 of 0 entries",
|
||||
"infoFiltered": "(filtered from _MAX_ total entries)",
|
||||
"infoPostFix": "",
|
||||
"thousands": ",",
|
||||
"lengthMenu": "Show _MENU_ entries",
|
||||
"loadingRecords": "Loading...",
|
||||
"processing": "Please wait...",
|
||||
"search": "Search:",
|
||||
"zeroRecords": "No matching records found",
|
||||
"paginate": {
|
||||
"first": "First",
|
||||
"last": "Last",
|
||||
"next": "Next",
|
||||
"previous": "Previous"
|
||||
},
|
||||
"aria": {
|
||||
"sortAscending": ": activate to sort column ascending",
|
||||
"sortDescending": ": activate to sort column descending"
|
||||
}
|
||||
},
|
||||
"debug": {
|
||||
"architecture": "Architecture",
|
||||
"chart_this_server": "Chart (this server)",
|
||||
"containers_info": "Container information",
|
||||
"container_running": "Running",
|
||||
"container_disabled": "Container stopped or disabled",
|
||||
"container_stopped": "Stopped",
|
||||
"cores": "Cores",
|
||||
"current_time": "System Time",
|
||||
"disk_usage": "Disk usage",
|
||||
"docs": "Docs",
|
||||
"error_show_ip": "Could not resolve the public IP addresses",
|
||||
"external_logs": "External logs",
|
||||
"history_all_servers": "History (all servers)",
|
||||
"in_memory_logs": "In-memory logs",
|
||||
@@ -508,6 +521,7 @@
|
||||
"online_users": "Users online",
|
||||
"restart_container": "Restart",
|
||||
"service": "Service",
|
||||
"show_ip": "Show public IP",
|
||||
"size": "Size",
|
||||
"solr_dead": "Solr is starting, disabled or died.",
|
||||
"solr_status": "Solr status",
|
||||
@@ -521,7 +535,8 @@
|
||||
"update_available": "There is an update available",
|
||||
"no_update_available": "The System is on the latest version",
|
||||
"update_failed": "Could not check for an Update",
|
||||
"username": "Username"
|
||||
"username": "Username",
|
||||
"wip": "Currently Work in Progress"
|
||||
},
|
||||
"diagnostics": {
|
||||
"cname_from_a": "Value derived from A/AAAA record. This is supported as long as the record points to the correct resource.",
|
||||
@@ -580,7 +595,7 @@
|
||||
"inactive": "Inactive",
|
||||
"kind": "Kind",
|
||||
"last_modified": "Last modified",
|
||||
"lookup_mx": "Destination is a regular expression to match against MX name (<code>.*google\\.com</code> to route all mail targeted to a MX ending in google.com over this hop)",
|
||||
"lookup_mx": "Destination is a regular expression to match against MX name (<code>.*\\.google\\.com</code> to route all mail targeted to a MX ending in google.com over this hop)",
|
||||
"mailbox": "Edit mailbox",
|
||||
"mailbox_quota_def": "Default mailbox quota",
|
||||
"mailbox_relayhost_info": "Applied to the mailbox and direct aliases only, does override a domain relayhost.",
|
||||
@@ -699,7 +714,7 @@
|
||||
},
|
||||
"login": {
|
||||
"delayed": "Login was delayed by %s seconds.",
|
||||
"fido2_webauthn": "FIDO2/WebAuthn",
|
||||
"fido2_webauthn": "FIDO2/WebAuthn Login",
|
||||
"login": "Login",
|
||||
"mobileconfig_info": "Please login as mailbox user to download the requested Apple connection profile.",
|
||||
"other_logins": "Key login",
|
||||
@@ -946,7 +961,7 @@
|
||||
"queue": {
|
||||
"delete": "Delete all",
|
||||
"flush": "Flush queue",
|
||||
"info" : "The mail queue contains all e-mails that are waiting for delivery. If an email is stuck in the mail queue for a long time, it is automatically deleted by the system.<br>The error message of the respective mail gives information about why the mail could not be delivered.",
|
||||
"info": "The mail queue contains all e-mails that are waiting for delivery. If an email is stuck in the mail queue for a long time, it is automatically deleted by the system.<br>The error message of the respective mail gives information about why the mail could not be delivered.",
|
||||
"legend": "Mail queue actions functions:",
|
||||
"ays": "Please confirm you want to delete all items from the current queue.",
|
||||
"deliver_mail": "Deliver",
|
||||
@@ -960,11 +975,11 @@
|
||||
"unhold_mail_legend": "Releases selected mails for delivery. (Requires prior hold)"
|
||||
},
|
||||
"ratelimit": {
|
||||
"disabled": "Disabled",
|
||||
"second": "msgs / second",
|
||||
"minute": "msgs / minute",
|
||||
"hour": "msgs / hour",
|
||||
"day": "msgs / day"
|
||||
"disabled": "Disabled",
|
||||
"second": "msgs / second",
|
||||
"minute": "msgs / minute",
|
||||
"hour": "msgs / hour",
|
||||
"day": "msgs / day"
|
||||
},
|
||||
"start": {
|
||||
"help": "Show/Hide help panel",
|
||||
@@ -1012,6 +1027,7 @@
|
||||
"forwarding_host_removed": "Forwarding host %s has been removed",
|
||||
"global_filter_written": "Filter was successfully written to file",
|
||||
"hash_deleted": "Hash deleted",
|
||||
"ip_check_opt_in_modified": "IP check was saved successfully",
|
||||
"item_deleted": "Item %s successfully deleted",
|
||||
"item_released": "Item %s released",
|
||||
"items_deleted": "Item %s successfully deleted",
|
||||
|
@@ -141,9 +141,11 @@
|
||||
"empty": "Sin resultados",
|
||||
"excludes": "Excluye a estos destinatarios",
|
||||
"f2b_ban_time": "Tiempo de restricción (s)",
|
||||
"f2b_ban_time_increment": "Tiempo de restricción se incrementa con cada restricción",
|
||||
"f2b_blacklist": "Redes y hosts en lista negra",
|
||||
"f2b_list_info": "Un host o red en lista negra siempre superará a una entidad de la lista blanca. <b>Las actualizaciones de la lista tardarán unos segundos en aplicarse.</b>",
|
||||
"f2b_max_attempts": "Max num. de intentos",
|
||||
"f2b_max_ban_time": "Max tiempo de restricción (s)",
|
||||
"f2b_netban_ipv4": "Tamaño de subred IPv4 para aplicar la restricción (8-32)",
|
||||
"f2b_netban_ipv6": "Tamaño de subred IPv6 para aplicar la restricción (8-128)",
|
||||
"f2b_parameters": "Parametros Fail2ban",
|
||||
@@ -602,7 +604,6 @@
|
||||
"toggle_all": "Seleccionar todos"
|
||||
},
|
||||
"queue": {
|
||||
"queue_deliver_mail": "Entregar",
|
||||
"queue_manager": "Administrador de cola"
|
||||
},
|
||||
"start": {
|
||||
|
@@ -686,7 +686,6 @@
|
||||
"toggle_all": "Valitse kaikki"
|
||||
},
|
||||
"queue": {
|
||||
"queue_deliver_mail": "Toimittaa",
|
||||
"queue_manager": "Jonon hallinta"
|
||||
},
|
||||
"start": {
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user