Merge remote-tracking branch 'origin/staging' into add-podman-support

This commit is contained in:
Roy Lenferink 2023-02-24 13:32:02 +01:00
commit 482854f7fa
65 changed files with 5912 additions and 5769 deletions

View File

@ -1,15 +1,19 @@
{
"enabled": true,
"timezone": "Europe/Berlin",
"dependencyDashboard": false,
"dependencyDashboard": true,
"dependencyDashboardTitle": "Renovate Dashboard",
"commitBody": "Signed-off-by: milkmaker <milkmaker@mailcow.de>",
"rebaseWhen": "auto",
"labels": ["renovate"],
"assignees": [
"@magiccc"
],
"baseBranches": ["staging"],
"enabledManagers": ["github-actions", "regex"],
"enabledManagers": ["github-actions", "regex", "docker-compose"],
"ignorePaths": [
"data\/web\/inc\/lib\/vendor\/matthiasmullie\/minify\/**"
],
"regexManagers": [
{
"fileMatch": ["^helper-scripts\/nextcloud.sh$"],

View File

@ -1,63 +0,0 @@
name: mailcow Integration Tests
on:
push:
branches: [ "master", "staging" ]
workflow_dispatch:
permissions:
contents: read
jobs:
integration_tests:
runs-on: ubuntu-latest
steps:
- name: Setup Ansible
run: |
export DEBIAN_FRONTEND=noninteractive
sudo apt-get update
sudo apt-get install python3 python3-pip git
sudo pip3 install ansible
- name: Prepair Test Environment
run: |
git clone https://github.com/mailcow/mailcow-integration-tests.git --branch $(curl -sL https://api.github.com/repos/mailcow/mailcow-integration-tests/releases/latest | jq -r '.tag_name') --single-branch .
./fork_check.sh
./ci.sh
./ci-pip-requirements.sh
env:
VAULT_PW: ${{ secrets.MAILCOW_TESTS_VAULT_PW }}
VAULT_FILE: ${{ secrets.MAILCOW_TESTS_VAULT_FILE }}
- name: Start Integration Test Server
run: |
./fork_check.sh
ansible-playbook mailcow-start-server.yml --diff
env:
PY_COLORS: '1'
ANSIBLE_FORCE_COLOR: '1'
ANSIBLE_HOST_KEY_CHECKING: 'false'
- name: Setup Integration Test Server
run: |
./fork_check.sh
sleep 30
ansible-playbook mailcow-setup-server.yml --private-key id_ssh_rsa --diff
env:
PY_COLORS: '1'
ANSIBLE_FORCE_COLOR: '1'
ANSIBLE_HOST_KEY_CHECKING: 'false'
- name: Run Integration Tests
run: |
./fork_check.sh
ansible-playbook mailcow-integration-tests.yml --private-key id_ssh_rsa --diff
env:
PY_COLORS: '1'
ANSIBLE_FORCE_COLOR: '1'
ANSIBLE_HOST_KEY_CHECKING: 'false'
- name: Delete Integration Test Server
if: always()
run: |
./fork_check.sh
ansible-playbook mailcow-delete-server.yml --diff
env:
PY_COLORS: '1'
ANSIBLE_FORCE_COLOR: '1'
ANSIBLE_HOST_KEY_CHECKING: 'false'

View File

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

View File

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

View File

@ -1,20 +0,0 @@
name: "Tweet trigger release"
on:
release:
types: [published]
jobs:
tweet:
runs-on: ubuntu-latest
steps:
- name: "Get Release Tag"
run: |
RELEASE_TAG=$(curl https://api.github.com/repos/mailcow/mailcow-dockerized/releases/latest | jq -r '.tag_name')
- name: Tweet-trigger-publish-release
uses: mugi111/tweet-trigger-release@v1.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,6 +1,5 @@
# 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/)
[![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,11 +213,13 @@ while true; do
done
ADDITIONAL_WC_ARR+=('autodiscover' 'autoconfig')
if [[ ${SKIP_IP_CHECK} != "y" ]]; then
# Start IP detection
log_f "Detecting IP addresses..."
IPV4=$(get_ipv4)
IPV6=$(get_ipv6)
log_f "OK: ${IPV4}, ${IPV6:-"0000:0000:0000:0000:0000:0000:0000:0000"}"
fi
#########################################
# IP and webroot challenge verification #

View File

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

View File

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

View File

@ -1,5 +1,6 @@
from fastapi import FastAPI, Response, Request
import aiodocker
import docker
import psutil
import sys
import re
@ -9,11 +10,38 @@ import json
import asyncio
import redis
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 = []
host_stats_isUpdating = False
app = FastAPI()
logger = logging.getLogger('api-logger')
@app.get("/host/stats")
@ -21,18 +49,15 @@ async def get_host_update_stats():
global host_stats_isUpdating
if host_stats_isUpdating == False:
print("start host stats task")
asyncio.create_task(get_host_stats())
host_stats_isUpdating = True
while True:
if redis_client.exists('host_stats'):
break
print("wait for host_stats results")
await asyncio.sleep(1.5)
print("host stats pulled")
stats = json.loads(redis_client.get('host_stats'))
return Response(content=json.dumps(stats, indent=4), media_type="application/json")
@ -106,14 +131,14 @@ async def post_containers(container_id : str, post_action : str, request: Reques
else:
api_call_method_name = '__'.join(['container_post', str(post_action) ])
docker_utils = DockerUtils(async_docker_client)
docker_utils = DockerUtils(sync_docker_client)
api_call_method = getattr(docker_utils, api_call_method_name, lambda container_id: Response(content=json.dumps({'type': 'danger', 'msg':'container_post - unknown api call' }, indent=4), media_type="application/json"))
print("api call: %s, container_id: %s" % (api_call_method_name, container_id))
return await api_call_method(container_id, request_json)
logger.info("api call: %s, container_id: %s" % (api_call_method_name, container_id))
return api_call_method(container_id, request_json)
except Exception as e:
print("error - container_post: %s" % str(e))
logger.error("error - container_post: %s" % str(e))
res = {
"type": "danger",
"msg": str(e)
@ -152,398 +177,289 @@ class DockerUtils:
self.docker_client = docker_client
# api call: container_post - post_action: stop
async def container_post__stop(self, container_id, request_json):
for container in (await self.docker_client.containers.list()):
if container._id == container_id:
await container.stop()
res = {
'type': 'success',
'msg': 'command completed successfully'
}
return Response(content=json.dumps(res, indent=4), media_type="application/json")
def container_post__stop(self, container_id, request_json):
for container in self.docker_client.containers.list(all=True, filters={"id": container_id}):
container.stop()
res = { 'type': 'success', 'msg': 'command completed successfully'}
return Response(content=json.dumps(res, indent=4), media_type="application/json")
# api call: container_post - post_action: start
async def container_post__start(self, container_id, request_json):
for container in (await self.docker_client.containers.list()):
if container._id == container_id:
await container.start()
res = {
'type': 'success',
'msg': 'command completed successfully'
}
def container_post__start(self, container_id, request_json):
for container in self.docker_client.containers.list(all=True, filters={"id": container_id}):
container.start()
res = { 'type': 'success', 'msg': 'command completed successfully'}
return Response(content=json.dumps(res, indent=4), media_type="application/json")
# api call: container_post - post_action: restart
async def container_post__restart(self, container_id, request_json):
for container in (await self.docker_client.containers.list()):
if container._id == container_id:
await container.restart()
res = {
'type': 'success',
'msg': 'command completed successfully'
}
def container_post__restart(self, container_id, request_json):
for container in self.docker_client.containers.list(all=True, filters={"id": container_id}):
container.restart()
res = { 'type': 'success', 'msg': 'command completed successfully'}
return Response(content=json.dumps(res, indent=4), media_type="application/json")
# api call: container_post - post_action: top
async def container_post__top(self, container_id, request_json):
for container in (await self.docker_client.containers.list()):
if container._id == container_id:
ps_exec = await container.exec("ps")
async with ps_exec.start(detach=False) as stream:
ps_return = await stream.read_out()
exec_details = await ps_exec.inspect()
if exec_details["ExitCode"] == None or exec_details["ExitCode"] == 0:
res = {
'type': 'success',
'msg': ps_return.data.decode('utf-8')
}
def container_post__top(self, container_id, request_json):
for container in self.docker_client.containers.list(all=True, filters={"id": container_id}):
res = { 'type': 'success', 'msg': container.top()}
return Response(content=json.dumps(res, indent=4), media_type="application/json")
else:
res = {
'type': 'danger',
'msg': ''
}
# api call: container_post - post_action: stats
def container_post__stats(self, container_id, request_json):
for container in self.docker_client.containers.list(all=True, filters={"id": container_id}):
for stat in container.stats(decode=True, stream=True):
res = { 'type': 'success', 'msg': stat}
return Response(content=json.dumps(res, indent=4), media_type="application/json")
# api call: container_post - post_action: exec - cmd: mailq - task: delete
async def container_post__exec__mailq__delete(self, container_id, request_json):
def container_post__exec__mailq__delete(self, container_id, request_json):
if 'items' in request_json:
r = re.compile("^[0-9a-fA-F]+$")
filtered_qids = filter(r.match, request_json['items'])
if filtered_qids:
flagged_qids = ['-d %s' % i for i in filtered_qids]
sanitized_string = str(' '.join(flagged_qids))
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
async def container_post__exec__mailq__hold(self, container_id, request_json):
def container_post__exec__mailq__hold(self, container_id, request_json):
if 'items' in request_json:
r = re.compile("^[0-9a-fA-F]+$")
filtered_qids = filter(r.match, request_json['items'])
if filtered_qids:
flagged_qids = ['-h %s' % i for i in filtered_qids]
sanitized_string = str(' '.join(flagged_qids))
for container in (await self.docker_client.containers.list()):
if container._id == container_id:
postsuper_r_exec = await container.exec(["/bin/bash", "-c", "/usr/sbin/postsuper " + sanitized_string])
return await exec_run_handler('generic', postsuper_r_exec)
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)
# api call: container_post - post_action: exec - cmd: mailq - task: cat
async def container_post__exec__mailq__cat(self, container_id, request_json):
def container_post__exec__mailq__cat(self, container_id, request_json):
if 'items' in request_json:
r = re.compile("^[0-9a-fA-F]+$")
filtered_qids = filter(r.match, request_json['items'])
if filtered_qids:
sanitized_string = str(' '.join(filtered_qids))
sanitized_string = str(' '.join(filtered_qids));
for container in (await self.docker_client.containers.list()):
if container._id == container_id:
postcat_exec = await container.exec(["/bin/bash", "-c", "/usr/sbin/postcat -q " + sanitized_string], user='postfix')
return await exec_run_handler('utf8_text_only', postcat_exec)
for container in self.docker_client.containers.list(filters={"id": container_id}):
postcat_return = container.exec_run(["/bin/bash", "-c", "/usr/sbin/postcat -q " + sanitized_string], user='postfix')
if not postcat_return:
postcat_return = 'err: invalid'
return exec_run_handler('utf8_text_only', postcat_return)
# api call: container_post - post_action: exec - cmd: mailq - task: unhold
async def container_post__exec__mailq__unhold(self, container_id, request_json):
def container_post__exec__mailq__unhold(self, container_id, request_json):
if 'items' in request_json:
r = re.compile("^[0-9a-fA-F]+$")
filtered_qids = filter(r.match, request_json['items'])
if filtered_qids:
flagged_qids = ['-H %s' % i for i in filtered_qids]
sanitized_string = str(' '.join(flagged_qids))
for container in (await self.docker_client.containers.list()):
if container._id == container_id:
postsuper_r_exec = await container.exec(["/bin/bash", "-c", "/usr/sbin/postsuper " + sanitized_string])
return await exec_run_handler('generic', postsuper_r_exec)
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)
# api call: container_post - post_action: exec - cmd: mailq - task: deliver
async def container_post__exec__mailq__deliver(self, container_id, request_json):
def container_post__exec__mailq__deliver(self, container_id, request_json):
if 'items' in request_json:
r = re.compile("^[0-9a-fA-F]+$")
filtered_qids = filter(r.match, request_json['items'])
if filtered_qids:
flagged_qids = ['-i %s' % i for i in filtered_qids]
for container in (await self.docker_client.containers.list()):
if container._id == container_id:
for container in self.docker_client.containers.list(filters={"id": container_id}):
for i in flagged_qids:
postsuper_r_exec = await container.exec(["/bin/bash", "-c", "/usr/sbin/postqueue " + i], user='postfix')
async with postsuper_r_exec.start(detach=False) as stream:
postsuper_r_return = await stream.read_out()
postqueue_r = container.exec_run(["/bin/bash", "-c", "/usr/sbin/postqueue " + i], user='postfix')
# todo: check each exit code
res = {
'type': 'success',
'msg': 'Scheduled immediate delivery'
}
res = { 'type': 'success', 'msg': 'Scheduled immediate delivery'}
return Response(content=json.dumps(res, indent=4), media_type="application/json")
# api call: container_post - post_action: exec - cmd: mailq - task: list
async def container_post__exec__mailq__list(self, container_id, request_json):
for container in (await self.docker_client.containers.list()):
if container._id == container_id:
mailq_exec = await container.exec(["/usr/sbin/postqueue", "-j"], user='postfix')
return await exec_run_handler('utf8_text_only', mailq_exec)
def container_post__exec__mailq__list(self, container_id, request_json):
for container in self.docker_client.containers.list(filters={"id": container_id}):
mailq_return = container.exec_run(["/usr/sbin/postqueue", "-j"], user='postfix')
return exec_run_handler('utf8_text_only', mailq_return)
# api call: container_post - post_action: exec - cmd: mailq - task: flush
async def container_post__exec__mailq__flush(self, container_id, request_json):
for container in (await self.docker_client.containers.list()):
if container._id == container_id:
postsuper_r_exec = await container.exec(["/usr/sbin/postqueue", "-f"], user='postfix')
return await exec_run_handler('generic', postsuper_r_exec)
def container_post__exec__mailq__flush(self, container_id, request_json):
for container in self.docker_client.containers.list(filters={"id": container_id}):
postqueue_r = container.exec_run(["/usr/sbin/postqueue", "-f"], user='postfix')
return exec_run_handler('generic', postqueue_r)
# api call: container_post - post_action: exec - cmd: mailq - task: super_delete
async def container_post__exec__mailq__super_delete(self, container_id, request_json):
for container in (await self.docker_client.containers.list()):
if container._id == container_id:
postsuper_r_exec = await container.exec(["/usr/sbin/postsuper", "-d", "ALL"])
return await exec_run_handler('generic', postsuper_r_exec)
def container_post__exec__mailq__super_delete(self, container_id, request_json):
for container in self.docker_client.containers.list(filters={"id": container_id}):
postsuper_r = container.exec_run(["/usr/sbin/postsuper", "-d", "ALL"])
return exec_run_handler('generic', postsuper_r)
# api call: container_post - post_action: exec - cmd: system - task: fts_rescan
async def container_post__exec__system__fts_rescan(self, container_id, request_json):
def container_post__exec__system__fts_rescan(self, container_id, request_json):
if 'username' in request_json:
for container in (await self.docker_client.containers.list()):
if container._id == container_id:
rescan_exec = await container.exec(["/bin/bash", "-c", "/usr/bin/doveadm fts rescan -u '" + request_json['username'].replace("'", "'\\''") + "'"], user='vmail')
async with rescan_exec.start(detach=False) as stream:
rescan_return = await stream.read_out()
exec_details = await rescan_exec.inspect()
if exec_details["ExitCode"] == None or exec_details["ExitCode"] == 0:
res = {
'type': 'success',
'msg': 'fts_rescan: rescan triggered'
}
for container in self.docker_client.containers.list(filters={"id": container_id}):
rescan_return = container.exec_run(["/bin/bash", "-c", "/usr/bin/doveadm fts rescan -u '" + request_json['username'].replace("'", "'\\''") + "'"], user='vmail')
if rescan_return.exit_code == 0:
res = { 'type': 'success', 'msg': 'fts_rescan: rescan triggered'}
return Response(content=json.dumps(res, indent=4), media_type="application/json")
else:
res = {
'type': 'warning',
'msg': 'fts_rescan error'
}
res = { 'type': 'warning', 'msg': 'fts_rescan error'}
return Response(content=json.dumps(res, indent=4), media_type="application/json")
if 'all' in request_json:
for container in (await self.docker_client.containers.list()):
if container._id == container_id:
rescan_exec = await container.exec(["/bin/bash", "-c", "/usr/bin/doveadm fts rescan -A"], user='vmail')
async with rescan_exec.start(detach=False) as stream:
rescan_return = await stream.read_out()
exec_details = await rescan_exec.inspect()
if exec_details["ExitCode"] == None or exec_details["ExitCode"] == 0:
res = {
'type': 'success',
'msg': 'fts_rescan: rescan triggered'
}
for container in self.docker_client.containers.list(filters={"id": container_id}):
rescan_return = container.exec_run(["/bin/bash", "-c", "/usr/bin/doveadm fts rescan -A"], user='vmail')
if rescan_return.exit_code == 0:
res = { 'type': 'success', 'msg': 'fts_rescan: rescan triggered'}
return Response(content=json.dumps(res, indent=4), media_type="application/json")
else:
res = {
'type': 'warning',
'msg': 'fts_rescan error'
}
res = { 'type': 'warning', 'msg': 'fts_rescan error'}
return Response(content=json.dumps(res, indent=4), media_type="application/json")
# api call: container_post - post_action: exec - cmd: system - task: df
async def container_post__exec__system__df(self, container_id, request_json):
def container_post__exec__system__df(self, container_id, request_json):
if 'dir' in request_json:
for container in (await self.docker_client.containers.list()):
if container._id == container_id:
df_exec = await container.exec(["/bin/bash", "-c", "/bin/df -H '" + request_json['dir'].replace("'", "'\\''") + "' | /usr/bin/tail -n1 | /usr/bin/tr -s [:blank:] | /usr/bin/tr ' ' ','"], user='nobody')
async with df_exec.start(detach=False) as stream:
df_return = await stream.read_out()
print(df_return)
print(await df_exec.inspect())
exec_details = await df_exec.inspect()
if exec_details["ExitCode"] == None or exec_details["ExitCode"] == 0:
return df_return.data.decode('utf-8').rstrip()
for container in self.docker_client.containers.list(filters={"id": container_id}):
df_return = container.exec_run(["/bin/bash", "-c", "/bin/df -H '" + request_json['dir'].replace("'", "'\\''") + "' | /usr/bin/tail -n1 | /usr/bin/tr -s [:blank:] | /usr/bin/tr ' ' ','"], user='nobody')
if df_return.exit_code == 0:
return df_return.output.decode('utf-8').rstrip()
else:
return "0,0,0,0,0,0"
# api call: container_post - post_action: exec - cmd: system - task: mysql_upgrade
async def container_post__exec__system__mysql_upgrade(self, container_id, request_json):
for container in (await self.docker_client.containers.list()):
if container._id == container_id:
sql_exec = await container.exec(["/bin/bash", "-c", "/usr/bin/mysql_upgrade -uroot -p'" + os.environ['DBROOT'].replace("'", "'\\''") + "'\n"], user='mysql')
async with sql_exec.start(detach=False) as stream:
sql_return = await stream.read_out()
exec_details = await sql_exec.inspect()
if exec_details["ExitCode"] == None or exec_details["ExitCode"] == 0:
def container_post__exec__system__mysql_upgrade(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_upgrade -uroot -p'" + os.environ['DBROOT'].replace("'", "'\\''") + "'\n"], user='mysql')
if sql_return.exit_code == 0:
matched = False
for line in sql_return.data.decode('utf-8').split("\n"):
for line in sql_return.output.decode('utf-8').split("\n"):
if 'is already upgraded to' in line:
matched = True
if matched:
res = {
'type': 'success',
'msg': 'mysql_upgrade: already upgraded',
'text': sql_return.data.decode('utf-8')
}
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:
await container.restart()
res = {
'type': 'warning',
'msg': 'mysql_upgrade: upgrade was applied',
'text': sql_return.data.decode('utf-8')
}
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.data.decode('utf-8')
}
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
async def container_post__exec__system__mysql_tzinfo_to_sql(self, container_id, request_json):
for container in (await self.docker_client.containers.list()):
if container._id == container_id:
sql_exec = await container.exec(["/bin/bash", "-c", "/usr/bin/mysql_tzinfo_to_sql /usr/share/zoneinfo | /bin/sed 's/Local time zone must be set--see zic manual page/FCTY/' | /usr/bin/mysql -uroot -p'" + os.environ['DBROOT'].replace("'", "'\\''") + "' mysql \n"], user='mysql')
async with sql_exec.start(detach=False) as stream:
sql_return = await stream.read_out()
exec_details = await sql_exec.inspect()
if exec_details["ExitCode"] == None or exec_details["ExitCode"] == 0:
res = {
'type': 'info',
'msg': 'mysql_tzinfo_to_sql: command completed successfully',
'text': sql_return.data.decode('utf-8')
}
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.data.decode('utf-8')
}
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
async def container_post__exec__reload__dovecot(self, container_id, request_json):
for container in (await self.docker_client.containers.list()):
if container._id == container_id:
reload_exec = await container.exec(["/bin/bash", "-c", "/usr/sbin/dovecot reload"])
return await exec_run_handler('generic', reload_exec)
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
async def container_post__exec__reload__postfix(self, container_id, request_json):
for container in (await self.docker_client.containers.list()):
if container._id == container_id:
reload_exec = await container.exec(["/bin/bash", "-c", "/usr/sbin/postfix reload"])
return await exec_run_handler('generic', reload_exec)
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
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)
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
async def container_post__exec__sieve__list(self, container_id, request_json):
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)
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
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:
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_exec = await container.exec(cmd)
return await exec_run_handler('utf8_text_only', sieve_exec)
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
async def container_post__exec__maildir__cleanup(self, container_id, request_json):
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:
for container in self.docker_client.containers.list(filters={"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)
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
async def container_post__exec__rspamd__worker_password(self, container_id, request_json):
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()
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
if "OK" in rspamd_password_return.data.decode('utf-8'):
for line in cmd_response.split("\n"):
if '$2$' in line:
hash = line.strip()
hash_out = re.search('\$2\$.+$', hash).group(0)
rspamd_passphrase_hash = re.sub('[^0-9a-zA-Z\$]+', '', hash_out.rstrip())
rspamd_password_filename = "/etc/rspamd/override.d/worker-controller-password.inc"
cmd = '''/bin/echo 'enable_password = "%s";' > %s && cat %s''' % (rspamd_passphrase_hash, rspamd_password_filename, rspamd_password_filename)
cmd_response = exec_cmd_container(container, cmd, user="_rspamd")
if rspamd_passphrase_hash.startswith("$2$") and rspamd_passphrase_hash in cmd_response:
container.restart()
matched = True
await container.restart()
if matched:
res = {
'type': 'success',
'msg': 'command completed successfully'
}
res = { 'type': 'success', 'msg': 'command completed successfully' }
logger.info('success changing Rspamd password')
return Response(content=json.dumps(res, indent=4), media_type="application/json")
else:
res = {
'type': 'danger',
'msg': 'command did not complete'
}
logger.error('failed changing Rspamd password')
res = { 'type': 'danger', 'msg': 'command did not complete' }
return Response(content=json.dumps(res, indent=4), media_type="application/json")
def exec_cmd_container(container, cmd, user, timeout=2, shell_cmd="/bin/bash"):
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 = ""
def recv_socket_data(c_socket, timeout):
c_socket.setblocking(0)
total_data=[]
data=''
begin=time.time()
while True:
if total_data and time.time()-begin > timeout:
break
elif time.time()-begin > timeout*2:
break
try:
data = c_socket.recv(8192)
if data:
total_data.append(data.decode('utf-8'))
#change the beginning time for measurement
begin=time.time()
else:
exec_return = exec_return.data.decode('utf-8')
#sleep for sometime to indicate a gap
time.sleep(0.1)
break
except:
pass
return ''.join(total_data)
try :
socket = container.exec_run([shell_cmd], stdin=True, socket=True, user=user).output._sock
if not cmd.endswith("\n"):
cmd = cmd + "\n"
socket.send(cmd.encode('utf-8'))
data = recv_socket_data(socket, timeout)
socket.close()
return data
except Exception as e:
logger.error("error - exec_cmd_container: %s" % str(e))
traceback.print_exc(file=sys.stdout)
def exec_run_handler(type, output):
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"
}
if output.exit_code == 0:
res = { 'type': 'success', 'msg': 'command completed successfully' }
return Response(content=json.dumps(res, indent=4), media_type="application/json")
else:
res = {
"type": "success",
"msg": "'command failed: " + exec_return
}
res = { 'type': 'danger', 'msg': 'command failed: ' + output.output.decode('utf-8') }
return Response(content=json.dumps(res, indent=4), media_type="application/json")
if type == 'utf8_text_only':
return Response(content=exec_return, media_type="text/plain")
return Response(content=output.output.decode('utf-8'), media_type="text/plain")
async def get_host_stats(wait=5):
global host_stats_isUpdating
@ -570,12 +486,10 @@ async def get_host_stats(wait=5):
"type": "danger",
"msg": str(e)
}
print(json.dumps(res, indent=4))
await asyncio.sleep(wait)
host_stats_isUpdating = False
async def get_container_stats(container_id, wait=5, stop=False):
global containerIds_to_update
@ -598,13 +512,11 @@ async def get_container_stats(container_id, wait=5, stop=False):
"type": "danger",
"msg": str(e)
}
print(json.dumps(res, indent=4))
else:
res = {
"type": "danger",
"msg": "no or invalid id defined"
}
print(json.dumps(res, indent=4))
await asyncio.sleep(wait)
if stop == True:
@ -615,9 +527,13 @@ async def get_container_stats(container_id, wait=5, stop=False):
await get_container_stats(container_id, wait=0, stop=True)
if os.environ['REDIS_SLAVEOF_IP'] != "":
redis_client = redis.Redis(host=os.environ['REDIS_SLAVEOF_IP'], port=os.environ['REDIS_SLAVEOF_PORT'], db=0)
else:
redis_client = redis.Redis(host='redis-mailcow', port=6379, db=0)
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')
logger.info('DockerApi started')

View File

@ -2,7 +2,8 @@ FROM debian:bullseye-slim
LABEL maintainer "Andre Peters <andre.peters@servercow.de>"
ARG DEBIAN_FRONTEND=noninteractive
ARG DOVECOT=2.3.19.1
# renovate: datasource=github-tags depName=dovecot/core versioning=semver-coerced
ARG DOVECOT=2.3.20
# renovate: datasource=github-releases depName=tianon/gosu versioning=semver-coerced
ARG GOSU_VERSION=1.16
ENV LC_ALL C

View File

@ -359,6 +359,12 @@ def snat4(snat_target):
chain = iptc.Chain(table, 'POSTROUTING')
table.autocommit = False
new_rule = get_snat4_rule()
if not chain.rules:
# if there are no rules in the chain, insert the new rule directly
logInfo(f'Added POSTROUTING rule for source network {new_rule.src} to SNAT target {snat_target}')
chain.insert_rule(new_rule)
else:
for position, rule in enumerate(chain.rules):
match = all((
new_rule.get_src() == rule.get_src(),
@ -374,6 +380,7 @@ def snat4(snat_target):
if match:
logInfo(f'Remove rule for source network {new_rule.src} to SNAT target {snat_target} from POSTROUTING chain at position {position}')
chain.delete_rule(rule)
table.commit()
table.autocommit = True
except:

View File

@ -1,12 +1,18 @@
FROM php:8.1-fpm-alpine3.17
LABEL maintainer "Andre Peters <andre.peters@servercow.de>"
ENV APCU_PECL 5.1.22
ENV IMAGICK_PECL 3.7.0
ENV MAILPARSE_PECL 3.1.4
ENV MEMCACHED_PECL 3.2.0
ENV REDIS_PECL 5.3.7
ENV COMPOSER 2.4.4
# renovate: datasource=github-tags depName=krakjoe/apcu versioning=semver-coerced
ARG APCU_PECL_VERSION=5.1.22
# renovate: datasource=github-tags depName=Imagick/imagick versioning=semver-coerced
ARG IMAGICK_PECL_VERSION=3.7.0
# renovate: datasource=github-tags depName=php/pecl-mail-mailparse versioning=semver-coerced
ARG MAILPARSE_PECL_VERSION=3.1.4
# renovate: datasource=github-tags depName=php-memcached-dev/php-memcached versioning=semver-coerced
ARG MEMCACHED_PECL_VERSION=3.2.0
# renovate: datasource=github-tags depName=phpredis/phpredis versioning=semver-coerced
ARG REDIS_PECL_VERSION=5.3.7
# renovate: datasource=github-tags depName=composer/composer versioning=semver-coerced
ARG COMPOSER_VERSION=2.5.4
RUN apk add -U --no-cache autoconf \
aspell-dev \
@ -55,11 +61,11 @@ RUN apk add -U --no-cache autoconf \
samba-client \
zlib-dev \
tzdata \
&& pecl install mailparse-${MAILPARSE_PECL} \
&& pecl install redis-${REDIS_PECL} \
&& pecl install memcached-${MEMCACHED_PECL} \
&& pecl install APCu-${APCU_PECL} \
&& pecl install imagick-${IMAGICK_PECL} \
&& pecl install APCu-${APCU_PECL_VERSION} \
&& pecl install imagick-${IMAGICK_PECL_VERSION} \
&& pecl install mailparse-${MAILPARSE_PECL_VERSION} \
&& pecl install memcached-${MEMCACHED_PECL_VERSION} \
&& pecl install redis-${REDIS_PECL_VERSION} \
&& docker-php-ext-enable apcu imagick memcached mailparse redis \
&& pecl clear-cache \
&& docker-php-ext-configure intl \
@ -72,7 +78,7 @@ RUN apk add -U --no-cache autoconf \
&& 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-install -j 4 imap \
&& curl --silent --show-error https://getcomposer.org/installer | php -- --version=${COMPOSER} \
&& curl --silent --show-error https://getcomposer.org/installer | php -- --version=${COMPOSER_VERSION} \
&& mv composer.phar /usr/local/bin/composer \
&& chmod +x /usr/local/bin/composer \
&& apk del --purge autoconf \

View File

@ -175,7 +175,7 @@ BAD_SUBJECT_00 {
type = "header";
header = "subject";
regexp = true;
map = "http://nullnull.org/bad-subject-regex.txt";
map = "http://fuzzy.mailcow.email/bad-subject-regex.txt";
score = 6.0;
symbols_set = ["BAD_SUBJECT_00"];
}

View File

@ -699,6 +699,38 @@ paths:
type: string
type: object
summary: Create Domain Admin user
/api/v1/add/sso/domain-admin:
post:
responses:
"401":
$ref: "#/components/responses/Unauthorized"
"200":
content:
application/json:
examples:
response:
value:
token: "591F6D-5C3DD2-7455CD-DAF1C1-AA4FCC"
description: OK
headers: { }
tags:
- Single Sign-On
description: >-
Using this endpoint you can issue a token for Domain Admin user. This token can be used for
autologin Domain Admin user by using query_string var sso_token={token}. Token expiration time is 30s
operationId: Issue Domain Admin SSO token
requestBody:
content:
application/json:
schema:
example:
username: testadmin
properties:
username:
description: the username for the admin user
type: object
type: object
summary: Issue Domain Admin SSO token
/api/v1/edit/da-acl:
post:
responses:
@ -5586,6 +5618,8 @@ tags:
description: Manage DKIM keys
- name: Domain admin
description: Create or udpdate domain admin users
- name: Single Sign-On
description: Issue tokens for users
- name: Address Rewriting
description: Create BCC maps or recipient maps
- name: Outgoing TLS Policy Map Overrides

View File

@ -78,3 +78,21 @@ table.dataTable>tbody>tr.child span.dtr-title {
width: 30%;
max-width: 250px;
}
div.dataTables_wrapper div.dataTables_filter {
text-align: left;
}
div.dataTables_wrapper div.dataTables_length {
text-align: right;
}
.dataTables_paginate, .dataTables_length, .dataTables_filter {
margin: 10px 0!important;
}
td.dt-text-right {
text-align: end !important;
}
th.dt-text-right {
text-align: end !important;
}

View File

@ -370,14 +370,3 @@ 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 {
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,6 +203,9 @@
text-align: left;
}
.senders-mw220 {
max-width: 100% !important;
}
}
@media (max-width: 350px) {

View File

@ -99,4 +99,6 @@ table tbody tr td input[type="checkbox"] {
font-size:110%;
margin:20px;
}
.senders-mw220 {
max-width: 220px;
}

View File

@ -405,3 +405,64 @@ function domain_admin($_action, $_data = null) {
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

@ -1739,7 +1739,7 @@ function verify_tfa_login($username, $_data) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $username, '*'),
'msg' => array('webauthn_verification_failed', 'authenticator not found')
'msg' => array('webauthn_authenticator_failed')
);
return false;
}
@ -1748,7 +1748,16 @@ function verify_tfa_login($username, $_data) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $username, '*'),
'msg' => array('webauthn_verification_failed', 'publicKey not found')
'msg' => array('webauthn_publickey_failed')
);
return false;
}
if ($process_webauthn['username'] != $_SESSION['pending_mailcow_cc_username']){
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $username, '*'),
'msg' => array('webauthn_username_failed')
);
return false;
}
@ -1784,21 +1793,12 @@ function verify_tfa_login($username, $_data) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $username, '*'),
'msg' => array('webauthn_verification_failed', 'could not determine user role')
'msg' => array('webauthn_role_failed')
);
return false;
}
}
if ($process_webauthn['username'] != $_SESSION['pending_mailcow_cc_username']){
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $username, '*'),
'msg' => array('webauthn_verification_failed', 'user who requests does not match with sql entry')
);
return false;
}
$_SESSION["mailcow_cc_username"] = $process_webauthn['username'];
$_SESSION['tfa_id'] = $process_webauthn['id'];
$_SESSION['authReq'] = null;

View File

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

View File

@ -3,7 +3,7 @@ function init_db_schema() {
try {
global $pdo;
$db_version = "23122022_1445";
$db_version = "14022023_1000";
$stmt = $pdo->query("SHOW TABLES LIKE 'versions'");
$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
@ -664,6 +664,19 @@ function init_db_schema() {
),
"attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC"
),
"da_sso" => array(
"cols" => array(
"username" => "VARCHAR(255) NOT NULL",
"token" => "VARCHAR(255) NOT NULL",
"created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)",
),
"keys" => array(
"primary" => array(
"" => array("token", "created")
),
),
"attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC"
),
"imapsync" => array(
"cols" => array(
"id" => "INT NOT NULL AUTO_INCREMENT",
@ -1083,7 +1096,7 @@ function init_db_schema() {
$stmt = $pdo->query("SHOW TABLES LIKE '" . $table . "'");
$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
if ($num_results != 0) {
$stmt = $pdo->prepare("SELECT CONCAT('ALTER TABLE ', `table_schema`, '.', `table_name`, ' DROP FOREIGN KEY ', `constraint_name`, ';') AS `FKEY_DROP` FROM `information_schema`.`table_constraints`
$stmt = $pdo->prepare("SELECT CONCAT('ALTER TABLE `', `table_schema`, '`.', `table_name`, ' DROP FOREIGN KEY ', `constraint_name`, ';') AS `FKEY_DROP` FROM `information_schema`.`table_constraints`
WHERE `constraint_type` = 'FOREIGN KEY' AND `table_name` = :table;");
$stmt->execute(array(':table' => $table));
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);

View File

@ -1,4 +1,15 @@
<?php
// SSO Domain Admin
if (!empty($_GET['sso_token'])) {
$username = domain_admin_sso('check', $_GET['sso_token']);
if ($username !== false) {
$_SESSION['mailcow_cc_username'] = $username;
$_SESSION['mailcow_cc_role'] = 'domainadmin';
header('Location: /mailbox');
}
}
if (isset($_POST["verify_tfa_login"])) {
if (verify_tfa_login($_SESSION['pending_mailcow_cc_username'], $_POST)) {
$_SESSION['mailcow_cc_username'] = $_SESSION['pending_mailcow_cc_username'];

View File

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

View File

@ -74,6 +74,7 @@ jQuery(function($){
processing: true,
serverSide: false,
stateSave: true,
pageLength: pagination_size,
dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" +
"tr" +
"<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
@ -117,7 +118,7 @@ jQuery(function($){
defaultContent: '',
render: function (data, type) {
if(data == 1) return '<i class="bi bi-check-lg"></i>';
else return '<i class="bi bi-x-lg"></i>'
else return '<i class="bi bi-x-lg"></i>';
}
},
{
@ -126,13 +127,13 @@ jQuery(function($){
defaultContent: '',
render: function (data, type) {
if(data == 1) return '<i class="bi bi-check-lg"></i>';
else return '<i class="bi bi-x-lg"></i>'
else return '<i class="bi bi-x-lg"></i>';
}
},
{
title: lang.action,
data: 'action',
className: 'text-md-end dt-sm-head-hidden dt-body-right',
className: 'dt-sm-head-hidden dt-text-right',
defaultContent: ''
},
],
@ -152,6 +153,7 @@ jQuery(function($){
processing: true,
serverSide: false,
stateSave: true,
pageLength: pagination_size,
dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" +
"tr" +
"<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
@ -202,7 +204,7 @@ jQuery(function($){
{
title: lang.action,
data: 'action',
className: 'text-md-end dt-sm-head-hidden dt-body-right',
className: 'dt-sm-head-hidden dt-text-right',
defaultContent: ''
},
]
@ -220,6 +222,7 @@ jQuery(function($){
processing: true,
serverSide: false,
stateSave: true,
pageLength: pagination_size,
dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" +
"tr" +
"<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
@ -258,7 +261,7 @@ jQuery(function($){
defaultContent: '',
render: function (data, type) {
if(data == 1) return '<i class="bi bi-check-lg"></i>';
else return '<i class="bi bi-x-lg"></i>'
else return '<i class="bi bi-x-lg"></i>';
}
},
{
@ -267,14 +270,14 @@ jQuery(function($){
defaultContent: '',
render: function (data, type) {
if(data == 1) return '<i class="bi bi-check-lg"></i>';
else return '<i class="bi bi-x-lg"></i>'
else return '<i class="bi bi-x-lg"></i>';
}
},
{
title: lang.action,
data: 'action',
defaultContent: '',
className: 'text-md-end dt-sm-head-hidden dt-body-right'
className: 'dt-sm-head-hidden dt-text-right'
},
]
});
@ -291,6 +294,7 @@ jQuery(function($){
processing: true,
serverSide: false,
stateSave: true,
pageLength: pagination_size,
dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" +
"tr" +
"<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
@ -339,7 +343,7 @@ jQuery(function($){
{
title: lang.action,
data: 'action',
className: 'text-md-end dt-sm-head-hidden dt-body-right',
className: 'dt-sm-head-hidden dt-text-right',
defaultContent: ''
},
]
@ -357,6 +361,7 @@ jQuery(function($){
processing: true,
serverSide: false,
stateSave: true,
pageLength: pagination_size,
dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" +
"tr" +
"<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
@ -410,13 +415,13 @@ jQuery(function($){
defaultContent: '',
render: function (data, type) {
if(data == 1) return '<i class="bi bi-check-lg"></i>';
else return '<i class="bi bi-x-lg"></i>'
else return '<i class="bi bi-x-lg"></i>';
}
},
{
title: lang.action,
data: 'action',
className: 'text-md-end dt-sm-head-hidden dt-body-right',
className: 'dt-sm-head-hidden dt-text-right',
defaultContent: ''
},
]
@ -434,6 +439,7 @@ jQuery(function($){
processing: true,
serverSide: false,
stateSave: true,
pageLength: pagination_size,
dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" +
"tr" +
"<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
@ -487,13 +493,13 @@ jQuery(function($){
defaultContent: '',
render: function (data, type) {
if(data == 1) return '<i class="bi bi-check-lg"></i>';
else return '<i class="bi bi-x-lg"></i>'
else return '<i class="bi bi-x-lg"></i>';
}
},
{
title: lang.action,
data: 'action',
className: 'text-md-end dt-sm-head-hidden dt-body-right',
className: 'dt-sm-head-hidden dt-text-right',
defaultContent: ''
},
]

View File

@ -34,7 +34,7 @@ $(document).ready(function() {
});
// set update loop container list
containersToUpdate = {}
containersToUpdate = {};
// set default ChartJs Font Color
Chart.defaults.color = '#999';
// create host cpu and mem charts
@ -44,8 +44,7 @@ $(document).ready(function() {
check_update(mailcow_info.version_tag, mailcow_info.project_url);
}
$("#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;
showVersionModal("Version " + mailcow_info.version_tag, mailcow_info.version_tag);
@ -123,6 +122,7 @@ jQuery(function($){
processing: true,
serverSide: false,
stateSave: true,
pageLength: log_pagination_size,
dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" +
"tr" +
"<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
@ -192,6 +192,7 @@ jQuery(function($){
processing: true,
serverSide: false,
stateSave: true,
pageLength: log_pagination_size,
dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" +
"tr" +
"<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
@ -246,6 +247,7 @@ jQuery(function($){
processing: true,
serverSide: false,
stateSave: true,
pageLength: log_pagination_size,
dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" +
"tr" +
"<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
@ -304,6 +306,7 @@ jQuery(function($){
processing: true,
serverSide: false,
stateSave: true,
pageLength: log_pagination_size,
dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" +
"tr" +
"<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
@ -369,6 +372,7 @@ jQuery(function($){
processing: true,
serverSide: false,
stateSave: true,
pageLength: log_pagination_size,
dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" +
"tr" +
"<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
@ -472,6 +476,7 @@ jQuery(function($){
processing: true,
serverSide: false,
stateSave: true,
pageLength: log_pagination_size,
dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" +
"tr" +
"<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
@ -555,6 +560,7 @@ jQuery(function($){
processing: true,
serverSide: false,
stateSave: true,
pageLength: log_pagination_size,
dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" +
"tr" +
"<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
@ -615,6 +621,7 @@ jQuery(function($){
processing: true,
serverSide: false,
stateSave: true,
pageLength: log_pagination_size,
dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" +
"tr" +
"<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
@ -664,6 +671,7 @@ jQuery(function($){
processing: true,
serverSide: false,
stateSave: true,
pageLength: log_pagination_size,
dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" +
"tr" +
"<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
@ -718,6 +726,7 @@ jQuery(function($){
processing: true,
serverSide: false,
stateSave: true,
pageLength: log_pagination_size,
dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" +
"tr" +
"<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
@ -772,6 +781,7 @@ jQuery(function($){
processing: true,
serverSide: false,
stateSave: true,
pageLength: log_pagination_size,
dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" +
"tr" +
"<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
@ -887,6 +897,7 @@ jQuery(function($){
processing: true,
serverSide: false,
stateSave: true,
pageLength: log_pagination_size,
dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" +
"tr" +
"<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
@ -998,31 +1009,31 @@ jQuery(function($){
item.rcpt = escapeHtml(item.rcpt_smtp.join(", "));
}
item.symbols = Object.keys(item.symbols).sort(function (a, b) {
if (item.symbols[a].score === 0) return 1
if (item.symbols[b].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 && 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) {
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) {
var sym = item.symbols[key];
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) {
sym.score_formatted = '(<span><b>' + sym.score + '</b></span>)'
sym.score_formatted = '(<span><b>' + sym.score + '</b></span>)';
}
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;
if (sym.options) {
str += ' [' + escapeHtml(sym.options.join(", ")) + "]";
}
return str
return str;
}).join('<br>\n');
item.subject = escapeHtml(item.subject);
var scan_time = item.time_real.toFixed(3);
@ -1155,14 +1166,14 @@ jQuery(function($){
}
});
}
return data
return data;
};
$('.add_log_lines').on('click', function (e) {
e.preventDefault();
var log_table= $(this).data("table")
var new_nrows = $(this).data("nrows")
var post_process = $(this).data("post-process")
var log_url = $(this).data("log-url")
var log_table= $(this).data("table");
var new_nrows = $(this).data("nrows");
var post_process = $(this).data("post-process");
var log_url = $(this).data("log-url");
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");
return;
@ -1220,7 +1231,6 @@ jQuery(function($){
onVisible("[id^=rspamd_donut]", () => rspamd_pie_graph());
// start polling host stats if tab is active
onVisible("[id^=tab-containers]", () => update_stats());
// start polling container stats if collapse is active

View File

@ -82,6 +82,7 @@ jQuery(function($){
processing: true,
serverSide: false,
stateSave: true,
pageLength: pagination_size,
dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" +
"tr" +
"<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
@ -142,6 +143,7 @@ jQuery(function($){
processing: true,
serverSide: false,
stateSave: true,
pageLength: pagination_size,
dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" +
"tr" +
"<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",

View File

@ -437,6 +437,7 @@ jQuery(function($){
processing: true,
serverSide: false,
stateSave: true,
pageLength: pagination_size,
dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" +
"tr" +
"<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
@ -612,7 +613,7 @@ jQuery(function($){
{
title: lang.action,
data: 'action',
className: 'dt-sm-head-hidden dt-data-w100 dtr-col-md',
className: 'dt-sm-head-hidden dt-data-w100 dtr-col-md dt-text-right',
responsivePriority: 5,
defaultContent: ''
},
@ -635,11 +636,12 @@ jQuery(function($){
processing: true,
serverSide: false,
stateSave: true,
pageLength: pagination_size,
dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" +
"tr" +
"<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
language: lang_datatables,
order:[[2, 'desc']],
order: [[2, 'desc']],
initComplete: function(){
hideTableExpandCollapseBtn('#tab-templates-domains', '#templates_domain_table');
},
@ -672,7 +674,7 @@ jQuery(function($){
'<a href="/edit/template/' + encodeURIComponent(item.id) + '" class="btn btn-xs btn-xs-half btn-secondary"><i class="bi bi-pencil-fill"></i> ' + lang.edit + '</a>' +
'</div>';
}
else{
else {
item.action = '<div class="btn-group">' +
'<a href="/edit/template/' + encodeURIComponent(item.id) + '" class="btn btn-xs btn-xs-half btn-secondary"><i class="bi bi-pencil-fill"></i> ' + lang.edit + '</a>' +
'<a href="#" data-action="delete_selected" data-id="single-template" data-api-url="delete/domain/template" data-item="' + encodeURIComponent(item.id) + '" class="btn btn-xs btn-xs-half btn-danger"><i class="bi bi-trash"></i> ' + lang.remove + '</a>' +
@ -821,7 +823,7 @@ jQuery(function($){
{
title: lang.action,
data: 'action',
className: 'dt-sm-head-hidden dt-data-w100 dtr-col-md',
className: 'dt-sm-head-hidden dt-data-w100 dtr-col-md dt-text-right',
responsivePriority: 6,
defaultContent: ''
},
@ -844,6 +846,7 @@ jQuery(function($){
processing: true,
serverSide: false,
stateSave: true,
pageLength: pagination_size,
dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" +
"tr" +
"<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
@ -923,9 +926,12 @@ jQuery(function($){
'<a href="#" data-action="delete_selected" data-id="single-mailbox" data-api-url="delete/mailbox" data-item="' + encodeURIComponent(item.username) + '" class="btn btn-xs btn-xs-half btn-danger"><i class="bi bi-trash"></i> ' + lang.remove + '</a>' +
'</div>';
}
item.in_use = '<div class="progress">' +
item.in_use = {
sortBy: item.percent_in_use,
value: '<div class="progress">' +
'<div class="progress-bar-mailbox progress-bar progress-bar-' + item.percent_class + '" role="progressbar" aria-valuenow="' + item.percent_in_use + '" aria-valuemin="0" aria-valuemax="100" ' +
'style="min-width:2em;width:' + item.percent_in_use + '%">' + item.percent_in_use + '%' + '</div></div>';
'style="min-width:2em;width:' + item.percent_in_use + '%">' + item.percent_in_use + '%' + '</div></div>'
};
item.username = escapeHtml(item.username);
if (Array.isArray(item.tags)){
@ -991,10 +997,11 @@ jQuery(function($){
},
{
title: lang.in_use,
data: 'in_use',
data: 'in_use.value',
defaultContent: '',
responsivePriority: 9,
className: 'dt-data-w100'
className: 'dt-data-w100',
orderData: 24
},
{
title: lang.fname,
@ -1092,14 +1099,19 @@ jQuery(function($){
{
title: lang.action,
data: 'action',
className: 'dt-sm-head-hidden dt-data-w100 dtr-col-md',
className: 'dt-sm-head-hidden dt-data-w100 dtr-col-md dt-text-right',
responsivePriority: 6,
defaultContent: ''
},
{
title: "",
data: 'quota.sortBy',
responsivePriority: 8,
defaultContent: '',
className: "d-none"
},
{
title: "",
data: 'in_use.sortBy',
defaultContent: '',
className: "d-none"
},
@ -1122,11 +1134,12 @@ jQuery(function($){
processing: true,
serverSide: false,
stateSave: true,
pageLength: pagination_size,
dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" +
"tr" +
"<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
language: lang_datatables,
order:[[2, 'desc']],
order: [[2, 'desc']],
initComplete: function(){
hideTableExpandCollapseBtn('#tab-templates-mbox', '#templates_mbox_table');
},
@ -1322,7 +1335,7 @@ jQuery(function($){
{
title: lang.action,
data: 'action',
className: 'dt-sm-head-hidden dt-data-w100 dtr-col-md',
className: 'dt-sm-head-hidden dt-data-w100 dtr-col-md dt-text-right',
responsivePriority: 6,
defaultContent: ''
},
@ -1345,6 +1358,7 @@ jQuery(function($){
processing: true,
serverSide: false,
stateSave: true,
pageLength: pagination_size,
dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" +
"tr" +
"<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
@ -1434,7 +1448,7 @@ jQuery(function($){
data: 'action',
responsivePriority: 5,
defaultContent: '',
className: 'dt-sm-head-hidden dt-data-w100 dtr-col-md dt-body-right'
className: 'dt-sm-head-hidden dt-data-w100 dtr-col-md dt-text-right'
},
]
});
@ -1448,14 +1462,14 @@ jQuery(function($){
// Domains
var optgroup = "<optgroup label='" + lang.domains + "'>";
$.each(data.domains, function(index, domain){
optgroup += "<option value='" + domain + "'>" + domain + "</option>"
optgroup += "<option value='" + domain + "'>" + domain + "</option>";
});
optgroup += "</optgroup>"
optgroup += "</optgroup>";
$('#bcc-local-dest').append(optgroup);
// Alias domains
var optgroup = "<optgroup label='" + lang.domain_aliases + "'>";
$.each(data.alias_domains, function(index, alias_domain){
optgroup += "<option value='" + alias_domain + "'>" + alias_domain + "</option>"
optgroup += "<option value='" + alias_domain + "'>" + alias_domain + "</option>";
});
optgroup += "</optgroup>"
$('#bcc-local-dest').append(optgroup);
@ -1463,9 +1477,9 @@ jQuery(function($){
$.each(data.mailboxes, function(mailbox, aliases){
var optgroup = "<optgroup label='" + mailbox + "'>";
$.each(aliases, function(index, alias){
optgroup += "<option value='" + alias + "'>" + alias + "</option>"
optgroup += "<option value='" + alias + "'>" + alias + "</option>";
});
optgroup += "</optgroup>"
optgroup += "</optgroup>";
$('#bcc-local-dest').append(optgroup);
});
// Finish
@ -1483,11 +1497,12 @@ jQuery(function($){
processing: true,
serverSide: false,
stateSave: true,
pageLength: pagination_size,
dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" +
"tr" +
"<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
language: lang_datatables,
order:[[2, 'desc']],
order: [[2, 'desc']],
initComplete: function(){
hideTableExpandCollapseBtn('#collapse-tab-bcc', '#bcc_table');
},
@ -1569,7 +1584,7 @@ jQuery(function($){
{
title: lang.action,
data: 'action',
className: 'dt-sm-head-hidden dt-data-w100 dtr-col-md dt-body-right',
className: 'dt-sm-head-hidden dt-data-w100 dtr-col-md dt-text-right',
responsivePriority: 5,
defaultContent: ''
},
@ -1592,11 +1607,12 @@ jQuery(function($){
processing: true,
serverSide: false,
stateSave: true,
pageLength: pagination_size,
dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" +
"tr" +
"<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
language: lang_datatables,
order:[[2, 'desc']],
order: [[2, 'desc']],
initComplete: function(){
hideTableExpandCollapseBtn('#collapse-tab-bcc-filters', '#recipient_map_table');
},
@ -1665,7 +1681,7 @@ jQuery(function($){
{
title: lang.action,
data: 'action',
className: 'dt-sm-head-hidden dt-data-w100 dtr-col-md dt-body-right',
className: 'dt-sm-head-hidden dt-data-w100 dtr-col-md dt-text-right',
responsivePriority: 5,
defaultContent: ''
},
@ -1688,11 +1704,12 @@ jQuery(function($){
processing: true,
serverSide: false,
stateSave: true,
pageLength: pagination_size,
dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" +
"tr" +
"<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
language: lang_datatables,
order:[[2, 'desc']],
order: [[2, 'desc']],
initComplete: function(){
hideTableExpandCollapseBtn('#tab-tls-policy', '#tls_policy_table');
},
@ -1771,7 +1788,7 @@ jQuery(function($){
{
title: lang.action,
data: 'action',
className: 'dt-sm-head-hidden dt-data-w100 dtr-col-md dt-body-right',
className: 'dt-sm-head-hidden dt-data-w100 dtr-col-md dt-text-right',
responsivePriority: 5,
defaultContent: ''
},
@ -1794,11 +1811,12 @@ jQuery(function($){
processing: true,
serverSide: false,
stateSave: true,
pageLength: pagination_size,
dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" +
"tr" +
"<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
language: lang_datatables,
order:[[2, 'desc']],
order: [[2, 'desc']],
initComplete: function(){
hideTableExpandCollapseBtn('#tab-mbox-aliases', '#alias_table');
},
@ -1924,7 +1942,7 @@ jQuery(function($){
{
title: lang.action,
data: 'action',
className: 'dt-sm-head-hidden dt-data-w100 dtr-col-md dt-body-right',
className: 'dt-sm-head-hidden dt-data-w100 dtr-col-md dt-text-right',
responsivePriority: 5,
defaultContent: ''
},
@ -1947,6 +1965,7 @@ jQuery(function($){
processing: true,
serverSide: false,
stateSave: true,
pageLength: pagination_size,
dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" +
"tr" +
"<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
@ -2018,7 +2037,7 @@ jQuery(function($){
{
title: lang.action,
data: 'action',
className: 'dt-sm-head-hidden dt-data-w100 dtr-col-md dt-body-right',
className: 'dt-sm-head-hidden dt-data-w100 dtr-col-md dt-text-right',
responsivePriority: 5,
defaultContent: ''
},
@ -2041,11 +2060,12 @@ jQuery(function($){
processing: true,
serverSide: false,
stateSave: true,
pageLength: pagination_size,
dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" +
"tr" +
"<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
language: lang_datatables,
order:[[2, 'desc']],
order: [[2, 'desc']],
initComplete: function(){
hideTableExpandCollapseBtn('#tab-syncjobs', '#sync_job_table');
},
@ -2170,7 +2190,7 @@ jQuery(function($){
{
title: lang.action,
data: 'action',
className: 'dt-sm-head-hidden dt-data-w100 dtr-col-md dt-body-right',
className: 'dt-sm-head-hidden dt-data-w100 dtr-col-md dt-text-right',
responsivePriority: 5,
defaultContent: ''
},
@ -2194,11 +2214,12 @@ jQuery(function($){
processing: true,
serverSide: false,
stateSave: true,
pageLength: pagination_size,
dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" +
"tr" +
"<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
language: lang_datatables,
order:[[2, 'desc']],
order: [[2, 'desc']],
initComplete: function(){
hideTableExpandCollapseBtn('#tab-filters', '#filter_table');
},
@ -2280,7 +2301,7 @@ jQuery(function($){
{
title: lang.action,
data: 'action',
className: 'dt-sm-head-hidden dt-data-w100 dtr-col-md dt-body-right',
className: 'dt-sm-head-hidden dt-data-w100 dtr-col-md dt-text-right',
responsivePriority: 5,
defaultContent: ''
},

View File

@ -25,24 +25,24 @@ jQuery(function($){
}
if (typeof data.symbols !== 'undefined') {
data.symbols.sort(function (a, b) {
if (a.score === 0) return 1
if (b.score === 0) return -1
if (a.score === 0) return 1;
if (b.score === 0) return -1;
if (b.score < 0 && a.score < 0) {
return a.score - b.score
return a.score - b.score;
}
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) {
var highlightClass = ''
if (value.score > 0) highlightClass = 'negative'
else if (value.score < 0) highlightClass = 'positive'
else highlightClass = 'neutral'
var highlightClass = '';
if (value.score > 0) highlightClass = 'negative';
else if (value.score < 0) highlightClass = 'positive';
else highlightClass = 'neutral';
$('#qid_detail_symbols').append('<span data-bs-toggle="tooltip" class="rspamd-symbol ' + highlightClass + '" title="' + (value.options ? value.options.join(', ') : '') + '">' + value.name + ' (<span class="score">' + value.score + '</span>)</span>');
});
$('[data-bs-toggle="tooltip"]').tooltip()
$('[data-bs-toggle="tooltip"]').tooltip();
}
if (typeof data.score !== 'undefined' && typeof data.action !== 'undefined') {
if (data.action === "add header") {

View File

@ -18,6 +18,17 @@ jQuery(function($){
processing: true,
serverSide: false,
stateSave: true,
pageLength: pagination_size,
order: [[2, 'desc']],
lengthMenu: [
[10, 25, 50, 100, -1],
[10, 25, 50, 100, 'all']
],
pagingType: 'first_last_numbers',
aColumns: [
{ sWidth: '8.25%' },
{ sClass: 'classDataTable' }
],
dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" +
"tr" +
"<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
@ -101,6 +112,7 @@ jQuery(function($){
{
title: lang.sender,
data: 'sender',
className: 'senders-mw220',
defaultContent: ''
},
{
@ -151,7 +163,7 @@ jQuery(function($){
{
title: lang.action,
data: 'action',
className: 'text-md-end dt-sm-head-hidden dt-body-right',
className: 'dt-text-right dt-sm-head-hidden',
defaultContent: ''
},
]
@ -193,24 +205,24 @@ jQuery(function($){
$('#qid_detail_fuzzy').html('');
if (typeof data.symbols !== 'undefined') {
data.symbols.sort(function (a, b) {
if (a.score === 0) return 1
if (b.score === 0) return -1
if (a.score === 0) return 1;
if (b.score === 0) return -1;
if (b.score < 0 && a.score < 0) {
return a.score - b.score
return a.score - b.score;
}
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) {
var highlightClass = ''
if (value.score > 0) highlightClass = 'negative'
else if (value.score < 0) highlightClass = 'positive'
else highlightClass = 'neutral'
var highlightClass = '';
if (value.score > 0) highlightClass = 'negative';
else if (value.score < 0) highlightClass = 'positive';
else highlightClass = 'neutral';
$('#qid_detail_symbols').append('<span data-bs-toggle="tooltip" class="rspamd-symbol ' + highlightClass + '" title="' + (value.options ? value.options.join(', ') : '') + '">' + value.name + ' (<span class="score">' + value.score + '</span>)</span>');
});
$('[data-bs-toggle="tooltip"]').tooltip()
$('[data-bs-toggle="tooltip"]').tooltip();
}
if (typeof data.fuzzy_hashes === 'object' && data.fuzzy_hashes !== null && data.fuzzy_hashes.length !== 0) {
$.each(data.fuzzy_hashes, function (index, value) {
@ -276,7 +288,6 @@ jQuery(function($){
// Initial table drawings
draw_quarantine_table();
function hideTableExpandCollapseBtn(table){
if ($(table).hasClass('collapsed'))
$(".table_collapse_option").show();

View File

@ -21,7 +21,6 @@ jQuery(function($){
url: '/api/v1/get/postcat/' + button.data('queue-id'),
dataType: 'text',
complete: function (data) {
console.log(data);
$('#queue_msg_content').text(data.responseText);
}
});
@ -39,6 +38,7 @@ jQuery(function($){
processing: true,
serverSide: false,
stateSave: true,
pageLength: pagination_size,
dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" +
"tr" +
"<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
@ -54,7 +54,7 @@ jQuery(function($){
});
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>' +
'<a href="#" data-bs-toggle="modal" data-bs-target="#showQueuedMsg" data-queue-id="' + encodeURI(item.queue_id) + '" class="btn btn-xs btn-secondary">' + lang.show_message + '</a>' +
'</div>';
});
return data;
@ -116,7 +116,7 @@ jQuery(function($){
{
title: lang_admin.action,
data: 'action',
className: 'text-md-end dt-sm-head-hidden dt-body-right',
className: 'dt-sm-head-hidden dt-text-right',
defaultContent: ''
},
]

View File

@ -139,6 +139,7 @@ jQuery(function($){
processing: true,
serverSide: false,
stateSave: true,
pageLength: pagination_size,
dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" +
"tr" +
"<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
@ -207,7 +208,7 @@ jQuery(function($){
{
title: lang.action,
data: 'action',
className: 'text-md-end dt-sm-head-hidden dt-body-right',
className: 'dt-sm-head-hidden dt-text-right',
defaultContent: ''
}
]
@ -225,6 +226,7 @@ jQuery(function($){
processing: true,
serverSide: false,
stateSave: true,
pageLength: pagination_size,
dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" +
"tr" +
"<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
@ -361,7 +363,7 @@ jQuery(function($){
{
title: lang.action,
data: 'action',
className: 'text-md-end dt-sm-head-hidden dt-body-right',
className: 'dt-sm-head-hidden dt-text-right',
defaultContent: '',
responsivePriority: 5
}
@ -380,6 +382,7 @@ jQuery(function($){
processing: true,
serverSide: false,
stateSave: true,
pageLength: pagination_size,
dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" +
"tr" +
"<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
@ -457,7 +460,7 @@ jQuery(function($){
{
title: lang.action,
data: 'action',
className: 'text-md-end dt-sm-head-hidden dt-body-right',
className: 'dt-sm-head-hidden dt-text-right',
defaultContent: ''
}
]
@ -475,6 +478,7 @@ jQuery(function($){
processing: true,
serverSide: false,
stateSave: true,
pageLength: pagination_size,
dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" +
"tr" +
"<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
@ -545,6 +549,7 @@ jQuery(function($){
processing: true,
serverSide: false,
stateSave: true,
pageLength: pagination_size,
dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" +
"tr" +
"<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",

View File

@ -288,6 +288,18 @@ if (isset($_GET['query'])) {
case "domain-admin":
process_add_return(domain_admin('add', $attr));
break;
case "sso":
switch ($object) {
case "domain-admin":
$data = domain_admin_sso('issue', $attr);
if($data) {
echo json_encode($data);
exit(0);
}
process_add_return($data);
break;
}
break;
case "admin":
process_add_return(admin('add', $attr));
break;

View File

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

View File

@ -1,6 +1,6 @@
{
"acl": {
"alias_domains": "Tilføj kældenavn domæner",
"alias_domains": "Tilføj domænealias",
"app_passwds": "Administrer app-adgangskoder",
"bcc_maps": "BCC kort",
"delimiter_action": "Afgrænsning handling",
@ -22,9 +22,9 @@
"spam_alias": "Midlertidige aliasser",
"spam_policy": "Sortliste / hvidliste",
"spam_score": "Spam-score",
"syncjobs": "Synkroniser job",
"syncjobs": "Synkroniserings job",
"tls_policy": "TLS politik",
"unlimited_quota": "Ubegrænset quote for mailbokse",
"unlimited_quota": "Ubegrænset plads for mailbokse",
"domain_desc": "Skift domæne beskrivelse"
},
"add": {
@ -33,7 +33,7 @@
"add": "Tilføj",
"add_domain_only": "Tilføj kun domæne",
"add_domain_restart": "Tilføj domæne og genstart SOGo",
"alias_address": "Alias adresse (r)",
"alias_address": "Alias adresse(r)",
"alias_address_info": "<small>Fuld e-mail-adresse eller @ eksempel.com for at fange alle beskeder til et domæne (kommasepareret). <b> kun mailcow-domæner</b>.</small>",
"alias_domain": "Alias-domæne",
"alias_domain_info": "<small>Kun gyldige domænenavne (kommasepareret).</small>",
@ -586,7 +586,7 @@
},
"login": {
"delayed": "Login blev forsinket med% s sekunder.",
"fido2_webauthn": "FIDO2/WebAuthn",
"fido2_webauthn": "FIDO2/WebAuthn Login",
"login": "Login",
"mobileconfig_info": "Log ind som postkassebruger for at downloade den anmodede Apple-forbindelsesprofil.",
"other_logins": "Nøgle login",

View File

@ -339,7 +339,8 @@
"oauth2_add_client": "Füge OAuth2 Client hinzu",
"api_read_only": "Schreibgeschützter Zugriff",
"api_read_write": "Lese-Schreib-Zugriff",
"oauth2_apps": "OAuth2 Apps"
"oauth2_apps": "OAuth2 Apps",
"queue_unban": "entsperren"
},
"danger": {
"access_denied": "Zugriff verweigert oder unvollständige/ungültige Daten",
@ -366,7 +367,7 @@
"domain_not_empty": "Domain %s ist nicht leer",
"domain_not_found": "Domain %s nicht gefunden",
"domain_quota_m_in_use": "Domain-Speicherplatzlimit muss größer oder gleich %d MiB sein",
"extended_sender_acl_denied": "Keine Rechte zum setzen von externen Absenderadressen",
"extended_sender_acl_denied": "Keine Rechte zum Setzen von externen Absenderadressen",
"extra_acl_invalid": "Externe Absenderadresse \"%s\" ist ungültig",
"extra_acl_invalid_domain": "Externe Absenderadresse \"%s\" verwendet eine ungültige Domain",
"fido2_verification_failed": "FIDO2-Verifizierung fehlgeschlagen: %s",
@ -454,17 +455,23 @@
"totp_verification_failed": "TOTP-Verifizierung fehlgeschlagen",
"transport_dest_exists": "Transport-Maps-Ziel \"%s\" existiert bereits",
"webauthn_verification_failed": "WebAuthn-Verifizierung fehlgeschlagen: %s",
"webauthn_authenticator_failed": "Der ausgewählte Authenticator wurde nicht gefunden",
"webauthn_publickey_failed": "Zu dem ausgewählten Authenticator wurde kein Publickey hinterlegt",
"webauthn_username_failed": "Der ausgewählte Authenticator gehört zu einem anderen Konto",
"unknown": "Ein unbekannter Fehler trat auf",
"unknown_tfa_method": "Unbekannte TFA-Methode",
"unlimited_quota_acl": "Unendliche Quota untersagt durch ACL",
"username_invalid": "Benutzername %s kann nicht verwendet werden",
"validity_missing": "Bitte geben Sie eine Gültigkeitsdauer an",
"value_missing": "Bitte alle Felder ausfüllen",
"yotp_verification_failed": "Yubico OTP-Verifizierung fehlgeschlagen: %s"
"yotp_verification_failed": "Yubico OTP-Verifizierung fehlgeschlagen: %s",
"template_exists": "Vorlage %s existiert bereits",
"template_id_invalid": "Vorlagen-ID %s ungültig",
"template_name_invalid": "Name der Vorlage ungültig"
},
"datatables": {
"collapse_all": "Alle Einklappen",
"decimal": "",
"decimal": ",",
"emptyTable": "Keine Daten in der Tabelle vorhanden",
"expand_all": "Alle Ausklappen",
"info": "_START_ bis _END_ von _TOTAL_ Einträgen",
@ -498,7 +505,7 @@
"current_time": "Systemzeit",
"disk_usage": "Festplattennutzung",
"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",
"history_all_servers": "History (alle Server)",
"in_memory_logs": "In-memory Logs",
@ -651,7 +658,8 @@
"title": "Objekt bearbeiten",
"unchanged_if_empty": "Unverändert, wenn leer",
"username": "Benutzername",
"validate_save": "Validieren und speichern"
"validate_save": "Validieren und speichern",
"pushover_sound": "Ton"
},
"fido2": {
"confirm": "Bestätigen",
@ -692,7 +700,8 @@
"quarantine": "Quarantäne",
"restart_netfilter": "Netfilter neustarten",
"restart_sogo": "SOGo neustarten",
"user_settings": "Benutzereinstellungen"
"user_settings": "Benutzereinstellungen",
"mailcow_system": "System"
},
"info": {
"awaiting_tfa_confirmation": "Warte auf TFA-Verifizierung",
@ -701,7 +710,7 @@
},
"login": {
"delayed": "Login wurde zur Sicherheit um %s Sekunde/n verzögert.",
"fido2_webauthn": "FIDO2/WebAuthn",
"fido2_webauthn": "FIDO2/WebAuthn Login",
"login": "Anmelden",
"mobileconfig_info": "Bitte als Mailbox-Benutzer einloggen, um das Verbindungsprofil herunterzuladen.",
"other_logins": "Key Login",
@ -1236,7 +1245,8 @@
"syncjob_EXIT_CONNECTION_FAILURE": "Verbindungsproblem",
"syncjob_EXIT_TLS_FAILURE": "Problem mit verschlüsselter Verbindung",
"syncjob_EXIT_AUTHENTICATION_FAILURE": "Authentifizierungsproblem",
"syncjob_EXIT_AUTHENTICATION_FAILURE_USER1": "Falscher Benutzername oder Passwort"
"syncjob_EXIT_AUTHENTICATION_FAILURE_USER1": "Falscher Benutzername oder Passwort",
"pushover_sound": "Ton"
},
"warning": {
"cannot_delete_self": "Kann derzeit eingeloggten Benutzer nicht entfernen",

View File

@ -458,6 +458,9 @@
"totp_verification_failed": "TOTP verification failed",
"transport_dest_exists": "Transport destination \"%s\" exists",
"webauthn_verification_failed": "WebAuthn verification failed: %s",
"webauthn_authenticator_failed": "The selected authenticator was not found",
"webauthn_publickey_failed": "No public key was stored for the selected authenticator",
"webauthn_username_failed": "The selected authenticator belongs to another account",
"unknown": "An unknown error occurred",
"unknown_tfa_method": "Unknown TFA method",
"unlimited_quota_acl": "Unlimited quota prohibited by ACL",
@ -468,7 +471,7 @@
},
"datatables": {
"collapse_all": "Collapse All",
"decimal": "",
"decimal": ".",
"emptyTable": "No data available in table",
"expand_all": "Expand All",
"info": "Showing _START_ to _END_ of _TOTAL_ entries",
@ -707,7 +710,7 @@
},
"login": {
"delayed": "Login was delayed by %s seconds.",
"fido2_webauthn": "FIDO2/WebAuthn",
"fido2_webauthn": "FIDO2/WebAuthn Login",
"login": "Login",
"mobileconfig_info": "Please login as mailbox user to download the requested Apple connection profile.",
"other_logins": "Key login",

View File

@ -612,7 +612,7 @@
},
"login": {
"delayed": "La connexion a été retardée de %s secondes.",
"fido2_webauthn": "FIDO2/WebAuthn",
"fido2_webauthn": "FIDO2/WebAuthn Login",
"login": "Connexion",
"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",

View File

@ -674,7 +674,7 @@
},
"login": {
"delayed": "L'accesso è stato ritardato di %s secondi.",
"fido2_webauthn": "FIDO2/WebAuthn",
"fido2_webauthn": "FIDO2/WebAuthn Login",
"login": "Login",
"mobileconfig_info": "Please login as mailbox user to download the requested Apple connection profile.",
"other_logins": "Key login",

View File

@ -3,7 +3,8 @@
"bcc_maps": "BCC kartes",
"filters": "Filtri",
"recipient_maps": "Saņēmēja kartes",
"syncjobs": "Sinhronizācijas uzdevumi"
"syncjobs": "Sinhronizācijas uzdevumi",
"spam_score": "Mēstules novērtējums"
},
"add": {
"activate_filter_warn": "Visi pārējie filtri tiks deaktivizēti, kad aktīvs ir atzīmēts.",
@ -104,10 +105,10 @@
"host": "Hosts",
"import": "Importēt",
"import_private_key": "Importēt privātu atslēgu",
"in_use_by": "Tiek lietots ar",
"in_use_by": "Izmanto",
"inactive": "Neaktīvs",
"link": "Saite",
"loading": "Lūdzu uzgaidiet...",
"loading": "Lūgums uzgaidīt...",
"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",
"merged_vars_hint": "Pelēkās rindas tika apvienotas <code>vars.(local.)inc.php</code> un nevar tikt modificētas.",
@ -144,7 +145,10 @@
"ui_texts": "UI etiķetes un teksti",
"unchanged_if_empty": "Ja nav veiktas izmaiņas, atstājiet tukšu",
"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": {
"access_denied": "Piekļuve liegta, vai nepareizi dati",
@ -170,7 +174,7 @@
"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_spam_alias": "%s ir jau zināms spam alias",
"last_key": "Pēdējā atslēga nevar būt dzēsta",
"last_key": "Pēdējo atslēgu nevar izdzēst, tā vietā jāatspējo divpakāpju pārbaude.",
"login_failed": "Ielogošanās neveiksmīga",
"mailbox_invalid": "Pastkastes vārds ir nederīgs",
"mailbox_quota_exceeded": "Kvota pārsniedz domēna limitu (max. %d MiB)",
@ -262,7 +266,8 @@
"title": "Labot priekšmetu",
"unchanged_if_empty": "Ja neizmainīts atstājiet tukšu",
"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": {
"cancel": "Atcelt",
@ -314,21 +319,21 @@
"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_local_dest": "Vietējais galamērķis",
"bcc_map_type": "BCC tips",
"bcc_map_type": "BCC veids",
"bcc_maps": "BCC kartes",
"bcc_rcpt_map": "saņēmēja karte",
"bcc_sender_map": "Sūtītāja karte",
"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_type": "BCC tips",
"deactivate": "Deaktivizēt",
"deactivate": "Deaktivēt",
"description": "Apraksts",
"dkim_key_length": "DKIM atslēgas garums (bits)",
"domain": "Domēns",
"domain_admins": "Domēna administratori",
"domain_aliases": "Domēna aliases",
"domain_quota": "Kvota",
"domain_quota_total": "Kopējā domēna kvota",
"domain_quota_total": "Kopējais domēna ierobežojums",
"domains": "Domēns",
"edit": "Labot",
"empty": "Nav rezultātu",
@ -341,7 +346,7 @@
"inactive": "Neaktīvs",
"kind": "Veids",
"last_run": "Pēdējā norise",
"last_run_reset": "Nākamais grafiks",
"last_run_reset": "Ievietot sarakstā kā nākamo",
"mailbox_quota": "Maks. pastkastes izmērs",
"mailboxes": "Pastkaste",
"max_aliases": "Maks. iespejamās aliases",
@ -374,7 +379,13 @@
"tls_enforce_out": "Piespiest TLS izejošajiem",
"toggle_all": "Pārslēgt visu",
"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": {
"action": "Darbības",
@ -547,5 +558,14 @@
"waiting": "Waiting",
"week": "Nedēļa",
"weeks": "Nedēļas"
},
"datatables": {
"paginate": {
"first": "Pirmā",
"last": "Pēdējā"
}
},
"debug": {
"last_modified": "Pēdējoreiz mainīts"
}
}

View File

@ -598,7 +598,7 @@
},
"login": {
"delayed": "Aanmelding vertraagd met %s seconden.",
"fido2_webauthn": "FIDO2/WebAuthn",
"fido2_webauthn": "FIDO2/WebAuthn Login",
"login": "Aanmelden",
"mobileconfig_info": "Log in als mailboxgebruiker om het Apple-verbindingsprofiel te downloaden.",
"other_logins": "Meld aan met key",

View File

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

View File

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

View File

@ -106,7 +106,8 @@
"username": "Používateľské meno",
"validate": "Overiť",
"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": {
"access": "Prístup",
@ -656,7 +657,7 @@
},
"login": {
"delayed": "Prihlásenie bolo oneskorené o %s sekúnd.",
"fido2_webauthn": "FIDO2/WebAuthn",
"fido2_webauthn": "FIDO2/WebAuthn Login",
"login": "Prihlásenie",
"mobileconfig_info": "Prosím, prihláste sa ako mailový používateľ pre stiahnutie požadovaného Apple profilu.",
"other_logins": "Prihlásenie kľúčom",

View File

@ -618,7 +618,7 @@
},
"login": {
"delayed": "Av säkerhetsskäl har inloggning inaktiverats i %s sekunder.",
"fido2_webauthn": "FIDO2/WebAuthn",
"fido2_webauthn": "FIDO2/WebAuthn Login",
"login": "Logga in",
"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",

View File

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

View File

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

View File

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

View File

@ -66,7 +66,7 @@ var lang = {{ lang_admin|raw }};
var lang_datatables = {{ lang_datatables|raw }};
var admin_username = '{{ mailcow_cc_username }}';
var csrf_token = '{{ csrf_token }}';
var pagination_size = '{{ pagination_size }}';
var log_pagination_size = '{{ log_pagination_size }}';
var pagination_size = Math.trunc('{{ pagination_size }}');
var log_pagination_size = Math.trunc('{{ log_pagination_size }}');
</script>
{% endblock %}

View File

@ -41,7 +41,7 @@
</div>
<div class="col-sm-12 col-md-8">
<div class="table-responsive" style="margin-top: 10px;">
<table class="table table-striped table-condensed">
<table class="table table-striped table-condensed w-100">
<tbody>
<tr>
<td>Hostname</td>
@ -627,6 +627,6 @@
var lang_debug = {{ lang_debug|raw }};
var lang_datatables = {{ lang_datatables|raw }};
var csrf_token = '{{ csrf_token }}';
var log_pagination_size = '{{ log_pagination_size }}';
var log_pagination_size = Math.trunc('{{ log_pagination_size }}');
</script>
{% endblock %}

View File

@ -46,7 +46,7 @@
<div class="col-sm-3 col-5 text-end">{{ lang.fido2.known_ids }}:</div>
<div class="col-sm-9 col-7">
<div class="table-responsive">
<table class="table table-striped table-hover table-condensed" id="fido2_keys">
<table class="table table-striped table-hover table-condensed w-100" id="fido2_keys">
<tr>
<th>ID</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_datatables = {{ lang_datatables|raw }};
var csrf_token = '{{ csrf_token }}';
var pagination_size = '{{ pagination_size }}';
var pagination_size = Math.trunc('{{ pagination_size }}');
var table_for_domain = '{{ domain }}';
</script>
{% endblock %}

View File

@ -38,15 +38,8 @@
</div>
</div>
<div class="d-flex mt-4" style="position: relative">
<div class="btn-group">
<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>
<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>
{% 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">
<span class="flag-icon flag-icon-{{ mailcow_locale[-2:] }}"></span>

View File

@ -58,7 +58,7 @@
var lang_rl = {{ lang_rl|raw }};
var lang_datatables = {{ lang_datatables|raw }};
var csrf_token = '{{ csrf_token }}';
var pagination_size = '{{ pagination_size }}';
var pagination_size = Math.trunc('{{ pagination_size }}');
var role = '{{ role }}';
var is_dual = {{ is_dual }};
var ALLOW_ADMIN_EMAIL_LOGIN = {{ allow_admin_email_login }};

View File

@ -37,7 +37,7 @@
</p>
{% endif %}
</p>
<table id="quarantinetable" class="table table-striped"></table>
<table id="quarantinetable" class="table table-striped w-100"></table>
<div class="mass-actions-quarantine mt-4">
<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>
@ -66,7 +66,7 @@ var acl = '{{ acl_json|raw }}';
var lang = {{ lang_quarantine|raw }};
var lang_datatables = {{ lang_datatables|raw }};
var csrf_token = '{{ csrf_token }}';
var pagination_size = '{{ pagination_size }}';
var pagination_size = Math.trunc('{{ pagination_size }}');
var role = '{{ role }}';
</script>
{% endblock %}

View File

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

View File

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

View File

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

View File

@ -64,7 +64,7 @@ services:
- redis
clamd-mailcow:
image: mailcow/clamd:1.60
image: mailcow/clamd:1.61
restart: always
depends_on:
- unbound-mailcow
@ -116,7 +116,7 @@ services:
- rspamd
php-fpm-mailcow:
image: mailcow/phpfpm:1.81
image: mailcow/phpfpm:1.82
command: "php-fpm -d date.timezone=${TZ} -d expose_php=0"
depends_on:
- redis-mailcow
@ -181,7 +181,7 @@ services:
- phpfpm
sogo-mailcow:
image: mailcow/sogo:1.113
image: mailcow/sogo:1.115
environment:
- DBNAME=${DBNAME}
- DBUSER=${DBUSER}
@ -230,7 +230,7 @@ services:
- sogo
dovecot-mailcow:
image: mailcow/dovecot:1.21
image: mailcow/dovecot:1.22
depends_on:
- mysql-mailcow
dns:
@ -411,7 +411,7 @@ services:
acme-mailcow:
depends_on:
- nginx-mailcow
image: mailcow/acme:1.83
image: mailcow/acme:1.84
dns:
- ${IPV4_NETWORK:-172.22.1}.254
environment:
@ -449,7 +449,7 @@ services:
- acme
netfilter-mailcow:
image: mailcow/netfilter:1.50
image: mailcow/netfilter:1.51
stop_grace_period: 30s
depends_on:
- dovecot-mailcow
@ -538,11 +538,10 @@ services:
- watchdog
dockerapi-mailcow:
image: mailcow/dockerapi:2.0
image: mailcow/dockerapi:2.01
security_opt:
- label=disable
restart: always
oom_kill_disable: true
dns:
- ${IPV4_NETWORK:-172.22.1}.254
environment:

View File

@ -56,9 +56,8 @@ done
MAILCOW_DOCKER_COMPOSE=${MAILCOW_DOCKER_COMPOSE:-"docker-compose"}
if [[ "${CONTAINER_ENGINE}" == "docker" ]] && command -v docker compose > /dev/null 2>&1; then
version=$(docker compose version --short)
if [[ $version =~ ^2\.([0-9]+)\.([0-9]+) ]]; then
if [[ "${CONTAINER_ENGINE}" == "docker" ]] && docker compose > /dev/null 2>&1; then
if docker compose version --short | grep "^2." > /dev/null 2>&1; then
COMPOSE_VERSION=native
echo -e "\e[31mFound Docker Compose Plugin (native).\e[0m"
echo -e "\e[31mSetting the DOCKER_COMPOSE_VERSION Variable to native\e[0m"
@ -66,12 +65,12 @@ if [[ "${CONTAINER_ENGINE}" == "docker" ]] && command -v docker compose > /dev/n
echo -e "\e[33mNotice: You'll have to update this Compose Version via your Package Manager manually! \e[0m"
else
echo -e "\e[31mCannot find Docker Compose with a Version Higher than 2.X.X.\e[0m"
echo -e "\e[31mPlease update/install manually regarding to this doc site: https://mailcow.github.io/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
fi
elif command -v $MAILCOW_DOCKER_COMPOSE > /dev/null 2>&1; then
version=$($MAILCOW_DOCKER_COMPOSE version --short)
if [[ $version =~ ^2\.([0-9]+)\.([0-9]+) ]]; then
elif $MAILCOW_DOCKER_COMPOSE > /dev/null 2>&1; then
if ! [[ $(alias $MAILCOW_DOCKER_COMPOSE 2> /dev/null) ]] ; then
if $MAILCOW_DOCKER_COMPOSE version --short | grep "^2." > /dev/null 2>&1; then
COMPOSE_VERSION=standalone
echo -e "\e[31mFound Docker Compose Standalone.\e[0m"
echo -e "\e[31mSetting the DOCKER_COMPOSE_VERSION Variable to standalone\e[0m"
@ -82,9 +81,11 @@ elif command -v $MAILCOW_DOCKER_COMPOSE > /dev/null 2>&1; then
echo -e "\e[31mPlease update/install manually regarding to this doc site: https://mailcow.github.io/mailcow-dockerized-docs/i_u_m/i_u_m_install/\e[0m"
exit 1
fi
fi
else
echo -e "\e[31mCannot find Docker Compose.\e[0m"
echo -e "\e[31mPlease install it manually regarding to this doc site: https://mailcow.github.io/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
fi
@ -520,6 +521,15 @@ case ${git_branch} in
mailcow_last_git_version=""
;;
esac
# if [ ${git_branch} == "master" ]; then
# mailcow_git_version=$(git describe --tags `git rev-list --tags --max-count=1`)
# elif [ ${git_branch} == "nightly" ]; then
# mailcow_git_version=$(git rev-parse --short $(git rev-parse @{upstream}))
# mailcow_last_git_version=""
# else
# mailcow_git_version=$(git rev-parse --short HEAD)
# mailcow_last_git_version=""
# fi
if [[ $SKIP_BRANCH != "y" ]]; then
mailcow_git_commit=$(git rev-parse origin/${git_branch})

View File

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

View File

@ -1,17 +1,25 @@
#!/usr/bin/env bash
# renovate: datasource=github-releases depName=nextcloud/server versioning=semver extractVersion=^v(?<version>.*)$
NEXTCLOUD_VERSION=25.0.2
NEXTCLOUD_VERSION=25.0.4
for bin in curl dirmngr; do
if [[ -z $(which ${bin}) ]]; then echo "Cannot find ${bin}, exiting..."; exit 1; fi
echo -ne "Checking prerequisites..."
sleep 1
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
echo -ne "\r\033[32mFound all prerequisites! Continuing...\033[0m\n"
[[ -z ${1} ]] && NC_HELP=y
while [ "$1" != '' ]; do
if [[ $# -ne 1 ]]; then
echo -e "\033[31mPlease use only one parameter at the same time!\033[0m" >&2
exit 2
fi
case "${1}" in
-p|--purge) NC_PURGE=y && shift;;
-i|--install) NC_INSTALL=y && shift;;
-u|--update) NC_UPDATE=y && shift;;
-r|--resetpw) NC_RESETPW=y && shift;;
-h|--help) NC_HELP=y && shift;;
*) echo "Unknown parameter: ${1}" && shift;;
@ -22,13 +30,11 @@ if [[ ${NC_HELP} == "y" ]]; then
printf 'Usage:\n\n'
printf ' -p|--purge\n Purge Nextcloud\n'
printf ' -i|--install\n Install Nextcloud\n'
printf ' -u|--update\n Update Nextcloud\n'
printf ' -r|--resetpw\n Reset password\n\n'
exit 0
fi
[[ ${NC_PURGE} == "y" ]] && [[ ${NC_INSTALL} == "y" ]] && { echo "Cannot use -p and -i at the same time!"; exit 1; }
[[ ${NC_PURGE} == "y" ]] && [[ ${NC_RESETPW} == "y" ]] && { echo "Cannot use -p and -r at the same time!"; exit 1; }
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
cd ${SCRIPT_DIR}/../
source mailcow.conf
@ -41,8 +47,27 @@ if [[ ${NC_PURGE} == "y" ]]; then
exit 1
fi
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
echo -e "\033[32mFound seperate Nextcloud database (newer scheme)!\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 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
echo -e "\033[32mFound Nextcloud (oc) tables inside of mailcow database (old scheme)!\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 "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)"
"$(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
echo -e "\033[32mFound Nextcloud (nc) tables inside of mailcow database (old scheme)!\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 "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
echo -e "\033[31mError: No Nextcloud databases/tables found!"
echo -e "\033[33mNot purging anything...\033[0m"
exit 1
fi
docker exec -it $(docker ps -f name=redis-mailcow -q) /bin/sh -c ' cat <<EOF | redis-cli
SELECT 10
FLUSHDB
@ -58,9 +83,10 @@ EOF
docker restart $(docker ps -aqf name=nginx-mailcow)
echo -e "\033[32mNextcloud has been uninstalled sucessfully!\033[0m"
elif [[ ${NC_UPDATE} == "y" ]]; then
exit;
read -r -p "Are you sure you want to update Nextcloud? [y/N] " response
read -r -p "Are you sure you want to update Nextcloud (with Nextclouds own updater)? [y/N] " response
response=${response,,}
if [[ ! "$response" =~ ^(yes|y)$ ]]; then
echo "OK, aborting."
@ -68,23 +94,14 @@ elif [[ ${NC_UPDATE} == "y" ]]; then
fi
if [ ! -f data/web/nextcloud/occ ]; then
echo "Nextcloud occ not found. Is Nextcloud installed?"
echo -e "\033[31mError: Nextcloud occ not found. Is Nextcloud installed?\033[0m"
exit 1
fi
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 "Nextcloud seems not to be installed."
exit 1
elif ! grep -q 'version: 20\.' <<<$(docker exec -it -u www-data $(docker ps -f name=php-fpm-mailcow -q) bash -c "/web/nextcloud/occ --no-warnings status"); then
echo "Cannot upgrade to new major version, please update manually."
exit 1
else
curl -L# -o nextcloud.tar.bz2 "https://download.nextcloud.com/server/releases/nextcloud-$NEXTCLOUD_VERSION.tar.bz2" || { echo "Failed to download Nextcloud archive."; exit 1; } \
&& tar -xjf nextcloud.tar.bz2 -C ./data/web/ \
&& rm nextcloud.tar.bz2 \
&& mkdir -p ./data/web/nextcloud/data \
&& chmod +x ./data/web/nextcloud/occ \
docker exec -it $(docker ps -f name=php-fpm-mailcow -q) bash -c "chown www-data:www-data -R /web/nextcloud" \
docker exec -it -u www-data $(docker ps -f name=php-fpm-mailcow -q) bash -c "/web/nextcloud/occ --no-warnings upgrade"
docker exec -it -u www-data $(docker ps -f name=php-fpm-mailcow -q) bash -c "php /web/nextcloud/updater/updater.phar"
fi
elif [[ ${NC_INSTALL} == "y" ]]; then
@ -97,25 +114,48 @@ elif [[ ${NC_INSTALL} == "y" ]]; then
[[ ! ${NC_CONT_FAIL,,} =~ ^(yes|y)$ ]] && { echo "Ok, exiting..."; exit 1; }
fi
ADMIN_NC_PASS=$(</dev/urandom tr -dc A-Za-z0-9 | head -c 28)
echo -e "\033[33mDownloading \033[34mNextcloud ${NEXTCLOUD_VERSION}\033[33m...\033[0m"
curl -L# -o nextcloud.tar.bz2 "https://download.nextcloud.com/server/releases/nextcloud-$NEXTCLOUD_VERSION.tar.bz2" || { echo "Failed to download Nextcloud archive."; exit 1; } \
&& tar -xjf nextcloud.tar.bz2 -C ./data/web/ \
&& rm nextcloud.tar.bz2 \
&& mkdir -p ./data/web/nextcloud/data \
&& chmod +x ./data/web/nextcloud/occ
echo -e "\033[33mCreating 'nextcloud' database...\033[0m"
NC_DBPASS=$(</dev/urandom tr -dc A-Za-z0-9 | head -c 28)
NC_DBUSER=nextcloud
NC_DBNAME=nextcloud
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};"
sleep 2
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}';"
sleep 2
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}'@'%';"
sleep 2
echo ""
echo -e "\033[33mInstalling Nextcloud...\033[0m"
ADMIN_NC_PASS=$(</dev/urandom tr -dc A-Za-z0-9 | head -c 28)
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"
sleep 2
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 \
--database mysql \
--database-host mysql \
--database-name ${DBNAME} \
--database-user ${DBUSER} \
--database-pass ${DBPASS} \
--database-name ${NC_DBNAME} \
--database-user ${NC_DBUSER} \
--database-pass ${NC_DBPASS} \
--admin-user admin \
--admin-pass ${ADMIN_NC_PASS} \
--data-dir /web/nextcloud/data
--data-dir /web/nextcloud/data > /dev/null 2>&1
echo -ne "\r[3/4] Setting custom parameters inside the Nextcloud config file"
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; \
/web/nextcloud/occ --no-warnings config:system:set redis port --value=6379 --type=integer; \
/web/nextcloud/occ --no-warnings config:system:set redis timeout --value=0.0 --type=integer; \
@ -141,13 +181,28 @@ 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 class --value=OC_User_IMAP; \
echo -e "\r[4/4] Enabling Nginx Configuration"
cp ./data/assets/nextcloud/nextcloud.conf ./data/conf/nginx/
sed -i "s/NC_SUBD/${NC_SUBD}/g" ./data/conf/nginx/nextcloud.conf
sleep 2
echo "Restarting Nginx..."
echo ""
echo -e "\033[33mFinalizing installation...\033[0m"
docker restart $(docker ps -aqf name=nginx-mailcow)
echo "Login as admin with password: ${ADMIN_NC_PASS}"
echo ""
echo "******************************************"
echo "* SAVE THESE CREDENTIALS *"
echo "* INSTALL DATE: $(date +%Y-%m-%d_%H-%M-%S) *"
echo "******************************************"
echo ""
echo -e "\033[36mDatabase name: ${NC_DBNAME}\033[0m"
echo -e "\033[36mDatabase user: ${NC_DBUSER}\033[0m"
echo -e "\033[36mDatabase password: ${NC_DBPASS}\033[0m"
echo ""
echo -e "\033[31mUI admin password: ${ADMIN_NC_PASS}\033[0m"
echo ""
elif [[ ${NC_RESETPW} == "y" ]]; then
printf 'You are about to set a new password for a Nextcloud user.\n\nDo not use this option if your Nextcloud is configured to use mailcow for authentication.\nSet a new password for the corresponding mailbox in mailcow, instead.\n\n'
@ -163,5 +218,4 @@ elif [[ ${NC_RESETPW} == "y" ]]; then
read -p "Enter the username: " NC_USER
done
docker exec -it -u www-data $(docker ps -f name=php-fpm-mailcow -q) /web/nextcloud/occ user:resetpassword ${NC_USER}
fi

View File

@ -23,9 +23,8 @@ function validate_input()
function detect_docker_compose_command()
{
if ! [[ "${DOCKER_COMPOSE_VERSION}" == "native" ]] && ! [[ "${DOCKER_COMPOSE_VERSION}" == "standalone" ]]; then
if command -v docker compose > /dev/null 2>&1; then
version=$(docker compose version --short)
if [[ $version =~ ^2\.([0-9]+)\.([0-9]+) ]]; then
if docker compose > /dev/null 2>&1; then
if docker compose version --short | grep "2." > /dev/null 2>&1; then
DOCKER_COMPOSE_VERSION=native
COMPOSE_COMMAND="docker compose"
echo -e "\e[31mFound Docker Compose Plugin (native).\e[0m"
@ -37,9 +36,9 @@ function detect_docker_compose_command()
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
fi
elif command -v docker-compose > /dev/null 2>&1; then
version=$(docker-compose version --short)
if [[ $version =~ ^2\.([0-9]+)\.([0-9]+) ]]; then
elif docker-compose > /dev/null 2>&1; then
if ! [[ $(alias docker-compose 2> /dev/null) ]] ; then
if docker-compose version --short | grep "^2." > /dev/null 2>&1; then
DOCKER_COMPOSE_VERSION=standalone
COMPOSE_COMMAND="docker-compose"
echo -e "\e[31mFound Docker Compose Standalone.\e[0m"
@ -51,6 +50,7 @@ function detect_docker_compose_command()
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
fi
fi
else
echo -e "\e[31mCannot find Docker Compose.\e[0m"