mirror of
https://github.com/mailcow/mailcow-dockerized.git
synced 2026-05-11 02:02:25 +00:00
[DockerApi] Rename DockerApi to Controller and add mailcow-adm tool
This commit is contained in:
128
data/Dockerfiles/controller/mailcow-adm/modules/Docker.py
Normal file
128
data/Dockerfiles/controller/mailcow-adm/modules/Docker.py
Normal file
@@ -0,0 +1,128 @@
|
||||
import docker
|
||||
from docker.errors import APIError
|
||||
|
||||
class Docker:
|
||||
def __init__(self):
|
||||
self.client = docker.from_env()
|
||||
|
||||
def exec_command(self, container_name, cmd, user=None):
|
||||
"""
|
||||
Execute a command in a container by its container name.
|
||||
:param container_name: The name of the container.
|
||||
:param cmd: The command to execute as a list (e.g., ["ls", "-la"]).
|
||||
:param user: The user to execute the command as (optional).
|
||||
:return: A standardized response with status, output, and exit_code.
|
||||
"""
|
||||
|
||||
filters = {"name": container_name}
|
||||
|
||||
try:
|
||||
for container in self.client.containers.list(filters=filters):
|
||||
exec_result = container.exec_run(cmd, user=user)
|
||||
return {
|
||||
"status": "success",
|
||||
"exit_code": exec_result.exit_code,
|
||||
"output": exec_result.output.decode("utf-8")
|
||||
}
|
||||
except APIError as e:
|
||||
return {
|
||||
"status": "error",
|
||||
"exit_code": "APIError",
|
||||
"output": str(e)
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
"status": "error",
|
||||
"exit_code": "Exception",
|
||||
"output": str(e)
|
||||
}
|
||||
|
||||
def start_container(self, container_name):
|
||||
"""
|
||||
Start a container by its container name.
|
||||
:param container_name: The name of the container.
|
||||
:return: A standardized response with status, output, and exit_code.
|
||||
"""
|
||||
|
||||
filters = {"name": container_name}
|
||||
|
||||
try:
|
||||
for container in self.client.containers.list(filters=filters):
|
||||
container.start()
|
||||
return {
|
||||
"status": "success",
|
||||
"exit_code": "0",
|
||||
"output": f"Container '{container_name}' started successfully."
|
||||
}
|
||||
except APIError as e:
|
||||
return {
|
||||
"status": "error",
|
||||
"exit_code": "APIError",
|
||||
"output": str(e)
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
"status": "error",
|
||||
"error_type": "Exception",
|
||||
"output": str(e)
|
||||
}
|
||||
|
||||
def stop_container(self, container_name):
|
||||
"""
|
||||
Stop a container by its container name.
|
||||
:param container_name: The name of the container.
|
||||
:return: A standardized response with status, output, and exit_code.
|
||||
"""
|
||||
|
||||
filters = {"name": container_name}
|
||||
|
||||
try:
|
||||
for container in self.client.containers.list(filters=filters):
|
||||
container.stop()
|
||||
return {
|
||||
"status": "success",
|
||||
"exit_code": "0",
|
||||
"output": f"Container '{container_name}' stopped successfully."
|
||||
}
|
||||
except APIError as e:
|
||||
return {
|
||||
"status": "error",
|
||||
"exit_code": "APIError",
|
||||
"output": str(e)
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
"status": "error",
|
||||
"exit_code": "Exception",
|
||||
"output": str(e)
|
||||
}
|
||||
|
||||
def restart_container(self, container_name):
|
||||
"""
|
||||
Restart a container by its container name.
|
||||
:param container_name: The name of the container.
|
||||
:return: A standardized response with status, output, and exit_code.
|
||||
"""
|
||||
|
||||
filters = {"name": container_name}
|
||||
|
||||
try:
|
||||
for container in self.client.containers.list(filters=filters):
|
||||
container.restart()
|
||||
return {
|
||||
"status": "success",
|
||||
"exit_code": "0",
|
||||
"output": f"Container '{container_name}' restarted successfully."
|
||||
}
|
||||
except APIError as e:
|
||||
return {
|
||||
"status": "error",
|
||||
"exit_code": "APIError",
|
||||
"output": str(e)
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
"status": "error",
|
||||
"exit_code": "Exception",
|
||||
"output": str(e)
|
||||
}
|
||||
206
data/Dockerfiles/controller/mailcow-adm/modules/Dovecot.py
Normal file
206
data/Dockerfiles/controller/mailcow-adm/modules/Dovecot.py
Normal file
@@ -0,0 +1,206 @@
|
||||
import os
|
||||
|
||||
from modules.Docker import Docker
|
||||
|
||||
class Dovecot:
|
||||
def __init__(self):
|
||||
self.docker = Docker()
|
||||
|
||||
def decryptMaildir(self, source_dir="/var/vmail/", output_dir=None):
|
||||
"""
|
||||
Decrypt files in /var/vmail using doveadm if they are encrypted.
|
||||
:param output_dir: Directory inside the Dovecot container to store decrypted files, Default overwrite.
|
||||
"""
|
||||
private_key = "/mail_crypt/ecprivkey.pem"
|
||||
public_key = "/mail_crypt/ecpubkey.pem"
|
||||
|
||||
if output_dir:
|
||||
# Ensure the output directory exists inside the container
|
||||
mkdir_result = self.docker.exec_command("dovecot-mailcow", f"bash -c 'mkdir -p {output_dir} && chown vmail:vmail {output_dir}'")
|
||||
if mkdir_result.get("status") != "success":
|
||||
print(f"Error creating output directory: {mkdir_result.get('output')}")
|
||||
return
|
||||
|
||||
find_command = [
|
||||
"find", source_dir, "-type", "f", "-regextype", "egrep", "-regex", ".*S=.*W=.*"
|
||||
]
|
||||
|
||||
try:
|
||||
find_result = self.docker.exec_command("dovecot-mailcow", " ".join(find_command))
|
||||
if find_result.get("status") != "success":
|
||||
print(f"Error finding files: {find_result.get('output')}")
|
||||
return
|
||||
|
||||
files = find_result.get("output", "").splitlines()
|
||||
|
||||
for file in files:
|
||||
head_command = f"head -c7 {file}"
|
||||
head_result = self.docker.exec_command("dovecot-mailcow", head_command)
|
||||
if head_result.get("status") == "success" and head_result.get("output", "").strip() == "CRYPTED":
|
||||
if output_dir:
|
||||
# Preserve the directory structure in the output directory
|
||||
relative_path = os.path.relpath(file, source_dir)
|
||||
output_file = os.path.join(output_dir, relative_path)
|
||||
current_path = output_dir
|
||||
for part in os.path.dirname(relative_path).split(os.sep):
|
||||
current_path = os.path.join(current_path, part)
|
||||
mkdir_result = self.docker.exec_command("dovecot-mailcow", f"bash -c '[ ! -d {current_path} ] && mkdir {current_path} && chown vmail:vmail {current_path}'")
|
||||
if mkdir_result.get("status") != "success":
|
||||
print(f"Error creating directory {current_path}: {mkdir_result.get('output')}")
|
||||
continue
|
||||
else:
|
||||
# Overwrite the original file
|
||||
output_file = file
|
||||
|
||||
decrypt_command = (
|
||||
f"bash -c 'doveadm fs get compress lz4:1:crypt:private_key_path={private_key}:public_key_path={public_key}:posix:prefix=/ {file} > {output_file}'"
|
||||
)
|
||||
|
||||
decrypt_result = self.docker.exec_command("dovecot-mailcow", decrypt_command)
|
||||
if decrypt_result.get("status") == "success":
|
||||
print(f"Decrypted {file}")
|
||||
|
||||
# Verify the file size and set permissions
|
||||
size_check_command = f"bash -c '[ -s {output_file} ] && chmod 600 {output_file} && chown vmail:vmail {output_file} || rm -f {output_file}'"
|
||||
size_check_result = self.docker.exec_command("dovecot-mailcow", size_check_command)
|
||||
if size_check_result.get("status") != "success":
|
||||
print(f"Error setting permissions for {output_file}: {size_check_result.get('output')}\n")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error during decryption: {e}")
|
||||
|
||||
return "Done"
|
||||
|
||||
def encryptMaildir(self, source_dir="/var/vmail/", output_dir=None):
|
||||
"""
|
||||
Encrypt files in /var/vmail using doveadm if they are not already encrypted.
|
||||
:param source_dir: Directory inside the Dovecot container to encrypt files.
|
||||
:param output_dir: Directory inside the Dovecot container to store encrypted files, Default overwrite.
|
||||
"""
|
||||
private_key = "/mail_crypt/ecprivkey.pem"
|
||||
public_key = "/mail_crypt/ecpubkey.pem"
|
||||
|
||||
if output_dir:
|
||||
# Ensure the output directory exists inside the container
|
||||
mkdir_result = self.docker.exec_command("dovecot-mailcow", f"mkdir -p {output_dir}")
|
||||
if mkdir_result.get("status") != "success":
|
||||
print(f"Error creating output directory: {mkdir_result.get('output')}")
|
||||
return
|
||||
|
||||
find_command = [
|
||||
"find", source_dir, "-type", "f", "-regextype", "egrep", "-regex", ".*S=.*W=.*"
|
||||
]
|
||||
|
||||
try:
|
||||
find_result = self.docker.exec_command("dovecot-mailcow", " ".join(find_command))
|
||||
if find_result.get("status") != "success":
|
||||
print(f"Error finding files: {find_result.get('output')}")
|
||||
return
|
||||
|
||||
files = find_result.get("output", "").splitlines()
|
||||
|
||||
for file in files:
|
||||
head_command = f"head -c7 {file}"
|
||||
head_result = self.docker.exec_command("dovecot-mailcow", head_command)
|
||||
if head_result.get("status") == "success" and head_result.get("output", "").strip() != "CRYPTED":
|
||||
if output_dir:
|
||||
# Preserve the directory structure in the output directory
|
||||
relative_path = os.path.relpath(file, source_dir)
|
||||
output_file = os.path.join(output_dir, relative_path)
|
||||
current_path = output_dir
|
||||
for part in os.path.dirname(relative_path).split(os.sep):
|
||||
current_path = os.path.join(current_path, part)
|
||||
mkdir_result = self.docker.exec_command("dovecot-mailcow", f"bash -c '[ ! -d {current_path} ] && mkdir {current_path} && chown vmail:vmail {current_path}'")
|
||||
if mkdir_result.get("status") != "success":
|
||||
print(f"Error creating directory {current_path}: {mkdir_result.get('output')}")
|
||||
continue
|
||||
else:
|
||||
# Overwrite the original file
|
||||
output_file = file
|
||||
|
||||
encrypt_command = (
|
||||
f"bash -c 'doveadm fs put crypt private_key_path={private_key}:public_key_path={public_key}:posix:prefix=/ {file} {output_file}'"
|
||||
)
|
||||
|
||||
encrypt_result = self.docker.exec_command("dovecot-mailcow", encrypt_command)
|
||||
if encrypt_result.get("status") == "success":
|
||||
print(f"Encrypted {file}")
|
||||
|
||||
# Set permissions
|
||||
permissions_command = f"bash -c 'chmod 600 {output_file} && chown 5000:5000 {output_file}'"
|
||||
permissions_result = self.docker.exec_command("dovecot-mailcow", permissions_command)
|
||||
if permissions_result.get("status") != "success":
|
||||
print(f"Error setting permissions for {output_file}: {permissions_result.get('output')}\n")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error during encryption: {e}")
|
||||
|
||||
return "Done"
|
||||
|
||||
def listDeletedMaildirs(self, source_dir="/var/vmail/_garbage"):
|
||||
"""
|
||||
List deleted maildirs in the specified garbage directory.
|
||||
:param source_dir: Directory to search for deleted maildirs.
|
||||
:return: List of maildirs.
|
||||
"""
|
||||
list_command = ["bash", "-c", f"ls -la {source_dir}"]
|
||||
|
||||
try:
|
||||
result = self.docker.exec_command("dovecot-mailcow", list_command)
|
||||
if result.get("status") != "success":
|
||||
print(f"Error listing deleted maildirs: {result.get('output')}")
|
||||
return []
|
||||
|
||||
lines = result.get("output", "").splitlines()
|
||||
maildirs = {}
|
||||
|
||||
for idx, line in enumerate(lines):
|
||||
parts = line.split()
|
||||
if "_" in line:
|
||||
folder_name = parts[-1]
|
||||
time, maildir = folder_name.split("_", 1)
|
||||
|
||||
if maildir.endswith("_index"):
|
||||
main_item = maildir[:-6]
|
||||
if main_item in maildirs:
|
||||
maildirs[main_item]["has_index"] = True
|
||||
else:
|
||||
maildirs[maildir] = {"item": idx, "time": time, "name": maildir, "has_index": False}
|
||||
|
||||
return list(maildirs.values())
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error during listing deleted maildirs: {e}")
|
||||
return []
|
||||
|
||||
def restoreMaildir(self, username, item, source_dir="/var/vmail/_garbage"):
|
||||
"""
|
||||
Restore a maildir item for a specific user from the deleted maildirs.
|
||||
:param username: Username to restore the item to.
|
||||
:param item: Item to restore (e.g., mailbox, folder).
|
||||
:param source_dir: Directory containing deleted maildirs.
|
||||
:return: Response from Dovecot.
|
||||
"""
|
||||
username_splitted = username.split("@")
|
||||
maildirs = self.listDeletedMaildirs()
|
||||
|
||||
maildir = None
|
||||
for mdir in maildirs:
|
||||
if mdir["item"] == int(item):
|
||||
maildir = mdir
|
||||
break
|
||||
if not maildir:
|
||||
return {"status": "error", "message": "Maildir not found."}
|
||||
|
||||
restore_command = f"mv {source_dir}/{maildir['time']}_{maildir['name']} /var/vmail/{username_splitted[1]}/{username_splitted[0]}"
|
||||
restore_index_command = f"mv {source_dir}/{maildir['time']}_{maildir['name']}_index /var/vmail_index/{username}"
|
||||
|
||||
result = self.docker.exec_command("dovecot-mailcow", ["bash", "-c", restore_command])
|
||||
if result.get("status") != "success":
|
||||
return {"status": "error", "message": "Failed to restore maildir."}
|
||||
|
||||
result = self.docker.exec_command("dovecot-mailcow", ["bash", "-c", restore_index_command])
|
||||
if result.get("status") != "success":
|
||||
return {"status": "error", "message": "Failed to restore maildir index."}
|
||||
|
||||
return "Done"
|
||||
457
data/Dockerfiles/controller/mailcow-adm/modules/Mailcow.py
Normal file
457
data/Dockerfiles/controller/mailcow-adm/modules/Mailcow.py
Normal file
@@ -0,0 +1,457 @@
|
||||
import requests
|
||||
import urllib3
|
||||
import sys
|
||||
import os
|
||||
import subprocess
|
||||
import tempfile
|
||||
import mysql.connector
|
||||
from contextlib import contextmanager
|
||||
from datetime import datetime
|
||||
from modules.Docker import Docker
|
||||
|
||||
|
||||
class Mailcow:
|
||||
def __init__(self):
|
||||
self.apiUrl = "/api/v1"
|
||||
self.ignore_ssl_errors = True
|
||||
|
||||
self.baseUrl = f"https://{os.getenv('IPv4_NETWORK', '172.22.1')}.247"
|
||||
self.host = os.getenv("MAILCOW_HOSTNAME", "")
|
||||
self.apiKey = ""
|
||||
if self.ignore_ssl_errors:
|
||||
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
||||
|
||||
self.db_config = {
|
||||
'user': os.getenv('DBUSER'),
|
||||
'password': os.getenv('DBPASS'),
|
||||
'database': os.getenv('DBNAME'),
|
||||
'unix_socket': '/var/run/mysqld/mysqld.sock',
|
||||
}
|
||||
|
||||
self.docker = Docker()
|
||||
|
||||
|
||||
# API Functions
|
||||
def addDomain(self, domain):
|
||||
"""
|
||||
Add a domain to the mailcow instance.
|
||||
:param domain: Dictionary containing domain details.
|
||||
:return: Response from the mailcow API.
|
||||
"""
|
||||
|
||||
return self.post('/add/domain', domain)
|
||||
|
||||
def addMailbox(self, mailbox):
|
||||
"""
|
||||
Add a mailbox to the mailcow instance.
|
||||
:param mailbox: Dictionary containing mailbox details.
|
||||
:return: Response from the mailcow API.
|
||||
"""
|
||||
|
||||
return self.post('/add/mailbox', mailbox)
|
||||
|
||||
def addAlias(self, alias):
|
||||
"""
|
||||
Add an alias to the mailcow instance.
|
||||
:param alias: Dictionary containing alias details.
|
||||
:return: Response from the mailcow API.
|
||||
"""
|
||||
|
||||
return self.post('/add/alias', alias)
|
||||
|
||||
def addSyncjob(self, syncjob):
|
||||
"""
|
||||
Add a sync job to the mailcow instance.
|
||||
:param syncjob: Dictionary containing sync job details.
|
||||
:return: Response from the mailcow API.
|
||||
"""
|
||||
|
||||
return self.post('/add/syncjob', syncjob)
|
||||
|
||||
def addDomainadmin(self, domainadmin):
|
||||
"""
|
||||
Add a domain admin to the mailcow instance.
|
||||
:param domainadmin: Dictionary containing domain admin details.
|
||||
:return: Response from the mailcow API.
|
||||
"""
|
||||
|
||||
return self.post('/add/domain-admin', domainadmin)
|
||||
|
||||
def deleteDomain(self, domain):
|
||||
"""
|
||||
Delete a domain from the mailcow instance.
|
||||
:param domain: Name of the domain to delete.
|
||||
:return: Response from the mailcow API.
|
||||
"""
|
||||
|
||||
items = [domain]
|
||||
return self.post('/delete/domain', items)
|
||||
|
||||
def deleteAlias(self, id):
|
||||
"""
|
||||
Delete an alias from the mailcow instance.
|
||||
:param id: ID of the alias to delete.
|
||||
:return: Response from the mailcow API.
|
||||
"""
|
||||
|
||||
items = [id]
|
||||
return self.post('/delete/alias', items)
|
||||
|
||||
def deleteSyncjob(self, id):
|
||||
"""
|
||||
Delete a sync job from the mailcow instance.
|
||||
:param id: ID of the sync job to delete.
|
||||
:return: Response from the mailcow API.
|
||||
"""
|
||||
|
||||
items = [id]
|
||||
return self.post('/delete/syncjob', items)
|
||||
|
||||
def deleteMailbox(self, mailbox):
|
||||
"""
|
||||
Delete a mailbox from the mailcow instance.
|
||||
:param mailbox: Name of the mailbox to delete.
|
||||
:return: Response from the mailcow API.
|
||||
"""
|
||||
|
||||
items = [mailbox]
|
||||
return self.post('/delete/mailbox', items)
|
||||
|
||||
def deleteDomainadmin(self, username):
|
||||
"""
|
||||
Delete a domain admin from the mailcow instance.
|
||||
:param username: Username of the domain admin to delete.
|
||||
:return: Response from the mailcow API.
|
||||
"""
|
||||
|
||||
items = [username]
|
||||
return self.post('/delete/domain-admin', items)
|
||||
|
||||
def post(self, endpoint, data):
|
||||
"""
|
||||
Make a POST request to the mailcow API.
|
||||
:param endpoint: The API endpoint to post to.
|
||||
:param data: Data to be sent in the POST request.
|
||||
:return: Response from the mailcow API.
|
||||
"""
|
||||
|
||||
url = f"{self.baseUrl}{self.apiUrl}/{endpoint.lstrip('/')}"
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Host": self.host
|
||||
}
|
||||
if self.apiKey:
|
||||
headers["X-Api-Key"] = self.apiKey
|
||||
response = requests.post(
|
||||
url,
|
||||
json=data,
|
||||
headers=headers,
|
||||
verify=not self.ignore_ssl_errors
|
||||
)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
def getDomain(self, domain):
|
||||
"""
|
||||
Get a domain from the mailcow instance.
|
||||
:param domain: Name of the domain to get.
|
||||
:return: Response from the mailcow API.
|
||||
"""
|
||||
|
||||
return self.get(f'/get/domain/{domain}')
|
||||
|
||||
def getMailbox(self, username):
|
||||
"""
|
||||
Get a mailbox from the mailcow instance.
|
||||
:param mailbox: Dictionary containing mailbox details (e.g. {"username": "user@example.com"})
|
||||
:return: Response from the mailcow API.
|
||||
"""
|
||||
return self.get(f'/get/mailbox/{username}')
|
||||
|
||||
def getAlias(self, id):
|
||||
"""
|
||||
Get an alias from the mailcow instance.
|
||||
:param alias: Dictionary containing alias details (e.g. {"address": "alias@example.com"})
|
||||
:return: Response from the mailcow API.
|
||||
"""
|
||||
return self.get(f'/get/alias/{id}')
|
||||
|
||||
def getSyncjob(self, id):
|
||||
"""
|
||||
Get a sync job from the mailcow instance.
|
||||
:param syncjob: Dictionary containing sync job details (e.g. {"id": "123"})
|
||||
:return: Response from the mailcow API.
|
||||
"""
|
||||
return self.get(f'/get/syncjobs/{id}')
|
||||
|
||||
def getDomainadmin(self, username):
|
||||
"""
|
||||
Get a domain admin from the mailcow instance.
|
||||
:param username: Username of the domain admin to get.
|
||||
:return: Response from the mailcow API.
|
||||
"""
|
||||
return self.get(f'/get/domain-admin/{username}')
|
||||
|
||||
def getStatusVersion(self):
|
||||
"""
|
||||
Get the version of the mailcow instance.
|
||||
:return: Response from the mailcow API.
|
||||
"""
|
||||
return self.get('/get/status/version')
|
||||
|
||||
def getStatusVmail(self):
|
||||
"""
|
||||
Get the vmail status from the mailcow instance.
|
||||
:return: Response from the mailcow API.
|
||||
"""
|
||||
return self.get('/get/status/vmail')
|
||||
|
||||
def getStatusContainers(self):
|
||||
"""
|
||||
Get the status of containers from the mailcow instance.
|
||||
:return: Response from the mailcow API.
|
||||
"""
|
||||
return self.get('/get/status/containers')
|
||||
|
||||
def get(self, endpoint, params=None):
|
||||
"""
|
||||
Make a GET request to the mailcow API.
|
||||
:param endpoint: The API endpoint to get from.
|
||||
:param params: Parameters to be sent in the GET request.
|
||||
:return: Response from the mailcow API.
|
||||
"""
|
||||
|
||||
url = f"{self.baseUrl}{self.apiUrl}/{endpoint.lstrip('/')}"
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Host": self.host
|
||||
}
|
||||
if self.apiKey:
|
||||
headers["X-Api-Key"] = self.apiKey
|
||||
response = requests.get(
|
||||
url,
|
||||
params=params,
|
||||
headers=headers,
|
||||
verify=not self.ignore_ssl_errors
|
||||
)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
def editDomain(self, domain, attributes):
|
||||
"""
|
||||
Edit an existing domain in the mailcow instance.
|
||||
:param domain: Name of the domain to edit
|
||||
:param attributes: Dictionary containing the new domain attributes.
|
||||
"""
|
||||
|
||||
items = [domain]
|
||||
return self.edit('/edit/domain', items, attributes)
|
||||
|
||||
def editMailbox(self, mailbox, attributes):
|
||||
"""
|
||||
Edit an existing mailbox in the mailcow instance.
|
||||
:param mailbox: Name of the mailbox to edit
|
||||
:param attributes: Dictionary containing the new mailbox attributes.
|
||||
"""
|
||||
|
||||
items = [mailbox]
|
||||
return self.edit('/edit/mailbox', items, attributes)
|
||||
|
||||
def editAlias(self, alias, attributes):
|
||||
"""
|
||||
Edit an existing alias in the mailcow instance.
|
||||
:param alias: Name of the alias to edit
|
||||
:param attributes: Dictionary containing the new alias attributes.
|
||||
"""
|
||||
|
||||
items = [alias]
|
||||
return self.edit('/edit/alias', items, attributes)
|
||||
|
||||
def editSyncjob(self, syncjob, attributes):
|
||||
"""
|
||||
Edit an existing sync job in the mailcow instance.
|
||||
:param syncjob: Name of the sync job to edit
|
||||
:param attributes: Dictionary containing the new sync job attributes.
|
||||
"""
|
||||
|
||||
items = [syncjob]
|
||||
return self.edit('/edit/syncjob', items, attributes)
|
||||
|
||||
def editDomainadmin(self, username, attributes):
|
||||
"""
|
||||
Edit an existing domain admin in the mailcow instance.
|
||||
:param username: Username of the domain admin to edit
|
||||
:param attributes: Dictionary containing the new domain admin attributes.
|
||||
"""
|
||||
|
||||
items = [username]
|
||||
return self.edit('/edit/domain-admin', items, attributes)
|
||||
|
||||
def edit(self, endpoint, items, attributes):
|
||||
"""
|
||||
Make a POST request to edit items in the mailcow API.
|
||||
:param items: List of items to edit.
|
||||
:param attributes: Dictionary containing the new attributes for the items.
|
||||
:return: Response from the mailcow API.
|
||||
"""
|
||||
|
||||
url = f"{self.baseUrl}{self.apiUrl}/{endpoint.lstrip('/')}"
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Host": self.host
|
||||
}
|
||||
if self.apiKey:
|
||||
headers["X-Api-Key"] = self.apiKey
|
||||
data = {
|
||||
"items": items,
|
||||
"attr": attributes
|
||||
}
|
||||
response = requests.post(
|
||||
url,
|
||||
json=data,
|
||||
headers=headers,
|
||||
verify=not self.ignore_ssl_errors
|
||||
)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
|
||||
# System Functions
|
||||
def runSyncjob(self, id, force=False):
|
||||
"""
|
||||
Run a sync job.
|
||||
:param id: ID of the sync job to run.
|
||||
:return: Response from the imapsync script.
|
||||
"""
|
||||
|
||||
creds_path = "/app/sieve.creds"
|
||||
|
||||
conn = mysql.connector.connect(**self.db_config)
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
|
||||
with open(creds_path, 'r') as file:
|
||||
master_user, master_pass = file.read().strip().split(':')
|
||||
|
||||
query = ("SELECT * FROM imapsync WHERE id = %s")
|
||||
cursor.execute(query, (id,))
|
||||
|
||||
success = False
|
||||
syncjob = cursor.fetchone()
|
||||
if not syncjob:
|
||||
cursor.close()
|
||||
conn.close()
|
||||
return f"Sync job with ID {id} not found."
|
||||
if syncjob['active'] == 0 and not force:
|
||||
cursor.close()
|
||||
conn.close()
|
||||
return f"Sync job with ID {id} is not active."
|
||||
|
||||
enc1_flag = "--tls1" if syncjob['enc1'] == "TLS" else "--ssl1" if syncjob['enc1'] == "SSL" else None
|
||||
|
||||
|
||||
passfile1_path = f"/tmp/passfile1_{id}.txt"
|
||||
passfile2_path = f"/tmp/passfile2_{id}.txt"
|
||||
passfile1_cmd = [
|
||||
"sh", "-c",
|
||||
f"echo {syncjob['password1']} > {passfile1_path}"
|
||||
]
|
||||
passfile2_cmd = [
|
||||
"sh", "-c",
|
||||
f"echo {master_pass} > {passfile2_path}"
|
||||
]
|
||||
|
||||
self.docker.exec_command("dovecot-mailcow", passfile1_cmd)
|
||||
self.docker.exec_command("dovecot-mailcow", passfile2_cmd)
|
||||
|
||||
imapsync_cmd = [
|
||||
"/usr/local/bin/imapsync",
|
||||
"--tmpdir", "/tmp",
|
||||
"--nofoldersizes",
|
||||
"--addheader"
|
||||
]
|
||||
|
||||
if int(syncjob['timeout1']) > 0:
|
||||
imapsync_cmd.extend(['--timeout1', str(syncjob['timeout1'])])
|
||||
if int(syncjob['timeout2']) > 0:
|
||||
imapsync_cmd.extend(['--timeout2', str(syncjob['timeout2'])])
|
||||
if syncjob['exclude']:
|
||||
imapsync_cmd.extend(['--exclude', syncjob['exclude']])
|
||||
if syncjob['subfolder2']:
|
||||
imapsync_cmd.extend(['--subfolder2', syncjob['subfolder2']])
|
||||
if int(syncjob['maxage']) > 0:
|
||||
imapsync_cmd.extend(['--maxage', str(syncjob['maxage'])])
|
||||
if int(syncjob['maxbytespersecond']) > 0:
|
||||
imapsync_cmd.extend(['--maxbytespersecond', str(syncjob['maxbytespersecond'])])
|
||||
if int(syncjob['delete2duplicates']) == 1:
|
||||
imapsync_cmd.append("--delete2duplicates")
|
||||
if int(syncjob['subscribeall']) == 1:
|
||||
imapsync_cmd.append("--subscribeall")
|
||||
if int(syncjob['delete1']) == 1:
|
||||
imapsync_cmd.append("--delete")
|
||||
if int(syncjob['delete2']) == 1:
|
||||
imapsync_cmd.append("--delete2")
|
||||
if int(syncjob['automap']) == 1:
|
||||
imapsync_cmd.append("--automap")
|
||||
if int(syncjob['skipcrossduplicates']) == 1:
|
||||
imapsync_cmd.append("--skipcrossduplicates")
|
||||
if enc1_flag:
|
||||
imapsync_cmd.append(enc1_flag)
|
||||
|
||||
imapsync_cmd.extend([
|
||||
"--host1", syncjob['host1'],
|
||||
"--user1", syncjob['user1'],
|
||||
"--passfile1", passfile1_path,
|
||||
"--port1", str(syncjob['port1']),
|
||||
"--host2", "localhost",
|
||||
"--user2", f"{syncjob['user2']}*{master_user}",
|
||||
"--passfile2", passfile2_path
|
||||
])
|
||||
|
||||
if syncjob['dry'] == 1:
|
||||
imapsync_cmd.append("--dry")
|
||||
|
||||
imapsync_cmd.extend([
|
||||
"--no-modulesversion",
|
||||
"--noreleasecheck"
|
||||
])
|
||||
|
||||
try:
|
||||
cursor.execute("UPDATE imapsync SET is_running = 1, success = NULL, exit_status = NULL WHERE id = %s", (id,))
|
||||
conn.commit()
|
||||
|
||||
result = self.docker.exec_command("dovecot-mailcow", imapsync_cmd)
|
||||
print(result)
|
||||
|
||||
success = result['status'] == "success" and result['exit_code'] == 0
|
||||
cursor.execute(
|
||||
"UPDATE imapsync SET returned_text = %s, success = %s, exit_status = %s WHERE id = %s",
|
||||
(result['output'], int(success), result['exit_code'], id)
|
||||
)
|
||||
conn.commit()
|
||||
|
||||
except Exception as e:
|
||||
cursor.execute(
|
||||
"UPDATE imapsync SET returned_text = %s, success = 0 WHERE id = %s",
|
||||
(str(e), id)
|
||||
)
|
||||
conn.commit()
|
||||
|
||||
finally:
|
||||
cursor.execute("UPDATE imapsync SET last_run = NOW(), is_running = 0 WHERE id = %s", (id,))
|
||||
conn.commit()
|
||||
|
||||
delete_passfile1_cmd = [
|
||||
"sh", "-c",
|
||||
f"rm -f {passfile1_path}"
|
||||
]
|
||||
delete_passfile2_cmd = [
|
||||
"sh", "-c",
|
||||
f"rm -f {passfile2_path}"
|
||||
]
|
||||
self.docker.exec_command("dovecot-mailcow", delete_passfile1_cmd)
|
||||
self.docker.exec_command("dovecot-mailcow", delete_passfile2_cmd)
|
||||
|
||||
cursor.close()
|
||||
conn.close()
|
||||
|
||||
return "Sync job completed successfully." if success else "Sync job failed."
|
||||
64
data/Dockerfiles/controller/mailcow-adm/modules/Mailer.py
Normal file
64
data/Dockerfiles/controller/mailcow-adm/modules/Mailer.py
Normal file
@@ -0,0 +1,64 @@
|
||||
import smtplib
|
||||
import json
|
||||
import os
|
||||
from email.mime.text import MIMEText
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from jinja2 import Environment, BaseLoader
|
||||
|
||||
class Mailer:
|
||||
def __init__(self, smtp_host, smtp_port, username, password, use_tls=True):
|
||||
self.smtp_host = smtp_host
|
||||
self.smtp_port = smtp_port
|
||||
self.username = username
|
||||
self.password = password
|
||||
self.use_tls = use_tls
|
||||
self.server = None
|
||||
self.env = Environment(loader=BaseLoader())
|
||||
|
||||
def connect(self):
|
||||
print("Connecting to the SMTP server...")
|
||||
self.server = smtplib.SMTP(self.smtp_host, self.smtp_port)
|
||||
if self.use_tls:
|
||||
self.server.starttls()
|
||||
print("TLS activated!")
|
||||
if self.username and self.password:
|
||||
self.server.login(self.username, self.password)
|
||||
print("Authenticated!")
|
||||
|
||||
def disconnect(self):
|
||||
if self.server:
|
||||
try:
|
||||
if self.server.sock:
|
||||
self.server.quit()
|
||||
except smtplib.SMTPServerDisconnected:
|
||||
pass
|
||||
finally:
|
||||
self.server = None
|
||||
|
||||
def render_inline_template(self, template_string, context):
|
||||
template = self.env.from_string(template_string)
|
||||
return template.render(context)
|
||||
|
||||
def send_mail(self, subject, from_addr, to_addrs, template, context = {}):
|
||||
try:
|
||||
if template == "":
|
||||
print("Cannot send email, template is empty!")
|
||||
return "Failed: Template is empty."
|
||||
|
||||
body = self.render_inline_template(template, context)
|
||||
|
||||
msg = MIMEMultipart()
|
||||
msg['From'] = from_addr
|
||||
msg['To'] = ', '.join(to_addrs) if isinstance(to_addrs, list) else to_addrs
|
||||
msg['Subject'] = subject
|
||||
msg.attach(MIMEText(body, 'html'))
|
||||
|
||||
self.connect()
|
||||
self.server.sendmail(from_addr, to_addrs, msg.as_string())
|
||||
self.disconnect()
|
||||
return f"Success: Email sent to {msg['To']}"
|
||||
except Exception as e:
|
||||
print(f"Error during send_mail: {type(e).__name__}: {e}")
|
||||
return f"Failed: {type(e).__name__}: {e}"
|
||||
finally:
|
||||
self.disconnect()
|
||||
51
data/Dockerfiles/controller/mailcow-adm/modules/Reader.py
Normal file
51
data/Dockerfiles/controller/mailcow-adm/modules/Reader.py
Normal file
@@ -0,0 +1,51 @@
|
||||
from jinja2 import Environment, Template
|
||||
import csv
|
||||
|
||||
def split_at(value, sep, idx):
|
||||
try:
|
||||
return value.split(sep)[idx]
|
||||
except Exception:
|
||||
return ''
|
||||
|
||||
class Reader:
|
||||
"""
|
||||
Reader class to handle reading and processing of CSV and JSON files for mailcow.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def read_csv(self, file_path, delimiter=',', encoding='iso-8859-1'):
|
||||
"""
|
||||
Read a CSV file and return a list of dictionaries.
|
||||
Each dictionary represents a row in the CSV file.
|
||||
:param file_path: Path to the CSV file.
|
||||
:param delimiter: Delimiter used in the CSV file (default: ',').
|
||||
"""
|
||||
with open(file_path, mode='r', encoding=encoding) as file:
|
||||
reader = csv.DictReader(file, delimiter=delimiter)
|
||||
reader.fieldnames = [h.replace(" ", "_") if h else h for h in reader.fieldnames]
|
||||
return [row for row in reader]
|
||||
|
||||
def map_csv_data(self, data, mapping_file_path, encoding='iso-8859-1'):
|
||||
"""
|
||||
Map CSV data to a specific structure based on the provided Jinja2 template file.
|
||||
:param data: List of dictionaries representing CSV rows.
|
||||
:param mapping_file_path: Path to the Jinja2 template file.
|
||||
:return: List of dictionaries with mapped data.
|
||||
"""
|
||||
with open(mapping_file_path, 'r', encoding=encoding) as tpl_file:
|
||||
template_content = tpl_file.read()
|
||||
env = Environment()
|
||||
env.filters['split_at'] = split_at
|
||||
template = env.from_string(template_content)
|
||||
|
||||
mapped_data = []
|
||||
for row in data:
|
||||
rendered = template.render(**row)
|
||||
try:
|
||||
mapped_row = eval(rendered)
|
||||
except Exception:
|
||||
mapped_row = rendered
|
||||
mapped_data.append(mapped_row)
|
||||
return mapped_data
|
||||
512
data/Dockerfiles/controller/mailcow-adm/modules/Sogo.py
Normal file
512
data/Dockerfiles/controller/mailcow-adm/modules/Sogo.py
Normal file
@@ -0,0 +1,512 @@
|
||||
import requests
|
||||
import urllib3
|
||||
import os
|
||||
from uuid import uuid4
|
||||
from collections import defaultdict
|
||||
|
||||
|
||||
class Sogo:
|
||||
def __init__(self, username, password=""):
|
||||
self.apiUrl = "/SOGo/so"
|
||||
self.davUrl = "/SOGo/dav"
|
||||
self.ignore_ssl_errors = True
|
||||
|
||||
self.baseUrl = f"https://{os.getenv('IPv4_NETWORK', '172.22.1')}.247"
|
||||
self.host = os.getenv("MAILCOW_HOSTNAME", "")
|
||||
if self.ignore_ssl_errors:
|
||||
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
||||
self.username = username
|
||||
self.password = password
|
||||
|
||||
def addCalendar(self, calendar_name):
|
||||
"""
|
||||
Add a new calendar to the sogo instance.
|
||||
:param calendar_name: Name of the calendar to be created
|
||||
:return: Response from the sogo API.
|
||||
"""
|
||||
|
||||
res = self.post(f"/{self.username}/Calendar/createFolder", {
|
||||
"name": calendar_name
|
||||
})
|
||||
try:
|
||||
return res.json()
|
||||
except ValueError:
|
||||
return res.text
|
||||
|
||||
def getCalendarIdByName(self, calendar_name):
|
||||
"""
|
||||
Get the calendar ID by its name.
|
||||
:param calendar_name: Name of the calendar to find
|
||||
:return: Calendar ID if found, otherwise None.
|
||||
"""
|
||||
|
||||
res = self.get(f"/{self.username}/Calendar/calendarslist")
|
||||
try:
|
||||
for calendar in res.json()["calendars"]:
|
||||
if calendar['name'] == calendar_name:
|
||||
return calendar['id']
|
||||
except ValueError:
|
||||
return None
|
||||
return None
|
||||
|
||||
def getCalendar(self):
|
||||
"""
|
||||
Get calendar list.
|
||||
:return: Response from SOGo API.
|
||||
"""
|
||||
|
||||
res = self.get(f"/{self.username}/Calendar/calendarslist")
|
||||
try:
|
||||
return res.json()
|
||||
except ValueError:
|
||||
return res.text
|
||||
|
||||
def deleteCalendar(self, calendar_id):
|
||||
"""
|
||||
Delete a calendar.
|
||||
:param calendar_id: ID of the calendar to be deleted
|
||||
:return: Response from SOGo API.
|
||||
"""
|
||||
res = self.get(f"/{self.username}/Calendar/{calendar_id}/delete")
|
||||
return res.status_code == 204
|
||||
|
||||
def importCalendar(self, calendar_name, ics_file):
|
||||
"""
|
||||
Import a calendar from an ICS file.
|
||||
:param calendar_name: Name of the calendar to import into
|
||||
:param ics_file: Path to the ICS file to import
|
||||
:return: Response from SOGo API.
|
||||
"""
|
||||
|
||||
try:
|
||||
with open(ics_file, "rb") as f:
|
||||
pass
|
||||
except Exception as e:
|
||||
print(f"Could not open ICS file '{ics_file}': {e}")
|
||||
return {"status": "error", "message": str(e)}
|
||||
|
||||
new_calendar = self.addCalendar(calendar_name)
|
||||
selected_calendar = new_calendar.json()["id"]
|
||||
|
||||
url = f"{self.baseUrl}{self.apiUrl}/{self.username}/Calendar/{selected_calendar}/import"
|
||||
auth = (self.username, self.password)
|
||||
with open(ics_file, "rb") as f:
|
||||
files = {'icsFile': (ics_file, f, 'text/calendar')}
|
||||
res = requests.post(
|
||||
url,
|
||||
files=files,
|
||||
auth=auth,
|
||||
verify=not self.ignore_ssl_errors
|
||||
)
|
||||
try:
|
||||
return res.json()
|
||||
except ValueError:
|
||||
return res.text
|
||||
|
||||
return None
|
||||
|
||||
def setCalendarACL(self, calendar_id, sharee_email, acl="r", subscribe=False):
|
||||
"""
|
||||
Set CalDAV calendar permissions for a user (sharee).
|
||||
:param calendar_id: ID of the calendar to share
|
||||
:param sharee_email: Email of the user to share with
|
||||
:param acl: "w" for write, "r" for read-only or combination "rw" for read-write
|
||||
:param subscribe: True will scubscribe the sharee to the calendar
|
||||
:return: None
|
||||
"""
|
||||
|
||||
# Access rights
|
||||
if acl == "" or len(acl) > 2:
|
||||
return "Invalid acl level specified. Use 'w', 'r' or combinations like 'rw'."
|
||||
rights = [{
|
||||
"c_email": sharee_email,
|
||||
"uid": sharee_email,
|
||||
"userClass": "normal-user",
|
||||
"rights": {
|
||||
"Public": "None",
|
||||
"Private": "None",
|
||||
"Confidential": "None",
|
||||
"canCreateObjects": 0,
|
||||
"canEraseObjects": 0
|
||||
}
|
||||
}]
|
||||
if "w" in acl:
|
||||
rights[0]["rights"]["canCreateObjects"] = 1
|
||||
rights[0]["rights"]["canEraseObjects"] = 1
|
||||
if "r" in acl:
|
||||
rights[0]["rights"]["Public"] = "Viewer"
|
||||
rights[0]["rights"]["Private"] = "Viewer"
|
||||
rights[0]["rights"]["Confidential"] = "Viewer"
|
||||
|
||||
r_add = self.get(f"/{self.username}/Calendar/{calendar_id}/addUserInAcls?uid={sharee_email}")
|
||||
if r_add.status_code < 200 or r_add.status_code > 299:
|
||||
try:
|
||||
return r_add.json()
|
||||
except ValueError:
|
||||
return r_add.text
|
||||
|
||||
r_save = self.post(f"/{self.username}/Calendar/{calendar_id}/saveUserRights", rights)
|
||||
if r_save.status_code < 200 or r_save.status_code > 299:
|
||||
try:
|
||||
return r_save.json()
|
||||
except ValueError:
|
||||
return r_save.text
|
||||
|
||||
if subscribe:
|
||||
r_subscribe = self.get(f"/{self.username}/Calendar/{calendar_id}/subscribeUsers?uids={sharee_email}")
|
||||
if r_subscribe.status_code < 200 or r_subscribe.status_code > 299:
|
||||
try:
|
||||
return r_subscribe.json()
|
||||
except ValueError:
|
||||
return r_subscribe.text
|
||||
|
||||
return r_save.status_code == 200
|
||||
|
||||
def getCalendarACL(self, calendar_id):
|
||||
"""
|
||||
Get CalDAV calendar permissions for a user (sharee).
|
||||
:param calendar_id: ID of the calendar to get ACL from
|
||||
:return: Response from SOGo API.
|
||||
"""
|
||||
|
||||
res = self.get(f"/{self.username}/Calendar/{calendar_id}/acls")
|
||||
try:
|
||||
return res.json()
|
||||
except ValueError:
|
||||
return res.text
|
||||
|
||||
def deleteCalendarACL(self, calendar_id, sharee_email):
|
||||
"""
|
||||
Delete a calendar ACL for a user (sharee).
|
||||
:param calendar_id: ID of the calendar to delete ACL from
|
||||
:param sharee_email: Email of the user whose ACL to delete
|
||||
:return: Response from SOGo API.
|
||||
"""
|
||||
|
||||
res = self.get(f"/{self.username}/Calendar/{calendar_id}/removeUserFromAcls?uid={sharee_email}")
|
||||
return res.status_code == 204
|
||||
|
||||
def addAddressbook(self, addressbook_name):
|
||||
"""
|
||||
Add a new addressbook to the sogo instance.
|
||||
:param addressbook_name: Name of the addressbook to be created
|
||||
:return: Response from the sogo API.
|
||||
"""
|
||||
|
||||
res = self.post(f"/{self.username}/Contacts/createFolder", {
|
||||
"name": addressbook_name
|
||||
})
|
||||
try:
|
||||
return res.json()
|
||||
except ValueError:
|
||||
return res.text
|
||||
|
||||
def getAddressbookIdByName(self, addressbook_name):
|
||||
"""
|
||||
Get the addressbook ID by its name.
|
||||
:param addressbook_name: Name of the addressbook to find
|
||||
:return: Addressbook ID if found, otherwise None.
|
||||
"""
|
||||
|
||||
res = self.get(f"/{self.username}/Contacts/addressbooksList")
|
||||
try:
|
||||
for addressbook in res.json()["addressbooks"]:
|
||||
if addressbook['name'] == addressbook_name:
|
||||
return addressbook['id']
|
||||
except ValueError:
|
||||
return None
|
||||
return None
|
||||
|
||||
def deleteAddressbook(self, addressbook_id):
|
||||
"""
|
||||
Delete an addressbook.
|
||||
:param addressbook_id: ID of the addressbook to be deleted
|
||||
:return: Response from SOGo API.
|
||||
"""
|
||||
|
||||
res = self.get(f"/{self.username}/Contacts/{addressbook_id}/delete")
|
||||
return res.status_code == 204
|
||||
|
||||
def getAddressbookList(self):
|
||||
"""
|
||||
Get addressbook list.
|
||||
:return: Response from SOGo API.
|
||||
"""
|
||||
|
||||
res = self.get(f"/{self.username}/Contacts/addressbooksList")
|
||||
try:
|
||||
return res.json()
|
||||
except ValueError:
|
||||
return res.text
|
||||
|
||||
def setAddressbookACL(self, addressbook_id, sharee_email, acl="r", subscribe=False):
|
||||
"""
|
||||
Set CalDAV addressbook permissions for a user (sharee).
|
||||
:param addressbook_id: ID of the addressbook to share
|
||||
:param sharee_email: Email of the user to share with
|
||||
:param acl: "w" for write, "r" for read-only or combination "rw" for read-write
|
||||
:param subscribe: True will subscribe the sharee to the addressbook
|
||||
:return: None
|
||||
"""
|
||||
|
||||
# Access rights
|
||||
if acl == "" or len(acl) > 2:
|
||||
print("Invalid acl level specified. Use 's', 'w', 'r' or combinations like 'rws'.")
|
||||
return "Invalid acl level specified. Use 'w', 'r' or combinations like 'rw'."
|
||||
rights = [{
|
||||
"c_email": sharee_email,
|
||||
"uid": sharee_email,
|
||||
"userClass": "normal-user",
|
||||
"rights": {
|
||||
"canCreateObjects": 0,
|
||||
"canEditObjects": 0,
|
||||
"canEraseObjects": 0,
|
||||
"canViewObjects": 0,
|
||||
}
|
||||
}]
|
||||
if "w" in acl:
|
||||
rights[0]["rights"]["canCreateObjects"] = 1
|
||||
rights[0]["rights"]["canEditObjects"] = 1
|
||||
rights[0]["rights"]["canEraseObjects"] = 1
|
||||
if "r" in acl:
|
||||
rights[0]["rights"]["canViewObjects"] = 1
|
||||
|
||||
r_add = self.get(f"/{self.username}/Contacts/{addressbook_id}/addUserInAcls?uid={sharee_email}")
|
||||
if r_add.status_code < 200 or r_add.status_code > 299:
|
||||
try:
|
||||
return r_add.json()
|
||||
except ValueError:
|
||||
return r_add.text
|
||||
|
||||
r_save = self.post(f"/{self.username}/Contacts/{addressbook_id}/saveUserRights", rights)
|
||||
if r_save.status_code < 200 or r_save.status_code > 299:
|
||||
try:
|
||||
return r_save.json()
|
||||
except ValueError:
|
||||
return r_save.text
|
||||
|
||||
if subscribe:
|
||||
r_subscribe = self.get(f"/{self.username}/Contacts/{addressbook_id}/subscribeUsers?uids={sharee_email}")
|
||||
if r_subscribe.status_code < 200 or r_subscribe.status_code > 299:
|
||||
try:
|
||||
return r_subscribe.json()
|
||||
except ValueError:
|
||||
return r_subscribe.text
|
||||
|
||||
return r_save.status_code == 200
|
||||
|
||||
def getAddressbookACL(self, addressbook_id):
|
||||
"""
|
||||
Get CalDAV addressbook permissions for a user (sharee).
|
||||
:param addressbook_id: ID of the addressbook to get ACL from
|
||||
:return: Response from SOGo API.
|
||||
"""
|
||||
|
||||
res = self.get(f"/{self.username}/Contacts/{addressbook_id}/acls")
|
||||
try:
|
||||
return res.json()
|
||||
except ValueError:
|
||||
return res.text
|
||||
|
||||
def deleteAddressbookACL(self, addressbook_id, sharee_email):
|
||||
"""
|
||||
Delete an addressbook ACL for a user (sharee).
|
||||
:param addressbook_id: ID of the addressbook to delete ACL from
|
||||
:param sharee_email: Email of the user whose ACL to delete
|
||||
:return: Response from SOGo API.
|
||||
"""
|
||||
|
||||
res = self.get(f"/{self.username}/Contacts/{addressbook_id}/removeUserFromAcls?uid={sharee_email}")
|
||||
return res.status_code == 204
|
||||
|
||||
def getAddressbookNewGuid(self, addressbook_id):
|
||||
"""
|
||||
Request a new GUID for a SOGo addressbook.
|
||||
:param addressbook_id: ID of the addressbook
|
||||
:return: JSON response from SOGo or None if not found
|
||||
"""
|
||||
res = self.get(f"/{self.username}/Contacts/{addressbook_id}/newguid")
|
||||
try:
|
||||
return res.json()
|
||||
except ValueError:
|
||||
return res.text
|
||||
|
||||
def addAddressbookContact(self, addressbook_id, contact_name, contact_email):
|
||||
"""
|
||||
Save a vCard as a contact in the specified addressbook.
|
||||
:param addressbook_id: ID of the addressbook
|
||||
:param contact_name: Name of the contact
|
||||
:param contact_email: Email of the contact
|
||||
:return: JSON response from SOGo or None if not found
|
||||
"""
|
||||
vcard_id = self.getAddressbookNewGuid(addressbook_id)
|
||||
contact_data = {
|
||||
"id": vcard_id["id"],
|
||||
"pid": vcard_id["pid"],
|
||||
"c_cn": contact_name,
|
||||
"emails": [{
|
||||
"type": "pref",
|
||||
"value": contact_email
|
||||
}],
|
||||
"isNew": True,
|
||||
"c_component": "vcard",
|
||||
}
|
||||
|
||||
endpoint = f"/{self.username}/Contacts/{addressbook_id}/{vcard_id['id']}/saveAsContact"
|
||||
res = self.post(endpoint, contact_data)
|
||||
try:
|
||||
return res.json()
|
||||
except ValueError:
|
||||
return res.text
|
||||
|
||||
def getAddressbookContacts(self, addressbook_id, contact_email=None):
|
||||
"""
|
||||
Get all contacts from the specified addressbook.
|
||||
:param addressbook_id: ID of the addressbook
|
||||
:return: JSON response with contacts or None if not found
|
||||
"""
|
||||
res = self.get(f"/{self.username}/Contacts/{addressbook_id}/view")
|
||||
try:
|
||||
res_json = res.json()
|
||||
headers = res_json.get("headers", [])
|
||||
if not headers or len(headers) < 2:
|
||||
return []
|
||||
|
||||
field_names = headers[0]
|
||||
contacts = []
|
||||
for row in headers[1:]:
|
||||
contact = dict(zip(field_names, row))
|
||||
contacts.append(contact)
|
||||
|
||||
if contact_email:
|
||||
contact = {}
|
||||
for c in contacts:
|
||||
if c["c_mail"] == contact_email or c["c_cn"] == contact_email:
|
||||
contact = c
|
||||
break
|
||||
return contact
|
||||
|
||||
return contacts
|
||||
except ValueError:
|
||||
return res.text
|
||||
|
||||
def addAddressbookContactList(self, addressbook_id, contact_name, contact_email=None):
|
||||
"""
|
||||
Add a new contact list to the addressbook.
|
||||
:param addressbook_id: ID of the addressbook
|
||||
:param contact_name: Name of the contact list
|
||||
:param contact_email: Comma-separated emails to include in the list
|
||||
:return: Response from SOGo API.
|
||||
"""
|
||||
gal_domain = self.username.split("@")[-1]
|
||||
vlist_id = self.getAddressbookNewGuid(addressbook_id)
|
||||
contact_emails = contact_email.split(",") if contact_email else []
|
||||
contacts = self.getAddressbookContacts(addressbook_id)
|
||||
|
||||
refs = []
|
||||
for contact in contacts:
|
||||
if contact['c_mail'] in contact_emails:
|
||||
refs.append({
|
||||
"refs": [],
|
||||
"categories": [],
|
||||
"c_screenname": contact.get("c_screenname", ""),
|
||||
"pid": contact.get("pid", vlist_id["pid"]),
|
||||
"id": contact.get("id", ""),
|
||||
"notes": [""],
|
||||
"empty": " ",
|
||||
"hasphoto": contact.get("hasphoto", 0),
|
||||
"c_cn": contact.get("c_cn", ""),
|
||||
"c_uid": contact.get("c_uid", None),
|
||||
"containername": contact.get("containername", f"GAL {gal_domain}"), # or your addressbook name
|
||||
"sourceid": contact.get("sourceid", gal_domain),
|
||||
"c_component": contact.get("c_component", "vcard"),
|
||||
"c_sn": contact.get("c_sn", ""),
|
||||
"c_givenname": contact.get("c_givenname", ""),
|
||||
"c_name": contact.get("c_name", contact.get("id", "")),
|
||||
"c_telephonenumber": contact.get("c_telephonenumber", ""),
|
||||
"fn": contact.get("fn", ""),
|
||||
"c_mail": contact.get("c_mail", ""),
|
||||
"emails": contact.get("emails", []),
|
||||
"c_o": contact.get("c_o", ""),
|
||||
"reference": contact.get("id", ""),
|
||||
"birthday": contact.get("birthday", "")
|
||||
})
|
||||
|
||||
contact_data = {
|
||||
"refs": refs,
|
||||
"categories": [],
|
||||
"c_screenname": None,
|
||||
"pid": vlist_id["pid"],
|
||||
"c_component": "vlist",
|
||||
"notes": [""],
|
||||
"empty": " ",
|
||||
"isNew": True,
|
||||
"id": vlist_id["id"],
|
||||
"c_cn": contact_name,
|
||||
"birthday": ""
|
||||
}
|
||||
|
||||
endpoint = f"/{self.username}/Contacts/{addressbook_id}/{vlist_id['id']}/saveAsList"
|
||||
res = self.post(endpoint, contact_data)
|
||||
try:
|
||||
return res.json()
|
||||
except ValueError:
|
||||
return res.text
|
||||
|
||||
def deleteAddressbookItem(self, addressbook_id, contact_name):
|
||||
"""
|
||||
Delete an addressbook item by its ID.
|
||||
:param addressbook_id: ID of the addressbook item to delete
|
||||
:param contact_name: Name of the contact to delete
|
||||
:return: Response from SOGo API.
|
||||
"""
|
||||
res = self.getAddressbookContacts(addressbook_id, contact_name)
|
||||
|
||||
if "id" not in res:
|
||||
print(f"Contact '{contact_name}' not found in addressbook '{addressbook_id}'.")
|
||||
return None
|
||||
res = self.post(f"/{self.username}/Contacts/{addressbook_id}/batchDelete", {
|
||||
"uids": [res["id"]],
|
||||
})
|
||||
return res.status_code == 204
|
||||
|
||||
def get(self, endpoint, params=None):
|
||||
"""
|
||||
Make a GET request to the mailcow API.
|
||||
:param endpoint: The API endpoint to get.
|
||||
:param params: Optional parameters for the GET request.
|
||||
:return: Response from the mailcow API.
|
||||
"""
|
||||
url = f"{self.baseUrl}{self.apiUrl}{endpoint}"
|
||||
auth = (self.username, self.password)
|
||||
headers = {"Host": self.host}
|
||||
|
||||
response = requests.get(
|
||||
url,
|
||||
params=params,
|
||||
auth=auth,
|
||||
headers=headers,
|
||||
verify=not self.ignore_ssl_errors
|
||||
)
|
||||
return response
|
||||
|
||||
def post(self, endpoint, data):
|
||||
"""
|
||||
Make a POST request to the mailcow API.
|
||||
:param endpoint: The API endpoint to post to.
|
||||
:param data: Data to be sent in the POST request.
|
||||
:return: Response from the mailcow API.
|
||||
"""
|
||||
url = f"{self.baseUrl}{self.apiUrl}{endpoint}"
|
||||
auth = (self.username, self.password)
|
||||
headers = {"Host": self.host}
|
||||
|
||||
response = requests.post(
|
||||
url,
|
||||
json=data,
|
||||
auth=auth,
|
||||
headers=headers,
|
||||
verify=not self.ignore_ssl_errors
|
||||
)
|
||||
return response
|
||||
|
||||
37
data/Dockerfiles/controller/mailcow-adm/modules/Utils.py
Normal file
37
data/Dockerfiles/controller/mailcow-adm/modules/Utils.py
Normal file
@@ -0,0 +1,37 @@
|
||||
import json
|
||||
import random
|
||||
import string
|
||||
|
||||
|
||||
class Utils:
|
||||
def __init(self):
|
||||
pass
|
||||
|
||||
def normalize_email(self, email):
|
||||
replacements = {
|
||||
"ä": "ae", "ö": "oe", "ü": "ue", "ß": "ss",
|
||||
"Ä": "Ae", "Ö": "Oe", "Ü": "Ue"
|
||||
}
|
||||
for orig, repl in replacements.items():
|
||||
email = email.replace(orig, repl)
|
||||
return email
|
||||
|
||||
def generate_password(self, length=8):
|
||||
chars = string.ascii_letters + string.digits
|
||||
return ''.join(random.choices(chars, k=length))
|
||||
|
||||
def pprint(self, data=""):
|
||||
"""
|
||||
Pretty print a dictionary, list, or text.
|
||||
If data is a text containing JSON, it will be printed in a formatted way.
|
||||
"""
|
||||
if isinstance(data, (dict, list)):
|
||||
print(json.dumps(data, indent=2, ensure_ascii=False))
|
||||
elif isinstance(data, str):
|
||||
try:
|
||||
json_data = json.loads(data)
|
||||
print(json.dumps(json_data, indent=2, ensure_ascii=False))
|
||||
except json.JSONDecodeError:
|
||||
print(data)
|
||||
else:
|
||||
print(data)
|
||||
Reference in New Issue
Block a user