Compare commits

..

2 Commits

Author SHA1 Message Date
Niklas Meyer
a57e4212ee Removed Debug Messages 2023-01-20 15:22:55 +01:00
Niklas Meyer
735989f6cf Added Inactive Badge for Syncjobs + Parity with user view 2023-01-20 15:11:25 +01:00
76 changed files with 5832 additions and 6034 deletions

View File

@@ -14,7 +14,7 @@ jobs:
pull-requests: write pull-requests: write
steps: steps:
- name: Mark/Close Stale Issues and Pull Requests 🗑️ - name: Mark/Close Stale Issues and Pull Requests 🗑️
uses: actions/stale@v8.0.0 uses: actions/stale@v7.0.0
with: with:
repo-token: ${{ secrets.STALE_ACTION_PAT }} repo-token: ${{ secrets.STALE_ACTION_PAT }}
days-before-stale: 60 days-before-stale: 60

63
.github/workflows/integration_tests.yml vendored Normal file
View File

@@ -0,0 +1,63 @@
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'

View File

@@ -12,7 +12,7 @@ jobs:
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Run the Action - name: Run the Action
uses: devops-infra/action-pull-request@v0.5.5 uses: devops-infra/action-pull-request@v0.5.3
with: with:
github_token: ${{ secrets.PRTONIGHTLY_ACTION_PAT }} github_token: ${{ secrets.PRTONIGHTLY_ACTION_PAT }}
title: Automatic PR to nightly from ${{ github.event.repository.updated_at}} title: Automatic PR to nightly from ${{ github.event.repository.updated_at}}

View File

@@ -26,7 +26,7 @@ jobs:
password: ${{ secrets.BACKUPIMAGEBUILD_ACTION_DOCKERHUB_TOKEN }} password: ${{ secrets.BACKUPIMAGEBUILD_ACTION_DOCKERHUB_TOKEN }}
- name: Build and push - name: Build and push
uses: docker/build-push-action@v4 uses: docker/build-push-action@v3
with: with:
context: . context: .
file: data/Dockerfiles/backup/Dockerfile file: data/Dockerfiles/backup/Dockerfile

View File

@@ -0,0 +1,20 @@
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.2
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'

View File

@@ -1,5 +1,6 @@
# mailcow: dockerized - 🐮 + 🐋 = 💕 # mailcow: dockerized - 🐮 + 🐋 = 💕
[![Mailcow Integration Tests](https://github.com/mailcow/mailcow-dockerized/actions/workflows/integration_tests.yml/badge.svg?branch=master)](https://github.com/mailcow/mailcow-dockerized/actions/workflows/integration_tests.yml)
[![Translation status](https://translate.mailcow.email/widgets/mailcow-dockerized/-/translation/svg-badge.svg)](https://translate.mailcow.email/engage/mailcow-dockerized/) [![Translation status](https://translate.mailcow.email/widgets/mailcow-dockerized/-/translation/svg-badge.svg)](https://translate.mailcow.email/engage/mailcow-dockerized/)
[![Twitter URL](https://img.shields.io/twitter/url/https/twitter.com/mailcow_email.svg?style=social&label=Follow%20%40mailcow_email)](https://twitter.com/mailcow_email) [![Twitter URL](https://img.shields.io/twitter/url/https/twitter.com/mailcow_email.svg?style=social&label=Follow%20%40mailcow_email)](https://twitter.com/mailcow_email)

View File

@@ -213,13 +213,11 @@ while true; do
done done
ADDITIONAL_WC_ARR+=('autodiscover' 'autoconfig') ADDITIONAL_WC_ARR+=('autodiscover' 'autoconfig')
if [[ ${SKIP_IP_CHECK} != "y" ]]; then
# Start IP detection # Start IP detection
log_f "Detecting IP addresses..." log_f "Detecting IP addresses..."
IPV4=$(get_ipv4) IPV4=$(get_ipv4)
IPV6=$(get_ipv6) IPV6=$(get_ipv6)
log_f "OK: ${IPV4}, ${IPV6:-"0000:0000:0000:0000:0000:0000:0000:0000"}" log_f "OK: ${IPV4}, ${IPV6:-"0000:0000:0000:0000:0000:0000:0000:0000"}"
fi
######################################### #########################################
# IP and webroot challenge verification # # IP and webroot challenge verification #

View File

@@ -1,4 +1,4 @@
FROM clamav/clamav:1.0.1-1_base FROM clamav/clamav:1.0_base
LABEL maintainer "André Peters <andre.peters@servercow.de>" LABEL maintainer "André Peters <andre.peters@servercow.de>"

View File

@@ -13,7 +13,6 @@ RUN apk add --update --no-cache python3 \
fastapi \ fastapi \
uvicorn \ uvicorn \
aiodocker \ aiodocker \
docker \
redis redis
COPY docker-entrypoint.sh /app/ COPY docker-entrypoint.sh /app/

View File

@@ -1,6 +1,5 @@
from fastapi import FastAPI, Response, Request from fastapi import FastAPI, Response, Request
import aiodocker import aiodocker
import docker
import psutil import psutil
import sys import sys
import re import re
@@ -10,38 +9,11 @@ import json
import asyncio import asyncio
import redis import redis
from datetime import datetime from datetime import datetime
import logging
from logging.config import dictConfig
log_config = {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"default": {
"()": "uvicorn.logging.DefaultFormatter",
"fmt": "%(levelprefix)s %(asctime)s %(message)s",
"datefmt": "%Y-%m-%d %H:%M:%S",
},
},
"handlers": {
"default": {
"formatter": "default",
"class": "logging.StreamHandler",
"stream": "ext://sys.stderr",
},
},
"loggers": {
"api-logger": {"handlers": ["default"], "level": "INFO"},
},
}
dictConfig(log_config)
containerIds_to_update = [] containerIds_to_update = []
host_stats_isUpdating = False host_stats_isUpdating = False
app = FastAPI() app = FastAPI()
logger = logging.getLogger('api-logger')
@app.get("/host/stats") @app.get("/host/stats")
@@ -49,15 +21,18 @@ async def get_host_update_stats():
global host_stats_isUpdating global host_stats_isUpdating
if host_stats_isUpdating == False: if host_stats_isUpdating == False:
print("start host stats task")
asyncio.create_task(get_host_stats()) asyncio.create_task(get_host_stats())
host_stats_isUpdating = True host_stats_isUpdating = True
while True: while True:
if redis_client.exists('host_stats'): if redis_client.exists('host_stats'):
break break
print("wait for host_stats results")
await asyncio.sleep(1.5) await asyncio.sleep(1.5)
print("host stats pulled")
stats = json.loads(redis_client.get('host_stats')) stats = json.loads(redis_client.get('host_stats'))
return Response(content=json.dumps(stats, indent=4), media_type="application/json") return Response(content=json.dumps(stats, indent=4), media_type="application/json")
@@ -131,14 +106,14 @@ async def post_containers(container_id : str, post_action : str, request: Reques
else: else:
api_call_method_name = '__'.join(['container_post', str(post_action) ]) api_call_method_name = '__'.join(['container_post', str(post_action) ])
docker_utils = DockerUtils(sync_docker_client) 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")) 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"))
logger.info("api call: %s, container_id: %s" % (api_call_method_name, container_id)) print("api call: %s, container_id: %s" % (api_call_method_name, container_id))
return api_call_method(container_id, request_json) return await api_call_method(container_id, request_json)
except Exception as e: except Exception as e:
logger.error("error - container_post: %s" % str(e)) print("error - container_post: %s" % str(e))
res = { res = {
"type": "danger", "type": "danger",
"msg": str(e) "msg": str(e)
@@ -177,294 +152,398 @@ class DockerUtils:
self.docker_client = docker_client self.docker_client = docker_client
# api call: container_post - post_action: stop # api call: container_post - post_action: stop
def container_post__stop(self, container_id, request_json): async def container_post__stop(self, container_id, request_json):
for container in self.docker_client.containers.list(all=True, filters={"id": container_id}): for container in (await self.docker_client.containers.list()):
container.stop() if container._id == container_id:
await container.stop()
res = { 'type': 'success', 'msg': 'command completed successfully'} res = {
'type': 'success',
'msg': 'command completed successfully'
}
return Response(content=json.dumps(res, indent=4), media_type="application/json") return Response(content=json.dumps(res, indent=4), media_type="application/json")
# api call: container_post - post_action: start # api call: container_post - post_action: start
def container_post__start(self, container_id, request_json): async def container_post__start(self, container_id, request_json):
for container in self.docker_client.containers.list(all=True, filters={"id": container_id}): for container in (await self.docker_client.containers.list()):
container.start() if container._id == container_id:
await container.start()
res = { 'type': 'success', 'msg': 'command completed successfully'} res = {
'type': 'success',
'msg': 'command completed successfully'
}
return Response(content=json.dumps(res, indent=4), media_type="application/json") return Response(content=json.dumps(res, indent=4), media_type="application/json")
# api call: container_post - post_action: restart # api call: container_post - post_action: restart
def container_post__restart(self, container_id, request_json): async def container_post__restart(self, container_id, request_json):
for container in self.docker_client.containers.list(all=True, filters={"id": container_id}): for container in (await self.docker_client.containers.list()):
container.restart() if container._id == container_id:
await container.restart()
res = { 'type': 'success', 'msg': 'command completed successfully'} res = {
'type': 'success',
'msg': 'command completed successfully'
}
return Response(content=json.dumps(res, indent=4), media_type="application/json") return Response(content=json.dumps(res, indent=4), media_type="application/json")
# api call: container_post - post_action: top # api call: container_post - post_action: top
def container_post__top(self, container_id, request_json): async def container_post__top(self, container_id, request_json):
for container in self.docker_client.containers.list(all=True, filters={"id": container_id}): for container in (await self.docker_client.containers.list()):
res = { 'type': 'success', 'msg': container.top()} if container._id == container_id:
return Response(content=json.dumps(res, indent=4), media_type="application/json") ps_exec = await container.exec("ps")
# api call: container_post - post_action: stats async with ps_exec.start(detach=False) as stream:
def container_post__stats(self, container_id, request_json): ps_return = await stream.read_out()
for container in self.docker_client.containers.list(all=True, filters={"id": container_id}):
for stat in container.stats(decode=True, stream=True): exec_details = await ps_exec.inspect()
res = { 'type': 'success', 'msg': stat} if exec_details["ExitCode"] == None or exec_details["ExitCode"] == 0:
return Response(content=json.dumps(res, indent=4), media_type="application/json") 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 # api call: container_post - post_action: exec - cmd: mailq - task: delete
def container_post__exec__mailq__delete(self, container_id, request_json): async def container_post__exec__mailq__delete(self, container_id, request_json):
if 'items' in request_json: if 'items' in request_json:
r = re.compile("^[0-9a-fA-F]+$") r = re.compile("^[0-9a-fA-F]+$")
filtered_qids = filter(r.match, request_json['items']) filtered_qids = filter(r.match, request_json['items'])
if filtered_qids: if filtered_qids:
flagged_qids = ['-d %s' % i for i in filtered_qids] flagged_qids = ['-d %s' % i for i in filtered_qids]
sanitized_string = str(' '.join(flagged_qids)); sanitized_string = str(' '.join(flagged_qids))
for container in self.docker_client.containers.list(filters={"id": container_id}):
postsuper_r = container.exec_run(["/bin/bash", "-c", "/usr/sbin/postsuper " + sanitized_string])
return exec_run_handler('generic', postsuper_r)
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 # api call: container_post - post_action: exec - cmd: mailq - task: hold
def container_post__exec__mailq__hold(self, container_id, request_json): async def container_post__exec__mailq__hold(self, container_id, request_json):
if 'items' in request_json: if 'items' in request_json:
r = re.compile("^[0-9a-fA-F]+$") r = re.compile("^[0-9a-fA-F]+$")
filtered_qids = filter(r.match, request_json['items']) filtered_qids = filter(r.match, request_json['items'])
if filtered_qids: if filtered_qids:
flagged_qids = ['-h %s' % i for i in filtered_qids] flagged_qids = ['-h %s' % i for i in filtered_qids]
sanitized_string = str(' '.join(flagged_qids)); sanitized_string = str(' '.join(flagged_qids))
for container in self.docker_client.containers.list(filters={"id": container_id}):
postsuper_r = container.exec_run(["/bin/bash", "-c", "/usr/sbin/postsuper " + sanitized_string]) for container in (await self.docker_client.containers.list()):
return exec_run_handler('generic', postsuper_r) 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 # api call: container_post - post_action: exec - cmd: mailq - task: cat
def container_post__exec__mailq__cat(self, container_id, request_json): async def container_post__exec__mailq__cat(self, container_id, request_json):
if 'items' in request_json: if 'items' in request_json:
r = re.compile("^[0-9a-fA-F]+$") r = re.compile("^[0-9a-fA-F]+$")
filtered_qids = filter(r.match, request_json['items']) filtered_qids = filter(r.match, request_json['items'])
if filtered_qids: if filtered_qids:
sanitized_string = str(' '.join(filtered_qids)); sanitized_string = str(' '.join(filtered_qids))
for container in self.docker_client.containers.list(filters={"id": container_id}): for container in (await self.docker_client.containers.list()):
postcat_return = container.exec_run(["/bin/bash", "-c", "/usr/sbin/postcat -q " + sanitized_string], user='postfix') if container._id == container_id:
if not postcat_return: postcat_exec = await container.exec(["/bin/bash", "-c", "/usr/sbin/postcat -q " + sanitized_string], user='postfix')
postcat_return = 'err: invalid' return await exec_run_handler('utf8_text_only', postcat_exec)
return exec_run_handler('utf8_text_only', postcat_return)
# api call: container_post - post_action: exec - cmd: mailq - task: unhold # api call: container_post - post_action: exec - cmd: mailq - task: unhold
def container_post__exec__mailq__unhold(self, container_id, request_json): async def container_post__exec__mailq__unhold(self, container_id, request_json):
if 'items' in request_json: if 'items' in request_json:
r = re.compile("^[0-9a-fA-F]+$") r = re.compile("^[0-9a-fA-F]+$")
filtered_qids = filter(r.match, request_json['items']) filtered_qids = filter(r.match, request_json['items'])
if filtered_qids: if filtered_qids:
flagged_qids = ['-H %s' % i for i in filtered_qids] flagged_qids = ['-H %s' % i for i in filtered_qids]
sanitized_string = str(' '.join(flagged_qids)); sanitized_string = str(' '.join(flagged_qids))
for container in self.docker_client.containers.list(filters={"id": container_id}):
postsuper_r = container.exec_run(["/bin/bash", "-c", "/usr/sbin/postsuper " + sanitized_string]) for container in (await self.docker_client.containers.list()):
return exec_run_handler('generic', postsuper_r) 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 # api call: container_post - post_action: exec - cmd: mailq - task: deliver
def container_post__exec__mailq__deliver(self, container_id, request_json): async def container_post__exec__mailq__deliver(self, container_id, request_json):
if 'items' in request_json: if 'items' in request_json:
r = re.compile("^[0-9a-fA-F]+$") r = re.compile("^[0-9a-fA-F]+$")
filtered_qids = filter(r.match, request_json['items']) filtered_qids = filter(r.match, request_json['items'])
if filtered_qids: if filtered_qids:
flagged_qids = ['-i %s' % i for i in filtered_qids] flagged_qids = ['-i %s' % i for i in filtered_qids]
for container in self.docker_client.containers.list(filters={"id": container_id}):
for i in flagged_qids: for container in (await self.docker_client.containers.list()):
postqueue_r = container.exec_run(["/bin/bash", "-c", "/usr/sbin/postqueue " + i], user='postfix') if container._id == container_id:
# todo: check each exit code for i in flagged_qids:
res = { 'type': 'success', 'msg': 'Scheduled immediate delivery'} postsuper_r_exec = await container.exec(["/bin/bash", "-c", "/usr/sbin/postqueue " + i], user='postfix')
return Response(content=json.dumps(res, indent=4), media_type="application/json") 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 # api call: container_post - post_action: exec - cmd: mailq - task: list
def container_post__exec__mailq__list(self, container_id, request_json): async def container_post__exec__mailq__list(self, container_id, request_json):
for container in self.docker_client.containers.list(filters={"id": container_id}): for container in (await self.docker_client.containers.list()):
mailq_return = container.exec_run(["/usr/sbin/postqueue", "-j"], user='postfix') if container._id == container_id:
return exec_run_handler('utf8_text_only', mailq_return) 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 # api call: container_post - post_action: exec - cmd: mailq - task: flush
def container_post__exec__mailq__flush(self, container_id, request_json): async def container_post__exec__mailq__flush(self, container_id, request_json):
for container in self.docker_client.containers.list(filters={"id": container_id}): for container in (await self.docker_client.containers.list()):
postqueue_r = container.exec_run(["/usr/sbin/postqueue", "-f"], user='postfix') if container._id == container_id:
return exec_run_handler('generic', postqueue_r) 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 # api call: container_post - post_action: exec - cmd: mailq - task: super_delete
def container_post__exec__mailq__super_delete(self, container_id, request_json): async def container_post__exec__mailq__super_delete(self, container_id, request_json):
for container in self.docker_client.containers.list(filters={"id": container_id}): for container in (await self.docker_client.containers.list()):
postsuper_r = container.exec_run(["/usr/sbin/postsuper", "-d", "ALL"]) if container._id == container_id:
return exec_run_handler('generic', postsuper_r) 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 # api call: container_post - post_action: exec - cmd: system - task: fts_rescan
def container_post__exec__system__fts_rescan(self, container_id, request_json): async def container_post__exec__system__fts_rescan(self, container_id, request_json):
if 'username' in request_json: if 'username' in request_json:
for container in self.docker_client.containers.list(filters={"id": container_id}): for container in (await self.docker_client.containers.list()):
rescan_return = container.exec_run(["/bin/bash", "-c", "/usr/bin/doveadm fts rescan -u '" + request_json['username'].replace("'", "'\\''") + "'"], user='vmail') if container._id == container_id:
if rescan_return.exit_code == 0: rescan_exec = await container.exec(["/bin/bash", "-c", "/usr/bin/doveadm fts rescan -u '" + request_json['username'].replace("'", "'\\''") + "'"], user='vmail')
res = { 'type': 'success', 'msg': 'fts_rescan: rescan triggered'} async with rescan_exec.start(detach=False) as stream:
return Response(content=json.dumps(res, indent=4), media_type="application/json") rescan_return = await stream.read_out()
else:
res = { 'type': 'warning', 'msg': 'fts_rescan error'} exec_details = await rescan_exec.inspect()
return Response(content=json.dumps(res, indent=4), media_type="application/json") 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: if 'all' in request_json:
for container in self.docker_client.containers.list(filters={"id": container_id}): for container in (await self.docker_client.containers.list()):
rescan_return = container.exec_run(["/bin/bash", "-c", "/usr/bin/doveadm fts rescan -A"], user='vmail') if container._id == container_id:
if rescan_return.exit_code == 0: rescan_exec = await container.exec(["/bin/bash", "-c", "/usr/bin/doveadm fts rescan -A"], user='vmail')
res = { 'type': 'success', 'msg': 'fts_rescan: rescan triggered'} async with rescan_exec.start(detach=False) as stream:
return Response(content=json.dumps(res, indent=4), media_type="application/json") rescan_return = await stream.read_out()
else:
res = { 'type': 'warning', 'msg': 'fts_rescan error'} exec_details = await rescan_exec.inspect()
return Response(content=json.dumps(res, indent=4), media_type="application/json") 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 # api call: container_post - post_action: exec - cmd: system - task: df
def container_post__exec__system__df(self, container_id, request_json): async def container_post__exec__system__df(self, container_id, request_json):
if 'dir' in request_json: if 'dir' in request_json:
for container in self.docker_client.containers.list(filters={"id": container_id}): for container in (await self.docker_client.containers.list()):
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 container._id == container_id:
if df_return.exit_code == 0: 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')
return df_return.output.decode('utf-8').rstrip() async with df_exec.start(detach=False) as stream:
else: df_return = await stream.read_out()
return "0,0,0,0,0,0"
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 # api call: container_post - post_action: exec - cmd: system - task: mysql_upgrade
def container_post__exec__system__mysql_upgrade(self, container_id, request_json): async def container_post__exec__system__mysql_upgrade(self, container_id, request_json):
for container in self.docker_client.containers.list(filters={"id": container_id}): for container in (await self.docker_client.containers.list()):
sql_return = container.exec_run(["/bin/bash", "-c", "/usr/bin/mysql_upgrade -uroot -p'" + os.environ['DBROOT'].replace("'", "'\\''") + "'\n"], user='mysql') if container._id == container_id:
if sql_return.exit_code == 0: sql_exec = await container.exec(["/bin/bash", "-c", "/usr/bin/mysql_upgrade -uroot -p'" + os.environ['DBROOT'].replace("'", "'\\''") + "'\n"], user='mysql')
matched = False async with sql_exec.start(detach=False) as stream:
for line in sql_return.output.decode('utf-8').split("\n"): sql_return = await stream.read_out()
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, container_id, request_json):
for container in self.docker_client.containers.list(filters={"id": container_id}):
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, container_id, request_json):
for container in self.docker_client.containers.list(filters={"id": container_id}):
reload_return = container.exec_run(["/bin/bash", "-c", "/usr/sbin/dovecot reload"])
return exec_run_handler('generic', reload_return)
# api call: container_post - post_action: exec - cmd: reload - task: postfix
def container_post__exec__reload__postfix(self, container_id, request_json):
for container in self.docker_client.containers.list(filters={"id": container_id}):
reload_return = container.exec_run(["/bin/bash", "-c", "/usr/sbin/postfix reload"])
return exec_run_handler('generic', reload_return)
# api call: container_post - post_action: exec - cmd: reload - task: nginx
def container_post__exec__reload__nginx(self, container_id, request_json):
for container in self.docker_client.containers.list(filters={"id": container_id}):
reload_return = container.exec_run(["/bin/sh", "-c", "/usr/sbin/nginx -s reload"])
return exec_run_handler('generic', reload_return)
# api call: container_post - post_action: exec - cmd: sieve - task: list
def container_post__exec__sieve__list(self, container_id, request_json):
if 'username' in request_json:
for container in self.docker_client.containers.list(filters={"id": container_id}):
sieve_return = container.exec_run(["/bin/bash", "-c", "/usr/bin/doveadm sieve list -u '" + request_json['username'].replace("'", "'\\''") + "'"])
return 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, container_id, request_json):
if 'username' in request.json and 'script_name' in request_json:
for container in self.docker_client.containers.list(filters={"id": container_id}):
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 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, container_id, request_json):
if 'maildir' in request_json:
for container in self.docker_client.containers.list(filters={"id": container_id}):
sane_name = re.sub(r'\W+', '', request_json['maildir'])
vmail_name = request_json['maildir'].replace("'", "'\\''")
index_name = request_json['maildir'].split("/")
index_name = index_name[1].replace("'", "'\\''") + "@" + index_name[0].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"
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]
maildir_cleanup = container.exec_run(cmd, user='vmail')
return 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, container_id, request_json):
if 'raw' in request_json:
for container in self.docker_client.containers.list(filters={"id": container_id}):
cmd = "/usr/bin/rspamadm pw -e -p '" + request_json['raw'].replace("'", "'\\''") + "' 2> /dev/null"
cmd_response = exec_cmd_container(container, cmd, user="_rspamd")
matched = False exec_details = await sql_exec.inspect()
for line in cmd_response.split("\n"): if exec_details["ExitCode"] == None or exec_details["ExitCode"] == 0:
if '$2$' in line: matched = False
hash = line.strip() for line in sql_return.data.decode('utf-8').split("\n"):
hash_out = re.search('\$2\$.+$', hash).group(0) if 'is already upgraded to' in line:
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 = exec_cmd_container(container, cmd, user="_rspamd")
if rspamd_passphrase_hash.startswith("$2$") and rspamd_passphrase_hash in cmd_response:
container.restart()
matched = True matched = True
if matched: if matched:
res = { 'type': 'success', 'msg': 'command completed successfully' } res = {
logger.info('success changing Rspamd password') 'type': 'success',
return Response(content=json.dumps(res, indent=4), media_type="application/json") '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: else:
logger.error('failed changing Rspamd password') res = {
res = { 'type': 'danger', 'msg': 'command did not complete' } '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") 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()
def exec_cmd_container(container, cmd, user, timeout=2, shell_cmd="/bin/bash"): exec_details = await sql_exec.inspect()
if exec_details["ExitCode"] == None or exec_details["ExitCode"] == 0:
def recv_socket_data(c_socket, timeout): res = {
c_socket.setblocking(0) 'type': 'info',
total_data=[] 'msg': 'mysql_tzinfo_to_sql: command completed successfully',
data='' 'text': sql_return.data.decode('utf-8')
begin=time.time() }
while True: return Response(content=json.dumps(res, indent=4), media_type="application/json")
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: else:
#sleep for sometime to indicate a gap res = {
time.sleep(0.1) 'type': 'error',
break 'msg': 'mysql_tzinfo_to_sql: error running command',
except: 'text': sql_return.data.decode('utf-8')
pass }
return ''.join(total_data) return Response(content=json.dumps(res, indent=4), media_type="application/json")
try : # api call: container_post - post_action: exec - cmd: reload - task: dovecot
socket = container.exec_run([shell_cmd], stdin=True, socket=True, user=user).output._sock async def container_post__exec__reload__dovecot(self, container_id, request_json):
if not cmd.endswith("\n"): for container in (await self.docker_client.containers.list()):
cmd = cmd + "\n" if container._id == container_id:
socket.send(cmd.encode('utf-8')) reload_exec = await container.exec(["/bin/bash", "-c", "/usr/sbin/dovecot reload"])
data = recv_socket_data(socket, timeout) return await exec_run_handler('generic', reload_exec)
socket.close()
return data
except Exception as e: # api call: container_post - post_action: exec - cmd: reload - task: postfix
logger.error("error - exec_cmd_container: %s" % str(e)) async def container_post__exec__reload__postfix(self, container_id, request_json):
traceback.print_exc(file=sys.stdout) for container in (await self.docker_client.containers.list()):
def exec_run_handler(type, output): if container._id == container_id:
if type == 'generic': reload_exec = await container.exec(["/bin/bash", "-c", "/usr/sbin/postfix reload"])
if output.exit_code == 0: return await exec_run_handler('generic', reload_exec)
res = { 'type': 'success', 'msg': 'command completed successfully' }
# 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") return Response(content=json.dumps(res, indent=4), media_type="application/json")
else: else:
res = { 'type': 'danger', 'msg': 'command failed: ' + output.output.decode('utf-8') } res = {
"type": "success",
"msg": "'command failed: " + exec_return
}
return Response(content=json.dumps(res, indent=4), media_type="application/json") return Response(content=json.dumps(res, indent=4), media_type="application/json")
if type == 'utf8_text_only': if type == 'utf8_text_only':
return Response(content=output.output.decode('utf-8'), media_type="text/plain") return Response(content=exec_return, media_type="text/plain")
async def get_host_stats(wait=5): async def get_host_stats(wait=5):
global host_stats_isUpdating global host_stats_isUpdating
@@ -491,10 +570,12 @@ async def get_host_stats(wait=5):
"type": "danger", "type": "danger",
"msg": str(e) "msg": str(e)
} }
print(json.dumps(res, indent=4))
await asyncio.sleep(wait) await asyncio.sleep(wait)
host_stats_isUpdating = False host_stats_isUpdating = False
async def get_container_stats(container_id, wait=5, stop=False): async def get_container_stats(container_id, wait=5, stop=False):
global containerIds_to_update global containerIds_to_update
@@ -517,11 +598,13 @@ async def get_container_stats(container_id, wait=5, stop=False):
"type": "danger", "type": "danger",
"msg": str(e) "msg": str(e)
} }
print(json.dumps(res, indent=4))
else: else:
res = { res = {
"type": "danger", "type": "danger",
"msg": "no or invalid id defined" "msg": "no or invalid id defined"
} }
print(json.dumps(res, indent=4))
await asyncio.sleep(wait) await asyncio.sleep(wait)
if stop == True: if stop == True:
@@ -532,13 +615,9 @@ async def get_container_stats(container_id, wait=5, stop=False):
await get_container_stats(container_id, wait=0, stop=True) await get_container_stats(container_id, wait=0, stop=True)
if os.environ['REDIS_SLAVEOF_IP'] != "": if os.environ['REDIS_SLAVEOF_IP'] != "":
redis_client = redis.Redis(host=os.environ['REDIS_SLAVEOF_IP'], port=os.environ['REDIS_SLAVEOF_PORT'], db=0) redis_client = redis.Redis(host=os.environ['REDIS_SLAVEOF_IP'], port=os.environ['REDIS_SLAVEOF_PORT'], db=0)
else: else:
redis_client = redis.Redis(host='redis-mailcow', port=6379, db=0) redis_client = redis.Redis(host='redis-mailcow', port=6379, db=0)
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') async_docker_client = aiodocker.Docker(url='unix:///var/run/docker.sock')
logger.info('DockerApi started')

View File

@@ -21,7 +21,6 @@ RUN groupadd -g 5000 vmail \
&& touch /etc/default/locale \ && touch /etc/default/locale \
&& apt-get update \ && apt-get update \
&& apt-get -y --no-install-recommends install \ && apt-get -y --no-install-recommends install \
build-essential \
apt-transport-https \ apt-transport-https \
ca-certificates \ ca-certificates \
cpanminus \ cpanminus \
@@ -62,7 +61,6 @@ RUN groupadd -g 5000 vmail \
libproc-processtable-perl \ libproc-processtable-perl \
libreadonly-perl \ libreadonly-perl \
libregexp-common-perl \ libregexp-common-perl \
libssl-dev \
libsys-meminfo-perl \ libsys-meminfo-perl \
libterm-readkey-perl \ libterm-readkey-perl \
libtest-deep-perl \ libtest-deep-perl \
@@ -112,8 +110,6 @@ RUN groupadd -g 5000 vmail \
&& apt-get autoclean \ && apt-get autoclean \
&& rm -rf /var/lib/apt/lists/* \ && rm -rf /var/lib/apt/lists/* \
&& rm -rf /tmp/* /var/tmp/* /root/.cache/ && 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 trim_logs.sh /usr/local/bin/trim_logs.sh
COPY clean_q_aged.sh /usr/local/bin/clean_q_aged.sh COPY clean_q_aged.sh /usr/local/bin/clean_q_aged.sh

View File

@@ -8492,7 +8492,6 @@ sub xoauth2
require HTML::Entities ; require HTML::Entities ;
require JSON ; require JSON ;
require JSON::WebToken::Crypt::RSA ; require JSON::WebToken::Crypt::RSA ;
require Crypt::OpenSSL::PKCS12;
require Crypt::OpenSSL::RSA ; require Crypt::OpenSSL::RSA ;
require Encode::Byte ; require Encode::Byte ;
require IO::Socket::SSL ; require IO::Socket::SSL ;
@@ -8533,9 +8532,8 @@ sub xoauth2
$sync->{ debug } and myprint( "Service account: $iss\nKey file: $keyfile\nKey password: $keypass\n"); $sync->{ debug } and myprint( "Service account: $iss\nKey file: $keyfile\nKey password: $keypass\n");
# Get private key from p12 file # Get private key from p12 file (would be better in perl...)
my $pkcs12 = Crypt::OpenSSL::PKCS12->new_from_file($keyfile); $key = `openssl pkcs12 -in "$keyfile" -nodes -nocerts -passin pass:$keypass -nomacver`;
$key = $pkcs12->private_key($keypass);
$sync->{ debug } and myprint( "Private key:\n$key\n"); $sync->{ debug } and myprint( "Private key:\n$key\n");
} }

View File

@@ -64,40 +64,28 @@ def refreshF2boptions():
global f2boptions global f2boptions
global quit_now global quit_now
global exit_code global exit_code
f2boptions = {}
if not r.get('F2B_OPTIONS'): if not r.get('F2B_OPTIONS'):
f2boptions['ban_time'] = r.get('F2B_BAN_TIME') f2boptions = {}
f2boptions['max_ban_time'] = r.get('F2B_MAX_BAN_TIME') f2boptions['ban_time'] = int
f2boptions['ban_time_increment'] = r.get('F2B_BAN_TIME_INCREMENT') f2boptions['max_attempts'] = int
f2boptions['max_attempts'] = r.get('F2B_MAX_ATTEMPTS') f2boptions['retry_window'] = int
f2boptions['retry_window'] = r.get('F2B_RETRY_WINDOW') f2boptions['netban_ipv4'] = int
f2boptions['netban_ipv4'] = r.get('F2B_NETBAN_IPV4') f2boptions['netban_ipv6'] = int
f2boptions['netban_ipv6'] = r.get('F2B_NETBAN_IPV6') 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))
else: else:
try: try:
f2boptions = {}
f2boptions = json.loads(r.get('F2B_OPTIONS')) f2boptions = json.loads(r.get('F2B_OPTIONS'))
except ValueError: except ValueError:
print('Error loading F2B options: F2B_OPTIONS is not json') print('Error loading F2B options: F2B_OPTIONS is not json')
quit_now = True quit_now = True
exit_code = 2 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(): def refreshF2bregex():
global f2bregex global f2bregex
global quit_now global quit_now
@@ -159,7 +147,6 @@ def ban(address):
global lock global lock
refreshF2boptions() refreshF2boptions()
BAN_TIME = int(f2boptions['ban_time']) BAN_TIME = int(f2boptions['ban_time'])
BAN_TIME_INCREMENT = bool(f2boptions['ban_time_increment'])
MAX_ATTEMPTS = int(f2boptions['max_attempts']) MAX_ATTEMPTS = int(f2boptions['max_attempts'])
RETRY_WINDOW = int(f2boptions['retry_window']) RETRY_WINDOW = int(f2boptions['retry_window'])
NETBAN_IPV4 = '/' + str(f2boptions['netban_ipv4']) NETBAN_IPV4 = '/' + str(f2boptions['netban_ipv4'])
@@ -187,16 +174,20 @@ def ban(address):
net = ipaddress.ip_network((address + (NETBAN_IPV4 if type(ip) is ipaddress.IPv4Address else NETBAN_IPV6)), strict=False) net = ipaddress.ip_network((address + (NETBAN_IPV4 if type(ip) is ipaddress.IPv4Address else NETBAN_IPV6)), strict=False)
net = str(net) net = str(net)
if not net in bans: if not net in bans or time.time() - bans[net]['last_attempt'] > RETRY_WINDOW:
bans[net] = {'attempts': 0, 'last_attempt': 0, 'ban_counter': 0} bans[net] = { 'attempts': 0 }
active_window = RETRY_WINDOW
else:
active_window = time.time() - bans[net]['last_attempt']
bans[net]['attempts'] += 1 bans[net]['attempts'] += 1
bans[net]['last_attempt'] = time.time() bans[net]['last_attempt'] = time.time()
active_window = time.time() - bans[net]['last_attempt']
if bans[net]['attempts'] >= MAX_ATTEMPTS: if bans[net]['attempts'] >= MAX_ATTEMPTS:
cur_time = int(round(time.time())) cur_time = int(round(time.time()))
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, BAN_TIME / 60))
logCrit('Banning %s for %d minutes' % (net, NET_BAN_TIME / 60 ))
if type(ip) is ipaddress.IPv4Address: if type(ip) is ipaddress.IPv4Address:
with lock: with lock:
chain = iptc.Chain(iptc.Table(iptc.Table.FILTER), 'MAILCOW') chain = iptc.Chain(iptc.Table(iptc.Table.FILTER), 'MAILCOW')
@@ -215,7 +206,7 @@ def ban(address):
rule.target = target rule.target = target
if rule not in chain.rules: if rule not in chain.rules:
chain.insert_rule(rule) chain.insert_rule(rule)
r.hset('F2B_ACTIVE_BANS', '%s' % net, cur_time + NET_BAN_TIME) r.hset('F2B_ACTIVE_BANS', '%s' % net, cur_time + BAN_TIME)
else: else:
logWarn('%d more attempts in the next %d seconds until %s is banned' % (MAX_ATTEMPTS - bans[net]['attempts'], RETRY_WINDOW, net)) logWarn('%d more attempts in the next %d seconds until %s is banned' % (MAX_ATTEMPTS - bans[net]['attempts'], RETRY_WINDOW, net))
@@ -247,8 +238,7 @@ def unban(net):
r.hdel('F2B_ACTIVE_BANS', '%s' % net) r.hdel('F2B_ACTIVE_BANS', '%s' % net)
r.hdel('F2B_QUEUE_UNBAN', '%s' % net) r.hdel('F2B_QUEUE_UNBAN', '%s' % net)
if net in bans: if net in bans:
bans[net]['attempts'] = 0 del bans[net]
bans[net]['ban_counter'] += 1
def permBan(net, unban=False): def permBan(net, unban=False):
global lock global lock
@@ -342,7 +332,7 @@ def watch():
logWarn('%s matched rule id %s (%s)' % (addr, rule_id, item['data'])) logWarn('%s matched rule id %s (%s)' % (addr, rule_id, item['data']))
ban(addr) ban(addr)
except Exception as ex: except Exception as ex:
logWarn('Error reading log line from pubsub: %s' % ex) logWarn('Error reading log line from pubsub')
quit_now = True quit_now = True
exit_code = 2 exit_code = 2
@@ -369,30 +359,21 @@ def snat4(snat_target):
chain = iptc.Chain(table, 'POSTROUTING') chain = iptc.Chain(table, 'POSTROUTING')
table.autocommit = False table.autocommit = False
new_rule = get_snat4_rule() new_rule = get_snat4_rule()
for position, rule in enumerate(chain.rules):
if not chain.rules: match = all((
# if there are no rules in the chain, insert the new rule directly new_rule.get_src() == rule.get_src(),
logInfo(f'Added POSTROUTING rule for source network {new_rule.src} to SNAT target {snat_target}') new_rule.get_dst() == rule.get_dst(),
chain.insert_rule(new_rule) new_rule.target.parameters == rule.target.parameters,
else: new_rule.target.name == rule.target.name
for position, rule in enumerate(chain.rules): ))
if not hasattr(rule.target, 'parameter'): if position == 0:
continue if not match:
match = all(( logInfo(f'Added POSTROUTING rule for source network {new_rule.src} to SNAT target {snat_target}')
new_rule.get_src() == rule.get_src(), chain.insert_rule(new_rule)
new_rule.get_dst() == rule.get_dst(), else:
new_rule.target.parameters == rule.target.parameters, if match:
new_rule.target.name == rule.target.name 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 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.commit()
table.autocommit = True table.autocommit = True
except: except:
@@ -437,8 +418,6 @@ def autopurge():
time.sleep(10) time.sleep(10)
refreshF2boptions() refreshF2boptions()
BAN_TIME = int(f2boptions['ban_time']) 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']) MAX_ATTEMPTS = int(f2boptions['max_attempts'])
QUEUE_UNBAN = r.hgetall('F2B_QUEUE_UNBAN') QUEUE_UNBAN = r.hgetall('F2B_QUEUE_UNBAN')
if QUEUE_UNBAN: if QUEUE_UNBAN:
@@ -446,9 +425,7 @@ def autopurge():
unban(str(net)) unban(str(net))
for net in bans.copy(): for net in bans.copy():
if bans[net]['attempts'] >= MAX_ATTEMPTS: if bans[net]['attempts'] >= MAX_ATTEMPTS:
NET_BAN_TIME = BAN_TIME if not BAN_TIME_INCREMENT else BAN_TIME * 2 ** bans[net]['ban_counter'] if time.time() - bans[net]['last_attempt'] > BAN_TIME:
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) unban(net)
def isIpNetwork(address): def isIpNetwork(address):

View File

@@ -1,4 +1,4 @@
FROM php:8.2-fpm-alpine3.17 FROM php:8.1-fpm-alpine3.17
LABEL maintainer "Andre Peters <andre.peters@servercow.de>" LABEL maintainer "Andre Peters <andre.peters@servercow.de>"
# renovate: datasource=github-tags depName=krakjoe/apcu versioning=semver-coerced # renovate: datasource=github-tags depName=krakjoe/apcu versioning=semver-coerced
@@ -12,7 +12,7 @@ ARG MEMCACHED_PECL_VERSION=3.2.0
# renovate: datasource=github-tags depName=phpredis/phpredis versioning=semver-coerced # renovate: datasource=github-tags depName=phpredis/phpredis versioning=semver-coerced
ARG REDIS_PECL_VERSION=5.3.7 ARG REDIS_PECL_VERSION=5.3.7
# renovate: datasource=github-tags depName=composer/composer versioning=semver-coerced # renovate: datasource=github-tags depName=composer/composer versioning=semver-coerced
ARG COMPOSER_VERSION=2.5.5 ARG COMPOSER_VERSION=2.5.1
RUN apk add -U --no-cache autoconf \ RUN apk add -U --no-cache autoconf \
aspell-dev \ aspell-dev \
@@ -52,7 +52,6 @@ RUN apk add -U --no-cache autoconf \
libxpm-dev \ libxpm-dev \
libzip \ libzip \
libzip-dev \ libzip-dev \
linux-headers \
make \ make \
mysql-client \ mysql-client \
openldap-dev \ openldap-dev \
@@ -76,7 +75,7 @@ RUN apk add -U --no-cache autoconf \
--with-webp \ --with-webp \
--with-xpm \ --with-xpm \
--with-avif \ --with-avif \
&& 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-install -j 4 exif gd gettext intl ldap opcache pcntl pdo pdo_mysql pspell soap sockets zip bcmath gmp \
&& docker-php-ext-configure imap --with-imap --with-imap-ssl \ && docker-php-ext-configure imap --with-imap --with-imap-ssl \
&& docker-php-ext-install -j 4 imap \ && docker-php-ext-install -j 4 imap \
&& curl --silent --show-error https://getcomposer.org/installer | php -- --version=${COMPOSER_VERSION} \ && curl --silent --show-error https://getcomposer.org/installer | php -- --version=${COMPOSER_VERSION} \
@@ -100,7 +99,6 @@ RUN apk add -U --no-cache autoconf \
libxml2-dev \ libxml2-dev \
libxpm-dev \ libxpm-dev \
libzip-dev \ libzip-dev \
linux-headers \
make \ make \
openldap-dev \ openldap-dev \
pcre-dev \ pcre-dev \

View File

@@ -24,7 +24,7 @@ server {
add_header X-Download-Options "noopen" always; add_header X-Download-Options "noopen" always;
add_header X-Frame-Options "SAMEORIGIN" always; add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Permitted-Cross-Domain-Policies "none" always; add_header X-Permitted-Cross-Domain-Policies "none" always;
add_header X-Robots-Tag "noindex, nofollow" always; add_header X-Robots-Tag "none" always;
add_header X-XSS-Protection "1; mode=block" always; add_header X-XSS-Protection "1; mode=block" always;
fastcgi_hide_header X-Powered-By; fastcgi_hide_header X-Powered-By;

View File

@@ -8,7 +8,7 @@ VIRUS_FOUND {
} }
# Bad policy from free mail providers # Bad policy from free mail providers
FREEMAIL_POLICY_FAILURE { FREEMAIL_POLICY_FAILURE {
expression = "FREEMAIL_FROM & !DMARC_POLICY_ALLOW & !MAILLIST& !WHITELISTED_FWD_HOST & -g+:policies"; expression = "-g+:policies & !DMARC_POLICY_ALLOW & !MAILLIST & ( FREEMAIL_ENVFROM | FREEMAIL_FROM ) & !WHITELISTED_FWD_HOST";
score = 16.0; score = 16.0;
} }
# Applies to freemail with undisclosed recipients # Applies to freemail with undisclosed recipients

View File

@@ -159,8 +159,8 @@ BAZAAR_ABUSE_CH {
} }
URLHAUS_ABUSE_CH { URLHAUS_ABUSE_CH {
type = "selector"; type = "url";
selector = "urls"; filter = "full";
map = "https://urlhaus.abuse.ch/downloads/text_online/"; map = "https://urlhaus.abuse.ch/downloads/text_online/";
score = 10.0; score = 10.0;
} }
@@ -175,7 +175,7 @@ BAD_SUBJECT_00 {
type = "header"; type = "header";
header = "subject"; header = "subject";
regexp = true; regexp = true;
map = "http://fuzzy.mailcow.email/bad-subject-regex.txt"; map = "http://nullnull.org/bad-subject-regex.txt";
score = 6.0; score = 6.0;
symbols_set = ["BAD_SUBJECT_00"]; symbols_set = ["BAD_SUBJECT_00"];
} }

View File

@@ -62,7 +62,7 @@
SOGoFirstDayOfWeek = "1"; SOGoFirstDayOfWeek = "1";
SOGoSieveFolderEncoding = "UTF-8"; SOGoSieveFolderEncoding = "UTF-8";
SOGoPasswordChangeEnabled = NO; SOGoPasswordChangeEnabled = YES;
SOGoSentFolderName = "Sent"; SOGoSentFolderName = "Sent";
SOGoMailShowSubscribedFoldersOnly = NO; SOGoMailShowSubscribedFoldersOnly = NO;
NGImap4ConnectionStringSeparator = "/"; NGImap4ConnectionStringSeparator = "/";

View File

@@ -699,38 +699,6 @@ paths:
type: string type: string
type: object type: object
summary: Create Domain Admin user 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: /api/v1/edit/da-acl:
post: post:
responses: responses:
@@ -2031,7 +1999,7 @@ paths:
- domain.tld - domain.tld
- domain2.tld - domain2.tld
properties: properties:
items: items:
type: array type: array
items: items:
type: string type: string
@@ -3025,7 +2993,7 @@ paths:
application/json: application/json:
schema: schema:
type: array type: array
items: items:
type: object type: object
properties: properties:
log: log:
@@ -3176,10 +3144,8 @@ paths:
example: example:
attr: attr:
ban_time: "86400" ban_time: "86400"
ban_time_increment: "1"
blacklist: "10.100.6.5/32,10.100.8.4/32" blacklist: "10.100.6.5/32,10.100.8.4/32"
max_attempts: "5" max_attempts: "5"
max_ban_time: "86400"
netban_ipv4: "24" netban_ipv4: "24"
netban_ipv6: "64" netban_ipv6: "64"
retry_window: "600" retry_window: "600"
@@ -3193,17 +3159,11 @@ paths:
description: the backlisted ips or hostnames separated by comma description: the backlisted ips or hostnames separated by comma
type: string type: string
ban_time: ban_time:
description: the time an ip should be banned description: the time a ip should be banned
type: number type: number
ban_time_increment:
description: if the time of the ban should increase each time
type: boolean
max_attempts: max_attempts:
description: the maximum numbe of wrong logins before a ip is banned description: the maximum numbe of wrong logins before a ip is banned
type: number type: number
max_ban_time:
description: the maximum time an ip should be banned
type: number
netban_ipv4: netban_ipv4:
description: the networks mask to ban for ipv4 description: the networks mask to ban for ipv4
type: number type: number
@@ -4121,12 +4081,10 @@ paths:
response: response:
value: value:
ban_time: 604800 ban_time: 604800
ban_time_increment: 1
blacklist: |- blacklist: |-
45.82.153.37/32 45.82.153.37/32
92.118.38.52/32 92.118.38.52/32
max_attempts: 1 max_attempts: 1
max_ban_time: 604800
netban_ipv4: 32 netban_ipv4: 32
netban_ipv6: 128 netban_ipv6: 128
perm_bans: perm_bans:
@@ -5628,8 +5586,6 @@ tags:
description: Manage DKIM keys description: Manage DKIM keys
- name: Domain admin - name: Domain admin
description: Create or udpdate domain admin users description: Create or udpdate domain admin users
- name: Single Sign-On
description: Issue tokens for users
- name: Address Rewriting - name: Address Rewriting
description: Create BCC maps or recipient maps description: Create BCC maps or recipient maps
- name: Outgoing TLS Policy Map Overrides - name: Outgoing TLS Policy Map Overrides

View File

@@ -77,22 +77,4 @@ li .dtr-data {
table.dataTable>tbody>tr.child span.dtr-title { table.dataTable>tbody>tr.child span.dtr-title {
width: 30%; width: 30%;
max-width: 250px; 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;
}

View File

@@ -370,3 +370,14 @@ button[aria-expanded='true'] > .caret {
.btn-check:checked+.btn-outline-secondary, .btn-check:active+.btn-outline-secondary, .btn-outline-secondary:active, .btn-outline-secondary.active, .btn-outline-secondary.dropdown-toggle.show { .btn-check:checked+.btn-outline-secondary, .btn-check:active+.btn-outline-secondary, .btn-outline-secondary:active, .btn-outline-secondary.active, .btn-outline-secondary.dropdown-toggle.show {
background-color: #f0f0f0 !important; background-color: #f0f0f0 !important;
} }
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;
}

View File

@@ -203,9 +203,6 @@
text-align: left; text-align: left;
} }
.senders-mw220 {
max-width: 100% !important;
}
} }
@media (max-width: 350px) { @media (max-width: 350px) {

View File

@@ -1,104 +1,102 @@
.pagination a { .pagination a {
text-decoration: none !important; text-decoration: none !important;
} }
.panel.panel-default { .panel.panel-default {
overflow: visible !important; overflow: visible !important;
} }
.table-responsive { .table-responsive {
overflow: visible !important; overflow: visible !important;
} }
.table-responsive { .table-responsive {
overflow-x: scroll !important; overflow-x: scroll !important;
} }
.footer-add-item { .footer-add-item {
display: block; display: block;
text-align: center; text-align: center;
font-style: italic; font-style: italic;
padding: 10px; padding: 10px;
background: #F5F5F5; background: #F5F5F5;
} }
@media (min-width: 992px) { @media (min-width: 992px) {
.container { .container {
width: 100%; width: 100%;
} }
} }
@media (min-width: 1920px) { @media (min-width: 1920px) {
.container { .container {
width: 80%; width: 80%;
} }
} }
.mass-actions-quarantine { .mass-actions-quarantine {
user-select: none; user-select: none;
} }
.inputMissingAttr { .inputMissingAttr {
border-color: #FF4136; border-color: #FF4136;
} }
.modal#qidDetailModal p { .modal#qidDetailModal p {
word-break: break-all; word-break: break-all;
} }
span#qid_detail_score { span#qid_detail_score {
font-weight: 700; font-weight: 700;
margin-left: 5px; margin-left: 5px;
} }
span.rspamd-symbol { span.rspamd-symbol {
display: inline-block; display: inline-block;
margin: 2px 6px 2px 0; margin: 2px 6px 2px 0;
border-radius: 4px; border-radius: 4px;
padding: 0 7px; padding: 0 7px;
} }
span.rspamd-symbol.positive { span.rspamd-symbol.positive {
background: #4CAF50; background: #4CAF50;
border: 1px solid #4CAF50; border: 1px solid #4CAF50;
color: white; color: white;
} }
span.rspamd-symbol.negative { span.rspamd-symbol.negative {
background: #ff4136; background: #ff4136;
border: 1px solid #ff4136; border: 1px solid #ff4136;
color: white; color: white;
} }
span.rspamd-symbol.neutral { span.rspamd-symbol.neutral {
background: #f5f5f5; background: #f5f5f5;
color: #333; color: #333;
border: 1px solid #ccc; border: 1px solid #ccc;
} }
span.rspamd-symbol span.score { span.rspamd-symbol span.score {
font-weight: 700; font-weight: 700;
} }
span.mail-address-item { span.mail-address-item {
background-color: #f5f5f5; background-color: #f5f5f5;
border-radius: 4px; border-radius: 4px;
border: 1px solid #ccc; border: 1px solid #ccc;
padding: 2px 7px; padding: 2px 7px;
display: inline-block; display: inline-block;
margin: 2px 6px 2px 0; margin: 2px 6px 2px 0;
} }
table tbody tr { table tbody tr {
cursor: pointer; cursor: pointer;
} }
table tbody tr td input[type="checkbox"] { table tbody tr td input[type="checkbox"] {
cursor: pointer; cursor: pointer;
} }
.label-rspamd-action { .label-rspamd-action {
font-size:110%; font-size:110%;
margin:20px; margin:20px;
} }
.senders-mw220 {
max-width: 220px;
}

View File

@@ -1,468 +1,407 @@
<?php <?php
function domain_admin($_action, $_data = null) { function domain_admin($_action, $_data = null) {
global $pdo; global $pdo;
global $lang; global $lang;
$_data_log = $_data; $_data_log = $_data;
!isset($_data_log['password']) ?: $_data_log['password'] = '*'; !isset($_data_log['password']) ?: $_data_log['password'] = '*';
!isset($_data_log['password2']) ?: $_data_log['password2'] = '*'; !isset($_data_log['password2']) ?: $_data_log['password2'] = '*';
!isset($_data_log['user_old_pass']) ?: $_data_log['user_old_pass'] = '*'; !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_pass']) ?: $_data_log['user_new_pass'] = '*';
!isset($_data_log['user_new_pass2']) ?: $_data_log['user_new_pass2'] = '*'; !isset($_data_log['user_new_pass2']) ?: $_data_log['user_new_pass2'] = '*';
switch ($_action) { switch ($_action) {
case 'add': case 'add':
$username = strtolower(trim($_data['username'])); $username = strtolower(trim($_data['username']));
$password = $_data['password']; $password = $_data['password'];
$password2 = $_data['password2']; $password2 = $_data['password2'];
$domains = (array)$_data['domains']; $domains = (array)$_data['domains'];
$active = intval($_data['active']); $active = intval($_data['active']);
if ($_SESSION['mailcow_cc_role'] != "admin") { if ($_SESSION['mailcow_cc_role'] != "admin") {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data_log), 'log' => array(__FUNCTION__, $_action, $_data_log),
'msg' => 'access_denied' 'msg' => 'access_denied'
); );
return false; return false;
} }
if (empty($domains)) { if (empty($domains)) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data_log), 'log' => array(__FUNCTION__, $_action, $_data_log),
'msg' => 'domain_invalid' 'msg' => 'domain_invalid'
); );
return false; return false;
} }
if (!ctype_alnum(str_replace(array('_', '.', '-'), '', $username)) || empty ($username) || $username == 'API') { if (!ctype_alnum(str_replace(array('_', '.', '-'), '', $username)) || empty ($username) || $username == 'API') {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data_log), 'log' => array(__FUNCTION__, $_action, $_data_log),
'msg' => array('username_invalid', $username) 'msg' => array('username_invalid', $username)
); );
return false; return false;
} }
$stmt = $pdo->prepare("SELECT `username` FROM `mailbox` $stmt = $pdo->prepare("SELECT `username` FROM `mailbox`
WHERE `username` = :username"); WHERE `username` = :username");
$stmt->execute(array(':username' => $username)); $stmt->execute(array(':username' => $username));
$num_results[] = count($stmt->fetchAll(PDO::FETCH_ASSOC)); $num_results[] = count($stmt->fetchAll(PDO::FETCH_ASSOC));
$stmt = $pdo->prepare("SELECT `username` FROM `admin` $stmt = $pdo->prepare("SELECT `username` FROM `admin`
WHERE `username` = :username"); WHERE `username` = :username");
$stmt->execute(array(':username' => $username)); $stmt->execute(array(':username' => $username));
$num_results[] = count($stmt->fetchAll(PDO::FETCH_ASSOC)); $num_results[] = count($stmt->fetchAll(PDO::FETCH_ASSOC));
$stmt = $pdo->prepare("SELECT `username` FROM `domain_admins` $stmt = $pdo->prepare("SELECT `username` FROM `domain_admins`
WHERE `username` = :username"); WHERE `username` = :username");
$stmt->execute(array(':username' => $username)); $stmt->execute(array(':username' => $username));
$num_results[] = count($stmt->fetchAll(PDO::FETCH_ASSOC)); $num_results[] = count($stmt->fetchAll(PDO::FETCH_ASSOC));
foreach ($num_results as $num_results_each) { foreach ($num_results as $num_results_each) {
if ($num_results_each != 0) { if ($num_results_each != 0) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data_log), 'log' => array(__FUNCTION__, $_action, $_data_log),
'msg' => array('object_exists', htmlspecialchars($username)) 'msg' => array('object_exists', htmlspecialchars($username))
); );
return false; return false;
} }
} }
if (password_check($password, $password2) !== true) { if (password_check($password, $password2) !== true) {
continue; continue;
} }
$password_hashed = hash_password($password); $password_hashed = hash_password($password);
$valid_domains = 0; $valid_domains = 0;
foreach ($domains as $domain) { foreach ($domains as $domain) {
if (!is_valid_domain_name($domain) || mailbox('get', 'domain_details', $domain) === false) { if (!is_valid_domain_name($domain) || mailbox('get', 'domain_details', $domain) === false) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data_log), 'log' => array(__FUNCTION__, $_action, $_data_log),
'msg' => array('domain_invalid', htmlspecialchars($domain)) 'msg' => array('domain_invalid', htmlspecialchars($domain))
); );
continue; continue;
} }
$valid_domains++; $valid_domains++;
$stmt = $pdo->prepare("INSERT INTO `domain_admins` (`username`, `domain`, `created`, `active`) $stmt = $pdo->prepare("INSERT INTO `domain_admins` (`username`, `domain`, `created`, `active`)
VALUES (:username, :domain, :created, :active)"); VALUES (:username, :domain, :created, :active)");
$stmt->execute(array( $stmt->execute(array(
':username' => $username, ':username' => $username,
':domain' => $domain, ':domain' => $domain,
':created' => date('Y-m-d H:i:s'), ':created' => date('Y-m-d H:i:s'),
':active' => $active ':active' => $active
)); ));
} }
if ($valid_domains != 0) { if ($valid_domains != 0) {
$stmt = $pdo->prepare("INSERT INTO `admin` (`username`, `password`, `superadmin`, `active`) $stmt = $pdo->prepare("INSERT INTO `admin` (`username`, `password`, `superadmin`, `active`)
VALUES (:username, :password_hashed, '0', :active)"); VALUES (:username, :password_hashed, '0', :active)");
$stmt->execute(array( $stmt->execute(array(
':username' => $username, ':username' => $username,
':password_hashed' => $password_hashed, ':password_hashed' => $password_hashed,
':active' => $active ':active' => $active
)); ));
} }
$stmt = $pdo->prepare("INSERT INTO `da_acl` (`username`) VALUES (:username)"); $stmt = $pdo->prepare("INSERT INTO `da_acl` (`username`) VALUES (:username)");
$stmt->execute(array( $stmt->execute(array(
':username' => $username ':username' => $username
)); ));
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'success', 'type' => 'success',
'log' => array(__FUNCTION__, $_action, $_data_log), 'log' => array(__FUNCTION__, $_action, $_data_log),
'msg' => array('domain_admin_added', htmlspecialchars($username)) 'msg' => array('domain_admin_added', htmlspecialchars($username))
); );
break; break;
case 'edit': case 'edit':
if ($_SESSION['mailcow_cc_role'] != "admin" && $_SESSION['mailcow_cc_role'] != "domainadmin") { if ($_SESSION['mailcow_cc_role'] != "admin" && $_SESSION['mailcow_cc_role'] != "domainadmin") {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data_log), 'log' => array(__FUNCTION__, $_action, $_data_log),
'msg' => 'access_denied' 'msg' => 'access_denied'
); );
return false; return false;
} }
// Administrator // Administrator
if ($_SESSION['mailcow_cc_role'] == "admin") { if ($_SESSION['mailcow_cc_role'] == "admin") {
if (!is_array($_data['username'])) { if (!is_array($_data['username'])) {
$usernames = array(); $usernames = array();
$usernames[] = $_data['username']; $usernames[] = $_data['username'];
} }
else { else {
$usernames = $_data['username']; $usernames = $_data['username'];
} }
foreach ($usernames as $username) { foreach ($usernames as $username) {
$is_now = domain_admin('details', $username); $is_now = domain_admin('details', $username);
$domains = (isset($_data['domains'])) ? (array)$_data['domains'] : null; $domains = (isset($_data['domains'])) ? (array)$_data['domains'] : null;
if (!empty($is_now)) { if (!empty($is_now)) {
$active = (isset($_data['active'])) ? intval($_data['active']) : $is_now['active']; $active = (isset($_data['active'])) ? intval($_data['active']) : $is_now['active'];
$domains = (!empty($domains)) ? $domains : $is_now['selected_domains']; $domains = (!empty($domains)) ? $domains : $is_now['selected_domains'];
$username_new = (!empty($_data['username_new'])) ? $_data['username_new'] : $is_now['username']; $username_new = (!empty($_data['username_new'])) ? $_data['username_new'] : $is_now['username'];
} }
else { else {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data_log), 'log' => array(__FUNCTION__, $_action, $_data_log),
'msg' => 'access_denied' 'msg' => 'access_denied'
); );
continue; continue;
} }
$password = $_data['password']; $password = $_data['password'];
$password2 = $_data['password2']; $password2 = $_data['password2'];
if (!empty($domains)) { if (!empty($domains)) {
foreach ($domains as $domain) { foreach ($domains as $domain) {
if (!is_valid_domain_name($domain) || mailbox('get', 'domain_details', $domain) === false) { if (!is_valid_domain_name($domain) || mailbox('get', 'domain_details', $domain) === false) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data_log), 'log' => array(__FUNCTION__, $_action, $_data_log),
'msg' => array('domain_invalid', htmlspecialchars($domain)) 'msg' => array('domain_invalid', htmlspecialchars($domain))
); );
continue 2; continue 2;
} }
} }
} }
if (!ctype_alnum(str_replace(array('_', '.', '-'), '', $username_new))) { if (!ctype_alnum(str_replace(array('_', '.', '-'), '', $username_new))) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data_log), 'log' => array(__FUNCTION__, $_action, $_data_log),
'msg' => array('username_invalid', $username_new) 'msg' => array('username_invalid', $username_new)
); );
continue; continue;
} }
if ($username_new != $username) { if ($username_new != $username) {
if (!empty(domain_admin('details', $username_new)['username'])) { if (!empty(domain_admin('details', $username_new)['username'])) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data_log), 'log' => array(__FUNCTION__, $_action, $_data_log),
'msg' => array('username_invalid', $username_new) 'msg' => array('username_invalid', $username_new)
); );
continue; continue;
} }
} }
$stmt = $pdo->prepare("DELETE FROM `domain_admins` WHERE `username` = :username"); $stmt = $pdo->prepare("DELETE FROM `domain_admins` WHERE `username` = :username");
$stmt->execute(array( $stmt->execute(array(
':username' => $username, ':username' => $username,
)); ));
$stmt = $pdo->prepare("UPDATE `da_acl` SET `username` = :username_new WHERE `username` = :username"); $stmt = $pdo->prepare("UPDATE `da_acl` SET `username` = :username_new WHERE `username` = :username");
$stmt->execute(array( $stmt->execute(array(
':username_new' => $username_new, ':username_new' => $username_new,
':username' => $username ':username' => $username
)); ));
if (!empty($domains)) { if (!empty($domains)) {
foreach ($domains as $domain) { foreach ($domains as $domain) {
$stmt = $pdo->prepare("INSERT INTO `domain_admins` (`username`, `domain`, `created`, `active`) $stmt = $pdo->prepare("INSERT INTO `domain_admins` (`username`, `domain`, `created`, `active`)
VALUES (:username_new, :domain, :created, :active)"); VALUES (:username_new, :domain, :created, :active)");
$stmt->execute(array( $stmt->execute(array(
':username_new' => $username_new, ':username_new' => $username_new,
':domain' => $domain, ':domain' => $domain,
':created' => date('Y-m-d H:i:s'), ':created' => date('Y-m-d H:i:s'),
':active' => $active ':active' => $active
)); ));
} }
} }
if (!empty($password)) { if (!empty($password)) {
if (password_check($password, $password2) !== true) { if (password_check($password, $password2) !== true) {
return false; return false;
} }
$password_hashed = hash_password($password); $password_hashed = hash_password($password);
$stmt = $pdo->prepare("UPDATE `admin` SET `username` = :username_new, `active` = :active, `password` = :password_hashed WHERE `username` = :username"); $stmt = $pdo->prepare("UPDATE `admin` SET `username` = :username_new, `active` = :active, `password` = :password_hashed WHERE `username` = :username");
$stmt->execute(array( $stmt->execute(array(
':password_hashed' => $password_hashed, ':password_hashed' => $password_hashed,
':username_new' => $username_new, ':username_new' => $username_new,
':username' => $username, ':username' => $username,
':active' => $active ':active' => $active
)); ));
if (isset($_data['disable_tfa'])) { if (isset($_data['disable_tfa'])) {
$stmt = $pdo->prepare("UPDATE `tfa` SET `active` = '0' WHERE `username` = :username"); $stmt = $pdo->prepare("UPDATE `tfa` SET `active` = '0' WHERE `username` = :username");
$stmt->execute(array(':username' => $username)); $stmt->execute(array(':username' => $username));
} }
else { else {
$stmt = $pdo->prepare("UPDATE `tfa` SET `username` = :username_new WHERE `username` = :username"); $stmt = $pdo->prepare("UPDATE `tfa` SET `username` = :username_new WHERE `username` = :username");
$stmt->execute(array(':username_new' => $username_new, ':username' => $username)); $stmt->execute(array(':username_new' => $username_new, ':username' => $username));
} }
} }
else { else {
$stmt = $pdo->prepare("UPDATE `admin` SET `username` = :username_new, `active` = :active WHERE `username` = :username"); $stmt = $pdo->prepare("UPDATE `admin` SET `username` = :username_new, `active` = :active WHERE `username` = :username");
$stmt->execute(array( $stmt->execute(array(
':username_new' => $username_new, ':username_new' => $username_new,
':username' => $username, ':username' => $username,
':active' => $active ':active' => $active
)); ));
if (isset($_data['disable_tfa'])) { if (isset($_data['disable_tfa'])) {
$stmt = $pdo->prepare("UPDATE `tfa` SET `active` = '0' WHERE `username` = :username"); $stmt = $pdo->prepare("UPDATE `tfa` SET `active` = '0' WHERE `username` = :username");
$stmt->execute(array(':username' => $username)); $stmt->execute(array(':username' => $username));
} }
else { else {
$stmt = $pdo->prepare("UPDATE `tfa` SET `username` = :username_new WHERE `username` = :username"); $stmt = $pdo->prepare("UPDATE `tfa` SET `username` = :username_new WHERE `username` = :username");
$stmt->execute(array(':username_new' => $username_new, ':username' => $username)); $stmt->execute(array(':username_new' => $username_new, ':username' => $username));
} }
} }
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'success', 'type' => 'success',
'log' => array(__FUNCTION__, $_action, $_data_log), 'log' => array(__FUNCTION__, $_action, $_data_log),
'msg' => array('domain_admin_modified', htmlspecialchars($username)) 'msg' => array('domain_admin_modified', htmlspecialchars($username))
); );
} }
return true; return true;
} }
// Domain administrator // Domain administrator
// Can only edit itself // Can only edit itself
elseif ($_SESSION['mailcow_cc_role'] == "domainadmin") { elseif ($_SESSION['mailcow_cc_role'] == "domainadmin") {
$username = $_SESSION['mailcow_cc_username']; $username = $_SESSION['mailcow_cc_username'];
$password_old = $_data['user_old_pass']; $password_old = $_data['user_old_pass'];
$password_new = $_data['user_new_pass']; $password_new = $_data['user_new_pass'];
$password_new2 = $_data['user_new_pass2']; $password_new2 = $_data['user_new_pass2'];
$stmt = $pdo->prepare("SELECT `password` FROM `admin` $stmt = $pdo->prepare("SELECT `password` FROM `admin`
WHERE `username` = :user"); WHERE `username` = :user");
$stmt->execute(array(':user' => $username)); $stmt->execute(array(':user' => $username));
$row = $stmt->fetch(PDO::FETCH_ASSOC); $row = $stmt->fetch(PDO::FETCH_ASSOC);
if (!verify_hash($row['password'], $password_old)) { if (!verify_hash($row['password'], $password_old)) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data_log), 'log' => array(__FUNCTION__, $_action, $_data_log),
'msg' => 'access_denied' 'msg' => 'access_denied'
); );
return false; return false;
} }
if (password_check($password_new, $password_new2) !== true) { if (password_check($password_new, $password_new2) !== true) {
return false; return false;
} }
$password_hashed = hash_password($password_new); $password_hashed = hash_password($password_new);
$stmt = $pdo->prepare("UPDATE `admin` SET `password` = :password_hashed WHERE `username` = :username"); $stmt = $pdo->prepare("UPDATE `admin` SET `password` = :password_hashed WHERE `username` = :username");
$stmt->execute(array( $stmt->execute(array(
':password_hashed' => $password_hashed, ':password_hashed' => $password_hashed,
':username' => $username ':username' => $username
)); ));
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'success', 'type' => 'success',
'log' => array(__FUNCTION__, $_action, $_data_log), 'log' => array(__FUNCTION__, $_action, $_data_log),
'msg' => array('domain_admin_modified', htmlspecialchars($username)) 'msg' => array('domain_admin_modified', htmlspecialchars($username))
); );
} }
break; break;
case 'delete': case 'delete':
if ($_SESSION['mailcow_cc_role'] != "admin") { if ($_SESSION['mailcow_cc_role'] != "admin") {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data_log), 'log' => array(__FUNCTION__, $_action, $_data_log),
'msg' => 'access_denied' 'msg' => 'access_denied'
); );
return false; return false;
} }
$usernames = (array)$_data['username']; $usernames = (array)$_data['username'];
foreach ($usernames as $username) { foreach ($usernames as $username) {
if (empty(domain_admin('details', $username))) { if (empty(domain_admin('details', $username))) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data_log), 'log' => array(__FUNCTION__, $_action, $_data_log),
'msg' => array('username_invalid', $username) 'msg' => array('username_invalid', $username)
); );
continue; continue;
} }
$stmt = $pdo->prepare("DELETE FROM `domain_admins` WHERE `username` = :username"); $stmt = $pdo->prepare("DELETE FROM `domain_admins` WHERE `username` = :username");
$stmt->execute(array( $stmt->execute(array(
':username' => $username, ':username' => $username,
)); ));
$stmt = $pdo->prepare("DELETE FROM `admin` WHERE `username` = :username"); $stmt = $pdo->prepare("DELETE FROM `admin` WHERE `username` = :username");
$stmt->execute(array( $stmt->execute(array(
':username' => $username, ':username' => $username,
)); ));
$stmt = $pdo->prepare("DELETE FROM `da_acl` WHERE `username` = :username"); $stmt = $pdo->prepare("DELETE FROM `da_acl` WHERE `username` = :username");
$stmt->execute(array( $stmt->execute(array(
':username' => $username, ':username' => $username,
)); ));
$stmt = $pdo->prepare("DELETE FROM `tfa` WHERE `username` = :username"); $stmt = $pdo->prepare("DELETE FROM `tfa` WHERE `username` = :username");
$stmt->execute(array( $stmt->execute(array(
':username' => $username, ':username' => $username,
)); ));
$stmt = $pdo->prepare("DELETE FROM `fido2` WHERE `username` = :username"); $stmt = $pdo->prepare("DELETE FROM `fido2` WHERE `username` = :username");
$stmt->execute(array( $stmt->execute(array(
':username' => $username, ':username' => $username,
)); ));
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'success', 'type' => 'success',
'log' => array(__FUNCTION__, $_action, $_data_log), 'log' => array(__FUNCTION__, $_action, $_data_log),
'msg' => array('domain_admin_removed', htmlspecialchars($username)) 'msg' => array('domain_admin_removed', htmlspecialchars($username))
); );
} }
break; break;
case 'get': case 'get':
$domainadmins = array(); $domainadmins = array();
if ($_SESSION['mailcow_cc_role'] != "admin") { if ($_SESSION['mailcow_cc_role'] != "admin") {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data_log), 'log' => array(__FUNCTION__, $_action, $_data_log),
'msg' => 'access_denied' 'msg' => 'access_denied'
); );
return false; return false;
} }
$stmt = $pdo->query("SELECT DISTINCT $stmt = $pdo->query("SELECT DISTINCT
`username` `username`
FROM `domain_admins` FROM `domain_admins`
WHERE `username` IN ( WHERE `username` IN (
SELECT `username` FROM `admin` SELECT `username` FROM `admin`
WHERE `superadmin`!='1' WHERE `superadmin`!='1'
)"); )");
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC); $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
while ($row = array_shift($rows)) { while ($row = array_shift($rows)) {
$domainadmins[] = $row['username']; $domainadmins[] = $row['username'];
} }
return $domainadmins; return $domainadmins;
break; break;
case 'details': case 'details':
$domainadmindata = array(); $domainadmindata = array();
if ($_SESSION['mailcow_cc_role'] == "domainadmin" && $_data != $_SESSION['mailcow_cc_username']) { if ($_SESSION['mailcow_cc_role'] == "domainadmin" && $_data != $_SESSION['mailcow_cc_username']) {
return false; return false;
} }
elseif ($_SESSION['mailcow_cc_role'] != "admin" || !isset($_data)) { elseif ($_SESSION['mailcow_cc_role'] != "admin" || !isset($_data)) {
return false; return false;
} }
if (!ctype_alnum(str_replace(array('_', '.', '-'), '', $_data))) { if (!ctype_alnum(str_replace(array('_', '.', '-'), '', $_data))) {
return false; return false;
} }
$stmt = $pdo->prepare("SELECT $stmt = $pdo->prepare("SELECT
`tfa`.`active` AS `tfa_active`, `tfa`.`active` AS `tfa_active`,
`domain_admins`.`username`, `domain_admins`.`username`,
`domain_admins`.`created`, `domain_admins`.`created`,
`domain_admins`.`active` AS `active` `domain_admins`.`active` AS `active`
FROM `domain_admins` FROM `domain_admins`
LEFT OUTER JOIN `tfa` ON `tfa`.`username`=`domain_admins`.`username` LEFT OUTER JOIN `tfa` ON `tfa`.`username`=`domain_admins`.`username`
WHERE `domain_admins`.`username`= :domain_admin"); WHERE `domain_admins`.`username`= :domain_admin");
$stmt->execute(array( $stmt->execute(array(
':domain_admin' => $_data ':domain_admin' => $_data
)); ));
$row = $stmt->fetch(PDO::FETCH_ASSOC); $row = $stmt->fetch(PDO::FETCH_ASSOC);
if (empty($row)) { if (empty($row)) {
return false; return false;
} }
$domainadmindata['username'] = $row['username']; $domainadmindata['username'] = $row['username'];
$domainadmindata['tfa_active'] = (is_null($row['tfa_active'])) ? 0 : $row['tfa_active']; $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['tfa_active_int'] = (is_null($row['tfa_active'])) ? 0 : $row['tfa_active'];
$domainadmindata['active'] = $row['active']; $domainadmindata['active'] = $row['active'];
$domainadmindata['active_int'] = $row['active']; $domainadmindata['active_int'] = $row['active'];
$domainadmindata['created'] = $row['created']; $domainadmindata['created'] = $row['created'];
// GET SELECTED // GET SELECTED
$stmt = $pdo->prepare("SELECT `domain` FROM `domain` $stmt = $pdo->prepare("SELECT `domain` FROM `domain`
WHERE `domain` IN ( WHERE `domain` IN (
SELECT `domain` FROM `domain_admins` SELECT `domain` FROM `domain_admins`
WHERE `username`= :domain_admin)"); WHERE `username`= :domain_admin)");
$stmt->execute(array(':domain_admin' => $_data)); $stmt->execute(array(':domain_admin' => $_data));
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC); $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
while($row = array_shift($rows)) { while($row = array_shift($rows)) {
$domainadmindata['selected_domains'][] = $row['domain']; $domainadmindata['selected_domains'][] = $row['domain'];
} }
// GET UNSELECTED // GET UNSELECTED
$stmt = $pdo->prepare("SELECT `domain` FROM `domain` $stmt = $pdo->prepare("SELECT `domain` FROM `domain`
WHERE `domain` NOT IN ( WHERE `domain` NOT IN (
SELECT `domain` FROM `domain_admins` SELECT `domain` FROM `domain_admins`
WHERE `username`= :domain_admin)"); WHERE `username`= :domain_admin)");
$stmt->execute(array(':domain_admin' => $_data)); $stmt->execute(array(':domain_admin' => $_data));
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC); $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
while($row = array_shift($rows)) { while($row = array_shift($rows)) {
$domainadmindata['unselected_domains'][] = $row['domain']; $domainadmindata['unselected_domains'][] = $row['domain'];
} }
if (!isset($domainadmindata['unselected_domains'])) { if (!isset($domainadmindata['unselected_domains'])) {
$domainadmindata['unselected_domains'] = ""; $domainadmindata['unselected_domains'] = "";
} }
return $domainadmindata; return $domainadmindata;
break; 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;
}
}

View File

@@ -239,9 +239,7 @@ function fail2ban($_action, $_data = null) {
$is_now = fail2ban('get'); $is_now = fail2ban('get');
if (!empty($is_now)) { if (!empty($is_now)) {
$ban_time = intval((isset($_data['ban_time'])) ? $_data['ban_time'] : $is_now['ban_time']); $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_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']); $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_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']); $netban_ipv6 = intval((isset($_data['netban_ipv6'])) ? $_data['netban_ipv6'] : $is_now['netban_ipv6']);
@@ -258,8 +256,6 @@ function fail2ban($_action, $_data = null) {
} }
$f2b_options = array(); $f2b_options = array();
$f2b_options['ban_time'] = ($ban_time < 60) ? 60 : $ban_time; $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_ipv4'] = ($netban_ipv4 < 8) ? 8 : $netban_ipv4;
$f2b_options['netban_ipv6'] = ($netban_ipv6 < 8) ? 8 : $netban_ipv6; $f2b_options['netban_ipv6'] = ($netban_ipv6 < 8) ? 8 : $netban_ipv6;
$f2b_options['netban_ipv4'] = ($netban_ipv4 > 32) ? 32 : $netban_ipv4; $f2b_options['netban_ipv4'] = ($netban_ipv4 > 32) ? 32 : $netban_ipv4;

View File

@@ -1739,7 +1739,7 @@ function verify_tfa_login($username, $_data) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
'log' => array(__FUNCTION__, $username, '*'), 'log' => array(__FUNCTION__, $username, '*'),
'msg' => array('webauthn_authenticator_failed') 'msg' => array('webauthn_verification_failed', 'authenticator not found')
); );
return false; return false;
} }
@@ -1748,20 +1748,11 @@ function verify_tfa_login($username, $_data) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
'log' => array(__FUNCTION__, $username, '*'), 'log' => array(__FUNCTION__, $username, '*'),
'msg' => array('webauthn_publickey_failed') 'msg' => array('webauthn_verification_failed', 'publicKey not found')
); );
return false; 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 { try {
$WebAuthn->processGet($clientDataJSON, $authenticatorData, $signature, $process_webauthn['publicKey'], $challenge, null, $GLOBALS['WEBAUTHN_UV_FLAG_LOGIN'], $GLOBALS['WEBAUTHN_USER_PRESENT_FLAG']); $WebAuthn->processGet($clientDataJSON, $authenticatorData, $signature, $process_webauthn['publicKey'], $challenge, null, $GLOBALS['WEBAUTHN_UV_FLAG_LOGIN'], $GLOBALS['WEBAUTHN_USER_PRESENT_FLAG']);
} }
@@ -1793,12 +1784,21 @@ function verify_tfa_login($username, $_data) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
'log' => array(__FUNCTION__, $username, '*'), 'log' => array(__FUNCTION__, $username, '*'),
'msg' => array('webauthn_role_failed') 'msg' => array('webauthn_verification_failed', 'could not determine user role')
); );
return false; 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["mailcow_cc_username"] = $process_webauthn['username'];
$_SESSION['tfa_id'] = $process_webauthn['id']; $_SESSION['tfa_id'] = $process_webauthn['id'];
$_SESSION['authReq'] = null; $_SESSION['authReq'] = null;

View File

@@ -5264,7 +5264,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
} }
break; break;
} }
if ($_action != 'get' && in_array($_type, array('domain', 'alias', 'alias_domain', 'mailbox', 'resource')) && getenv('SKIP_SOGO') != "y") { if ($_action != 'get' && in_array($_type, array('domain', 'alias', 'alias_domain', 'mailbox', 'resource'))) {
update_sogo_static_view(); update_sogo_static_view();
} }
} }

File diff suppressed because it is too large Load Diff

View File

@@ -1,140 +1,140 @@
<?php <?php
// Start session // Start session
if (session_status() !== PHP_SESSION_ACTIVE) { if (session_status() !== PHP_SESSION_ACTIVE) {
ini_set("session.cookie_httponly", 1); ini_set("session.cookie_httponly", 1);
ini_set('session.gc_maxlifetime', $SESSION_LIFETIME); ini_set('session.gc_maxlifetime', $SESSION_LIFETIME);
} }
if (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && if (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) &&
strtolower($_SERVER['HTTP_X_FORWARDED_PROTO']) == "https") { strtolower($_SERVER['HTTP_X_FORWARDED_PROTO']) == "https") {
if (session_status() !== PHP_SESSION_ACTIVE) { if (session_status() !== PHP_SESSION_ACTIVE) {
ini_set("session.cookie_secure", 1); ini_set("session.cookie_secure", 1);
} }
$IS_HTTPS = true; $IS_HTTPS = true;
} }
elseif (isset($_SERVER['HTTPS'])) { elseif (isset($_SERVER['HTTPS'])) {
if (session_status() !== PHP_SESSION_ACTIVE) { if (session_status() !== PHP_SESSION_ACTIVE) {
ini_set("session.cookie_secure", 1); ini_set("session.cookie_secure", 1);
} }
$IS_HTTPS = true; $IS_HTTPS = true;
} }
else { else {
$IS_HTTPS = false; $IS_HTTPS = false;
} }
if (session_status() !== PHP_SESSION_ACTIVE) { if (session_status() !== PHP_SESSION_ACTIVE) {
session_start(); session_start();
} }
if (!isset($_SESSION['CSRF']['TOKEN'])) { if (!isset($_SESSION['CSRF']['TOKEN'])) {
$_SESSION['CSRF']['TOKEN'] = bin2hex(random_bytes(32)); $_SESSION['CSRF']['TOKEN'] = bin2hex(random_bytes(32));
} }
// Set session UA // Set session UA
if (!isset($_SESSION['SESS_REMOTE_UA'])) { if (!isset($_SESSION['SESS_REMOTE_UA'])) {
$_SESSION['SESS_REMOTE_UA'] = $_SERVER['HTTP_USER_AGENT']; $_SESSION['SESS_REMOTE_UA'] = $_SERVER['HTTP_USER_AGENT'];
} }
// Keep session active // Keep session active
if (isset($_SESSION['LAST_ACTIVITY']) && (time() - $_SESSION['LAST_ACTIVITY'] > $SESSION_LIFETIME)) { if (isset($_SESSION['LAST_ACTIVITY']) && (time() - $_SESSION['LAST_ACTIVITY'] > $SESSION_LIFETIME)) {
session_unset(); session_unset();
session_destroy(); session_destroy();
} }
$_SESSION['LAST_ACTIVITY'] = time(); $_SESSION['LAST_ACTIVITY'] = time();
// API // API
if (!empty($_SERVER['HTTP_X_API_KEY'])) { if (!empty($_SERVER['HTTP_X_API_KEY'])) {
$stmt = $pdo->prepare("SELECT * FROM `api` WHERE `api_key` = :api_key AND `active` = '1';"); $stmt = $pdo->prepare("SELECT * FROM `api` WHERE `api_key` = :api_key AND `active` = '1';");
$stmt->execute(array( $stmt->execute(array(
':api_key' => preg_replace('/[^a-zA-Z0-9-]/', '', $_SERVER['HTTP_X_API_KEY']) ':api_key' => preg_replace('/[^a-zA-Z0-9-]/', '', $_SERVER['HTTP_X_API_KEY'])
)); ));
$api_return = $stmt->fetch(PDO::FETCH_ASSOC); $api_return = $stmt->fetch(PDO::FETCH_ASSOC);
if (!empty($api_return['api_key'])) { if (!empty($api_return['api_key'])) {
$skip_ip_check = ($api_return['skip_ip_check'] == 1); $skip_ip_check = ($api_return['skip_ip_check'] == 1);
$remote = get_remote_ip(false); $remote = get_remote_ip(false);
$allow_from = array_map('trim', preg_split( "/( |,|;|\n)/", $api_return['allow_from'])); $allow_from = array_map('trim', preg_split( "/( |,|;|\n)/", $api_return['allow_from']));
if ($skip_ip_check === true || ip_acl($remote, $allow_from)) { if ($skip_ip_check === true || ip_acl($remote, $allow_from)) {
$_SESSION['mailcow_cc_username'] = 'API'; $_SESSION['mailcow_cc_username'] = 'API';
$_SESSION['mailcow_cc_role'] = 'admin'; $_SESSION['mailcow_cc_role'] = 'admin';
$_SESSION['mailcow_cc_api'] = true; $_SESSION['mailcow_cc_api'] = true;
if ($api_return['access'] == 'rw') { if ($api_return['access'] == 'rw') {
$_SESSION['mailcow_cc_api_access'] = 'rw'; $_SESSION['mailcow_cc_api_access'] = 'rw';
} }
else { else {
$_SESSION['mailcow_cc_api_access'] = 'ro'; $_SESSION['mailcow_cc_api_access'] = 'ro';
} }
} }
else { else {
$redis->publish("F2B_CHANNEL", "mailcow UI: Invalid password for API_USER by " . $_SERVER['REMOTE_ADDR']); $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']); error_log("mailcow UI: Invalid password for " . $user . " by " . $_SERVER['REMOTE_ADDR']);
http_response_code(401); http_response_code(401);
echo json_encode(array( echo json_encode(array(
'type' => 'error', 'type' => 'error',
'msg' => 'api access denied for ip ' . $_SERVER['REMOTE_ADDR'] 'msg' => 'api access denied for ip ' . $_SERVER['REMOTE_ADDR']
)); ));
unset($_POST); unset($_POST);
exit(); exit();
} }
} }
else { else {
$redis->publish("F2B_CHANNEL", "mailcow UI: Invalid password for API_USER by " . $_SERVER['REMOTE_ADDR']); $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']); error_log("mailcow UI: Invalid password for " . $user . " by " . $_SERVER['REMOTE_ADDR']);
http_response_code(401); http_response_code(401);
echo json_encode(array( echo json_encode(array(
'type' => 'error', 'type' => 'error',
'msg' => 'authentication failed' 'msg' => 'authentication failed'
)); ));
unset($_POST); unset($_POST);
exit(); exit();
} }
} }
// Handle logouts // Handle logouts
if (isset($_POST["logout"])) { if (isset($_POST["logout"])) {
if (isset($_SESSION["dual-login"])) { if (isset($_SESSION["dual-login"])) {
$_SESSION["mailcow_cc_username"] = $_SESSION["dual-login"]["username"]; $_SESSION["mailcow_cc_username"] = $_SESSION["dual-login"]["username"];
$_SESSION["mailcow_cc_role"] = $_SESSION["dual-login"]["role"]; $_SESSION["mailcow_cc_role"] = $_SESSION["dual-login"]["role"];
unset($_SESSION["dual-login"]); unset($_SESSION["dual-login"]);
header("Location: /mailbox"); header("Location: /mailbox");
exit(); exit();
} }
else { else {
session_regenerate_id(true); session_regenerate_id(true);
session_unset(); session_unset();
session_destroy(); session_destroy();
session_write_close(); session_write_close();
header("Location: /"); header("Location: /");
} }
} }
// Check session // Check session
function session_check() { function session_check() {
if (isset($_SESSION['mailcow_cc_api']) && $_SESSION['mailcow_cc_api'] === true) { if (isset($_SESSION['mailcow_cc_api']) && $_SESSION['mailcow_cc_api'] === true) {
return true; return true;
} }
if (!isset($_SESSION['SESS_REMOTE_UA']) || ($_SESSION['SESS_REMOTE_UA'] != $_SERVER['HTTP_USER_AGENT'])) { if (!isset($_SESSION['SESS_REMOTE_UA']) || ($_SESSION['SESS_REMOTE_UA'] != $_SERVER['HTTP_USER_AGENT'])) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'warning', 'type' => 'warning',
'msg' => 'session_ua' 'msg' => 'session_ua'
); );
return false; return false;
} }
if (!empty($_POST)) { if (!empty($_POST)) {
if ($_SESSION['CSRF']['TOKEN'] != $_POST['csrf_token']) { if ($_SESSION['CSRF']['TOKEN'] != $_POST['csrf_token']) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'warning', 'type' => 'warning',
'msg' => 'session_token' 'msg' => 'session_token'
); );
return false; return false;
} }
unset($_POST['csrf_token']); unset($_POST['csrf_token']);
$_SESSION['CSRF']['TOKEN'] = bin2hex(random_bytes(32)); $_SESSION['CSRF']['TOKEN'] = bin2hex(random_bytes(32));
$_SESSION['CSRF']['TIME'] = time(); $_SESSION['CSRF']['TIME'] = time();
} }
return true; return true;
} }
if (isset($_SESSION['mailcow_cc_role']) && session_check() === false) { if (isset($_SESSION['mailcow_cc_role']) && session_check() === false) {
$_POST = array(); $_POST = array();
$_FILES = array(); $_FILES = array();
} }

View File

@@ -1,15 +1,4 @@
<?php <?php
// SSO Domain Admin
if (!empty($_GET['sso_token'])) {
$username = domain_admin_sso('check', $_GET['sso_token']);
if ($username !== false) {
$_SESSION['mailcow_cc_username'] = $username;
$_SESSION['mailcow_cc_role'] = 'domainadmin';
header('Location: /mailbox');
}
}
if (isset($_POST["verify_tfa_login"])) { if (isset($_POST["verify_tfa_login"])) {
if (verify_tfa_login($_SESSION['pending_mailcow_cc_username'], $_POST)) { if (verify_tfa_login($_SESSION['pending_mailcow_cc_username'], $_POST)) {
$_SESSION['mailcow_cc_username'] = $_SESSION['pending_mailcow_cc_username']; $_SESSION['mailcow_cc_username'] = $_SESSION['pending_mailcow_cc_username'];
@@ -17,7 +6,7 @@ if (isset($_POST["verify_tfa_login"])) {
unset($_SESSION['pending_mailcow_cc_username']); unset($_SESSION['pending_mailcow_cc_username']);
unset($_SESSION['pending_mailcow_cc_role']); unset($_SESSION['pending_mailcow_cc_role']);
unset($_SESSION['pending_tfa_methods']); unset($_SESSION['pending_tfa_methods']);
header("Location: /user"); header("Location: /user");
} else { } else {
unset($_SESSION['pending_mailcow_cc_username']); unset($_SESSION['pending_mailcow_cc_username']);
@@ -45,7 +34,7 @@ if (isset($_POST["quick_delete"])) {
if (isset($_POST["login_user"]) && isset($_POST["pass_user"])) { if (isset($_POST["login_user"]) && isset($_POST["pass_user"])) {
$login_user = strtolower(trim($_POST["login_user"])); $login_user = strtolower(trim($_POST["login_user"]));
$as = check_login($login_user, $_POST["pass_user"]); $as = check_login($login_user, $_POST["pass_user"]);
if ($as == "admin") { if ($as == "admin") {
$_SESSION['mailcow_cc_username'] = $login_user; $_SESSION['mailcow_cc_username'] = $login_user;
$_SESSION['mailcow_cc_role'] = "admin"; $_SESSION['mailcow_cc_role'] = "admin";

View File

@@ -124,7 +124,7 @@ $MAILCOW_APPS = array(
); );
// Rows until pagination begins // Rows until pagination begins
$PAGINATION_SIZE = 25; $PAGINATION_SIZE = 20;
// Default number of rows/lines to display (log table) // Default number of rows/lines to display (log table)
$LOG_LINES = 1000; $LOG_LINES = 1000;

File diff suppressed because it is too large Load Diff

View File

@@ -34,7 +34,7 @@ $(document).ready(function() {
}); });
// set update loop container list // set update loop container list
containersToUpdate = {}; containersToUpdate = {}
// set default ChartJs Font Color // set default ChartJs Font Color
Chart.defaults.color = '#999'; Chart.defaults.color = '#999';
// create host cpu and mem charts // create host cpu and mem charts
@@ -44,13 +44,14 @@ $(document).ready(function() {
check_update(mailcow_info.version_tag, mailcow_info.project_url); check_update(mailcow_info.version_tag, mailcow_info.project_url);
} }
$("#maiclow_version").click(function(){ $("#maiclow_version").click(function(){
if (mailcow_cc_role !== "admin" && mailcow_cc_role !== "domainadmin" || mailcow_info.branch !== "master") if (mailcow_cc_role !== "admin" && mailcow_cc_role !== "domainadmin" ||
mailcow_info.branch !== "master")
return; return;
showVersionModal("Version " + mailcow_info.version_tag, mailcow_info.version_tag); showVersionModal("Version " + mailcow_info.version_tag, mailcow_info.version_tag);
}) })
// get public ips // get public ips
$("#host_show_ip").click(function(){ $("#host_show_ip").click(function(){
$("#host_show_ip").find(".text").addClass("d-none"); $("#host_show_ip").find(".text").addClass("d-none");
$("#host_show_ip").find(".spinner-border").removeClass("d-none"); $("#host_show_ip").find(".spinner-border").removeClass("d-none");
@@ -75,7 +76,7 @@ $(document).ready(function() {
$("#host_ipv6").addClass("d-block"); $("#host_ipv6").addClass("d-block");
}).catch(function(error){ }).catch(function(error){
console.log(error); console.log(error);
$("#host_ipv6").removeClass("d-none"); $("#host_ipv6").removeClass("d-none");
$("#host_ipv6").addClass("d-block"); $("#host_ipv6").addClass("d-block");
$("#host_ipv6").addClass("text-danger"); $("#host_ipv6").addClass("text-danger");
@@ -118,11 +119,10 @@ jQuery(function($){
} }
var table = $('#autodiscover_log').DataTable({ var table = $('#autodiscover_log').DataTable({
responsive: true, responsive: true,
processing: true, processing: true,
serverSide: false, serverSide: false,
stateSave: true, stateSave: true,
pageLength: log_pagination_size,
dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" + dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" +
"tr" + "tr" +
"<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>", "<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
@@ -188,11 +188,10 @@ jQuery(function($){
} }
var table = $('#postfix_log').DataTable({ var table = $('#postfix_log').DataTable({
responsive: true, responsive: true,
processing: true, processing: true,
serverSide: false, serverSide: false,
stateSave: true, stateSave: true,
pageLength: log_pagination_size,
dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" + dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" +
"tr" + "tr" +
"<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>", "<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
@@ -243,11 +242,10 @@ jQuery(function($){
} }
var table = $('#watchdog_log').DataTable({ var table = $('#watchdog_log').DataTable({
responsive: true, responsive: true,
processing: true, processing: true,
serverSide: false, serverSide: false,
stateSave: true, stateSave: true,
pageLength: log_pagination_size,
dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" + dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" +
"tr" + "tr" +
"<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>", "<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
@@ -302,11 +300,10 @@ jQuery(function($){
} }
var table = $('#api_log').DataTable({ var table = $('#api_log').DataTable({
responsive: true, responsive: true,
processing: true, processing: true,
serverSide: false, serverSide: false,
stateSave: true, stateSave: true,
pageLength: log_pagination_size,
dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" + dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" +
"tr" + "tr" +
"<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>", "<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
@@ -355,7 +352,7 @@ jQuery(function($){
} }
] ]
}); });
table.on('responsive-resize', function (e, datatable, columns){ table.on('responsive-resize', function (e, datatable, columns){
hideTableExpandCollapseBtn('#tab-api-logs', '#api_log'); hideTableExpandCollapseBtn('#tab-api-logs', '#api_log');
}); });
@@ -368,11 +365,10 @@ jQuery(function($){
} }
var table = $('#rl_log').DataTable({ var table = $('#rl_log').DataTable({
responsive: true, responsive: true,
processing: true, processing: true,
serverSide: false, serverSide: false,
stateSave: true, stateSave: true,
pageLength: log_pagination_size,
dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" + dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" +
"tr" + "tr" +
"<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>", "<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
@@ -459,7 +455,7 @@ jQuery(function($){
} }
] ]
}); });
table.on('responsive-resize', function (e, datatable, columns){ table.on('responsive-resize', function (e, datatable, columns){
hideTableExpandCollapseBtn('#tab-rl-logs', '#rl_log'); hideTableExpandCollapseBtn('#tab-rl-logs', '#rl_log');
}); });
@@ -472,11 +468,10 @@ jQuery(function($){
} }
var table = $('#ui_logs').DataTable({ var table = $('#ui_logs').DataTable({
responsive: true, responsive: true,
processing: true, processing: true,
serverSide: false, serverSide: false,
stateSave: true, stateSave: true,
pageLength: log_pagination_size,
dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" + dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" +
"tr" + "tr" +
"<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>", "<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
@@ -543,7 +538,7 @@ jQuery(function($){
} }
] ]
}); });
table.on('responsive-resize', function (e, datatable, columns){ table.on('responsive-resize', function (e, datatable, columns){
hideTableExpandCollapseBtn('#tab-ui-logs', '#ui_log'); hideTableExpandCollapseBtn('#tab-ui-logs', '#ui_log');
}); });
@@ -556,11 +551,10 @@ jQuery(function($){
} }
var table = $('#sasl_logs').DataTable({ var table = $('#sasl_logs').DataTable({
responsive: true, responsive: true,
processing: true, processing: true,
serverSide: false, serverSide: false,
stateSave: true, stateSave: true,
pageLength: log_pagination_size,
dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" + dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" +
"tr" + "tr" +
"<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>", "<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
@@ -604,7 +598,7 @@ jQuery(function($){
} }
] ]
}); });
table.on('responsive-resize', function (e, datatable, columns){ table.on('responsive-resize', function (e, datatable, columns){
hideTableExpandCollapseBtn('#tab-sasl-logs', '#sasl_logs'); hideTableExpandCollapseBtn('#tab-sasl-logs', '#sasl_logs');
}); });
@@ -617,11 +611,10 @@ jQuery(function($){
} }
var table = $('#acme_log').DataTable({ var table = $('#acme_log').DataTable({
responsive: true, responsive: true,
processing: true, processing: true,
serverSide: false, serverSide: false,
stateSave: true, stateSave: true,
pageLength: log_pagination_size,
dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" + dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" +
"tr" + "tr" +
"<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>", "<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
@@ -654,7 +647,7 @@ jQuery(function($){
} }
] ]
}); });
table.on('responsive-resize', function (e, datatable, columns){ table.on('responsive-resize', function (e, datatable, columns){
hideTableExpandCollapseBtn('#tab-acme-logs', '#acme_log'); hideTableExpandCollapseBtn('#tab-acme-logs', '#acme_log');
}); });
@@ -667,11 +660,10 @@ jQuery(function($){
} }
var table = $('#netfilter_log').DataTable({ var table = $('#netfilter_log').DataTable({
responsive: true, responsive: true,
processing: true, processing: true,
serverSide: false, serverSide: false,
stateSave: true, stateSave: true,
pageLength: log_pagination_size,
dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" + dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" +
"tr" + "tr" +
"<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>", "<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
@@ -709,7 +701,7 @@ jQuery(function($){
} }
] ]
}); });
table.on('responsive-resize', function (e, datatable, columns){ table.on('responsive-resize', function (e, datatable, columns){
hideTableExpandCollapseBtn('#tab-netfilter-logs', '#netfilter_log'); hideTableExpandCollapseBtn('#tab-netfilter-logs', '#netfilter_log');
}); });
@@ -722,11 +714,10 @@ jQuery(function($){
} }
var table = $('#sogo_log').DataTable({ var table = $('#sogo_log').DataTable({
responsive: true, responsive: true,
processing: true, processing: true,
serverSide: false, serverSide: false,
stateSave: true, stateSave: true,
pageLength: log_pagination_size,
dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" + dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" +
"tr" + "tr" +
"<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>", "<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
@@ -764,7 +755,7 @@ jQuery(function($){
} }
] ]
}); });
table.on('responsive-resize', function (e, datatable, columns){ table.on('responsive-resize', function (e, datatable, columns){
hideTableExpandCollapseBtn('#tab-sogo-logs', '#sogo_log'); hideTableExpandCollapseBtn('#tab-sogo-logs', '#sogo_log');
}); });
@@ -777,11 +768,10 @@ jQuery(function($){
} }
var table = $('#dovecot_log').DataTable({ var table = $('#dovecot_log').DataTable({
responsive: true, responsive: true,
processing: true, processing: true,
serverSide: false, serverSide: false,
stateSave: true, stateSave: true,
pageLength: log_pagination_size,
dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" + dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" +
"tr" + "tr" +
"<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>", "<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
@@ -893,11 +883,10 @@ jQuery(function($){
} }
var table = $('#rspamd_history').DataTable({ var table = $('#rspamd_history').DataTable({
responsive: true, responsive: true,
processing: true, processing: true,
serverSide: false, serverSide: false,
stateSave: true, stateSave: true,
pageLength: log_pagination_size,
dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" + dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" +
"tr" + "tr" +
"<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>", "<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
@@ -994,7 +983,7 @@ jQuery(function($){
} }
] ]
}); });
table.on('responsive-resize', function (e, datatable, columns){ table.on('responsive-resize', function (e, datatable, columns){
hideTableExpandCollapseBtn('#tab-rspamd-history', '#rspamd_history'); hideTableExpandCollapseBtn('#tab-rspamd-history', '#rspamd_history');
}); });
@@ -1009,31 +998,31 @@ jQuery(function($){
item.rcpt = escapeHtml(item.rcpt_smtp.join(", ")); item.rcpt = escapeHtml(item.rcpt_smtp.join(", "));
} }
item.symbols = Object.keys(item.symbols).sort(function (a, b) { item.symbols = Object.keys(item.symbols).sort(function (a, b) {
if (item.symbols[a].score === 0) return 1; if (item.symbols[a].score === 0) return 1
if (item.symbols[b].score === 0) return -1; if (item.symbols[b].score === 0) return -1
if (item.symbols[b].score < 0 && item.symbols[a].score < 0) { if (item.symbols[b].score < 0 && item.symbols[a].score < 0) {
return item.symbols[a].score - item.symbols[b].score; return item.symbols[a].score - item.symbols[b].score
} }
if (item.symbols[b].score > 0 && item.symbols[a].score > 0) { if (item.symbols[b].score > 0 && item.symbols[a].score > 0) {
return item.symbols[b].score - item.symbols[a].score; return item.symbols[b].score - item.symbols[a].score
} }
return item.symbols[b].score - item.symbols[a].score; return item.symbols[b].score - item.symbols[a].score
}).map(function(key) { }).map(function(key) {
var sym = item.symbols[key]; var sym = item.symbols[key];
if (sym.score < 0) { if (sym.score < 0) {
sym.score_formatted = '(<span class="text-success"><b>' + sym.score + '</b></span>)'; sym.score_formatted = '(<span class="text-success"><b>' + sym.score + '</b></span>)'
} }
else if (sym.score === 0) { else if (sym.score === 0) {
sym.score_formatted = '(<span><b>' + sym.score + '</b></span>)'; sym.score_formatted = '(<span><b>' + sym.score + '</b></span>)'
} }
else { else {
sym.score_formatted = '(<span class="text-danger"><b>' + sym.score + '</b></span>)'; sym.score_formatted = '(<span class="text-danger"><b>' + sym.score + '</b></span>)'
} }
var str = '<strong>' + key + '</strong> ' + sym.score_formatted; var str = '<strong>' + key + '</strong> ' + sym.score_formatted;
if (sym.options) { if (sym.options) {
str += ' [' + escapeHtml(sym.options.join(", ")) + "]"; str += ' [' + escapeHtml(sym.options.join(", ")) + "]";
} }
return str; return str
}).join('<br>\n'); }).join('<br>\n');
item.subject = escapeHtml(item.subject); item.subject = escapeHtml(item.subject);
var scan_time = item.time_real.toFixed(3); var scan_time = item.time_real.toFixed(3);
@@ -1166,14 +1155,14 @@ jQuery(function($){
} }
}); });
} }
return data; return data
}; };
$('.add_log_lines').on('click', function (e) { $('.add_log_lines').on('click', function (e) {
e.preventDefault(); e.preventDefault();
var log_table= $(this).data("table"); var log_table= $(this).data("table")
var new_nrows = $(this).data("nrows"); var new_nrows = $(this).data("nrows")
var post_process = $(this).data("post-process"); var post_process = $(this).data("post-process")
var log_url = $(this).data("log-url"); var log_url = $(this).data("log-url")
if (log_table === undefined || new_nrows === undefined || post_process === undefined || log_url === undefined) { if (log_table === undefined || new_nrows === undefined || post_process === undefined || log_url === undefined) {
console.log("no data-table or data-nrows or log_url or data-post-process attr found"); console.log("no data-table or data-nrows or log_url or data-post-process attr found");
return; return;
@@ -1181,7 +1170,7 @@ jQuery(function($){
if (table = $('#' + log_table).DataTable()) { if (table = $('#' + log_table).DataTable()) {
var heading = $('#' + log_table).closest('.card').find('.card-header'); var heading = $('#' + log_table).closest('.card').find('.card-header');
var load_rows = (table.data().length + 1) + '-' + (table.data().length + new_nrows) var load_rows = (table.page.len() + 1) + '-' + (table.page.len() + new_nrows)
$.get('/api/v1/get/logs/' + log_url + '/' + load_rows).then(function(data){ $.get('/api/v1/get/logs/' + log_url + '/' + load_rows).then(function(data){
if (data.length === undefined) { mailcow_alert_box(lang.no_new_rows, "info"); return; } if (data.length === undefined) { mailcow_alert_box(lang.no_new_rows, "info"); return; }
@@ -1195,9 +1184,9 @@ jQuery(function($){
}) })
function hideTableExpandCollapseBtn(tab, table){ function hideTableExpandCollapseBtn(tab, table){
if ($(table).hasClass('collapsed')) if ($(table).hasClass('collapsed'))
$(tab).find(".table_collapse_option").show(); $(tab).find(".table_collapse_option").show();
else else
$(tab).find(".table_collapse_option").hide(); $(tab).find(".table_collapse_option").hide();
} }
// detect element visibility changes // detect element visibility changes
@@ -1231,6 +1220,7 @@ jQuery(function($){
onVisible("[id^=rspamd_donut]", () => rspamd_pie_graph()); onVisible("[id^=rspamd_donut]", () => rspamd_pie_graph());
// start polling host stats if tab is active // start polling host stats if tab is active
onVisible("[id^=tab-containers]", () => update_stats()); onVisible("[id^=tab-containers]", () => update_stats());
// start polling container stats if collapse is active // start polling container stats if collapse is active
@@ -1313,9 +1303,9 @@ function update_stats(timeout=5){
if (mem_chart.data.labels.length > 30) mem_chart.data.labels.shift(); if (mem_chart.data.labels.length > 30) mem_chart.data.labels.shift();
cpu_chart.data.datasets[0].data.push(data.cpu.usage); cpu_chart.data.datasets[0].data.push(data.cpu.usage);
if (cpu_chart.data.datasets[0].data.length > 30) cpu_chart.data.datasets[0].data.shift(); if (cpu_chart.data.datasets[0].data.length > 30) cpu_chart.data.datasets[0].data.shift();
mem_chart.data.datasets[0].data.push(data.memory.usage); mem_chart.data.datasets[0].data.push(data.memory.usage);
if (mem_chart.data.datasets[0].data.length > 30) mem_chart.data.datasets[0].data.shift(); if (mem_chart.data.datasets[0].data.length > 30) mem_chart.data.datasets[0].data.shift();
cpu_chart.update(); cpu_chart.update();
mem_chart.update(); mem_chart.update();
@@ -1474,23 +1464,23 @@ function createReadWriteChart(chart_id, read_lable, write_lable){
}; };
var optionsNet = { var optionsNet = {
interaction: { interaction: {
mode: 'index' mode: 'index'
}, },
scales: { scales: {
yAxis: { yAxis: {
min: 0, min: 0,
grid: { grid: {
display: false display: false
}, },
ticks: { ticks: {
callback: function(i, index, ticks) { callback: function(i, index, ticks) {
return formatBytes(i); return formatBytes(i);
} }
} }
}, },
xAxis: { xAxis: {
grid: { grid: {
display: false display: false
} }
} }
} }
@@ -1538,13 +1528,13 @@ function createHostCpuAndMemChart(){
}; };
var optionsCpu = { var optionsCpu = {
interaction: { interaction: {
mode: 'index' mode: 'index'
}, },
scales: { scales: {
yAxis: { yAxis: {
min: 0, min: 0,
grid: { grid: {
display: false display: false
}, },
ticks: { ticks: {
callback: function(i, index, ticks) { callback: function(i, index, ticks) {
@@ -1554,7 +1544,7 @@ function createHostCpuAndMemChart(){
}, },
xAxis: { xAxis: {
grid: { grid: {
display: false display: false
} }
} }
} }
@@ -1576,13 +1566,13 @@ function createHostCpuAndMemChart(){
}; };
var optionsMem = { var optionsMem = {
interaction: { interaction: {
mode: 'index' mode: 'index'
}, },
scales: { scales: {
yAxis: { yAxis: {
min: 0, min: 0,
grid: { grid: {
display: false display: false
}, },
ticks: { ticks: {
callback: function(i, index, ticks) { callback: function(i, index, ticks) {
@@ -1592,7 +1582,7 @@ function createHostCpuAndMemChart(){
}, },
xAxis: { xAxis: {
grid: { grid: {
display: false display: false
} }
} }
} }
@@ -1688,22 +1678,22 @@ function parseGithubMarkdownLinks(inputText) {
replacePattern1 = /(\b(https?):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/gim; replacePattern1 = /(\b(https?):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/gim;
replacedText = inputText.replace(replacePattern1, (matched, index, original, input_string) => { replacedText = inputText.replace(replacePattern1, (matched, index, original, input_string) => {
if (matched.includes('github.com')){ if (matched.includes('github.com')){
// return short link if it's github link // return short link if it's github link
last_uri_path = matched.split('/'); last_uri_path = matched.split('/');
last_uri_path = last_uri_path[last_uri_path.length - 1]; last_uri_path = last_uri_path[last_uri_path.length - 1];
// adjust Full Changelog link to match last git version and new git version, if link is a compare link // adjust Full Changelog link to match last git version and new git version, if link is a compare link
if (matched.includes('/compare/') && mailcow_info.last_version_tag !== ''){ if (matched.includes('/compare/') && mailcow_info.last_version_tag !== ''){
matched = matched.replace(last_uri_path, mailcow_info.last_version_tag + '...' + mailcow_info.version_tag); matched = matched.replace(last_uri_path, mailcow_info.last_version_tag + '...' + mailcow_info.version_tag);
last_uri_path = mailcow_info.last_version_tag + '...' + mailcow_info.version_tag; last_uri_path = mailcow_info.last_version_tag + '...' + mailcow_info.version_tag;
} }
return '<a href="' + matched + '" target="_blank">' + last_uri_path + '</a><br>'; return '<a href="' + matched + '" target="_blank">' + last_uri_path + '</a><br>';
}; };
// if it's not a github link, return complete link // if it's not a github link, return complete link
return '<a href="' + matched + '" target="_blank">' + matched + '</a>'; return '<a href="' + matched + '" target="_blank">' + matched + '</a>';
}); });
return replacedText; return replacedText;

View File

@@ -1,222 +1,220 @@
$(document).ready(function() { $(document).ready(function() {
$(".arrow-toggle").on('click', function(e) { e.preventDefault(); $(this).find('.arrow').toggleClass("animation"); }); $(".arrow-toggle").on('click', function(e) { e.preventDefault(); $(this).find('.arrow').toggleClass("animation"); });
$("#pushover_delete").click(function() { return confirm(lang.delete_ays); }); $("#pushover_delete").click(function() { return confirm(lang.delete_ays); });
$(".goto_checkbox").click(function( event ) { $(".goto_checkbox").click(function( event ) {
$("form[data-id='editalias'] .goto_checkbox").not(this).prop('checked', false); $("form[data-id='editalias'] .goto_checkbox").not(this).prop('checked', false);
if ($("form[data-id='editalias'] .goto_checkbox:checked").length > 0) { if ($("form[data-id='editalias'] .goto_checkbox:checked").length > 0) {
$('#textarea_alias_goto').prop('disabled', true); $('#textarea_alias_goto').prop('disabled', true);
} }
else { else {
$("#textarea_alias_goto").removeAttr('disabled'); $("#textarea_alias_goto").removeAttr('disabled');
} }
}); });
$("#disable_sender_check").click(function( event ) { $("#disable_sender_check").click(function( event ) {
if ($("form[data-id='editmailbox'] #disable_sender_check:checked").length > 0) { if ($("form[data-id='editmailbox'] #disable_sender_check:checked").length > 0) {
$('#editSelectSenderACL').prop('disabled', true); $('#editSelectSenderACL').prop('disabled', true);
$('#editSelectSenderACL').selectpicker('refresh'); $('#editSelectSenderACL').selectpicker('refresh');
} }
else { else {
$('#editSelectSenderACL').prop('disabled', false); $('#editSelectSenderACL').prop('disabled', false);
$('#editSelectSenderACL').selectpicker('refresh'); $('#editSelectSenderACL').selectpicker('refresh');
} }
}); });
if ($("form[data-id='editalias'] .goto_checkbox:checked").length > 0) { if ($("form[data-id='editalias'] .goto_checkbox:checked").length > 0) {
$('#textarea_alias_goto').prop('disabled', true); $('#textarea_alias_goto').prop('disabled', true);
} }
$("#mailbox-password-warning-close").click(function( event ) { $("#mailbox-password-warning-close").click(function( event ) {
$('#mailbox-passwd-hidden-info').addClass('hidden'); $('#mailbox-passwd-hidden-info').addClass('hidden');
$('#mailbox-passwd-form-groups').removeClass('hidden'); $('#mailbox-passwd-form-groups').removeClass('hidden');
}); });
// Sender ACL // Sender ACL
if ($("#editSelectSenderACL option[value='\*']:selected").length > 0){ if ($("#editSelectSenderACL option[value='\*']:selected").length > 0){
$("#sender_acl_disabled").show(); $("#sender_acl_disabled").show();
} }
$('#editSelectSenderACL').change(function() { $('#editSelectSenderACL').change(function() {
if ($("#editSelectSenderACL option[value='\*']:selected").length > 0){ if ($("#editSelectSenderACL option[value='\*']:selected").length > 0){
$("#sender_acl_disabled").show(); $("#sender_acl_disabled").show();
} }
else { else {
$("#sender_acl_disabled").hide(); $("#sender_acl_disabled").hide();
} }
}); });
// Resources // Resources
if ($("#editSelectMultipleBookings").val() == "custom") { if ($("#editSelectMultipleBookings").val() == "custom") {
$("#multiple_bookings_custom_div").show(); $("#multiple_bookings_custom_div").show();
$('input[name=multiple_bookings]').val($("#multiple_bookings_custom").val()); $('input[name=multiple_bookings]').val($("#multiple_bookings_custom").val());
} }
$("#editSelectMultipleBookings").change(function() { $("#editSelectMultipleBookings").change(function() {
$('input[name=multiple_bookings]').val($("#editSelectMultipleBookings").val()); $('input[name=multiple_bookings]').val($("#editSelectMultipleBookings").val());
if ($('input[name=multiple_bookings]').val() == "custom") { if ($('input[name=multiple_bookings]').val() == "custom") {
$("#multiple_bookings_custom_div").show(); $("#multiple_bookings_custom_div").show();
} }
else { else {
$("#multiple_bookings_custom_div").hide(); $("#multiple_bookings_custom_div").hide();
} }
}); });
$("#multiple_bookings_custom").bind("change keypress keyup blur", function() { $("#multiple_bookings_custom").bind("change keypress keyup blur", function() {
$('input[name=multiple_bookings]').val($("#multiple_bookings_custom").val()); $('input[name=multiple_bookings]').val($("#multiple_bookings_custom").val());
}); });
// load tags // load tags
if ($('#tags').length){ if ($('#tags').length){
var tagsEl = $('#tags').parent().find('.tag-values')[0]; var tagsEl = $('#tags').parent().find('.tag-values')[0];
console.log($(tagsEl).val()) console.log($(tagsEl).val())
var tags = JSON.parse($(tagsEl).val()); var tags = JSON.parse($(tagsEl).val());
$(tagsEl).val(""); $(tagsEl).val("");
for (var i = 0; i < tags.length; i++) for (var i = 0; i < tags.length; i++)
addTag($('#tags'), tags[i]); addTag($('#tags'), tags[i]);
} }
}); });
jQuery(function($){ jQuery(function($){
// http://stackoverflow.com/questions/46155/validate-email-address-in-javascript // http://stackoverflow.com/questions/46155/validate-email-address-in-javascript
function validateEmail(email) { 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,}))$/; 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); return re.test(email);
} }
function draw_wl_policy_domain_table() { function draw_wl_policy_domain_table() {
$('#wl_policy_domain_table').DataTable({ $('#wl_policy_domain_table').DataTable({
responsive: true, responsive: true,
processing: true, processing: true,
serverSide: false, serverSide: false,
stateSave: true, stateSave: true,
pageLength: pagination_size, dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" +
dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" + "tr" +
"tr" + "<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
"<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>", language: lang_datatables,
language: lang_datatables, ajax: {
ajax: { type: "GET",
type: "GET", url: '/api/v1/get/policy_wl_domain/' + table_for_domain,
url: '/api/v1/get/policy_wl_domain/' + table_for_domain, dataSrc: function(data){
dataSrc: function(data){ $.each(data, function (i, item) {
$.each(data, function (i, item) { if (!validateEmail(item.object)) {
if (!validateEmail(item.object)) { item.chkbox = '<input type="checkbox" data-id="policy_wl_domain" name="multi_select" value="' + item.prefid + '" />';
item.chkbox = '<input type="checkbox" data-id="policy_wl_domain" name="multi_select" value="' + item.prefid + '" />'; }
} else {
else { item.chkbox = '<input type="checkbox" disabled title="' + lang_user.spamfilter_table_domain_policy + '" />';
item.chkbox = '<input type="checkbox" disabled title="' + lang_user.spamfilter_table_domain_policy + '" />'; }
} });
});
return data;
return data; }
} },
}, columns: [
columns: [ {
{ // placeholder, so checkbox will not block child row toggle
// placeholder, so checkbox will not block child row toggle title: '',
title: '', data: null,
data: null, searchable: false,
searchable: false, orderable: false,
orderable: false, defaultContent: ''
defaultContent: '' },
}, {
{ title: '',
title: '', data: 'chkbox',
data: 'chkbox', searchable: false,
searchable: false, orderable: false,
orderable: false, defaultContent: ''
defaultContent: '' },
}, {
{ title: 'ID',
title: 'ID', data: 'prefid',
data: 'prefid', defaultContent: ''
defaultContent: '' },
}, {
{ title: lang_user.spamfilter_table_rule,
title: lang_user.spamfilter_table_rule, data: 'value',
data: 'value', defaultContent: ''
defaultContent: '' },
}, {
{ title: 'Scope',
title: 'Scope', data: 'object',
data: 'object', defaultContent: ''
defaultContent: '' }
} ]
] });
}); }
} function draw_bl_policy_domain_table() {
function draw_bl_policy_domain_table() { $('#bl_policy_domain_table').DataTable({
$('#bl_policy_domain_table').DataTable({ responsive: true,
responsive: true, processing: true,
processing: true, serverSide: false,
serverSide: false, stateSave: true,
stateSave: true, dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" +
pageLength: pagination_size, "tr" +
dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" + "<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
"tr" + language: lang_datatables,
"<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>", ajax: {
language: lang_datatables, type: "GET",
ajax: { url: '/api/v1/get/policy_bl_domain/' + table_for_domain,
type: "GET", dataSrc: function(data){
url: '/api/v1/get/policy_bl_domain/' + table_for_domain, $.each(data, function (i, item) {
dataSrc: function(data){ if (!validateEmail(item.object)) {
$.each(data, function (i, item) { item.chkbox = '<input type="checkbox" data-id="policy_bl_domain" name="multi_select" value="' + item.prefid + '" />';
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 + '" />';
else { }
item.chkbox = '<input type="checkbox" disabled tooltip="' + lang_user.spamfilter_table_domain_policy + '" />'; });
}
}); return data;
}
return data; },
} columns: [
}, {
columns: [ // placeholder, so checkbox will not block child row toggle
{ title: '',
// placeholder, so checkbox will not block child row toggle data: null,
title: '', searchable: false,
data: null, orderable: false,
searchable: false, defaultContent: ''
orderable: false, },
defaultContent: '' {
}, title: '',
{ data: 'chkbox',
title: '', searchable: false,
data: 'chkbox', orderable: false,
searchable: false, defaultContent: ''
orderable: false, },
defaultContent: '' {
}, title: 'ID',
{ data: 'prefid',
title: 'ID', defaultContent: ''
data: 'prefid', },
defaultContent: '' {
}, title: lang_user.spamfilter_table_rule,
{ data: 'value',
title: lang_user.spamfilter_table_rule, defaultContent: ''
data: 'value', },
defaultContent: '' {
}, title: 'Scope',
{ data: 'object',
title: 'Scope', defaultContent: ''
data: 'object', }
defaultContent: '' ]
} });
] }
});
}
// detect element visibility changes
function onVisible(element, callback) {
// detect element visibility changes $(document).ready(function() {
function onVisible(element, callback) { element_object = document.querySelector(element);
$(document).ready(function() { if (element_object === null) return;
element_object = document.querySelector(element);
if (element_object === null) return; new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
new IntersectionObserver((entries, observer) => { if(entry.intersectionRatio > 0) {
entries.forEach(entry => { callback(element_object);
if(entry.intersectionRatio > 0) { observer.disconnect();
callback(element_object); }
observer.disconnect(); });
} }).observe(element_object);
}); });
}).observe(element_object); }
}); // Draw Table if tab is active
} onVisible("[id^=wl_policy_domain_table]", () => draw_wl_policy_domain_table());
// Draw Table if tab is active onVisible("[id^=bl_policy_domain_table]", () => draw_bl_policy_domain_table());
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

View File

@@ -1,71 +1,71 @@
jQuery(function($){ jQuery(function($){
var qitem = $('legend').data('hash'); var qitem = $('legend').data('hash');
var qError = $("#qid_error"); var qError = $("#qid_error");
$.ajax({ $.ajax({
url: '/inc/ajax/qitem_details.php', url: '/inc/ajax/qitem_details.php',
data: { hash: qitem }, data: { hash: qitem },
dataType: 'json', dataType: 'json',
success: function(data){ success: function(data){
$('[data-id="qitems_single"]').each(function(index) { $('[data-id="qitems_single"]').each(function(index) {
$(this).attr("data-item", qitem); $(this).attr("data-item", qitem);
}); });
$('#qid_detail_subj').text(data.subject); $('#qid_detail_subj').text(data.subject);
$('#qid_detail_hfrom').text(data.header_from); $('#qid_detail_hfrom').text(data.header_from);
$('#qid_detail_efrom').text(data.env_from); $('#qid_detail_efrom').text(data.env_from);
$('#qid_detail_score').html(''); $('#qid_detail_score').html('');
$('#qid_detail_symbols').html(''); $('#qid_detail_symbols').html('');
$('#qid_detail_recipients').html(''); $('#qid_detail_recipients').html('');
$('#qid_detail_fuzzy').html(''); $('#qid_detail_fuzzy').html('');
if (typeof data.fuzzy_hashes === 'object' && data.fuzzy_hashes !== null && data.fuzzy_hashes.length !== 0) { if (typeof data.fuzzy_hashes === 'object' && data.fuzzy_hashes !== null && data.fuzzy_hashes.length !== 0) {
$.each(data.fuzzy_hashes, function (index, value) { $.each(data.fuzzy_hashes, function (index, value) {
$('#qid_detail_fuzzy').append('<p style="font-family:monospace">' + value + '</p>'); $('#qid_detail_fuzzy').append('<p style="font-family:monospace">' + value + '</p>');
}); });
} else { } else {
$('#qid_detail_fuzzy').append('-'); $('#qid_detail_fuzzy').append('-');
} }
if (typeof data.symbols !== 'undefined') { if (typeof data.symbols !== 'undefined') {
data.symbols.sort(function (a, b) { data.symbols.sort(function (a, b) {
if (a.score === 0) return 1; if (a.score === 0) return 1
if (b.score === 0) return -1; if (b.score === 0) return -1
if (b.score < 0 && a.score < 0) { if (b.score < 0 && a.score < 0) {
return a.score - b.score; return a.score - b.score
} }
if (b.score > 0 && a.score > 0) { if (b.score > 0 && a.score > 0) {
return b.score - a.score; return b.score - a.score
} }
return b.score - a.score; return b.score - a.score
}) })
$.each(data.symbols, function (index, value) { $.each(data.symbols, function (index, value) {
var highlightClass = ''; var highlightClass = ''
if (value.score > 0) highlightClass = 'negative'; if (value.score > 0) highlightClass = 'negative'
else if (value.score < 0) highlightClass = 'positive'; else if (value.score < 0) highlightClass = 'positive'
else highlightClass = 'neutral'; 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>'); $('#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(); $('[data-bs-toggle="tooltip"]').tooltip()
} }
if (typeof data.score !== 'undefined' && typeof data.action !== 'undefined') { if (typeof data.score !== 'undefined' && typeof data.action !== 'undefined') {
if (data.action === "add header") { 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>'); $('#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") { } 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>'); $('#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") { } 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>'); $('#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') { if (typeof data.recipients !== 'undefined') {
$.each(data.recipients, function(index, value) { $.each(data.recipients, function(index, value) {
var elem = $('<span class="mail-address-item"></span>'); var elem = $('<span class="mail-address-item"></span>');
elem.text(value.address + ' (' + value.type.toUpperCase() + ')'); elem.text(value.address + ' (' + value.type.toUpperCase() + ')');
$('#qid_detail_recipients').append(elem); $('#qid_detail_recipients').append(elem);
}); });
} }
}, },
error: function(data){ error: function(data){
if (typeof data.error !== 'undefined') { if (typeof data.error !== 'undefined') {
qError.text("Error loading quarantine item"); qError.text("Error loading quarantine item");
qError.show(); qError.show();
} }
} }
}); });
}); });

View File

@@ -1,297 +1,286 @@
// Base64 functions // 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}}; 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($){ jQuery(function($){
acl_data = JSON.parse(acl); acl_data = JSON.parse(acl);
// http://stackoverflow.com/questions/24816/escaping-html-strings-with-jquery // http://stackoverflow.com/questions/24816/escaping-html-strings-with-jquery
var entityMap={"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;","/":"&#x2F;","`":"&#x60;","=":"&#x3D;"}; var entityMap={"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;","/":"&#x2F;","`":"&#x60;","=":"&#x3D;"};
function escapeHtml(n){return String(n).replace(/[&<>"'`=\/]/g,function(n){return entityMap[n]})} 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]} 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) { $(".refresh_table").on('click', function(e) {
e.preventDefault(); e.preventDefault();
var table_name = $(this).data('table'); var table_name = $(this).data('table');
$('#' + table_name).DataTable().ajax.reload(); $('#' + table_name).DataTable().ajax.reload();
}); });
function draw_quarantine_table() { function draw_quarantine_table() {
var table = $('#quarantinetable').DataTable({ var table = $('#quarantinetable').DataTable({
responsive: true, responsive: true,
processing: true, processing: true,
serverSide: false, serverSide: false,
stateSave: true, stateSave: true,
pageLength: pagination_size, dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" +
order: [[2, 'desc']], "tr" +
lengthMenu: [ "<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
[10, 25, 50, 100, -1], language: lang_datatables,
[10, 25, 50, 100, 'all'] initComplete: function(){
], hideTableExpandCollapseBtn('#quarantinetable');
pagingType: 'first_last_numbers', },
aColumns: [ ajax: {
{ sWidth: '8.25%' }, type: "GET",
{ sClass: 'classDataTable' } url: "/api/v1/get/quarantine/all",
], dataSrc: function(data){
dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" + $.each(data, function (i, item) {
"tr" + if (item.subject === null) {
"<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>", item.subject = '';
language: lang_datatables, } else {
initComplete: function(){ item.subject = escapeHtml(item.subject);
hideTableExpandCollapseBtn('#quarantinetable'); }
}, if (item.score === null) {
ajax: { item.score = '-';
type: "GET", }
url: "/api/v1/get/quarantine/all", if (item.virus_flag > 0) {
dataSrc: function(data){ item.virus = '<span class="badge fs-6 bg-danger">' + lang.high_danger + '</span>';
$.each(data, function (i, item) { } else {
if (item.subject === null) { item.virus = '<span class="badge fs-6 bg-secondary">' + lang.neutral_danger + '</span>';
item.subject = ''; }
} else { if (item.action === "reject") {
item.subject = escapeHtml(item.subject); item.rspamdaction = '<span class="badge fs-6 bg-danger">' + lang.rejected + '</span>';
} } else if (item.action === "add header") {
if (item.score === null) { item.rspamdaction = '<span class="badge fs-6 bg-warning">' + lang.junk_folder + '</span>';
item.score = '-'; } else if (item.action === "rewrite subject") {
} item.rspamdaction = '<span class="badge fs-6 bg-warning">' + lang.rewrite_subject + '</span>';
if (item.virus_flag > 0) { }
item.virus = '<span class="badge fs-6 bg-danger">' + lang.high_danger + '</span>'; if(item.notified > 0) {
} else { item.notified = '&#10004;';
item.virus = '<span class="badge fs-6 bg-secondary">' + lang.neutral_danger + '</span>'; } else {
} item.notified = '&#10006;';
if (item.action === "reject") { }
item.rspamdaction = '<span class="badge fs-6 bg-danger">' + lang.rejected + '</span>'; if (acl_data.login_as === 1) {
} else if (item.action === "add header") { item.action = '<div class="btn-group">' +
item.rspamdaction = '<span class="badge fs-6 bg-warning">' + lang.junk_folder + '</span>'; '<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>' +
} else if (item.action === "rewrite subject") { '<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>' +
item.rspamdaction = '<span class="badge fs-6 bg-warning">' + lang.rewrite_subject + '</span>'; '</div>';
} }
if(item.notified > 0) { else {
item.notified = '&#10004;'; item.action = '<div class="btn-group">' +
} else { '<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>' +
item.notified = '&#10006;'; '</div>';
} }
if (acl_data.login_as === 1) { item.chkbox = '<input type="checkbox" data-id="qitems" name="multi_select" value="' + item.id + '" />';
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>' + return data;
'</div>'; }
} },
else { columns: [
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>' + // placeholder, so checkbox will not block child row toggle
'</div>'; title: '',
} data: null,
item.chkbox = '<input type="checkbox" data-id="qitems" name="multi_select" value="' + item.id + '" />'; searchable: false,
}); orderable: false,
defaultContent: ''
return data; },
} {
}, title: '',
columns: [ data: 'chkbox',
{ searchable: false,
// placeholder, so checkbox will not block child row toggle orderable: false,
title: '', defaultContent: ''
data: null, },
searchable: false, {
orderable: false, title: 'ID',
defaultContent: '' data: 'id',
}, defaultContent: ''
{ },
title: '', {
data: 'chkbox', title: lang.qid,
searchable: false, data: 'qid',
orderable: false, defaultContent: ''
defaultContent: '' },
}, {
{ title: lang.sender,
title: 'ID', data: 'sender',
data: 'id', defaultContent: ''
defaultContent: '' },
}, {
{ title: lang.subj,
title: lang.qid, data: 'subject',
data: 'qid', defaultContent: ''
defaultContent: '' },
}, {
{ title: lang.rspamd_result,
title: lang.sender, data: 'rspamdaction',
data: 'sender', defaultContent: ''
className: 'senders-mw220', },
defaultContent: '' {
}, title: lang.rcpt,
{ data: 'rcpt',
title: lang.subj, defaultContent: ''
data: 'subject', },
defaultContent: '' {
}, title: lang.danger,
{ data: 'virus',
title: lang.rspamd_result, defaultContent: ''
data: 'rspamdaction', },
defaultContent: '' {
}, title: lang.spam_score,
{ data: 'score',
title: lang.rcpt, defaultContent: ''
data: 'rcpt', },
defaultContent: '' {
}, title: lang.notified,
{ data: 'notified',
title: lang.danger, defaultContent: ''
data: 'virus', },
defaultContent: '' {
}, title: lang.received,
{ data: 'created',
title: lang.spam_score, defaultContent: '',
data: 'score', createdCell: function(td, cellData) {
defaultContent: '' $(td).attr({
}, "data-order": cellData,
{ "data-sort": cellData
title: lang.notified, });
data: 'notified',
defaultContent: '' 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.received, }
data: 'created', },
defaultContent: '', {
createdCell: function(td, cellData) { title: lang.action,
$(td).attr({ data: 'action',
"data-order": cellData, className: 'text-md-end dt-sm-head-hidden dt-body-right',
"data-sort": cellData defaultContent: ''
}); },
]
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); table.on('responsive-resize', function (e, datatable, columns){
} hideTableExpandCollapseBtn('#quarantinetable');
}, });
{ }
title: lang.action,
data: 'action', $('body').on('click', '.show_qid_info', function (e) {
className: 'dt-text-right dt-sm-head-hidden', e.preventDefault();
defaultContent: '' var qitem = $(this).attr('data-item');
}, var qError = $("#qid_error");
]
}); $('#qidDetailModal').modal('show');
qError.hide();
table.on('responsive-resize', function (e, datatable, columns){
hideTableExpandCollapseBtn('#quarantinetable'); $.ajax({
}); url: '/inc/ajax/qitem_details.php',
} data: { id: qitem },
dataType: 'json',
$('body').on('click', '.show_qid_info', function (e) { success: function(data){
e.preventDefault();
var qitem = $(this).attr('data-item'); $('[data-id="qitems_single"]').each(function(index) {
var qError = $("#qid_error"); $(this).attr("data-item", qitem);
});
$('#qidDetailModal').modal('show');
qError.hide(); $("#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')");
$.ajax({ $("#quick_delete_link").attr("onclick", "window.open('/inc/ajax/qitem_details.php?id=" + qitem + "&quick_delete', '_blank')");
url: '/inc/ajax/qitem_details.php',
data: { id: qitem }, $('#qid_detail_subj').text(data.subject);
dataType: 'json', $('#qid_detail_hfrom').text(data.header_from);
success: function(data){ $('#qid_detail_efrom').text(data.env_from);
$('#qid_detail_score').html('');
$('[data-id="qitems_single"]').each(function(index) { $('#qid_detail_recipients').html('');
$(this).attr("data-item", qitem); $('#qid_detail_symbols').html('');
}); $('#qid_detail_fuzzy').html('');
if (typeof data.symbols !== 'undefined') {
$("#quick_download_link").attr("onclick", "window.open('/inc/ajax/qitem_details.php?id=" + qitem + "&eml', '_blank')"); data.symbols.sort(function (a, b) {
$("#quick_release_link").attr("onclick", "window.open('/inc/ajax/qitem_details.php?id=" + qitem + "&quick_release', '_blank')"); if (a.score === 0) return 1
$("#quick_delete_link").attr("onclick", "window.open('/inc/ajax/qitem_details.php?id=" + qitem + "&quick_delete', '_blank')"); if (b.score === 0) return -1
if (b.score < 0 && a.score < 0) {
$('#qid_detail_subj').text(data.subject); return a.score - b.score
$('#qid_detail_hfrom').text(data.header_from); }
$('#qid_detail_efrom').text(data.env_from); if (b.score > 0 && a.score > 0) {
$('#qid_detail_score').html(''); return b.score - a.score
$('#qid_detail_recipients').html(''); }
$('#qid_detail_symbols').html(''); return b.score - a.score
$('#qid_detail_fuzzy').html(''); })
if (typeof data.symbols !== 'undefined') { $.each(data.symbols, function (index, value) {
data.symbols.sort(function (a, b) { var highlightClass = ''
if (a.score === 0) return 1; if (value.score > 0) highlightClass = 'negative'
if (b.score === 0) return -1; else if (value.score < 0) highlightClass = 'positive'
if (b.score < 0 && a.score < 0) { else highlightClass = 'neutral'
return a.score - b.score; $('#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>');
} });
if (b.score > 0 && a.score > 0) { $('[data-bs-toggle="tooltip"]').tooltip()
return b.score - a.score; }
} if (typeof data.fuzzy_hashes === 'object' && data.fuzzy_hashes !== null && data.fuzzy_hashes.length !== 0) {
return b.score - a.score; $.each(data.fuzzy_hashes, function (index, value) {
}) $('#qid_detail_fuzzy').append('<p style="font-family:monospace">' + value + '</p>');
$.each(data.symbols, function (index, value) { });
var highlightClass = ''; } else {
if (value.score > 0) highlightClass = 'negative'; $('#qid_detail_fuzzy').append('-');
else if (value.score < 0) highlightClass = 'positive'; }
else highlightClass = 'neutral'; if (typeof data.score !== 'undefined' && typeof data.action !== 'undefined') {
$('#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>'); 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>');
$('[data-bs-toggle="tooltip"]').tooltip(); } 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>');
if (typeof data.fuzzy_hashes === 'object' && data.fuzzy_hashes !== null && data.fuzzy_hashes.length !== 0) { } else if (data.action == "rewrite subject") {
$.each(data.fuzzy_hashes, function (index, value) { $('#qid_detail_score').append('<span class="label-rspamd-action badge fs-6 bg-warning"><b>' + data.score + '</b> - ' + lang.rewrite_subject + '</span>');
$('#qid_detail_fuzzy').append('<p style="font-family:monospace">' + value + '</p>'); }
}); }
} else { if (typeof data.recipients !== 'undefined') {
$('#qid_detail_fuzzy').append('-'); $.each(data.recipients, function(index, value) {
} var elem = $('<span class="mail-address-item"></span>');
if (typeof data.score !== 'undefined' && typeof data.action !== 'undefined') { elem.text(value.address + ' (' + value.type.toUpperCase() + ')');
if (data.action == "add header") { $('#qid_detail_recipients').append(elem);
$('#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>'); $('#qid_detail_text').text(data.text_plain);
} else if (data.action == "rewrite subject") { $('#qid_detail_text_from_html').text(data.text_html);
$('#qid_detail_score').append('<span class="label-rspamd-action badge fs-6 bg-warning"><b>' + data.score + '</b> - ' + lang.rewrite_subject + '</span>'); var qAtts = $("#qid_detail_atts");
} if (typeof data.attachments !== 'undefined') {
} qAtts.text('');
if (typeof data.recipients !== 'undefined') { $.each(data.attachments, function(index, value) {
$.each(data.recipients, function(index, value) { qAtts.append(
var elem = $('<span class="mail-address-item"></span>'); '<p><a href="/inc/ajax/qitem_details.php?id=' + qitem + '&att=' + index + '" target="_blank">' + value[0] + '</a> (' + value[1] + ')' +
elem.text(value.address + ' (' + value.type.toUpperCase() + ')'); ' - <small><a href="' + value[3] + '" target="_blank">' + lang.check_hash + '</a></small></p>'
$('#qid_detail_recipients').append(elem); );
}); });
} }
$('#qid_detail_text').text(data.text_plain); else {
$('#qid_detail_text_from_html').text(data.text_html); qAtts.text('-');
var qAtts = $("#qid_detail_atts"); }
if (typeof data.attachments !== 'undefined') { },
qAtts.text(''); error: function(data){
$.each(data.attachments, function(index, value) { if (typeof data.error !== 'undefined') {
qAtts.append( $('#qid_detail_subj').text('-');
'<p><a href="/inc/ajax/qitem_details.php?id=' + qitem + '&att=' + index + '" target="_blank">' + value[0] + '</a> (' + value[1] + ')' + $('#qid_detail_hfrom').text('-');
' - <small><a href="' + value[3] + '" target="_blank">' + lang.check_hash + '</a></small></p>' $('#qid_detail_efrom').text('-');
); $('#qid_detail_score').html('-');
}); $('#qid_detail_recipients').html('-');
} $('#qid_detail_symbols').html('-');
else { $('#qid_detail_fuzzy').html('-');
qAtts.text('-'); $('#qid_detail_text').text('-');
} $('#qid_detail_text_from_html').text('-');
}, qError.text("Error loading quarantine item");
error: function(data){ qError.show();
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('-'); $('body').on('click', 'span.footable-toggle', function () {
$('#qid_detail_symbols').html('-'); event.stopPropagation();
$('#qid_detail_fuzzy').html('-'); })
$('#qid_detail_text').text('-');
$('#qid_detail_text_from_html').text('-'); // Initial table drawings
qError.text("Error loading quarantine item"); draw_quarantine_table();
qError.show();
}
} function hideTableExpandCollapseBtn(table){
}); if ($(table).hasClass('collapsed'))
}); $(".table_collapse_option").show();
else
$('body').on('click', 'span.footable-toggle', function () { $(".table_collapse_option").hide();
event.stopPropagation(); }
}) });
// Initial table drawings
draw_quarantine_table();
function hideTableExpandCollapseBtn(table){
if ($(table).hasClass('collapsed'))
$(".table_collapse_option").show();
else
$(".table_collapse_option").hide();
}
});

View File

@@ -1,128 +1,128 @@
jQuery(function($){ jQuery(function($){
$(".refresh_table").on('click', function(e) { $(".refresh_table").on('click', function(e) {
e.preventDefault(); e.preventDefault();
var table_name = $(this).data('table'); var table_name = $(this).data('table');
$('#' + table_name).DataTable().ajax.reload(); $('#' + 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]}
// 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);
}
}); });
})
function draw_queue() {
// just recalc width if instance already exists
if ($.fn.DataTable.isDataTable('#queuetable') ) {
$('#queuetable').DataTable().columns.adjust().responsive.recalc();
return;
}
$('#queuetable').DataTable({ 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]}
responsive: true,
processing: true, // Queue item
serverSide: false, $('#showQueuedMsg').on('show.bs.modal', function (e) {
stateSave: true, $('#queue_msg_content').text(lang.loading);
pageLength: pagination_size, button = $(e.relatedTarget)
dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" + if (button != null) {
"tr" + $('#queue_id').text(button.data('queue-id'));
"<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>", }
language: lang_datatables, $.ajax({
ajax: { type: 'GET',
type: "GET", url: '/api/v1/get/postcat/' + button.data('queue-id'),
url: "/api/v1/get/mailq/all", dataType: 'text',
dataSrc: function(data){ complete: function (data) {
$.each(data, function (i, item) { console.log(data);
item.chkbox = '<input type="checkbox" data-id="mailqitems" name="multi_select" value="' + item.queue_id + '" />'; $('#queue_msg_content').text(data.responseText);
rcpts = $.map(item.recipients, function(i) { }
return escapeHtml(i); });
}); })
item.recipients = rcpts.join('<hr style="margin:1px!important">');
item.action = '<div class="btn-group">' + function draw_queue() {
'<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>' + // 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,
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.queue_show_message + '</a>' +
'</div>'; '</div>';
}); });
return data; return data;
} }
}, },
columns: [ columns: [
{ {
// placeholder, so checkbox will not block child row toggle // placeholder, so checkbox will not block child row toggle
title: '', title: '',
data: null, data: null,
searchable: false, searchable: false,
orderable: false, orderable: false,
defaultContent: '' defaultContent: ''
}, },
{ {
title: '', title: '',
data: 'chkbox', data: 'chkbox',
searchable: false, searchable: false,
orderable: false, orderable: false,
defaultContent: '' defaultContent: ''
}, },
{ {
title: 'QID', title: 'QID',
data: 'queue_id', data: 'queue_id',
defaultContent: '' defaultContent: ''
}, },
{ {
title: 'Queue', title: 'Queue',
data: 'queue_name', data: 'queue_name',
defaultContent: '' defaultContent: ''
}, },
{ {
title: lang_admin.arrival_time, title: lang_admin.arrival_time,
data: 'arrival_time', data: 'arrival_time',
defaultContent: '', defaultContent: '',
render: function (data, type){ render: function (data, type){
var date = new Date(data ? data * 1000 : 0); 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"}); 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, title: lang_admin.message_size,
data: 'message_size', data: 'message_size',
defaultContent: '', defaultContent: '',
render: function (data, type){ render: function (data, type){
return humanFileSize(data); return humanFileSize(data);
} }
}, },
{ {
title: lang_admin.sender, title: lang_admin.sender,
data: 'sender', data: 'sender',
defaultContent: '' defaultContent: ''
}, },
{ {
title: lang_admin.recipients, title: lang_admin.recipients,
data: 'recipients', data: 'recipients',
defaultContent: '' defaultContent: ''
}, },
{ {
title: lang_admin.action, title: lang_admin.action,
data: 'action', data: 'action',
className: 'dt-sm-head-hidden dt-text-right', className: 'text-md-end dt-sm-head-hidden dt-body-right',
defaultContent: '' defaultContent: ''
}, },
] ]
}); });
} }
draw_queue(); draw_queue();
}) })

File diff suppressed because it is too large Load Diff

View File

@@ -288,18 +288,6 @@ if (isset($_GET['query'])) {
case "domain-admin": case "domain-admin":
process_add_return(domain_admin('add', $attr)); process_add_return(domain_admin('add', $attr));
break; 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;
}
break;
case "admin": case "admin":
process_add_return(admin('add', $attr)); process_add_return(admin('add', $attr));
break; break;

View File

@@ -650,7 +650,7 @@
}, },
"login": { "login": {
"delayed": "Přihlášení zpožděno o %s sekund.", "delayed": "Přihlášení zpožděno o %s sekund.",
"fido2_webauthn": "FIDO2/WebAuthn Login", "fido2_webauthn": "FIDO2/WebAuthn",
"login": "Přihlásit", "login": "Přihlásit",
"mobileconfig_info": "Ke stažení profilového souboru se přihlaste jako uživatel schránky.", "mobileconfig_info": "Ke stažení profilového souboru se přihlaste jako uživatel schránky.",
"other_logins": "Přihlášení klíčem", "other_logins": "Přihlášení klíčem",

View File

@@ -1,18 +1,18 @@
{ {
"acl": { "acl": {
"alias_domains": "Tilføj domænealias", "alias_domains": "Tilføj kældenavn domæner",
"app_passwds": "Administrer app-adgangskoder", "app_passwds": "Administrer app-adgangskoder",
"bcc_maps": "BCC kort", "bcc_maps": "BCC kort",
"delimiter_action": "Afgrænsning handling", "delimiter_action": "Afgrænsning handling",
"eas_reset": "Nulstil EAS enheder", "eas_reset": "Nulstil EAS endheder",
"extend_sender_acl": "Tillad at udvide afsenderens ACL med eksterne adresser", "extend_sender_acl": "Tillad at udvide afsenderens ACL med eksterne adresser",
"filters": "Filtre", "filters": "Filtre",
"login_as": "Login som mailboks bruger", "login_as": "Login som mailboks bruger",
"prohibited": "Nægtet af ACL", "prohibited": "Forbudt af ACL",
"protocol_access": "Skift protokol adgang", "protocol_access": "Ændre protokol adgang",
"pushover": "Pushover", "pushover": "Pushover",
"quarantine": "Karantænehandlinger", "quarantine": "Karantæneaktioner",
"quarantine_attachments": "Karantænevedhæftede filer", "quarantine_attachments": "Karantæne vedhæftede filer",
"quarantine_notification": "Skift karantænemeddelelser", "quarantine_notification": "Skift karantænemeddelelser",
"ratelimit": "Satsgrænse", "ratelimit": "Satsgrænse",
"recipient_maps": "Modtagerkort", "recipient_maps": "Modtagerkort",
@@ -20,15 +20,12 @@
"sogo_access": "Tillad styring af SOGo-adgang", "sogo_access": "Tillad styring af SOGo-adgang",
"sogo_profile_reset": "Nulstil SOGo-profil", "sogo_profile_reset": "Nulstil SOGo-profil",
"spam_alias": "Midlertidige aliasser", "spam_alias": "Midlertidige aliasser",
"spam_policy": "Sortliste/hvidliste", "spam_policy": "Sortliste / hvidliste",
"spam_score": "Spam-score", "spam_score": "Spam-score",
"syncjobs": "Synkroniserings job", "syncjobs": "Synkroniser job",
"tls_policy": "TLS politik", "tls_policy": "TLS politik",
"unlimited_quota": "Ubegrænset plads for mailbokse", "unlimited_quota": "Ubegrænset quote for mailbokse",
"domain_desc": "Skift domæne beskrivelse", "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": { "add": {
"activate_filter_warn": "Alle andre filtre deaktiveres, når aktiv er markeret.", "activate_filter_warn": "Alle andre filtre deaktiveres, når aktiv er markeret.",
@@ -36,7 +33,7 @@
"add": "Tilføj", "add": "Tilføj",
"add_domain_only": "Tilføj kun domæne", "add_domain_only": "Tilføj kun domæne",
"add_domain_restart": "Tilføj domæne og genstart SOGo", "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_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": "Alias-domæne",
"alias_domain_info": "<small>Kun gyldige domænenavne (kommasepareret).</small>", "alias_domain_info": "<small>Kun gyldige domænenavne (kommasepareret).</small>",
@@ -62,7 +59,7 @@
"gal": "Global adresseliste", "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>", "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", "generate": "generere",
"goto_ham": "Lær som <span class=\"text-success\"><b>ønsket</b></span>", "goto_ham": "Lær som <span class=\"text-success\"><b>ham</b></span>",
"goto_null": "Kassér e-mail i stilhed", "goto_null": "Kassér e-mail i stilhed",
"goto_spam": "Lær som <span class=\"text-danger\"><b>spam</b></span>", "goto_spam": "Lær som <span class=\"text-danger\"><b>spam</b></span>",
"hostname": "Vært", "hostname": "Vært",
@@ -83,7 +80,7 @@
"private_comment": "Privat kommentar", "private_comment": "Privat kommentar",
"public_comment": "Offentlig kommentar", "public_comment": "Offentlig kommentar",
"quota_mb": "Kvota (Mb)", "quota_mb": "Kvota (Mb)",
"relay_all": "Besvar alle modtager", "relay_all": "Send alle modtagere videre",
"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_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_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.", "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.",
@@ -104,10 +101,7 @@
"timeout2": "Timeout for forbindelse til lokal vært", "timeout2": "Timeout for forbindelse til lokal vært",
"username": "Brugernavn", "username": "Brugernavn",
"validate": "Bekræft", "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": { "admin": {
"access": "Adgang", "access": "Adgang",
@@ -314,10 +308,7 @@
"username": "Brugernavn", "username": "Brugernavn",
"validate_license_now": "Valider GUID mod licensserver", "validate_license_now": "Valider GUID mod licensserver",
"verify": "Verificere", "verify": "Verificere",
"yes": "&#10003;", "yes": "&#10003;"
"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": { "danger": {
"access_denied": "Adgang nægtet eller ugyldig formular data", "access_denied": "Adgang nægtet eller ugyldig formular data",
@@ -434,8 +425,7 @@
"username_invalid": "Brugernavn %s kan ikke bruges", "username_invalid": "Brugernavn %s kan ikke bruges",
"validity_missing": "Tildel venligst en gyldighedsperiode", "validity_missing": "Tildel venligst en gyldighedsperiode",
"value_missing": "Angiv alle værdier", "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": { "debug": {
"chart_this_server": "Diagram (denne server)", "chart_this_server": "Diagram (denne server)",
@@ -452,8 +442,7 @@
"solr_status": "Solr-status", "solr_status": "Solr-status",
"started_on": "Startede den", "started_on": "Startede den",
"static_logs": "Statiske logfiler", "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": { "diagnostics": {
"cname_from_a": "Værdi afledt af A / AAAA-post. Dette understøttes, så længe posten peger på den korrekte ressource.", "cname_from_a": "Værdi afledt af A / AAAA-post. Dette understøttes, så længe posten peger på den korrekte ressource.",
@@ -564,11 +553,7 @@
"title": "Rediger objekt", "title": "Rediger objekt",
"unchanged_if_empty": "Lad være tomt, hvis uændret", "unchanged_if_empty": "Lad være tomt, hvis uændret",
"username": "Brugernavn", "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": { "footer": {
"cancel": "Afbestille", "cancel": "Afbestille",
@@ -586,7 +571,7 @@
"header": { "header": {
"administration": "Konfiguration og detailer", "administration": "Konfiguration og detailer",
"apps": "Apps", "apps": "Apps",
"debug": "Information", "debug": "Systemoplysninger",
"email": "E-Mail", "email": "E-Mail",
"mailcow_config": "Konfiguration", "mailcow_config": "Konfiguration",
"quarantine": "Karantæne", "quarantine": "Karantæne",
@@ -601,7 +586,7 @@
}, },
"login": { "login": {
"delayed": "Login blev forsinket med% s sekunder.", "delayed": "Login blev forsinket med% s sekunder.",
"fido2_webauthn": "FIDO2/WebAuthn Login", "fido2_webauthn": "FIDO2/WebAuthn",
"login": "Login", "login": "Login",
"mobileconfig_info": "Log ind som postkassebruger for at downloade den anmodede Apple-forbindelsesprofil.", "mobileconfig_info": "Log ind som postkassebruger for at downloade den anmodede Apple-forbindelsesprofil.",
"other_logins": "Nøgle login", "other_logins": "Nøgle login",
@@ -754,10 +739,7 @@
"username": "Brugernavn", "username": "Brugernavn",
"waiting": "Venter", "waiting": "Venter",
"weekly": "Ugentlig", "weekly": "Ugentlig",
"yes": "&#10003;", "yes": "&#10003;"
"goto_ham": "Lær som <b>ønsket</b>",
"catch_all": "Fang-alt",
"open_logs": "Åben logfiler"
}, },
"oauth2": { "oauth2": {
"access_denied": "Log ind som mailboks ejer for at give adgang via OAuth2.", "access_denied": "Log ind som mailboks ejer for at give adgang via OAuth2.",
@@ -1048,7 +1030,7 @@
"spamfilter_table_empty": "Intet data at vise", "spamfilter_table_empty": "Intet data at vise",
"spamfilter_table_remove": "slet", "spamfilter_table_remove": "slet",
"spamfilter_table_rule": "Regl", "spamfilter_table_rule": "Regl",
"spamfilter_wl": "Hvidliste", "spamfilter_wl": "Hvisliste",
"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_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", "spamfilter_yellow": "Gul: denne besked kan være spam, vil blive tagget som spam og flyttes til din junk-mappe",
"status": "Status", "status": "Status",
@@ -1084,11 +1066,5 @@
"quota_exceeded_scope": "Domænekvote overskredet: Kun ubegrænsede postkasser kan oprettes i dette domæneomfang.", "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_token": "Form nøgle ugyldig: Nøgle passer ikke",
"session_ua": "Form nøgle ugyldig: Bruger-Agent gyldighedskontrols fejl" "session_ua": "Form nøgle ugyldig: Bruger-Agent gyldighedskontrols fejl"
},
"datatables": {
"lengthMenu": "Vis _MENU_ poster",
"paginate": {
"first": "Først"
}
} }
} }

View File

@@ -175,12 +175,10 @@
"empty": "Keine Einträge vorhanden", "empty": "Keine Einträge vorhanden",
"excludes": "Diese Empfänger ausschließen", "excludes": "Diese Empfänger ausschließen",
"f2b_ban_time": "Bannzeit in Sekunden", "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_blacklist": "Blacklist für Netzwerke und Hosts",
"f2b_filter": "Regex-Filter", "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_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_attempts": "Max. Versuche",
"f2b_max_ban_time": "Maximale Bannzeit in Sekunden",
"f2b_netban_ipv4": "Netzbereich für IPv4-Banns (8-32)", "f2b_netban_ipv4": "Netzbereich für IPv4-Banns (8-32)",
"f2b_netban_ipv6": "Netzbereich für IPv6-Banns (8-128)", "f2b_netban_ipv6": "Netzbereich für IPv6-Banns (8-128)",
"f2b_parameters": "Fail2ban-Parameter", "f2b_parameters": "Fail2ban-Parameter",
@@ -341,8 +339,7 @@
"oauth2_add_client": "Füge OAuth2 Client hinzu", "oauth2_add_client": "Füge OAuth2 Client hinzu",
"api_read_only": "Schreibgeschützter Zugriff", "api_read_only": "Schreibgeschützter Zugriff",
"api_read_write": "Lese-Schreib-Zugriff", "api_read_write": "Lese-Schreib-Zugriff",
"oauth2_apps": "OAuth2 Apps", "oauth2_apps": "OAuth2 Apps"
"queue_unban": "entsperren"
}, },
"danger": { "danger": {
"access_denied": "Zugriff verweigert oder unvollständige/ungültige Daten", "access_denied": "Zugriff verweigert oder unvollständige/ungültige Daten",
@@ -369,7 +366,7 @@
"domain_not_empty": "Domain %s ist nicht leer", "domain_not_empty": "Domain %s ist nicht leer",
"domain_not_found": "Domain %s nicht gefunden", "domain_not_found": "Domain %s nicht gefunden",
"domain_quota_m_in_use": "Domain-Speicherplatzlimit muss größer oder gleich %d MiB sein", "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", "extended_sender_acl_denied": "Keine Rechte zum setzen von externen Absenderadressen",
"extra_acl_invalid": "Externe Absenderadresse \"%s\" ist ungültig", "extra_acl_invalid": "Externe Absenderadresse \"%s\" ist ungültig",
"extra_acl_invalid_domain": "Externe Absenderadresse \"%s\" verwendet eine ungültige Domain", "extra_acl_invalid_domain": "Externe Absenderadresse \"%s\" verwendet eine ungültige Domain",
"fido2_verification_failed": "FIDO2-Verifizierung fehlgeschlagen: %s", "fido2_verification_failed": "FIDO2-Verifizierung fehlgeschlagen: %s",
@@ -457,23 +454,17 @@
"totp_verification_failed": "TOTP-Verifizierung fehlgeschlagen", "totp_verification_failed": "TOTP-Verifizierung fehlgeschlagen",
"transport_dest_exists": "Transport-Maps-Ziel \"%s\" existiert bereits", "transport_dest_exists": "Transport-Maps-Ziel \"%s\" existiert bereits",
"webauthn_verification_failed": "WebAuthn-Verifizierung fehlgeschlagen: %s", "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": "Ein unbekannter Fehler trat auf",
"unknown_tfa_method": "Unbekannte TFA-Methode", "unknown_tfa_method": "Unbekannte TFA-Methode",
"unlimited_quota_acl": "Unendliche Quota untersagt durch ACL", "unlimited_quota_acl": "Unendliche Quota untersagt durch ACL",
"username_invalid": "Benutzername %s kann nicht verwendet werden", "username_invalid": "Benutzername %s kann nicht verwendet werden",
"validity_missing": "Bitte geben Sie eine Gültigkeitsdauer an", "validity_missing": "Bitte geben Sie eine Gültigkeitsdauer an",
"value_missing": "Bitte alle Felder ausfüllen", "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": { "datatables": {
"collapse_all": "Alle Einklappen", "collapse_all": "Alle Einklappen",
"decimal": ",", "decimal": "",
"emptyTable": "Keine Daten in der Tabelle vorhanden", "emptyTable": "Keine Daten in der Tabelle vorhanden",
"expand_all": "Alle Ausklappen", "expand_all": "Alle Ausklappen",
"info": "_START_ bis _END_ von _TOTAL_ Einträgen", "info": "_START_ bis _END_ von _TOTAL_ Einträgen",
@@ -507,7 +498,7 @@
"current_time": "Systemzeit", "current_time": "Systemzeit",
"disk_usage": "Festplattennutzung", "disk_usage": "Festplattennutzung",
"docs": "Dokumente", "docs": "Dokumente",
"error_show_ip": "Konnte die öffentlichen IP Adressen nicht auflösen", "error_show_ip": "konnte die öffentlichen IP Adressen nicht auflösen",
"external_logs": "Externe Logs", "external_logs": "Externe Logs",
"history_all_servers": "History (alle Server)", "history_all_servers": "History (alle Server)",
"in_memory_logs": "In-memory Logs", "in_memory_logs": "In-memory Logs",
@@ -660,8 +651,7 @@
"title": "Objekt bearbeiten", "title": "Objekt bearbeiten",
"unchanged_if_empty": "Unverändert, wenn leer", "unchanged_if_empty": "Unverändert, wenn leer",
"username": "Benutzername", "username": "Benutzername",
"validate_save": "Validieren und speichern", "validate_save": "Validieren und speichern"
"pushover_sound": "Ton"
}, },
"fido2": { "fido2": {
"confirm": "Bestätigen", "confirm": "Bestätigen",
@@ -702,8 +692,7 @@
"quarantine": "Quarantäne", "quarantine": "Quarantäne",
"restart_netfilter": "Netfilter neustarten", "restart_netfilter": "Netfilter neustarten",
"restart_sogo": "SOGo neustarten", "restart_sogo": "SOGo neustarten",
"user_settings": "Benutzereinstellungen", "user_settings": "Benutzereinstellungen"
"mailcow_system": "System"
}, },
"info": { "info": {
"awaiting_tfa_confirmation": "Warte auf TFA-Verifizierung", "awaiting_tfa_confirmation": "Warte auf TFA-Verifizierung",
@@ -712,7 +701,7 @@
}, },
"login": { "login": {
"delayed": "Login wurde zur Sicherheit um %s Sekunde/n verzögert.", "delayed": "Login wurde zur Sicherheit um %s Sekunde/n verzögert.",
"fido2_webauthn": "FIDO2/WebAuthn Login", "fido2_webauthn": "FIDO2/WebAuthn",
"login": "Anmelden", "login": "Anmelden",
"mobileconfig_info": "Bitte als Mailbox-Benutzer einloggen, um das Verbindungsprofil herunterzuladen.", "mobileconfig_info": "Bitte als Mailbox-Benutzer einloggen, um das Verbindungsprofil herunterzuladen.",
"other_logins": "Key Login", "other_logins": "Key Login",
@@ -782,6 +771,7 @@
"edit": "Bearbeiten", "edit": "Bearbeiten",
"empty": "Keine Einträge vorhanden", "empty": "Keine Einträge vorhanden",
"enable_x": "Aktivieren", "enable_x": "Aktivieren",
"encryption": "Verschlüsselung",
"excludes": "Ausschlüsse", "excludes": "Ausschlüsse",
"filter_table": "Filtern", "filter_table": "Filtern",
"filters": "Filter", "filters": "Filter",
@@ -1186,6 +1176,7 @@
"recent_successful_connections": "Kürzlich erfolgreiche Verbindungen", "recent_successful_connections": "Kürzlich erfolgreiche Verbindungen",
"remove": "Entfernen", "remove": "Entfernen",
"running": "Wird ausgeführt", "running": "Wird ausgeführt",
"inactive": "Inaktiv",
"save": "Änderungen speichern", "save": "Änderungen speichern",
"save_changes": "Änderungen speichern", "save_changes": "Änderungen speichern",
"sender_acl_disabled": "<span class=\"badge fs-6 bg-danger\">Absenderprüfung deaktiviert</span>", "sender_acl_disabled": "<span class=\"badge fs-6 bg-danger\">Absenderprüfung deaktiviert</span>",
@@ -1231,7 +1222,7 @@
"user_settings": "Benutzereinstellungen", "user_settings": "Benutzereinstellungen",
"username": "Benutzername", "username": "Benutzername",
"verify": "Verifizieren", "verify": "Verifizieren",
"waiting": "Warte auf Ausführung", "waiting": "Wartend",
"week": "Woche", "week": "Woche",
"weekly": "Wöchentlich", "weekly": "Wöchentlich",
"weeks": "Wochen", "weeks": "Wochen",
@@ -1247,8 +1238,7 @@
"syncjob_EXIT_CONNECTION_FAILURE": "Verbindungsproblem", "syncjob_EXIT_CONNECTION_FAILURE": "Verbindungsproblem",
"syncjob_EXIT_TLS_FAILURE": "Problem mit verschlüsselter Verbindung", "syncjob_EXIT_TLS_FAILURE": "Problem mit verschlüsselter Verbindung",
"syncjob_EXIT_AUTHENTICATION_FAILURE": "Authentifizierungsproblem", "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": { "warning": {
"cannot_delete_self": "Kann derzeit eingeloggten Benutzer nicht entfernen", "cannot_delete_self": "Kann derzeit eingeloggten Benutzer nicht entfernen",

View File

@@ -177,12 +177,10 @@
"empty": "No results", "empty": "No results",
"excludes": "Excludes these recipients", "excludes": "Excludes these recipients",
"f2b_ban_time": "Ban time (s)", "f2b_ban_time": "Ban time (s)",
"f2b_ban_time_increment": "Ban time is incremented with each ban",
"f2b_blacklist": "Blacklisted networks/hosts", "f2b_blacklist": "Blacklisted networks/hosts",
"f2b_filter": "Regex filters", "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_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_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_ipv4": "IPv4 subnet size to apply ban on (8-32)",
"f2b_netban_ipv6": "IPv6 subnet size to apply ban on (8-128)", "f2b_netban_ipv6": "IPv6 subnet size to apply ban on (8-128)",
"f2b_parameters": "Fail2ban parameters", "f2b_parameters": "Fail2ban parameters",
@@ -460,9 +458,6 @@
"totp_verification_failed": "TOTP verification failed", "totp_verification_failed": "TOTP verification failed",
"transport_dest_exists": "Transport destination \"%s\" exists", "transport_dest_exists": "Transport destination \"%s\" exists",
"webauthn_verification_failed": "WebAuthn verification failed: %s", "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": "An unknown error occurred",
"unknown_tfa_method": "Unknown TFA method", "unknown_tfa_method": "Unknown TFA method",
"unlimited_quota_acl": "Unlimited quota prohibited by ACL", "unlimited_quota_acl": "Unlimited quota prohibited by ACL",
@@ -473,7 +468,7 @@
}, },
"datatables": { "datatables": {
"collapse_all": "Collapse All", "collapse_all": "Collapse All",
"decimal": ".", "decimal": "",
"emptyTable": "No data available in table", "emptyTable": "No data available in table",
"expand_all": "Expand All", "expand_all": "Expand All",
"info": "Showing _START_ to _END_ of _TOTAL_ entries", "info": "Showing _START_ to _END_ of _TOTAL_ entries",
@@ -712,7 +707,7 @@
}, },
"login": { "login": {
"delayed": "Login was delayed by %s seconds.", "delayed": "Login was delayed by %s seconds.",
"fido2_webauthn": "FIDO2/WebAuthn Login", "fido2_webauthn": "FIDO2/WebAuthn",
"login": "Login", "login": "Login",
"mobileconfig_info": "Please login as mailbox user to download the requested Apple connection profile.", "mobileconfig_info": "Please login as mailbox user to download the requested Apple connection profile.",
"other_logins": "Key login", "other_logins": "Key login",
@@ -784,6 +779,7 @@
"edit": "Edit", "edit": "Edit",
"empty": "No results", "empty": "No results",
"enable_x": "Enable", "enable_x": "Enable",
"encryption": "Encryption",
"excludes": "Excludes", "excludes": "Excludes",
"filter_table": "Filter table", "filter_table": "Filter table",
"filters": "Filters", "filters": "Filters",
@@ -1148,6 +1144,7 @@
"hour": "hour", "hour": "hour",
"hourly": "Hourly", "hourly": "Hourly",
"hours": "hours", "hours": "hours",
"inactive": "Inactive",
"in_use": "Used", "in_use": "Used",
"interval": "Interval", "interval": "Interval",
"is_catch_all": "Catch-all for domain/s", "is_catch_all": "Catch-all for domain/s",

View File

@@ -141,11 +141,9 @@
"empty": "Sin resultados", "empty": "Sin resultados",
"excludes": "Excluye a estos destinatarios", "excludes": "Excluye a estos destinatarios",
"f2b_ban_time": "Tiempo de restricción (s)", "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_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_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_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_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_netban_ipv6": "Tamaño de subred IPv6 para aplicar la restricción (8-128)",
"f2b_parameters": "Parametros Fail2ban", "f2b_parameters": "Parametros Fail2ban",

View File

@@ -24,7 +24,7 @@
"spam_policy": "Liste Noire/Liste Blanche", "spam_policy": "Liste Noire/Liste Blanche",
"spam_score": "Score SPAM", "spam_score": "Score SPAM",
"syncjobs": "Tâches de synchronisation", "syncjobs": "Tâches de synchronisation",
"tls_policy": "Politique TLS", "tls_policy": "Police TLS",
"unlimited_quota": "Quota illimité pour les boites de courriel", "unlimited_quota": "Quota illimité pour les boites de courriel",
"domain_desc": "Modifier la description du domaine", "domain_desc": "Modifier la description du domaine",
"domain_relayhost": "Changer le relais pour un domaine", "domain_relayhost": "Changer le relais pour un domaine",
@@ -106,8 +106,7 @@
"validate": "Valider", "validate": "Valider",
"validation_success": "Validation réussie", "validation_success": "Validation réussie",
"bcc_dest_format": "La destination Cci doit être une seule adresse e-mail valide.<br>Si vous avez besoin d'envoyer une copie à plusieurs adresses, créez un alias et utilisez-le ici.", "bcc_dest_format": "La destination Cci doit être une seule adresse e-mail valide.<br>Si vous avez besoin d'envoyer une copie à plusieurs adresses, créez un alias et utilisez-le ici.",
"tags": "Etiquettes", "tags": "Etiquettes"
"app_passwd_protocols": "Protocoles autorisés pour le mot de passe de l'application"
}, },
"admin": { "admin": {
"access": "Accès", "access": "Accès",
@@ -172,13 +171,11 @@
"edit": "Editer", "edit": "Editer",
"empty": "Aucun résultat", "empty": "Aucun résultat",
"excludes": "Exclure ces destinataires", "excludes": "Exclure ces destinataires",
"f2b_ban_time": "Durée du bannissement (s)", "f2b_ban_time": "Durée du bannissement(s)",
"f2b_ban_time_increment": "Durée du bannissement est augmentée à chaque bannissement",
"f2b_blacklist": "Réseaux/Domaines sur Liste Noire", "f2b_blacklist": "Réseaux/Domaines sur Liste Noire",
"f2b_filter": "Filtre(s) Regex", "f2b_filter": "Filtre(s) Regex",
"f2b_list_info": "Un hôte ou un réseau sur liste noire l'emportera toujours sur une entité de liste blanche. <b>L'application des mises à jour de liste prendra quelques secondes.</b>", "f2b_list_info": "Un hôte ou un réseau sur liste noire l'emportera toujours sur une entité de liste blanche. <b>L'application des mises à jour de liste prendra quelques secondes.</b>",
"f2b_max_attempts": "Nb max. de tentatives", "f2b_max_attempts": "Nb max. de tentatives",
"f2b_max_ban_time": "Max. durée du bannissement (s)",
"f2b_netban_ipv4": "Taille du sous-réseau IPv4 pour l'application du bannissement (8-32)", "f2b_netban_ipv4": "Taille du sous-réseau IPv4 pour l'application du bannissement (8-32)",
"f2b_netban_ipv6": "Taille du sous-réseau IPv6 pour l'application du bannissement (8-128)", "f2b_netban_ipv6": "Taille du sous-réseau IPv6 pour l'application du bannissement (8-128)",
"f2b_parameters": "Paramètres Fail2ban", "f2b_parameters": "Paramètres Fail2ban",
@@ -324,9 +321,7 @@
"admins": "Administrateurs", "admins": "Administrateurs",
"api_read_only": "Accès lecture-seule", "api_read_only": "Accès lecture-seule",
"password_policy_lowerupper": "Doit contenir des caractères minuscules et majuscules", "password_policy_lowerupper": "Doit contenir des caractères minuscules et majuscules",
"password_policy_numbers": "Doit contenir au moins un chiffre", "password_policy_numbers": "Doit contenir au moins un chiffre"
"ip_check": "Vérification IP",
"ip_check_disabled": "La vérification IP est désactivée. Vous pouvez l'activer sous<br> <strong>Système > Configuration > Options > Personnaliser</strong>"
}, },
"danger": { "danger": {
"access_denied": "Accès refusé ou données de formulaire non valides", "access_denied": "Accès refusé ou données de formulaire non valides",
@@ -445,12 +440,7 @@
"username_invalid": "Le nom d'utilisateur %s ne peut pas être utilisé", "username_invalid": "Le nom d'utilisateur %s ne peut pas être utilisé",
"validity_missing": "Veuillez attribuer une période de validité", "validity_missing": "Veuillez attribuer une période de validité",
"value_missing": "Veuillez fournir toutes les valeurs", "value_missing": "Veuillez fournir toutes les valeurs",
"yotp_verification_failed": "La vérification Yubico OTP a échoué : %s", "yotp_verification_failed": "La vérification Yubico OTP a échoué : %s"
"webauthn_authenticator_failed": "L'authentificateur selectionné est introuvable",
"demo_mode_enabled": "Le mode de démonstration est activé",
"template_exists": "La template %s existe déja",
"template_id_invalid": "Le numéro de template %s est invalide",
"template_name_invalid": "Le nom de la template est invalide"
}, },
"debug": { "debug": {
"chart_this_server": "Graphique (ce serveur)", "chart_this_server": "Graphique (ce serveur)",
@@ -588,7 +578,7 @@
"unchanged_if_empty": "Si non modifié, laisser en blanc", "unchanged_if_empty": "Si non modifié, laisser en blanc",
"username": "Nom d'utilisateur", "username": "Nom d'utilisateur",
"validate_save": "Valider et sauver", "validate_save": "Valider et sauver",
"lookup_mx": "La destination est une expression régulière qui doit correspondre avec le nom du MX (<code>.*google\\.com</code> pour acheminer tout le courrier destiné à un MX se terminant par google.com via ce saut)", "lookup_mx": "La destination est une expression régulière qui doit correspondre avec le nom du MX (<code>.*google\\.com</code> pour acheminer tout le courrier destiné à un MX se terminant par google.com via ce saut).",
"mailbox_relayhost_info": "S'applique uniquement à la boîte aux lettres et aux alias directs, remplace le relayhost du domaine." "mailbox_relayhost_info": "S'applique uniquement à la boîte aux lettres et aux alias directs, remplace le relayhost du domaine."
}, },
"footer": { "footer": {
@@ -622,7 +612,7 @@
}, },
"login": { "login": {
"delayed": "La connexion a été retardée de %s secondes.", "delayed": "La connexion a été retardée de %s secondes.",
"fido2_webauthn": "FIDO2/WebAuthn Login", "fido2_webauthn": "FIDO2/WebAuthn",
"login": "Connexion", "login": "Connexion",
"mobileconfig_info": "Veuillez vous connecter en tant quutilisateur de la boîte pour télécharger le profil de connexion Apple demandé.", "mobileconfig_info": "Veuillez vous connecter en tant quutilisateur de la boîte pour télécharger le profil de connexion Apple demandé.",
"other_logins": "Clé d'authentification", "other_logins": "Clé d'authentification",
@@ -1091,12 +1081,9 @@
"username": "Nom d'utilisateur", "username": "Nom d'utilisateur",
"verify": "Vérification", "verify": "Vérification",
"waiting": "En attente", "waiting": "En attente",
"week": "semaine", "week": "Semaine",
"weekly": "Hebdomadaire", "weekly": "Hebdomadaire",
"weeks": "semaines", "weeks": "semaines"
"months": "mois",
"year": "année",
"years": "années"
}, },
"warning": { "warning": {
"cannot_delete_self": "Impossible de supprimer lutilisateur connecté", "cannot_delete_self": "Impossible de supprimer lutilisateur connecté",

View File

@@ -175,12 +175,10 @@
"empty": "Nessun risultato", "empty": "Nessun risultato",
"excludes": "Esclude questi destinatari", "excludes": "Esclude questi destinatari",
"f2b_ban_time": "Tempo di blocco (s)", "f2b_ban_time": "Tempo di blocco (s)",
"f2b_ban_time_increment": "Tempo di blocco aumenta ad ogni blocco",
"f2b_blacklist": "Host/reti in blacklist", "f2b_blacklist": "Host/reti in blacklist",
"f2b_filter": "Filtri Regex", "f2b_filter": "Filtri Regex",
"f2b_list_info": "Un host oppure una rete in blacklist, avrà sempre un peso maggiore rispetto ad una in whitelist. <b>L'aggiornamento della lista richiede alcuni secondi per la sua entrata in azione.</b>", "f2b_list_info": "Un host oppure una rete in blacklist, avrà sempre un peso maggiore rispetto ad una in whitelist. <b>L'aggiornamento della lista richiede alcuni secondi per la sua entrata in azione.</b>",
"f2b_max_attempts": "Tentativi massimi", "f2b_max_attempts": "Tentativi massimi",
"f2b_max_ban_time": "Tempo massimo di blocco (s)",
"f2b_netban_ipv4": "IPv4 subnet size to apply ban on (8-32)", "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_netban_ipv6": "IPv6 subnet size to apply ban on (8-128)",
"f2b_parameters": "Parametri Fail2ban", "f2b_parameters": "Parametri Fail2ban",
@@ -676,7 +674,7 @@
}, },
"login": { "login": {
"delayed": "L'accesso è stato ritardato di %s secondi.", "delayed": "L'accesso è stato ritardato di %s secondi.",
"fido2_webauthn": "FIDO2/WebAuthn Login", "fido2_webauthn": "FIDO2/WebAuthn",
"login": "Login", "login": "Login",
"mobileconfig_info": "Please login as mailbox user to download the requested Apple connection profile.", "mobileconfig_info": "Please login as mailbox user to download the requested Apple connection profile.",
"other_logins": "Key login", "other_logins": "Key login",

View File

@@ -3,8 +3,7 @@
"bcc_maps": "BCC kartes", "bcc_maps": "BCC kartes",
"filters": "Filtri", "filters": "Filtri",
"recipient_maps": "Saņēmēja kartes", "recipient_maps": "Saņēmēja kartes",
"syncjobs": "Sinhronizācijas uzdevumi", "syncjobs": "Sinhronizācijas uzdevumi"
"spam_score": "Mēstules novērtējums"
}, },
"add": { "add": {
"activate_filter_warn": "Visi pārējie filtri tiks deaktivizēti, kad aktīvs ir atzīmēts.", "activate_filter_warn": "Visi pārējie filtri tiks deaktivizēti, kad aktīvs ir atzīmēts.",
@@ -105,10 +104,10 @@
"host": "Hosts", "host": "Hosts",
"import": "Importēt", "import": "Importēt",
"import_private_key": "Importēt privātu atslēgu", "import_private_key": "Importēt privātu atslēgu",
"in_use_by": "Izmanto", "in_use_by": "Tiek lietots ar",
"inactive": "Neaktīvs", "inactive": "Neaktīvs",
"link": "Saite", "link": "Saite",
"loading": "Lūgums uzgaidīt...", "loading": "Lūdzu uzgaidiet...",
"logo_info": "Jūsu attēls augšējā navigācijas joslā tiks palielināts līdz 40 pikseļiem un maks. sākumlapas platums par 250 pikseļi. Ir ļoti ieteicama pielāgojama grafikaYour image will be scaled to a height of 40px for the top navigation bar and a max. width of 250px for the start page. Ir ļoti ieteicama pielāgojamā grafika", "logo_info": "Jūsu attēls augšējā navigācijas joslā tiks palielināts līdz 40 pikseļiem un maks. sākumlapas platums par 250 pikseļi. Ir ļoti ieteicama pielāgojama grafikaYour image will be scaled to a height of 40px for the top navigation bar and a max. width of 250px for the start page. Ir ļoti ieteicama pielāgojamā grafika",
"main_name": "\"mailcow UI\" nosaukums", "main_name": "\"mailcow UI\" nosaukums",
"merged_vars_hint": "Pelēkās rindas tika apvienotas <code>vars.(local.)inc.php</code> un nevar tikt modificētas.", "merged_vars_hint": "Pelēkās rindas tika apvienotas <code>vars.(local.)inc.php</code> un nevar tikt modificētas.",
@@ -145,10 +144,7 @@
"ui_texts": "UI etiķetes un teksti", "ui_texts": "UI etiķetes un teksti",
"unchanged_if_empty": "Ja nav veiktas izmaiņas, atstājiet tukšu", "unchanged_if_empty": "Ja nav veiktas izmaiņas, atstājiet tukšu",
"upload": "Augšupielādēt", "upload": "Augšupielādēt",
"username": "Lietotājvārds", "username": "Lietotājvārds"
"generate": "izveidot",
"message": "Ziņojums",
"last_applied": "Pēdējoreiz pielietots"
}, },
"danger": { "danger": {
"access_denied": "Piekļuve liegta, vai nepareizi dati", "access_denied": "Piekļuve liegta, vai nepareizi dati",
@@ -174,7 +170,7 @@
"is_alias": "%s jau ir zināms alias", "is_alias": "%s jau ir zināms alias",
"is_alias_or_mailbox": "%s jau ir zināms alias, pastkastes vai alias addrese izvērsta no alias domēna.", "is_alias_or_mailbox": "%s jau ir zināms alias, pastkastes vai alias addrese izvērsta no alias domēna.",
"is_spam_alias": "%s ir jau zināms spam alias", "is_spam_alias": "%s ir jau zināms spam alias",
"last_key": "Pēdējo atslēgu nevar izdzēst, tā vietā jāatspējo divpakāpju pārbaude.", "last_key": "Pēdējā atslēga nevar būt dzēsta",
"login_failed": "Ielogošanās neveiksmīga", "login_failed": "Ielogošanās neveiksmīga",
"mailbox_invalid": "Pastkastes vārds ir nederīgs", "mailbox_invalid": "Pastkastes vārds ir nederīgs",
"mailbox_quota_exceeded": "Kvota pārsniedz domēna limitu (max. %d MiB)", "mailbox_quota_exceeded": "Kvota pārsniedz domēna limitu (max. %d MiB)",
@@ -266,8 +262,7 @@
"title": "Labot priekšmetu", "title": "Labot priekšmetu",
"unchanged_if_empty": "Ja neizmainīts atstājiet tukšu", "unchanged_if_empty": "Ja neizmainīts atstājiet tukšu",
"username": "Lietotājvārds", "username": "Lietotājvārds",
"validate_save": "Apstiprināt un saglabāt", "validate_save": "Apstiprināt un saglabāt"
"last_modified": "Pēdējoreiz mainīts"
}, },
"footer": { "footer": {
"cancel": "Atcelt", "cancel": "Atcelt",
@@ -319,21 +314,21 @@
"bcc_destinations": "BCC galamērķi/s", "bcc_destinations": "BCC galamērķi/s",
"bcc_info": "BCC kartes tiek izmantotas, lai klusu pārsūtītu visu ziņojumu kopijas uz citu adresi. Saņēmēja kartes tipa ieraksts tiek izmantots, kad vietējais galamērķis darbojas kā pasta adresāts. Sūtītāja kartes atbilst vienam un tam pašam principam. <br/>\r\n   Vietējais galamērķis netiks informēts par piegādes neveiksmi. ", "bcc_info": "BCC kartes tiek izmantotas, lai klusu pārsūtītu visu ziņojumu kopijas uz citu adresi. Saņēmēja kartes tipa ieraksts tiek izmantots, kad vietējais galamērķis darbojas kā pasta adresāts. Sūtītāja kartes atbilst vienam un tam pašam principam. <br/>\r\n   Vietējais galamērķis netiks informēts par piegādes neveiksmi. ",
"bcc_local_dest": "Vietējais galamērķis", "bcc_local_dest": "Vietējais galamērķis",
"bcc_map_type": "BCC veids", "bcc_map_type": "BCC tips",
"bcc_maps": "BCC kartes", "bcc_maps": "BCC kartes",
"bcc_rcpt_map": "saņēmēja karte", "bcc_rcpt_map": "saņēmēja karte",
"bcc_sender_map": "Sūtītāja karte", "bcc_sender_map": "Sūtītāja karte",
"bcc_to_rcpt": "Pārslēdzieties uz adresāta kartes tipu", "bcc_to_rcpt": "Pārslēdzieties uz adresāta kartes tipu",
"bcc_to_sender": "Pārslēgties uz sūtītāja kartes tipu", "bcc_to_sender": "Pārslēgties uz sūtītāja kartes tipu",
"bcc_type": "BCC tips", "bcc_type": "BCC tips",
"deactivate": "Deaktivēt", "deactivate": "Deaktivizēt",
"description": "Apraksts", "description": "Apraksts",
"dkim_key_length": "DKIM atslēgas garums (bits)", "dkim_key_length": "DKIM atslēgas garums (bits)",
"domain": "Domēns", "domain": "Domēns",
"domain_admins": "Domēna administratori", "domain_admins": "Domēna administratori",
"domain_aliases": "Domēna aliases", "domain_aliases": "Domēna aliases",
"domain_quota": "Kvota", "domain_quota": "Kvota",
"domain_quota_total": "Kopējais domēna ierobežojums", "domain_quota_total": "Kopējā domēna kvota",
"domains": "Domēns", "domains": "Domēns",
"edit": "Labot", "edit": "Labot",
"empty": "Nav rezultātu", "empty": "Nav rezultātu",
@@ -346,7 +341,7 @@
"inactive": "Neaktīvs", "inactive": "Neaktīvs",
"kind": "Veids", "kind": "Veids",
"last_run": "Pēdējā norise", "last_run": "Pēdējā norise",
"last_run_reset": "Ievietot sarakstā kā nākamo", "last_run_reset": "Nākamais grafiks",
"mailbox_quota": "Maks. pastkastes izmērs", "mailbox_quota": "Maks. pastkastes izmērs",
"mailboxes": "Pastkaste", "mailboxes": "Pastkaste",
"max_aliases": "Maks. iespejamās aliases", "max_aliases": "Maks. iespejamās aliases",
@@ -379,13 +374,7 @@
"tls_enforce_out": "Piespiest TLS izejošajiem", "tls_enforce_out": "Piespiest TLS izejošajiem",
"toggle_all": "Pārslēgt visu", "toggle_all": "Pārslēgt visu",
"username": "Lietotājvārds", "username": "Lietotājvārds",
"waiting": "Gaidīšana", "waiting": "Gaidīšana"
"last_modified": "Pēdējoreiz mainīts",
"booking_0_short": "Vienmēŗ bezmaksas",
"daily": "Ik dienu",
"hourly": "Ik stundu",
"last_mail_login": "Pēdējā pieteikšanās pastkastē",
"mailbox": "Pastkaste"
}, },
"quarantine": { "quarantine": {
"action": "Darbības", "action": "Darbības",
@@ -558,14 +547,5 @@
"waiting": "Waiting", "waiting": "Waiting",
"week": "Nedēļa", "week": "Nedēļa",
"weeks": "Nedēļas" "weeks": "Nedēļas"
},
"datatables": {
"paginate": {
"first": "Pirmā",
"last": "Pēdējā"
}
},
"debug": {
"last_modified": "Pēdējoreiz mainīts"
} }
} }

View File

@@ -168,12 +168,10 @@
"empty": "Geen resultaten", "empty": "Geen resultaten",
"excludes": "Exclusief", "excludes": "Exclusief",
"f2b_ban_time": "Verbanningstijd (s)", "f2b_ban_time": "Verbanningstijd (s)",
"f2b_ban_time_increment": "Verbanningstijd wordt verhoogd met elk verbanning",
"f2b_blacklist": "Netwerken/hosts op de blacklist", "f2b_blacklist": "Netwerken/hosts op de blacklist",
"f2b_filter": "Regex-filters", "f2b_filter": "Regex-filters",
"f2b_list_info": "Een host of netwerk op de blacklist staat altijd boven eenzelfde op de whitelist. <b>Het doorvoeren van wijzigingen kan enkele seconden in beslag nemen.</b>", "f2b_list_info": "Een host of netwerk op de blacklist staat altijd boven eenzelfde op de whitelist. <b>Het doorvoeren van wijzigingen kan enkele seconden in beslag nemen.</b>",
"f2b_max_attempts": "Maximaal aantal pogingen", "f2b_max_attempts": "Maximaal aantal pogingen",
"f2b_max_ban_time": "Maximaal verbanningstijd (s)",
"f2b_netban_ipv4": "Voer de IPv4-subnetgrootte in waar de verbanning van kracht moet zijn (8-32)", "f2b_netban_ipv4": "Voer de IPv4-subnetgrootte in waar de verbanning van kracht moet zijn (8-32)",
"f2b_netban_ipv6": "Voer de IPv6-subnetgrootte in waar de verbanning van kracht moet zijn (8-128)", "f2b_netban_ipv6": "Voer de IPv6-subnetgrootte in waar de verbanning van kracht moet zijn (8-128)",
"f2b_parameters": "Fail2ban", "f2b_parameters": "Fail2ban",
@@ -600,7 +598,7 @@
}, },
"login": { "login": {
"delayed": "Aanmelding vertraagd met %s seconden.", "delayed": "Aanmelding vertraagd met %s seconden.",
"fido2_webauthn": "FIDO2/WebAuthn Login", "fido2_webauthn": "FIDO2/WebAuthn",
"login": "Aanmelden", "login": "Aanmelden",
"mobileconfig_info": "Log in als mailboxgebruiker om het Apple-verbindingsprofiel te downloaden.", "mobileconfig_info": "Log in als mailboxgebruiker om het Apple-verbindingsprofiel te downloaden.",
"other_logins": "Meld aan met key", "other_logins": "Meld aan met key",

View File

@@ -1,8 +1,7 @@
{ {
"acl": { "acl": {
"sogo_profile_reset": "Usuń profil SOGo (webmail)", "sogo_profile_reset": "Usuń profil SOGo (webmail)",
"syncjobs": "Polecenie synchronizacji", "syncjobs": "Polecenie synchronizacji"
"alias_domains": "Dodaj aliasy domen"
}, },
"add": { "add": {
"active": "Aktywny", "active": "Aktywny",

View File

@@ -656,7 +656,7 @@
}, },
"login": { "login": {
"delayed": "Conectarea a fost întârziată cu %s secunde.", "delayed": "Conectarea a fost întârziată cu %s secunde.",
"fido2_webauthn": "FIDO2/WebAuthn Login", "fido2_webauthn": "FIDO2/WebAuthn",
"login": "Autentificare", "login": "Autentificare",
"mobileconfig_info": "Autentificați-vă cu adresa de email pentru a descărca profilul de conexiune Apple.", "mobileconfig_info": "Autentificați-vă cu adresa de email pentru a descărca profilul de conexiune Apple.",
"other_logins": "Autentificare cu cheie", "other_logins": "Autentificare cu cheie",

View File

@@ -37,7 +37,7 @@
"add_domain_only": "Только добавить домен", "add_domain_only": "Только добавить домен",
"add_domain_restart": "Добавить домен и перезапустить SOGo", "add_domain_restart": "Добавить домен и перезапустить SOGo",
"alias_address": "Псевдоним/ы", "alias_address": "Псевдоним/ы",
"alias_address_info": "<small>Адрес(а) электронной почты (через запятую) или @example.com (для перехвата всех писем для домена). <b>только домены mailcow</b>.</small>", "alias_address_info": "<small>Укажите почтовые адреса разделенные запятыми или, если хотите пересылать все сообщения для домена владельцам псевдонима то: <code>@example.com</code>. <b>Только домены mailcow разрешены</b>.</small>",
"alias_domain": "Псевдоним домена", "alias_domain": "Псевдоним домена",
"alias_domain_info": "<small>Действительные имена доменов, раздёленные запятыми.</small>", "alias_domain_info": "<small>Действительные имена доменов, раздёленные запятыми.</small>",
"app_name": "Название приложения", "app_name": "Название приложения",
@@ -335,8 +335,7 @@
"username": "Имя пользователя", "username": "Имя пользователя",
"validate_license_now": "Получить лицензию на основе GUID с сервера лицензий", "validate_license_now": "Получить лицензию на основе GUID с сервера лицензий",
"verify": "Проверить", "verify": "Проверить",
"yes": "&#10003;", "yes": "&#10003;"
"queue_unban": "разблокировать"
}, },
"danger": { "danger": {
"access_denied": "Доступ запрещён, или указаны неверные данные", "access_denied": "Доступ запрещён, или указаны неверные данные",
@@ -655,7 +654,7 @@
}, },
"login": { "login": {
"delayed": "Вход был отложен на %s секунд.", "delayed": "Вход был отложен на %s секунд.",
"fido2_webauthn": "FIDO2/WebAuthn Login", "fido2_webauthn": "FIDO2/WebAuthn",
"login": "Войти", "login": "Войти",
"mobileconfig_info": "Пожалуйста, войдите в систему как пользователь почтового аккаунта для загрузки профиля подключения Apple.", "mobileconfig_info": "Пожалуйста, войдите в систему как пользователь почтового аккаунта для загрузки профиля подключения Apple.",
"other_logins": "Вход с помощью ключа", "other_logins": "Вход с помощью ключа",

View File

@@ -106,8 +106,7 @@
"username": "Používateľské meno", "username": "Používateľské meno",
"validate": "Overiť", "validate": "Overiť",
"validation_success": "Úspešne overené", "validation_success": "Úspešne overené",
"app_passwd_protocols": "Povolené protokoly k heslu aplikácie", "app_passwd_protocols": "Povolené protokoly k heslu aplikácie"
"tags": "Štítky"
}, },
"admin": { "admin": {
"access": "Prístup", "access": "Prístup",
@@ -657,7 +656,7 @@
}, },
"login": { "login": {
"delayed": "Prihlásenie bolo oneskorené o %s sekúnd.", "delayed": "Prihlásenie bolo oneskorené o %s sekúnd.",
"fido2_webauthn": "FIDO2/WebAuthn Login", "fido2_webauthn": "FIDO2/WebAuthn",
"login": "Prihlásenie", "login": "Prihlásenie",
"mobileconfig_info": "Prosím, prihláste sa ako mailový používateľ pre stiahnutie požadovaného Apple profilu.", "mobileconfig_info": "Prosím, prihláste sa ako mailový používateľ pre stiahnutie požadovaného Apple profilu.",
"other_logins": "Prihlásenie kľúčom", "other_logins": "Prihlásenie kľúčom",

View File

@@ -618,7 +618,7 @@
}, },
"login": { "login": {
"delayed": "Av säkerhetsskäl har inloggning inaktiverats i %s sekunder.", "delayed": "Av säkerhetsskäl har inloggning inaktiverats i %s sekunder.",
"fido2_webauthn": "FIDO2/WebAuthn Login", "fido2_webauthn": "FIDO2/WebAuthn",
"login": "Logga in", "login": "Logga in",
"mobileconfig_info": "Logga in som en användare av brevlåda för att ladda ner den begärda Apple-anslutningsprofilen.", "mobileconfig_info": "Logga in som en användare av brevlåda för att ladda ner den begärda Apple-anslutningsprofilen.",
"other_logins": "Loggain med nyckel", "other_logins": "Loggain med nyckel",

View File

@@ -656,7 +656,7 @@
"awaiting_tfa_confirmation": "В очікуванні підтвердження TFA" "awaiting_tfa_confirmation": "В очікуванні підтвердження TFA"
}, },
"login": { "login": {
"fido2_webauthn": "FIDO2/WebAuthn Login", "fido2_webauthn": "FIDO2/WebAuthn",
"login": "Увійти", "login": "Увійти",
"other_logins": "Вхід за допомогою ключа", "other_logins": "Вхід за допомогою ключа",
"password": "Пароль", "password": "Пароль",

View File

@@ -661,7 +661,7 @@
}, },
"login": { "login": {
"delayed": "请在 %s 秒后重新登录。", "delayed": "请在 %s 秒后重新登录。",
"fido2_webauthn": "使用 FIDO2/WebAuthn Login 登录", "fido2_webauthn": "使用 FIDO2/WebAuthn 登录",
"login": "登录", "login": "登录",
"mobileconfig_info": "请使用邮箱用户登录以下载 Apple 连接描述文件。", "mobileconfig_info": "请使用邮箱用户登录以下载 Apple 连接描述文件。",
"other_logins": "Key 登录", "other_logins": "Key 登录",

View File

@@ -655,7 +655,7 @@
}, },
"login": { "login": {
"delayed": "請在 %s 秒後重新登入。", "delayed": "請在 %s 秒後重新登入。",
"fido2_webauthn": "FIDO2/WebAuthn Login", "fido2_webauthn": "FIDO2/WebAuthn",
"login": "登入", "login": "登入",
"mobileconfig_info": "請使用信箱使用者登入以下載 Apple 連接描述檔案。", "mobileconfig_info": "請使用信箱使用者登入以下載 Apple 連接描述檔案。",
"other_logins": "金鑰登入", "other_logins": "金鑰登入",

View File

@@ -57,7 +57,7 @@
</div> </div>
</div> <!-- /col-md-12 --> </div> <!-- /col-md-12 -->
</div> <!-- /row --> </div> <!-- /row -->
</div> </div>
{% include 'modals/admin.twig' %} {% include 'modals/admin.twig' %}
@@ -66,7 +66,7 @@ var lang = {{ lang_admin|raw }};
var lang_datatables = {{ lang_datatables|raw }}; var lang_datatables = {{ lang_datatables|raw }};
var admin_username = '{{ mailcow_cc_username }}'; var admin_username = '{{ mailcow_cc_username }}';
var csrf_token = '{{ csrf_token }}'; var csrf_token = '{{ csrf_token }}';
var pagination_size = Math.trunc('{{ pagination_size }}'); var pagination_size = '{{ pagination_size }}';
var log_pagination_size = Math.trunc('{{ log_pagination_size }}'); var log_pagination_size = '{{ log_pagination_size }}';
</script> </script>
{% endblock %} {% endblock %}

View File

@@ -12,14 +12,6 @@
<label for="f2b_ban_time">{{ lang.admin.f2b_ban_time }}:</label> <label for="f2b_ban_time">{{ lang.admin.f2b_ban_time }}:</label>
<input type="number" class="form-control" id="f2b_ban_time" name="ban_time" value="{{ f2b_data.ban_time }}" required> <input type="number" class="form-control" id="f2b_ban_time" name="ban_time" value="{{ f2b_data.ban_time }}" required>
</div> </div>
<div class="mb-4">
<label for="f2b_max_ban_time">{{ lang.admin.f2b_max_ban_time }}:</label>
<input type="number" class="form-control" id="f2b_max_ban_time" name="max_ban_time" value="{{ f2b_data.max_ban_time }}" required>
</div>
<div class="mb-4">
<input class="form-check-input" type="checkbox" value="1" name="ban_time_increment" id="f2b_ban_time_increment" {% if f2b_data.ban_time_increment == 1 %}checked{% endif %}>
<label class="form-check-label" for="f2b_ban_time_increment">{{ lang.admin.f2b_ban_time_increment }}</label>
</div>
<div class="mb-4"> <div class="mb-4">
<label for="f2b_max_attempts">{{ lang.admin.f2b_max_attempts }}:</label> <label for="f2b_max_attempts">{{ lang.admin.f2b_max_attempts }}:</label>
<input type="number" class="form-control" id="f2b_max_attempts" name="max_attempts" value="{{ f2b_data.max_attempts }}" required> <input type="number" class="form-control" id="f2b_max_attempts" name="max_attempts" value="{{ f2b_data.max_attempts }}" required>

File diff suppressed because one or more lines are too long

View File

@@ -46,7 +46,7 @@
<div class="col-sm-3 col-5 text-end">{{ lang.fido2.known_ids }}:</div> <div class="col-sm-3 col-5 text-end">{{ lang.fido2.known_ids }}:</div>
<div class="col-sm-9 col-7"> <div class="col-sm-9 col-7">
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-striped table-hover table-condensed w-100" id="fido2_keys"> <table class="table table-striped table-hover table-condensed" id="fido2_keys">
<tr> <tr>
<th>ID</th> <th>ID</th>
<th style="min-width:240px;text-align: right">{{ lang.admin.action }}</th> <th style="min-width:240px;text-align: right">{{ lang.admin.action }}</th>

View File

@@ -26,7 +26,7 @@
var lang_user = {{ lang_user|raw }}; var lang_user = {{ lang_user|raw }};
var lang_datatables = {{ lang_datatables|raw }}; var lang_datatables = {{ lang_datatables|raw }};
var csrf_token = '{{ csrf_token }}'; var csrf_token = '{{ csrf_token }}';
var pagination_size = Math.trunc('{{ pagination_size }}'); var pagination_size = '{{ pagination_size }}';
var table_for_domain = '{{ domain }}'; var table_for_domain = '{{ domain }}';
</script> </script>
{% endblock %} {% endblock %}

View File

@@ -38,8 +38,15 @@
</div> </div>
</div> </div>
<div class="d-flex mt-4" style="position: relative"> <div class="d-flex mt-4" style="position: relative">
<button type="submit" class="btn btn-xs-lg btn-success" value="Login">{{ lang.login.login }}</button> <div class="btn-group">
<button type="button" class="btn btn-xs-lg btn-success ms-2" id="fido2-login"><i class="bi bi-shield-fill-check"></i> {{ lang.login.fido2_webauthn }}</button> <div class="btn-group">
<button type="submit" class="btn btn-xs-lg btn-success" value="Login">{{ lang.login.login }}</button>
<button type="button" class="btn btn-xs-lg btn-success dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false"></button>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="#" id="fido2-login"><i class="bi bi-shield-fill-check"></i> {{ lang.login.fido2_webauthn }}</a></li>
</ul>
</div>
</div>
{% if not oauth2_request %} {% if not oauth2_request %}
<button type="button" {% if available_languages|length == 1 %}disabled="true"{% endif %} class="btn btn-xs-lg btn-secondary ms-auto dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> <button type="button" {% if available_languages|length == 1 %}disabled="true"{% endif %} class="btn btn-xs-lg btn-secondary ms-auto dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="flag-icon flag-icon-{{ mailcow_locale[-2:] }}"></span> <span class="flag-icon flag-icon-{{ mailcow_locale[-2:] }}"></span>

View File

@@ -19,7 +19,7 @@
</li> </li>
<li class="nav-item" role="presentation"><button class="nav-link" aria-controls="tab-resources" role="tab" data-bs-toggle="tab" data-bs-target="#tab-resources">{{ lang.mailbox.resources }}</button></li> <li class="nav-item" role="presentation"><button class="nav-link" aria-controls="tab-resources" role="tab" data-bs-toggle="tab" data-bs-target="#tab-resources">{{ lang.mailbox.resources }}</button></li>
<li class="nav-item dropdown"> <li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" data-bs-toggle="dropdown" href="#">{{ lang.mailbox.aliases }}</a> <a class="nav-link dropdown-toggle" data-bs-toggle="dropdown" data-bs-target="#">{{ lang.mailbox.aliases }}</a>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
<li role="presentation"><button class="dropdown-item" aria-selected="false" aria-controls="tab-mbox-aliases" role="tab" data-bs-toggle="tab" data-bs-target="#tab-mbox-aliases">{{ lang.mailbox.aliases }}</button></li> <li role="presentation"><button class="dropdown-item" aria-selected="false" aria-controls="tab-mbox-aliases" role="tab" data-bs-toggle="tab" data-bs-target="#tab-mbox-aliases">{{ lang.mailbox.aliases }}</button></li>
<li role="presentation"><button class="dropdown-item" aria-selected="false" aria-controls="tab-domain-aliases" role="tab" data-bs-toggle="tab" data-bs-target="#tab-domain-aliases">{{ lang.mailbox.domain_aliases }}</button></li> <li role="presentation"><button class="dropdown-item" aria-selected="false" aria-controls="tab-domain-aliases" role="tab" data-bs-toggle="tab" data-bs-target="#tab-domain-aliases">{{ lang.mailbox.domain_aliases }}</button></li>
@@ -58,7 +58,7 @@
var lang_rl = {{ lang_rl|raw }}; var lang_rl = {{ lang_rl|raw }};
var lang_datatables = {{ lang_datatables|raw }}; var lang_datatables = {{ lang_datatables|raw }};
var csrf_token = '{{ csrf_token }}'; var csrf_token = '{{ csrf_token }}';
var pagination_size = Math.trunc('{{ pagination_size }}'); var pagination_size = '{{ pagination_size }}';
var role = '{{ role }}'; var role = '{{ role }}';
var is_dual = {{ is_dual }}; var is_dual = {{ is_dual }};
var ALLOW_ADMIN_EMAIL_LOGIN = {{ allow_admin_email_login }}; var ALLOW_ADMIN_EMAIL_LOGIN = {{ allow_admin_email_login }};

View File

@@ -37,7 +37,7 @@
</p> </p>
{% endif %} {% endif %}
</p> </p>
<table id="quarantinetable" class="table table-striped w-100"></table> <table id="quarantinetable" class="table table-striped"></table>
<div class="mass-actions-quarantine mt-4"> <div class="mass-actions-quarantine mt-4">
<div class="btn-group" data-acl="{{ acl.quarantine }}"> <div class="btn-group" data-acl="{{ acl.quarantine }}">
<a class="btn btn-sm btn-xs-half d-block d-sm-inline btn-secondary" id="toggle_multi_select_all" data-id="qitems" href="#"><i class="bi bi-check-all"></i> {{ lang.quarantine.toggle_all }}</a> <a class="btn btn-sm btn-xs-half d-block d-sm-inline btn-secondary" id="toggle_multi_select_all" data-id="qitems" href="#"><i class="bi bi-check-all"></i> {{ lang.quarantine.toggle_all }}</a>
@@ -66,7 +66,7 @@ var acl = '{{ acl_json|raw }}';
var lang = {{ lang_quarantine|raw }}; var lang = {{ lang_quarantine|raw }};
var lang_datatables = {{ lang_datatables|raw }}; var lang_datatables = {{ lang_datatables|raw }};
var csrf_token = '{{ csrf_token }}'; var csrf_token = '{{ csrf_token }}';
var pagination_size = Math.trunc('{{ pagination_size }}'); var pagination_size = '{{ pagination_size }}';
var role = '{{ role }}'; var role = '{{ role }}';
</script> </script>
{% endblock %} {% endblock %}

View File

@@ -55,7 +55,7 @@
var lang = {{ lang_queue|raw }}; var lang = {{ lang_queue|raw }};
var lang_datatables = {{ lang_datatables|raw }}; var lang_datatables = {{ lang_datatables|raw }};
var csrf_token = '{{ csrf_token }}'; var csrf_token = '{{ csrf_token }}';
var pagination_size = Math.trunc('{{ pagination_size }}'); var pagination_size = '{{ pagination_size }}';
var table_for_domain = '{{ domain }}'; var table_for_domain = '{{ domain }}';
</script> </script>
{% endblock %} {% endblock %}

View File

@@ -4,7 +4,11 @@
<button class="btn d-md-none flex-grow-1 text-start" data-bs-target="#collapse-tab-Syncjobs" data-bs-toggle="collapse" aria-controls="collapse-tab-Syncjobs"> <button class="btn d-md-none flex-grow-1 text-start" data-bs-target="#collapse-tab-Syncjobs" data-bs-toggle="collapse" aria-controls="collapse-tab-Syncjobs">
{{ lang.user.sync_jobs }} {{ lang.user.sync_jobs }}
</button> </button>
<span class="d-none d-md-block">{{ lang.user.sync_jobs }} <span class="d-none d-md-block">{{ lang.user.sync_jobs }} <span class="badge bg-info table-lines"></span></span>
<div class="btn-group ms-auto d-flex">
<button class="btn btn-xs btn-secondary refresh_table" data-draw="draw_sync_job_table" data-table="sync_job_table">{{ lang.admin.refresh }}</button>
</div>
</div> </div>
<div id="collapse-tab-Syncjobs" class="card-body collapse" data-bs-parent="#user-content"> <div id="collapse-tab-Syncjobs" class="card-body collapse" data-bs-parent="#user-content">
<div class="mass-actions-user mb-4"> <div class="mass-actions-user mb-4">

View File

@@ -4,7 +4,7 @@
var acl = '{{ acl_json|raw }}'; var acl = '{{ acl_json|raw }}';
var lang = {{ lang_user|raw }}; var lang = {{ lang_user|raw }};
var csrf_token = '{{ csrf_token }}'; var csrf_token = '{{ csrf_token }}';
var pagination_size = Math.trunc('{{ pagination_size }}'); var pagination_size = '{{ pagination_size }}';
var mailcow_cc_username = '{{ mailcow_cc_username }}'; var mailcow_cc_username = '{{ mailcow_cc_username }}';
var user_spam_score = [{{ user_spam_score }}]; var user_spam_score = [{{ user_spam_score }}];
var lang_datatables = {{ lang_datatables|raw }}; var lang_datatables = {{ lang_datatables|raw }};

View File

@@ -20,7 +20,6 @@ if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'doma
'tfa_data' => $tfa_data, 'tfa_data' => $tfa_data,
'fido2_data' => $fido2_data, 'fido2_data' => $fido2_data,
'lang_user' => json_encode($lang['user']), 'lang_user' => json_encode($lang['user']),
'lang_datatables' => json_encode($lang['datatables']),
]; ];
} }
elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user') { elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user') {

View File

@@ -58,7 +58,7 @@ services:
- redis - redis
clamd-mailcow: clamd-mailcow:
image: mailcow/clamd:1.61 image: mailcow/clamd:1.60
restart: always restart: always
depends_on: depends_on:
- unbound-mailcow - unbound-mailcow
@@ -76,7 +76,7 @@ services:
- clamd - clamd
rspamd-mailcow: rspamd-mailcow:
image: mailcow/rspamd:1.93 image: mailcow/rspamd:1.92
stop_grace_period: 30s stop_grace_period: 30s
depends_on: depends_on:
- dovecot-mailcow - dovecot-mailcow
@@ -106,7 +106,7 @@ services:
- rspamd - rspamd
php-fpm-mailcow: php-fpm-mailcow:
image: mailcow/phpfpm:1.83 image: mailcow/phpfpm:1.82
command: "php-fpm -d date.timezone=${TZ} -d expose_php=0" command: "php-fpm -d date.timezone=${TZ} -d expose_php=0"
depends_on: depends_on:
- redis-mailcow - redis-mailcow
@@ -169,7 +169,7 @@ services:
- phpfpm - phpfpm
sogo-mailcow: sogo-mailcow:
image: mailcow/sogo:1.116 image: mailcow/sogo:1.114
environment: environment:
- DBNAME=${DBNAME} - DBNAME=${DBNAME}
- DBUSER=${DBUSER} - DBUSER=${DBUSER}
@@ -191,7 +191,7 @@ services:
volumes: volumes:
- ./data/hooks/sogo:/hooks:Z - ./data/hooks/sogo:/hooks:Z
- ./data/conf/sogo/:/etc/sogo/:z - ./data/conf/sogo/:/etc/sogo/:z
- ./data/web/inc/init_db.inc.php:/init_db.inc.php:z - ./data/web/inc/init_db.inc.php:/init_db.inc.php:Z
- ./data/conf/sogo/custom-favicon.ico:/usr/lib/GNUstep/SOGo/WebServerResources/img/sogo.ico:z - ./data/conf/sogo/custom-favicon.ico:/usr/lib/GNUstep/SOGo/WebServerResources/img/sogo.ico:z
- ./data/conf/sogo/custom-theme.js:/usr/lib/GNUstep/SOGo/WebServerResources/js/theme.js:z - ./data/conf/sogo/custom-theme.js:/usr/lib/GNUstep/SOGo/WebServerResources/js/theme.js:z
- ./data/conf/sogo/custom-sogo.js:/usr/lib/GNUstep/SOGo/WebServerResources/js/custom-sogo.js:z - ./data/conf/sogo/custom-sogo.js:/usr/lib/GNUstep/SOGo/WebServerResources/js/custom-sogo.js:z
@@ -216,7 +216,7 @@ services:
- sogo - sogo
dovecot-mailcow: dovecot-mailcow:
image: mailcow/dovecot:1.23 image: mailcow/dovecot:1.22
depends_on: depends_on:
- mysql-mailcow - mysql-mailcow
dns: dns:
@@ -389,7 +389,7 @@ services:
acme-mailcow: acme-mailcow:
depends_on: depends_on:
- nginx-mailcow - nginx-mailcow
image: mailcow/acme:1.84 image: mailcow/acme:1.83
dns: dns:
- ${IPV4_NETWORK:-172.22.1}.254 - ${IPV4_NETWORK:-172.22.1}.254
environment: environment:
@@ -425,7 +425,7 @@ services:
- acme - acme
netfilter-mailcow: netfilter-mailcow:
image: mailcow/netfilter:1.52 image: mailcow/netfilter:1.50
stop_grace_period: 30s stop_grace_period: 30s
depends_on: depends_on:
- dovecot-mailcow - dovecot-mailcow
@@ -510,7 +510,7 @@ services:
- watchdog - watchdog
dockerapi-mailcow: dockerapi-mailcow:
image: mailcow/dockerapi:2.02 image: mailcow/dockerapi:2.0
security_opt: security_opt:
- label=disable - label=disable
restart: always restart: always

View File

@@ -205,8 +205,8 @@ DBUSER=mailcow
# Please use long, random alphanumeric strings (A-Za-z0-9) # Please use long, random alphanumeric strings (A-Za-z0-9)
DBPASS=$(LC_ALL=C </dev/urandom tr -dc A-Za-z0-9 2> /dev/null | head -c 28) DBPASS=$(LC_ALL=C </dev/urandom tr -dc A-Za-z0-9 | head -c 28)
DBROOT=$(LC_ALL=C </dev/urandom tr -dc A-Za-z0-9 2> /dev/null | head -c 28) DBROOT=$(LC_ALL=C </dev/urandom tr -dc A-Za-z0-9 | head -c 28)
# ------------------------------ # ------------------------------
# HTTP/S Bindings # HTTP/S Bindings

View File

@@ -26,6 +26,6 @@ services:
- /var/run/mysqld/mysqld.sock:/var/run/mysqld/mysqld.sock - /var/run/mysqld/mysqld.sock:/var/run/mysqld/mysqld.sock
mysql-mailcow: mysql-mailcow:
image: alpine:3.17 image: alpine:3.10
command: /bin/true command: /bin/true
restart: "no" restart: "no"

15
helper-scripts/expiry-dates.sh Executable file → Normal file
View File

@@ -3,11 +3,10 @@
[[ -f mailcow.conf ]] && source mailcow.conf [[ -f mailcow.conf ]] && source mailcow.conf
[[ -f ../mailcow.conf ]] && source ../mailcow.conf [[ -f ../mailcow.conf ]] && source ../mailcow.conf
POSTFIX=$(echo | openssl s_client -connect ${MAILCOW_HOSTNAME}:${SMTP_PORT} -starttls smtp 2>/dev/null | openssl x509 -inform pem -noout -enddate | cut -d "=" -f 2) POSTFIX=$(echo | openssl s_client -connect ${MAILCOW_HOSTNAME}:25 -starttls smtp 2>/dev/null | openssl x509 -inform pem -noout -enddate | cut -d "=" -f 2)
DOVECOT=$(echo | openssl s_client -connect ${MAILCOW_HOSTNAME}:${IMAP_PORT} -starttls imap 2>/dev/null | openssl x509 -inform pem -noout -enddate | cut -d "=" -f 2) DOVECOT=$(echo | openssl s_client -connect ${MAILCOW_HOSTNAME}:143 -starttls imap 2>/dev/null | openssl x509 -inform pem -noout -enddate | cut -d "=" -f 2)
NGINX=$(echo | openssl s_client -connect ${MAILCOW_HOSTNAME}:${HTTPS_PORT} 2>/dev/null | openssl x509 -inform pem -noout -enddate | cut -d "=" -f 2) NGINX=$(echo | openssl s_client -connect ${MAILCOW_HOSTNAME}:443 2>/dev/null | openssl x509 -inform pem -noout -enddate | cut -d "=" -f 2)
echo TLS expiry dates:
echo "TLS expiry dates:" echo Postfix: ${POSTFIX}
echo "Postfix: ${POSTFIX}" echo Dovecot: ${DOVECOT}
echo "Dovecot: ${DOVECOT}" echo Nginx: ${NGINX}
echo "Nginx: ${NGINX}"

View File

@@ -19,7 +19,7 @@ read -r -p "Are you sure you want to reset the mailcow administrator account? [y
response=${response,,} # tolower response=${response,,} # tolower
if [[ "$response" =~ ^(yes|y)$ ]]; then if [[ "$response" =~ ^(yes|y)$ ]]; then
echo -e "\nWorking, please wait..." echo -e "\nWorking, please wait..."
random=$(</dev/urandom tr -dc _A-Z-a-z-0-9 2> /dev/null | head -c${1:-16}) random=$(</dev/urandom tr -dc _A-Z-a-z-0-9 | head -c${1:-16})
password=$(docker exec -it $(docker ps -qf name=dovecot-mailcow) doveadm pw -s SSHA256 -p ${random} | tr -d '\r') password=$(docker exec -it $(docker ps -qf name=dovecot-mailcow) doveadm pw -s SSHA256 -p ${random} | tr -d '\r')
docker exec -it $(docker ps -qf name=mysql-mailcow) mysql -u${DBUSER} -p${DBPASS} ${DBNAME} -e "DELETE FROM admin WHERE username='admin';" docker exec -it $(docker ps -qf name=mysql-mailcow) mysql -u${DBUSER} -p${DBPASS} ${DBNAME} -e "DELETE FROM admin WHERE username='admin';"
docker exec -it $(docker ps -qf name=mysql-mailcow) mysql -u${DBUSER} -p${DBPASS} ${DBNAME} -e "DELETE FROM domain_admins WHERE username='admin';" docker exec -it $(docker ps -qf name=mysql-mailcow) mysql -u${DBUSER} -p${DBPASS} ${DBNAME} -e "DELETE FROM domain_admins WHERE username='admin';"

View File

@@ -1,13 +1,10 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# renovate: datasource=github-releases depName=nextcloud/server versioning=semver extractVersion=^v(?<version>.*)$ # renovate: datasource=github-releases depName=nextcloud/server versioning=semver extractVersion=^v(?<version>.*)$
NEXTCLOUD_VERSION=26.0.0 NEXTCLOUD_VERSION=25.0.2
echo -ne "Checking prerequisites..." for bin in curl dirmngr; do
sleep 1 if [[ -z $(which ${bin}) ]]; then echo "Cannot find ${bin}, exiting..."; exit 1; fi
for bin in curl dirmngr tar bzip2; do
if [[ -z $(which ${bin}) ]]; then echo -ne "\r\033[31mCannot find ${bin}, exiting...\033[0m\n"; exit 1; fi
done done
echo -ne "\r\033[32mFound all prerequisites! Continuing...\033[0m\n"
[[ -z ${1} ]] && NC_HELP=y [[ -z ${1} ]] && NC_HELP=y
@@ -49,22 +46,22 @@ if [[ ${NC_PURGE} == "y" ]]; then
echo -e "\033[33mDetecting Database information...\033[0m" echo -e "\033[33mDetecting Database information...\033[0m"
if [[ $(docker exec -it $(docker ps -f name=mysql-mailcow -q) mysql -uroot -p${DBROOT} -e "Show databases" | grep "nextcloud") ]]; then if [[ $(docker exec -it $(docker ps -f name=mysql-mailcow -q) mysql -uroot -p${DBROOT} -e "Show databases" | grep "nextcloud") ]]; then
echo -e "\033[32mFound seperate Nextcloud database (newer scheme)!\033[0m" echo -e "\033[32mFound seperate nextcloud Database (newer scheme)!\033[0m"
echo -e "\033[31mPurging...\033[0m" echo -e "\033[31mPurging...\033[0m"
docker exec -it $(docker ps -f name=mysql-mailcow -q) mysql -uroot -p${DBROOT} -e "DROP DATABASE nextcloud;" > /dev/null docker exec -it $(docker ps -f name=mysql-mailcow -q) mysql -uroot -p${DBROOT} -e "DROP DATABASE nextcloud;" > /dev/null
docker exec -it $(docker ps -f name=mysql-mailcow -q) mysql -uroot -p${DBROOT} -e "DROP USER 'nextcloud'@'%';" > /dev/null docker exec -it $(docker ps -f name=mysql-mailcow -q) mysql -uroot -p${DBROOT} -e "DROP USER 'nextcloud'@'%';" > /dev/null
elif [[ $(docker exec -it $(docker ps -f name=mysql-mailcow -q) mysql -uroot -p${DBROOT} mailcow -e "SHOW TABLES LIKE 'oc_%'") && $? -eq 0 ]]; then elif [[ $(docker exec -it $(docker ps -f name=mysql-mailcow -q) mysql -uroot -p${DBROOT} mailcow -e "SHOW TABLES LIKE 'oc_%'") && $? -eq 0 ]]; then
echo -e "\033[32mFound Nextcloud (oc) tables inside of mailcow database (old scheme)!\033[0m" echo -e "\033[32mFound nextcloud (oc) tables inside of mailcow Database (old scheme)!\033[0m"
echo -e "\033[31mPurging...\033[0m" echo -e "\033[31mPurging...\033[0m"
docker exec -it $(docker ps -f name=mysql-mailcow -q) mysql -uroot -p${DBROOT} -e \ docker exec -it $(docker ps -f name=mysql-mailcow -q) mysql -uroot -p${DBROOT} -e \
"$(docker exec -it $(docker ps -f name=mysql-mailcow -q) mysql -uroot -p${DBROOT} -e "SELECT IFNULL(GROUP_CONCAT('DROP TABLE ', TABLE_SCHEMA, '.', TABLE_NAME SEPARATOR ';'),'SELECT NULL;') FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME LIKE 'oc_%' AND TABLE_SCHEMA = '${DBNAME}';" -BN)" > /dev/null "$(docker exec -it $(docker ps -f name=mysql-mailcow -q) mysql -uroot -p${DBROOT} -e "SELECT IFNULL(GROUP_CONCAT('DROP TABLE ', TABLE_SCHEMA, '.', TABLE_NAME SEPARATOR ';'),'SELECT NULL;') FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME LIKE 'oc_%' AND TABLE_SCHEMA = '${DBNAME}';" -BN)" > /dev/null
elif [[ $(docker exec -it $(docker ps -f name=mysql-mailcow -q) mysql -uroot -p${DBROOT} mailcow -e "SHOW TABLES LIKE 'nc_%'") && $? -eq 0 ]]; then elif [[ $(docker exec -it $(docker ps -f name=mysql-mailcow -q) mysql -uroot -p${DBROOT} mailcow -e "SHOW TABLES LIKE 'nc_%'") && $? -eq 0 ]]; then
echo -e "\033[32mFound Nextcloud (nc) tables inside of mailcow database (old scheme)!\033[0m" echo -e "\033[32mFound nextcloud (nc) tables inside of mailcow Database (old scheme)!\033[0m"
echo -e "\033[31mPurging...\033[0m" echo -e "\033[31mPurging...\033[0m"
docker exec -it $(docker ps -f name=mysql-mailcow -q) mysql -uroot -p${DBROOT} -e \ docker exec -it $(docker ps -f name=mysql-mailcow -q) mysql -uroot -p${DBROOT} -e \
"$(docker exec -it $(docker ps -f name=mysql-mailcow -q) mysql -uroot -p${DBROOT} -e "SELECT IFNULL(GROUP_CONCAT('DROP TABLE ', TABLE_SCHEMA, '.', TABLE_NAME SEPARATOR ';'),'SELECT NULL;') FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME LIKE 'nc_%' AND TABLE_SCHEMA = '${DBNAME}';" -BN)" > /dev/null "$(docker exec -it $(docker ps -f name=mysql-mailcow -q) mysql -uroot -p${DBROOT} -e "SELECT IFNULL(GROUP_CONCAT('DROP TABLE ', TABLE_SCHEMA, '.', TABLE_NAME SEPARATOR ';'),'SELECT NULL;') FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME LIKE 'nc_%' AND TABLE_SCHEMA = '${DBNAME}';" -BN)" > /dev/null
else else
echo -e "\033[31mError: No Nextcloud databases/tables found!" echo -e "\033[31mError: No Nextcloud Databases/Tables found!"
echo -e "\033[33mNot purging anything...\033[0m" echo -e "\033[33mNot purging anything...\033[0m"
exit 1 exit 1
fi fi
@@ -83,10 +80,10 @@ EOF
docker restart $(docker ps -aqf name=nginx-mailcow) docker restart $(docker ps -aqf name=nginx-mailcow)
echo -e "\033[32mNextcloud has been uninstalled sucessfully!\033[0m" echo -e "\033[32mNextcloud has been sucessfully uninstalled!\033[0m"
elif [[ ${NC_UPDATE} == "y" ]]; then elif [[ ${NC_UPDATE} == "y" ]]; then
read -r -p "Are you sure you want to update Nextcloud (with Nextclouds own updater)? [y/N] " response read -r -p "Are you sure you want to update Nextcloud (with nextclouds own updater)? [y/N] " response
response=${response,,} response=${response,,}
if [[ ! "$response" =~ ^(yes|y)$ ]]; then if [[ ! "$response" =~ ^(yes|y)$ ]]; then
echo "OK, aborting." echo "OK, aborting."
@@ -97,12 +94,8 @@ elif [[ ${NC_UPDATE} == "y" ]]; then
echo -e "\033[31mError: Nextcloud occ not found. Is Nextcloud installed?\033[0m" echo -e "\033[31mError: Nextcloud occ not found. Is Nextcloud installed?\033[0m"
exit 1 exit 1
fi fi
if grep -q 'This version of Nextcloud is not compatible with PHP>=8.2.' <<<$(docker exec -it -u www-data $(docker ps -f name=php-fpm-mailcow -q) bash -c "/web/nextcloud/occ --no-warnings status"); then if ! grep -q 'installed: true' <<<$(docker exec -it -u www-data $(docker ps -f name=php-fpm-mailcow -q) bash -c "/web/nextcloud/occ --no-warnings status"); then
echo -e "\033[31mError: This version of Nextcloud is not compatible with PHP>=8.2, we'll fix it\033[0m" echo "Nextcloud seems not to be installed."
wget -q https://raw.githubusercontent.com/nextcloud/server/v26.0.0/lib/versioncheck.php -O ./data/web/nextcloud/lib/versioncheck.php
echo -e "\e[33mPlease restart the update again.\e[0m"
elif ! grep -q 'installed: true' <<<$(docker exec -it -u www-data $(docker ps -f name=php-fpm-mailcow -q) bash -c "/web/nextcloud/occ --no-warnings status"); then
echo -e "\033[31mError: Nextcloud seems not to be installed.\033[0m"
exit 1 exit 1
else else
docker exec -it -u www-data $(docker ps -f name=php-fpm-mailcow -q) bash -c "php /web/nextcloud/updater/updater.phar" docker exec -it -u www-data $(docker ps -f name=php-fpm-mailcow -q) bash -c "php /web/nextcloud/updater/updater.phar"
@@ -125,29 +118,29 @@ elif [[ ${NC_INSTALL} == "y" ]]; then
&& mkdir -p ./data/web/nextcloud/data \ && mkdir -p ./data/web/nextcloud/data \
&& chmod +x ./data/web/nextcloud/occ && chmod +x ./data/web/nextcloud/occ
echo -e "\033[33mCreating 'nextcloud' database...\033[0m" echo -e "\033[33mCreating Nextcloud Database...\033[0m"
NC_DBPASS=$(</dev/urandom tr -dc A-Za-z0-9 2> /dev/null | head -c 28) NC_DBPASS=$(</dev/urandom tr -dc A-Za-z0-9 | head -c 28)
NC_DBUSER=nextcloud NC_DBUSER=nextcloud
NC_DBNAME=nextcloud NC_DBNAME=nextcloud
echo -ne "[1/3] Creating 'nextcloud' database" echo -ne "[1/3] Creating nextcloud Database"
docker exec -it $(docker ps -f name=mysql-mailcow -q) mysql -uroot -p${DBROOT} -e "CREATE DATABASE ${NC_DBNAME};" docker exec -it $(docker ps -f name=mysql-mailcow -q) mysql -uroot -p${DBROOT} -e "CREATE DATABASE ${NC_DBNAME};"
sleep 2 sleep 2
echo -ne "\r[2/3] Creating 'nextcloud' database user" echo -ne "\r[2/3] Creating nextcloud Database user"
docker exec -it $(docker ps -f name=mysql-mailcow -q) mysql -uroot -p${DBROOT} -e "CREATE USER '${NC_DBUSER}'@'%' IDENTIFIED BY '${NC_DBPASS}';" docker exec -it $(docker ps -f name=mysql-mailcow -q) mysql -uroot -p${DBROOT} -e "CREATE USER '${NC_DBUSER}'@'%' IDENTIFIED BY '${NC_DBPASS}';"
sleep 2 sleep 2
echo -ne "\r[3/3] Granting 'nextcloud' user all permissions on database 'nextcloud'" echo -ne "\r[3/3] Granting nextcloud user all permissions on database nextcloud"
docker exec -it $(docker ps -f name=mysql-mailcow -q) mysql -uroot -p${DBROOT} -e "GRANT ALL PRIVILEGES ON ${NC_DBNAME}.* TO '${NC_DBUSER}'@'%';" docker exec -it $(docker ps -f name=mysql-mailcow -q) mysql -uroot -p${DBROOT} -e "GRANT ALL PRIVILEGES ON ${NC_DBNAME}.* TO '${NC_DBUSER}'@'%';"
sleep 2 sleep 2
echo "" echo ""
echo -e "\033[33mInstalling Nextcloud...\033[0m" echo -e "\033[33mInstalling Nextcloud...\033[0m"
ADMIN_NC_PASS=$(</dev/urandom tr -dc A-Za-z0-9 2> /dev/null | head -c 28) ADMIN_NC_PASS=$(</dev/urandom tr -dc A-Za-z0-9 | head -c 28)
echo -ne "[1/4] Setting correct permissions for www-data" echo -ne "[1/4] Setting correct permissions for www-data"
docker exec -it $(docker ps -f name=php-fpm-mailcow -q) /bin/bash -c "chown -R www-data:www-data /web/nextcloud" docker exec -it $(docker ps -f name=php-fpm-mailcow -q) /bin/bash -c "chown -R www-data:www-data /web/nextcloud"
sleep 2 sleep 2
echo -ne "\r[2/4] Running occ maintenance:install to install Nextcloud" echo -ne "\r[2/4] Running occ maintenance:install to install nextcloud"
docker exec -it -u www-data $(docker ps -f name=php-fpm-mailcow -q) /web/nextcloud/occ --no-warnings maintenance:install \ docker exec -it -u www-data $(docker ps -f name=php-fpm-mailcow -q) /web/nextcloud/occ --no-warnings maintenance:install \
--database mysql \ --database mysql \
--database-host mysql \ --database-host mysql \
@@ -156,9 +149,9 @@ elif [[ ${NC_INSTALL} == "y" ]]; then
--database-pass ${NC_DBPASS} \ --database-pass ${NC_DBPASS} \
--admin-user admin \ --admin-user admin \
--admin-pass ${ADMIN_NC_PASS} \ --admin-pass ${ADMIN_NC_PASS} \
--data-dir /web/nextcloud/data > /dev/null 2>&1 --data-dir /web/nextcloud/data 2>&1 /dev/null
echo -ne "\r[3/4] Setting custom parameters inside the Nextcloud config file" echo -ne "\r[3/4] Setting custom parameters inside the nextcloud config file"
echo "" echo ""
docker exec -it -u www-data $(docker ps -f name=php-fpm-mailcow -q) bash -c "/web/nextcloud/occ --no-warnings config:system:set redis host --value=redis --type=string; \ docker exec -it -u www-data $(docker ps -f name=php-fpm-mailcow -q) bash -c "/web/nextcloud/occ --no-warnings config:system:set redis host --value=redis --type=string; \
/web/nextcloud/occ --no-warnings config:system:set redis port --value=6379 --type=integer; \ /web/nextcloud/occ --no-warnings config:system:set redis port --value=6379 --type=integer; \
@@ -185,7 +178,7 @@ elif [[ ${NC_INSTALL} == "y" ]]; then
#/web/nextcloud/occ --no-warnings config:system:set user_backends 0 arguments 0 --value={dovecot:143/imap/tls/novalidate-cert}; \ #/web/nextcloud/occ --no-warnings config:system:set user_backends 0 arguments 0 --value={dovecot:143/imap/tls/novalidate-cert}; \
#/web/nextcloud/occ --no-warnings config:system:set user_backends 0 class --value=OC_User_IMAP; \ #/web/nextcloud/occ --no-warnings config:system:set user_backends 0 class --value=OC_User_IMAP; \
echo -e "\r[4/4] Enabling Nginx Configuration" echo -e "\r[4/4] Enabling NGINX Configuration"
cp ./data/assets/nextcloud/nextcloud.conf ./data/conf/nginx/ cp ./data/assets/nextcloud/nextcloud.conf ./data/conf/nginx/
sed -i "s/NC_SUBD/${NC_SUBD}/g" ./data/conf/nginx/nextcloud.conf sed -i "s/NC_SUBD/${NC_SUBD}/g" ./data/conf/nginx/nextcloud.conf
sleep 2 sleep 2
@@ -200,11 +193,11 @@ elif [[ ${NC_INSTALL} == "y" ]]; then
echo "* INSTALL DATE: $(date +%Y-%m-%d_%H-%M-%S) *" echo "* INSTALL DATE: $(date +%Y-%m-%d_%H-%M-%S) *"
echo "******************************************" echo "******************************************"
echo "" echo ""
echo -e "\033[36mDatabase name: ${NC_DBNAME}\033[0m" echo -e "\033[36mDatabase Name: ${NC_DBNAME}\033[0m"
echo -e "\033[36mDatabase user: ${NC_DBUSER}\033[0m" echo -e "\033[36mDatabase User: ${NC_DBUSER}\033[0m"
echo -e "\033[36mDatabase password: ${NC_DBPASS}\033[0m" echo -e "\033[36mDatabase Password: ${NC_DBPASS}\033[0m"
echo "" echo ""
echo -e "\033[31mUI admin password: ${ADMIN_NC_PASS}\033[0m" echo -e "\033[31mUI Admin Password: ${ADMIN_NC_PASS}\033[0m"
echo "" echo ""
@@ -222,4 +215,5 @@ elif [[ ${NC_RESETPW} == "y" ]]; then
read -p "Enter the username: " NC_USER read -p "Enter the username: " NC_USER
done done
docker exec -it -u www-data $(docker ps -f name=php-fpm-mailcow -q) /web/nextcloud/occ user:resetpassword ${NC_USER} docker exec -it -u www-data $(docker ps -f name=php-fpm-mailcow -q) /web/nextcloud/occ user:resetpassword ${NC_USER}
fi fi

View File

@@ -176,19 +176,18 @@ remove_obsolete_nginx_ports() {
} }
detect_docker_compose_command(){ detect_docker_compose_command(){
if ! [[ "${DOCKER_COMPOSE_VERSION}" =~ ^(native|standalone)$ ]]; then if ! [ "${DOCKER_COMPOSE_VERSION}" == "native" ] && ! [ "${DOCKER_COMPOSE_VERSION}" == "standalone" ]; then
if docker compose > /dev/null 2>&1; then if docker compose > /dev/null 2>&1; then
if docker compose version --short | grep "2." > /dev/null 2>&1; then if docker compose version --short | grep "2." > /dev/null 2>&1; then
DOCKER_COMPOSE_VERSION=native DOCKER_COMPOSE_VERSION=native
COMPOSE_COMMAND="docker compose" COMPOSE_COMMAND="docker compose"
echo -e "\e[31mFound Docker Compose Plugin (native).\e[0m" echo -e "\e[31mFound Docker Compose Plugin (native).\e[0m"
echo -e "\e[31mSetting the DOCKER_COMPOSE_VERSION Variable to native\e[0m" echo -e "\e[31mSetting the DOCKER_COMPOSE_VERSION Variable to native\e[0m"
sed -i 's/^DOCKER_COMPOSE_VERSION=.*/DOCKER_COMPOSE_VERSION=native/' $SCRIPT_DIR/mailcow.conf
sleep 2 sleep 2
echo -e "\e[33mNotice: You'll have to update this Compose Version via your Package Manager manually!\e[0m" echo -e "\e[33mNotice: You'll have to update this Compose Version via your Package Manager manually!\e[0m"
else else
echo -e "\e[31mCannot find Docker Compose with a Version Higher than 2.X.X.\e[0m" echo -e "\e[31mCannot find Docker Compose with a Version Higher than 2.X.X.\e[0m"
echo -e "\e[31mPlease update/install it manually regarding to this doc site: https://docs.mailcow.email/mailcow-dockerized-docs/i_u_m/i_u_m_install/\e[0m" echo -e "\e[31mPlease update/install it manually regarding to this doc site: https://mailcow.github.io/mailcow-dockerized-docs/i_u_m/i_u_m_install/\e[0m"
exit 1 exit 1
fi fi
elif docker-compose > /dev/null 2>&1; then elif docker-compose > /dev/null 2>&1; then
@@ -198,60 +197,26 @@ if ! [[ "${DOCKER_COMPOSE_VERSION}" =~ ^(native|standalone)$ ]]; then
COMPOSE_COMMAND="docker-compose" COMPOSE_COMMAND="docker-compose"
echo -e "\e[31mFound Docker Compose Standalone.\e[0m" echo -e "\e[31mFound Docker Compose Standalone.\e[0m"
echo -e "\e[31mSetting the DOCKER_COMPOSE_VERSION Variable to standalone\e[0m" echo -e "\e[31mSetting the DOCKER_COMPOSE_VERSION Variable to standalone\e[0m"
sed -i 's/^DOCKER_COMPOSE_VERSION=.*/DOCKER_COMPOSE_VERSION=standalone/' $SCRIPT_DIR/mailcow.conf
sleep 2 sleep 2
echo -e "\e[33mNotice: For an automatic update of docker-compose please use the update_compose.sh scripts located at the helper-scripts folder.\e[0m" echo -e "\e[33mNotice: For an automatic update of docker-compose please use the update_compose.sh scripts located at the helper-scripts folder.\e[0m"
else else
echo -e "\e[31mCannot find Docker Compose with a Version Higher than 2.X.X.\e[0m" echo -e "\e[31mCannot find Docker Compose with a Version Higher than 2.X.X.\e[0m"
echo -e "\e[31mPlease update/install regarding to this doc site: https://docs.mailcow.email/mailcow-dockerized-docs/i_u_m/i_u_m_install/\e[0m" echo -e "\e[31mPlease update/install regarding to this doc site: https://mailcow.github.io/mailcow-dockerized-docs/i_u_m/i_u_m_install/\e[0m"
exit 1 exit 1
fi fi
fi fi
else else
echo -e "\e[31mCannot find Docker Compose.\e[0m" echo -e "\e[31mCannot find Docker Compose.\e[0m"
echo -e "\e[31mPlease install it regarding to this doc site: https://docs.mailcow.email/mailcow-dockerized-docs/i_u_m/i_u_m_install/\e[0m" echo -e "\e[31mPlease install it regarding to this doc site: https://mailcow.github.io/mailcow-dockerized-docs/i_u_m/i_u_m_install/\e[0m"
exit 1 exit 1
fi fi
elif [ "${DOCKER_COMPOSE_VERSION}" == "native" ]; then elif [ "${DOCKER_COMPOSE_VERSION}" == "native" ]; then
COMPOSE_COMMAND="docker compose" COMPOSE_COMMAND="docker compose"
# Check if Native Compose works and has not been deleted
if ! $COMPOSE_COMMAND > /dev/null 2>&1; then
# IF it not exists/work anymore try the other command
COMPOSE_COMMAND="docker-compose"
if ! $COMPOSE_COMMAND > /dev/null 2>&1 || ! $COMPOSE_COMMAND --version | grep "^2." > /dev/null 2>&1; then
# IF it cannot find Standalone in > 2.X, then script stops
echo -e "\e[31mCannot find Docker Compose or the Version is lower then 2.X.X.\e[0m"
echo -e "\e[31mPlease install it regarding to this doc site: https://docs.mailcow.email/mailcow-dockerized-docs/i_u_m/i_u_m_install/\e[0m"
exit 1
fi
# If it finds the standalone Plugin it will use this instead and change the mailcow.conf Variable accordingly
echo -e "\e[31mFound different Docker Compose Version then declared in mailcow.conf!\e[0m"
echo -e "\e[31mSetting the DOCKER_COMPOSE_VERSION Variable from native to standalone\e[0m"
sed -i 's/^DOCKER_COMPOSE_VERSION=.*/DOCKER_COMPOSE_VERSION=standalone/' $SCRIPT_DIR/mailcow.conf
sleep 2
fi
elif [ "${DOCKER_COMPOSE_VERSION}" == "standalone" ]; then elif [ "${DOCKER_COMPOSE_VERSION}" == "standalone" ]; then
COMPOSE_COMMAND="docker-compose" COMPOSE_COMMAND="docker-compose"
# Check if Standalone Compose works and has not been deleted
if ! $COMPOSE_COMMAND > /dev/null 2>&1 && ! $COMPOSE_COMMAND --version > /dev/null 2>&1 | grep "^2." > /dev/null 2>&1; then
# IF it not exists/work anymore try the other command
COMPOSE_COMMAND="docker compose"
if ! $COMPOSE_COMMAND > /dev/null 2>&1; then
# IF it cannot find Native in > 2.X, then script stops
echo -e "\e[31mCannot find Docker Compose.\e[0m"
echo -e "\e[31mPlease install it regarding to this doc site: https://docs.mailcow.email/mailcow-dockerized-docs/i_u_m/i_u_m_install/\e[0m"
exit 1
fi
# If it finds the native Plugin it will use this instead and change the mailcow.conf Variable accordingly
echo -e "\e[31mFound different Docker Compose Version then declared in mailcow.conf!\e[0m"
echo -e "\e[31mSetting the DOCKER_COMPOSE_VERSION Variable from standalone to native\e[0m"
sed -i 's/^DOCKER_COMPOSE_VERSION=.*/DOCKER_COMPOSE_VERSION=native/' $SCRIPT_DIR/mailcow.conf
sleep 2
fi
fi fi
} }
@@ -361,12 +326,8 @@ while (($#)); do
echo -e "\e[32mRunning in forced mode...\e[0m" echo -e "\e[32mRunning in forced mode...\e[0m"
FORCE=y FORCE=y
;; ;;
-d|--dev)
echo -e "\e[32mRunning in Developer mode...\e[0m"
DEV=y
;;
--help|-h) --help|-h)
echo './update.sh [-c|--check, --ours, --gc, --nightly, --prefetch, --skip-start, --skip-ping-check, --stable, -f|--force, -d|--dev, -h|--help] echo './update.sh [-c|--check, --ours, --gc, --nightly, --prefetch, --skip-start, --skip-ping-check, --stable, -f|--force, -h|--help]
-c|--check - Check for updates and exit (exit codes => 0: update available, 3: no updates) -c|--check - Check for updates and exit (exit codes => 0: update available, 3: no updates)
--ours - Use merge strategy option "ours" to solve conflicts in favor of non-mailcow code (local changes over remote changes), not recommended! --ours - Use merge strategy option "ours" to solve conflicts in favor of non-mailcow code (local changes over remote changes), not recommended!
@@ -377,7 +338,6 @@ while (($#)); do
--skip-ping-check - Skip ICMP Check to public DNS resolvers (Use it only if you´ve blocked any ICMP Connections to your mailcow machine) --skip-ping-check - Skip ICMP Check to public DNS resolvers (Use it only if you´ve blocked any ICMP Connections to your mailcow machine)
--stable - Switch your mailcow updates to the stable (master) branch. Default unless you changed it with --nightly. --stable - Switch your mailcow updates to the stable (master) branch. Default unless you changed it with --nightly.
-f|--force - Force update, do not ask questions -f|--force - Force update, do not ask questions
-d|--dev - Enables Developer Mode (No Checkout of update.sh for tests)
' '
exit 1 exit 1
esac esac
@@ -637,7 +597,7 @@ for option in ${CONFIG_ARRAY[@]}; do
echo "Adding new option \"${option}\" to mailcow.conf" echo "Adding new option \"${option}\" to mailcow.conf"
echo '# Password hash algorithm' >> mailcow.conf echo '# Password hash algorithm' >> mailcow.conf
echo '# Only certain password hash algorithm are supported. For a fully list of supported schemes,' >> mailcow.conf echo '# Only certain password hash algorithm are supported. For a fully list of supported schemes,' >> mailcow.conf
echo '# see https://docs.mailcow.email/models/model-passwd/' >> mailcow.conf echo '# see https://mailcow.github.io/mailcow-dockerized-docs/models/model-passwd/' >> mailcow.conf
echo "MAILCOW_PASS_SCHEME=BLF-CRYPT" >> mailcow.conf echo "MAILCOW_PASS_SCHEME=BLF-CRYPT" >> mailcow.conf
fi fi
elif [[ ${option} == "ADDITIONAL_SERVER_NAMES" ]]; then elif [[ ${option} == "ADDITIONAL_SERVER_NAMES" ]]; then
@@ -657,7 +617,7 @@ for option in ${CONFIG_ARRAY[@]}; do
echo '# Optional: Leave empty for none' >> mailcow.conf echo '# Optional: Leave empty for none' >> mailcow.conf
echo '# This value is only used on first order!' >> mailcow.conf echo '# This value is only used on first order!' >> mailcow.conf
echo '# Setting it at a later point will require the following steps:' >> mailcow.conf echo '# Setting it at a later point will require the following steps:' >> mailcow.conf
echo '# https://docs.mailcow.email/troubleshooting/debug-reset_tls/' >> mailcow.conf echo '# https://mailcow.github.io/mailcow-dockerized-docs/troubleshooting/debug-reset_tls/' >> mailcow.conf
echo 'ACME_CONTACT=' >> mailcow.conf echo 'ACME_CONTACT=' >> mailcow.conf
fi fi
elif [[ ${option} == "WEBAUTHN_ONLY_TRUSTED_VENDORS" ]]; then elif [[ ${option} == "WEBAUTHN_ONLY_TRUSTED_VENDORS" ]]; then
@@ -767,17 +727,15 @@ elif [ $NEW_BRANCH == "nightly" ] && [ $CURRENT_BRANCH != "nightly" ]; then
git checkout -f ${BRANCH} git checkout -f ${BRANCH}
fi fi
if [ ! $DEV ]; then echo -e "\e[32mChecking for newer update script...\e[0m"
echo -e "\e[32mChecking for newer update script...\e[0m" SHA1_1=$(sha1sum update.sh)
SHA1_1=$(sha1sum update.sh) git fetch origin #${BRANCH}
git fetch origin #${BRANCH} git checkout origin/${BRANCH} update.sh
git checkout origin/${BRANCH} update.sh SHA1_2=$(sha1sum update.sh)
SHA1_2=$(sha1sum update.sh) if [[ ${SHA1_1} != ${SHA1_2} ]]; then
if [[ ${SHA1_1} != ${SHA1_2} ]]; then echo "update.sh changed, please run this script again, exiting."
echo "update.sh changed, please run this script again, exiting." chmod +x update.sh
chmod +x update.sh exit 2
exit 2
fi
fi fi
if [ ! $FORCE ]; then if [ ! $FORCE ]; then
@@ -944,6 +902,9 @@ else
echo -e "\e[33mCannot determine current git repository version...\e[0m" echo -e "\e[33mCannot determine current git repository version...\e[0m"
fi fi
# Set DOCKER_COMPOSE_VERSION
sed -i 's/^DOCKER_COMPOSE_VERSION=$/DOCKER_COMPOSE_VERSION='$DOCKER_COMPOSE_VERSION'/g' mailcow.conf
if [[ ${SKIP_START} == "y" ]]; then if [[ ${SKIP_START} == "y" ]]; then
echo -e "\e[33mNot starting mailcow, please run \"$COMPOSE_COMMAND up -d --remove-orphans\" to start mailcow.\e[0m" echo -e "\e[33mNot starting mailcow, please run \"$COMPOSE_COMMAND up -d --remove-orphans\" to start mailcow.\e[0m"
else else