commit
23fc54f2cf
@ -10,7 +10,7 @@ jobs:
|
|||||||
if: github.event.pull_request.base.ref != 'staging' #check if the target branch is not staging
|
if: github.event.pull_request.base.ref != 'staging' #check if the target branch is not staging
|
||||||
steps:
|
steps:
|
||||||
- name: Send message
|
- name: Send message
|
||||||
uses: thollander/actions-comment-pull-request@v2.3.1
|
uses: thollander/actions-comment-pull-request@v2.4.0
|
||||||
with:
|
with:
|
||||||
GITHUB_TOKEN: ${{ secrets.CHECKIFPRISSTAGING_ACTION_PAT }}
|
GITHUB_TOKEN: ${{ secrets.CHECKIFPRISSTAGING_ACTION_PAT }}
|
||||||
message: |
|
message: |
|
||||||
|
@ -14,9 +14,12 @@ RUN apk add --update --no-cache python3 \
|
|||||||
uvicorn \
|
uvicorn \
|
||||||
aiodocker \
|
aiodocker \
|
||||||
docker \
|
docker \
|
||||||
redis
|
aioredis
|
||||||
|
RUN mkdir /app/modules
|
||||||
|
|
||||||
COPY docker-entrypoint.sh /app/
|
COPY docker-entrypoint.sh /app/
|
||||||
COPY dockerapi.py /app/
|
COPY main.py /app/main.py
|
||||||
|
COPY modules/ /app/modules/
|
||||||
|
|
||||||
ENTRYPOINT ["/bin/sh", "/app/docker-entrypoint.sh"]
|
ENTRYPOINT ["/bin/sh", "/app/docker-entrypoint.sh"]
|
||||||
|
CMD exec python main.py
|
@ -6,4 +6,4 @@
|
|||||||
-subj /CN=dockerapi/O=mailcow \
|
-subj /CN=dockerapi/O=mailcow \
|
||||||
-addext subjectAltName=DNS:dockerapi`
|
-addext subjectAltName=DNS:dockerapi`
|
||||||
|
|
||||||
`uvicorn --host 0.0.0.0 --port 443 --ssl-certfile=/app/dockerapi_cert.pem --ssl-keyfile=/app/dockerapi_key.pem dockerapi:app`
|
exec "$@"
|
||||||
|
@ -1,549 +0,0 @@
|
|||||||
from fastapi import FastAPI, Response, Request
|
|
||||||
import aiodocker
|
|
||||||
import docker
|
|
||||||
import psutil
|
|
||||||
import sys
|
|
||||||
import re
|
|
||||||
import time
|
|
||||||
import os
|
|
||||||
import json
|
|
||||||
import asyncio
|
|
||||||
import redis
|
|
||||||
import platform
|
|
||||||
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")
|
|
||||||
async def get_host_update_stats():
|
|
||||||
global host_stats_isUpdating
|
|
||||||
|
|
||||||
if host_stats_isUpdating == False:
|
|
||||||
asyncio.create_task(get_host_stats())
|
|
||||||
host_stats_isUpdating = True
|
|
||||||
|
|
||||||
while True:
|
|
||||||
if redis_client.exists('host_stats'):
|
|
||||||
break
|
|
||||||
await asyncio.sleep(1.5)
|
|
||||||
|
|
||||||
|
|
||||||
stats = json.loads(redis_client.get('host_stats'))
|
|
||||||
return Response(content=json.dumps(stats, indent=4), media_type="application/json")
|
|
||||||
|
|
||||||
@app.get("/containers/{container_id}/json")
|
|
||||||
async def get_container(container_id : str):
|
|
||||||
if container_id and container_id.isalnum():
|
|
||||||
try:
|
|
||||||
for container in (await async_docker_client.containers.list()):
|
|
||||||
if container._id == container_id:
|
|
||||||
container_info = await container.show()
|
|
||||||
return Response(content=json.dumps(container_info, indent=4), media_type="application/json")
|
|
||||||
|
|
||||||
res = {
|
|
||||||
"type": "danger",
|
|
||||||
"msg": "no container found"
|
|
||||||
}
|
|
||||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
|
||||||
except Exception as e:
|
|
||||||
res = {
|
|
||||||
"type": "danger",
|
|
||||||
"msg": str(e)
|
|
||||||
}
|
|
||||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
|
||||||
else:
|
|
||||||
res = {
|
|
||||||
"type": "danger",
|
|
||||||
"msg": "no or invalid id defined"
|
|
||||||
}
|
|
||||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
|
||||||
|
|
||||||
@app.get("/containers/json")
|
|
||||||
async def get_containers():
|
|
||||||
containers = {}
|
|
||||||
try:
|
|
||||||
for container in (await async_docker_client.containers.list()):
|
|
||||||
container_info = await container.show()
|
|
||||||
containers.update({container_info['Id']: container_info})
|
|
||||||
return Response(content=json.dumps(containers, indent=4), media_type="application/json")
|
|
||||||
except Exception as e:
|
|
||||||
res = {
|
|
||||||
"type": "danger",
|
|
||||||
"msg": str(e)
|
|
||||||
}
|
|
||||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
|
||||||
|
|
||||||
@app.post("/containers/{container_id}/{post_action}")
|
|
||||||
async def post_containers(container_id : str, post_action : str, request: Request):
|
|
||||||
try :
|
|
||||||
request_json = await request.json()
|
|
||||||
except Exception as err:
|
|
||||||
request_json = {}
|
|
||||||
|
|
||||||
if container_id and container_id.isalnum() and post_action:
|
|
||||||
try:
|
|
||||||
"""Dispatch container_post api call"""
|
|
||||||
if post_action == 'exec':
|
|
||||||
if not request_json or not 'cmd' in request_json:
|
|
||||||
res = {
|
|
||||||
"type": "danger",
|
|
||||||
"msg": "cmd is missing"
|
|
||||||
}
|
|
||||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
|
||||||
if not request_json or not 'task' in request_json:
|
|
||||||
res = {
|
|
||||||
"type": "danger",
|
|
||||||
"msg": "task is missing"
|
|
||||||
}
|
|
||||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
|
||||||
|
|
||||||
api_call_method_name = '__'.join(['container_post', str(post_action), str(request_json['cmd']), str(request_json['task']) ])
|
|
||||||
else:
|
|
||||||
api_call_method_name = '__'.join(['container_post', str(post_action) ])
|
|
||||||
|
|
||||||
docker_utils = DockerUtils(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"))
|
|
||||||
|
|
||||||
|
|
||||||
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:
|
|
||||||
logger.error("error - container_post: %s" % str(e))
|
|
||||||
res = {
|
|
||||||
"type": "danger",
|
|
||||||
"msg": str(e)
|
|
||||||
}
|
|
||||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
|
||||||
|
|
||||||
else:
|
|
||||||
res = {
|
|
||||||
"type": "danger",
|
|
||||||
"msg": "invalid container id or missing action"
|
|
||||||
}
|
|
||||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
|
||||||
|
|
||||||
@app.post("/container/{container_id}/stats/update")
|
|
||||||
async def post_container_update_stats(container_id : str):
|
|
||||||
global containerIds_to_update
|
|
||||||
|
|
||||||
# start update task for container if no task is running
|
|
||||||
if container_id not in containerIds_to_update:
|
|
||||||
asyncio.create_task(get_container_stats(container_id))
|
|
||||||
containerIds_to_update.append(container_id)
|
|
||||||
|
|
||||||
while True:
|
|
||||||
if redis_client.exists(container_id + '_stats'):
|
|
||||||
break
|
|
||||||
await asyncio.sleep(1.5)
|
|
||||||
|
|
||||||
stats = json.loads(redis_client.get(container_id + '_stats'))
|
|
||||||
return Response(content=json.dumps(stats, indent=4), media_type="application/json")
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class DockerUtils:
|
|
||||||
def __init__(self, docker_client):
|
|
||||||
self.docker_client = docker_client
|
|
||||||
|
|
||||||
# api call: container_post - post_action: stop
|
|
||||||
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
|
|
||||||
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
|
|
||||||
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
|
|
||||||
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")
|
|
||||||
# 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
|
|
||||||
def container_post__exec__mailq__delete(self, container_id, request_json):
|
|
||||||
if 'items' in request_json:
|
|
||||||
r = re.compile("^[0-9a-fA-F]+$")
|
|
||||||
filtered_qids = filter(r.match, request_json['items'])
|
|
||||||
if filtered_qids:
|
|
||||||
flagged_qids = ['-d %s' % i for i in filtered_qids]
|
|
||||||
sanitized_string = str(' '.join(flagged_qids));
|
|
||||||
for container in 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: hold
|
|
||||||
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 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
|
|
||||||
def container_post__exec__mailq__cat(self, container_id, request_json):
|
|
||||||
if 'items' in request_json:
|
|
||||||
r = re.compile("^[0-9a-fA-F]+$")
|
|
||||||
filtered_qids = filter(r.match, request_json['items'])
|
|
||||||
if filtered_qids:
|
|
||||||
sanitized_string = str(' '.join(filtered_qids));
|
|
||||||
|
|
||||||
for container in 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
|
|
||||||
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 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
|
|
||||||
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 self.docker_client.containers.list(filters={"id": container_id}):
|
|
||||||
for i in flagged_qids:
|
|
||||||
postqueue_r = container.exec_run(["/bin/bash", "-c", "/usr/sbin/postqueue " + i], user='postfix')
|
|
||||||
# todo: check each exit code
|
|
||||||
res = { 'type': 'success', 'msg': 'Scheduled immediate delivery'}
|
|
||||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
|
||||||
|
|
||||||
# api call: container_post - post_action: exec - cmd: mailq - task: list
|
|
||||||
def container_post__exec__mailq__list(self, 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
|
|
||||||
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
|
|
||||||
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
|
|
||||||
def container_post__exec__system__fts_rescan(self, container_id, request_json):
|
|
||||||
if 'username' in request_json:
|
|
||||||
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'}
|
|
||||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
|
||||||
if 'all' in request_json:
|
|
||||||
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'}
|
|
||||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
|
||||||
# api call: container_post - post_action: exec - cmd: system - task: df
|
|
||||||
def container_post__exec__system__df(self, container_id, request_json):
|
|
||||||
if 'dir' in request_json:
|
|
||||||
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
|
|
||||||
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.output.decode('utf-8').split("\n"):
|
|
||||||
if 'is already upgraded to' in line:
|
|
||||||
matched = True
|
|
||||||
if matched:
|
|
||||||
res = { 'type': 'success', 'msg':'mysql_upgrade: already upgraded', 'text': sql_return.output.decode('utf-8')}
|
|
||||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
|
||||||
else:
|
|
||||||
container.restart()
|
|
||||||
res = { 'type': 'warning', 'msg':'mysql_upgrade: upgrade was applied', 'text': sql_return.output.decode('utf-8')}
|
|
||||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
|
||||||
else:
|
|
||||||
res = { 'type': 'error', 'msg': 'mysql_upgrade: error running command', 'text': sql_return.output.decode('utf-8')}
|
|
||||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
|
||||||
# api call: container_post - post_action: exec - cmd: system - task: mysql_tzinfo_to_sql
|
|
||||||
def container_post__exec__system__mysql_tzinfo_to_sql(self, container_id, request_json):
|
|
||||||
for container in self.docker_client.containers.list(filters={"id": container_id}):
|
|
||||||
sql_return = container.exec_run(["/bin/bash", "-c", "/usr/bin/mysql_tzinfo_to_sql /usr/share/zoneinfo | /bin/sed 's/Local time zone must be set--see zic manual page/FCTY/' | /usr/bin/mysql -uroot -p'" + os.environ['DBROOT'].replace("'", "'\\''") + "' mysql \n"], user='mysql')
|
|
||||||
if sql_return.exit_code == 0:
|
|
||||||
res = { 'type': 'info', 'msg': 'mysql_tzinfo_to_sql: command completed successfully', 'text': sql_return.output.decode('utf-8')}
|
|
||||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
|
||||||
else:
|
|
||||||
res = { 'type': 'error', 'msg': 'mysql_tzinfo_to_sql: error running command', 'text': sql_return.output.decode('utf-8')}
|
|
||||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
|
||||||
# api call: container_post - post_action: exec - cmd: reload - task: dovecot
|
|
||||||
def container_post__exec__reload__dovecot(self, container_id, request_json):
|
|
||||||
for container in self.docker_client.containers.list(filters={"id": container_id}):
|
|
||||||
reload_return = container.exec_run(["/bin/bash", "-c", "/usr/sbin/dovecot reload"])
|
|
||||||
return exec_run_handler('generic', reload_return)
|
|
||||||
# api call: container_post - post_action: exec - cmd: reload - task: postfix
|
|
||||||
def container_post__exec__reload__postfix(self, container_id, request_json):
|
|
||||||
for container in self.docker_client.containers.list(filters={"id": container_id}):
|
|
||||||
reload_return = container.exec_run(["/bin/bash", "-c", "/usr/sbin/postfix reload"])
|
|
||||||
return exec_run_handler('generic', reload_return)
|
|
||||||
# api call: container_post - post_action: exec - cmd: reload - task: nginx
|
|
||||||
def container_post__exec__reload__nginx(self, container_id, request_json):
|
|
||||||
for container in self.docker_client.containers.list(filters={"id": container_id}):
|
|
||||||
reload_return = container.exec_run(["/bin/sh", "-c", "/usr/sbin/nginx -s reload"])
|
|
||||||
return exec_run_handler('generic', reload_return)
|
|
||||||
# api call: container_post - post_action: exec - cmd: sieve - task: list
|
|
||||||
def container_post__exec__sieve__list(self, container_id, request_json):
|
|
||||||
if 'username' in request_json:
|
|
||||||
for container in self.docker_client.containers.list(filters={"id": container_id}):
|
|
||||||
sieve_return = container.exec_run(["/bin/bash", "-c", "/usr/bin/doveadm sieve list -u '" + request_json['username'].replace("'", "'\\''") + "'"])
|
|
||||||
return exec_run_handler('utf8_text_only', sieve_return)
|
|
||||||
# api call: container_post - post_action: exec - cmd: sieve - task: print
|
|
||||||
def container_post__exec__sieve__print(self, container_id, request_json):
|
|
||||||
if 'username' in request_json and 'script_name' in request_json:
|
|
||||||
for container in self.docker_client.containers.list(filters={"id": container_id}):
|
|
||||||
cmd = ["/bin/bash", "-c", "/usr/bin/doveadm sieve get -u '" + request_json['username'].replace("'", "'\\''") + "' '" + request_json['script_name'].replace("'", "'\\''") + "'"]
|
|
||||||
sieve_return = container.exec_run(cmd)
|
|
||||||
return exec_run_handler('utf8_text_only', sieve_return)
|
|
||||||
# api call: container_post - post_action: exec - cmd: maildir - task: cleanup
|
|
||||||
def container_post__exec__maildir__cleanup(self, container_id, request_json):
|
|
||||||
if 'maildir' in request_json:
|
|
||||||
for container in self.docker_client.containers.list(filters={"id": container_id}):
|
|
||||||
sane_name = re.sub(r'\W+', '', request_json['maildir'])
|
|
||||||
vmail_name = request_json['maildir'].replace("'", "'\\''")
|
|
||||||
cmd_vmail = "if [[ -d '/var/vmail/" + vmail_name + "' ]]; then /bin/mv '/var/vmail/" + vmail_name + "' '/var/vmail/_garbage/" + str(int(time.time())) + "_" + sane_name + "'; fi"
|
|
||||||
index_name = request_json['maildir'].split("/")
|
|
||||||
if len(index_name) > 1:
|
|
||||||
index_name = index_name[1].replace("'", "'\\''") + "@" + index_name[0].replace("'", "'\\''")
|
|
||||||
cmd_vmail_index = "if [[ -d '/var/vmail_index/" + index_name + "' ]]; then /bin/mv '/var/vmail_index/" + index_name + "' '/var/vmail/_garbage/" + str(int(time.time())) + "_" + sane_name + "_index'; fi"
|
|
||||||
cmd = ["/bin/bash", "-c", cmd_vmail + " && " + cmd_vmail_index]
|
|
||||||
else:
|
|
||||||
cmd = ["/bin/bash", "-c", cmd_vmail]
|
|
||||||
maildir_cleanup = container.exec_run(cmd, user='vmail')
|
|
||||||
return exec_run_handler('generic', maildir_cleanup)
|
|
||||||
# api call: container_post - post_action: exec - cmd: rspamd - task: worker_password
|
|
||||||
def container_post__exec__rspamd__worker_password(self, container_id, request_json):
|
|
||||||
if 'raw' in request_json:
|
|
||||||
for container in self.docker_client.containers.list(filters={"id": container_id}):
|
|
||||||
cmd = "/usr/bin/rspamadm pw -e -p '" + request_json['raw'].replace("'", "'\\''") + "' 2> /dev/null"
|
|
||||||
cmd_response = exec_cmd_container(container, cmd, user="_rspamd")
|
|
||||||
|
|
||||||
matched = False
|
|
||||||
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
|
|
||||||
if matched:
|
|
||||||
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:
|
|
||||||
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"):
|
|
||||||
|
|
||||||
def recv_socket_data(c_socket, timeout):
|
|
||||||
c_socket.setblocking(0)
|
|
||||||
total_data=[]
|
|
||||||
data=''
|
|
||||||
begin=time.time()
|
|
||||||
while True:
|
|
||||||
if total_data and time.time()-begin > timeout:
|
|
||||||
break
|
|
||||||
elif time.time()-begin > timeout*2:
|
|
||||||
break
|
|
||||||
try:
|
|
||||||
data = c_socket.recv(8192)
|
|
||||||
if data:
|
|
||||||
total_data.append(data.decode('utf-8'))
|
|
||||||
#change the beginning time for measurement
|
|
||||||
begin=time.time()
|
|
||||||
else:
|
|
||||||
#sleep for sometime to indicate a gap
|
|
||||||
time.sleep(0.1)
|
|
||||||
break
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
return ''.join(total_data)
|
|
||||||
|
|
||||||
|
|
||||||
try :
|
|
||||||
socket = container.exec_run([shell_cmd], stdin=True, socket=True, user=user).output._sock
|
|
||||||
if not cmd.endswith("\n"):
|
|
||||||
cmd = cmd + "\n"
|
|
||||||
socket.send(cmd.encode('utf-8'))
|
|
||||||
data = recv_socket_data(socket, timeout)
|
|
||||||
socket.close()
|
|
||||||
return data
|
|
||||||
except Exception as e:
|
|
||||||
logger.error("error - exec_cmd_container: %s" % str(e))
|
|
||||||
traceback.print_exc(file=sys.stdout)
|
|
||||||
def exec_run_handler(type, output):
|
|
||||||
if type == 'generic':
|
|
||||||
if output.exit_code == 0:
|
|
||||||
res = { 'type': 'success', 'msg': 'command completed successfully' }
|
|
||||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
|
||||||
else:
|
|
||||||
res = { 'type': 'danger', 'msg': 'command failed: ' + output.output.decode('utf-8') }
|
|
||||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
|
||||||
if type == 'utf8_text_only':
|
|
||||||
return Response(content=output.output.decode('utf-8'), media_type="text/plain")
|
|
||||||
|
|
||||||
async def get_host_stats(wait=5):
|
|
||||||
global host_stats_isUpdating
|
|
||||||
|
|
||||||
try:
|
|
||||||
system_time = datetime.now()
|
|
||||||
host_stats = {
|
|
||||||
"cpu": {
|
|
||||||
"cores": psutil.cpu_count(),
|
|
||||||
"usage": psutil.cpu_percent()
|
|
||||||
},
|
|
||||||
"memory": {
|
|
||||||
"total": psutil.virtual_memory().total,
|
|
||||||
"usage": psutil.virtual_memory().percent,
|
|
||||||
"swap": psutil.swap_memory()
|
|
||||||
},
|
|
||||||
"uptime": time.time() - psutil.boot_time(),
|
|
||||||
"system_time": system_time.strftime("%d.%m.%Y %H:%M:%S"),
|
|
||||||
"architecture": platform.machine()
|
|
||||||
}
|
|
||||||
|
|
||||||
redis_client.set('host_stats', json.dumps(host_stats), ex=10)
|
|
||||||
except Exception as e:
|
|
||||||
res = {
|
|
||||||
"type": "danger",
|
|
||||||
"msg": str(e)
|
|
||||||
}
|
|
||||||
|
|
||||||
await asyncio.sleep(wait)
|
|
||||||
host_stats_isUpdating = False
|
|
||||||
|
|
||||||
async def get_container_stats(container_id, wait=5, stop=False):
|
|
||||||
global containerIds_to_update
|
|
||||||
|
|
||||||
if container_id and container_id.isalnum():
|
|
||||||
try:
|
|
||||||
for container in (await async_docker_client.containers.list()):
|
|
||||||
if container._id == container_id:
|
|
||||||
res = await container.stats(stream=False)
|
|
||||||
|
|
||||||
if redis_client.exists(container_id + '_stats'):
|
|
||||||
stats = json.loads(redis_client.get(container_id + '_stats'))
|
|
||||||
else:
|
|
||||||
stats = []
|
|
||||||
stats.append(res[0])
|
|
||||||
if len(stats) > 3:
|
|
||||||
del stats[0]
|
|
||||||
redis_client.set(container_id + '_stats', json.dumps(stats), ex=60)
|
|
||||||
except Exception as e:
|
|
||||||
res = {
|
|
||||||
"type": "danger",
|
|
||||||
"msg": str(e)
|
|
||||||
}
|
|
||||||
else:
|
|
||||||
res = {
|
|
||||||
"type": "danger",
|
|
||||||
"msg": "no or invalid id defined"
|
|
||||||
}
|
|
||||||
|
|
||||||
await asyncio.sleep(wait)
|
|
||||||
if stop == True:
|
|
||||||
# update task was called second time, stop
|
|
||||||
containerIds_to_update.remove(container_id)
|
|
||||||
else:
|
|
||||||
# call update task a second time
|
|
||||||
await get_container_stats(container_id, wait=0, stop=True)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if os.environ['REDIS_SLAVEOF_IP'] != "":
|
|
||||||
redis_client = redis.Redis(host=os.environ['REDIS_SLAVEOF_IP'], port=os.environ['REDIS_SLAVEOF_PORT'], db=0)
|
|
||||||
else:
|
|
||||||
redis_client = redis.Redis(host='redis-mailcow', port=6379, db=0)
|
|
||||||
|
|
||||||
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')
|
|
260
data/Dockerfiles/dockerapi/main.py
Normal file
260
data/Dockerfiles/dockerapi/main.py
Normal file
@ -0,0 +1,260 @@
|
|||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import uvicorn
|
||||||
|
import json
|
||||||
|
import uuid
|
||||||
|
import async_timeout
|
||||||
|
import asyncio
|
||||||
|
import aioredis
|
||||||
|
import aiodocker
|
||||||
|
import docker
|
||||||
|
import logging
|
||||||
|
from logging.config import dictConfig
|
||||||
|
from fastapi import FastAPI, Response, Request
|
||||||
|
from modules.DockerApi import DockerApi
|
||||||
|
|
||||||
|
dockerapi = None
|
||||||
|
app = FastAPI()
|
||||||
|
|
||||||
|
# Define Routes
|
||||||
|
@app.get("/host/stats")
|
||||||
|
async def get_host_update_stats():
|
||||||
|
global dockerapi
|
||||||
|
|
||||||
|
if dockerapi.host_stats_isUpdating == False:
|
||||||
|
asyncio.create_task(dockerapi.get_host_stats())
|
||||||
|
dockerapi.host_stats_isUpdating = True
|
||||||
|
|
||||||
|
while True:
|
||||||
|
if await dockerapi.redis_client.exists('host_stats'):
|
||||||
|
break
|
||||||
|
await asyncio.sleep(1.5)
|
||||||
|
|
||||||
|
stats = json.loads(await dockerapi.redis_client.get('host_stats'))
|
||||||
|
return Response(content=json.dumps(stats, indent=4), media_type="application/json")
|
||||||
|
|
||||||
|
@app.get("/containers/{container_id}/json")
|
||||||
|
async def get_container(container_id : str):
|
||||||
|
global dockerapi
|
||||||
|
|
||||||
|
if container_id and container_id.isalnum():
|
||||||
|
try:
|
||||||
|
for container in (await dockerapi.async_docker_client.containers.list()):
|
||||||
|
if container._id == container_id:
|
||||||
|
container_info = await container.show()
|
||||||
|
return Response(content=json.dumps(container_info, indent=4), media_type="application/json")
|
||||||
|
|
||||||
|
res = {
|
||||||
|
"type": "danger",
|
||||||
|
"msg": "no container found"
|
||||||
|
}
|
||||||
|
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||||
|
except Exception as e:
|
||||||
|
res = {
|
||||||
|
"type": "danger",
|
||||||
|
"msg": str(e)
|
||||||
|
}
|
||||||
|
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||||
|
else:
|
||||||
|
res = {
|
||||||
|
"type": "danger",
|
||||||
|
"msg": "no or invalid id defined"
|
||||||
|
}
|
||||||
|
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||||
|
|
||||||
|
@app.get("/containers/json")
|
||||||
|
async def get_containers():
|
||||||
|
global dockerapi
|
||||||
|
|
||||||
|
containers = {}
|
||||||
|
try:
|
||||||
|
for container in (await dockerapi.async_docker_client.containers.list()):
|
||||||
|
container_info = await container.show()
|
||||||
|
containers.update({container_info['Id']: container_info})
|
||||||
|
return Response(content=json.dumps(containers, indent=4), media_type="application/json")
|
||||||
|
except Exception as e:
|
||||||
|
res = {
|
||||||
|
"type": "danger",
|
||||||
|
"msg": str(e)
|
||||||
|
}
|
||||||
|
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||||
|
|
||||||
|
@app.post("/containers/{container_id}/{post_action}")
|
||||||
|
async def post_containers(container_id : str, post_action : str, request: Request):
|
||||||
|
global dockerapi
|
||||||
|
|
||||||
|
try :
|
||||||
|
request_json = await request.json()
|
||||||
|
except Exception as err:
|
||||||
|
request_json = {}
|
||||||
|
|
||||||
|
if container_id and container_id.isalnum() and post_action:
|
||||||
|
try:
|
||||||
|
"""Dispatch container_post api call"""
|
||||||
|
if post_action == 'exec':
|
||||||
|
if not request_json or not 'cmd' in request_json:
|
||||||
|
res = {
|
||||||
|
"type": "danger",
|
||||||
|
"msg": "cmd is missing"
|
||||||
|
}
|
||||||
|
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||||
|
if not request_json or not 'task' in request_json:
|
||||||
|
res = {
|
||||||
|
"type": "danger",
|
||||||
|
"msg": "task is missing"
|
||||||
|
}
|
||||||
|
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||||
|
|
||||||
|
api_call_method_name = '__'.join(['container_post', str(post_action), str(request_json['cmd']), str(request_json['task']) ])
|
||||||
|
else:
|
||||||
|
api_call_method_name = '__'.join(['container_post', str(post_action) ])
|
||||||
|
|
||||||
|
api_call_method = getattr(dockerapi, api_call_method_name, lambda container_id: Response(content=json.dumps({'type': 'danger', 'msg':'container_post - unknown api call' }, indent=4), media_type="application/json"))
|
||||||
|
|
||||||
|
dockerapi.logger.info("api call: %s, container_id: %s" % (api_call_method_name, container_id))
|
||||||
|
return api_call_method(request_json, container_id=container_id)
|
||||||
|
except Exception as e:
|
||||||
|
dockerapi.logger.error("error - container_post: %s" % str(e))
|
||||||
|
res = {
|
||||||
|
"type": "danger",
|
||||||
|
"msg": str(e)
|
||||||
|
}
|
||||||
|
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||||
|
|
||||||
|
else:
|
||||||
|
res = {
|
||||||
|
"type": "danger",
|
||||||
|
"msg": "invalid container id or missing action"
|
||||||
|
}
|
||||||
|
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||||
|
|
||||||
|
@app.post("/container/{container_id}/stats/update")
|
||||||
|
async def post_container_update_stats(container_id : str):
|
||||||
|
global dockerapi
|
||||||
|
|
||||||
|
# start update task for container if no task is running
|
||||||
|
if container_id not in dockerapi.containerIds_to_update:
|
||||||
|
asyncio.create_task(dockerapi.get_container_stats(container_id))
|
||||||
|
dockerapi.containerIds_to_update.append(container_id)
|
||||||
|
|
||||||
|
while True:
|
||||||
|
if await dockerapi.redis_client.exists(container_id + '_stats'):
|
||||||
|
break
|
||||||
|
await asyncio.sleep(1.5)
|
||||||
|
|
||||||
|
stats = json.loads(await dockerapi.redis_client.get(container_id + '_stats'))
|
||||||
|
return Response(content=json.dumps(stats, indent=4), media_type="application/json")
|
||||||
|
|
||||||
|
# Events
|
||||||
|
@app.on_event("startup")
|
||||||
|
async def startup_event():
|
||||||
|
global dockerapi
|
||||||
|
|
||||||
|
# Initialize a custom logger
|
||||||
|
logger = logging.getLogger("dockerapi")
|
||||||
|
logger.setLevel(logging.INFO)
|
||||||
|
# Configure the logger to output logs to the terminal
|
||||||
|
handler = logging.StreamHandler()
|
||||||
|
handler.setLevel(logging.INFO)
|
||||||
|
formatter = logging.Formatter("%(levelname)s: %(message)s")
|
||||||
|
handler.setFormatter(formatter)
|
||||||
|
logger.addHandler(handler)
|
||||||
|
|
||||||
|
logger.info("Init APP")
|
||||||
|
|
||||||
|
# Init redis client
|
||||||
|
if os.environ['REDIS_SLAVEOF_IP'] != "":
|
||||||
|
redis_client = redis = await aioredis.from_url(f"redis://{os.environ['REDIS_SLAVEOF_IP']}:{os.environ['REDIS_SLAVEOF_PORT']}/0")
|
||||||
|
else:
|
||||||
|
redis_client = redis = await aioredis.from_url("redis://redis-mailcow:6379/0")
|
||||||
|
|
||||||
|
# Init docker clients
|
||||||
|
sync_docker_client = docker.DockerClient(base_url='unix://var/run/docker.sock', version='auto')
|
||||||
|
async_docker_client = aiodocker.Docker(url='unix:///var/run/docker.sock')
|
||||||
|
|
||||||
|
dockerapi = DockerApi(redis_client, sync_docker_client, async_docker_client, logger)
|
||||||
|
|
||||||
|
logger.info("Subscribe to redis channel")
|
||||||
|
# Subscribe to redis channel
|
||||||
|
dockerapi.pubsub = redis.pubsub()
|
||||||
|
await dockerapi.pubsub.subscribe("MC_CHANNEL")
|
||||||
|
asyncio.create_task(handle_pubsub_messages(dockerapi.pubsub))
|
||||||
|
|
||||||
|
@app.on_event("shutdown")
|
||||||
|
async def shutdown_event():
|
||||||
|
global dockerapi
|
||||||
|
|
||||||
|
# Close docker connections
|
||||||
|
dockerapi.sync_docker_client.close()
|
||||||
|
await dockerapi.async_docker_client.close()
|
||||||
|
|
||||||
|
# Close redis
|
||||||
|
await dockerapi.pubsub.unsubscribe("MC_CHANNEL")
|
||||||
|
await dockerapi.redis_client.close()
|
||||||
|
|
||||||
|
# PubSub Handler
|
||||||
|
async def handle_pubsub_messages(channel: aioredis.client.PubSub):
|
||||||
|
global dockerapi
|
||||||
|
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
async with async_timeout.timeout(1):
|
||||||
|
message = await channel.get_message(ignore_subscribe_messages=True)
|
||||||
|
if message is not None:
|
||||||
|
# Parse message
|
||||||
|
data_json = json.loads(message['data'].decode('utf-8'))
|
||||||
|
dockerapi.logger.info(f"PubSub Received - {json.dumps(data_json)}")
|
||||||
|
|
||||||
|
# Handle api_call
|
||||||
|
if 'api_call' in data_json:
|
||||||
|
# api_call: container_post
|
||||||
|
if data_json['api_call'] == "container_post":
|
||||||
|
if 'post_action' in data_json and 'container_name' in data_json:
|
||||||
|
try:
|
||||||
|
"""Dispatch container_post api call"""
|
||||||
|
request_json = {}
|
||||||
|
if data_json['post_action'] == 'exec':
|
||||||
|
if 'request' in data_json:
|
||||||
|
request_json = data_json['request']
|
||||||
|
if 'cmd' in request_json:
|
||||||
|
if 'task' in request_json:
|
||||||
|
api_call_method_name = '__'.join(['container_post', str(data_json['post_action']), str(request_json['cmd']), str(request_json['task']) ])
|
||||||
|
else:
|
||||||
|
dockerapi.logger.error("api call: task missing")
|
||||||
|
else:
|
||||||
|
dockerapi.logger.error("api call: cmd missing")
|
||||||
|
else:
|
||||||
|
dockerapi.logger.error("api call: request missing")
|
||||||
|
else:
|
||||||
|
api_call_method_name = '__'.join(['container_post', str(data_json['post_action'])])
|
||||||
|
|
||||||
|
if api_call_method_name:
|
||||||
|
api_call_method = getattr(dockerapi, api_call_method_name)
|
||||||
|
if api_call_method:
|
||||||
|
dockerapi.logger.info("api call: %s, container_name: %s" % (api_call_method_name, data_json['container_name']))
|
||||||
|
api_call_method(request_json, container_name=data_json['container_name'])
|
||||||
|
else:
|
||||||
|
dockerapi.logger.error("api call not found: %s, container_name: %s" % (api_call_method_name, data_json['container_name']))
|
||||||
|
except Exception as e:
|
||||||
|
dockerapi.logger.error("container_post: %s" % str(e))
|
||||||
|
else:
|
||||||
|
dockerapi.logger.error("api call: missing container_name, post_action or request")
|
||||||
|
else:
|
||||||
|
dockerapi.logger.error("Unknwon PubSub recieved - %s" % json.dumps(data_json))
|
||||||
|
else:
|
||||||
|
dockerapi.logger.error("Unknwon PubSub recieved - %s" % json.dumps(data_json))
|
||||||
|
|
||||||
|
await asyncio.sleep(0.01)
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
uvicorn.run(
|
||||||
|
app,
|
||||||
|
host="0.0.0.0",
|
||||||
|
port=443,
|
||||||
|
ssl_certfile="/app/dockerapi_cert.pem",
|
||||||
|
ssl_keyfile="/app/dockerapi_key.pem",
|
||||||
|
log_level="info",
|
||||||
|
loop="none"
|
||||||
|
)
|
487
data/Dockerfiles/dockerapi/modules/DockerApi.py
Normal file
487
data/Dockerfiles/dockerapi/modules/DockerApi.py
Normal file
@ -0,0 +1,487 @@
|
|||||||
|
import psutil
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import time
|
||||||
|
import json
|
||||||
|
import asyncio
|
||||||
|
import platform
|
||||||
|
from datetime import datetime
|
||||||
|
from fastapi import FastAPI, Response, Request
|
||||||
|
|
||||||
|
class DockerApi:
|
||||||
|
def __init__(self, redis_client, sync_docker_client, async_docker_client, logger):
|
||||||
|
self.redis_client = redis_client
|
||||||
|
self.sync_docker_client = sync_docker_client
|
||||||
|
self.async_docker_client = async_docker_client
|
||||||
|
self.logger = logger
|
||||||
|
|
||||||
|
self.host_stats_isUpdating = False
|
||||||
|
self.containerIds_to_update = []
|
||||||
|
|
||||||
|
# api call: container_post - post_action: stop
|
||||||
|
def container_post__stop(self, request_json, **kwargs):
|
||||||
|
if 'container_id' in kwargs:
|
||||||
|
filters = {"id": kwargs['container_id']}
|
||||||
|
elif 'container_name' in kwargs:
|
||||||
|
filters = {"name": kwargs['container_name']}
|
||||||
|
|
||||||
|
for container in self.sync_docker_client.containers.list(all=True, filters=filters):
|
||||||
|
container.stop()
|
||||||
|
|
||||||
|
res = { 'type': 'success', 'msg': 'command completed successfully'}
|
||||||
|
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||||
|
# api call: container_post - post_action: start
|
||||||
|
def container_post__start(self, request_json, **kwargs):
|
||||||
|
if 'container_id' in kwargs:
|
||||||
|
filters = {"id": kwargs['container_id']}
|
||||||
|
elif 'container_name' in kwargs:
|
||||||
|
filters = {"name": kwargs['container_name']}
|
||||||
|
|
||||||
|
for container in self.sync_docker_client.containers.list(all=True, filters=filters):
|
||||||
|
container.start()
|
||||||
|
|
||||||
|
res = { 'type': 'success', 'msg': 'command completed successfully'}
|
||||||
|
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||||
|
# api call: container_post - post_action: restart
|
||||||
|
def container_post__restart(self, request_json, **kwargs):
|
||||||
|
if 'container_id' in kwargs:
|
||||||
|
filters = {"id": kwargs['container_id']}
|
||||||
|
elif 'container_name' in kwargs:
|
||||||
|
filters = {"name": kwargs['container_name']}
|
||||||
|
|
||||||
|
for container in self.sync_docker_client.containers.list(all=True, filters=filters):
|
||||||
|
container.restart()
|
||||||
|
|
||||||
|
res = { 'type': 'success', 'msg': 'command completed successfully'}
|
||||||
|
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||||
|
# api call: container_post - post_action: top
|
||||||
|
def container_post__top(self, request_json, **kwargs):
|
||||||
|
if 'container_id' in kwargs:
|
||||||
|
filters = {"id": kwargs['container_id']}
|
||||||
|
elif 'container_name' in kwargs:
|
||||||
|
filters = {"name": kwargs['container_name']}
|
||||||
|
|
||||||
|
for container in self.sync_docker_client.containers.list(all=True, filters=filters):
|
||||||
|
res = { 'type': 'success', 'msg': container.top()}
|
||||||
|
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||||
|
# api call: container_post - post_action: stats
|
||||||
|
def container_post__stats(self, request_json, **kwargs):
|
||||||
|
if 'container_id' in kwargs:
|
||||||
|
filters = {"id": kwargs['container_id']}
|
||||||
|
elif 'container_name' in kwargs:
|
||||||
|
filters = {"name": kwargs['container_name']}
|
||||||
|
|
||||||
|
for container in self.sync_docker_client.containers.list(all=True, filters=filters):
|
||||||
|
for stat in container.stats(decode=True, stream=True):
|
||||||
|
res = { 'type': 'success', 'msg': stat}
|
||||||
|
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||||
|
# api call: container_post - post_action: exec - cmd: mailq - task: delete
|
||||||
|
def container_post__exec__mailq__delete(self, request_json, **kwargs):
|
||||||
|
if 'container_id' in kwargs:
|
||||||
|
filters = {"id": kwargs['container_id']}
|
||||||
|
elif 'container_name' in kwargs:
|
||||||
|
filters = {"name": kwargs['container_name']}
|
||||||
|
|
||||||
|
if 'items' in request_json:
|
||||||
|
r = re.compile("^[0-9a-fA-F]+$")
|
||||||
|
filtered_qids = filter(r.match, request_json['items'])
|
||||||
|
if filtered_qids:
|
||||||
|
flagged_qids = ['-d %s' % i for i in filtered_qids]
|
||||||
|
sanitized_string = str(' '.join(flagged_qids))
|
||||||
|
for container in self.sync_docker_client.containers.list(filters=filters):
|
||||||
|
postsuper_r = container.exec_run(["/bin/bash", "-c", "/usr/sbin/postsuper " + sanitized_string])
|
||||||
|
return self.exec_run_handler('generic', postsuper_r)
|
||||||
|
# api call: container_post - post_action: exec - cmd: mailq - task: hold
|
||||||
|
def container_post__exec__mailq__hold(self, request_json, **kwargs):
|
||||||
|
if 'container_id' in kwargs:
|
||||||
|
filters = {"id": kwargs['container_id']}
|
||||||
|
elif 'container_name' in kwargs:
|
||||||
|
filters = {"name": kwargs['container_name']}
|
||||||
|
|
||||||
|
if 'items' in request_json:
|
||||||
|
r = re.compile("^[0-9a-fA-F]+$")
|
||||||
|
filtered_qids = filter(r.match, request_json['items'])
|
||||||
|
if filtered_qids:
|
||||||
|
flagged_qids = ['-h %s' % i for i in filtered_qids]
|
||||||
|
sanitized_string = str(' '.join(flagged_qids))
|
||||||
|
for container in self.sync_docker_client.containers.list(filters=filters):
|
||||||
|
postsuper_r = container.exec_run(["/bin/bash", "-c", "/usr/sbin/postsuper " + sanitized_string])
|
||||||
|
return self.exec_run_handler('generic', postsuper_r)
|
||||||
|
# api call: container_post - post_action: exec - cmd: mailq - task: cat
|
||||||
|
def container_post__exec__mailq__cat(self, request_json, **kwargs):
|
||||||
|
if 'container_id' in kwargs:
|
||||||
|
filters = {"id": kwargs['container_id']}
|
||||||
|
elif 'container_name' in kwargs:
|
||||||
|
filters = {"name": kwargs['container_name']}
|
||||||
|
|
||||||
|
if 'items' in request_json:
|
||||||
|
r = re.compile("^[0-9a-fA-F]+$")
|
||||||
|
filtered_qids = filter(r.match, request_json['items'])
|
||||||
|
if filtered_qids:
|
||||||
|
sanitized_string = str(' '.join(filtered_qids))
|
||||||
|
|
||||||
|
for container in self.sync_docker_client.containers.list(filters=filters):
|
||||||
|
postcat_return = container.exec_run(["/bin/bash", "-c", "/usr/sbin/postcat -q " + sanitized_string], user='postfix')
|
||||||
|
if not postcat_return:
|
||||||
|
postcat_return = 'err: invalid'
|
||||||
|
return self.exec_run_handler('utf8_text_only', postcat_return)
|
||||||
|
# api call: container_post - post_action: exec - cmd: mailq - task: unhold
|
||||||
|
def container_post__exec__mailq__unhold(self, request_json, **kwargs):
|
||||||
|
if 'container_id' in kwargs:
|
||||||
|
filters = {"id": kwargs['container_id']}
|
||||||
|
elif 'container_name' in kwargs:
|
||||||
|
filters = {"name": kwargs['container_name']}
|
||||||
|
|
||||||
|
if 'items' in request_json:
|
||||||
|
r = re.compile("^[0-9a-fA-F]+$")
|
||||||
|
filtered_qids = filter(r.match, request_json['items'])
|
||||||
|
if filtered_qids:
|
||||||
|
flagged_qids = ['-H %s' % i for i in filtered_qids]
|
||||||
|
sanitized_string = str(' '.join(flagged_qids))
|
||||||
|
for container in self.sync_docker_client.containers.list(filters=filters):
|
||||||
|
postsuper_r = container.exec_run(["/bin/bash", "-c", "/usr/sbin/postsuper " + sanitized_string])
|
||||||
|
return self.exec_run_handler('generic', postsuper_r)
|
||||||
|
# api call: container_post - post_action: exec - cmd: mailq - task: deliver
|
||||||
|
def container_post__exec__mailq__deliver(self, request_json, **kwargs):
|
||||||
|
if 'container_id' in kwargs:
|
||||||
|
filters = {"id": kwargs['container_id']}
|
||||||
|
elif 'container_name' in kwargs:
|
||||||
|
filters = {"name": kwargs['container_name']}
|
||||||
|
|
||||||
|
if 'items' in request_json:
|
||||||
|
r = re.compile("^[0-9a-fA-F]+$")
|
||||||
|
filtered_qids = filter(r.match, request_json['items'])
|
||||||
|
if filtered_qids:
|
||||||
|
flagged_qids = ['-i %s' % i for i in filtered_qids]
|
||||||
|
for container in self.sync_docker_client.containers.list(filters=filters):
|
||||||
|
for i in flagged_qids:
|
||||||
|
postqueue_r = container.exec_run(["/bin/bash", "-c", "/usr/sbin/postqueue " + i], user='postfix')
|
||||||
|
# todo: check each exit code
|
||||||
|
res = { 'type': 'success', 'msg': 'Scheduled immediate delivery'}
|
||||||
|
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||||
|
# api call: container_post - post_action: exec - cmd: mailq - task: list
|
||||||
|
def container_post__exec__mailq__list(self, request_json, **kwargs):
|
||||||
|
if 'container_id' in kwargs:
|
||||||
|
filters = {"id": kwargs['container_id']}
|
||||||
|
elif 'container_name' in kwargs:
|
||||||
|
filters = {"name": kwargs['container_name']}
|
||||||
|
|
||||||
|
for container in self.sync_docker_client.containers.list(filters=filters):
|
||||||
|
mailq_return = container.exec_run(["/usr/sbin/postqueue", "-j"], user='postfix')
|
||||||
|
return self.exec_run_handler('utf8_text_only', mailq_return)
|
||||||
|
# api call: container_post - post_action: exec - cmd: mailq - task: flush
|
||||||
|
def container_post__exec__mailq__flush(self, request_json, **kwargs):
|
||||||
|
if 'container_id' in kwargs:
|
||||||
|
filters = {"id": kwargs['container_id']}
|
||||||
|
elif 'container_name' in kwargs:
|
||||||
|
filters = {"name": kwargs['container_name']}
|
||||||
|
|
||||||
|
for container in self.sync_docker_client.containers.list(filters=filters):
|
||||||
|
postqueue_r = container.exec_run(["/usr/sbin/postqueue", "-f"], user='postfix')
|
||||||
|
return self.exec_run_handler('generic', postqueue_r)
|
||||||
|
# api call: container_post - post_action: exec - cmd: mailq - task: super_delete
|
||||||
|
def container_post__exec__mailq__super_delete(self, request_json, **kwargs):
|
||||||
|
if 'container_id' in kwargs:
|
||||||
|
filters = {"id": kwargs['container_id']}
|
||||||
|
elif 'container_name' in kwargs:
|
||||||
|
filters = {"name": kwargs['container_name']}
|
||||||
|
|
||||||
|
for container in self.sync_docker_client.containers.list(filters=filters):
|
||||||
|
postsuper_r = container.exec_run(["/usr/sbin/postsuper", "-d", "ALL"])
|
||||||
|
return self.exec_run_handler('generic', postsuper_r)
|
||||||
|
# api call: container_post - post_action: exec - cmd: system - task: fts_rescan
|
||||||
|
def container_post__exec__system__fts_rescan(self, request_json, **kwargs):
|
||||||
|
if 'container_id' in kwargs:
|
||||||
|
filters = {"id": kwargs['container_id']}
|
||||||
|
elif 'container_name' in kwargs:
|
||||||
|
filters = {"name": kwargs['container_name']}
|
||||||
|
|
||||||
|
if 'username' in request_json:
|
||||||
|
for container in self.sync_docker_client.containers.list(filters=filters):
|
||||||
|
rescan_return = container.exec_run(["/bin/bash", "-c", "/usr/bin/doveadm fts rescan -u '" + request_json['username'].replace("'", "'\\''") + "'"], user='vmail')
|
||||||
|
if rescan_return.exit_code == 0:
|
||||||
|
res = { 'type': 'success', 'msg': 'fts_rescan: rescan triggered'}
|
||||||
|
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||||
|
else:
|
||||||
|
res = { 'type': 'warning', 'msg': 'fts_rescan error'}
|
||||||
|
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||||
|
if 'all' in request_json:
|
||||||
|
for container in self.sync_docker_client.containers.list(filters=filters):
|
||||||
|
rescan_return = container.exec_run(["/bin/bash", "-c", "/usr/bin/doveadm fts rescan -A"], user='vmail')
|
||||||
|
if rescan_return.exit_code == 0:
|
||||||
|
res = { 'type': 'success', 'msg': 'fts_rescan: rescan triggered'}
|
||||||
|
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||||
|
else:
|
||||||
|
res = { 'type': 'warning', 'msg': 'fts_rescan error'}
|
||||||
|
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||||
|
# api call: container_post - post_action: exec - cmd: system - task: df
|
||||||
|
def container_post__exec__system__df(self, request_json, **kwargs):
|
||||||
|
if 'container_id' in kwargs:
|
||||||
|
filters = {"id": kwargs['container_id']}
|
||||||
|
elif 'container_name' in kwargs:
|
||||||
|
filters = {"name": kwargs['container_name']}
|
||||||
|
|
||||||
|
if 'dir' in request_json:
|
||||||
|
for container in self.sync_docker_client.containers.list(filters=filters):
|
||||||
|
df_return = container.exec_run(["/bin/bash", "-c", "/bin/df -H '" + request_json['dir'].replace("'", "'\\''") + "' | /usr/bin/tail -n1 | /usr/bin/tr -s [:blank:] | /usr/bin/tr ' ' ','"], user='nobody')
|
||||||
|
if df_return.exit_code == 0:
|
||||||
|
return df_return.output.decode('utf-8').rstrip()
|
||||||
|
else:
|
||||||
|
return "0,0,0,0,0,0"
|
||||||
|
# api call: container_post - post_action: exec - cmd: system - task: mysql_upgrade
|
||||||
|
def container_post__exec__system__mysql_upgrade(self, request_json, **kwargs):
|
||||||
|
if 'container_id' in kwargs:
|
||||||
|
filters = {"id": kwargs['container_id']}
|
||||||
|
elif 'container_name' in kwargs:
|
||||||
|
filters = {"name": kwargs['container_name']}
|
||||||
|
|
||||||
|
for container in self.sync_docker_client.containers.list(filters=filters):
|
||||||
|
sql_return = container.exec_run(["/bin/bash", "-c", "/usr/bin/mysql_upgrade -uroot -p'" + os.environ['DBROOT'].replace("'", "'\\''") + "'\n"], user='mysql')
|
||||||
|
if sql_return.exit_code == 0:
|
||||||
|
matched = False
|
||||||
|
for line in sql_return.output.decode('utf-8').split("\n"):
|
||||||
|
if 'is already upgraded to' in line:
|
||||||
|
matched = True
|
||||||
|
if matched:
|
||||||
|
res = { 'type': 'success', 'msg':'mysql_upgrade: already upgraded', 'text': sql_return.output.decode('utf-8')}
|
||||||
|
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||||
|
else:
|
||||||
|
container.restart()
|
||||||
|
res = { 'type': 'warning', 'msg':'mysql_upgrade: upgrade was applied', 'text': sql_return.output.decode('utf-8')}
|
||||||
|
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||||
|
else:
|
||||||
|
res = { 'type': 'error', 'msg': 'mysql_upgrade: error running command', 'text': sql_return.output.decode('utf-8')}
|
||||||
|
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||||
|
# api call: container_post - post_action: exec - cmd: system - task: mysql_tzinfo_to_sql
|
||||||
|
def container_post__exec__system__mysql_tzinfo_to_sql(self, request_json, **kwargs):
|
||||||
|
if 'container_id' in kwargs:
|
||||||
|
filters = {"id": kwargs['container_id']}
|
||||||
|
elif 'container_name' in kwargs:
|
||||||
|
filters = {"name": kwargs['container_name']}
|
||||||
|
|
||||||
|
for container in self.sync_docker_client.containers.list(filters=filters):
|
||||||
|
sql_return = container.exec_run(["/bin/bash", "-c", "/usr/bin/mysql_tzinfo_to_sql /usr/share/zoneinfo | /bin/sed 's/Local time zone must be set--see zic manual page/FCTY/' | /usr/bin/mysql -uroot -p'" + os.environ['DBROOT'].replace("'", "'\\''") + "' mysql \n"], user='mysql')
|
||||||
|
if sql_return.exit_code == 0:
|
||||||
|
res = { 'type': 'info', 'msg': 'mysql_tzinfo_to_sql: command completed successfully', 'text': sql_return.output.decode('utf-8')}
|
||||||
|
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||||
|
else:
|
||||||
|
res = { 'type': 'error', 'msg': 'mysql_tzinfo_to_sql: error running command', 'text': sql_return.output.decode('utf-8')}
|
||||||
|
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||||
|
# api call: container_post - post_action: exec - cmd: reload - task: dovecot
|
||||||
|
def container_post__exec__reload__dovecot(self, request_json, **kwargs):
|
||||||
|
if 'container_id' in kwargs:
|
||||||
|
filters = {"id": kwargs['container_id']}
|
||||||
|
elif 'container_name' in kwargs:
|
||||||
|
filters = {"name": kwargs['container_name']}
|
||||||
|
|
||||||
|
for container in self.sync_docker_client.containers.list(filters=filters):
|
||||||
|
reload_return = container.exec_run(["/bin/bash", "-c", "/usr/sbin/dovecot reload"])
|
||||||
|
return self.exec_run_handler('generic', reload_return)
|
||||||
|
# api call: container_post - post_action: exec - cmd: reload - task: postfix
|
||||||
|
def container_post__exec__reload__postfix(self, request_json, **kwargs):
|
||||||
|
if 'container_id' in kwargs:
|
||||||
|
filters = {"id": kwargs['container_id']}
|
||||||
|
elif 'container_name' in kwargs:
|
||||||
|
filters = {"name": kwargs['container_name']}
|
||||||
|
|
||||||
|
for container in self.sync_docker_client.containers.list(filters=filters):
|
||||||
|
reload_return = container.exec_run(["/bin/bash", "-c", "/usr/sbin/postfix reload"])
|
||||||
|
return self.exec_run_handler('generic', reload_return)
|
||||||
|
# api call: container_post - post_action: exec - cmd: reload - task: nginx
|
||||||
|
def container_post__exec__reload__nginx(self, request_json, **kwargs):
|
||||||
|
if 'container_id' in kwargs:
|
||||||
|
filters = {"id": kwargs['container_id']}
|
||||||
|
elif 'container_name' in kwargs:
|
||||||
|
filters = {"name": kwargs['container_name']}
|
||||||
|
|
||||||
|
for container in self.sync_docker_client.containers.list(filters=filters):
|
||||||
|
reload_return = container.exec_run(["/bin/sh", "-c", "/usr/sbin/nginx -s reload"])
|
||||||
|
return self.exec_run_handler('generic', reload_return)
|
||||||
|
# api call: container_post - post_action: exec - cmd: sieve - task: list
|
||||||
|
def container_post__exec__sieve__list(self, request_json, **kwargs):
|
||||||
|
if 'container_id' in kwargs:
|
||||||
|
filters = {"id": kwargs['container_id']}
|
||||||
|
elif 'container_name' in kwargs:
|
||||||
|
filters = {"name": kwargs['container_name']}
|
||||||
|
|
||||||
|
if 'username' in request_json:
|
||||||
|
for container in self.sync_docker_client.containers.list(filters=filters):
|
||||||
|
sieve_return = container.exec_run(["/bin/bash", "-c", "/usr/bin/doveadm sieve list -u '" + request_json['username'].replace("'", "'\\''") + "'"])
|
||||||
|
return self.exec_run_handler('utf8_text_only', sieve_return)
|
||||||
|
# api call: container_post - post_action: exec - cmd: sieve - task: print
|
||||||
|
def container_post__exec__sieve__print(self, request_json, **kwargs):
|
||||||
|
if 'container_id' in kwargs:
|
||||||
|
filters = {"id": kwargs['container_id']}
|
||||||
|
elif 'container_name' in kwargs:
|
||||||
|
filters = {"name": kwargs['container_name']}
|
||||||
|
|
||||||
|
if 'username' in request_json and 'script_name' in request_json:
|
||||||
|
for container in self.sync_docker_client.containers.list(filters=filters):
|
||||||
|
cmd = ["/bin/bash", "-c", "/usr/bin/doveadm sieve get -u '" + request_json['username'].replace("'", "'\\''") + "' '" + request_json['script_name'].replace("'", "'\\''") + "'"]
|
||||||
|
sieve_return = container.exec_run(cmd)
|
||||||
|
return self.exec_run_handler('utf8_text_only', sieve_return)
|
||||||
|
# api call: container_post - post_action: exec - cmd: maildir - task: cleanup
|
||||||
|
def container_post__exec__maildir__cleanup(self, request_json, **kwargs):
|
||||||
|
if 'container_id' in kwargs:
|
||||||
|
filters = {"id": kwargs['container_id']}
|
||||||
|
elif 'container_name' in kwargs:
|
||||||
|
filters = {"name": kwargs['container_name']}
|
||||||
|
|
||||||
|
if 'maildir' in request_json:
|
||||||
|
for container in self.sync_docker_client.containers.list(filters=filters):
|
||||||
|
sane_name = re.sub(r'\W+', '', request_json['maildir'])
|
||||||
|
vmail_name = request_json['maildir'].replace("'", "'\\''")
|
||||||
|
cmd_vmail = "if [[ -d '/var/vmail/" + vmail_name + "' ]]; then /bin/mv '/var/vmail/" + vmail_name + "' '/var/vmail/_garbage/" + str(int(time.time())) + "_" + sane_name + "'; fi"
|
||||||
|
index_name = request_json['maildir'].split("/")
|
||||||
|
if len(index_name) > 1:
|
||||||
|
index_name = index_name[1].replace("'", "'\\''") + "@" + index_name[0].replace("'", "'\\''")
|
||||||
|
cmd_vmail_index = "if [[ -d '/var/vmail_index/" + index_name + "' ]]; then /bin/mv '/var/vmail_index/" + index_name + "' '/var/vmail/_garbage/" + str(int(time.time())) + "_" + sane_name + "_index'; fi"
|
||||||
|
cmd = ["/bin/bash", "-c", cmd_vmail + " && " + cmd_vmail_index]
|
||||||
|
else:
|
||||||
|
cmd = ["/bin/bash", "-c", cmd_vmail]
|
||||||
|
maildir_cleanup = container.exec_run(cmd, user='vmail')
|
||||||
|
return self.exec_run_handler('generic', maildir_cleanup)
|
||||||
|
# api call: container_post - post_action: exec - cmd: rspamd - task: worker_password
|
||||||
|
def container_post__exec__rspamd__worker_password(self, request_json, **kwargs):
|
||||||
|
if 'container_id' in kwargs:
|
||||||
|
filters = {"id": kwargs['container_id']}
|
||||||
|
elif 'container_name' in kwargs:
|
||||||
|
filters = {"name": kwargs['container_name']}
|
||||||
|
|
||||||
|
if 'raw' in request_json:
|
||||||
|
for container in self.sync_docker_client.containers.list(filters=filters):
|
||||||
|
cmd = "/usr/bin/rspamadm pw -e -p '" + request_json['raw'].replace("'", "'\\''") + "' 2> /dev/null"
|
||||||
|
cmd_response = self.exec_cmd_container(container, cmd, user="_rspamd")
|
||||||
|
|
||||||
|
matched = False
|
||||||
|
for line in cmd_response.split("\n"):
|
||||||
|
if '$2$' in line:
|
||||||
|
hash = line.strip()
|
||||||
|
hash_out = re.search('\$2\$.+$', hash).group(0)
|
||||||
|
rspamd_passphrase_hash = re.sub('[^0-9a-zA-Z\$]+', '', hash_out.rstrip())
|
||||||
|
rspamd_password_filename = "/etc/rspamd/override.d/worker-controller-password.inc"
|
||||||
|
cmd = '''/bin/echo 'enable_password = "%s";' > %s && cat %s''' % (rspamd_passphrase_hash, rspamd_password_filename, rspamd_password_filename)
|
||||||
|
cmd_response = self.exec_cmd_container(container, cmd, user="_rspamd")
|
||||||
|
if rspamd_passphrase_hash.startswith("$2$") and rspamd_passphrase_hash in cmd_response:
|
||||||
|
container.restart()
|
||||||
|
matched = True
|
||||||
|
if matched:
|
||||||
|
res = { 'type': 'success', 'msg': 'command completed successfully' }
|
||||||
|
self.logger.info('success changing Rspamd password')
|
||||||
|
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||||
|
else:
|
||||||
|
self.logger.error('failed changing Rspamd password')
|
||||||
|
res = { 'type': 'danger', 'msg': 'command did not complete' }
|
||||||
|
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||||
|
|
||||||
|
# Collect host stats
|
||||||
|
async def get_host_stats(self, wait=5):
|
||||||
|
try:
|
||||||
|
system_time = datetime.now()
|
||||||
|
host_stats = {
|
||||||
|
"cpu": {
|
||||||
|
"cores": psutil.cpu_count(),
|
||||||
|
"usage": psutil.cpu_percent()
|
||||||
|
},
|
||||||
|
"memory": {
|
||||||
|
"total": psutil.virtual_memory().total,
|
||||||
|
"usage": psutil.virtual_memory().percent,
|
||||||
|
"swap": psutil.swap_memory()
|
||||||
|
},
|
||||||
|
"uptime": time.time() - psutil.boot_time(),
|
||||||
|
"system_time": system_time.strftime("%d.%m.%Y %H:%M:%S"),
|
||||||
|
"architecture": platform.machine()
|
||||||
|
}
|
||||||
|
|
||||||
|
await self.redis_client.set('host_stats', json.dumps(host_stats), ex=10)
|
||||||
|
except Exception as e:
|
||||||
|
res = {
|
||||||
|
"type": "danger",
|
||||||
|
"msg": str(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
await asyncio.sleep(wait)
|
||||||
|
self.host_stats_isUpdating = False
|
||||||
|
# Collect container stats
|
||||||
|
async def get_container_stats(self, container_id, wait=5, stop=False):
|
||||||
|
if container_id and container_id.isalnum():
|
||||||
|
try:
|
||||||
|
for container in (await self.async_docker_client.containers.list()):
|
||||||
|
if container._id == container_id:
|
||||||
|
res = await container.stats(stream=False)
|
||||||
|
|
||||||
|
if await self.redis_client.exists(container_id + '_stats'):
|
||||||
|
stats = json.loads(await self.redis_client.get(container_id + '_stats'))
|
||||||
|
else:
|
||||||
|
stats = []
|
||||||
|
stats.append(res[0])
|
||||||
|
if len(stats) > 3:
|
||||||
|
del stats[0]
|
||||||
|
await self.redis_client.set(container_id + '_stats', json.dumps(stats), ex=60)
|
||||||
|
except Exception as e:
|
||||||
|
res = {
|
||||||
|
"type": "danger",
|
||||||
|
"msg": str(e)
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
res = {
|
||||||
|
"type": "danger",
|
||||||
|
"msg": "no or invalid id defined"
|
||||||
|
}
|
||||||
|
|
||||||
|
await asyncio.sleep(wait)
|
||||||
|
if stop == True:
|
||||||
|
# update task was called second time, stop
|
||||||
|
self.containerIds_to_update.remove(container_id)
|
||||||
|
else:
|
||||||
|
# call update task a second time
|
||||||
|
await self.get_container_stats(container_id, wait=0, stop=True)
|
||||||
|
|
||||||
|
def exec_cmd_container(self, container, cmd, user, timeout=2, shell_cmd="/bin/bash"):
|
||||||
|
def recv_socket_data(c_socket, timeout):
|
||||||
|
c_socket.setblocking(0)
|
||||||
|
total_data=[]
|
||||||
|
data=''
|
||||||
|
begin=time.time()
|
||||||
|
while True:
|
||||||
|
if total_data and time.time()-begin > timeout:
|
||||||
|
break
|
||||||
|
elif time.time()-begin > timeout*2:
|
||||||
|
break
|
||||||
|
try:
|
||||||
|
data = c_socket.recv(8192)
|
||||||
|
if data:
|
||||||
|
total_data.append(data.decode('utf-8'))
|
||||||
|
#change the beginning time for measurement
|
||||||
|
begin=time.time()
|
||||||
|
else:
|
||||||
|
#sleep for sometime to indicate a gap
|
||||||
|
time.sleep(0.1)
|
||||||
|
break
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
return ''.join(total_data)
|
||||||
|
|
||||||
|
try :
|
||||||
|
socket = container.exec_run([shell_cmd], stdin=True, socket=True, user=user).output._sock
|
||||||
|
if not cmd.endswith("\n"):
|
||||||
|
cmd = cmd + "\n"
|
||||||
|
socket.send(cmd.encode('utf-8'))
|
||||||
|
data = recv_socket_data(socket, timeout)
|
||||||
|
socket.close()
|
||||||
|
return data
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error("error - exec_cmd_container: %s" % str(e))
|
||||||
|
traceback.print_exc(file=sys.stdout)
|
||||||
|
|
||||||
|
def exec_run_handler(self, type, output):
|
||||||
|
if type == 'generic':
|
||||||
|
if output.exit_code == 0:
|
||||||
|
res = { 'type': 'success', 'msg': 'command completed successfully' }
|
||||||
|
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||||
|
else:
|
||||||
|
res = { 'type': 'danger', 'msg': 'command failed: ' + output.output.decode('utf-8') }
|
||||||
|
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||||
|
if type == 'utf8_text_only':
|
||||||
|
return Response(content=output.output.decode('utf-8'), media_type="text/plain")
|
0
data/Dockerfiles/dockerapi/modules/__init__.py
Normal file
0
data/Dockerfiles/dockerapi/modules/__init__.py
Normal file
@ -114,7 +114,7 @@
|
|||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
proxy_redirect off;
|
proxy_redirect off;
|
||||||
error_page 403 /_rspamderror.php;
|
error_page 401 /_rspamderror.php;
|
||||||
}
|
}
|
||||||
proxy_pass http://rspamd:11334/;
|
proxy_pass http://rspamd:11334/;
|
||||||
proxy_set_header Host $http_host;
|
proxy_set_header Host $http_host;
|
||||||
|
@ -68,3 +68,39 @@ WL_FWD_HOST {
|
|||||||
ENCRYPTED_CHAT {
|
ENCRYPTED_CHAT {
|
||||||
expression = "CHAT_VERSION_HEADER & ENCRYPTED_PGP";
|
expression = "CHAT_VERSION_HEADER & ENCRYPTED_PGP";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CLAMD_SPAM_FOUND {
|
||||||
|
expression = "CLAM_SECI_SPAM & !MAILCOW_WHITE";
|
||||||
|
description = "Probably Spam, Securite Spam Flag set through ClamAV";
|
||||||
|
score = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
CLAMD_BAD_PDF {
|
||||||
|
expression = "CLAM_SECI_PDF & !MAILCOW_WHITE";
|
||||||
|
description = "Bad PDF Found, Securite bad PDF Flag set through ClamAV";
|
||||||
|
score = 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
CLAMD_BAD_JPG {
|
||||||
|
expression = "CLAM_SECI_JPG & !MAILCOW_WHITE";
|
||||||
|
description = "Bad JPG Found, Securite bad JPG Flag set through ClamAV";
|
||||||
|
score = 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
CLAMD_ASCII_MALWARE {
|
||||||
|
expression = "CLAM_SECI_ASCII & !MAILCOW_WHITE";
|
||||||
|
description = "ASCII malware found, Securite ASCII malware Flag set through ClamAV";
|
||||||
|
score = 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
CLAMD_HTML_MALWARE {
|
||||||
|
expression = "CLAM_SECI_HTML & !MAILCOW_WHITE";
|
||||||
|
description = "HTML malware found, Securite HTML malware Flag set through ClamAV";
|
||||||
|
score = 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
CLAMD_JS_MALWARE {
|
||||||
|
expression = "CLAM_SECI_JS & !MAILCOW_WHITE";
|
||||||
|
description = "JS malware found, Securite JS malware Flag set through ClamAV";
|
||||||
|
score = 8;
|
||||||
|
}
|
@ -340,6 +340,10 @@ rspamd_config:register_symbol({
|
|||||||
if not bcc_dest then
|
if not bcc_dest then
|
||||||
return -- stop
|
return -- stop
|
||||||
end
|
end
|
||||||
|
-- dot stuff content before sending
|
||||||
|
local email_content = tostring(task:get_content())
|
||||||
|
email_content = string.gsub(email_content, "\r\n%.", "\r\n..")
|
||||||
|
-- send mail
|
||||||
lua_smtp.sendmail({
|
lua_smtp.sendmail({
|
||||||
task = task,
|
task = task,
|
||||||
host = os.getenv("IPV4_NETWORK") .. '.253',
|
host = os.getenv("IPV4_NETWORK") .. '.253',
|
||||||
@ -347,8 +351,8 @@ rspamd_config:register_symbol({
|
|||||||
from = task:get_from(stp)[1].addr,
|
from = task:get_from(stp)[1].addr,
|
||||||
recipients = bcc_dest,
|
recipients = bcc_dest,
|
||||||
helo = 'bcc',
|
helo = 'bcc',
|
||||||
timeout = 10,
|
timeout = 20,
|
||||||
}, task:get_content(), sendmail_cb)
|
}, email_content, sendmail_cb)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- determine from
|
-- determine from
|
||||||
|
@ -80,6 +80,11 @@ foreach ($RSPAMD_MAPS['regex'] as $rspamd_regex_desc => $rspamd_regex_map) {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// cors settings
|
||||||
|
$cors_settings = cors('get');
|
||||||
|
$cors_settings['allowed_origins'] = str_replace(", ", "\n", $cors_settings['allowed_origins']);
|
||||||
|
$cors_settings['allowed_methods'] = explode(", ", $cors_settings['allowed_methods']);
|
||||||
|
|
||||||
$template = 'admin.twig';
|
$template = 'admin.twig';
|
||||||
$template_data = [
|
$template_data = [
|
||||||
'tfa_data' => $tfa_data,
|
'tfa_data' => $tfa_data,
|
||||||
@ -106,6 +111,7 @@ $template_data = [
|
|||||||
'ip_check' => customize('get', 'ip_check'),
|
'ip_check' => customize('get', 'ip_check'),
|
||||||
'password_complexity' => password_complexity('get'),
|
'password_complexity' => password_complexity('get'),
|
||||||
'show_rspamd_global_filters' => @$_SESSION['show_rspamd_global_filters'],
|
'show_rspamd_global_filters' => @$_SESSION['show_rspamd_global_filters'],
|
||||||
|
'cors_settings' => $cors_settings,
|
||||||
'lang_admin' => json_encode($lang['admin']),
|
'lang_admin' => json_encode($lang['admin']),
|
||||||
'lang_datatables' => json_encode($lang['datatables'])
|
'lang_datatables' => json_encode($lang['datatables'])
|
||||||
];
|
];
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
openapi: 3.0.0
|
openapi: 3.1.0
|
||||||
info:
|
info:
|
||||||
description: >-
|
description: >-
|
||||||
mailcow is complete e-mailing solution with advanced antispam, antivirus,
|
mailcow is complete e-mailing solution with advanced antispam, antivirus,
|
||||||
@ -5602,6 +5602,50 @@ paths:
|
|||||||
description: You can list all mailboxes existing in system for a specific domain.
|
description: You can list all mailboxes existing in system for a specific domain.
|
||||||
operationId: Get mailboxes of a domain
|
operationId: Get mailboxes of a domain
|
||||||
summary: Get mailboxes of a domain
|
summary: Get mailboxes of a domain
|
||||||
|
/api/v1/edit/cors:
|
||||||
|
post:
|
||||||
|
responses:
|
||||||
|
"401":
|
||||||
|
$ref: "#/components/responses/Unauthorized"
|
||||||
|
"200":
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
examples:
|
||||||
|
response:
|
||||||
|
value:
|
||||||
|
- type: "success"
|
||||||
|
log: ["cors", "edit", {"allowed_origins": ["*", "mail.mailcow.tld"], "allowed_methods": ["POST", "GET", "DELETE", "PUT"]}]
|
||||||
|
msg: "cors_headers_edited"
|
||||||
|
description: OK
|
||||||
|
headers: { }
|
||||||
|
tags:
|
||||||
|
- Cross-Origin Resource Sharing (CORS)
|
||||||
|
description: >-
|
||||||
|
This endpoint allows you to manage Cross-Origin Resource Sharing (CORS) settings for the API.
|
||||||
|
CORS is a security feature implemented by web browsers to prevent unauthorized cross-origin requests.
|
||||||
|
By editing the CORS settings, you can specify which domains and which methods are permitted to access the API resources from outside the mailcow domain.
|
||||||
|
operationId: Edit Cross-Origin Resource Sharing (CORS) settings
|
||||||
|
requestBody:
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
example:
|
||||||
|
attr:
|
||||||
|
allowed_origins: ["*", "mail.mailcow.tld"]
|
||||||
|
allowed_methods: ["POST", "GET", "DELETE", "PUT"]
|
||||||
|
properties:
|
||||||
|
attr:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
allowed_origins:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
allowed_methods:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
summary: Edit Cross-Origin Resource Sharing (CORS) settings
|
||||||
|
|
||||||
tags:
|
tags:
|
||||||
- name: Domains
|
- name: Domains
|
||||||
@ -5646,3 +5690,5 @@ tags:
|
|||||||
description: Get the status of your cow
|
description: Get the status of your cow
|
||||||
- name: Ratelimits
|
- name: Ratelimits
|
||||||
description: Edit domain ratelimits
|
description: Edit domain ratelimits
|
||||||
|
- name: Cross-Origin Resource Sharing (CORS)
|
||||||
|
description: Manage Cross-Origin Resource Sharing (CORS) settings
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
window.onload = function() {
|
window.onload = function() {
|
||||||
// Begin Swagger UI call region
|
// Begin Swagger UI call region
|
||||||
const ui = SwaggerUIBundle({
|
window.ui = SwaggerUIBundle({
|
||||||
urls: [{url: "/api/openapi.yaml", name: "mailcow API"}],
|
urls: [{url: "/api/openapi.yaml", name: "mailcow API"}],
|
||||||
dom_id: '#swagger-ui',
|
dom_id: '#swagger-ui',
|
||||||
deepLinking: true,
|
deepLinking: true,
|
||||||
@ -15,5 +15,4 @@ window.onload = function() {
|
|||||||
});
|
});
|
||||||
// End Swagger UI call region
|
// End Swagger UI call region
|
||||||
|
|
||||||
window.ui = ui;
|
|
||||||
};
|
};
|
||||||
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -192,5 +192,16 @@ function docker($action, $service_name = null, $attr1 = null, $attr2 = null, $ex
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
break;
|
break;
|
||||||
|
case 'broadcast':
|
||||||
|
$request = array(
|
||||||
|
"api_call" => "container_post",
|
||||||
|
"container_name" => $service_name,
|
||||||
|
"post_action" => $attr1,
|
||||||
|
"request" => $attr2
|
||||||
|
);
|
||||||
|
|
||||||
|
$redis->publish("MC_CHANNEL", json_encode($request));
|
||||||
|
return true;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -526,8 +526,9 @@ function logger($_data = false) {
|
|||||||
':remote' => get_remote_ip()
|
':remote' => get_remote_ip()
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
catch (Exception $e) {
|
catch (PDOException $e) {
|
||||||
// Do nothing
|
# handle the exception here, as the exception handler function results in a white page
|
||||||
|
error_log($e->getMessage(), 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -2131,6 +2132,120 @@ function rspamd_ui($action, $data = null) {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
function cors($action, $data = null) {
|
||||||
|
global $redis;
|
||||||
|
|
||||||
|
switch ($action) {
|
||||||
|
case "edit":
|
||||||
|
if ($_SESSION['mailcow_cc_role'] != "admin") {
|
||||||
|
$_SESSION['return'][] = array(
|
||||||
|
'type' => 'danger',
|
||||||
|
'log' => array(__FUNCTION__, $action, $data),
|
||||||
|
'msg' => 'access_denied'
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$allowed_origins = isset($data['allowed_origins']) ? $data['allowed_origins'] : array($_SERVER['SERVER_NAME']);
|
||||||
|
$allowed_origins = !is_array($allowed_origins) ? array_filter(array_map('trim', explode("\n", $allowed_origins))) : $allowed_origins;
|
||||||
|
foreach ($allowed_origins as $origin) {
|
||||||
|
if (!filter_var($origin, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME) && $origin != '*') {
|
||||||
|
$_SESSION['return'][] = array(
|
||||||
|
'type' => 'danger',
|
||||||
|
'log' => array(__FUNCTION__, $action, $data),
|
||||||
|
'msg' => 'cors_invalid_origin'
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$allowed_methods = isset($data['allowed_methods']) ? $data['allowed_methods'] : array('GET', 'POST', 'PUT', 'DELETE');
|
||||||
|
$allowed_methods = !is_array($allowed_methods) ? array_map('trim', preg_split( "/( |,|;|\n)/", $allowed_methods)) : $allowed_methods;
|
||||||
|
$available_methods = array('GET', 'POST', 'PUT', 'DELETE');
|
||||||
|
foreach ($allowed_methods as $method) {
|
||||||
|
if (!in_array($method, $available_methods)) {
|
||||||
|
$_SESSION['return'][] = array(
|
||||||
|
'type' => 'danger',
|
||||||
|
'log' => array(__FUNCTION__, $action, $data),
|
||||||
|
'msg' => 'cors_invalid_method'
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$redis->hMSet('CORS_SETTINGS', array(
|
||||||
|
'allowed_origins' => implode(', ', $allowed_origins),
|
||||||
|
'allowed_methods' => implode(', ', $allowed_methods)
|
||||||
|
));
|
||||||
|
} catch (RedisException $e) {
|
||||||
|
$_SESSION['return'][] = array(
|
||||||
|
'type' => 'danger',
|
||||||
|
'log' => array(__FUNCTION__, $action, $data),
|
||||||
|
'msg' => array('redis_error', $e)
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$_SESSION['return'][] = array(
|
||||||
|
'type' => 'success',
|
||||||
|
'log' => array(__FUNCTION__, $action, $data),
|
||||||
|
'msg' => 'cors_headers_edited'
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
break;
|
||||||
|
case "get":
|
||||||
|
try {
|
||||||
|
$cors_settings = $redis->hMGet('CORS_SETTINGS', array('allowed_origins', 'allowed_methods'));
|
||||||
|
} catch (RedisException $e) {
|
||||||
|
$_SESSION['return'][] = array(
|
||||||
|
'type' => 'danger',
|
||||||
|
'log' => array(__FUNCTION__, $action, $data),
|
||||||
|
'msg' => array('redis_error', $e)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$cors_settings = !$cors_settings ? array('allowed_origins' => $_SERVER['SERVER_NAME'], 'allowed_methods' => 'GET, POST, PUT, DELETE') : $cors_settings;
|
||||||
|
$cors_settings['allowed_origins'] = empty($cors_settings['allowed_origins']) ? $_SERVER['SERVER_NAME'] : $cors_settings['allowed_origins'];
|
||||||
|
$cors_settings['allowed_methods'] = empty($cors_settings['allowed_methods']) ? 'GET, POST, PUT, DELETE, OPTION' : $cors_settings['allowed_methods'];
|
||||||
|
|
||||||
|
return $cors_settings;
|
||||||
|
break;
|
||||||
|
case "set_headers":
|
||||||
|
$cors_settings = cors('get');
|
||||||
|
// check if requested origin is in allowed origins
|
||||||
|
$allowed_origins = explode(', ', $cors_settings['allowed_origins']);
|
||||||
|
$cors_settings['allowed_origins'] = $allowed_origins[0];
|
||||||
|
if (in_array('*', $allowed_origins)){
|
||||||
|
$cors_settings['allowed_origins'] = '*';
|
||||||
|
} else if (in_array($_SERVER['HTTP_ORIGIN'], $allowed_origins)) {
|
||||||
|
$cors_settings['allowed_origins'] = $_SERVER['HTTP_ORIGIN'];
|
||||||
|
}
|
||||||
|
// always allow OPTIONS for preflight request
|
||||||
|
$cors_settings["allowed_methods"] = empty($cors_settings["allowed_methods"]) ? 'OPTIONS' : $cors_settings["allowed_methods"] . ', ' . 'OPTIONS';
|
||||||
|
|
||||||
|
header('Access-Control-Allow-Origin: ' . $cors_settings['allowed_origins']);
|
||||||
|
header('Access-Control-Allow-Methods: '. $cors_settings['allowed_methods']);
|
||||||
|
header('Access-Control-Allow-Headers: Accept, Content-Type, X-Api-Key, Origin');
|
||||||
|
|
||||||
|
// Access-Control settings requested, this is just a preflight request
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] == 'OPTIONS' &&
|
||||||
|
isset($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_METHOD']) &&
|
||||||
|
isset($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_HEADERS'])) {
|
||||||
|
|
||||||
|
$allowed_methods = explode(', ', $cors_settings["allowed_methods"]);
|
||||||
|
if (in_array($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_METHOD'], $allowed_methods, true))
|
||||||
|
// method allowed send 200 OK
|
||||||
|
http_response_code(200);
|
||||||
|
else
|
||||||
|
// method not allowed send 405 METHOD NOT ALLOWED
|
||||||
|
http_response_code(405);
|
||||||
|
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function get_logs($application, $lines = false) {
|
function get_logs($application, $lines = false) {
|
||||||
if ($lines === false) {
|
if ($lines === false) {
|
||||||
|
@ -4930,13 +4930,19 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
|||||||
if (!empty($mailbox_details['domain']) && !empty($mailbox_details['local_part'])) {
|
if (!empty($mailbox_details['domain']) && !empty($mailbox_details['local_part'])) {
|
||||||
$maildir = $mailbox_details['domain'] . '/' . $mailbox_details['local_part'];
|
$maildir = $mailbox_details['domain'] . '/' . $mailbox_details['local_part'];
|
||||||
$exec_fields = array('cmd' => 'maildir', 'task' => 'cleanup', 'maildir' => $maildir);
|
$exec_fields = array('cmd' => 'maildir', 'task' => 'cleanup', 'maildir' => $maildir);
|
||||||
$maildir_gc = json_decode(docker('post', 'dovecot-mailcow', 'exec', $exec_fields), true);
|
|
||||||
if ($maildir_gc['type'] != 'success') {
|
if (getenv("CLUSTERMODE") == "replication") {
|
||||||
$_SESSION['return'][] = array(
|
// broadcast to each dovecot container
|
||||||
'type' => 'warning',
|
docker('broadcast', 'dovecot-mailcow', 'exec', $exec_fields);
|
||||||
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
} else {
|
||||||
'msg' => 'Could not move maildir to garbage collector: ' . $maildir_gc['msg']
|
$maildir_gc = json_decode(docker('post', 'dovecot-mailcow', 'exec', $exec_fields), true);
|
||||||
);
|
if ($maildir_gc['type'] != 'success') {
|
||||||
|
$_SESSION['return'][] = array(
|
||||||
|
'type' => 'warning',
|
||||||
|
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
||||||
|
'msg' => 'Could not move maildir to garbage collector: ' . $maildir_gc['msg']
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
@ -1173,7 +1173,7 @@ jQuery(function($){
|
|||||||
|
|
||||||
if (table = $('#' + log_table).DataTable()) {
|
if (table = $('#' + log_table).DataTable()) {
|
||||||
var heading = $('#' + log_table).closest('.card').find('.card-header');
|
var heading = $('#' + log_table).closest('.card').find('.card-header');
|
||||||
var load_rows = (table.data().length + 1) + '-' + (table.data().length + new_nrows)
|
var load_rows = (table.data().count() + 1) + '-' + (table.data().count() + new_nrows)
|
||||||
|
|
||||||
$.get('/api/v1/get/logs/' + log_url + '/' + load_rows).then(function(data){
|
$.get('/api/v1/get/logs/' + log_url + '/' + load_rows).then(function(data){
|
||||||
if (data.length === undefined) { mailcow_alert_box(lang.no_new_rows, "info"); return; }
|
if (data.length === undefined) { mailcow_alert_box(lang.no_new_rows, "info"); return; }
|
||||||
|
@ -2,9 +2,9 @@
|
|||||||
/*
|
/*
|
||||||
see /api
|
see /api
|
||||||
*/
|
*/
|
||||||
|
|
||||||
header('Content-Type: application/json');
|
|
||||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
|
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
|
||||||
|
cors("set_headers");
|
||||||
|
header('Content-Type: application/json');
|
||||||
error_reporting(0);
|
error_reporting(0);
|
||||||
|
|
||||||
function api_log($_data) {
|
function api_log($_data) {
|
||||||
@ -288,18 +288,18 @@ if (isset($_GET['query'])) {
|
|||||||
case "domain-admin":
|
case "domain-admin":
|
||||||
process_add_return(domain_admin('add', $attr));
|
process_add_return(domain_admin('add', $attr));
|
||||||
break;
|
break;
|
||||||
case "sso":
|
case "sso":
|
||||||
switch ($object) {
|
switch ($object) {
|
||||||
case "domain-admin":
|
case "domain-admin":
|
||||||
$data = domain_admin_sso('issue', $attr);
|
$data = domain_admin_sso('issue', $attr);
|
||||||
if($data) {
|
if($data) {
|
||||||
echo json_encode($data);
|
echo json_encode($data);
|
||||||
exit(0);
|
exit(0);
|
||||||
}
|
}
|
||||||
process_add_return($data);
|
process_add_return($data);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case "admin":
|
case "admin":
|
||||||
process_add_return(admin('add', $attr));
|
process_add_return(admin('add', $attr));
|
||||||
break;
|
break;
|
||||||
@ -1946,6 +1946,9 @@ if (isset($_GET['query'])) {
|
|||||||
process_edit_return(edit_user_account($attr));
|
process_edit_return(edit_user_account($attr));
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case "cors":
|
||||||
|
process_edit_return(cors('edit', $attr));
|
||||||
|
break;
|
||||||
// return no route found if no case is matched
|
// return no route found if no case is matched
|
||||||
default:
|
default:
|
||||||
http_response_code(404);
|
http_response_code(404);
|
||||||
|
@ -147,6 +147,7 @@
|
|||||||
"change_logo": "Logo ändern",
|
"change_logo": "Logo ändern",
|
||||||
"configuration": "Konfiguration",
|
"configuration": "Konfiguration",
|
||||||
"convert_html_to_text": "Konvertiere HTML zu reinem Text",
|
"convert_html_to_text": "Konvertiere HTML zu reinem Text",
|
||||||
|
"cors_settings": "CORS Einstellungen",
|
||||||
"credentials_transport_warning": "<b>Warnung</b>: Das Hinzufügen einer neuen Regel bewirkt die Aktualisierung der Authentifizierungsdaten aller vorhandenen Einträge mit identischem Next Hop.",
|
"credentials_transport_warning": "<b>Warnung</b>: Das Hinzufügen einer neuen Regel bewirkt die Aktualisierung der Authentifizierungsdaten aller vorhandenen Einträge mit identischem Next Hop.",
|
||||||
"customer_id": "Kunde",
|
"customer_id": "Kunde",
|
||||||
"customize": "UI-Anpassung",
|
"customize": "UI-Anpassung",
|
||||||
@ -358,6 +359,8 @@
|
|||||||
"bcc_exists": "Ein BCC-Map-Eintrag %s existiert bereits als Typ %s",
|
"bcc_exists": "Ein BCC-Map-Eintrag %s existiert bereits als Typ %s",
|
||||||
"bcc_must_be_email": "BCC-Ziel %s ist keine gültige E-Mail-Adresse",
|
"bcc_must_be_email": "BCC-Ziel %s ist keine gültige E-Mail-Adresse",
|
||||||
"comment_too_long": "Kommentarfeld darf maximal 160 Zeichen enthalten",
|
"comment_too_long": "Kommentarfeld darf maximal 160 Zeichen enthalten",
|
||||||
|
"cors_invalid_method": "Allow-Methods enthält eine ungültige Methode",
|
||||||
|
"cors_invalid_origin": "Allow-Origins enthält eine ungültige Origin",
|
||||||
"defquota_empty": "Standard-Quota darf nicht 0 sein",
|
"defquota_empty": "Standard-Quota darf nicht 0 sein",
|
||||||
"demo_mode_enabled": "Demo Mode ist aktiviert",
|
"demo_mode_enabled": "Demo Mode ist aktiviert",
|
||||||
"description_invalid": "Ressourcenbeschreibung für %s ist ungültig",
|
"description_invalid": "Ressourcenbeschreibung für %s ist ungültig",
|
||||||
@ -998,6 +1001,7 @@
|
|||||||
"bcc_deleted": "BCC-Map-Einträge gelöscht: %s",
|
"bcc_deleted": "BCC-Map-Einträge gelöscht: %s",
|
||||||
"bcc_edited": "BCC-Map-Eintrag %s wurde geändert",
|
"bcc_edited": "BCC-Map-Eintrag %s wurde geändert",
|
||||||
"bcc_saved": "BCC- Map-Eintrag wurde gespeichert",
|
"bcc_saved": "BCC- Map-Eintrag wurde gespeichert",
|
||||||
|
"cors_headers_edited": "CORS Einstellungen wurden erfolgreich gespeichert",
|
||||||
"db_init_complete": "Datenbankinitialisierung abgeschlossen",
|
"db_init_complete": "Datenbankinitialisierung abgeschlossen",
|
||||||
"delete_filter": "Filter-ID %s wurde gelöscht",
|
"delete_filter": "Filter-ID %s wurde gelöscht",
|
||||||
"delete_filters": "Filter gelöscht: %s",
|
"delete_filters": "Filter gelöscht: %s",
|
||||||
|
@ -133,6 +133,8 @@
|
|||||||
"admins": "Administrators",
|
"admins": "Administrators",
|
||||||
"admins_ldap": "LDAP Administrators",
|
"admins_ldap": "LDAP Administrators",
|
||||||
"advanced_settings": "Advanced settings",
|
"advanced_settings": "Advanced settings",
|
||||||
|
"allowed_methods": "Access-Control-Allow-Methods",
|
||||||
|
"allowed_origins": "Access-Control-Allow-Origin",
|
||||||
"api_allow_from": "Allow API access from these IPs/CIDR network notations",
|
"api_allow_from": "Allow API access from these IPs/CIDR network notations",
|
||||||
"api_info": "The API is a work in progress. The documentation can be found at <a href=\"/api\">/api</a>",
|
"api_info": "The API is a work in progress. The documentation can be found at <a href=\"/api\">/api</a>",
|
||||||
"api_key": "API key",
|
"api_key": "API key",
|
||||||
@ -149,6 +151,7 @@
|
|||||||
"change_logo": "Change logo",
|
"change_logo": "Change logo",
|
||||||
"configuration": "Configuration",
|
"configuration": "Configuration",
|
||||||
"convert_html_to_text": "Convert HTML to plain text",
|
"convert_html_to_text": "Convert HTML to plain text",
|
||||||
|
"cors_settings": "CORS Settings",
|
||||||
"credentials_transport_warning": "<b>Warning</b>: Adding a new transport map entry will update the credentials for all entries with a matching next hop column.",
|
"credentials_transport_warning": "<b>Warning</b>: Adding a new transport map entry will update the credentials for all entries with a matching next hop column.",
|
||||||
"customer_id": "Customer ID",
|
"customer_id": "Customer ID",
|
||||||
"customize": "Customize",
|
"customize": "Customize",
|
||||||
@ -358,6 +361,8 @@
|
|||||||
"bcc_exists": "A BCC map %s exists for type %s",
|
"bcc_exists": "A BCC map %s exists for type %s",
|
||||||
"bcc_must_be_email": "BCC destination %s is not a valid email address",
|
"bcc_must_be_email": "BCC destination %s is not a valid email address",
|
||||||
"comment_too_long": "Comment too long, max 160 chars allowed",
|
"comment_too_long": "Comment too long, max 160 chars allowed",
|
||||||
|
"cors_invalid_method": "Invalid Allow-Method specified",
|
||||||
|
"cors_invalid_origin": "Invalid Allow-Origin specified",
|
||||||
"defquota_empty": "Default quota per mailbox must not be 0.",
|
"defquota_empty": "Default quota per mailbox must not be 0.",
|
||||||
"demo_mode_enabled": "Demo Mode is enabled",
|
"demo_mode_enabled": "Demo Mode is enabled",
|
||||||
"description_invalid": "Resource description for %s is invalid",
|
"description_invalid": "Resource description for %s is invalid",
|
||||||
@ -1005,6 +1010,7 @@
|
|||||||
"bcc_deleted": "BCC map entries deleted: %s",
|
"bcc_deleted": "BCC map entries deleted: %s",
|
||||||
"bcc_edited": "BCC map entry %s edited",
|
"bcc_edited": "BCC map entry %s edited",
|
||||||
"bcc_saved": "BCC map entry saved",
|
"bcc_saved": "BCC map entry saved",
|
||||||
|
"cors_headers_edited": "CORS settings have been saved",
|
||||||
"db_init_complete": "Database initialization completed",
|
"db_init_complete": "Database initialization completed",
|
||||||
"delete_filter": "Deleted filters ID %s",
|
"delete_filter": "Deleted filters ID %s",
|
||||||
"delete_filters": "Deleted filters: %s",
|
"delete_filters": "Deleted filters: %s",
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
<div role="tabpanel" class="tab-pane fade show active" id="tab-config-admins" role="tabpanel" aria-labelledby="tab-config-admins">
|
<div class="tab-pane fade show active" id="tab-config-admins" role="tabpanel" aria-labelledby="tab-config-admins">
|
||||||
<div class="card mb-4">
|
<div class="card mb-4">
|
||||||
<div class="card-header bg-danger text-white d-flex fs-5">
|
<div class="card-header bg-danger text-white d-flex fs-5">
|
||||||
<button class="btn d-md-none text-white flex-grow-1 text-start" data-bs-target="#collapse-tab-config-admins" data-bs-toggle="collapse" aria-controls="collapse-tab-config-admins">
|
<button class="btn d-md-none text-white flex-grow-1 text-start" data-bs-target="#collapse-tab-config-admins" data-bs-toggle="collapse" aria-controls="collapse-tab-config-admins">
|
||||||
@ -97,6 +97,39 @@
|
|||||||
<div class="col-lg-12">
|
<div class="col-lg-12">
|
||||||
<p class="text-muted">{{ lang.admin.api_info|raw }}</p>
|
<p class="text-muted">{{ lang.admin.api_info|raw }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col-lg-12">
|
||||||
|
<div class="card mb-3">
|
||||||
|
<div class="card-header">
|
||||||
|
<h4 class="card-title"><i class="bi bi-file-earmark-arrow-down"></i> {{ lang.admin.cors_settings }}</h4>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form class="form-horizontal" autocapitalize="none" autocorrect="off" role="form" data-id="editcors" method="post">
|
||||||
|
<div class="row mb-4">
|
||||||
|
<label class="control-label col-sm-2 mb-4" for="allowed_origins">{{ lang.admin.allowed_origins }}</label>
|
||||||
|
<div class="col-sm-9 mb-4">
|
||||||
|
<textarea class="form-control textarea-code" rows="7" name="allowed_origins" id="allowed_origins">{{ cors_settings.allowed_origins }}</textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mb-4">
|
||||||
|
<label class="control-label col-sm-2" for="allowed_methods">{{ lang.admin.allowed_methods }}</label>
|
||||||
|
<div class="col-sm-9">
|
||||||
|
<select name="allowed_methods" id="allowed_methods" multiple class="form-control">
|
||||||
|
<option value="POST"{% if "POST" in cors_settings.allowed_methods %} selected{% endif %}>POST</option>
|
||||||
|
<option value="GET"{% if "GET" in cors_settings.allowed_methods %} selected{% endif %}>GET</option>
|
||||||
|
<option value="DELETE"{% if "DELETE" in cors_settings.allowed_methods %} selected{% endif %}>DELETE</option>
|
||||||
|
<option value="PUT"{% if "PUT" in cors_settings.allowed_methods %} selected{% endif %}>PUT</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="offset-sm-2 col-sm-9">
|
||||||
|
<button class="btn btn-sm visible-xs-block visible-sm-inline visible-md-inline visible-lg-inline btn-success" data-item="cors" data-api-url="edit/cors" data-id="editcors" data-action="edit_selected" href="#"><i class="bi bi-check-lg"></i> {{ lang.admin.save }}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="col-lg-6">
|
<div class="col-lg-6">
|
||||||
<div class="card mb-3">
|
<div class="card mb-3">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
@ -194,7 +227,7 @@
|
|||||||
|
|
||||||
<div class="card mb-4">
|
<div class="card mb-4">
|
||||||
<div class="card-header d-flex fs-5">
|
<div class="card-header d-flex fs-5">
|
||||||
<button class="btn d-md-none flex-grow-1 text-start" data-bs-target="#collapse-tab-config-dadmins" data-bs-toggle="collapse" aria-controls="ollapse-tab-config-dadmins">
|
<button class="btn d-md-none flex-grow-1 text-start" data-bs-target="#collapse-tab-config-dadmins" data-bs-toggle="collapse" aria-controls="collapse-tab-config-dadmins">
|
||||||
{{ lang.admin.domain_admins }}
|
{{ lang.admin.domain_admins }}
|
||||||
</button>
|
</button>
|
||||||
<span class="d-none d-md-block">{{ lang.admin.domain_admins }}</span>
|
<span class="d-none d-md-block">{{ lang.admin.domain_admins }}</span>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<div role="tabpanel" class="tab-pane fade" id="tab-config-customize" role="tabpanel" aria-labelledby="tab-config-customize">
|
<div class="tab-pane fade" id="tab-config-customize" role="tabpanel" aria-labelledby="tab-config-customize">
|
||||||
<div class="card mb-4">
|
<div class="card mb-4">
|
||||||
<div class="card-header d-flex fs-5">
|
<div class="card-header d-flex fs-5">
|
||||||
<button class="btn d-md-none flex-grow-1 text-start" data-bs-target="#collapse-tab-config-customize" data-bs-toggle="collapse" aria-controls="ollapse-tab-config-customize">
|
<button class="btn d-md-none flex-grow-1 text-start" data-bs-target="#collapse-tab-config-customize" data-bs-toggle="collapse" aria-controls="collapse-tab-config-customize">
|
||||||
{{ lang.admin.customize }}
|
{{ lang.admin.customize }}
|
||||||
</button>
|
</button>
|
||||||
<span class="d-none d-md-block">{{ lang.admin.customize }}</span>
|
<span class="d-none d-md-block">{{ lang.admin.customize }}</span>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<div role="tabpanel" class="tab-pane fade" id="tab-config-dkim" role="tabpanel" aria-labelledby="tab-config-dkim">
|
<div class="tab-pane fade" id="tab-config-dkim" role="tabpanel" aria-labelledby="tab-config-dkim">
|
||||||
<div class="card mb-4">
|
<div class="card mb-4">
|
||||||
<div class="card-header d-flex fs-5">
|
<div class="card-header d-flex fs-5">
|
||||||
<button class="btn d-md-none flex-grow-1 text-start" data-bs-target="#collapse-tab-config-dkim" data-bs-toggle="collapse" aria-controls="ollapse-tab-config-dkim">
|
<button class="btn d-md-none flex-grow-1 text-start" data-bs-target="#collapse-tab-config-dkim" data-bs-toggle="collapse" aria-controls="collapse-tab-config-dkim">
|
||||||
{{ lang.admin.dkim_keys }}
|
{{ lang.admin.dkim_keys }}
|
||||||
</button>
|
</button>
|
||||||
<span class="d-none d-md-block">{{ lang.admin.dkim_keys }}</span>
|
<span class="d-none d-md-block">{{ lang.admin.dkim_keys }}</span>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<div role="tabpanel" class="tab-pane fade" id="tab-config-f2b" role="tabpanel" aria-labelledby="tab-config-f2b">
|
<div class="tab-pane fade" id="tab-config-f2b" role="tabpanel" aria-labelledby="tab-config-f2b">
|
||||||
<div class="card mb-4">
|
<div class="card mb-4">
|
||||||
<div class="card-header d-flex fs-5">
|
<div class="card-header d-flex fs-5">
|
||||||
<button class="btn d-md-none flex-grow-1 text-start" data-bs-target="#collapse-tab-config-f2b" data-bs-toggle="collapse" aria-controls="ollapse-tab-config-f2b">
|
<button class="btn d-md-none flex-grow-1 text-start" data-bs-target="#collapse-tab-config-f2b" data-bs-toggle="collapse" aria-controls="collapse-tab-config-f2b">
|
||||||
{{ lang.admin.f2b_parameters }}
|
{{ lang.admin.f2b_parameters }}
|
||||||
</button>
|
</button>
|
||||||
<span class="d-none d-md-block">{{ lang.admin.f2b_parameters }}</span>
|
<span class="d-none d-md-block">{{ lang.admin.f2b_parameters }}</span>
|
||||||
@ -92,16 +92,16 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% for active_ban in f2b_data.active_bans %}
|
{% for active_ban in f2b_data.active_bans %}
|
||||||
<p>
|
<p>
|
||||||
<span class="badge fs-5 bg-info" style="padding:4px;font-size:85%;">
|
<span class="badge fs-5 bg-info py-0">
|
||||||
<i class="bi bi-funnel-fill"></i>
|
<i class="bi bi-funnel-fill"></i>
|
||||||
<a href="https://bgp.he.net/ip/{{ active_ban.ip }}" target="_blank" style="color:white">
|
<a href="https://bgp.he.net/ip/{{ active_ban.ip }}" target="_blank" style="color:white">
|
||||||
{{ active_ban.network }}
|
{{ active_ban.network }}
|
||||||
</a>
|
</a>
|
||||||
({{ active_ban.banned_until }}) -
|
({{ active_ban.banned_until }}) -
|
||||||
{% if active_ban.queued_for_unban == 0 %}
|
{% if active_ban.queued_for_unban == 0 %}
|
||||||
<a data-action="edit_selected" data-item="{{ active_ban.network }}" data-id="f2b-quick" data-api-url='edit/fail2ban' data-api-attr='{"action":"unban"}' href="#">[{{ lang.admin.queue_unban }}]</a>
|
<a class="btn btn-lg btn-link p-0 text-info" data-action="edit_selected" data-item="{{ active_ban.network }}" data-id="f2b-quick" data-api-url='edit/fail2ban' data-api-attr='{"action":"unban"}' href="#">[{{ lang.admin.queue_unban }}]</a>
|
||||||
<a data-action="edit_selected" data-item="{{ active_ban.network }}" data-id="f2b-quick" data-api-url='edit/fail2ban' data-api-attr='{"action":"whitelist"}' href="#">[whitelist]</a>
|
<a class="btn btn-lg btn-link p-0 text-info" data-action="edit_selected" data-item="{{ active_ban.network }}" data-id="f2b-quick" data-api-url='edit/fail2ban' data-api-attr='{"action":"whitelist"}' href="#">[whitelist]</a>
|
||||||
<a data-action="edit_selected" data-item="{{ active_ban.network }}" data-id="f2b-quick" data-api-url='edit/fail2ban' data-api-attr='{"action":"blacklist"}' href="#">[blacklist (<b>needs restart</b>)]</a>
|
<a class="btn btn-lg btn-link p-0 text-info" data-action="edit_selected" data-item="{{ active_ban.network }}" data-id="f2b-quick" data-api-url='edit/fail2ban' data-api-attr='{"action":"blacklist"}' href="#">[blacklist (<b>needs restart</b>)]</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
<i>{{ lang.admin.unban_pending }}</i>
|
<i>{{ lang.admin.unban_pending }}</i>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<div role="tabpanel" class="tab-pane fade" id="tab-config-fwdhosts" role="tabpanel" aria-labelledby="tab-config-fwdhosts">
|
<div class="tab-pane fade" id="tab-config-fwdhosts" role="tabpanel" aria-labelledby="tab-config-fwdhosts">
|
||||||
<div class="card mb-4">
|
<div class="card mb-4">
|
||||||
<div class="card-header d-flex fs-5">
|
<div class="card-header d-flex fs-5">
|
||||||
<button class="btn d-md-none flex-grow-1 text-start" data-bs-target="#collapse-tab-config-fwdhosts" data-bs-toggle="collapse" aria-controls="ollapse-tab-config-fwdhosts">
|
<button class="btn d-md-none flex-grow-1 text-start" data-bs-target="#collapse-tab-config-fwdhosts" data-bs-toggle="collapse" aria-controls="collapse-tab-config-fwdhosts">
|
||||||
{{ lang.admin.forwarding_hosts }}
|
{{ lang.admin.forwarding_hosts }}
|
||||||
</button>
|
</button>
|
||||||
<span class="d-none d-md-block">{{ lang.admin.forwarding_hosts }}</span>
|
<span class="d-none d-md-block">{{ lang.admin.forwarding_hosts }}</span>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<div role="tabpanel" class="tab-pane fade" id="tab-config-oauth2" role="tabpanel" aria-labelledby="tab-config-oauth2">
|
<div class="tab-pane fade" id="tab-config-oauth2" role="tabpanel" aria-labelledby="tab-config-oauth2">
|
||||||
<div class="card mb-4">
|
<div class="card mb-4">
|
||||||
<div class="card-header d-flex fs-5">
|
<div class="card-header d-flex fs-5">
|
||||||
<button class="btn d-md-none flex-grow-1 text-start" data-bs-target="#collapse-tab-config-oauth2" data-bs-toggle="collapse" aria-controls="ollapse-tab-config-oauth2">
|
<button class="btn d-md-none flex-grow-1 text-start" data-bs-target="#collapse-tab-config-oauth2" data-bs-toggle="collapse" aria-controls="collapse-tab-config-oauth2">
|
||||||
{{ lang.admin.oauth2_apps }}
|
{{ lang.admin.oauth2_apps }}
|
||||||
</button>
|
</button>
|
||||||
<span class="d-none d-md-block">{{ lang.admin.oauth2_apps }}</span>
|
<span class="d-none d-md-block">{{ lang.admin.oauth2_apps }}</span>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<div role="tabpanel" class="tab-pane fade" id="tab-config-password-policy" role="tabpanel" aria-labelledby="tab-config-password-policy">
|
<div class="tab-pane fade" id="tab-config-password-policy" role="tabpanel" aria-labelledby="tab-config-password-policy">
|
||||||
<div class="card mb-4">
|
<div class="card mb-4">
|
||||||
<div class="card-header d-flex fs-5">
|
<div class="card-header d-flex fs-5">
|
||||||
<button class="btn d-md-none flex-grow-1 text-start" data-bs-target="#collapse-tab-config-password-policy" data-bs-toggle="collapse" aria-controls="ollapse-tab-config-password-policy">
|
<button class="btn d-md-none flex-grow-1 text-start" data-bs-target="#collapse-tab-config-password-policy" data-bs-toggle="collapse" aria-controls="collapse-tab-config-password-policy">
|
||||||
{{ lang.admin.password_policy }}
|
{{ lang.admin.password_policy }}
|
||||||
</button>
|
</button>
|
||||||
<span class="d-none d-md-block">{{ lang.admin.password_policy }}</span>
|
<span class="d-none d-md-block">{{ lang.admin.password_policy }}</span>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<div role="tabpanel" class="tab-pane fade" id="tab-config-quarantine" role="tabpanel" aria-labelledby="tab-config-quarantine">
|
<div class="tab-pane fade" id="tab-config-quarantine" role="tabpanel" aria-labelledby="tab-config-quarantine">
|
||||||
<div class="card mb-4">
|
<div class="card mb-4">
|
||||||
<div class="card-header d-flex fs-5">
|
<div class="card-header d-flex fs-5">
|
||||||
<button class="btn d-md-none flex-grow-1 text-start" data-bs-target="#collapse-tab-config-quarantine" data-bs-toggle="collapse" aria-controls="ollapse-tab-config-quarantine">
|
<button class="btn d-md-none flex-grow-1 text-start" data-bs-target="#collapse-tab-config-quarantine" data-bs-toggle="collapse" aria-controls="collapse-tab-config-quarantine">
|
||||||
{{ lang.admin.quarantine }}
|
{{ lang.admin.quarantine }}
|
||||||
</button>
|
</button>
|
||||||
<span class="d-none d-md-block">{{ lang.admin.quarantine }}</span>
|
<span class="d-none d-md-block">{{ lang.admin.quarantine }}</span>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<div role="tabpanel" class="tab-pane fade" id="tab-config-quota" role="tabpanel" aria-labelledby="tab-config-quota">
|
<div class="tab-pane fade" id="tab-config-quota" role="tabpanel" aria-labelledby="tab-config-quota">
|
||||||
<div class="card mb-4">
|
<div class="card mb-4">
|
||||||
<div class="card-header d-flex fs-5">
|
<div class="card-header d-flex fs-5">
|
||||||
<button class="btn d-md-none flex-grow-1 text-start" data-bs-target="#collapse-tab-config-quota" data-bs-toggle="collapse" aria-controls="ollapse-tab-config-quota">
|
<button class="btn d-md-none flex-grow-1 text-start" data-bs-target="#collapse-tab-config-quota" data-bs-toggle="collapse" aria-controls="collapse-tab-config-quota">
|
||||||
{{ lang.admin.quota_notifications }}
|
{{ lang.admin.quota_notifications }}
|
||||||
</button>
|
</button>
|
||||||
<span class="d-none d-md-block">{{ lang.admin.quota_notifications }}</span>
|
<span class="d-none d-md-block">{{ lang.admin.quota_notifications }}</span>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<div role="tabpanel" class="tab-pane fade" id="tab-config-rsettings" role="tabpanel" aria-labelledby="tab-config-rsettings">
|
<div class="tab-pane fade" id="tab-config-rsettings" role="tabpanel" aria-labelledby="tab-config-rsettings">
|
||||||
<div class="card mb-4">
|
<div class="card mb-4">
|
||||||
<div class="card-header d-flex fs-5">
|
<div class="card-header d-flex fs-5">
|
||||||
<button class="btn d-md-none flex-grow-1 text-start" data-bs-target="#collapse-tab-config-rsettings" data-bs-toggle="collapse" aria-controls="ollapse-tab-config-rsettings">
|
<button class="btn d-md-none flex-grow-1 text-start" data-bs-target="#collapse-tab-config-rsettings" data-bs-toggle="collapse" aria-controls="collapse-tab-config-rsettings">
|
||||||
{{ lang.admin.rspamd_settings_map }}
|
{{ lang.admin.rspamd_settings_map }}
|
||||||
</button>
|
</button>
|
||||||
<span class="d-none d-md-block">{{ lang.admin.rspamd_settings_map }}</span>
|
<span class="d-none d-md-block">{{ lang.admin.rspamd_settings_map }}</span>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<div role="tabpanel" class="tab-pane fade" id="tab-config-rspamd" role="tabpanel" aria-labelledby="tab-config-rspamd">
|
<div class="tab-pane fade" id="tab-config-rspamd" role="tabpanel" aria-labelledby="tab-config-rspamd">
|
||||||
<div class="card mb-4">
|
<div class="card mb-4">
|
||||||
<div class="card-header d-flex fs-5">
|
<div class="card-header d-flex fs-5">
|
||||||
<button class="btn d-md-none flex-grow-1 text-start" data-bs-target="#collapse-tab-config-rspamd" data-bs-toggle="collapse" aria-controls="ollapse-tab-config-rspamd">
|
<button class="btn d-md-none flex-grow-1 text-start" data-bs-target="#collapse-tab-config-rspamd" data-bs-toggle="collapse" aria-controls="collapse-tab-config-rspamd">
|
||||||
Rspamd UI
|
Rspamd UI
|
||||||
</button>
|
</button>
|
||||||
<span class="d-none d-md-block">Rspamd UI</span>
|
<span class="d-none d-md-block">Rspamd UI</span>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<div role="tabpanel" class="tab-pane fade" id="tab-globalfilter-regex" role="tabpanel" aria-labelledby="tab-globalfilter-regex">
|
<div class="tab-pane fade" id="tab-globalfilter-regex" role="tabpanel" aria-labelledby="tab-globalfilter-regex">
|
||||||
<div class="card mb-4">
|
<div class="card mb-4">
|
||||||
<div class="card-header d-flex fs-5">
|
<div class="card-header d-flex fs-5">
|
||||||
<button class="btn d-md-none flex-grow-1 text-start" data-bs-target="#collapse-tab-config-regex" data-bs-toggle="collapse" aria-controls="ollapse-tab-config-regex">
|
<button class="btn d-md-none flex-grow-1 text-start" data-bs-target="#collapse-tab-config-regex" data-bs-toggle="collapse" aria-controls="collapse-tab-config-regex">
|
||||||
{{ lang.admin.rspamd_global_filters }}
|
{{ lang.admin.rspamd_global_filters }}
|
||||||
</button>
|
</button>
|
||||||
<span class="d-none d-md-block">{{ lang.admin.rspamd_global_filters }}</span>
|
<span class="d-none d-md-block">{{ lang.admin.rspamd_global_filters }}</span>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<div role="tabpanel" class="tab-pane fade" id="tab-config-ldap-admins" role="tabpanel" aria-labelledby="tab-config-ldap-admins">
|
<div class="tab-pane fade" id="tab-config-ldap-admins" role="tabpanel" aria-labelledby="tab-config-ldap-admins">
|
||||||
<div class="card mb-4">
|
<div class="card mb-4">
|
||||||
<div class="card-header d-flex fs-5">
|
<div class="card-header d-flex fs-5">
|
||||||
<button class="btn d-md-none flex-grow-1 text-start" data-bs-target="#collapse-tab-config-ldap-admins" data-bs-toggle="collapse" aria-controls="ollapse-tab-config-ldap-admins">
|
<button class="btn d-md-none flex-grow-1 text-start" data-bs-target="#collapse-tab-config-ldap-admins" data-bs-toggle="collapse" aria-controls="collapse-tab-config-ldap-admins">
|
||||||
{{ lang.admin.admins_ldap }}
|
{{ lang.admin.admins_ldap }}
|
||||||
</button>
|
</button>
|
||||||
<span class="d-none d-md-block">{{ lang.admin.admins_ldap }}</span>
|
<span class="d-none d-md-block">{{ lang.admin.admins_ldap }}</span>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<div role="tabpanel" class="tab-pane fade" id="tab-routing" role="tabpanel" aria-labelledby="tab-routing">
|
<div class="tab-pane fade" id="tab-routing" role="tabpanel" aria-labelledby="tab-routing">
|
||||||
<div class="card mb-4">
|
<div class="card mb-4">
|
||||||
<div class="card-header d-flex fs-5">
|
<div class="card-header d-flex fs-5">
|
||||||
<button class="btn d-md-none flex-grow-1 text-start" data-bs-target="#collapse-tab-routing" data-bs-toggle="collapse" aria-controls="ollapse-tab-routing">
|
<button class="btn d-md-none flex-grow-1 text-start" data-bs-target="#collapse-tab-routing" data-bs-toggle="collapse" aria-controls="collapse-tab-routing">
|
||||||
{{ lang.admin.relayhosts }}
|
{{ lang.admin.relayhosts }}
|
||||||
</button>
|
</button>
|
||||||
<span class="d-none d-md-block">{{ lang.admin.relayhosts }}</span>
|
<span class="d-none d-md-block">{{ lang.admin.relayhosts }}</span>
|
||||||
@ -47,7 +47,7 @@
|
|||||||
|
|
||||||
<div class="card mb-4">
|
<div class="card mb-4">
|
||||||
<div class="card-header d-flex">
|
<div class="card-header d-flex">
|
||||||
<button class="btn d-md-none flex-grow-1 text-start" data-bs-target="#collapse-tab-maps" data-bs-toggle="collapse" aria-controls="ollapse-tab-maps">
|
<button class="btn d-md-none flex-grow-1 text-start" data-bs-target="#collapse-tab-maps" data-bs-toggle="collapse" aria-controls="collapse-tab-maps">
|
||||||
{{ lang.admin.transport_maps }}
|
{{ lang.admin.transport_maps }}
|
||||||
</button>
|
</button>
|
||||||
<span class="d-none d-md-block">{{ lang.admin.transport_maps }}</span>
|
<span class="d-none d-md-block">{{ lang.admin.transport_maps }}</span>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<div role="tabpanel" class="tab-pane fade" id="tab-sys-mails" role="tabpanel" aria-labelledby="tab-sys-mails">
|
<div class="tab-pane fade" id="tab-sys-mails" role="tabpanel" aria-labelledby="tab-sys-mails">
|
||||||
<div class="card mb-4">
|
<div class="card mb-4">
|
||||||
<div class="card-header d-flex fs-5">
|
<div class="card-header d-flex fs-5">
|
||||||
<button class="btn d-md-none flex-grow-1 text-start" data-bs-target="#collapse-tab-sys-mails" data-bs-toggle="collapse" aria-controls="ollapse-tab-sys-mails">
|
<button class="btn d-md-none flex-grow-1 text-start" data-bs-target="#collapse-tab-sys-mails" data-bs-toggle="collapse" aria-controls="collapse-tab-sys-mails">
|
||||||
{{ lang.admin.sys_mails }}
|
{{ lang.admin.sys_mails }}
|
||||||
</button>
|
</button>
|
||||||
<span class="d-none d-md-block">{{ lang.admin.sys_mails }}</span>
|
<span class="d-none d-md-block">{{ lang.admin.sys_mails }}</span>
|
||||||
|
@ -497,8 +497,8 @@
|
|||||||
<legend>{{ lang.debug.history_all_servers }}</legend><hr />
|
<legend>{{ lang.debug.history_all_servers }}</legend><hr />
|
||||||
<a class="btn btn-sm btn-secondary dropdown-toggle mb-4" data-bs-toggle="dropdown" href="#">{{ lang.mailbox.quick_actions }}</a>
|
<a class="btn btn-sm btn-secondary dropdown-toggle mb-4" data-bs-toggle="dropdown" href="#">{{ lang.mailbox.quick_actions }}</a>
|
||||||
<ul class="dropdown-menu">
|
<ul class="dropdown-menu">
|
||||||
<li><a class="dropdown-item add_log_lines" data-post-process="rspamd_history" data-table="rspamd_history" data-log-url="rspamd_history" data-nrows="100" href="#">+ 100</a></li>
|
<li><a class="dropdown-item add_log_lines" data-post-process="rspamd_history" data-table="rspamd_history" data-log-url="rspamd-history" data-nrows="100" href="#">+ 100</a></li>
|
||||||
<li><a class="dropdown-item add_log_lines" data-post-process="rspamd_history" data-table="rspamd_history" data-log-url="rspamd_history" data-nrows="1000" href="#">+ 1000</a></li>
|
<li><a class="dropdown-item add_log_lines" data-post-process="rspamd_history" data-table="rspamd_history" data-log-url="rspamd-history" data-nrows="1000" href="#">+ 1000</a></li>
|
||||||
<li class="table_collapse_option"><hr class="dropdown-divider"></li>
|
<li class="table_collapse_option"><hr class="dropdown-divider"></li>
|
||||||
<li class="table_collapse_option"><a class="dropdown-item" data-datatables-expand="rspamd_history" data-table="rspamd_history" href="#">{{ lang.datatables.expand_all }}</a></li>
|
<li class="table_collapse_option"><a class="dropdown-item" data-datatables-expand="rspamd_history" data-table="rspamd_history" href="#">{{ lang.datatables.expand_all }}</a></li>
|
||||||
<li class="table_collapse_option"><a class="dropdown-item" data-datatables-collapse="rspamd_history" data-table="rspamd_history" href="#">{{ lang.datatables.collapse_all }}</a></li>
|
<li class="table_collapse_option"><a class="dropdown-item" data-datatables-collapse="rspamd_history" data-table="rspamd_history" href="#">{{ lang.datatables.collapse_all }}</a></li>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
<div role="tabpanel" class="tab-pane fade" id="tab-bcc" role="tabpanel" aria-labelledby="tab-bcc">
|
<div class="tab-pane fade" id="tab-bcc" role="tabpanel" aria-labelledby="tab-bcc">
|
||||||
<div class="card mb-4">
|
<div class="card mb-4">
|
||||||
<div class="card-header d-flex fs-5">
|
<div class="card-header d-flex fs-5">
|
||||||
<button class="btn d-md-none flex-grow-1 text-start" data-bs-target="#collapse-tab-bcc" data-bs-toggle="collapse" aria-controls="collapse-tab-bcc">
|
<button class="btn d-md-none flex-grow-1 text-start" data-bs-target="#collapse-tab-bcc" data-bs-toggle="collapse" aria-controls="collapse-tab-bcc">
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
<div role="tabpanel" class="tab-pane fade" id="tab-domain-aliases" role="tabpanel" aria-labelledby="tab-domain-aliases">
|
<div class="tab-pane fade" id="tab-domain-aliases" role="tabpanel" aria-labelledby="tab-domain-aliases">
|
||||||
<div class="card mb-4">
|
<div class="card mb-4">
|
||||||
<div class="card-header d-flex fs-5">
|
<div class="card-header d-flex fs-5">
|
||||||
<button class="btn d-md-none flex-grow-1 text-start" data-bs-target="#collapse-tab-domain-aliases" data-bs-toggle="collapse" aria-controls="collapse-tab-domain-aliases">
|
<button class="btn d-md-none flex-grow-1 text-start" data-bs-target="#collapse-tab-domain-aliases" data-bs-toggle="collapse" aria-controls="collapse-tab-domain-aliases">
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
<div role="tabpanel" class="tab-pane fade show active" id="tab-domains" role="tabpanel" aria-labelledby="tab-domains">
|
<div class="tab-pane fade show active" id="tab-domains" role="tabpanel" aria-labelledby="tab-domains">
|
||||||
<div class="card mb-4">
|
<div class="card mb-4">
|
||||||
<div class="card-header d-flex fs-5">
|
<div class="card-header d-flex fs-5">
|
||||||
<button class="btn d-sm-block d-md-none flex-grow-1 text-start" data-bs-target="#collapse-tab-domains" data-bs-toggle="collapse" aria-controls="collapse-tab-domains">
|
<button class="btn d-sm-block d-md-none flex-grow-1 text-start" data-bs-target="#collapse-tab-domains" data-bs-toggle="collapse" aria-controls="collapse-tab-domains">
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
<div role="tabpanel" class="tab-pane fade" id="tab-filters" role="tabpanel" aria-labelledby="tab-filters">
|
<div class="tab-pane fade" id="tab-filters" role="tabpanel" aria-labelledby="tab-filters">
|
||||||
<div class="card mb-4">
|
<div class="card mb-4">
|
||||||
<div class="card-header d-flex fs-5">
|
<div class="card-header d-flex fs-5">
|
||||||
<button class="btn d-md-none flex-grow-1 text-start" data-bs-target="#collapse-tab-filters" data-bs-toggle="collapse" aria-controls="collapse-tab-filters">
|
<button class="btn d-md-none flex-grow-1 text-start" data-bs-target="#collapse-tab-filters" data-bs-toggle="collapse" aria-controls="collapse-tab-filters">
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
<div role="tabpanel" class="tab-pane fade" id="tab-mailboxes" role="tabpanel" aria-labelledby="tab-mailboxes">
|
<div class="tab-pane fade" id="tab-mailboxes" role="tabpanel" aria-labelledby="tab-mailboxes">
|
||||||
<div class="card mb-4">
|
<div class="card mb-4">
|
||||||
<div class="card-header d-flex fs-5">
|
<div class="card-header d-flex fs-5">
|
||||||
<button class="btn d-md-none flex-grow-1 text-start" data-bs-target="#collapse-tab-mailboxes" data-bs-toggle="collapse" aria-controls="collapse-tab-mailboxes">
|
<button class="btn d-md-none flex-grow-1 text-start" data-bs-target="#collapse-tab-mailboxes" data-bs-toggle="collapse" aria-controls="collapse-tab-mailboxes">
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
<div role="tabpanel" class="tab-pane fade" id="tab-mbox-aliases" role="tabpanel" aria-labelledby="tab-mbox-aliases">
|
<div class="tab-pane fade" id="tab-mbox-aliases" role="tabpanel" aria-labelledby="tab-mbox-aliases">
|
||||||
<div class="card mb-4">
|
<div class="card mb-4">
|
||||||
<div class="card-header d-flex fs-5">
|
<div class="card-header d-flex fs-5">
|
||||||
<button class="btn d-md-none flex-grow-1 text-start" data-bs-target="#collapse-tab-mbox-aliases" data-bs-toggle="collapse" aria-controls="collapse-tab-mbox-aliases">
|
<button class="btn d-md-none flex-grow-1 text-start" data-bs-target="#collapse-tab-mbox-aliases" data-bs-toggle="collapse" aria-controls="collapse-tab-mbox-aliases">
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
<div role="tabpanel" class="tab-pane fade" id="tab-resources" role="tabpanel" aria-labelledby="tab-resources">
|
<div class="tab-pane fade" id="tab-resources" role="tabpanel" aria-labelledby="tab-resources">
|
||||||
<div class="card mb-4">
|
<div class="card mb-4">
|
||||||
<div class="card-header d-flex fs-5">
|
<div class="card-header d-flex fs-5">
|
||||||
<button class="btn d-md-none flex-grow-1 text-start" data-bs-target="#collapse-tab-resources" data-bs-toggle="collapse" aria-controls="collapse-tab-resources">
|
<button class="btn d-md-none flex-grow-1 text-start" data-bs-target="#collapse-tab-resources" data-bs-toggle="collapse" aria-controls="collapse-tab-resources">
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
<div role="tabpanel" class="tab-pane fade" id="tab-syncjobs" role="tabpanel" aria-labelledby="tab-syncjobs">
|
<div class="tab-pane fade" id="tab-syncjobs" role="tabpanel" aria-labelledby="tab-syncjobs">
|
||||||
<div class="card mb-4">
|
<div class="card mb-4">
|
||||||
<div class="card-header d-flex fs-5">
|
<div class="card-header d-flex fs-5">
|
||||||
<button class="btn d-md-none flex-grow-1 text-start" data-bs-target="#collapse-tab-syncjobs" data-bs-toggle="collapse" aria-controls="collapse-tab-syncjobs">
|
<button class="btn d-md-none flex-grow-1 text-start" data-bs-target="#collapse-tab-syncjobs" data-bs-toggle="collapse" aria-controls="collapse-tab-syncjobs">
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
<div role="tabpanel" class="tab-pane fade show" id="tab-templates-domains" role="tabpanel" aria-labelledby="tab-templates-domains">
|
<div class="tab-pane fade show" id="tab-templates-domains" role="tabpanel" aria-labelledby="tab-templates-domains">
|
||||||
<div class="card mb-4">
|
<div class="card mb-4">
|
||||||
<div class="card-header d-flex fs-5">
|
<div class="card-header d-flex fs-5">
|
||||||
<button class="btn d-sm-block d-md-none flex-grow-1 text-start" data-bs-target="#collapse-tab-templates-domains" data-bs-toggle="collapse" aria-controls="collapse-tab-templates-domains">
|
<button class="btn d-sm-block d-md-none flex-grow-1 text-start" data-bs-target="#collapse-tab-templates-domains" data-bs-toggle="collapse" aria-controls="collapse-tab-templates-domains">
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
<div role="tabpanel" class="tab-pane fade show" id="tab-templates-mbox" role="tabpanel" aria-labelledby="tab-templates-mbox">
|
<div class="tab-pane fade show" id="tab-templates-mbox" role="tabpanel" aria-labelledby="tab-templates-mbox">
|
||||||
<div class="card mb-4">
|
<div class="card mb-4">
|
||||||
<div class="card-header d-flex fs-5">
|
<div class="card-header d-flex fs-5">
|
||||||
<button class="btn d-sm-block d-md-none flex-grow-1 text-start" data-bs-target="#collapse-tab-templates-mbox" data-bs-toggle="collapse" aria-controls="collapse-tab-templates-mbox">
|
<button class="btn d-sm-block d-md-none flex-grow-1 text-start" data-bs-target="#collapse-tab-templates-mbox" data-bs-toggle="collapse" aria-controls="collapse-tab-templates-mbox">
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
<div role="tabpanel" class="tab-pane fade{% if mailcow_cc_role != 'admin' %} d-none{% endif %}" id="tab-tls-policy" role="tabpanel" aria-labelledby="tab-tls-policy">
|
<div class="tab-pane fade{% if mailcow_cc_role != 'admin' %} d-none{% endif %}" id="tab-tls-policy" role="tabpanel" aria-labelledby="tab-tls-policy">
|
||||||
<div class="card mb-4">
|
<div class="card mb-4">
|
||||||
<div class="card-header d-flex fs-5">
|
<div class="card-header d-flex fs-5">
|
||||||
<button class="btn d-md-none flex-grow-1 text-start" data-bs-target="#collapse-tab-tls-policy" data-bs-toggle="collapse" aria-controls="collapse-tab-tls-policy">
|
<button class="btn d-md-none flex-grow-1 text-start" data-bs-target="#collapse-tab-tls-policy" data-bs-toggle="collapse" aria-controls="collapse-tab-tls-policy">
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
<div role="tabpanel" class="tab-pane fade" id="AppPasswds" role="tabpanel" aria-labelledby="AppPasswds">
|
<div class="tab-pane fade" id="AppPasswds" role="tabpanel" aria-labelledby="AppPasswds">
|
||||||
<div class="card mb-4">
|
<div class="card mb-4">
|
||||||
<div class="card-header d-flex fs-5">
|
<div class="card-header d-flex fs-5">
|
||||||
<button class="btn d-md-none flex-grow-1 text-start" data-bs-target="#collapse-tab-AppPasswds" data-bs-toggle="collapse" aria-controls="collapse-tab-AppPasswds">
|
<button class="btn d-md-none flex-grow-1 text-start" data-bs-target="#collapse-tab-AppPasswds" data-bs-toggle="collapse" aria-controls="collapse-tab-AppPasswds">
|
||||||
{{ lang.user.app_passwds }}
|
{{ lang.user.app_passwds }}
|
||||||
</button>
|
</button>
|
||||||
<span class="d-none d-md-block">{{ lang.user.app_passwds }}
|
<span class="d-none d-md-block">{{ lang.user.app_passwds }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div id="collapse-tab-AppPasswds" class="card-body collapse" data-bs-parent="#user-content">
|
<div id="collapse-tab-AppPasswds" class="card-body collapse" data-bs-parent="#user-content">
|
||||||
<div class="mass-actions-user mb-4">
|
<div class="mass-actions-user mb-4">
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
<div role="tabpanel" class="tab-pane fade" id="Pushover" role="tabpanel" aria-labelledby="Pushover">
|
<div class="tab-pane fade" id="Pushover" role="tabpanel" aria-labelledby="Pushover">
|
||||||
<div class="card mb-4">
|
<div class="card mb-4">
|
||||||
<div class="card-header d-flex fs-5">
|
<div class="card-header d-flex fs-5">
|
||||||
<button class="btn d-md-none flex-grow-1 text-start" data-bs-target="#collapse-tab-Pushover" data-bs-toggle="collapse" aria-controls="collapse-tab-Pushover">
|
<button class="btn d-md-none flex-grow-1 text-start" data-bs-target="#collapse-tab-Pushover" data-bs-toggle="collapse" aria-controls="collapse-tab-Pushover">
|
||||||
Pushover API
|
Pushover API
|
||||||
</button>
|
</button>
|
||||||
<span class="d-none d-md-block">Pushover API
|
<span class="d-none d-md-block">Pushover API</span>
|
||||||
</div>
|
</div>
|
||||||
<div id="collapse-tab-Pushover" class="card-body collapse" data-bs-parent="#user-content">
|
<div id="collapse-tab-Pushover" class="card-body collapse" data-bs-parent="#user-content">
|
||||||
<form data-id="pushover" class="form well" method="post">
|
<form data-id="pushover" class="form well" method="post">
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
<div role="tabpanel" class="tab-pane fade" id="SpamAliases" role="tabpanel" aria-labelledby="SpamAliases">
|
<div class="tab-pane fade" id="SpamAliases" role="tabpanel" aria-labelledby="SpamAliases">
|
||||||
<div class="card mb-4">
|
<div class="card mb-4">
|
||||||
<div class="card-header d-flex fs-5">
|
<div class="card-header d-flex fs-5">
|
||||||
<button class="btn d-md-none flex-grow-1 text-start" data-bs-target="#collapse-tab-SpamAliases" data-bs-toggle="collapse" aria-controls="collapse-tab-SpamAliases">
|
<button class="btn d-md-none flex-grow-1 text-start" data-bs-target="#collapse-tab-SpamAliases" data-bs-toggle="collapse" aria-controls="collapse-tab-SpamAliases">
|
||||||
{{ lang.user.spam_aliases }}
|
{{ lang.user.spam_aliases }}
|
||||||
</button>
|
</button>
|
||||||
<span class="d-none d-md-block">{{ lang.user.spam_aliases }}
|
<span class="d-none d-md-block">{{ lang.user.spam_aliases }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div id="collapse-tab-SpamAliases" class="card-body collapse" data-bs-parent="#user-content">
|
<div id="collapse-tab-SpamAliases" class="card-body collapse" data-bs-parent="#user-content">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
<div role="tabpanel" class="tab-pane fade" id="Spamfilter" role="tabpanel" aria-labelledby="Spamfilter">
|
<div class="tab-pane fade" id="Spamfilter" role="tabpanel" aria-labelledby="Spamfilter">
|
||||||
<div class="card mb-4">
|
<div class="card mb-4">
|
||||||
<div class="card-header d-flex fs-5">
|
<div class="card-header d-flex fs-5">
|
||||||
<button class="btn d-md-none flex-grow-1 text-start" data-bs-target="#collapse-tab-Spamfilter" data-bs-toggle="collapse" aria-controls="collapse-tab-Spamfilter">
|
<button class="btn d-md-none flex-grow-1 text-start" data-bs-target="#collapse-tab-Spamfilter" data-bs-toggle="collapse" aria-controls="collapse-tab-Spamfilter">
|
||||||
{{ lang.user.spamfilter }}
|
{{ lang.user.spamfilter }}
|
||||||
</button>
|
</button>
|
||||||
<span class="d-none d-md-block">{{ lang.user.spamfilter }}
|
<span class="d-none d-md-block">{{ lang.user.spamfilter }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div id="collapse-tab-Spamfilter" class="card-body collapse" data-bs-parent="#user-content">
|
<div id="collapse-tab-Spamfilter" class="card-body collapse" data-bs-parent="#user-content">
|
||||||
<h4>{{ lang.user.spamfilter_behavior }}</h4>
|
<h4>{{ lang.user.spamfilter_behavior }}</h4>
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
<div role="tabpanel" class="tab-pane fade" id="Syncjobs" role="tabpanel" aria-labelledby="Syncjobs">
|
<div class="tab-pane fade" id="Syncjobs" role="tabpanel" aria-labelledby="Syncjobs">
|
||||||
<div class="card mb-4">
|
<div class="card mb-4">
|
||||||
<div class="card-header d-flex fs-5">
|
<div class="card-header d-flex fs-5">
|
||||||
<button class="btn d-md-none flex-grow-1 text-start" data-bs-target="#collapse-tab-Syncjobs" data-bs-toggle="collapse" aria-controls="collapse-tab-Syncjobs">
|
<button class="btn d-md-none flex-grow-1 text-start" data-bs-target="#collapse-tab-Syncjobs" data-bs-toggle="collapse" aria-controls="collapse-tab-Syncjobs">
|
||||||
{{ lang.user.sync_jobs }}
|
{{ lang.user.sync_jobs }}
|
||||||
</button>
|
</button>
|
||||||
<span class="d-none d-md-block">{{ lang.user.sync_jobs }}
|
<span class="d-none d-md-block">{{ lang.user.sync_jobs }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div id="collapse-tab-Syncjobs" class="card-body collapse" data-bs-parent="#user-content">
|
<div id="collapse-tab-Syncjobs" class="card-body collapse" data-bs-parent="#user-content">
|
||||||
<div class="mass-actions-user mb-4">
|
<div class="mass-actions-user mb-4">
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
<div role="tabpanel" class="tab-pane fade in active show" id="tab-user-auth" role="tabpanel" aria-labelledby="tab-user-auth">
|
<div class="tab-pane fade in active show" id="tab-user-auth" role="tabpanel" aria-labelledby="tab-user-auth">
|
||||||
<div class="card mb-4">
|
<div class="card mb-4">
|
||||||
<div class="card-header d-flex fs-5">
|
<div class="card-header d-flex fs-5">
|
||||||
<button class="btn d-md-none flex-grow-1 text-start" data-bs-target="#collapse-tab-user-auth" data-bs-toggle="collapse" aria-controls="collapse-tab-user-auth">
|
<button class="btn d-md-none flex-grow-1 text-start" data-bs-target="#collapse-tab-user-auth" data-bs-toggle="collapse" aria-controls="collapse-tab-user-auth">
|
||||||
{{ lang.user.mailbox_general }}
|
{{ lang.user.mailbox_general }}
|
||||||
</button>
|
</button>
|
||||||
<span class="d-none d-md-block">{{ lang.user.mailbox_general }}
|
<span class="d-none d-md-block">{{ lang.user.mailbox_general }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div id="collapse-tab-user-auth" class="card-body collapse" data-bs-parent="#user-content">
|
<div id="collapse-tab-user-auth" class="card-body collapse" data-bs-parent="#user-content">
|
||||||
{% if mailboxdata.attributes.force_pw_update == '1' %}
|
{% if mailboxdata.attributes.force_pw_update == '1' %}
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
<div role="tabpanel" class="tab-pane fade" id="tab-user-details" role="tabpanel" aria-labelledby="tab-user-details">
|
<div class="tab-pane fade" id="tab-user-details" role="tabpanel" aria-labelledby="tab-user-details">
|
||||||
<div class="card mb-4">
|
<div class="card mb-4">
|
||||||
<div class="card-header d-flex fs-5">
|
<div class="card-header d-flex fs-5">
|
||||||
<button class="btn d-md-none flex-grow-1 text-start" data-bs-target="#collapse-tab-user-details" data-bs-toggle="collapse" aria-controls="collapse-tab-user-details">
|
<button class="btn d-md-none flex-grow-1 text-start" data-bs-target="#collapse-tab-user-details" data-bs-toggle="collapse" aria-controls="collapse-tab-user-details">
|
||||||
{{ lang.user.mailbox_details }}
|
{{ lang.user.mailbox_details }}
|
||||||
</button>
|
</button>
|
||||||
<span class="d-none d-md-block">{{ lang.user.mailbox_details }}
|
<span class="d-none d-md-block">{{ lang.user.mailbox_details }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div id="collapse-tab-user-details" class="card-body collapse" data-bs-parent="#user-content">
|
<div id="collapse-tab-user-details" class="card-body collapse" data-bs-parent="#user-content">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@ -46,7 +46,7 @@
|
|||||||
<div class="col-sm-8 col-md-9 col-12">
|
<div class="col-sm-8 col-md-9 col-12">
|
||||||
<p>
|
<p>
|
||||||
{% if user_get_alias_details.aliases_also_send_as == '*' %}
|
{% if user_get_alias_details.aliases_also_send_as == '*' %}
|
||||||
{{ lang.user.sender_acl_disabled }}
|
{{ lang.user.sender_acl_disabled | raw }}
|
||||||
{% elseif user_get_alias_details.aliases_also_send_as %}
|
{% elseif user_get_alias_details.aliases_also_send_as %}
|
||||||
{{ user_get_alias_details.aliases_also_send_as }}
|
{{ user_get_alias_details.aliases_also_send_as }}
|
||||||
{% else %}
|
{% else %}
|
||||||
@ -58,13 +58,13 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-sm-4 col-md-3 col-12 text-sm-end text-start mb-4">{{ lang.user.aliases_send_as_all }}:</div>
|
<div class="col-sm-4 col-md-3 col-12 text-sm-end text-start mb-4">{{ lang.user.aliases_send_as_all }}:</div>
|
||||||
<div class="col-sm-8 col-md-9 col-12">
|
<div class="col-sm-8 col-md-9 col-12">
|
||||||
<p>{% if not user_get_alias_details.aliases_send_as_all %}<i class="bi bi-x-lg"></i>{% endif %}</p>
|
<p>{% if not user_get_alias_details.aliases_send_as_all %}<i class="bi bi-x-lg"></i>{% else %}{{ user_get_alias_details.aliases_send_as_all }}{% endif %}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-sm-4 col-md-3 col-12 text-sm-end text-start mb-4">{{ lang.user.is_catch_all }}:</div>
|
<div class="col-sm-4 col-md-3 col-12 text-sm-end text-start mb-4">{{ lang.user.is_catch_all }}:</div>
|
||||||
<div class="col-sm-8 col-md-9 col-12">
|
<div class="col-sm-8 col-md-9 col-12">
|
||||||
<p>{% if not user_get_alias_details.is_catch_all %}<i class="bi bi-x-lg"></i>{% endif %}</p>
|
<p>{% if not user_get_alias_details.is_catch_all %}<i class="bi bi-x-lg"></i>{% else %}{{ user_get_alias_details.is_catch_all }}{% endif %}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
<div role="tabpanel" class="tab-pane fade" id="tab-user-settings" role="tabpanel" aria-labelledby="tab-user-settings">
|
<div class="tab-pane fade" id="tab-user-settings" role="tabpanel" aria-labelledby="tab-user-settings">
|
||||||
<div class="card mb-4">
|
<div class="card mb-4">
|
||||||
<div class="card-header d-flex fs-5">
|
<div class="card-header d-flex fs-5">
|
||||||
<button class="btn d-md-none flex-grow-1 text-start" data-bs-target="#collapse-tab-user-settings" data-bs-toggle="collapse" aria-controls="collapse-tab-user-settings">
|
<button class="btn d-md-none flex-grow-1 text-start" data-bs-target="#collapse-tab-user-settings" data-bs-toggle="collapse" aria-controls="collapse-tab-user-settings">
|
||||||
{{ lang.user.mailbox_settings }}
|
{{ lang.user.mailbox_settings }}
|
||||||
</button>
|
</button>
|
||||||
<span class="d-none d-md-block">{{ lang.user.mailbox_settings }}
|
<span class="d-none d-md-block">{{ lang.user.mailbox_settings }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div id="collapse-tab-user-settings" class="card-body collapse" data-bs-parent="#user-content">
|
<div id="collapse-tab-user-settings" class="card-body collapse" data-bs-parent="#user-content">
|
||||||
{# Show tagging options #}
|
{# Show tagging options #}
|
||||||
|
@ -162,6 +162,7 @@ services:
|
|||||||
- DEV_MODE=${DEV_MODE:-n}
|
- DEV_MODE=${DEV_MODE:-n}
|
||||||
- DEMO_MODE=${DEMO_MODE:-n}
|
- DEMO_MODE=${DEMO_MODE:-n}
|
||||||
- WEBAUTHN_ONLY_TRUSTED_VENDORS=${WEBAUTHN_ONLY_TRUSTED_VENDORS:-n}
|
- WEBAUTHN_ONLY_TRUSTED_VENDORS=${WEBAUTHN_ONLY_TRUSTED_VENDORS:-n}
|
||||||
|
- CLUSTERMODE=${CLUSTERMODE:-}
|
||||||
restart: always
|
restart: always
|
||||||
networks:
|
networks:
|
||||||
mailcow-network:
|
mailcow-network:
|
||||||
@ -510,7 +511,7 @@ services:
|
|||||||
- watchdog
|
- watchdog
|
||||||
|
|
||||||
dockerapi-mailcow:
|
dockerapi-mailcow:
|
||||||
image: mailcow/dockerapi:2.04
|
image: mailcow/dockerapi:2.05
|
||||||
security_opt:
|
security_opt:
|
||||||
- label=disable
|
- label=disable
|
||||||
restart: always
|
restart: always
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# renovate: datasource=github-releases depName=nextcloud/server versioning=semver extractVersion=^v(?<version>.*)$
|
# renovate: datasource=github-releases depName=nextcloud/server versioning=semver extractVersion=^v(?<version>.*)$
|
||||||
NEXTCLOUD_VERSION=26.0.2
|
NEXTCLOUD_VERSION=27.0.1
|
||||||
|
|
||||||
echo -ne "Checking prerequisites..."
|
echo -ne "Checking prerequisites..."
|
||||||
sleep 1
|
sleep 1
|
||||||
@ -178,6 +178,8 @@ elif [[ ${NC_INSTALL} == "y" ]]; then
|
|||||||
/web/nextcloud/occ --no-warnings config:system:set mail_domain --value=${MAILCOW_HOSTNAME}; \
|
/web/nextcloud/occ --no-warnings config:system:set mail_domain --value=${MAILCOW_HOSTNAME}; \
|
||||||
/web/nextcloud/occ --no-warnings config:system:set mail_smtphost --value=postfix; \
|
/web/nextcloud/occ --no-warnings config:system:set mail_smtphost --value=postfix; \
|
||||||
/web/nextcloud/occ --no-warnings config:system:set mail_smtpport --value=588; \
|
/web/nextcloud/occ --no-warnings config:system:set mail_smtpport --value=588; \
|
||||||
|
/web/nextcloud/occ --no-warnings config:system:set mail_smtpstreamoptions ssl verify_peer --value=false --type=boolean
|
||||||
|
/web/nextcloud/occ --no-warnings config:system:set mail_smtpstreamoptions ssl verify_peer_name --value=false --type=boolean
|
||||||
/web/nextcloud/occ --no-warnings db:convert-filecache-bigint -n"
|
/web/nextcloud/occ --no-warnings db:convert-filecache-bigint -n"
|
||||||
|
|
||||||
# Not installing by default, broke too often
|
# Not installing by default, broke too often
|
||||||
|
@ -876,7 +876,7 @@ if grep -q 'SYSCTL_IPV6_DISABLED=1' mailcow.conf; then
|
|||||||
echo '!! IMPORTANT !!'
|
echo '!! IMPORTANT !!'
|
||||||
echo
|
echo
|
||||||
echo 'SYSCTL_IPV6_DISABLED was removed due to complications. IPv6 can be disabled by editing "docker-compose.yml" and setting "enable_ipv6: true" to "enable_ipv6: false".'
|
echo 'SYSCTL_IPV6_DISABLED was removed due to complications. IPv6 can be disabled by editing "docker-compose.yml" and setting "enable_ipv6: true" to "enable_ipv6: false".'
|
||||||
echo 'This setting will only be active after a complete shutdown of mailcow by running $COMPOSE_COMMAND down followed by $COMPOSE_COMMAND up -d".'
|
echo "This setting will only be active after a complete shutdown of mailcow by running $COMPOSE_COMMAND down followed by $COMPOSE_COMMAND up -d."
|
||||||
echo
|
echo
|
||||||
echo '!! IMPORTANT !!'
|
echo '!! IMPORTANT !!'
|
||||||
echo
|
echo
|
||||||
|
Loading…
Reference in New Issue
Block a user