mirror of
https://github.com/mailcow/mailcow-dockerized.git
synced 2026-02-15 21:10:04 +00:00
Compare commits
31 Commits
2025-09a
...
feat/pytho
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8d211ea767 | ||
|
|
4cc463a728 | ||
|
|
909eb8a63a | ||
|
|
4c4a440cdf | ||
|
|
743bfcec60 | ||
|
|
744aa5d137 | ||
|
|
e8d155d7e0 | ||
|
|
6382e3128d | ||
|
|
bc11aed753 | ||
|
|
a6e13daa8c | ||
|
|
eb7d2628ac | ||
|
|
5f93ff04a9 | ||
|
|
f35def48cb | ||
|
|
dba9675a9b | ||
|
|
13b4f86d29 | ||
|
|
3eb17a5f78 | ||
|
|
c38a4c203e | ||
|
|
f329549c2e | ||
|
|
767d746419 | ||
|
|
9174a05af3 | ||
|
|
faf8fa8c2c | ||
|
|
55d90afee4 | ||
|
|
669f75182d | ||
|
|
5a39ae45cb | ||
|
|
8baa3c9fb5 | ||
|
|
b8888521f1 | ||
|
|
2efea9c832 | ||
|
|
5a097ed5f7 | ||
|
|
cde2ba4851 | ||
|
|
1d482ed425 | ||
|
|
d3185c3c68 |
44
.gitignore
vendored
44
.gitignore
vendored
@@ -1,6 +1,3 @@
|
||||
!data/conf/nginx/dynmaps.conf
|
||||
!data/conf/nginx/meta_exporter.conf
|
||||
!data/conf/nginx/site.conf
|
||||
!/**/.gitkeep
|
||||
*.iml
|
||||
.idea
|
||||
@@ -8,52 +5,33 @@
|
||||
data/assets/ssl-example/*
|
||||
data/assets/ssl/*
|
||||
data/conf/borgmatic/
|
||||
data/conf/clamav/whitelist.ign2
|
||||
data/conf/dovecot/acl_anyone
|
||||
data/conf/dovecot/dovecot-master.passwd
|
||||
data/conf/dovecot/dovecot-master.userdb
|
||||
data/conf/clamav/rendered_configs
|
||||
data/conf/dovecot/extra.conf
|
||||
data/conf/dovecot/mail_replica.conf
|
||||
data/conf/dovecot/global_sieve_*
|
||||
data/conf/dovecot/last_login
|
||||
data/conf/dovecot/lua
|
||||
data/conf/dovecot/mail_plugins*
|
||||
data/conf/dovecot/shared_namespace.conf
|
||||
data/conf/dovecot/sni.conf
|
||||
data/conf/dovecot/sogo-sso.conf
|
||||
data/conf/dovecot/sogo_trusted_ip.conf
|
||||
data/conf/dovecot/sql
|
||||
data/conf/dovecot/conf.d/fts.conf
|
||||
data/conf/nextcloud-*.bak
|
||||
data/conf/nginx/*.active
|
||||
data/conf/nginx/*.bak
|
||||
data/conf/nginx/*.conf
|
||||
data/conf/nginx/*.custom
|
||||
data/conf/dovecot/rendered_configs
|
||||
data/conf/nginx/rendered_configs
|
||||
data/conf/phpfpm/sogo-sso/sogo-sso.pass
|
||||
data/conf/phpfpm/rendered_configs
|
||||
data/conf/portainer/
|
||||
data/conf/postfix/allow_mailcow_local.regexp
|
||||
data/conf/postfix/custom_postscreen_whitelist.cidr
|
||||
data/conf/postfix/custom_transport.pcre
|
||||
data/conf/postfix/extra.cf
|
||||
data/conf/postfix/sni.map
|
||||
data/conf/postfix/sni.map.db
|
||||
data/conf/postfix/sql
|
||||
data/conf/postfix/dns_blocklists.cf
|
||||
data/conf/postfix/dnsbl_reply.map
|
||||
data/conf/postfix/rendered_configs
|
||||
data/conf/rspamd/custom/*
|
||||
data/conf/rspamd/local.d/*
|
||||
data/conf/rspamd/override.d/*
|
||||
data/conf/rspamd/rendered_configs
|
||||
data/conf/sogo/custom-theme.js
|
||||
data/conf/sogo/plist_ldap
|
||||
data/conf/sogo/plist_ldap.sh
|
||||
data/conf/sogo/sieve.creds
|
||||
data/conf/sogo/cron.creds
|
||||
data/conf/sogo/custom-fulllogo.svg
|
||||
data/conf/sogo/custom-shortlogo.svg
|
||||
data/conf/sogo/custom-fulllogo.png
|
||||
data/conf/sogo/rendered_configs
|
||||
data/conf/mysql/rendered_configs
|
||||
data/gitea/
|
||||
data/gogs/
|
||||
data/hooks/clamd/*
|
||||
data/hooks/dovecot/*
|
||||
data/hooks/mariadb/*
|
||||
data/hooks/nginx/*
|
||||
data/hooks/phpfpm/*
|
||||
data/hooks/postfix/*
|
||||
data/hooks/rspamd/*
|
||||
|
||||
66
data/Dockerfiles/bootstrap/main.py
Normal file
66
data/Dockerfiles/bootstrap/main.py
Normal file
@@ -0,0 +1,66 @@
|
||||
import os
|
||||
import sys
|
||||
import signal
|
||||
import ipaddress
|
||||
|
||||
def handle_sigterm(signum, frame):
|
||||
print("Received SIGTERM, exiting gracefully...")
|
||||
sys.exit(0)
|
||||
|
||||
def get_mysql_config(service_name):
|
||||
db_config = {
|
||||
"user": os.getenv("DBUSER") or os.getenv("MYSQL_USER"),
|
||||
"password": os.getenv("DBPASS") or os.getenv("MYSQL_PASSWORD"),
|
||||
"database": os.getenv("DBNAME") or os.getenv("MYSQL_DATABASE"),
|
||||
"connection_timeout": 2,
|
||||
"service_table": "service_settings",
|
||||
"service_types": [service_name]
|
||||
}
|
||||
|
||||
db_host = os.getenv("DB_HOST")
|
||||
if db_host.startswith("/"):
|
||||
db_config["host"] = "localhost"
|
||||
db_config["unix_socket"] = db_host
|
||||
else:
|
||||
db_config["host"] = db_host
|
||||
|
||||
return db_config
|
||||
|
||||
def get_redis_config():
|
||||
redis_config = {
|
||||
"read_host": os.getenv("REDIS_HOST"),
|
||||
"read_port": 6379,
|
||||
"write_host": os.getenv("REDIS_SLAVEOF_IP") or os.getenv("REDIS_HOST"),
|
||||
"write_port": int(os.getenv("REDIS_SLAVEOF_PORT") or 6379),
|
||||
"password": os.getenv("REDISPASS"),
|
||||
"db": 0
|
||||
}
|
||||
|
||||
return redis_config
|
||||
|
||||
def main():
|
||||
signal.signal(signal.SIGTERM, handle_sigterm)
|
||||
|
||||
container_name = os.getenv("CONTAINER_NAME")
|
||||
service_name = container_name.replace("-mailcow", "").replace("-", "")
|
||||
module_name = f"Bootstrap{service_name.capitalize()}"
|
||||
|
||||
try:
|
||||
mod = __import__(f"modules.{module_name}", fromlist=[module_name])
|
||||
Bootstrap = getattr(mod, module_name)
|
||||
except (ImportError, AttributeError) as e:
|
||||
print(f"Failed to load bootstrap module for: {container_name} → {module_name}")
|
||||
print(str(e))
|
||||
sys.exit(1)
|
||||
|
||||
b = Bootstrap(
|
||||
container=container_name,
|
||||
service=service_name,
|
||||
db_config=get_mysql_config(service_name),
|
||||
redis_config=get_redis_config()
|
||||
)
|
||||
|
||||
b.bootstrap()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
827
data/Dockerfiles/bootstrap/modules/BootstrapBase.py
Normal file
827
data/Dockerfiles/bootstrap/modules/BootstrapBase.py
Normal file
@@ -0,0 +1,827 @@
|
||||
import os
|
||||
import pwd
|
||||
import grp
|
||||
import shutil
|
||||
import secrets
|
||||
import string
|
||||
import subprocess
|
||||
import time
|
||||
import socket
|
||||
import re
|
||||
import redis
|
||||
import hashlib
|
||||
import json
|
||||
import psutil
|
||||
import signal
|
||||
from urllib.parse import quote
|
||||
from pathlib import Path
|
||||
import dns.resolver
|
||||
import mysql.connector
|
||||
|
||||
class BootstrapBase:
|
||||
def __init__(self, container, service, db_config, redis_config):
|
||||
self.container = container
|
||||
self.service = service
|
||||
self.db_config = db_config
|
||||
self.redis_config = redis_config
|
||||
|
||||
self.env = None
|
||||
self.env_vars = None
|
||||
self.mysql_conn = None
|
||||
self.redis_connr = None
|
||||
self.redis_connw = None
|
||||
|
||||
def render_config(self, config_dir):
|
||||
"""
|
||||
Renders multiple Jinja2 templates from a config.json file in a given directory.
|
||||
|
||||
Args:
|
||||
config_dir (str or Path): Path to the directory containing config.json
|
||||
|
||||
Behavior:
|
||||
- Renders each template defined in config.json
|
||||
- Writes the result to the specified output path
|
||||
- Also copies the rendered file to: <config_dir>/rendered_configs/<relative_output_path>
|
||||
"""
|
||||
|
||||
config_dir = Path(config_dir)
|
||||
config_path = config_dir / "config.json"
|
||||
|
||||
if not config_path.exists():
|
||||
print(f"config.json not found in: {config_dir}")
|
||||
return
|
||||
|
||||
with config_path.open("r") as f:
|
||||
entries = json.load(f)
|
||||
|
||||
for entry in entries:
|
||||
template_name = entry["template"]
|
||||
output_path = Path(entry["output"])
|
||||
clean_blank_lines = entry.get("clean_blank_lines", False)
|
||||
if_not_exists = entry.get("if_not_exists", False)
|
||||
|
||||
if if_not_exists and output_path.exists():
|
||||
print(f"Skipping {output_path} (already exists)")
|
||||
continue
|
||||
|
||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
try:
|
||||
template = self.env.get_template(template_name)
|
||||
except Exception as e:
|
||||
print(f"Template not found: {template_name} ({e})")
|
||||
continue
|
||||
|
||||
rendered = template.render(self.env_vars)
|
||||
|
||||
if clean_blank_lines:
|
||||
rendered = "\n".join(line for line in rendered.splitlines() if line.strip())
|
||||
|
||||
rendered = rendered.replace('\r\n', '\n').replace('\r', '\n')
|
||||
|
||||
with output_path.open("w") as f:
|
||||
f.write(rendered)
|
||||
|
||||
rendered_copy_path = config_dir / "rendered_configs" / output_path.name
|
||||
rendered_copy_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
self.copy_file(output_path, rendered_copy_path)
|
||||
|
||||
print(f"Rendered {template_name} → {output_path}")
|
||||
|
||||
def prepare_template_vars(self, overwrite_path, extra_vars = None):
|
||||
"""
|
||||
Loads and merges environment variables for Jinja2 templates from multiple sources, and registers custom template filters.
|
||||
|
||||
This method combines variables from:
|
||||
1. System environment variables
|
||||
2. The MySQL `service_settings` table (filtered by service type if defined)
|
||||
3. An optional `extra_vars` dictionary
|
||||
4. A JSON overwrite file (if it exists at the given path)
|
||||
|
||||
Also registers custom Jinja2 filters.
|
||||
|
||||
Args:
|
||||
overwrite_path (str or Path): Path to a JSON file containing key-value overrides.
|
||||
extra_vars (dict, optional): Additional variables to merge into the environment.
|
||||
|
||||
Returns:
|
||||
dict: A dictionary containing all resolved template variables.
|
||||
|
||||
Raises:
|
||||
Prints errors if database fetch or JSON parsing fails, but does not raise exceptions.
|
||||
"""
|
||||
|
||||
# 1. setup filters
|
||||
self.env.filters['sha1'] = self.sha1_filter
|
||||
self.env.filters['urlencode'] = self.urlencode_filter
|
||||
self.env.filters['escape_quotes'] = self.escape_quotes_filter
|
||||
|
||||
# 2. Load env vars
|
||||
env_vars = dict(os.environ)
|
||||
|
||||
# 3. Load from MySQL
|
||||
try:
|
||||
cursor = self.mysql_conn.cursor()
|
||||
|
||||
if self.db_config['service_types']:
|
||||
placeholders = ','.join(['%s'] * len(self.db_config['service_types']))
|
||||
sql = f"SELECT `key`, `value` FROM {self.db_config['service_table']} WHERE `type` IN ({placeholders})"
|
||||
cursor.execute(sql, self.db_config['service_types'])
|
||||
else:
|
||||
cursor.execute(f"SELECT `key`, `value` FROM {self.db_config['service_table']}")
|
||||
|
||||
for key, value in cursor.fetchall():
|
||||
env_vars[key] = value
|
||||
|
||||
cursor.close()
|
||||
except Exception as e:
|
||||
print(f"Failed to fetch DB service settings: {e}")
|
||||
|
||||
# 4. Load extra vars
|
||||
if extra_vars:
|
||||
env_vars.update(extra_vars)
|
||||
|
||||
# 5. Load overwrites
|
||||
overwrite_path = Path(overwrite_path)
|
||||
if overwrite_path.exists():
|
||||
try:
|
||||
with overwrite_path.open("r") as f:
|
||||
overwrite_data = json.load(f)
|
||||
env_vars.update(overwrite_data)
|
||||
except Exception as e:
|
||||
print(f"Failed to parse overwrites: {e}")
|
||||
|
||||
return env_vars
|
||||
|
||||
def set_timezone(self):
|
||||
"""
|
||||
Sets the system timezone based on the TZ environment variable.
|
||||
|
||||
If the TZ variable is set, writes its value to /etc/timezone.
|
||||
"""
|
||||
|
||||
timezone = os.getenv("TZ")
|
||||
if timezone:
|
||||
with open("/etc/timezone", "w") as f:
|
||||
f.write(timezone + "\n")
|
||||
|
||||
def set_syslog_redis(self):
|
||||
"""
|
||||
Reconfigures syslog-ng to use a Redis slave configuration.
|
||||
|
||||
If the REDIS_SLAVEOF_IP environment variable is set, replaces the syslog-ng config
|
||||
with the Redis slave-specific config.
|
||||
"""
|
||||
|
||||
redis_slave_ip = os.getenv("REDIS_SLAVEOF_IP")
|
||||
if redis_slave_ip:
|
||||
shutil.copy("/etc/syslog-ng/syslog-ng-redis_slave.conf", "/etc/syslog-ng/syslog-ng.conf")
|
||||
|
||||
def rsync_file(self, src, dst, recursive=False, owner=None, mode=None):
|
||||
"""
|
||||
Copies files or directories using rsync, with optional ownership and permissions.
|
||||
|
||||
Args:
|
||||
src (str or Path): Source file or directory.
|
||||
dst (str or Path): Destination directory.
|
||||
recursive (bool): If True, copies contents recursively.
|
||||
owner (tuple): Tuple of (user, group) to set ownership.
|
||||
mode (int): File mode (e.g., 0o644) to set permissions after sync.
|
||||
"""
|
||||
|
||||
src_path = Path(src)
|
||||
dst_path = Path(dst)
|
||||
dst_path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
rsync_cmd = ["rsync", "-a"]
|
||||
if recursive:
|
||||
rsync_cmd.append(str(src_path) + "/")
|
||||
else:
|
||||
rsync_cmd.append(str(src_path))
|
||||
rsync_cmd.append(str(dst_path))
|
||||
|
||||
try:
|
||||
subprocess.run(rsync_cmd, check=True)
|
||||
except Exception as e:
|
||||
print(f"Rsync failed: {e}")
|
||||
|
||||
if owner:
|
||||
self.set_owner(dst_path, *owner, recursive=True)
|
||||
if mode:
|
||||
self.set_permissions(dst_path, mode)
|
||||
|
||||
def set_permissions(self, path, mode):
|
||||
"""
|
||||
Sets file or directory permissions.
|
||||
|
||||
Args:
|
||||
path (str or Path): Path to the file or directory.
|
||||
mode (int): File mode to apply, e.g., 0o644.
|
||||
|
||||
Raises:
|
||||
FileNotFoundError: If the path does not exist.
|
||||
"""
|
||||
|
||||
file_path = Path(path)
|
||||
if not file_path.exists():
|
||||
raise FileNotFoundError(f"Cannot chmod: {file_path} does not exist")
|
||||
os.chmod(file_path, mode)
|
||||
|
||||
def set_owner(self, path, user, group=None, recursive=False):
|
||||
"""
|
||||
Changes ownership of a file or directory.
|
||||
|
||||
Args:
|
||||
path (str or Path): Path to the file or directory.
|
||||
user (str or int): Username or UID for new owner.
|
||||
group (str or int, optional): Group name or GID; defaults to user's group if not provided.
|
||||
recursive (bool): If True and path is a directory, ownership is applied recursively.
|
||||
|
||||
Raises:
|
||||
FileNotFoundError: If the path does not exist.
|
||||
"""
|
||||
|
||||
# Resolve UID
|
||||
uid = int(user) if str(user).isdigit() else pwd.getpwnam(user).pw_uid
|
||||
# Resolve GID
|
||||
if group is not None:
|
||||
gid = int(group) if str(group).isdigit() else grp.getgrnam(group).gr_gid
|
||||
else:
|
||||
gid = uid if isinstance(user, int) or str(user).isdigit() else grp.getgrnam(user).gr_gid
|
||||
|
||||
p = Path(path)
|
||||
if not p.exists():
|
||||
raise FileNotFoundError(f"{path} does not exist")
|
||||
|
||||
if recursive and p.is_dir():
|
||||
for sub_path in p.rglob("*"):
|
||||
os.chown(sub_path, uid, gid)
|
||||
os.chown(p, uid, gid)
|
||||
|
||||
def fix_permissions(self, path, user=None, group=None, mode=None, recursive=False):
|
||||
"""
|
||||
Sets owner and/or permissions on a file or directory.
|
||||
|
||||
Args:
|
||||
path (str or Path): Target path.
|
||||
user (str|int, optional): Username or UID.
|
||||
group (str|int, optional): Group name or GID.
|
||||
mode (int, optional): File mode (e.g. 0o644).
|
||||
recursive (bool): Apply recursively if path is a directory.
|
||||
"""
|
||||
|
||||
if user or group:
|
||||
self.set_owner(path, user, group, recursive)
|
||||
if mode:
|
||||
self.set_permissions(path, mode)
|
||||
|
||||
def move_file(self, src, dst, overwrite=True):
|
||||
"""
|
||||
Moves a file from src to dst, optionally overwriting existing files.
|
||||
|
||||
Args:
|
||||
src (str or Path): Source file path.
|
||||
dst (str or Path): Destination path.
|
||||
overwrite (bool): If False, raises error if dst exists.
|
||||
|
||||
Raises:
|
||||
FileNotFoundError: If the source file does not exist.
|
||||
FileExistsError: If the destination file exists and overwrite is False.
|
||||
"""
|
||||
|
||||
src_path = Path(src)
|
||||
dst_path = Path(dst)
|
||||
|
||||
if not src_path.exists():
|
||||
raise FileNotFoundError(f"Source file does not exist: {src}")
|
||||
|
||||
dst_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
if dst_path.exists() and not overwrite:
|
||||
raise FileExistsError(f"Destination already exists: {dst} (set overwrite=True to overwrite)")
|
||||
|
||||
shutil.move(str(src_path), str(dst_path))
|
||||
|
||||
def copy_file(self, src, dst, overwrite=True):
|
||||
"""
|
||||
Copies a file from src to dst using shutil.
|
||||
|
||||
Args:
|
||||
src (str or Path): Source file path.
|
||||
dst (str or Path): Destination file path.
|
||||
overwrite (bool): Whether to overwrite the destination if it exists.
|
||||
|
||||
Raises:
|
||||
FileNotFoundError: If the source file doesn't exist.
|
||||
FileExistsError: If the destination exists and overwrite is False.
|
||||
IOError: If the copy operation fails.
|
||||
"""
|
||||
|
||||
src_path = Path(src)
|
||||
dst_path = Path(dst)
|
||||
|
||||
if not src_path.is_file():
|
||||
raise FileNotFoundError(f"Source file not found: {src_path}")
|
||||
|
||||
if dst_path.exists() and not overwrite:
|
||||
raise FileExistsError(f"Destination exists: {dst_path}")
|
||||
|
||||
dst_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
shutil.copy2(src_path, dst_path)
|
||||
|
||||
def remove(self, path, recursive=False, wipe_contents=False, exclude=None):
|
||||
"""
|
||||
Removes a file or directory with optional exclusion logic.
|
||||
|
||||
Args:
|
||||
path (str or Path): The file or directory path to remove.
|
||||
recursive (bool): If True, directories will be removed recursively.
|
||||
wipe_contents (bool): If True and path is a directory, only its contents are removed, not the dir itself.
|
||||
exclude (list[str], optional): List of filenames to exclude from deletion.
|
||||
|
||||
Raises:
|
||||
FileNotFoundError: If the path does not exist.
|
||||
ValueError: If a directory is passed without recursive or wipe_contents.
|
||||
"""
|
||||
|
||||
|
||||
path = Path(path)
|
||||
exclude = set(exclude or [])
|
||||
|
||||
if not path.exists():
|
||||
raise FileNotFoundError(f"Cannot remove: {path} does not exist")
|
||||
|
||||
if wipe_contents and path.is_dir():
|
||||
for child in path.iterdir():
|
||||
if child.name in exclude:
|
||||
continue
|
||||
if child.is_dir():
|
||||
shutil.rmtree(child)
|
||||
else:
|
||||
child.unlink()
|
||||
elif path.is_file():
|
||||
if path.name not in exclude:
|
||||
path.unlink()
|
||||
elif path.is_dir():
|
||||
if recursive:
|
||||
shutil.rmtree(path)
|
||||
else:
|
||||
raise ValueError(f"{path} is a directory. Use recursive=True or wipe_contents=True to remove it.")
|
||||
|
||||
def create_dir(self, path):
|
||||
"""
|
||||
Creates a directory if it does not exist.
|
||||
|
||||
If the directory is missing, it will be created along with any necessary parent directories.
|
||||
|
||||
Args:
|
||||
path (str or Path): The directory path to create.
|
||||
"""
|
||||
|
||||
dir_path = Path(path)
|
||||
if not dir_path.exists():
|
||||
print(f"Creating directory: {dir_path}")
|
||||
dir_path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
def patch_exists(self, target_file, patch_file, reverse=False):
|
||||
"""
|
||||
Checks whether a patch can be applied (or reversed) to a target file.
|
||||
|
||||
Args:
|
||||
target_file (str): File to test the patch against.
|
||||
patch_file (str): Patch file to apply.
|
||||
reverse (bool): If True, checks whether the patch can be reversed.
|
||||
|
||||
Returns:
|
||||
bool: True if patch is applicable, False otherwise.
|
||||
"""
|
||||
|
||||
cmd = ["patch", "-sfN", "--dry-run", target_file, "<", patch_file]
|
||||
if reverse:
|
||||
cmd.insert(1, "-R")
|
||||
try:
|
||||
result = subprocess.run(
|
||||
" ".join(cmd),
|
||||
shell=True,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL
|
||||
)
|
||||
return result.returncode == 0
|
||||
except Exception as e:
|
||||
print(f"Patch dry-run failed: {e}")
|
||||
return False
|
||||
|
||||
def apply_patch(self, target_file, patch_file, reverse=False):
|
||||
"""
|
||||
Applies a patch file to a target file.
|
||||
|
||||
Args:
|
||||
target_file (str): File to be patched.
|
||||
patch_file (str): Patch file containing the diff.
|
||||
reverse (bool): If True, applies the patch in reverse (rollback).
|
||||
|
||||
Logs:
|
||||
Success or failure of the patching operation.
|
||||
"""
|
||||
|
||||
cmd = ["patch", target_file, "<", patch_file]
|
||||
if reverse:
|
||||
cmd.insert(0, "-R")
|
||||
try:
|
||||
subprocess.run(" ".join(cmd), shell=True, check=True)
|
||||
print(f"Applied patch {'(reverse)' if reverse else ''} to {target_file}")
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"Patch failed: {e}")
|
||||
|
||||
def isYes(self, value):
|
||||
"""
|
||||
Determines whether a given string represents a "yes"-like value.
|
||||
|
||||
Args:
|
||||
value (str): Input string to evaluate.
|
||||
|
||||
Returns:
|
||||
bool: True if value is "yes" or "y" (case-insensitive), otherwise False.
|
||||
"""
|
||||
return value.lower() in ["yes", "y"]
|
||||
|
||||
def is_port_open(self, host, port):
|
||||
"""
|
||||
Checks whether a TCP port is open on a given host.
|
||||
|
||||
Args:
|
||||
host (str): The hostname or IP address to check.
|
||||
port (int): The TCP port number to test.
|
||||
|
||||
Returns:
|
||||
bool: True if the port is open and accepting connections, False otherwise.
|
||||
"""
|
||||
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
||||
sock.settimeout(1)
|
||||
result = sock.connect_ex((host, port))
|
||||
return result == 0
|
||||
|
||||
def resolve_docker_dns_record(self, hostname, record_type="A"):
|
||||
"""
|
||||
Resolves DNS A or AAAA records for a given hostname.
|
||||
|
||||
Args:
|
||||
hostname (str): The domain to query.
|
||||
record_type (str): "A" for IPv4, "AAAA" for IPv6. Default is "A".
|
||||
|
||||
Returns:
|
||||
list[str]: A list of resolved IP addresses.
|
||||
|
||||
Raises:
|
||||
Exception: If resolution fails or no results are found.
|
||||
"""
|
||||
|
||||
try:
|
||||
resolver = dns.resolver.Resolver()
|
||||
resolver.nameservers = ["127.0.0.11"]
|
||||
answers = resolver.resolve(hostname, record_type)
|
||||
return [answer.to_text() for answer in answers]
|
||||
except Exception as e:
|
||||
raise Exception(f"Failed to resolve {record_type} record for {hostname}: {e}")
|
||||
|
||||
def kill_proc(self, process_name):
|
||||
"""
|
||||
Sends SIGTERM to all running processes matching the given name.
|
||||
|
||||
Args:
|
||||
process_name (str): Name of the process to terminate.
|
||||
|
||||
Returns:
|
||||
int: Number of processes successfully signaled.
|
||||
"""
|
||||
|
||||
killed = 0
|
||||
for proc in psutil.process_iter(['name']):
|
||||
try:
|
||||
if proc.info['name'] == process_name:
|
||||
proc.send_signal(signal.SIGTERM)
|
||||
killed += 1
|
||||
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
||||
continue
|
||||
return killed
|
||||
|
||||
def connect_mysql(self, socket=None):
|
||||
"""
|
||||
Establishes a connection to the MySQL database using the provided configuration.
|
||||
|
||||
Continuously retries the connection until the database is reachable. Stores
|
||||
the connection in `self.mysql_conn` once successful.
|
||||
|
||||
Logs:
|
||||
Connection status and retry errors to stdout.
|
||||
|
||||
Args:
|
||||
socket (str, optional): Custom UNIX socket path to override the default.
|
||||
"""
|
||||
|
||||
print("Connecting to MySQL...")
|
||||
config = {
|
||||
"host": self.db_config['host'],
|
||||
"user": self.db_config['user'],
|
||||
"password": self.db_config['password'],
|
||||
"database": self.db_config['database'],
|
||||
'connection_timeout': self.db_config['connection_timeout']
|
||||
}
|
||||
if self.db_config['unix_socket']:
|
||||
config["unix_socket"] = socket or self.db_config['unix_socket']
|
||||
|
||||
while True:
|
||||
try:
|
||||
self.mysql_conn = mysql.connector.connect(**config)
|
||||
if self.mysql_conn.is_connected():
|
||||
print("MySQL is up and ready!")
|
||||
break
|
||||
except mysql.connector.Error as e:
|
||||
print(f"Waiting for MySQL... ({e})")
|
||||
time.sleep(2)
|
||||
|
||||
def close_mysql(self):
|
||||
"""
|
||||
Closes the MySQL connection if it's currently open and connected.
|
||||
|
||||
Safe to call even if the connection has already been closed.
|
||||
"""
|
||||
|
||||
if self.mysql_conn and self.mysql_conn.is_connected():
|
||||
self.mysql_conn.close()
|
||||
|
||||
def connect_redis(self, max_retries=10, delay=2):
|
||||
"""
|
||||
Connects to both read and write Redis servers and stores the connections.
|
||||
|
||||
Read server: tries indefinitely until successful.
|
||||
Write server: tries up to `max_retries` before giving up.
|
||||
|
||||
Sets:
|
||||
self.redis_connr: Redis client for read
|
||||
self.redis_connw: Redis client for write
|
||||
"""
|
||||
|
||||
use_rw = self.redis_config['read_host'] == self.redis_config['write_host'] and self.redis_config['read_port'] == self.redis_config['write_port']
|
||||
|
||||
if use_rw:
|
||||
print("Connecting to Redis read server...")
|
||||
else:
|
||||
print("Connecting to Redis server...")
|
||||
|
||||
while True:
|
||||
try:
|
||||
clientr = redis.Redis(
|
||||
host=self.redis_config['read_host'],
|
||||
port=self.redis_config['read_port'],
|
||||
password=self.redis_config['password'],
|
||||
db=self.redis_config['db'],
|
||||
decode_responses=True
|
||||
)
|
||||
if clientr.ping():
|
||||
self.redis_connr = clientr
|
||||
print("Redis read server is up and ready!")
|
||||
if use_rw:
|
||||
break
|
||||
else:
|
||||
self.redis_connw = clientr
|
||||
return
|
||||
except redis.RedisError as e:
|
||||
print(f"Waiting for Redis read... ({e})")
|
||||
time.sleep(delay)
|
||||
|
||||
|
||||
print("Connecting to Redis write server...")
|
||||
for attempt in range(max_retries):
|
||||
try:
|
||||
clientw = redis.Redis(
|
||||
host=self.redis_config['write_host'],
|
||||
port=self.redis_config['write_port'],
|
||||
password=self.redis_config['password'],
|
||||
db=self.redis_config['db'],
|
||||
decode_responses=True
|
||||
)
|
||||
if clientw.ping():
|
||||
self.redis_connw = clientw
|
||||
print("Redis write server is up and ready!")
|
||||
return
|
||||
except redis.RedisError as e:
|
||||
print(f"Waiting for Redis write... (attempt {attempt + 1}/{max_retries}) ({e})")
|
||||
time.sleep(delay)
|
||||
print("Redis write server is unreachable.")
|
||||
|
||||
def close_redis(self):
|
||||
"""
|
||||
Closes the Redis read/write connections if open.
|
||||
"""
|
||||
|
||||
if self.redis_connr:
|
||||
try:
|
||||
self.redis_connr.close()
|
||||
except Exception as e:
|
||||
print(f"Error while closing Redis read connection: {e}")
|
||||
finally:
|
||||
self.redis_connr = None
|
||||
|
||||
if self.redis_connw:
|
||||
try:
|
||||
self.redis_connw.close()
|
||||
except Exception as e:
|
||||
print(f"Error while closing Redis write connection: {e}")
|
||||
finally:
|
||||
self.redis_connw = None
|
||||
|
||||
def wait_for_schema_update(self, init_file_path="init_db.inc.php", check_interval=5):
|
||||
"""
|
||||
Waits until the current database schema version matches the expected version
|
||||
defined in a PHP initialization file.
|
||||
|
||||
Compares the `version` value in the `versions` table for `application = 'db_schema'`
|
||||
with the `$db_version` value extracted from the specified PHP file.
|
||||
|
||||
Args:
|
||||
init_file_path (str): Path to the PHP file containing the expected version string.
|
||||
check_interval (int): Time in seconds to wait between version checks.
|
||||
|
||||
Logs:
|
||||
Current vs. expected schema versions until they match.
|
||||
"""
|
||||
|
||||
print("Checking database schema version...")
|
||||
|
||||
while True:
|
||||
current_version = self._get_current_db_version()
|
||||
expected_version = self._get_expected_schema_version(init_file_path)
|
||||
|
||||
if current_version == expected_version:
|
||||
print(f"DB schema is up to date: {current_version}")
|
||||
break
|
||||
|
||||
print(f"Waiting for schema update... (DB: {current_version}, Expected: {expected_version})")
|
||||
time.sleep(check_interval)
|
||||
|
||||
def wait_for_host(self, host, retry_interval=1.0, count=1):
|
||||
"""
|
||||
Waits for a host to respond to ICMP ping.
|
||||
|
||||
Args:
|
||||
host (str): Hostname or IP to ping.
|
||||
retry_interval (float): Seconds to wait between pings.
|
||||
count (int): Number of ping packets to send per check (default 1).
|
||||
"""
|
||||
while True:
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["ping", "-c", str(count), host],
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL
|
||||
)
|
||||
if result.returncode == 0:
|
||||
print(f"{host} is reachable via ping.")
|
||||
break
|
||||
except Exception:
|
||||
pass
|
||||
print(f"Waiting for {host}...")
|
||||
time.sleep(retry_interval)
|
||||
|
||||
def wait_for_dns(self, domain, retry_interval=1, timeout=30):
|
||||
"""
|
||||
Waits until the domain resolves via DNS using pure Python (socket).
|
||||
|
||||
Args:
|
||||
domain (str): The domain to resolve.
|
||||
retry_interval (int): Time (seconds) to wait between attempts.
|
||||
timeout (int): Maximum total wait time (seconds).
|
||||
|
||||
Returns:
|
||||
bool: True if resolved, False if timed out.
|
||||
"""
|
||||
|
||||
start = time.time()
|
||||
while True:
|
||||
try:
|
||||
socket.gethostbyname(domain)
|
||||
print(f"{domain} is resolving via DNS.")
|
||||
return True
|
||||
except socket.gaierror:
|
||||
pass
|
||||
|
||||
if time.time() - start > timeout:
|
||||
print(f"DNS resolution for {domain} timed out.")
|
||||
return False
|
||||
|
||||
print(f"Waiting for DNS for {domain}...")
|
||||
time.sleep(retry_interval)
|
||||
|
||||
def _get_current_db_version(self):
|
||||
"""
|
||||
Fetches the current schema version from the database.
|
||||
|
||||
Executes a SELECT query on the `versions` table where `application = 'db_schema'`.
|
||||
|
||||
Returns:
|
||||
str or None: The current schema version as a string, or None if not found or on error.
|
||||
|
||||
Logs:
|
||||
Error message if the query fails.
|
||||
"""
|
||||
|
||||
try:
|
||||
cursor = self.mysql_conn.cursor()
|
||||
cursor.execute("SELECT version FROM versions WHERE application = 'db_schema'")
|
||||
result = cursor.fetchone()
|
||||
cursor.close()
|
||||
return result[0] if result else None
|
||||
except Exception as e:
|
||||
print(f"Error fetching current DB schema version: {e}")
|
||||
return None
|
||||
|
||||
def _get_expected_schema_version(self, filepath):
|
||||
"""
|
||||
Extracts the expected database schema version from a PHP initialization file.
|
||||
|
||||
Looks for a line in the form of: `$db_version = "..."` and extracts the version string.
|
||||
|
||||
Args:
|
||||
filepath (str): Path to the PHP file containing the `$db_version` definition.
|
||||
|
||||
Returns:
|
||||
str or None: The extracted version string, or None if not found or on error.
|
||||
|
||||
Logs:
|
||||
Error message if the file cannot be read or parsed.
|
||||
"""
|
||||
|
||||
try:
|
||||
with open(filepath, "r") as f:
|
||||
content = f.read()
|
||||
match = re.search(r'\$db_version\s*=\s*"([^"]+)"', content)
|
||||
if match:
|
||||
return match.group(1)
|
||||
except Exception as e:
|
||||
print(f"Error reading expected schema version from {filepath}: {e}")
|
||||
return None
|
||||
|
||||
def rand_pass(self, length=22):
|
||||
"""
|
||||
Generates a secure random password using allowed characters.
|
||||
|
||||
Allowed characters include upper/lowercase letters, digits, underscores, and hyphens.
|
||||
|
||||
Args:
|
||||
length (int): Length of the password to generate. Default is 22.
|
||||
|
||||
Returns:
|
||||
str: A securely generated random password string.
|
||||
"""
|
||||
|
||||
allowed_chars = string.ascii_letters + string.digits + "_-"
|
||||
return ''.join(secrets.choice(allowed_chars) for _ in range(length))
|
||||
|
||||
def run_command(self, command, check=True, shell=False, input_stream=None, log_output=True):
|
||||
"""
|
||||
Executes a shell command and optionally logs output.
|
||||
|
||||
Args:
|
||||
command (str or list): Command to run.
|
||||
check (bool): Raise if non-zero exit.
|
||||
shell (bool): Run in shell.
|
||||
input_stream: stdin stream.
|
||||
log_output (bool): If True, print output.
|
||||
|
||||
Returns:
|
||||
subprocess.CompletedProcess
|
||||
"""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
command,
|
||||
shell=shell,
|
||||
check=check,
|
||||
stdin=input_stream,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True
|
||||
)
|
||||
if log_output:
|
||||
if result.stdout:
|
||||
print(result.stdout.strip())
|
||||
if result.stderr:
|
||||
print(result.stderr.strip())
|
||||
return result
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"Command failed with exit code {e.returncode}: {e.cmd}")
|
||||
print(e.stderr.strip())
|
||||
if check:
|
||||
raise
|
||||
return e
|
||||
|
||||
def sha1_filter(self, value):
|
||||
return hashlib.sha1(value.encode()).hexdigest()
|
||||
|
||||
def urlencode_filter(self, value):
|
||||
return quote(value, safe='')
|
||||
|
||||
def escape_quotes_filter(self, value):
|
||||
return value.replace('"', r'\"')
|
||||
60
data/Dockerfiles/bootstrap/modules/BootstrapClamd.py
Normal file
60
data/Dockerfiles/bootstrap/modules/BootstrapClamd.py
Normal file
@@ -0,0 +1,60 @@
|
||||
from jinja2 import Environment, FileSystemLoader
|
||||
from modules.BootstrapBase import BootstrapBase
|
||||
from pathlib import Path
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
|
||||
class BootstrapClamd(BootstrapBase):
|
||||
def bootstrap(self):
|
||||
# Skip Clamd if set
|
||||
if self.isYes(os.getenv("SKIP_CLAMD", "")):
|
||||
print("SKIP_CLAMD is set, skipping ClamAV startup...")
|
||||
time.sleep(365 * 24 * 60 * 60)
|
||||
sys.exit(1)
|
||||
|
||||
# Connect to MySQL
|
||||
self.connect_mysql()
|
||||
|
||||
print("Cleaning up tmp files...")
|
||||
tmp_files = Path("/var/lib/clamav").glob("clamav-*.tmp")
|
||||
for tmp_file in tmp_files:
|
||||
try:
|
||||
self.remove(tmp_file)
|
||||
print(f"Removed: {tmp_file}")
|
||||
except Exception as e:
|
||||
print(f"Failed to remove {tmp_file}: {e}")
|
||||
|
||||
self.create_dir("/run/clamav")
|
||||
self.create_dir("/var/lib/clamav")
|
||||
|
||||
# Setup Jinja2 Environment and load vars
|
||||
self.env = Environment(
|
||||
loader=FileSystemLoader([
|
||||
'/service_config/custom_templates',
|
||||
'/service_config/config_templates'
|
||||
]),
|
||||
keep_trailing_newline=True,
|
||||
lstrip_blocks=True,
|
||||
trim_blocks=True
|
||||
)
|
||||
extra_vars = {
|
||||
}
|
||||
self.env_vars = self.prepare_template_vars('/service_config/overwrites.json', extra_vars)
|
||||
|
||||
print("Set Timezone")
|
||||
self.set_timezone()
|
||||
|
||||
print("Render config")
|
||||
self.render_config("/service_config")
|
||||
|
||||
# Fix permissions
|
||||
self.set_owner("/var/lib/clamav", "clamav", "clamav", recursive=True)
|
||||
self.set_owner("/run/clamav", "clamav", "clamav", recursive=True)
|
||||
self.set_permissions("/var/lib/clamav", 0o755)
|
||||
for item in Path("/var/lib/clamav").glob("*"):
|
||||
self.set_permissions(item, 0o644)
|
||||
self.set_permissions("/run/clamav", 0o750)
|
||||
|
||||
# Copying to /etc/clamav to expose file as-is to administrator
|
||||
self.copy_file("/var/lib/clamav/whitelist.ign2", "/etc/clamav/whitelist.ign2")
|
||||
289
data/Dockerfiles/bootstrap/modules/BootstrapDovecot.py
Normal file
289
data/Dockerfiles/bootstrap/modules/BootstrapDovecot.py
Normal file
@@ -0,0 +1,289 @@
|
||||
from jinja2 import Environment, FileSystemLoader
|
||||
from modules.BootstrapBase import BootstrapBase
|
||||
from pathlib import Path
|
||||
import os
|
||||
import pwd
|
||||
import hashlib
|
||||
|
||||
class BootstrapDovecot(BootstrapBase):
|
||||
def bootstrap(self):
|
||||
# Connect to MySQL
|
||||
self.connect_mysql()
|
||||
self.wait_for_schema_update()
|
||||
|
||||
# Connect to Redis
|
||||
self.connect_redis()
|
||||
if self.redis_connw:
|
||||
self.redis_connw.set("DOVECOT_REPL_HEALTH", 1)
|
||||
|
||||
# Wait for DNS
|
||||
self.wait_for_dns("mailcow.email")
|
||||
|
||||
# Create missing directories
|
||||
self.create_dir("/etc/dovecot/sql/")
|
||||
self.create_dir("/etc/dovecot/auth/")
|
||||
self.create_dir("/var/vmail/_garbage")
|
||||
self.create_dir("/var/vmail/sieve")
|
||||
self.create_dir("/etc/sogo")
|
||||
self.create_dir("/var/volatile")
|
||||
|
||||
# Setup Jinja2 Environment and load vars
|
||||
self.env = Environment(
|
||||
loader=FileSystemLoader([
|
||||
'/service_config/custom_templates',
|
||||
'/service_config/config_templates'
|
||||
]),
|
||||
keep_trailing_newline=True,
|
||||
lstrip_blocks=True,
|
||||
trim_blocks=True
|
||||
)
|
||||
extra_vars = {
|
||||
"VALID_CERT_DIRS": self.get_valid_cert_dirs(),
|
||||
"RAND_USER": self.rand_pass(),
|
||||
"RAND_PASS": self.rand_pass(),
|
||||
"RAND_PASS2": self.rand_pass(),
|
||||
"ENV_VARS": dict(os.environ)
|
||||
}
|
||||
self.env_vars = self.prepare_template_vars('/service_config/overwrites.json', extra_vars)
|
||||
|
||||
print("Set Timezone")
|
||||
self.set_timezone()
|
||||
|
||||
print("Render config")
|
||||
self.render_config("/service_config")
|
||||
|
||||
files = [
|
||||
"/etc/dovecot/mail_plugins",
|
||||
"/etc/dovecot/mail_plugins_imap",
|
||||
"/etc/dovecot/mail_plugins_lmtp",
|
||||
"/templates/quarantine.tpl"
|
||||
]
|
||||
for file in files:
|
||||
self.set_permissions(file, 0o644)
|
||||
|
||||
try:
|
||||
# Migrate old sieve_after file
|
||||
self.move_file("/etc/dovecot/sieve_after", "/var/vmail/sieve/global_sieve_after.sieve")
|
||||
except Exception as e:
|
||||
pass
|
||||
try:
|
||||
# Cleanup random user maildirs
|
||||
self.remove("/var/vmail/mailcow.local", wipe_contents=True)
|
||||
except Exception as e:
|
||||
pass
|
||||
try:
|
||||
# Cleanup PIDs
|
||||
self.remove("/tmp/quarantine_notify.pid")
|
||||
except Exception as e:
|
||||
pass
|
||||
try:
|
||||
self.remove("/var/run/dovecot/master.pid")
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
# Check permissions of vmail/index/garbage directories.
|
||||
# Do not do this every start-up, it may take a very long time. So we use a stat check here.
|
||||
files = [
|
||||
"/var/vmail",
|
||||
"/var/vmail/_garbage",
|
||||
"/var/vmail_index"
|
||||
]
|
||||
for file in files:
|
||||
path = Path(file)
|
||||
try:
|
||||
stat_info = path.stat()
|
||||
current_user = pwd.getpwuid(stat_info.st_uid).pw_name
|
||||
|
||||
if current_user != "vmail":
|
||||
print(f"Ownership of {path} is {current_user}, fixing to vmail:vmail...")
|
||||
self.set_owner(path, user="vmail", group="vmail", recursive=True)
|
||||
else:
|
||||
print(f"Ownership of {path} is already correct (vmail)")
|
||||
except Exception as e:
|
||||
print(f"Error checking ownership of {path}: {e}")
|
||||
|
||||
# Compile sieve scripts
|
||||
files = [
|
||||
"/var/vmail/sieve/global_sieve_before.sieve",
|
||||
"/var/vmail/sieve/global_sieve_after.sieve",
|
||||
"/usr/lib/dovecot/sieve/report-spam.sieve",
|
||||
"/usr/lib/dovecot/sieve/report-ham.sieve",
|
||||
]
|
||||
for file in files:
|
||||
self.run_command(["sievec", file], check=False)
|
||||
|
||||
# Fix permissions
|
||||
for path in Path("/etc/dovecot/sql").glob("*.conf"):
|
||||
self.set_owner(path, "root", "root")
|
||||
self.set_permissions(path, 0o640)
|
||||
|
||||
files = [
|
||||
"/etc/dovecot/auth/passwd-verify.lua",
|
||||
*Path("/etc/dovecot/sql").glob("dovecot-dict-sql-sieve*"),
|
||||
*Path("/etc/dovecot/sql").glob("dovecot-dict-sql-quota*")
|
||||
]
|
||||
for file in files:
|
||||
self.set_owner(file, "root", "dovecot")
|
||||
|
||||
self.set_permissions("/etc/dovecot/auth/passwd-verify.lua", 0o640)
|
||||
|
||||
for file in ["/var/vmail/sieve", "/var/volatile", "/var/vmail_index"]:
|
||||
self.set_owner(file, "vmail", "vmail", recursive=True)
|
||||
|
||||
self.run_command(["adduser", "vmail", "tty"])
|
||||
self.run_command(["chmod", "g+rw", "/dev/console"])
|
||||
self.set_owner("/dev/console", "root", "tty")
|
||||
files = [
|
||||
"/usr/lib/dovecot/sieve/rspamd-pipe-ham",
|
||||
"/usr/lib/dovecot/sieve/rspamd-pipe-spam",
|
||||
"/usr/local/bin/imapsync_runner.pl",
|
||||
"/usr/local/bin/imapsync",
|
||||
"/usr/local/bin/trim_logs.sh",
|
||||
"/usr/local/bin/sa-rules.sh",
|
||||
"/usr/local/bin/clean_q_aged.sh",
|
||||
"/usr/local/bin/maildir_gc.sh",
|
||||
"/usr/local/sbin/stop-supervisor.sh",
|
||||
"/usr/local/bin/quota_notify.py",
|
||||
"/usr/local/bin/repl_health.sh",
|
||||
"/usr/local/bin/optimize-fts.sh"
|
||||
]
|
||||
for file in files:
|
||||
self.set_permissions(file, 0o755)
|
||||
|
||||
# Collect SA rules once now
|
||||
self.run_command(["/usr/local/bin/sa-rules.sh"], check=False)
|
||||
|
||||
self.generate_mail_crypt_keys()
|
||||
self.cleanup_imapsync_jobs()
|
||||
self.generate_guid_version()
|
||||
|
||||
def get_valid_cert_dirs(self):
|
||||
"""
|
||||
Returns a mapping of domains to their certificate directory path.
|
||||
|
||||
Example:
|
||||
{
|
||||
"example.com": "/etc/ssl/mail/example.com/",
|
||||
"www.example.com": "/etc/ssl/mail/example.com/"
|
||||
}
|
||||
"""
|
||||
sni_map = {}
|
||||
base_path = Path("/etc/ssl/mail")
|
||||
if not base_path.exists():
|
||||
return sni_map
|
||||
|
||||
for cert_dir in base_path.iterdir():
|
||||
if not cert_dir.is_dir():
|
||||
continue
|
||||
|
||||
domains_file = cert_dir / "domains"
|
||||
cert_file = cert_dir / "cert.pem"
|
||||
key_file = cert_dir / "key.pem"
|
||||
|
||||
if not (domains_file.exists() and cert_file.exists() and key_file.exists()):
|
||||
continue
|
||||
|
||||
with open(domains_file, "r") as f:
|
||||
domains = [line.strip() for line in f if line.strip()]
|
||||
for domain in domains:
|
||||
sni_map[domain] = str(cert_dir)
|
||||
|
||||
return sni_map
|
||||
|
||||
def generate_mail_crypt_keys(self):
|
||||
"""
|
||||
Ensures mail_crypt EC keypair exists. Generates if missing. Adjusts permissions.
|
||||
"""
|
||||
|
||||
key_dir = Path("/mail_crypt")
|
||||
priv_key = key_dir / "ecprivkey.pem"
|
||||
pub_key = key_dir / "ecpubkey.pem"
|
||||
|
||||
# Generate keys if they don't exist or are empty
|
||||
if not priv_key.exists() or priv_key.stat().st_size == 0 or \
|
||||
not pub_key.exists() or pub_key.stat().st_size == 0:
|
||||
self.run_command(
|
||||
"openssl ecparam -name prime256v1 -genkey | openssl pkey -out /mail_crypt/ecprivkey.pem",
|
||||
shell=True
|
||||
)
|
||||
self.run_command(
|
||||
"openssl pkey -in /mail_crypt/ecprivkey.pem -pubout -out /mail_crypt/ecpubkey.pem",
|
||||
shell=True
|
||||
)
|
||||
|
||||
# Set ownership to UID 401 (dovecot)
|
||||
self.set_owner(priv_key, user='401')
|
||||
self.set_owner(pub_key, user='401')
|
||||
|
||||
def cleanup_imapsync_jobs(self):
|
||||
"""
|
||||
Cleans up stale imapsync locks and resets running status in the database.
|
||||
|
||||
Deletes the imapsync_busy.lock file if present and sets `is_running` to 0
|
||||
in the `imapsync` table, if it exists.
|
||||
|
||||
Logs:
|
||||
Any issues with file operations or SQL execution.
|
||||
"""
|
||||
|
||||
lock_file = Path("/tmp/imapsync_busy.lock")
|
||||
if lock_file.exists():
|
||||
try:
|
||||
lock_file.unlink()
|
||||
except Exception as e:
|
||||
print(f"Failed to remove lock file: {e}")
|
||||
|
||||
try:
|
||||
cursor = self.mysql_conn.cursor()
|
||||
cursor.execute("SHOW TABLES LIKE 'imapsync'")
|
||||
result = cursor.fetchone()
|
||||
if result:
|
||||
cursor.execute("UPDATE imapsync SET is_running='0'")
|
||||
self.mysql_conn.commit()
|
||||
cursor.close()
|
||||
except Exception as e:
|
||||
print(f"Error updating imapsync table: {e}")
|
||||
|
||||
def generate_guid_version(self):
|
||||
"""
|
||||
Waits for the `versions` table to be created, then generates a GUID
|
||||
based on the mail hostname and Dovecot's public key and inserts it
|
||||
into the `versions` table.
|
||||
|
||||
If the key or hash is missing or malformed, marks it as INVALID.
|
||||
"""
|
||||
|
||||
try:
|
||||
result = self.run_command(["doveconf", "-P"], check=True, log_output=False)
|
||||
pubkey_path = None
|
||||
for line in result.stdout.splitlines():
|
||||
if "mail_crypt_global_public_key" in line:
|
||||
parts = line.split('<')
|
||||
if len(parts) > 1:
|
||||
pubkey_path = parts[1].strip()
|
||||
break
|
||||
|
||||
if pubkey_path and Path(pubkey_path).exists():
|
||||
with open(pubkey_path, "rb") as key_file:
|
||||
pubkey_data = key_file.read()
|
||||
|
||||
hostname = self.env_vars.get("MAILCOW_HOSTNAME", "mailcow.local").encode("utf-8")
|
||||
concat = hostname + pubkey_data
|
||||
guid = hashlib.sha256(concat).hexdigest()
|
||||
|
||||
if len(guid) == 64:
|
||||
version_value = guid
|
||||
else:
|
||||
version_value = "INVALID"
|
||||
|
||||
cursor = self.mysql_conn.cursor()
|
||||
cursor.execute(
|
||||
"REPLACE INTO versions (application, version) VALUES (%s, %s)",
|
||||
("GUID", version_value)
|
||||
)
|
||||
self.mysql_conn.commit()
|
||||
cursor.close()
|
||||
else:
|
||||
print("Public key not found or unreadable. GUID not generated.")
|
||||
except Exception as e:
|
||||
print(f"Failed to generate or store GUID: {e}")
|
||||
163
data/Dockerfiles/bootstrap/modules/BootstrapMysql.py
Normal file
163
data/Dockerfiles/bootstrap/modules/BootstrapMysql.py
Normal file
@@ -0,0 +1,163 @@
|
||||
from jinja2 import Environment, FileSystemLoader
|
||||
from modules.BootstrapBase import BootstrapBase
|
||||
import os
|
||||
import time
|
||||
import subprocess
|
||||
|
||||
class BootstrapMysql(BootstrapBase):
|
||||
def bootstrap(self):
|
||||
dbuser = "root"
|
||||
dbpass = os.getenv("MYSQL_ROOT_PASSWORD", "")
|
||||
socket = "/tmp/mysql-temp.sock"
|
||||
|
||||
# Check if mysql has been initialized
|
||||
if os.path.exists("/var/lib/mysql/mysql/db.frm"):
|
||||
print("Starting temporary mysqld for upgrade...")
|
||||
self.start_temporary(socket)
|
||||
|
||||
self.connect_mysql(socket)
|
||||
|
||||
print("Running mysql_upgrade...")
|
||||
self.upgrade_mysql(dbuser, dbpass, socket)
|
||||
print("Checking timezone support with CONVERT_TZ...")
|
||||
self.check_and_import_timezone_support(dbuser, dbpass, socket)
|
||||
|
||||
print("Shutting down temporary mysqld...")
|
||||
self.close_mysql()
|
||||
self.stop_temporary(dbuser, dbpass, socket)
|
||||
|
||||
|
||||
# Setup Jinja2 Environment and load vars
|
||||
self.env = Environment(
|
||||
loader=FileSystemLoader([
|
||||
'/service_config/custom_templates',
|
||||
'/service_config/config_templates'
|
||||
]),
|
||||
keep_trailing_newline=True,
|
||||
lstrip_blocks=True,
|
||||
trim_blocks=True
|
||||
)
|
||||
extra_vars = {
|
||||
}
|
||||
self.env_vars = self.prepare_template_vars('/service_config/overwrites.json', extra_vars)
|
||||
|
||||
print("Set Timezone")
|
||||
self.set_timezone()
|
||||
|
||||
print("Render config")
|
||||
self.render_config("/service_config")
|
||||
|
||||
def start_temporary(self, socket):
|
||||
"""
|
||||
Starts a temporary mysqld process in the background using the given UNIX socket.
|
||||
|
||||
The server is started with networking disabled (--skip-networking).
|
||||
|
||||
Args:
|
||||
socket (str): Path to the UNIX socket file for MySQL to listen on.
|
||||
|
||||
Returns:
|
||||
subprocess.Popen: The running mysqld process object.
|
||||
"""
|
||||
|
||||
return subprocess.Popen([
|
||||
"mysqld",
|
||||
"--user=mysql",
|
||||
"--skip-networking",
|
||||
f"--socket={socket}"
|
||||
])
|
||||
|
||||
def stop_temporary(self, dbuser, dbpass, socket):
|
||||
"""
|
||||
Shuts down the temporary mysqld instance gracefully.
|
||||
|
||||
Uses mariadb-admin to issue a shutdown command to the running server.
|
||||
|
||||
Args:
|
||||
dbuser (str): The MySQL username with shutdown privileges (typically 'root').
|
||||
dbpass (str): The password for the MySQL user.
|
||||
socket (str): Path to the UNIX socket the server is listening on.
|
||||
"""
|
||||
|
||||
self.run_command([
|
||||
"mariadb-admin",
|
||||
"shutdown",
|
||||
f"--socket={socket}",
|
||||
"-u", dbuser,
|
||||
f"-p{dbpass}"
|
||||
])
|
||||
|
||||
def upgrade_mysql(self, dbuser, dbpass, socket, max_retries=5, wait_interval=3):
|
||||
"""
|
||||
Executes mysql_upgrade to check and fix any schema or table incompatibilities.
|
||||
|
||||
Retries the upgrade command if it fails, up to a maximum number of attempts.
|
||||
|
||||
Args:
|
||||
dbuser (str): MySQL username with privilege to perform the upgrade.
|
||||
dbpass (str): Password for the MySQL user.
|
||||
socket (str): Path to the MySQL UNIX socket for local communication.
|
||||
max_retries (int): Maximum number of attempts before giving up. Default is 5.
|
||||
wait_interval (int): Number of seconds to wait between retries. Default is 3.
|
||||
|
||||
Returns:
|
||||
bool: True if upgrade succeeded, False if all attempts failed.
|
||||
"""
|
||||
|
||||
retries = 0
|
||||
while retries < max_retries:
|
||||
result = self.run_command([
|
||||
"mysql_upgrade",
|
||||
"-u", dbuser,
|
||||
f"-p{dbpass}",
|
||||
f"--socket={socket}"
|
||||
], check=False)
|
||||
|
||||
if result.returncode == 0:
|
||||
print("mysql_upgrade completed successfully.")
|
||||
break
|
||||
else:
|
||||
print(f"mysql_upgrade failed (try {retries+1}/{max_retries})")
|
||||
retries += 1
|
||||
time.sleep(wait_interval)
|
||||
else:
|
||||
print("mysql_upgrade failed after all retries.")
|
||||
return False
|
||||
|
||||
def check_and_import_timezone_support(self, dbuser, dbpass, socket):
|
||||
"""
|
||||
Checks if MySQL supports timezone conversion (CONVERT_TZ).
|
||||
If not, it imports timezone info using mysql_tzinfo_to_sql piped into mariadb.
|
||||
"""
|
||||
|
||||
try:
|
||||
cursor = self.mysql_conn.cursor()
|
||||
cursor.execute("SELECT CONVERT_TZ('2019-11-02 23:33:00','Europe/Berlin','UTC')")
|
||||
result = cursor.fetchone()
|
||||
cursor.close()
|
||||
|
||||
if not result or result[0] is None:
|
||||
print("Timezone conversion failed or returned NULL. Importing timezone info...")
|
||||
|
||||
# Use mysql_tzinfo_to_sql piped into mariadb
|
||||
tz_dump = subprocess.Popen(
|
||||
["mysql_tzinfo_to_sql", "/usr/share/zoneinfo"],
|
||||
stdout=subprocess.PIPE
|
||||
)
|
||||
|
||||
self.run_command([
|
||||
"mariadb",
|
||||
"--socket", socket,
|
||||
"-u", dbuser,
|
||||
f"-p{dbpass}",
|
||||
"mysql"
|
||||
], input_stream=tz_dump.stdout)
|
||||
|
||||
tz_dump.stdout.close()
|
||||
tz_dump.wait()
|
||||
|
||||
print("Timezone info successfully imported.")
|
||||
else:
|
||||
print(f"Timezone support is working. Sample result: {result[0]}")
|
||||
except Exception as e:
|
||||
print(f"Failed to verify or import timezone info: {e}")
|
||||
65
data/Dockerfiles/bootstrap/modules/BootstrapNginx.py
Normal file
65
data/Dockerfiles/bootstrap/modules/BootstrapNginx.py
Normal file
@@ -0,0 +1,65 @@
|
||||
from jinja2 import Environment, FileSystemLoader
|
||||
from modules.BootstrapBase import BootstrapBase
|
||||
import os
|
||||
|
||||
class BootstrapNginx(BootstrapBase):
|
||||
def bootstrap(self):
|
||||
# Connect to MySQL
|
||||
self.connect_mysql()
|
||||
|
||||
# wait for Hosts
|
||||
php_service = os.getenv("PHPFPM_HOST") or "php-fpm-mailcow"
|
||||
rspamd_service = os.getenv("RSPAMD_HOST") or "rspamd-mailcow"
|
||||
sogo_service = os.getenv("SOGO_HOST")
|
||||
self.wait_for_host(php_service)
|
||||
if not self.isYes(os.getenv("SKIP_RSPAMD", False)):
|
||||
self.wait_for_host(rspamd_service)
|
||||
if not self.isYes(os.getenv("SKIP_SOGO", False)):
|
||||
self.wait_for_host(sogo_service)
|
||||
|
||||
# Setup Jinja2 Environment and load vars
|
||||
self.env = Environment(
|
||||
loader=FileSystemLoader([
|
||||
'/service_config/custom_templates',
|
||||
'/service_config/config_templates'
|
||||
]),
|
||||
keep_trailing_newline=True,
|
||||
lstrip_blocks=True,
|
||||
trim_blocks=True
|
||||
)
|
||||
extra_vars = {
|
||||
"VALID_CERT_DIRS": self.get_valid_cert_dirs(),
|
||||
'TRUSTED_PROXIES': [item.strip() for item in os.getenv("TRUSTED_PROXIES", "").split(",") if item.strip()],
|
||||
'ADDITIONAL_SERVER_NAMES': [item.strip() for item in os.getenv("ADDITIONAL_SERVER_NAMES", "").split(",") if item.strip()],
|
||||
}
|
||||
self.env_vars = self.prepare_template_vars('/service_config/overwrites.json', extra_vars)
|
||||
|
||||
print("Set Timezone")
|
||||
self.set_timezone()
|
||||
|
||||
print("Render config")
|
||||
self.render_config("/service_config")
|
||||
|
||||
def get_valid_cert_dirs(self):
|
||||
ssl_dir = '/etc/ssl/mail/'
|
||||
valid_cert_dirs = []
|
||||
for d in os.listdir(ssl_dir):
|
||||
full_path = os.path.join(ssl_dir, d)
|
||||
if not os.path.isdir(full_path):
|
||||
continue
|
||||
|
||||
cert_path = os.path.join(full_path, 'cert.pem')
|
||||
key_path = os.path.join(full_path, 'key.pem')
|
||||
domains_path = os.path.join(full_path, 'domains')
|
||||
|
||||
if os.path.isfile(cert_path) and os.path.isfile(key_path) and os.path.isfile(domains_path):
|
||||
with open(domains_path, 'r') as file:
|
||||
domains = file.read().strip()
|
||||
domains_list = domains.split()
|
||||
if domains_list and os.getenv("MAILCOW_HOSTNAME", "") not in domains_list:
|
||||
valid_cert_dirs.append({
|
||||
'cert_path': full_path + '/',
|
||||
'domains': domains
|
||||
})
|
||||
|
||||
return valid_cert_dirs
|
||||
202
data/Dockerfiles/bootstrap/modules/BootstrapPhpfpm.py
Normal file
202
data/Dockerfiles/bootstrap/modules/BootstrapPhpfpm.py
Normal file
@@ -0,0 +1,202 @@
|
||||
from jinja2 import Environment, FileSystemLoader
|
||||
from modules.BootstrapBase import BootstrapBase
|
||||
import os
|
||||
import ipaddress
|
||||
|
||||
class BootstrapPhpfpm(BootstrapBase):
|
||||
def bootstrap(self):
|
||||
self.connect_mysql()
|
||||
self.connect_redis()
|
||||
|
||||
# Setup Jinja2 Environment and load vars
|
||||
self.env = Environment(
|
||||
loader=FileSystemLoader([
|
||||
'/service_config/custom_templates',
|
||||
'/service_config/config_templates'
|
||||
]),
|
||||
keep_trailing_newline=True,
|
||||
lstrip_blocks=True,
|
||||
trim_blocks=True
|
||||
)
|
||||
extra_vars = {
|
||||
}
|
||||
self.env_vars = self.prepare_template_vars('/service_config/overwrites.json', extra_vars)
|
||||
|
||||
print("Set Timezone")
|
||||
self.set_timezone()
|
||||
|
||||
# Prepare Redis and MySQL Database
|
||||
# TODO: move to dockerapi
|
||||
if self.isYes(os.getenv("MASTER", "")):
|
||||
print("We are master, preparing...")
|
||||
self.prepare_redis()
|
||||
self.setup_apikeys(
|
||||
os.getenv("API_ALLOW_FROM", "").strip(),
|
||||
os.getenv("API_KEY", "").strip(),
|
||||
os.getenv("API_KEY_READ_ONLY", "").strip()
|
||||
)
|
||||
self.setup_mysql_events()
|
||||
|
||||
|
||||
print("Render config")
|
||||
self.render_config("/service_config")
|
||||
|
||||
self.copy_file("/usr/local/etc/php/conf.d/opcache-recommended.ini", "/php-conf/opcache-recommended.ini")
|
||||
self.copy_file("/usr/local/etc/php-fpm.d/z-pools.conf", "/php-conf/pools.conf")
|
||||
self.copy_file("/usr/local/etc/php/conf.d/zzz-other.ini", "/php-conf/other.ini")
|
||||
self.copy_file("/usr/local/etc/php/conf.d/upload.ini", "/php-conf/upload.ini")
|
||||
self.copy_file("/usr/local/etc/php/conf.d/session_store.ini", "/php-conf/session_store.ini")
|
||||
|
||||
self.set_owner("/global_sieve", 82, 82, recursive=True)
|
||||
self.set_owner("/web/templates/cache", 82, 82, recursive=True)
|
||||
self.remove("/web/templates/cache", wipe_contents=True, exclude=[".gitkeep"])
|
||||
|
||||
print("Running DB init...")
|
||||
self.run_command(["php", "-c", "/usr/local/etc/php", "-f", "/web/inc/init_db.inc.php"], check=False)
|
||||
|
||||
def prepare_redis(self):
|
||||
print("Setting default Redis keys if missing...")
|
||||
|
||||
# Q_RELEASE_FORMAT
|
||||
if self.redis_connw and self.redis_connr.get("Q_RELEASE_FORMAT") is None:
|
||||
self.redis_connw.set("Q_RELEASE_FORMAT", "raw")
|
||||
|
||||
# Q_MAX_AGE
|
||||
if self.redis_connw and self.redis_connr.get("Q_MAX_AGE") is None:
|
||||
self.redis_connw.set("Q_MAX_AGE", 365)
|
||||
|
||||
# PASSWD_POLICY hash defaults
|
||||
if self.redis_connw and self.redis_connr.hget("PASSWD_POLICY", "length") is None:
|
||||
self.redis_connw.hset("PASSWD_POLICY", mapping={
|
||||
"length": 6,
|
||||
"chars": 0,
|
||||
"special_chars": 0,
|
||||
"lowerupper": 0,
|
||||
"numbers": 0
|
||||
})
|
||||
|
||||
# DOMAIN_MAP
|
||||
print("Rebuilding DOMAIN_MAP from MySQL...")
|
||||
if self.redis_connw:
|
||||
self.redis_connw.delete("DOMAIN_MAP")
|
||||
domains = set()
|
||||
try:
|
||||
cursor = self.mysql_conn.cursor()
|
||||
|
||||
cursor.execute("SELECT domain FROM domain")
|
||||
domains.update(row[0] for row in cursor.fetchall())
|
||||
cursor.execute("SELECT alias_domain FROM alias_domain")
|
||||
domains.update(row[0] for row in cursor.fetchall())
|
||||
|
||||
cursor.close()
|
||||
|
||||
if domains:
|
||||
for domain in domains:
|
||||
if self.redis_connw:
|
||||
self.redis_conn.hset("DOMAIN_MAP", domain, 1)
|
||||
print(f"{len(domains)} domains added to DOMAIN_MAP.")
|
||||
else:
|
||||
print("No domains found to insert into DOMAIN_MAP.")
|
||||
except Exception as e:
|
||||
print(f"Failed to rebuild DOMAIN_MAP: {e}")
|
||||
|
||||
def setup_apikeys(self, api_allow_from, api_key_rw, api_key_ro):
|
||||
if not api_allow_from or api_allow_from == "invalid":
|
||||
return
|
||||
|
||||
print("Validating API_ALLOW_FROM IPs...")
|
||||
ip_list = [ip.strip() for ip in api_allow_from.split(",")]
|
||||
validated_ips = []
|
||||
|
||||
for ip in ip_list:
|
||||
try:
|
||||
ipaddress.ip_network(ip, strict=False)
|
||||
validated_ips.append(ip)
|
||||
except ValueError:
|
||||
continue
|
||||
if not validated_ips:
|
||||
print("No valid IPs found in API_ALLOW_FROM")
|
||||
return
|
||||
|
||||
allow_from_str = ",".join(validated_ips)
|
||||
cursor = self.mysql_conn.cursor()
|
||||
try:
|
||||
if api_key_rw and api_key_rw != "invalid":
|
||||
print("Setting RW API key...")
|
||||
cursor.execute("DELETE FROM api WHERE access = 'rw'")
|
||||
cursor.execute(
|
||||
"INSERT INTO api (api_key, active, allow_from, access) VALUES (%s, %s, %s, %s)",
|
||||
(api_key_rw, 1, allow_from_str, "rw")
|
||||
)
|
||||
|
||||
if api_key_ro and api_key_ro != "invalid":
|
||||
print("Setting RO API key...")
|
||||
cursor.execute("DELETE FROM api WHERE access = 'ro'")
|
||||
cursor.execute(
|
||||
"INSERT INTO api (api_key, active, allow_from, access) VALUES (%s, %s, %s, %s)",
|
||||
(api_key_ro, 1, allow_from_str, "ro")
|
||||
)
|
||||
|
||||
self.mysql_conn.commit()
|
||||
print("API key(s) set successfully.")
|
||||
except Exception as e:
|
||||
print(f"Failed to configure API keys: {e}")
|
||||
self.mysql_conn.rollback()
|
||||
finally:
|
||||
cursor.close()
|
||||
|
||||
def setup_mysql_events(self):
|
||||
print("Creating scheduled MySQL EVENTS...")
|
||||
|
||||
queries = [
|
||||
"DROP EVENT IF EXISTS clean_spamalias;",
|
||||
"""
|
||||
CREATE EVENT clean_spamalias
|
||||
ON SCHEDULE EVERY 1 DAY
|
||||
DO
|
||||
DELETE FROM spamalias WHERE validity < UNIX_TIMESTAMP();
|
||||
""",
|
||||
"DROP EVENT IF EXISTS clean_oauth2;",
|
||||
"""
|
||||
CREATE EVENT clean_oauth2
|
||||
ON SCHEDULE EVERY 1 DAY
|
||||
DO
|
||||
BEGIN
|
||||
DELETE FROM oauth_refresh_tokens WHERE expires < NOW();
|
||||
DELETE FROM oauth_access_tokens WHERE expires < NOW();
|
||||
DELETE FROM oauth_authorization_codes WHERE expires < NOW();
|
||||
END;
|
||||
""",
|
||||
"DROP EVENT IF EXISTS clean_sasl_log;",
|
||||
"""
|
||||
CREATE EVENT clean_sasl_log
|
||||
ON SCHEDULE EVERY 1 DAY
|
||||
DO
|
||||
BEGIN
|
||||
DELETE sasl_log.* FROM sasl_log
|
||||
LEFT JOIN (
|
||||
SELECT username, service, MAX(datetime) AS lastdate
|
||||
FROM sasl_log
|
||||
GROUP BY username, service
|
||||
) AS last
|
||||
ON sasl_log.username = last.username AND sasl_log.service = last.service
|
||||
WHERE datetime < DATE_SUB(NOW(), INTERVAL 31 DAY)
|
||||
AND datetime < lastdate;
|
||||
|
||||
DELETE FROM sasl_log
|
||||
WHERE username NOT IN (SELECT username FROM mailbox)
|
||||
AND datetime < DATE_SUB(NOW(), INTERVAL 31 DAY);
|
||||
END;
|
||||
"""
|
||||
]
|
||||
|
||||
try:
|
||||
cursor = self.mysql_conn.cursor()
|
||||
for query in queries:
|
||||
cursor.execute(query)
|
||||
self.mysql_conn.commit()
|
||||
cursor.close()
|
||||
print("MySQL EVENTS created successfully.")
|
||||
except Exception as e:
|
||||
print(f"Failed to create MySQL EVENTS: {e}")
|
||||
self.mysql_conn.rollback()
|
||||
83
data/Dockerfiles/bootstrap/modules/BootstrapPostfix.py
Normal file
83
data/Dockerfiles/bootstrap/modules/BootstrapPostfix.py
Normal file
@@ -0,0 +1,83 @@
|
||||
from jinja2 import Environment, FileSystemLoader
|
||||
from modules.BootstrapBase import BootstrapBase
|
||||
from pathlib import Path
|
||||
|
||||
class BootstrapPostfix(BootstrapBase):
|
||||
def bootstrap(self):
|
||||
# Connect to MySQL
|
||||
self.connect_mysql()
|
||||
|
||||
# Wait for DNS
|
||||
self.wait_for_dns("mailcow.email")
|
||||
|
||||
self.create_dir("/opt/postfix/conf/sql/")
|
||||
|
||||
# Setup Jinja2 Environment and load vars
|
||||
self.env = Environment(
|
||||
loader=FileSystemLoader([
|
||||
'/service_config/custom_templates',
|
||||
'/service_config/config_templates'
|
||||
]),
|
||||
keep_trailing_newline=True,
|
||||
lstrip_blocks=True,
|
||||
trim_blocks=True
|
||||
)
|
||||
extra_vars = {
|
||||
"VALID_CERT_DIRS": self.get_valid_cert_dirs()
|
||||
}
|
||||
self.env_vars = self.prepare_template_vars('/service_config/overwrites.json', extra_vars)
|
||||
|
||||
print("Set Timezone")
|
||||
self.set_timezone()
|
||||
|
||||
print("Set Syslog redis")
|
||||
self.set_syslog_redis()
|
||||
|
||||
print("Render config")
|
||||
self.render_config("/service_config")
|
||||
|
||||
# Create aliases DB
|
||||
self.run_command(["newaliases"])
|
||||
|
||||
# Create SNI Config
|
||||
self.run_command(["postmap", "-F", "hash:/opt/postfix/conf/sni.map"])
|
||||
|
||||
# Fix Postfix permissions
|
||||
self.set_owner("/opt/postfix/conf/sql", user="root", group="postfix", recursive=True)
|
||||
self.set_owner("/opt/postfix/conf/custom_transport.pcre", user="root", group="postfix")
|
||||
for cf_file in Path("/opt/postfix/conf/sql").glob("*.cf"):
|
||||
self.set_permissions(cf_file, 0o640)
|
||||
self.set_permissions("/opt/postfix/conf/custom_transport.pcre", 0o640)
|
||||
self.set_owner("/var/spool/postfix/public", user="root", group="postdrop", recursive=True)
|
||||
self.set_owner("/var/spool/postfix/maildrop", user="root", group="postdrop", recursive=True)
|
||||
self.run_command(["postfix", "set-permissions"], check=False)
|
||||
|
||||
# Checking if there is a leftover of a crashed postfix container before starting a new one
|
||||
pid_file = Path("/var/spool/postfix/pid/master.pid")
|
||||
if pid_file.exists():
|
||||
print(f"Removing stale Postfix PID file: {pid_file}")
|
||||
pid_file.unlink()
|
||||
|
||||
def get_valid_cert_dirs(self):
|
||||
certs = {}
|
||||
base_path = Path("/etc/ssl/mail")
|
||||
if not base_path.exists():
|
||||
return certs
|
||||
|
||||
for cert_dir in base_path.iterdir():
|
||||
if not cert_dir.is_dir():
|
||||
continue
|
||||
|
||||
domains_file = cert_dir / "domains"
|
||||
cert_file = cert_dir / "cert.pem"
|
||||
key_file = cert_dir / "key.pem"
|
||||
|
||||
if not (domains_file.exists() and cert_file.exists() and key_file.exists()):
|
||||
continue
|
||||
|
||||
with open(domains_file, "r") as f:
|
||||
domains = [line.strip() for line in f if line.strip()]
|
||||
if domains:
|
||||
certs[str(cert_dir)] = domains
|
||||
|
||||
return certs
|
||||
132
data/Dockerfiles/bootstrap/modules/BootstrapRspamd.py
Normal file
132
data/Dockerfiles/bootstrap/modules/BootstrapRspamd.py
Normal file
@@ -0,0 +1,132 @@
|
||||
from jinja2 import Environment, FileSystemLoader
|
||||
from modules.BootstrapBase import BootstrapBase
|
||||
from pathlib import Path
|
||||
import time
|
||||
import platform
|
||||
|
||||
class BootstrapRspamd(BootstrapBase):
|
||||
def bootstrap(self):
|
||||
# Connect to MySQL
|
||||
self.connect_mysql()
|
||||
|
||||
# Connect to MySQL
|
||||
self.connect_redis()
|
||||
|
||||
# get dovecot ips
|
||||
dovecot_v4 = []
|
||||
dovecot_v6 = []
|
||||
while not dovecot_v4 and not dovecot_v6:
|
||||
try:
|
||||
dovecot_v4 = self.resolve_docker_dns_record("dovecot-mailcow", "A")
|
||||
dovecot_v6 = self.resolve_docker_dns_record("dovecot-mailcow", "AAAA")
|
||||
except Exception as e:
|
||||
print(e)
|
||||
if not dovecot_v4 and not dovecot_v6:
|
||||
print("Waiting for Dovecot IPs...")
|
||||
time.sleep(3)
|
||||
|
||||
# get rspamd ips
|
||||
rspamd_v4 = []
|
||||
rspamd_v6 = []
|
||||
while not rspamd_v4 and not rspamd_v6:
|
||||
try:
|
||||
rspamd_v4 = self.resolve_docker_dns_record("rspamd-mailcow", "A")
|
||||
rspamd_v6 = self.resolve_docker_dns_record("rspamd-mailcow", "AAAA")
|
||||
except Exception:
|
||||
print(e)
|
||||
if not rspamd_v4 and not rspamd_v6:
|
||||
print("Waiting for Rspamd IPs...")
|
||||
time.sleep(3)
|
||||
|
||||
# wait for Services
|
||||
services = [
|
||||
["php-fpm-mailcow", 9001],
|
||||
["php-fpm-mailcow", 9002]
|
||||
]
|
||||
for service in services:
|
||||
while not self.is_port_open(service[0], service[1]):
|
||||
print(f"Waiting for {service[0]} on port {service[1]}...")
|
||||
time.sleep(1)
|
||||
print(f"Service {service[0]} on port {service[1]} is ready!")
|
||||
|
||||
for dir_path in ["/etc/rspamd/plugins.d", "/etc/rspamd/custom"]:
|
||||
Path(dir_path).mkdir(parents=True, exist_ok=True)
|
||||
for file_path in ["/etc/rspamd/rspamd.conf.local", "/etc/rspamd/rspamd.conf.override"]:
|
||||
Path(file_path).touch(exist_ok=True)
|
||||
self.set_permissions("/var/lib/rspamd", 0o755)
|
||||
|
||||
|
||||
# Setup Jinja2 Environment and load vars
|
||||
self.env = Environment(
|
||||
loader=FileSystemLoader([
|
||||
'/service_config/custom_templates',
|
||||
'/service_config/config_templates'
|
||||
]),
|
||||
keep_trailing_newline=True,
|
||||
lstrip_blocks=True,
|
||||
trim_blocks=True
|
||||
)
|
||||
extra_vars = {
|
||||
"DOVECOT_V4": dovecot_v4[0],
|
||||
"DOVECOT_V6": dovecot_v6[0],
|
||||
"RSPAMD_V4": rspamd_v4[0],
|
||||
"RSPAMD_V6": rspamd_v6[0],
|
||||
}
|
||||
self.env_vars = self.prepare_template_vars('/service_config/overwrites.json', extra_vars)
|
||||
|
||||
print("Set Timezone")
|
||||
self.set_timezone()
|
||||
|
||||
print("Render config")
|
||||
self.render_config("/service_config")
|
||||
|
||||
# Fix missing default global maps, if any
|
||||
# These exists in mailcow UI and should not be removed
|
||||
files = [
|
||||
"/etc/rspamd/custom/global_mime_from_blacklist.map",
|
||||
"/etc/rspamd/custom/global_rcpt_blacklist.map",
|
||||
"/etc/rspamd/custom/global_smtp_from_blacklist.map",
|
||||
"/etc/rspamd/custom/global_mime_from_whitelist.map",
|
||||
"/etc/rspamd/custom/global_rcpt_whitelist.map",
|
||||
"/etc/rspamd/custom/global_smtp_from_whitelist.map",
|
||||
"/etc/rspamd/custom/bad_languages.map",
|
||||
"/etc/rspamd/custom/sa-rules",
|
||||
"/etc/rspamd/custom/dovecot_trusted.map",
|
||||
"/etc/rspamd/custom/rspamd_trusted.map",
|
||||
"/etc/rspamd/custom/mailcow_networks.map",
|
||||
"/etc/rspamd/custom/ip_wl.map",
|
||||
"/etc/rspamd/custom/fishy_tlds.map",
|
||||
"/etc/rspamd/custom/bad_words.map",
|
||||
"/etc/rspamd/custom/bad_asn.map",
|
||||
"/etc/rspamd/custom/bad_words_de.map",
|
||||
"/etc/rspamd/custom/bulk_header.map",
|
||||
"/etc/rspamd/custom/bad_header.map"
|
||||
]
|
||||
for file in files:
|
||||
path = Path(file)
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.touch(exist_ok=True)
|
||||
|
||||
# Fix permissions
|
||||
paths_rspamd = [
|
||||
"/var/lib/rspamd",
|
||||
"/etc/rspamd/local.d",
|
||||
"/etc/rspamd/override.d",
|
||||
"/etc/rspamd/rspamd.conf.local",
|
||||
"/etc/rspamd/rspamd.conf.override",
|
||||
"/etc/rspamd/plugins.d"
|
||||
]
|
||||
for path in paths_rspamd:
|
||||
self.set_owner(path, "_rspamd", "_rspamd", recursive=True)
|
||||
self.set_owner("/etc/rspamd/custom", "_rspamd", "_rspamd")
|
||||
self.set_permissions("/etc/rspamd/custom", 0o755)
|
||||
|
||||
custom_path = Path("/etc/rspamd/custom")
|
||||
for child in custom_path.iterdir():
|
||||
if child.is_file():
|
||||
self.set_owner(child, 82, 82)
|
||||
self.set_permissions(child, 0o644)
|
||||
|
||||
# Provide additional lua modules
|
||||
arch = platform.machine()
|
||||
self.run_command(["ln", "-s", f"/usr/lib/{arch}-linux-gnu/liblua5.1-cjson.so.0.0.0", "/usr/lib/rspamd/cjson.so"], check=False)
|
||||
138
data/Dockerfiles/bootstrap/modules/BootstrapSogo.py
Normal file
138
data/Dockerfiles/bootstrap/modules/BootstrapSogo.py
Normal file
@@ -0,0 +1,138 @@
|
||||
from jinja2 import Environment, FileSystemLoader
|
||||
from modules.BootstrapBase import BootstrapBase
|
||||
from pathlib import Path
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
|
||||
class BootstrapSogo(BootstrapBase):
|
||||
def bootstrap(self):
|
||||
# Skip SOGo if set
|
||||
if self.isYes(os.getenv("SKIP_SOGO", "")):
|
||||
print("SKIP_SOGO is set, skipping SOGo startup...")
|
||||
time.sleep(365 * 24 * 60 * 60)
|
||||
sys.exit(1)
|
||||
|
||||
# Connect to MySQL
|
||||
self.connect_mysql()
|
||||
|
||||
# Wait until port is free
|
||||
while self.is_port_open(os.getenv("SOGO_HOST"), 20000):
|
||||
print("Port 20000 still in use — terminating sogod...")
|
||||
self.kill_proc("sogod")
|
||||
time.sleep(3)
|
||||
|
||||
# Wait for schema to update to expected version
|
||||
self.wait_for_schema_update(init_file_path="init_db.inc.php")
|
||||
|
||||
# Setup Jinja2 Environment and load vars
|
||||
self.env = Environment(
|
||||
loader=FileSystemLoader([
|
||||
'/service_config/custom_templates',
|
||||
'/service_config/config_templates'
|
||||
]),
|
||||
keep_trailing_newline=True,
|
||||
lstrip_blocks=True,
|
||||
trim_blocks=True
|
||||
)
|
||||
extra_vars = {
|
||||
"SQL_DOMAINS": self.get_domains(),
|
||||
"IAM_SETTINGS": self.get_identity_provider_settings()
|
||||
}
|
||||
self.env_vars = self.prepare_template_vars('/service_config/overwrites.json', extra_vars)
|
||||
|
||||
print("Set Timezone")
|
||||
self.set_timezone()
|
||||
|
||||
print("Set Syslog redis")
|
||||
self.set_syslog_redis()
|
||||
|
||||
print("Render config")
|
||||
self.render_config("/service_config")
|
||||
|
||||
print("Fix permissions")
|
||||
self.set_owner("/var/lib/sogo", "sogo", "sogo", recursive=True)
|
||||
self.set_permissions("/var/lib/sogo/GNUstep/Defaults/sogod.plist", 0o600)
|
||||
|
||||
# Rename custom logo
|
||||
logo_src = Path("/etc/sogo/sogo-full.svg")
|
||||
if logo_src.exists():
|
||||
print("Set Logo")
|
||||
self.move_file(logo_src, "/etc/sogo/custom-fulllogo.svg")
|
||||
|
||||
# Rsync web content
|
||||
print("Syncing web content")
|
||||
self.rsync_file("/usr/lib/GNUstep/SOGo/", "/sogo_web/", recursive=True)
|
||||
|
||||
# Chown backup path
|
||||
self.set_owner("/sogo_backup", "sogo", "sogo", recursive=True)
|
||||
|
||||
def get_domains(self):
|
||||
"""
|
||||
Retrieves a list of domains and their GAL (Global Address List) status.
|
||||
|
||||
Executes a SQL query to select:
|
||||
- `domain`
|
||||
- a human-readable GAL status ("YES" or "NO")
|
||||
- `ldap_gal` as a boolean (True/False)
|
||||
|
||||
Returns:
|
||||
list[dict]: A list of dicts with keys: domain, gal_status, ldap_gal.
|
||||
Example: [{"domain": "example.com", "gal_status": "YES", "ldap_gal": True}]
|
||||
|
||||
Logs:
|
||||
Error messages if the query fails.
|
||||
"""
|
||||
|
||||
query = """
|
||||
SELECT domain,
|
||||
CASE gal WHEN '1' THEN 'YES' ELSE 'NO' END AS gal_status,
|
||||
ldap_gal = 1 AS ldap_gal
|
||||
FROM domain;
|
||||
"""
|
||||
try:
|
||||
cursor = self.mysql_conn.cursor()
|
||||
cursor.execute(query)
|
||||
result = cursor.fetchall()
|
||||
cursor.close()
|
||||
|
||||
return [
|
||||
{
|
||||
"domain": row[0],
|
||||
"gal_status": row[1],
|
||||
"ldap_gal": bool(row[2])
|
||||
}
|
||||
for row in result
|
||||
]
|
||||
except Exception as e:
|
||||
print(f"Error fetching domains: {e}")
|
||||
return []
|
||||
|
||||
def get_identity_provider_settings(self):
|
||||
"""
|
||||
Retrieves all key-value identity provider settings.
|
||||
|
||||
Returns:
|
||||
dict: Settings in the format { key: value }
|
||||
|
||||
Logs:
|
||||
Error messages if the query fails.
|
||||
"""
|
||||
query = "SELECT `key`, `value` FROM identity_provider;"
|
||||
try:
|
||||
cursor = self.mysql_conn.cursor()
|
||||
cursor.execute(query)
|
||||
result = cursor.fetchall()
|
||||
cursor.close()
|
||||
|
||||
iam_settings = {row[0]: row[1] for row in result}
|
||||
|
||||
if iam_settings['authsource'] == "ldap":
|
||||
protocol = "ldaps" if iam_settings.get("use_ssl") else "ldap"
|
||||
starttls = "/????!StartTLS" if iam_settings.get("use_tls") else ""
|
||||
iam_settings['ldap_url'] = f"{protocol}://{iam_settings['host']}:{iam_settings['port']}{starttls}"
|
||||
|
||||
return iam_settings
|
||||
except Exception as e:
|
||||
print(f"Error fetching identity provider settings: {e}")
|
||||
return {}
|
||||
0
data/Dockerfiles/bootstrap/modules/__init__.py
Normal file
0
data/Dockerfiles/bootstrap/modules/__init__.py
Normal file
@@ -41,7 +41,7 @@ RUN wget -P /src https://www.clamav.net/downloads/production/clamav-${CLAMD_VERS
|
||||
-D ENABLE_MILTER=ON \
|
||||
-D ENABLE_MAN_PAGES=OFF \
|
||||
-D ENABLE_STATIC_LIB=OFF \
|
||||
-D ENABLE_JSON_SHARED=ON \
|
||||
-D ENABLE_JSON_SHARED=ON \
|
||||
&& cmake --build . \
|
||||
&& make DESTDIR="/clamav" -j$(($(nproc) - 1)) install \
|
||||
&& rm -r "/clamav/usr/lib/pkgconfig/" \
|
||||
@@ -88,23 +88,42 @@ RUN apk upgrade --no-cache \
|
||||
pcre2 \
|
||||
zlib \
|
||||
libgcc \
|
||||
py3-pip \
|
||||
&& addgroup -S "clamav" && \
|
||||
adduser -D -G "clamav" -h "/var/lib/clamav" -s "/bin/false" -S "clamav" && \
|
||||
install -d -m 755 -g "clamav" -o "clamav" "/var/log/clamav" && \
|
||||
chown -R clamav:clamav /var/lib/clamav
|
||||
|
||||
RUN apk add --no-cache --virtual .build-deps \
|
||||
gcc \
|
||||
musl-dev \
|
||||
python3-dev \
|
||||
linux-headers \
|
||||
&& pip install --break-system-packages psutil \
|
||||
&& apk del .build-deps
|
||||
|
||||
RUN pip install --break-system-packages \
|
||||
mysql-connector-python \
|
||||
jinja2 \
|
||||
redis \
|
||||
dnspython
|
||||
|
||||
|
||||
COPY --from=builder "/clamav" "/"
|
||||
|
||||
# init
|
||||
COPY clamd.sh /clamd.sh
|
||||
RUN chmod +x /sbin/tini
|
||||
|
||||
# healthcheck
|
||||
COPY healthcheck.sh /healthcheck.sh
|
||||
COPY clamdcheck.sh /usr/local/bin
|
||||
RUN chmod +x /healthcheck.sh
|
||||
RUN chmod +x /usr/local/bin/clamdcheck.sh
|
||||
COPY data/Dockerfiles/bootstrap /bootstrap
|
||||
COPY data/Dockerfiles/clamd/docker-entrypoint.sh /docker-entrypoint.sh
|
||||
COPY data/Dockerfiles/clamd/clamd.sh /clamd.sh
|
||||
COPY data/Dockerfiles/clamd/healthcheck.sh /healthcheck.sh
|
||||
COPY data/Dockerfiles/clamd/clamdcheck.sh /usr/local/bin
|
||||
HEALTHCHECK --start-period=6m CMD "/healthcheck.sh"
|
||||
|
||||
ENTRYPOINT []
|
||||
RUN chmod +x /docker-entrypoint.sh \
|
||||
/clamd.sh \
|
||||
/healthcheck.sh \
|
||||
/usr/local/bin/clamdcheck.sh \
|
||||
/sbin/tini
|
||||
|
||||
ENTRYPOINT ["/docker-entrypoint.sh"]
|
||||
CMD ["/sbin/tini", "-g", "--", "/clamd.sh"]
|
||||
@@ -1,48 +1,5 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [[ "${SKIP_CLAMD}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
|
||||
echo "SKIP_CLAMD=y, skipping ClamAV..."
|
||||
sleep 365d
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Cleaning up garbage
|
||||
echo "Cleaning up tmp files..."
|
||||
rm -rf /var/lib/clamav/clamav-*.tmp
|
||||
|
||||
# Prepare whitelist
|
||||
|
||||
mkdir -p /run/clamav /var/lib/clamav
|
||||
|
||||
if [[ -s /etc/clamav/whitelist.ign2 ]]; then
|
||||
echo "Copying non-empty whitelist.ign2 to /var/lib/clamav/whitelist.ign2"
|
||||
cp /etc/clamav/whitelist.ign2 /var/lib/clamav/whitelist.ign2
|
||||
fi
|
||||
|
||||
if [[ ! -f /var/lib/clamav/whitelist.ign2 ]]; then
|
||||
echo "Creating /var/lib/clamav/whitelist.ign2"
|
||||
cat <<EOF > /var/lib/clamav/whitelist.ign2
|
||||
# Please restart ClamAV after changing signatures
|
||||
Example-Signature.Ignore-1
|
||||
PUA.Win.Trojan.EmbeddedPDF-1
|
||||
PUA.Pdf.Trojan.EmbeddedJavaScript-1
|
||||
PUA.Pdf.Trojan.OpenActionObjectwithJavascript-1
|
||||
EOF
|
||||
fi
|
||||
|
||||
chown clamav:clamav -R /var/lib/clamav /run/clamav
|
||||
|
||||
chmod 755 /var/lib/clamav
|
||||
chmod 644 -R /var/lib/clamav/*
|
||||
chmod 750 /run/clamav
|
||||
|
||||
stat /var/lib/clamav/whitelist.ign2
|
||||
dos2unix /var/lib/clamav/whitelist.ign2
|
||||
sed -i '/^\s*$/d' /var/lib/clamav/whitelist.ign2
|
||||
# Copying to /etc/clamav to expose file as-is to administrator
|
||||
cp -p /var/lib/clamav/whitelist.ign2 /etc/clamav/whitelist.ign2
|
||||
|
||||
|
||||
BACKGROUND_TASKS=()
|
||||
|
||||
echo "Running freshclam..."
|
||||
|
||||
20
data/Dockerfiles/clamd/docker-entrypoint.sh
Normal file
20
data/Dockerfiles/clamd/docker-entrypoint.sh
Normal file
@@ -0,0 +1,20 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Run hooks
|
||||
for file in /hooks/*; do
|
||||
if [ -x "${file}" ]; then
|
||||
echo "Running hook ${file}"
|
||||
"${file}"
|
||||
fi
|
||||
done
|
||||
|
||||
python3 -u /bootstrap/main.py
|
||||
BOOTSTRAP_EXIT_CODE=$?
|
||||
|
||||
if [ $BOOTSTRAP_EXIT_CODE -ne 0 ]; then
|
||||
echo "Bootstrap failed with exit code $BOOTSTRAP_EXIT_CODE. Not starting Clamd."
|
||||
exit $BOOTSTRAP_EXIT_CODE
|
||||
fi
|
||||
|
||||
echo "Bootstrap succeeded. Starting Clamd..."
|
||||
exec "$@"
|
||||
@@ -19,9 +19,9 @@ RUN apk add --update --no-cache python3 \
|
||||
docker
|
||||
RUN mkdir /app/modules
|
||||
|
||||
COPY docker-entrypoint.sh /app/
|
||||
COPY main.py /app/main.py
|
||||
COPY modules/ /app/modules/
|
||||
COPY data/Dockerfiles/dockerapi/docker-entrypoint.sh /app/
|
||||
COPY data/Dockerfiles/dockerapi/main.py /app/main.py
|
||||
COPY data/Dockerfiles/dockerapi/modules/ /app/modules/
|
||||
|
||||
ENTRYPOINT ["/bin/sh", "/app/docker-entrypoint.sh"]
|
||||
CMD ["python", "main.py"]
|
||||
@@ -36,7 +36,7 @@ async def lifespan(app: FastAPI):
|
||||
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", password=os.environ['REDISPASS'])
|
||||
else:
|
||||
redis_client = redis = await aioredis.from_url("redis://redis-mailcow:6379/0", password=os.environ['REDISPASS'])
|
||||
redis_client = redis = await aioredis.from_url(f"redis://{os.environ['REDIS_HOST']}:6379/0", password=os.environ['REDISPASS'])
|
||||
|
||||
# Init docker clients
|
||||
sync_docker_client = docker.DockerClient(base_url='unix://var/run/docker.sock', version='auto')
|
||||
|
||||
@@ -87,11 +87,11 @@ RUN addgroup -g 5000 vmail \
|
||||
perl-proc-processtable \
|
||||
perl-app-cpanminus \
|
||||
procps \
|
||||
python3 \
|
||||
py3-mysqlclient \
|
||||
python3 py3-pip python3-dev \
|
||||
py3-html2text \
|
||||
py3-jinja2 \
|
||||
py3-redis \
|
||||
linux-headers \
|
||||
musl-dev \
|
||||
gcc \
|
||||
redis \
|
||||
syslog-ng \
|
||||
syslog-ng-redis \
|
||||
@@ -115,25 +115,36 @@ RUN addgroup -g 5000 vmail \
|
||||
&& chmod +x /usr/local/bin/gosu \
|
||||
&& gosu nobody true
|
||||
|
||||
COPY trim_logs.sh /usr/local/bin/trim_logs.sh
|
||||
COPY clean_q_aged.sh /usr/local/bin/clean_q_aged.sh
|
||||
COPY syslog-ng.conf /etc/syslog-ng/syslog-ng.conf
|
||||
COPY syslog-ng-redis_slave.conf /etc/syslog-ng/syslog-ng-redis_slave.conf
|
||||
COPY imapsync /usr/local/bin/imapsync
|
||||
COPY imapsync_runner.pl /usr/local/bin/imapsync_runner.pl
|
||||
COPY report-spam.sieve /usr/lib/dovecot/sieve/report-spam.sieve
|
||||
COPY report-ham.sieve /usr/lib/dovecot/sieve/report-ham.sieve
|
||||
COPY rspamd-pipe-ham /usr/lib/dovecot/sieve/rspamd-pipe-ham
|
||||
COPY rspamd-pipe-spam /usr/lib/dovecot/sieve/rspamd-pipe-spam
|
||||
COPY sa-rules.sh /usr/local/bin/sa-rules.sh
|
||||
COPY maildir_gc.sh /usr/local/bin/maildir_gc.sh
|
||||
COPY docker-entrypoint.sh /
|
||||
COPY supervisord.conf /etc/supervisor/supervisord.conf
|
||||
COPY stop-supervisor.sh /usr/local/sbin/stop-supervisor.sh
|
||||
COPY quarantine_notify.py /usr/local/bin/quarantine_notify.py
|
||||
COPY quota_notify.py /usr/local/bin/quota_notify.py
|
||||
COPY repl_health.sh /usr/local/bin/repl_health.sh
|
||||
COPY optimize-fts.sh /usr/local/bin/optimize-fts.sh
|
||||
RUN pip install --break-system-packages \
|
||||
mysql-connector-python \
|
||||
jinja2 \
|
||||
redis \
|
||||
dnspython \
|
||||
psutil
|
||||
|
||||
|
||||
COPY data/Dockerfiles/bootstrap /bootstrap
|
||||
COPY data/Dockerfiles/dovecot/trim_logs.sh /usr/local/bin/trim_logs.sh
|
||||
COPY data/Dockerfiles/dovecot/clean_q_aged.sh /usr/local/bin/clean_q_aged.sh
|
||||
COPY data/Dockerfiles/dovecot/syslog-ng.conf /etc/syslog-ng/syslog-ng.conf
|
||||
COPY data/Dockerfiles/dovecot/syslog-ng-redis_slave.conf /etc/syslog-ng/syslog-ng-redis_slave.conf
|
||||
COPY data/Dockerfiles/dovecot/imapsync /usr/local/bin/imapsync
|
||||
COPY data/Dockerfiles/dovecot/imapsync_runner.pl /usr/local/bin/imapsync_runner.pl
|
||||
COPY data/Dockerfiles/dovecot/report-spam.sieve /usr/lib/dovecot/sieve/report-spam.sieve
|
||||
COPY data/Dockerfiles/dovecot/report-ham.sieve /usr/lib/dovecot/sieve/report-ham.sieve
|
||||
COPY data/Dockerfiles/dovecot/rspamd-pipe-ham /usr/lib/dovecot/sieve/rspamd-pipe-ham
|
||||
COPY data/Dockerfiles/dovecot/rspamd-pipe-spam /usr/lib/dovecot/sieve/rspamd-pipe-spam
|
||||
COPY data/Dockerfiles/dovecot/sa-rules.sh /usr/local/bin/sa-rules.sh
|
||||
COPY data/Dockerfiles/dovecot/docker-entrypoint.sh /
|
||||
COPY data/Dockerfiles/dovecot/supervisord.conf /etc/supervisor/supervisord.conf
|
||||
COPY data/Dockerfiles/dovecot/stop-supervisor.sh /usr/local/sbin/stop-supervisor.sh
|
||||
COPY data/Dockerfiles/dovecot/quarantine_notify.py /usr/local/bin/quarantine_notify.py
|
||||
COPY data/Dockerfiles/dovecot/quota_notify.py /usr/local/bin/quota_notify.py
|
||||
COPY data/Dockerfiles/dovecot/repl_health.sh /usr/local/bin/repl_health.sh
|
||||
COPY data/Dockerfiles/dovecot/optimize-fts.sh /usr/local/bin/optimize-fts.sh
|
||||
|
||||
RUN chmod +x /docker-entrypoint.sh \
|
||||
/usr/local/sbin/stop-supervisor.sh
|
||||
|
||||
|
||||
ENTRYPOINT ["/docker-entrypoint.sh"]
|
||||
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/supervisord.conf"]
|
||||
|
||||
@@ -1,253 +1,15 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Wait for MySQL to warm-up
|
||||
while ! mariadb-admin status --ssl=false --socket=/var/run/mysqld/mysqld.sock -u${DBUSER} -p${DBPASS} --silent; do
|
||||
echo "Waiting for database to come up..."
|
||||
sleep 2
|
||||
done
|
||||
|
||||
until dig +short mailcow.email > /dev/null; do
|
||||
echo "Waiting for DNS..."
|
||||
sleep 1
|
||||
done
|
||||
|
||||
# Do not attempt to write to slave
|
||||
if [[ ! -z ${REDIS_SLAVEOF_IP} ]]; then
|
||||
REDIS_CMDLINE="redis-cli -h ${REDIS_SLAVEOF_IP} -p ${REDIS_SLAVEOF_PORT} -a ${REDISPASS} --no-auth-warning"
|
||||
else
|
||||
REDIS_CMDLINE="redis-cli -h redis -p 6379 -a ${REDISPASS} --no-auth-warning"
|
||||
fi
|
||||
|
||||
until [[ $(${REDIS_CMDLINE} PING) == "PONG" ]]; do
|
||||
echo "Waiting for Redis..."
|
||||
sleep 2
|
||||
done
|
||||
|
||||
${REDIS_CMDLINE} SET DOVECOT_REPL_HEALTH 1 > /dev/null
|
||||
|
||||
# Create missing directories
|
||||
[[ ! -d /etc/dovecot/sql/ ]] && mkdir -p /etc/dovecot/sql/
|
||||
[[ ! -d /etc/dovecot/auth/ ]] && mkdir -p /etc/dovecot/auth/
|
||||
[[ ! -d /etc/dovecot/conf.d/ ]] && mkdir -p /etc/dovecot/conf.d/
|
||||
[[ ! -d /var/vmail/_garbage ]] && mkdir -p /var/vmail/_garbage
|
||||
[[ ! -d /var/vmail/sieve ]] && mkdir -p /var/vmail/sieve
|
||||
[[ ! -d /etc/sogo ]] && mkdir -p /etc/sogo
|
||||
[[ ! -d /var/volatile ]] && mkdir -p /var/volatile
|
||||
|
||||
# Set Dovecot sql config parameters, escape " in db password
|
||||
DBPASS=$(echo ${DBPASS} | sed 's/"/\\"/g')
|
||||
|
||||
# Create quota dict for Dovecot
|
||||
if [[ "${MASTER}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
|
||||
QUOTA_TABLE=quota2
|
||||
else
|
||||
QUOTA_TABLE=quota2replica
|
||||
fi
|
||||
cat <<EOF > /etc/dovecot/sql/dovecot-dict-sql-quota.conf
|
||||
# Autogenerated by mailcow
|
||||
connect = "host=/var/run/mysqld/mysqld.sock dbname=${DBNAME} user=${DBUSER} password=${DBPASS}"
|
||||
map {
|
||||
pattern = priv/quota/storage
|
||||
table = ${QUOTA_TABLE}
|
||||
username_field = username
|
||||
value_field = bytes
|
||||
}
|
||||
map {
|
||||
pattern = priv/quota/messages
|
||||
table = ${QUOTA_TABLE}
|
||||
username_field = username
|
||||
value_field = messages
|
||||
}
|
||||
EOF
|
||||
|
||||
# Create dict used for sieve pre and postfilters
|
||||
cat <<EOF > /etc/dovecot/sql/dovecot-dict-sql-sieve_before.conf
|
||||
# Autogenerated by mailcow
|
||||
connect = "host=/var/run/mysqld/mysqld.sock dbname=${DBNAME} user=${DBUSER} password=${DBPASS}"
|
||||
map {
|
||||
pattern = priv/sieve/name/\$script_name
|
||||
table = sieve_before
|
||||
username_field = username
|
||||
value_field = id
|
||||
fields {
|
||||
script_name = \$script_name
|
||||
}
|
||||
}
|
||||
map {
|
||||
pattern = priv/sieve/data/\$id
|
||||
table = sieve_before
|
||||
username_field = username
|
||||
value_field = script_data
|
||||
fields {
|
||||
id = \$id
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
cat <<EOF > /etc/dovecot/sql/dovecot-dict-sql-sieve_after.conf
|
||||
# Autogenerated by mailcow
|
||||
connect = "host=/var/run/mysqld/mysqld.sock dbname=${DBNAME} user=${DBUSER} password=${DBPASS}"
|
||||
map {
|
||||
pattern = priv/sieve/name/\$script_name
|
||||
table = sieve_after
|
||||
username_field = username
|
||||
value_field = id
|
||||
fields {
|
||||
script_name = \$script_name
|
||||
}
|
||||
}
|
||||
map {
|
||||
pattern = priv/sieve/data/\$id
|
||||
table = sieve_after
|
||||
username_field = username
|
||||
value_field = script_data
|
||||
fields {
|
||||
id = \$id
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
echo -n ${ACL_ANYONE} > /etc/dovecot/acl_anyone
|
||||
|
||||
if [[ "${SKIP_FTS}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
|
||||
echo -e "\e[33mDetecting SKIP_FTS=y... not enabling Flatcurve (FTS) then...\e[0m"
|
||||
echo -n 'quota acl zlib mail_crypt mail_crypt_acl mail_log notify listescape replication lazy_expunge' > /etc/dovecot/mail_plugins
|
||||
echo -n 'quota imap_quota imap_acl acl zlib imap_zlib imap_sieve mail_crypt mail_crypt_acl notify listescape replication mail_log' > /etc/dovecot/mail_plugins_imap
|
||||
echo -n 'quota sieve acl zlib mail_crypt mail_crypt_acl notify listescape replication' > /etc/dovecot/mail_plugins_lmtp
|
||||
else
|
||||
echo -e "\e[32mDetecting SKIP_FTS=n... enabling Flatcurve (FTS)\e[0m"
|
||||
echo -n 'quota acl zlib mail_crypt mail_crypt_acl mail_log notify fts fts_flatcurve listescape replication lazy_expunge' > /etc/dovecot/mail_plugins
|
||||
echo -n 'quota imap_quota imap_acl acl zlib imap_zlib imap_sieve mail_crypt mail_crypt_acl notify mail_log fts fts_flatcurve listescape replication' > /etc/dovecot/mail_plugins_imap
|
||||
echo -n 'quota sieve acl zlib mail_crypt mail_crypt_acl fts fts_flatcurve notify listescape replication' > /etc/dovecot/mail_plugins_lmtp
|
||||
fi
|
||||
chmod 644 /etc/dovecot/mail_plugins /etc/dovecot/mail_plugins_imap /etc/dovecot/mail_plugins_lmtp /templates/quarantine.tpl
|
||||
|
||||
cat <<EOF > /etc/dovecot/sql/dovecot-dict-sql-userdb.conf
|
||||
# Autogenerated by mailcow
|
||||
driver = mysql
|
||||
connect = "host=/var/run/mysqld/mysqld.sock dbname=${DBNAME} user=${DBUSER} password=${DBPASS}"
|
||||
user_query = SELECT CONCAT(JSON_UNQUOTE(JSON_VALUE(attributes, '$.mailbox_format')), mailbox_path_prefix, '%d/%n/${MAILDIR_SUB}:VOLATILEDIR=/var/volatile/%u:INDEX=/var/vmail_index/%u') AS mail, '%s' AS protocol, 5000 AS uid, 5000 AS gid, concat('*:bytes=', quota) AS quota_rule FROM mailbox WHERE username = '%u' AND (active = '1' OR active = '2')
|
||||
iterate_query = SELECT username FROM mailbox WHERE active = '1' OR active = '2';
|
||||
EOF
|
||||
|
||||
|
||||
# Migrate old sieve_after file
|
||||
[[ -f /etc/dovecot/sieve_after ]] && mv /etc/dovecot/sieve_after /etc/dovecot/global_sieve_after
|
||||
# Create global sieve scripts
|
||||
cat /etc/dovecot/global_sieve_after > /var/vmail/sieve/global_sieve_after.sieve
|
||||
cat /etc/dovecot/global_sieve_before > /var/vmail/sieve/global_sieve_before.sieve
|
||||
|
||||
# Check permissions of vmail/index/garbage directories.
|
||||
# Do not do this every start-up, it may take a very long time. So we use a stat check here.
|
||||
if [[ $(stat -c %U /var/vmail/) != "vmail" ]] ; then chown -R vmail:vmail /var/vmail ; fi
|
||||
if [[ $(stat -c %U /var/vmail/_garbage) != "vmail" ]] ; then chown -R vmail:vmail /var/vmail/_garbage ; fi
|
||||
if [[ $(stat -c %U /var/vmail_index) != "vmail" ]] ; then chown -R vmail:vmail /var/vmail_index ; fi
|
||||
|
||||
# Cleanup random user maildirs
|
||||
rm -rf /var/vmail/mailcow.local/*
|
||||
# Cleanup PIDs
|
||||
[[ -f /tmp/quarantine_notify.pid ]] && rm /tmp/quarantine_notify.pid
|
||||
|
||||
# create sni configuration
|
||||
echo "" > /etc/dovecot/sni.conf
|
||||
for cert_dir in /etc/ssl/mail/*/ ; do
|
||||
if [[ ! -f ${cert_dir}domains ]] || [[ ! -f ${cert_dir}cert.pem ]] || [[ ! -f ${cert_dir}key.pem ]]; then
|
||||
continue
|
||||
# Run hooks
|
||||
for file in /hooks/*; do
|
||||
if [ -x "${file}" ]; then
|
||||
echo "Running hook ${file}"
|
||||
"${file}"
|
||||
fi
|
||||
domains=($(cat ${cert_dir}domains))
|
||||
for domain in ${domains[@]}; do
|
||||
echo 'local_name '${domain}' {' >> /etc/dovecot/sni.conf;
|
||||
echo ' ssl_cert = <'${cert_dir}'cert.pem' >> /etc/dovecot/sni.conf;
|
||||
echo ' ssl_key = <'${cert_dir}'key.pem' >> /etc/dovecot/sni.conf;
|
||||
echo '}' >> /etc/dovecot/sni.conf;
|
||||
done
|
||||
done
|
||||
|
||||
# Create random master for SOGo sieve features
|
||||
RAND_USER=$(cat /dev/urandom | tr -dc 'a-z0-9' | fold -w 16 | head -n 1)
|
||||
RAND_PASS=$(cat /dev/urandom | tr -dc 'a-z0-9' | fold -w 24 | head -n 1)
|
||||
|
||||
if [[ ! -z ${DOVECOT_MASTER_USER} ]] && [[ ! -z ${DOVECOT_MASTER_PASS} ]]; then
|
||||
RAND_USER=${DOVECOT_MASTER_USER}
|
||||
RAND_PASS=${DOVECOT_MASTER_PASS}
|
||||
fi
|
||||
echo ${RAND_USER}@mailcow.local:{SHA1}$(echo -n ${RAND_PASS} | sha1sum | awk '{print $1}'):::::: > /etc/dovecot/dovecot-master.passwd
|
||||
echo ${RAND_USER}@mailcow.local::5000:5000:::: > /etc/dovecot/dovecot-master.userdb
|
||||
echo ${RAND_USER}@mailcow.local:${RAND_PASS} > /etc/sogo/sieve.creds
|
||||
|
||||
if [[ -z ${MAILDIR_SUB} ]]; then
|
||||
MAILDIR_SUB_SHARED=
|
||||
else
|
||||
MAILDIR_SUB_SHARED=/${MAILDIR_SUB}
|
||||
fi
|
||||
cat <<EOF > /etc/dovecot/shared_namespace.conf
|
||||
# Autogenerated by mailcow
|
||||
namespace {
|
||||
type = shared
|
||||
separator = /
|
||||
prefix = Shared/%%u/
|
||||
location = maildir:%%h${MAILDIR_SUB_SHARED}:INDEX=~${MAILDIR_SUB_SHARED}/Shared/%%u
|
||||
subscriptions = no
|
||||
list = children
|
||||
}
|
||||
EOF
|
||||
|
||||
|
||||
cat <<EOF > /etc/dovecot/sogo_trusted_ip.conf
|
||||
# Autogenerated by mailcow
|
||||
remote ${IPV4_NETWORK}.248 {
|
||||
disable_plaintext_auth = no
|
||||
}
|
||||
EOF
|
||||
|
||||
# Create random master Password for SOGo SSO
|
||||
RAND_PASS=$(cat /dev/urandom | tr -dc 'a-z0-9' | fold -w 32 | head -n 1)
|
||||
echo -n ${RAND_PASS} > /etc/phpfpm/sogo-sso.pass
|
||||
# Creating additional creds file for SOGo notify crons (calendars, etc)
|
||||
echo -n ${RAND_USER}@mailcow.local:${RAND_PASS} > /etc/sogo/cron.creds
|
||||
cat <<EOF > /etc/dovecot/sogo-sso.conf
|
||||
# Autogenerated by mailcow
|
||||
passdb {
|
||||
driver = static
|
||||
args = allow_real_nets=${IPV4_NETWORK}.248/32 password={plain}${RAND_PASS}
|
||||
}
|
||||
EOF
|
||||
|
||||
if [[ "${MASTER}" =~ ^([nN][oO]|[nN])+$ ]]; then
|
||||
# Toggling MASTER will result in a rebuild of containers, so the quota script will be recreated
|
||||
cat <<'EOF' > /usr/local/bin/quota_notify.py
|
||||
#!/usr/bin/python3
|
||||
import sys
|
||||
sys.exit()
|
||||
EOF
|
||||
fi
|
||||
|
||||
# Set mail_replica for HA setups
|
||||
if [[ -n ${MAILCOW_REPLICA_IP} && -n ${DOVEADM_REPLICA_PORT} ]]; then
|
||||
cat <<EOF > /etc/dovecot/mail_replica.conf
|
||||
# Autogenerated by mailcow
|
||||
mail_replica = tcp:${MAILCOW_REPLICA_IP}:${DOVEADM_REPLICA_PORT}
|
||||
EOF
|
||||
fi
|
||||
|
||||
# Setting variables for indexer-worker inside fts.conf automatically according to mailcow.conf settings
|
||||
if [[ "${SKIP_FTS}" =~ ^([nN][oO]|[nN])+$ ]]; then
|
||||
echo -e "\e[94mConfiguring FTS Settings...\e[0m"
|
||||
echo -e "\e[94mSetting FTS Memory Limit (per process) to ${FTS_HEAP} MB\e[0m"
|
||||
sed -i "s/vsz_limit\s*=\s*[0-9]*\s*MB*/vsz_limit=${FTS_HEAP} MB/" /etc/dovecot/conf.d/fts.conf
|
||||
echo -e "\e[94mSetting FTS Process Limit to ${FTS_PROCS}\e[0m"
|
||||
sed -i "s/process_limit\s*=\s*[0-9]*/process_limit=${FTS_PROCS}/" /etc/dovecot/conf.d/fts.conf
|
||||
fi
|
||||
|
||||
# 401 is user dovecot
|
||||
if [[ ! -s /mail_crypt/ecprivkey.pem || ! -s /mail_crypt/ecpubkey.pem ]]; then
|
||||
openssl ecparam -name prime256v1 -genkey | openssl pkey -out /mail_crypt/ecprivkey.pem
|
||||
openssl pkey -in /mail_crypt/ecprivkey.pem -pubout -out /mail_crypt/ecpubkey.pem
|
||||
chown 401 /mail_crypt/ecprivkey.pem /mail_crypt/ecpubkey.pem
|
||||
else
|
||||
chown 401 /mail_crypt/ecprivkey.pem /mail_crypt/ecpubkey.pem
|
||||
fi
|
||||
python3 -u /bootstrap/main.py
|
||||
BOOTSTRAP_EXIT_CODE=$?
|
||||
|
||||
# Fix OpenSSL 3.X TLS1.0, 1.1 support (https://community.mailcow.email/d/4062-hi-all/20)
|
||||
if grep -qE 'ssl_min_protocol\s*=\s*(TLSv1|TLSv1\.1)\s*$' /etc/dovecot/dovecot.conf /etc/dovecot/extra.conf; then
|
||||
@@ -260,89 +22,10 @@ if grep -qE 'ssl_min_protocol\s*=\s*(TLSv1|TLSv1\.1)\s*$' /etc/dovecot/dovecot.c
|
||||
echo "CipherString = DEFAULT@SECLEVEL=0" >> /etc/ssl/openssl.cnf
|
||||
fi
|
||||
|
||||
# Compile sieve scripts
|
||||
sievec /var/vmail/sieve/global_sieve_before.sieve
|
||||
sievec /var/vmail/sieve/global_sieve_after.sieve
|
||||
sievec /usr/lib/dovecot/sieve/report-spam.sieve
|
||||
sievec /usr/lib/dovecot/sieve/report-ham.sieve
|
||||
|
||||
# Fix permissions
|
||||
chown root:root /etc/dovecot/sql/*.conf
|
||||
chown root:dovecot /etc/dovecot/sql/dovecot-dict-sql-sieve* /etc/dovecot/sql/dovecot-dict-sql-quota* /etc/dovecot/auth/passwd-verify.lua
|
||||
chmod 640 /etc/dovecot/sql/*.conf /etc/dovecot/auth/passwd-verify.lua
|
||||
chown -R vmail:vmail /var/vmail/sieve
|
||||
chown -R vmail:vmail /var/volatile
|
||||
chown -R vmail:vmail /var/vmail_index
|
||||
adduser vmail tty
|
||||
chmod g+rw /dev/console
|
||||
chown root:tty /dev/console
|
||||
chmod +x /usr/lib/dovecot/sieve/rspamd-pipe-ham \
|
||||
/usr/lib/dovecot/sieve/rspamd-pipe-spam \
|
||||
/usr/local/bin/imapsync_runner.pl \
|
||||
/usr/local/bin/imapsync \
|
||||
/usr/local/bin/trim_logs.sh \
|
||||
/usr/local/bin/sa-rules.sh \
|
||||
/usr/local/bin/clean_q_aged.sh \
|
||||
/usr/local/bin/maildir_gc.sh \
|
||||
/usr/local/sbin/stop-supervisor.sh \
|
||||
/usr/local/bin/quota_notify.py \
|
||||
/usr/local/bin/repl_health.sh \
|
||||
/usr/local/bin/optimize-fts.sh
|
||||
|
||||
# Prepare environment file for cronjobs
|
||||
printenv | sed 's/^\(.*\)$/export \1/g' > /source_env.sh
|
||||
|
||||
# Clean old PID if any
|
||||
[[ -f /var/run/dovecot/master.pid ]] && rm /var/run/dovecot/master.pid
|
||||
|
||||
# Clean stopped imapsync jobs
|
||||
rm -f /tmp/imapsync_busy.lock
|
||||
IMAPSYNC_TABLE=$(mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SHOW TABLES LIKE 'imapsync'" -Bs)
|
||||
[[ ! -z ${IMAPSYNC_TABLE} ]] && mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "UPDATE imapsync SET is_running='0'"
|
||||
|
||||
# Envsubst maildir_gc
|
||||
echo "$(envsubst < /usr/local/bin/maildir_gc.sh)" > /usr/local/bin/maildir_gc.sh
|
||||
|
||||
# GUID generation
|
||||
while [[ ${VERSIONS_OK} != 'OK' ]]; do
|
||||
if [[ ! -z $(mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -B -e "SELECT 'OK' FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = \"${DBNAME}\" AND TABLE_NAME = 'versions'") ]]; then
|
||||
VERSIONS_OK=OK
|
||||
else
|
||||
echo "Waiting for versions table to be created..."
|
||||
sleep 3
|
||||
fi
|
||||
done
|
||||
PUBKEY_MCRYPT=$(doveconf -P 2> /dev/null | grep -i mail_crypt_global_public_key | cut -d '<' -f2)
|
||||
if [ -f ${PUBKEY_MCRYPT} ]; then
|
||||
GUID=$(cat <(echo ${MAILCOW_HOSTNAME}) /mail_crypt/ecpubkey.pem | sha256sum | cut -d ' ' -f1 | tr -cd "[a-fA-F0-9.:/] ")
|
||||
if [ ${#GUID} -eq 64 ]; then
|
||||
mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} << EOF
|
||||
REPLACE INTO versions (application, version) VALUES ("GUID", "${GUID}");
|
||||
EOF
|
||||
else
|
||||
mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} << EOF
|
||||
REPLACE INTO versions (application, version) VALUES ("GUID", "INVALID");
|
||||
EOF
|
||||
fi
|
||||
if [ $BOOTSTRAP_EXIT_CODE -ne 0 ]; then
|
||||
echo "Bootstrap failed with exit code $BOOTSTRAP_EXIT_CODE. Not starting Dovecot."
|
||||
exit $BOOTSTRAP_EXIT_CODE
|
||||
fi
|
||||
|
||||
# Collect SA rules once now
|
||||
/usr/local/bin/sa-rules.sh
|
||||
|
||||
# Run hooks
|
||||
for file in /hooks/*; do
|
||||
if [ -x "${file}" ]; then
|
||||
echo "Running hook ${file}"
|
||||
"${file}"
|
||||
fi
|
||||
done
|
||||
|
||||
# For some strange, unknown and stupid reason, Dovecot may run into a race condition, when this file is not touched before it is read by dovecot/auth
|
||||
# May be related to something inside Docker, I seriously don't know
|
||||
touch /etc/dovecot/auth/passwd-verify.lua
|
||||
|
||||
if [[ ! -z ${REDIS_SLAVEOF_IP} ]]; then
|
||||
cp /etc/syslog-ng/syslog-ng-redis_slave.conf /etc/syslog-ng/syslog-ng.conf
|
||||
fi
|
||||
|
||||
exec "$@"
|
||||
echo "Bootstrap succeeded. Starting Dovecot..."
|
||||
/usr/sbin/dovecot -F
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
#!/bin/bash
|
||||
[ -d /var/vmail/_garbage/ ] && /usr/bin/find /var/vmail/_garbage/ -mindepth 1 -maxdepth 1 -type d -cmin +${MAILDIR_GC_TIME} -exec rm -r {} \;
|
||||
@@ -14,6 +14,11 @@ import sys
|
||||
import html2text
|
||||
from subprocess import Popen, PIPE, STDOUT
|
||||
|
||||
|
||||
# Don't run if role is not master
|
||||
if os.getenv("MASTER").lower() in ["n", "no"]:
|
||||
sys.exit()
|
||||
|
||||
if len(sys.argv) > 2:
|
||||
percent = int(sys.argv[1])
|
||||
username = str(sys.argv[2])
|
||||
|
||||
@@ -11,8 +11,8 @@ stderr_logfile=/dev/stderr
|
||||
stderr_logfile_maxbytes=0
|
||||
autostart=true
|
||||
|
||||
[program:dovecot]
|
||||
command=/usr/sbin/dovecot -F
|
||||
[program:bootstrap]
|
||||
command=/docker-entrypoint.sh
|
||||
stdout_logfile=/dev/stdout
|
||||
stdout_logfile_maxbytes=0
|
||||
stderr_logfile=/dev/stderr
|
||||
|
||||
28
data/Dockerfiles/mariadb/Dockerfile
Normal file
28
data/Dockerfiles/mariadb/Dockerfile
Normal file
@@ -0,0 +1,28 @@
|
||||
FROM mariadb:10.11
|
||||
|
||||
LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
|
||||
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends \
|
||||
python3 \
|
||||
python3-pip \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN pip install \
|
||||
mysql-connector-python \
|
||||
jinja2 \
|
||||
redis \
|
||||
dnspython \
|
||||
psutil
|
||||
|
||||
|
||||
COPY data/Dockerfiles/bootstrap /bootstrap
|
||||
COPY data/Dockerfiles/mariadb/docker-entrypoint.sh /docker-entrypoint.sh
|
||||
|
||||
RUN chmod +x /docker-entrypoint.sh
|
||||
|
||||
|
||||
ENTRYPOINT ["/docker-entrypoint.sh"]
|
||||
CMD ["mysqld"]
|
||||
20
data/Dockerfiles/mariadb/docker-entrypoint.sh
Normal file
20
data/Dockerfiles/mariadb/docker-entrypoint.sh
Normal file
@@ -0,0 +1,20 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Run hooks
|
||||
for file in /hooks/*; do
|
||||
if [ -x "${file}" ]; then
|
||||
echo "Running hook ${file}"
|
||||
"${file}"
|
||||
fi
|
||||
done
|
||||
|
||||
python3 -u /bootstrap/main.py
|
||||
BOOTSTRAP_EXIT_CODE=$?
|
||||
|
||||
if [ $BOOTSTRAP_EXIT_CODE -ne 0 ]; then
|
||||
echo "Bootstrap failed with exit code $BOOTSTRAP_EXIT_CODE. Not starting MariaDB."
|
||||
exit $BOOTSTRAP_EXIT_CODE
|
||||
fi
|
||||
|
||||
echo "Bootstrap succeeded. Starting MariaDB..."
|
||||
exec /usr/local/bin/docker-entrypoint.sh "$@"
|
||||
@@ -4,15 +4,33 @@ LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>"
|
||||
ENV PIP_BREAK_SYSTEM_PACKAGES=1
|
||||
|
||||
RUN apk add --no-cache nginx \
|
||||
python3 \
|
||||
py3-pip && \
|
||||
pip install --upgrade pip && \
|
||||
pip install Jinja2
|
||||
python3 py3-pip \
|
||||
supervisor
|
||||
|
||||
RUN apk add --no-cache --virtual .build-deps \
|
||||
gcc \
|
||||
musl-dev \
|
||||
python3-dev \
|
||||
linux-headers \
|
||||
&& pip install --break-system-packages psutil \
|
||||
&& apk del .build-deps
|
||||
|
||||
RUN pip install --break-system-packages \
|
||||
mysql-connector-python \
|
||||
jinja2 \
|
||||
redis \
|
||||
dnspython
|
||||
|
||||
RUN mkdir -p /etc/nginx/includes
|
||||
|
||||
COPY ./bootstrap.py /
|
||||
COPY ./docker-entrypoint.sh /
|
||||
|
||||
ENTRYPOINT ["/docker-entrypoint.sh"]
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
COPY data/Dockerfiles/bootstrap /bootstrap
|
||||
COPY data/Dockerfiles/nginx/docker-entrypoint.sh /
|
||||
COPY data/Dockerfiles/nginx/supervisord.conf /etc/supervisor/supervisord.conf
|
||||
COPY data/Dockerfiles/nginx/stop-supervisor.sh /usr/local/sbin/stop-supervisor.sh
|
||||
|
||||
RUN chmod +x /docker-entrypoint.sh
|
||||
RUN chmod +x /usr/local/sbin/stop-supervisor.sh
|
||||
|
||||
|
||||
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/supervisord.conf"]
|
||||
|
||||
@@ -1,100 +0,0 @@
|
||||
import os
|
||||
import subprocess
|
||||
from jinja2 import Environment, FileSystemLoader
|
||||
|
||||
def includes_conf(env, template_vars):
|
||||
server_name = "server_name.active"
|
||||
listen_plain = "listen_plain.active"
|
||||
listen_ssl = "listen_ssl.active"
|
||||
|
||||
server_name_config = f"server_name {template_vars['MAILCOW_HOSTNAME']} autodiscover.* autoconfig.* {' '.join(template_vars['ADDITIONAL_SERVER_NAMES'])};"
|
||||
listen_plain_config = f"listen {template_vars['HTTP_PORT']};"
|
||||
listen_ssl_config = f"listen {template_vars['HTTPS_PORT']};"
|
||||
if not template_vars['DISABLE_IPv6']:
|
||||
listen_plain_config += f"\nlisten [::]:{template_vars['HTTP_PORT']};"
|
||||
listen_ssl_config += f"\nlisten [::]:{template_vars['HTTPS_PORT']} ssl;"
|
||||
listen_ssl_config += "\nhttp2 on;"
|
||||
|
||||
with open(f"/etc/nginx/conf.d/{server_name}", "w") as f:
|
||||
f.write(server_name_config)
|
||||
|
||||
with open(f"/etc/nginx/conf.d/{listen_plain}", "w") as f:
|
||||
f.write(listen_plain_config)
|
||||
|
||||
with open(f"/etc/nginx/conf.d/{listen_ssl}", "w") as f:
|
||||
f.write(listen_ssl_config)
|
||||
|
||||
def sites_default_conf(env, template_vars):
|
||||
config_name = "sites-default.conf"
|
||||
template = env.get_template(f"{config_name}.j2")
|
||||
config = template.render(template_vars)
|
||||
|
||||
with open(f"/etc/nginx/includes/{config_name}", "w") as f:
|
||||
f.write(config)
|
||||
|
||||
def nginx_conf(env, template_vars):
|
||||
config_name = "nginx.conf"
|
||||
template = env.get_template(f"{config_name}.j2")
|
||||
config = template.render(template_vars)
|
||||
|
||||
with open(f"/etc/nginx/{config_name}", "w") as f:
|
||||
f.write(config)
|
||||
|
||||
def prepare_template_vars():
|
||||
ipv4_network = os.getenv("IPV4_NETWORK", "172.22.1")
|
||||
additional_server_names = os.getenv("ADDITIONAL_SERVER_NAMES", "")
|
||||
trusted_proxies = os.getenv("TRUSTED_PROXIES", "")
|
||||
|
||||
template_vars = {
|
||||
'IPV4_NETWORK': ipv4_network,
|
||||
'TRUSTED_PROXIES': [item.strip() for item in trusted_proxies.split(",") if item.strip()],
|
||||
'SKIP_RSPAMD': os.getenv("SKIP_RSPAMD", "n").lower() in ("y", "yes"),
|
||||
'SKIP_SOGO': os.getenv("SKIP_SOGO", "n").lower() in ("y", "yes"),
|
||||
'NGINX_USE_PROXY_PROTOCOL': os.getenv("NGINX_USE_PROXY_PROTOCOL", "n").lower() in ("y", "yes"),
|
||||
'MAILCOW_HOSTNAME': os.getenv("MAILCOW_HOSTNAME", ""),
|
||||
'ADDITIONAL_SERVER_NAMES': [item.strip() for item in additional_server_names.split(",") if item.strip()],
|
||||
'HTTP_PORT': os.getenv("HTTP_PORT", "80"),
|
||||
'HTTPS_PORT': os.getenv("HTTPS_PORT", "443"),
|
||||
'SOGOHOST': os.getenv("SOGOHOST", ipv4_network + ".248"),
|
||||
'RSPAMDHOST': os.getenv("RSPAMDHOST", "rspamd-mailcow"),
|
||||
'PHPFPMHOST': os.getenv("PHPFPMHOST", "php-fpm-mailcow"),
|
||||
'DISABLE_IPv6': os.getenv("DISABLE_IPv6", "n").lower() in ("y", "yes"),
|
||||
'HTTP_REDIRECT': os.getenv("HTTP_REDIRECT", "n").lower() in ("y", "yes"),
|
||||
}
|
||||
|
||||
ssl_dir = '/etc/ssl/mail/'
|
||||
template_vars['valid_cert_dirs'] = []
|
||||
for d in os.listdir(ssl_dir):
|
||||
full_path = os.path.join(ssl_dir, d)
|
||||
if not os.path.isdir(full_path):
|
||||
continue
|
||||
|
||||
cert_path = os.path.join(full_path, 'cert.pem')
|
||||
key_path = os.path.join(full_path, 'key.pem')
|
||||
domains_path = os.path.join(full_path, 'domains')
|
||||
|
||||
if os.path.isfile(cert_path) and os.path.isfile(key_path) and os.path.isfile(domains_path):
|
||||
with open(domains_path, 'r') as file:
|
||||
domains = file.read().strip()
|
||||
domains_list = domains.split()
|
||||
if domains_list and template_vars["MAILCOW_HOSTNAME"] not in domains_list:
|
||||
template_vars['valid_cert_dirs'].append({
|
||||
'cert_path': full_path + '/',
|
||||
'domains': domains
|
||||
})
|
||||
|
||||
return template_vars
|
||||
|
||||
def main():
|
||||
env = Environment(loader=FileSystemLoader('./etc/nginx/conf.d/templates'))
|
||||
|
||||
# Render config
|
||||
print("Render config")
|
||||
template_vars = prepare_template_vars()
|
||||
sites_default_conf(env, template_vars)
|
||||
nginx_conf(env, template_vars)
|
||||
includes_conf(env, template_vars)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,26 +1,20 @@
|
||||
#!/bin/sh
|
||||
|
||||
PHPFPMHOST=${PHPFPMHOST:-"php-fpm-mailcow"}
|
||||
SOGOHOST=${SOGOHOST:-"$IPV4_NETWORK.248"}
|
||||
RSPAMDHOST=${RSPAMDHOST:-"rspamd-mailcow"}
|
||||
|
||||
until ping ${PHPFPMHOST} -c1 > /dev/null; do
|
||||
echo "Waiting for PHP..."
|
||||
sleep 1
|
||||
# Run hooks
|
||||
for file in /hooks/*; do
|
||||
if [ -x "${file}" ]; then
|
||||
echo "Running hook ${file}"
|
||||
"${file}"
|
||||
fi
|
||||
done
|
||||
if ! printf "%s\n" "${SKIP_SOGO}" | grep -E '^([yY][eE][sS]|[yY])+$' >/dev/null; then
|
||||
until ping ${SOGOHOST} -c1 > /dev/null; do
|
||||
echo "Waiting for SOGo..."
|
||||
sleep 1
|
||||
done
|
||||
fi
|
||||
if ! printf "%s\n" "${SKIP_RSPAMD}" | grep -E '^([yY][eE][sS]|[yY])+$' >/dev/null; then
|
||||
until ping ${RSPAMDHOST} -c1 > /dev/null; do
|
||||
echo "Waiting for Rspamd..."
|
||||
sleep 1
|
||||
done
|
||||
|
||||
python3 -u /bootstrap/main.py
|
||||
BOOTSTRAP_EXIT_CODE=$?
|
||||
|
||||
if [ $BOOTSTRAP_EXIT_CODE -ne 0 ]; then
|
||||
echo "Bootstrap failed with exit code $BOOTSTRAP_EXIT_CODE. Not starting Nginx."
|
||||
exit $BOOTSTRAP_EXIT_CODE
|
||||
fi
|
||||
|
||||
python3 /bootstrap.py
|
||||
|
||||
exec "$@"
|
||||
echo "Bootstrap succeeded. Starting Nginx..."
|
||||
nginx -g "daemon off;"
|
||||
|
||||
8
data/Dockerfiles/nginx/stop-supervisor.sh
Executable file
8
data/Dockerfiles/nginx/stop-supervisor.sh
Executable file
@@ -0,0 +1,8 @@
|
||||
#!/bin/bash
|
||||
|
||||
printf "READY\n";
|
||||
|
||||
while read line; do
|
||||
echo "Processing Event: $line" >&2;
|
||||
kill -3 $(cat "/var/run/supervisord.pid")
|
||||
done < /dev/stdin
|
||||
27
data/Dockerfiles/nginx/supervisord.conf
Normal file
27
data/Dockerfiles/nginx/supervisord.conf
Normal file
@@ -0,0 +1,27 @@
|
||||
[supervisord]
|
||||
nodaemon=true
|
||||
user=root
|
||||
|
||||
[program:syslog-ng]
|
||||
command=/usr/sbin/syslog-ng --foreground --no-caps
|
||||
stdout_logfile=/dev/stdout
|
||||
stdout_logfile_maxbytes=0
|
||||
stderr_logfile=/dev/stderr
|
||||
stderr_logfile_maxbytes=0
|
||||
autostart=true
|
||||
priority=1
|
||||
|
||||
[program:bootstrap]
|
||||
command=/docker-entrypoint.sh
|
||||
stdout_logfile=/dev/stdout
|
||||
stdout_logfile_maxbytes=0
|
||||
stderr_logfile=/dev/stderr
|
||||
stderr_logfile_maxbytes=0
|
||||
priority=2
|
||||
startretries=10
|
||||
autorestart=true
|
||||
stopwaitsecs=120
|
||||
|
||||
[eventlistener:processes]
|
||||
command=/usr/local/sbin/stop-supervisor.sh
|
||||
events=PROCESS_STATE_STOPPED, PROCESS_STATE_EXITED, PROCESS_STATE_FATAL
|
||||
@@ -63,6 +63,7 @@ RUN apk add -U --no-cache autoconf \
|
||||
samba-client \
|
||||
zlib-dev \
|
||||
tzdata \
|
||||
python3 py3-pip \
|
||||
&& pecl install APCu-${APCU_PECL_VERSION} \
|
||||
&& pecl install imagick-${IMAGICK_PECL_VERSION} \
|
||||
&& pecl install mailparse-${MAILPARSE_PECL_VERSION} \
|
||||
@@ -72,7 +73,7 @@ RUN apk add -U --no-cache autoconf \
|
||||
&& pecl clear-cache \
|
||||
&& docker-php-ext-configure intl \
|
||||
&& docker-php-ext-configure exif \
|
||||
&& docker-php-ext-configure gd --with-freetype=/usr/include/ \
|
||||
&& docker-php-ext-configure gd --with-freetype=/usr/include/ \
|
||||
--with-jpeg=/usr/include/ \
|
||||
--with-webp \
|
||||
--with-xpm \
|
||||
@@ -107,8 +108,26 @@ RUN apk add -U --no-cache autoconf \
|
||||
pcre-dev \
|
||||
zlib-dev
|
||||
|
||||
COPY ./docker-entrypoint.sh /
|
||||
RUN apk add --no-cache --virtual .build-deps \
|
||||
gcc \
|
||||
musl-dev \
|
||||
python3-dev \
|
||||
linux-headers \
|
||||
&& pip install --break-system-packages psutil \
|
||||
&& apk del .build-deps
|
||||
|
||||
RUN pip install --break-system-packages \
|
||||
mysql-connector-python \
|
||||
jinja2 \
|
||||
redis \
|
||||
dnspython
|
||||
|
||||
|
||||
COPY data/Dockerfiles/bootstrap /bootstrap
|
||||
COPY data/Dockerfiles/phpfpm/docker-entrypoint.sh /
|
||||
|
||||
RUN chmod +x /docker-entrypoint.sh
|
||||
|
||||
|
||||
ENTRYPOINT ["/docker-entrypoint.sh"]
|
||||
|
||||
CMD ["php-fpm"]
|
||||
|
||||
@@ -1,219 +1,5 @@
|
||||
#!/bin/bash
|
||||
|
||||
function array_by_comma { local IFS=","; echo "$*"; }
|
||||
|
||||
# Wait for containers
|
||||
while ! mariadb-admin status --ssl=false --socket=/var/run/mysqld/mysqld.sock -u${DBUSER} -p${DBPASS} --silent; do
|
||||
echo "Waiting for SQL..."
|
||||
sleep 2
|
||||
done
|
||||
|
||||
# Do not attempt to write to slave
|
||||
if [[ ! -z ${REDIS_SLAVEOF_IP} ]]; then
|
||||
REDIS_HOST=$REDIS_SLAVEOF_IP
|
||||
REDIS_PORT=$REDIS_SLAVEOF_PORT
|
||||
else
|
||||
REDIS_HOST="redis"
|
||||
REDIS_PORT="6379"
|
||||
fi
|
||||
REDIS_CMDLINE="redis-cli -h ${REDIS_HOST} -p ${REDIS_PORT} -a ${REDISPASS} --no-auth-warning"
|
||||
|
||||
until [[ $(${REDIS_CMDLINE} PING) == "PONG" ]]; do
|
||||
echo "Waiting for Redis..."
|
||||
sleep 2
|
||||
done
|
||||
|
||||
# Set redis session store
|
||||
echo -n '
|
||||
session.save_handler = redis
|
||||
session.save_path = "tcp://'${REDIS_HOST}':'${REDIS_PORT}'?auth='${REDISPASS}'"
|
||||
' > /usr/local/etc/php/conf.d/session_store.ini
|
||||
|
||||
# Check mysql_upgrade (master and slave)
|
||||
CONTAINER_ID=
|
||||
until [[ ! -z "${CONTAINER_ID}" ]] && [[ "${CONTAINER_ID}" =~ ^[[:alnum:]]*$ ]]; do
|
||||
CONTAINER_ID=$(curl --silent --insecure https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/json | jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], project: .Config.Labels[\"com.docker.compose.project\"], id: .Id}" 2> /dev/null | jq -rc "select( .name | tostring | contains(\"mysql-mailcow\")) | select( .project | tostring | contains(\"${COMPOSE_PROJECT_NAME,,}\")) | .id" 2> /dev/null)
|
||||
echo "Could not get mysql-mailcow container id... trying again"
|
||||
sleep 2
|
||||
done
|
||||
echo "MySQL @ ${CONTAINER_ID}"
|
||||
SQL_LOOP_C=0
|
||||
SQL_CHANGED=0
|
||||
until [[ ${SQL_UPGRADE_STATUS} == 'success' ]]; do
|
||||
if [ ${SQL_LOOP_C} -gt 4 ]; then
|
||||
echo "Tried to upgrade MySQL and failed, giving up after ${SQL_LOOP_C} retries and starting container (oops, not good)"
|
||||
break
|
||||
fi
|
||||
SQL_FULL_UPGRADE_RETURN=$(curl --silent --insecure -XPOST https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${CONTAINER_ID}/exec -d '{"cmd":"system", "task":"mysql_upgrade"}' --silent -H 'Content-type: application/json')
|
||||
SQL_UPGRADE_STATUS=$(echo ${SQL_FULL_UPGRADE_RETURN} | jq -r .type)
|
||||
SQL_LOOP_C=$((SQL_LOOP_C+1))
|
||||
echo "SQL upgrade iteration #${SQL_LOOP_C}"
|
||||
if [[ ${SQL_UPGRADE_STATUS} == 'warning' ]]; then
|
||||
SQL_CHANGED=1
|
||||
echo "MySQL applied an upgrade, debug output:"
|
||||
echo ${SQL_FULL_UPGRADE_RETURN}
|
||||
sleep 3
|
||||
while ! mariadb-admin status --ssl=false --socket=/var/run/mysqld/mysqld.sock -u${DBUSER} -p${DBPASS} --silent; do
|
||||
echo "Waiting for SQL to return, please wait"
|
||||
sleep 2
|
||||
done
|
||||
continue
|
||||
elif [[ ${SQL_UPGRADE_STATUS} == 'success' ]]; then
|
||||
echo "MySQL is up-to-date - debug output:"
|
||||
echo ${SQL_FULL_UPGRADE_RETURN}
|
||||
else
|
||||
echo "No valid reponse for mysql_upgrade was received, debug output:"
|
||||
echo ${SQL_FULL_UPGRADE_RETURN}
|
||||
fi
|
||||
done
|
||||
|
||||
# doing post-installation stuff, if SQL was upgraded (master and slave)
|
||||
if [ ${SQL_CHANGED} -eq 1 ]; then
|
||||
POSTFIX=$(curl --silent --insecure https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/json | jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], project: .Config.Labels[\"com.docker.compose.project\"], id: .Id}" 2> /dev/null | jq -rc "select( .name | tostring | contains(\"postfix-mailcow\")) | select( .project | tostring | contains(\"${COMPOSE_PROJECT_NAME,,}\")) | .id" 2> /dev/null)
|
||||
if [[ -z "${POSTFIX}" ]] || ! [[ "${POSTFIX}" =~ ^[[:alnum:]]*$ ]]; then
|
||||
echo "Could not determine Postfix container ID, skipping Postfix restart."
|
||||
else
|
||||
echo "Restarting Postfix"
|
||||
curl -X POST --silent --insecure https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${POSTFIX}/restart | jq -r '.msg'
|
||||
echo "Sleeping 5 seconds..."
|
||||
sleep 5
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check mysql tz import (master and slave)
|
||||
TZ_CHECK=$(mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT CONVERT_TZ('2019-11-02 23:33:00','Europe/Berlin','UTC') AS time;" -BN 2> /dev/null)
|
||||
if [[ -z ${TZ_CHECK} ]] || [[ "${TZ_CHECK}" == "NULL" ]]; then
|
||||
SQL_FULL_TZINFO_IMPORT_RETURN=$(curl --silent --insecure -XPOST https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${CONTAINER_ID}/exec -d '{"cmd":"system", "task":"mysql_tzinfo_to_sql"}' --silent -H 'Content-type: application/json')
|
||||
echo "MySQL mysql_tzinfo_to_sql - debug output:"
|
||||
echo ${SQL_FULL_TZINFO_IMPORT_RETURN}
|
||||
fi
|
||||
|
||||
if [[ "${MASTER}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
|
||||
echo "We are master, preparing..."
|
||||
# Set a default release format
|
||||
if [[ -z $(${REDIS_CMDLINE} --raw GET Q_RELEASE_FORMAT) ]]; then
|
||||
${REDIS_CMDLINE} --raw SET Q_RELEASE_FORMAT raw
|
||||
fi
|
||||
|
||||
# Set max age of q items - if unset
|
||||
if [[ -z $(${REDIS_CMDLINE} --raw GET Q_MAX_AGE) ]]; then
|
||||
${REDIS_CMDLINE} --raw SET Q_MAX_AGE 365
|
||||
fi
|
||||
|
||||
# Set default password policy - if unset
|
||||
if [[ -z $(${REDIS_CMDLINE} --raw HGET PASSWD_POLICY length) ]]; then
|
||||
${REDIS_CMDLINE} --raw HSET PASSWD_POLICY length 6
|
||||
${REDIS_CMDLINE} --raw HSET PASSWD_POLICY chars 0
|
||||
${REDIS_CMDLINE} --raw HSET PASSWD_POLICY special_chars 0
|
||||
${REDIS_CMDLINE} --raw HSET PASSWD_POLICY lowerupper 0
|
||||
${REDIS_CMDLINE} --raw HSET PASSWD_POLICY numbers 0
|
||||
fi
|
||||
|
||||
# Trigger db init
|
||||
echo "Running DB init..."
|
||||
php -c /usr/local/etc/php -f /web/inc/init_db.inc.php
|
||||
|
||||
# Recreating domain map
|
||||
echo "Rebuilding domain map in Redis..."
|
||||
declare -a DOMAIN_ARR
|
||||
${REDIS_CMDLINE} DEL DOMAIN_MAP > /dev/null
|
||||
while read line
|
||||
do
|
||||
DOMAIN_ARR+=("$line")
|
||||
done < <(mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT domain FROM domain" -Bs)
|
||||
while read line
|
||||
do
|
||||
DOMAIN_ARR+=("$line")
|
||||
done < <(mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT alias_domain FROM alias_domain" -Bs)
|
||||
|
||||
if [[ ! -z ${DOMAIN_ARR} ]]; then
|
||||
for domain in "${DOMAIN_ARR[@]}"; do
|
||||
${REDIS_CMDLINE} HSET DOMAIN_MAP ${domain} 1 > /dev/null
|
||||
done
|
||||
fi
|
||||
|
||||
# Set API options if env vars are not empty
|
||||
if [[ ${API_ALLOW_FROM} != "invalid" ]] && [[ ! -z ${API_ALLOW_FROM} ]]; then
|
||||
IFS=',' read -r -a API_ALLOW_FROM_ARR <<< "${API_ALLOW_FROM}"
|
||||
declare -a VALIDATED_API_ALLOW_FROM_ARR
|
||||
REGEX_IP6='^([0-9a-fA-F]{0,4}:){1,7}[0-9a-fA-F]{0,4}(/([0-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8]))?$'
|
||||
REGEX_IP4='^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+(/([0-9]|[1-2][0-9]|3[0-2]))?$'
|
||||
for IP in "${API_ALLOW_FROM_ARR[@]}"; do
|
||||
if [[ ${IP} =~ ${REGEX_IP6} ]] || [[ ${IP} =~ ${REGEX_IP4} ]]; then
|
||||
VALIDATED_API_ALLOW_FROM_ARR+=("${IP}")
|
||||
fi
|
||||
done
|
||||
VALIDATED_IPS=$(array_by_comma ${VALIDATED_API_ALLOW_FROM_ARR[*]})
|
||||
if [[ ! -z ${VALIDATED_IPS} ]]; then
|
||||
if [[ ${API_KEY} != "invalid" ]] && [[ ! -z ${API_KEY} ]]; then
|
||||
mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} << EOF
|
||||
DELETE FROM api WHERE access = 'rw';
|
||||
INSERT INTO api (api_key, active, allow_from, access) VALUES ("${API_KEY}", "1", "${VALIDATED_IPS}", "rw");
|
||||
EOF
|
||||
fi
|
||||
if [[ ${API_KEY_READ_ONLY} != "invalid" ]] && [[ ! -z ${API_KEY_READ_ONLY} ]]; then
|
||||
mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} << EOF
|
||||
DELETE FROM api WHERE access = 'ro';
|
||||
INSERT INTO api (api_key, active, allow_from, access) VALUES ("${API_KEY_READ_ONLY}", "1", "${VALIDATED_IPS}", "ro");
|
||||
EOF
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# Create events (master only, STATUS for event on slave will be SLAVESIDE_DISABLED)
|
||||
mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} << EOF
|
||||
DROP EVENT IF EXISTS clean_spamalias;
|
||||
DELIMITER //
|
||||
CREATE EVENT clean_spamalias
|
||||
ON SCHEDULE EVERY 1 DAY DO
|
||||
BEGIN
|
||||
DELETE FROM spamalias WHERE validity < UNIX_TIMESTAMP();
|
||||
END;
|
||||
//
|
||||
DELIMITER ;
|
||||
DROP EVENT IF EXISTS clean_oauth2;
|
||||
DELIMITER //
|
||||
CREATE EVENT clean_oauth2
|
||||
ON SCHEDULE EVERY 1 DAY DO
|
||||
BEGIN
|
||||
DELETE FROM oauth_refresh_tokens WHERE expires < NOW();
|
||||
DELETE FROM oauth_access_tokens WHERE expires < NOW();
|
||||
DELETE FROM oauth_authorization_codes WHERE expires < NOW();
|
||||
END;
|
||||
//
|
||||
DELIMITER ;
|
||||
DROP EVENT IF EXISTS clean_sasl_log;
|
||||
DELIMITER //
|
||||
CREATE EVENT clean_sasl_log
|
||||
ON SCHEDULE EVERY 1 DAY DO
|
||||
BEGIN
|
||||
DELETE sasl_log.* FROM sasl_log
|
||||
LEFT JOIN (
|
||||
SELECT username, service, MAX(datetime) AS lastdate
|
||||
FROM sasl_log
|
||||
GROUP BY username, service
|
||||
) AS last ON sasl_log.username = last.username AND sasl_log.service = last.service
|
||||
WHERE datetime < DATE_SUB(NOW(), INTERVAL 31 DAY) AND datetime < lastdate;
|
||||
DELETE FROM sasl_log
|
||||
WHERE username NOT IN (SELECT username FROM mailbox) AND
|
||||
datetime < DATE_SUB(NOW(), INTERVAL 31 DAY);
|
||||
END;
|
||||
//
|
||||
DELIMITER ;
|
||||
EOF
|
||||
fi
|
||||
|
||||
# Create dummy for custom overrides of mailcow style
|
||||
[[ ! -f /web/css/build/0081-custom-mailcow.css ]] && echo '/* Autogenerated by mailcow */' > /web/css/build/0081-custom-mailcow.css
|
||||
|
||||
# Fix permissions for global filters
|
||||
chown -R 82:82 /global_sieve/*
|
||||
|
||||
# Fix permissions on twig cache folder
|
||||
chown -R 82:82 /web/templates/cache
|
||||
# Clear cache
|
||||
find /web/templates/cache/* -not -name '.gitkeep' -delete
|
||||
|
||||
# Run hooks
|
||||
for file in /hooks/*; do
|
||||
if [ -x "${file}" ]; then
|
||||
@@ -222,4 +8,13 @@ for file in /hooks/*; do
|
||||
fi
|
||||
done
|
||||
|
||||
python3 -u /bootstrap/main.py
|
||||
BOOTSTRAP_EXIT_CODE=$?
|
||||
|
||||
if [ $BOOTSTRAP_EXIT_CODE -ne 0 ]; then
|
||||
echo "Bootstrap failed with exit code $BOOTSTRAP_EXIT_CODE. Not starting PHP-FPM."
|
||||
exit $BOOTSTRAP_EXIT_CODE
|
||||
fi
|
||||
|
||||
echo "Bootstrap succeeded. Starting PHP-FPM..."
|
||||
exec "$@"
|
||||
|
||||
@@ -34,23 +34,31 @@ RUN groupadd -g 102 postfix \
|
||||
syslog-ng-core \
|
||||
syslog-ng-mod-redis \
|
||||
tzdata \
|
||||
python3 python3-pip \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& touch /etc/default/locale \
|
||||
&& printf '#!/bin/bash\n/usr/sbin/postconf -c /opt/postfix/conf "$@"' > /usr/local/sbin/postconf \
|
||||
&& chmod +x /usr/local/sbin/postconf
|
||||
|
||||
COPY supervisord.conf /etc/supervisor/supervisord.conf
|
||||
COPY syslog-ng.conf /etc/syslog-ng/syslog-ng.conf
|
||||
COPY syslog-ng-redis_slave.conf /etc/syslog-ng/syslog-ng-redis_slave.conf
|
||||
COPY postfix.sh /opt/postfix.sh
|
||||
COPY rspamd-pipe-ham /usr/local/bin/rspamd-pipe-ham
|
||||
COPY rspamd-pipe-spam /usr/local/bin/rspamd-pipe-spam
|
||||
COPY whitelist_forwardinghosts.sh /usr/local/bin/whitelist_forwardinghosts.sh
|
||||
COPY stop-supervisor.sh /usr/local/sbin/stop-supervisor.sh
|
||||
COPY docker-entrypoint.sh /docker-entrypoint.sh
|
||||
RUN pip install --break-system-packages \
|
||||
mysql-connector-python \
|
||||
jinja2 \
|
||||
redis \
|
||||
dnspython \
|
||||
psutil
|
||||
|
||||
RUN chmod +x /opt/postfix.sh \
|
||||
/usr/local/bin/rspamd-pipe-ham \
|
||||
COPY data/Dockerfiles/bootstrap /bootstrap
|
||||
COPY data/Dockerfiles/postfix/supervisord.conf /etc/supervisor/supervisord.conf
|
||||
COPY data/Dockerfiles/postfix/syslog-ng.conf /etc/syslog-ng/syslog-ng.conf
|
||||
COPY data/Dockerfiles/postfix/syslog-ng-redis_slave.conf /etc/syslog-ng/syslog-ng-redis_slave.conf
|
||||
COPY data/Dockerfiles/postfix/rspamd-pipe-ham /usr/local/bin/rspamd-pipe-ham
|
||||
COPY data/Dockerfiles/postfix/rspamd-pipe-spam /usr/local/bin/rspamd-pipe-spam
|
||||
COPY data/Dockerfiles/postfix/whitelist_forwardinghosts.sh /usr/local/bin/whitelist_forwardinghosts.sh
|
||||
COPY data/Dockerfiles/postfix/stop-supervisor.sh /usr/local/sbin/stop-supervisor.sh
|
||||
COPY data/Dockerfiles/postfix/docker-entrypoint.sh /docker-entrypoint.sh
|
||||
|
||||
RUN chmod +x /usr/local/bin/rspamd-pipe-ham \
|
||||
/docker-entrypoint.sh \
|
||||
/usr/local/bin/rspamd-pipe-spam \
|
||||
/usr/local/bin/whitelist_forwardinghosts.sh \
|
||||
/usr/local/sbin/stop-supervisor.sh
|
||||
@@ -58,6 +66,5 @@ RUN rm -rf /tmp/* /var/tmp/*
|
||||
|
||||
EXPOSE 588
|
||||
|
||||
ENTRYPOINT ["/docker-entrypoint.sh"]
|
||||
|
||||
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/supervisord.conf"]
|
||||
|
||||
@@ -8,8 +8,12 @@ for file in /hooks/*; do
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ ! -z ${REDIS_SLAVEOF_IP} ]]; then
|
||||
cp /etc/syslog-ng/syslog-ng-redis_slave.conf /etc/syslog-ng/syslog-ng.conf
|
||||
python3 -u /bootstrap/main.py
|
||||
BOOTSTRAP_EXIT_CODE=$?
|
||||
|
||||
if [ $BOOTSTRAP_EXIT_CODE -ne 0 ]; then
|
||||
echo "Bootstrap failed with exit code $BOOTSTRAP_EXIT_CODE. Not starting Postfix."
|
||||
exit $BOOTSTRAP_EXIT_CODE
|
||||
fi
|
||||
|
||||
# Fix OpenSSL 3.X TLS1.0, 1.1 support (https://community.mailcow.email/d/4062-hi-all/20)
|
||||
@@ -21,6 +25,16 @@ if grep -qE '\!SSLv2|\!SSLv3|>=TLSv1(\.[0-1])?$' /opt/postfix/conf/main.cf /opt/
|
||||
echo "[tls_system_default]" >> /etc/ssl/openssl.cnf
|
||||
echo "MinProtocol = TLSv1" >> /etc/ssl/openssl.cnf
|
||||
echo "CipherString = DEFAULT@SECLEVEL=0" >> /etc/ssl/openssl.cnf
|
||||
fi
|
||||
fi
|
||||
|
||||
exec "$@"
|
||||
|
||||
# Start Postfix
|
||||
postconf -c /opt/postfix/conf > /dev/null
|
||||
if [[ $? != 0 ]]; then
|
||||
echo "Postfix configuration error, refusing to start."
|
||||
exit 1
|
||||
else
|
||||
echo "Bootstrap succeeded. Starting Postfix..."
|
||||
postfix -c /opt/postfix/conf start
|
||||
sleep 126144000
|
||||
fi
|
||||
|
||||
@@ -1,527 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
trap "postfix stop" EXIT
|
||||
|
||||
[[ ! -d /opt/postfix/conf/sql/ ]] && mkdir -p /opt/postfix/conf/sql/
|
||||
|
||||
# Wait for MySQL to warm-up
|
||||
while ! mariadb-admin status --ssl=false --socket=/var/run/mysqld/mysqld.sock -u${DBUSER} -p${DBPASS} --silent; do
|
||||
echo "Waiting for database to come up..."
|
||||
sleep 2
|
||||
done
|
||||
|
||||
until dig +short mailcow.email > /dev/null; do
|
||||
echo "Waiting for DNS..."
|
||||
sleep 1
|
||||
done
|
||||
|
||||
cat <<EOF > /etc/aliases
|
||||
# Autogenerated by mailcow
|
||||
null: /dev/null
|
||||
watchdog: /dev/null
|
||||
ham: "|/usr/local/bin/rspamd-pipe-ham"
|
||||
spam: "|/usr/local/bin/rspamd-pipe-spam"
|
||||
EOF
|
||||
newaliases;
|
||||
|
||||
# create sni configuration
|
||||
if [[ "${SKIP_LETS_ENCRYPT}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
|
||||
echo -n "" > /opt/postfix/conf/sni.map
|
||||
else
|
||||
echo -n "" > /opt/postfix/conf/sni.map;
|
||||
for cert_dir in /etc/ssl/mail/*/ ; do
|
||||
if [[ ! -f ${cert_dir}domains ]] || [[ ! -f ${cert_dir}cert.pem ]] || [[ ! -f ${cert_dir}key.pem ]]; then
|
||||
continue;
|
||||
fi
|
||||
IFS=" " read -r -a domains <<< "$(cat "${cert_dir}domains")"
|
||||
for domain in "${domains[@]}"; do
|
||||
echo -n "${domain} ${cert_dir}key.pem ${cert_dir}cert.pem" >> /opt/postfix/conf/sni.map;
|
||||
echo "" >> /opt/postfix/conf/sni.map;
|
||||
done
|
||||
done
|
||||
fi
|
||||
postmap -F hash:/opt/postfix/conf/sni.map;
|
||||
|
||||
cat <<EOF > /opt/postfix/conf/sql/mysql_relay_ne.cf
|
||||
# Autogenerated by mailcow
|
||||
user = ${DBUSER}
|
||||
password = ${DBPASS}
|
||||
hosts = unix:/var/run/mysqld/mysqld.sock
|
||||
dbname = ${DBNAME}
|
||||
query = SELECT IF(EXISTS(SELECT address, domain FROM alias
|
||||
WHERE address = '%s'
|
||||
AND domain IN (
|
||||
SELECT domain FROM domain
|
||||
WHERE backupmx = '1'
|
||||
AND relay_all_recipients = '1'
|
||||
AND relay_unknown_only = '1')
|
||||
|
||||
), 'lmtp:inet:dovecot:24', NULL) AS 'transport'
|
||||
EOF
|
||||
|
||||
cat <<EOF > /opt/postfix/conf/sql/mysql_relay_recipient_maps.cf
|
||||
# Autogenerated by mailcow
|
||||
user = ${DBUSER}
|
||||
password = ${DBPASS}
|
||||
hosts = unix:/var/run/mysqld/mysqld.sock
|
||||
dbname = ${DBNAME}
|
||||
query = SELECT DISTINCT
|
||||
CASE WHEN '%d' IN (
|
||||
SELECT domain FROM domain
|
||||
WHERE relay_all_recipients=1
|
||||
AND domain='%d'
|
||||
AND backupmx=1
|
||||
)
|
||||
THEN '%s' ELSE (
|
||||
SELECT goto FROM alias WHERE address='%s' AND active='1'
|
||||
)
|
||||
END AS result;
|
||||
EOF
|
||||
|
||||
cat <<EOF > /opt/postfix/conf/sql/mysql_tls_policy_override_maps.cf
|
||||
# Autogenerated by mailcow
|
||||
user = ${DBUSER}
|
||||
password = ${DBPASS}
|
||||
hosts = unix:/var/run/mysqld/mysqld.sock
|
||||
dbname = ${DBNAME}
|
||||
query = SELECT CONCAT(policy, ' ', parameters) AS tls_policy FROM tls_policy_override WHERE active = '1' AND dest = '%s'
|
||||
EOF
|
||||
|
||||
cat <<EOF > /opt/postfix/conf/sql/mysql_tls_enforce_in_policy.cf
|
||||
# Autogenerated by mailcow
|
||||
user = ${DBUSER}
|
||||
password = ${DBPASS}
|
||||
hosts = unix:/var/run/mysqld/mysqld.sock
|
||||
dbname = ${DBNAME}
|
||||
query = SELECT IF(EXISTS(
|
||||
SELECT 'TLS_ACTIVE' FROM alias
|
||||
LEFT OUTER JOIN mailbox ON mailbox.username = alias.goto
|
||||
WHERE (address='%s'
|
||||
OR address IN (
|
||||
SELECT CONCAT('%u', '@', target_domain) FROM alias_domain
|
||||
WHERE alias_domain='%d'
|
||||
)
|
||||
) AND JSON_UNQUOTE(JSON_VALUE(attributes, '$.tls_enforce_in')) = '1' AND mailbox.active = '1'
|
||||
), 'reject_plaintext_session', NULL) AS 'tls_enforce_in';
|
||||
EOF
|
||||
|
||||
cat <<EOF > /opt/postfix/conf/sql/mysql_sender_dependent_default_transport_maps.cf
|
||||
# Autogenerated by mailcow
|
||||
user = ${DBUSER}
|
||||
password = ${DBPASS}
|
||||
hosts = unix:/var/run/mysqld/mysqld.sock
|
||||
dbname = ${DBNAME}
|
||||
query = SELECT GROUP_CONCAT(transport SEPARATOR '') AS transport_maps
|
||||
FROM (
|
||||
SELECT IF(EXISTS(SELECT 'smtp_type' FROM alias
|
||||
LEFT OUTER JOIN mailbox ON mailbox.username = alias.goto
|
||||
WHERE (address = '%s'
|
||||
OR address IN (
|
||||
SELECT CONCAT('%u', '@', target_domain) FROM alias_domain
|
||||
WHERE alias_domain = '%d'
|
||||
)
|
||||
)
|
||||
AND JSON_UNQUOTE(JSON_VALUE(attributes, '$.tls_enforce_out')) = '1'
|
||||
AND mailbox.active = '1'
|
||||
), 'smtp_enforced_tls:', 'smtp:') AS 'transport'
|
||||
UNION ALL
|
||||
SELECT COALESCE(
|
||||
(SELECT hostname FROM relayhosts
|
||||
LEFT OUTER JOIN mailbox ON JSON_UNQUOTE(JSON_VALUE(mailbox.attributes, '$.relayhost')) = relayhosts.id
|
||||
WHERE relayhosts.active = '1'
|
||||
AND (
|
||||
mailbox.username IN (SELECT alias.goto from alias
|
||||
JOIN mailbox ON mailbox.username = alias.goto
|
||||
WHERE alias.active = '1'
|
||||
AND alias.address = '%s'
|
||||
AND alias.address NOT LIKE '@%%'
|
||||
)
|
||||
)
|
||||
),
|
||||
(SELECT hostname FROM relayhosts
|
||||
LEFT OUTER JOIN domain ON domain.relayhost = relayhosts.id
|
||||
WHERE relayhosts.active = '1'
|
||||
AND (domain.domain = '%d'
|
||||
OR domain.domain IN (
|
||||
SELECT target_domain FROM alias_domain
|
||||
WHERE alias_domain = '%d'
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
) AS transport_view;
|
||||
EOF
|
||||
|
||||
cat <<EOF > /opt/postfix/conf/sql/mysql_transport_maps.cf
|
||||
# Autogenerated by mailcow
|
||||
user = ${DBUSER}
|
||||
password = ${DBPASS}
|
||||
hosts = unix:/var/run/mysqld/mysqld.sock
|
||||
dbname = ${DBNAME}
|
||||
query = SELECT CONCAT('smtp_via_transport_maps:', nexthop) AS transport FROM transports
|
||||
WHERE active = '1'
|
||||
AND destination = '%s';
|
||||
EOF
|
||||
|
||||
cat <<EOF > /opt/postfix/conf/sql/mysql_virtual_resource_maps.cf
|
||||
# Autogenerated by mailcow
|
||||
user = ${DBUSER}
|
||||
password = ${DBPASS}
|
||||
hosts = unix:/var/run/mysqld/mysqld.sock
|
||||
dbname = ${DBNAME}
|
||||
query = SELECT 'null@localhost' FROM mailbox
|
||||
WHERE kind REGEXP 'location|thing|group' AND username = '%s';
|
||||
EOF
|
||||
|
||||
cat <<EOF > /opt/postfix/conf/sql/mysql_sasl_passwd_maps_sender_dependent.cf
|
||||
# Autogenerated by mailcow
|
||||
user = ${DBUSER}
|
||||
password = ${DBPASS}
|
||||
hosts = unix:/var/run/mysqld/mysqld.sock
|
||||
dbname = ${DBNAME}
|
||||
query = SELECT CONCAT_WS(':', username, password) AS auth_data FROM relayhosts
|
||||
WHERE id IN (
|
||||
SELECT COALESCE(
|
||||
(SELECT id FROM relayhosts
|
||||
LEFT OUTER JOIN domain ON domain.relayhost = relayhosts.id
|
||||
WHERE relayhosts.active = '1'
|
||||
AND (domain.domain = '%d'
|
||||
OR domain.domain IN (
|
||||
SELECT target_domain FROM alias_domain
|
||||
WHERE alias_domain = '%d'
|
||||
)
|
||||
)
|
||||
),
|
||||
(SELECT id FROM relayhosts
|
||||
LEFT OUTER JOIN mailbox ON JSON_UNQUOTE(JSON_VALUE(mailbox.attributes, '$.relayhost')) = relayhosts.id
|
||||
WHERE relayhosts.active = '1'
|
||||
AND (
|
||||
mailbox.username IN (
|
||||
SELECT alias.goto from alias
|
||||
JOIN mailbox ON mailbox.username = alias.goto
|
||||
WHERE alias.active = '1'
|
||||
AND alias.address = '%s'
|
||||
AND alias.address NOT LIKE '@%%'
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
AND active = '1'
|
||||
AND username != '';
|
||||
EOF
|
||||
|
||||
cat <<EOF > /opt/postfix/conf/sql/mysql_sasl_passwd_maps_transport_maps.cf
|
||||
# Autogenerated by mailcow
|
||||
user = ${DBUSER}
|
||||
password = ${DBPASS}
|
||||
hosts = unix:/var/run/mysqld/mysqld.sock
|
||||
dbname = ${DBNAME}
|
||||
query = SELECT CONCAT_WS(':', username, password) AS auth_data FROM transports
|
||||
WHERE nexthop = '%s'
|
||||
AND active = '1'
|
||||
AND username != ''
|
||||
LIMIT 1;
|
||||
EOF
|
||||
|
||||
cat <<EOF > /opt/postfix/conf/sql/mysql_virtual_alias_domain_maps.cf
|
||||
# Autogenerated by mailcow
|
||||
user = ${DBUSER}
|
||||
password = ${DBPASS}
|
||||
hosts = unix:/var/run/mysqld/mysqld.sock
|
||||
dbname = ${DBNAME}
|
||||
query = SELECT username FROM mailbox, alias_domain
|
||||
WHERE alias_domain.alias_domain = '%d'
|
||||
AND mailbox.username = CONCAT('%u', '@', alias_domain.target_domain)
|
||||
AND (mailbox.active = '1' OR mailbox.active = '2')
|
||||
AND alias_domain.active='1'
|
||||
EOF
|
||||
|
||||
cat <<EOF > /opt/postfix/conf/sql/mysql_virtual_alias_maps.cf
|
||||
# Autogenerated by mailcow
|
||||
user = ${DBUSER}
|
||||
password = ${DBPASS}
|
||||
hosts = unix:/var/run/mysqld/mysqld.sock
|
||||
dbname = ${DBNAME}
|
||||
query = SELECT goto FROM alias
|
||||
WHERE address='%s'
|
||||
AND (active='1' OR active='2');
|
||||
EOF
|
||||
|
||||
cat <<EOF > /opt/postfix/conf/sql/mysql_recipient_bcc_maps.cf
|
||||
# Autogenerated by mailcow
|
||||
user = ${DBUSER}
|
||||
password = ${DBPASS}
|
||||
hosts = unix:/var/run/mysqld/mysqld.sock
|
||||
dbname = ${DBNAME}
|
||||
query = SELECT bcc_dest FROM bcc_maps
|
||||
WHERE local_dest='%s'
|
||||
AND type='rcpt'
|
||||
AND active='1';
|
||||
EOF
|
||||
|
||||
cat <<EOF > /opt/postfix/conf/sql/mysql_sender_bcc_maps.cf
|
||||
# Autogenerated by mailcow
|
||||
user = ${DBUSER}
|
||||
password = ${DBPASS}
|
||||
hosts = unix:/var/run/mysqld/mysqld.sock
|
||||
dbname = ${DBNAME}
|
||||
query = SELECT bcc_dest FROM bcc_maps
|
||||
WHERE local_dest='%s'
|
||||
AND type='sender'
|
||||
AND active='1';
|
||||
EOF
|
||||
|
||||
cat <<EOF > /opt/postfix/conf/sql/mysql_recipient_canonical_maps.cf
|
||||
# Autogenerated by mailcow
|
||||
user = ${DBUSER}
|
||||
password = ${DBPASS}
|
||||
hosts = unix:/var/run/mysqld/mysqld.sock
|
||||
dbname = ${DBNAME}
|
||||
query = SELECT new_dest FROM recipient_maps
|
||||
WHERE old_dest='%s'
|
||||
AND active='1';
|
||||
EOF
|
||||
|
||||
cat <<EOF > /opt/postfix/conf/sql/mysql_virtual_domains_maps.cf
|
||||
# Autogenerated by mailcow
|
||||
user = ${DBUSER}
|
||||
password = ${DBPASS}
|
||||
hosts = unix:/var/run/mysqld/mysqld.sock
|
||||
dbname = ${DBNAME}
|
||||
query = SELECT alias_domain from alias_domain WHERE alias_domain='%s' AND active='1'
|
||||
UNION
|
||||
SELECT domain FROM domain
|
||||
WHERE domain='%s'
|
||||
AND active = '1'
|
||||
AND backupmx = '0'
|
||||
EOF
|
||||
|
||||
cat <<EOF > /opt/postfix/conf/sql/mysql_virtual_mailbox_maps.cf
|
||||
# Autogenerated by mailcow
|
||||
user = ${DBUSER}
|
||||
password = ${DBPASS}
|
||||
hosts = unix:/var/run/mysqld/mysqld.sock
|
||||
dbname = ${DBNAME}
|
||||
query = SELECT CONCAT(JSON_UNQUOTE(JSON_VALUE(attributes, '$.mailbox_format')), mailbox_path_prefix, '%d/%u/') FROM mailbox WHERE username='%s' AND (active = '1' OR active = '2')
|
||||
EOF
|
||||
|
||||
cat <<EOF > /opt/postfix/conf/sql/mysql_virtual_relay_domain_maps.cf
|
||||
# Autogenerated by mailcow
|
||||
user = ${DBUSER}
|
||||
password = ${DBPASS}
|
||||
hosts = unix:/var/run/mysqld/mysqld.sock
|
||||
dbname = ${DBNAME}
|
||||
query = SELECT domain FROM domain WHERE domain='%s' AND backupmx = '1' AND active = '1'
|
||||
EOF
|
||||
|
||||
cat <<EOF > /opt/postfix/conf/sql/mysql_virtual_sender_acl.cf
|
||||
# Autogenerated by mailcow
|
||||
user = ${DBUSER}
|
||||
password = ${DBPASS}
|
||||
hosts = unix:/var/run/mysqld/mysqld.sock
|
||||
dbname = ${DBNAME}
|
||||
# First select queries domain and alias_domain to determine if domains are active.
|
||||
query = SELECT goto FROM alias
|
||||
WHERE id IN (
|
||||
SELECT COALESCE (
|
||||
(
|
||||
SELECT id FROM alias
|
||||
WHERE address='%s'
|
||||
AND (active='1' OR active='2')
|
||||
), (
|
||||
SELECT id FROM alias
|
||||
WHERE address='@%d'
|
||||
AND (active='1' OR active='2')
|
||||
)
|
||||
)
|
||||
)
|
||||
AND active='1'
|
||||
AND (domain IN
|
||||
(SELECT domain FROM domain
|
||||
WHERE domain='%d'
|
||||
AND active='1')
|
||||
OR domain in (
|
||||
SELECT alias_domain FROM alias_domain
|
||||
WHERE alias_domain='%d'
|
||||
AND active='1'
|
||||
)
|
||||
)
|
||||
UNION
|
||||
SELECT logged_in_as FROM sender_acl
|
||||
WHERE send_as='@%d'
|
||||
OR send_as='%s'
|
||||
OR send_as='*'
|
||||
OR send_as IN (
|
||||
SELECT CONCAT('@',target_domain) FROM alias_domain
|
||||
WHERE alias_domain = '%d')
|
||||
OR send_as IN (
|
||||
SELECT CONCAT('%u','@',target_domain) FROM alias_domain
|
||||
WHERE alias_domain = '%d')
|
||||
AND logged_in_as NOT IN (
|
||||
SELECT goto FROM alias
|
||||
WHERE address='%s')
|
||||
UNION
|
||||
SELECT username FROM mailbox, alias_domain
|
||||
WHERE alias_domain.alias_domain = '%d'
|
||||
AND mailbox.username = CONCAT('%u','@',alias_domain.target_domain)
|
||||
AND (mailbox.active = '1' OR mailbox.active ='2')
|
||||
AND alias_domain.active='1';
|
||||
EOF
|
||||
|
||||
# MX based routing
|
||||
cat <<EOF > /opt/postfix/conf/sql/mysql_mbr_access_maps.cf
|
||||
# Autogenerated by mailcow
|
||||
user = ${DBUSER}
|
||||
password = ${DBPASS}
|
||||
hosts = unix:/var/run/mysqld/mysqld.sock
|
||||
dbname = ${DBNAME}
|
||||
query = SELECT CONCAT('FILTER smtp_via_transport_maps:', nexthop) as transport FROM transports
|
||||
WHERE '%s' REGEXP destination
|
||||
AND active='1'
|
||||
AND is_mx_based='1';
|
||||
EOF
|
||||
|
||||
cat <<EOF > /opt/postfix/conf/sql/mysql_virtual_spamalias_maps.cf
|
||||
# Autogenerated by mailcow
|
||||
user = ${DBUSER}
|
||||
password = ${DBPASS}
|
||||
hosts = unix:/var/run/mysqld/mysqld.sock
|
||||
dbname = ${DBNAME}
|
||||
query = SELECT goto FROM spamalias
|
||||
WHERE address='%s'
|
||||
AND validity >= UNIX_TIMESTAMP()
|
||||
EOF
|
||||
|
||||
if [ ! -f /opt/postfix/conf/dns_blocklists.cf ]; then
|
||||
cat <<EOF > /opt/postfix/conf/dns_blocklists.cf
|
||||
# This file can be edited.
|
||||
# Delete this file and restart postfix container to revert any changes.
|
||||
postscreen_dnsbl_sites = wl.mailspike.net=127.0.0.[18;19;20]*-2
|
||||
hostkarma.junkemailfilter.com=127.0.0.1*-2
|
||||
list.dnswl.org=127.0.[0..255].0*-2
|
||||
list.dnswl.org=127.0.[0..255].1*-4
|
||||
list.dnswl.org=127.0.[0..255].2*-6
|
||||
list.dnswl.org=127.0.[0..255].3*-8
|
||||
bl.spamcop.net*2
|
||||
bl.suomispam.net*2
|
||||
hostkarma.junkemailfilter.com=127.0.0.2*3
|
||||
hostkarma.junkemailfilter.com=127.0.0.4*2
|
||||
hostkarma.junkemailfilter.com=127.0.1.2*1
|
||||
backscatter.spameatingmonkey.net*2
|
||||
bl.ipv6.spameatingmonkey.net*2
|
||||
bl.spameatingmonkey.net*2
|
||||
b.barracudacentral.org=127.0.0.2*7
|
||||
bl.mailspike.net=127.0.0.2*5
|
||||
bl.mailspike.net=127.0.0.[10;11;12]*4
|
||||
EOF
|
||||
fi
|
||||
|
||||
# Remove discontinued DNSBLs from existing dns_blocklists.cf
|
||||
sed -i '/ix\.dnsbl\.manitu\.net\*2/d' /opt/postfix/conf/dns_blocklists.cf # Nixspam
|
||||
|
||||
DNSBL_CONFIG=$(grep -v '^#' /opt/postfix/conf/dns_blocklists.cf | grep '\S')
|
||||
|
||||
if [ ! -z "$DNSBL_CONFIG" ]; then
|
||||
echo -e "\e[33mChecking if ASN for your IP is listed for Spamhaus Bad ASN List...\e[0m"
|
||||
if [ -n "$SPAMHAUS_DQS_KEY" ]; then
|
||||
echo -e "\e[32mDetected SPAMHAUS_DQS_KEY variable from mailcow.conf...\e[0m"
|
||||
echo -e "\e[33mUsing DQS Blocklists from Spamhaus!\e[0m"
|
||||
SPAMHAUS_DNSBL_CONFIG=$(cat <<EOF
|
||||
${SPAMHAUS_DQS_KEY}.zen.dq.spamhaus.net=127.0.0.[4..7]*6
|
||||
${SPAMHAUS_DQS_KEY}.zen.dq.spamhaus.net=127.0.0.[10;11]*8
|
||||
${SPAMHAUS_DQS_KEY}.zen.dq.spamhaus.net=127.0.0.3*4
|
||||
${SPAMHAUS_DQS_KEY}.zen.dq.spamhaus.net=127.0.0.2*3
|
||||
postscreen_dnsbl_reply_map = texthash:/opt/postfix/conf/dnsbl_reply.map
|
||||
EOF
|
||||
|
||||
cat <<EOF > /opt/postfix/conf/dnsbl_reply.map
|
||||
# Autogenerated by mailcow, using Spamhaus DQS reply domains
|
||||
${SPAMHAUS_DQS_KEY}.sbl.dq.spamhaus.net sbl.spamhaus.org
|
||||
${SPAMHAUS_DQS_KEY}.xbl.dq.spamhaus.net xbl.spamhaus.org
|
||||
${SPAMHAUS_DQS_KEY}.pbl.dq.spamhaus.net pbl.spamhaus.org
|
||||
${SPAMHAUS_DQS_KEY}.zen.dq.spamhaus.net zen.spamhaus.org
|
||||
${SPAMHAUS_DQS_KEY}.dbl.dq.spamhaus.net dbl.spamhaus.org
|
||||
${SPAMHAUS_DQS_KEY}.zrd.dq.spamhaus.net zrd.spamhaus.org
|
||||
EOF
|
||||
)
|
||||
else
|
||||
if [ -f "/opt/postfix/conf/dnsbl_reply.map" ]; then
|
||||
rm /opt/postfix/conf/dnsbl_reply.map
|
||||
fi
|
||||
response=$(curl --connect-timeout 15 --max-time 30 -s -o /dev/null -w "%{http_code}" "https://asn-check.mailcow.email")
|
||||
if [ "$response" -eq 503 ]; then
|
||||
echo -e "\e[31mThe AS of your IP is listed as a banned AS from Spamhaus!\e[0m"
|
||||
echo -e "\e[33mNo SPAMHAUS_DQS_KEY found... Skipping Spamhaus blocklists entirely!\e[0m"
|
||||
SPAMHAUS_DNSBL_CONFIG=""
|
||||
elif [ "$response" -eq 200 ]; then
|
||||
echo -e "\e[32mThe AS of your IP is NOT listed as a banned AS from Spamhaus!\e[0m"
|
||||
echo -e "\e[33mUsing the open Spamhaus blocklists.\e[0m"
|
||||
SPAMHAUS_DNSBL_CONFIG=$(cat <<EOF
|
||||
zen.spamhaus.org=127.0.0.[10;11]*8
|
||||
zen.spamhaus.org=127.0.0.[4..7]*6
|
||||
zen.spamhaus.org=127.0.0.3*4
|
||||
zen.spamhaus.org=127.0.0.2*3
|
||||
EOF
|
||||
)
|
||||
|
||||
else
|
||||
echo -e "\e[31mWe couldn't determine your AS... (maybe DNS/Network issue?) Response Code: $response\e[0m"
|
||||
echo -e "\e[33mDeactivating Spamhaus DNS Blocklists to be on the safe site!\e[0m"
|
||||
SPAMHAUS_DNSBL_CONFIG=""
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# Reset main.cf
|
||||
sed -i '/Overrides/q' /opt/postfix/conf/main.cf
|
||||
echo >> /opt/postfix/conf/main.cf
|
||||
# Append postscreen dnsbl sites to main.cf
|
||||
if [ ! -z "$DNSBL_CONFIG" ]; then
|
||||
echo -e "${DNSBL_CONFIG}\n${SPAMHAUS_DNSBL_CONFIG}" >> /opt/postfix/conf/main.cf
|
||||
fi
|
||||
# Append user overrides
|
||||
echo -e "\n# User Overrides" >> /opt/postfix/conf/main.cf
|
||||
touch /opt/postfix/conf/extra.cf
|
||||
sed -i '/\$myhostname/! { /myhostname/d }' /opt/postfix/conf/extra.cf
|
||||
echo -e "myhostname = ${MAILCOW_HOSTNAME}\n$(cat /opt/postfix/conf/extra.cf)" > /opt/postfix/conf/extra.cf
|
||||
cat /opt/postfix/conf/extra.cf >> /opt/postfix/conf/main.cf
|
||||
|
||||
if [ ! -f /opt/postfix/conf/custom_transport.pcre ]; then
|
||||
echo "Creating dummy custom_transport.pcre"
|
||||
touch /opt/postfix/conf/custom_transport.pcre
|
||||
fi
|
||||
|
||||
if [[ ! -f /opt/postfix/conf/custom_postscreen_whitelist.cidr ]]; then
|
||||
echo "Creating dummy custom_postscreen_whitelist.cidr"
|
||||
cat <<EOF > /opt/postfix/conf/custom_postscreen_whitelist.cidr
|
||||
# Autogenerated by mailcow
|
||||
# Rules are evaluated in the order as specified.
|
||||
# Blacklist 192.168.* except 192.168.0.1.
|
||||
# 192.168.0.1 permit
|
||||
# 192.168.0.0/16 reject
|
||||
EOF
|
||||
fi
|
||||
|
||||
# Fix Postfix permissions
|
||||
chown -R root:postfix /opt/postfix/conf/sql/ /opt/postfix/conf/custom_transport.pcre
|
||||
chmod 640 /opt/postfix/conf/sql/*.cf /opt/postfix/conf/custom_transport.pcre
|
||||
chgrp -R postdrop /var/spool/postfix/public
|
||||
chgrp -R postdrop /var/spool/postfix/maildrop
|
||||
postfix set-permissions
|
||||
|
||||
# Checking if there is a leftover of a crashed postfix container before starting a new one
|
||||
if [ -e /var/spool/postfix/pid/master.pid ]; then
|
||||
rm -rf /var/spool/postfix/pid/master.pid
|
||||
fi
|
||||
|
||||
# Check Postfix configuration
|
||||
postconf -c /opt/postfix/conf > /dev/null
|
||||
|
||||
if [[ $? != 0 ]]; then
|
||||
echo "Postfix configuration error, refusing to start."
|
||||
exit 1
|
||||
else
|
||||
postfix -c /opt/postfix/conf start
|
||||
sleep 126144000
|
||||
fi
|
||||
@@ -11,8 +11,8 @@ stderr_logfile=/dev/stderr
|
||||
stderr_logfile_maxbytes=0
|
||||
autostart=true
|
||||
|
||||
[program:postfix]
|
||||
command=/opt/postfix.sh
|
||||
[program:bootstrap]
|
||||
command=/docker-entrypoint.sh
|
||||
stdout_logfile=/dev/stdout
|
||||
stdout_logfile_maxbytes=0
|
||||
stderr_logfile=/dev/stderr
|
||||
|
||||
@@ -14,10 +14,11 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
dnsutils \
|
||||
netcat-traditional \
|
||||
wget \
|
||||
redis-tools \
|
||||
procps \
|
||||
redis-tools \
|
||||
procps \
|
||||
nano \
|
||||
lua-cjson \
|
||||
python3 python3-pip \
|
||||
&& arch=$(arch | sed s/aarch64/arm64/ | sed s/x86_64/amd64/) \
|
||||
&& wget -P /tmp https://rspamd.com/apt-stable/pool/main/r/rspamd/${RSPAMD_VER}~${CODENAME}_${arch}.deb\
|
||||
&& apt install -y /tmp/${RSPAMD_VER}~${CODENAME}_${arch}.deb \
|
||||
@@ -29,12 +30,20 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
&& echo 'alias ll="ls -la --color"' >> ~/.bashrc \
|
||||
&& sed -i 's/#analysis_keyword_table > 0/analysis_cat_table.macro_exist == "M"/g' /usr/share/rspamd/lualib/lua_scanners/oletools.lua
|
||||
|
||||
COPY settings.conf /etc/rspamd/settings.conf
|
||||
COPY set_worker_password.sh /set_worker_password.sh
|
||||
COPY docker-entrypoint.sh /docker-entrypoint.sh
|
||||
RUN pip install --break-system-packages \
|
||||
mysql-connector-python \
|
||||
jinja2 \
|
||||
redis \
|
||||
dnspython \
|
||||
psutil
|
||||
|
||||
|
||||
COPY data/Dockerfiles/bootstrap /bootstrap
|
||||
COPY data/Dockerfiles/rspamd/settings.conf /etc/rspamd/settings.conf
|
||||
COPY data/Dockerfiles/rspamd/set_worker_password.sh /set_worker_password.sh
|
||||
COPY data/Dockerfiles/rspamd/docker-entrypoint.sh /docker-entrypoint.sh
|
||||
|
||||
ENTRYPOINT ["/docker-entrypoint.sh"]
|
||||
|
||||
STOPSIGNAL SIGTERM
|
||||
|
||||
ENTRYPOINT ["/docker-entrypoint.sh"]
|
||||
CMD ["/usr/bin/rspamd", "-f", "-u", "_rspamd", "-g", "_rspamd"]
|
||||
|
||||
@@ -1,144 +1,5 @@
|
||||
#!/bin/bash
|
||||
|
||||
until nc phpfpm 9001 -z; do
|
||||
echo "Waiting for PHP on port 9001..."
|
||||
sleep 3
|
||||
done
|
||||
|
||||
until nc phpfpm 9002 -z; do
|
||||
echo "Waiting for PHP on port 9002..."
|
||||
sleep 3
|
||||
done
|
||||
|
||||
mkdir -p /etc/rspamd/plugins.d \
|
||||
/etc/rspamd/custom
|
||||
|
||||
touch /etc/rspamd/rspamd.conf.local \
|
||||
/etc/rspamd/rspamd.conf.override
|
||||
|
||||
chmod 755 /var/lib/rspamd
|
||||
|
||||
|
||||
[[ ! -f /etc/rspamd/override.d/worker-controller-password.inc ]] && echo '# Autogenerated by mailcow' > /etc/rspamd/override.d/worker-controller-password.inc
|
||||
|
||||
echo ${IPV4_NETWORK}.0/24 > /etc/rspamd/custom/mailcow_networks.map
|
||||
echo ${IPV6_NETWORK} >> /etc/rspamd/custom/mailcow_networks.map
|
||||
|
||||
DOVECOT_V4=
|
||||
DOVECOT_V6=
|
||||
until [[ ! -z ${DOVECOT_V4} ]]; do
|
||||
DOVECOT_V4=$(dig a dovecot +short)
|
||||
DOVECOT_V6=$(dig aaaa dovecot +short)
|
||||
[[ ! -z ${DOVECOT_V4} ]] && break;
|
||||
echo "Waiting for Dovecot..."
|
||||
sleep 3
|
||||
done
|
||||
echo ${DOVECOT_V4}/32 > /etc/rspamd/custom/dovecot_trusted.map
|
||||
if [[ ! -z ${DOVECOT_V6} ]]; then
|
||||
echo ${DOVECOT_V6}/128 >> /etc/rspamd/custom/dovecot_trusted.map
|
||||
fi
|
||||
|
||||
RSPAMD_V4=
|
||||
RSPAMD_V6=
|
||||
until [[ ! -z ${RSPAMD_V4} ]]; do
|
||||
RSPAMD_V4=$(dig a rspamd +short)
|
||||
RSPAMD_V6=$(dig aaaa rspamd +short)
|
||||
[[ ! -z ${RSPAMD_V4} ]] && break;
|
||||
echo "Waiting for Rspamd..."
|
||||
sleep 3
|
||||
done
|
||||
echo ${RSPAMD_V4}/32 > /etc/rspamd/custom/rspamd_trusted.map
|
||||
if [[ ! -z ${RSPAMD_V6} ]]; then
|
||||
echo ${RSPAMD_V6}/128 >> /etc/rspamd/custom/rspamd_trusted.map
|
||||
fi
|
||||
|
||||
if [[ ! -z ${REDIS_SLAVEOF_IP} ]]; then
|
||||
cat <<EOF > /etc/rspamd/local.d/redis.conf
|
||||
read_servers = "redis:6379";
|
||||
write_servers = "${REDIS_SLAVEOF_IP}:${REDIS_SLAVEOF_PORT}";
|
||||
password = "${REDISPASS}";
|
||||
timeout = 10;
|
||||
EOF
|
||||
until [[ $(redis-cli -h redis-mailcow -a ${REDISPASS} --no-auth-warning PING) == "PONG" ]]; do
|
||||
echo "Waiting for Redis @redis-mailcow..."
|
||||
sleep 2
|
||||
done
|
||||
until [[ $(redis-cli -h ${REDIS_SLAVEOF_IP} -p ${REDIS_SLAVEOF_PORT} -a ${REDISPASS} --no-auth-warning PING) == "PONG" ]]; do
|
||||
echo "Waiting for Redis @${REDIS_SLAVEOF_IP}..."
|
||||
sleep 2
|
||||
done
|
||||
redis-cli -h redis-mailcow -a ${REDISPASS} --no-auth-warning SLAVEOF ${REDIS_SLAVEOF_IP} ${REDIS_SLAVEOF_PORT}
|
||||
else
|
||||
cat <<EOF > /etc/rspamd/local.d/redis.conf
|
||||
servers = "redis:6379";
|
||||
password = "${REDISPASS}";
|
||||
timeout = 10;
|
||||
EOF
|
||||
until [[ $(redis-cli -h redis-mailcow -a ${REDISPASS} --no-auth-warning PING) == "PONG" ]]; do
|
||||
echo "Waiting for Redis slave..."
|
||||
sleep 2
|
||||
done
|
||||
redis-cli -h redis-mailcow -a ${REDISPASS} --no-auth-warning SLAVEOF NO ONE
|
||||
fi
|
||||
|
||||
if [[ "${SKIP_OLEFY}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
|
||||
if [[ -f /etc/rspamd/local.d/external_services.conf ]]; then
|
||||
rm /etc/rspamd/local.d/external_services.conf
|
||||
fi
|
||||
else
|
||||
cat <<EOF > /etc/rspamd/local.d/external_services.conf
|
||||
oletools {
|
||||
# default olefy settings
|
||||
servers = "olefy:10055";
|
||||
# needs to be set explicitly for Rspamd < 1.9.5
|
||||
scan_mime_parts = true;
|
||||
# mime-part regex matching in content-type or filename
|
||||
# block all macros
|
||||
extended = true;
|
||||
max_size = 3145728;
|
||||
timeout = 20.0;
|
||||
retransmits = 1;
|
||||
}
|
||||
EOF
|
||||
fi
|
||||
|
||||
# Provide additional lua modules
|
||||
ln -s /usr/lib/$(uname -m)-linux-gnu/liblua5.1-cjson.so.0.0.0 /usr/lib/rspamd/cjson.so
|
||||
|
||||
chown -R _rspamd:_rspamd /var/lib/rspamd \
|
||||
/etc/rspamd/local.d \
|
||||
/etc/rspamd/override.d \
|
||||
/etc/rspamd/rspamd.conf.local \
|
||||
/etc/rspamd/rspamd.conf.override \
|
||||
/etc/rspamd/plugins.d
|
||||
|
||||
# Fix missing default global maps, if any
|
||||
# These exists in mailcow UI and should not be removed
|
||||
touch /etc/rspamd/custom/global_mime_from_blacklist.map \
|
||||
/etc/rspamd/custom/global_rcpt_blacklist.map \
|
||||
/etc/rspamd/custom/global_smtp_from_blacklist.map \
|
||||
/etc/rspamd/custom/global_mime_from_whitelist.map \
|
||||
/etc/rspamd/custom/global_rcpt_whitelist.map \
|
||||
/etc/rspamd/custom/global_smtp_from_whitelist.map \
|
||||
/etc/rspamd/custom/bad_languages.map \
|
||||
/etc/rspamd/custom/sa-rules \
|
||||
/etc/rspamd/custom/dovecot_trusted.map \
|
||||
/etc/rspamd/custom/rspamd_trusted.map \
|
||||
/etc/rspamd/custom/mailcow_networks.map \
|
||||
/etc/rspamd/custom/ip_wl.map \
|
||||
/etc/rspamd/custom/fishy_tlds.map \
|
||||
/etc/rspamd/custom/bad_words.map \
|
||||
/etc/rspamd/custom/bad_asn.map \
|
||||
/etc/rspamd/custom/bad_words_de.map \
|
||||
/etc/rspamd/custom/bulk_header.map \
|
||||
/etc/rspamd/custom/bad_header.map
|
||||
|
||||
# www-data (82) group needs to write to these files
|
||||
chown _rspamd:_rspamd /etc/rspamd/custom/
|
||||
chmod 0755 /etc/rspamd/custom/.
|
||||
chown -R 82:82 /etc/rspamd/custom/*
|
||||
chmod 644 -R /etc/rspamd/custom/*
|
||||
|
||||
# Run hooks
|
||||
for file in /hooks/*; do
|
||||
if [ -x "${file}" ]; then
|
||||
@@ -147,190 +8,13 @@ for file in /hooks/*; do
|
||||
fi
|
||||
done
|
||||
|
||||
# If DQS KEY is set in mailcow.conf add Spamhaus DQS RBLs
|
||||
if [[ ! -z ${SPAMHAUS_DQS_KEY} ]]; then
|
||||
cat <<EOF > /etc/rspamd/custom/dqs-rbl.conf
|
||||
# Autogenerated by mailcow. DO NOT TOUCH!
|
||||
spamhaus {
|
||||
rbl = "${SPAMHAUS_DQS_KEY}.zen.dq.spamhaus.net";
|
||||
from = false;
|
||||
}
|
||||
spamhaus_from {
|
||||
from = true;
|
||||
received = false;
|
||||
rbl = "${SPAMHAUS_DQS_KEY}.zen.dq.spamhaus.net";
|
||||
returncodes {
|
||||
SPAMHAUS_ZEN = [ "127.0.0.2", "127.0.0.3", "127.0.0.4", "127.0.0.5", "127.0.0.6", "127.0.0.7", "127.0.0.9", "127.0.0.10", "127.0.0.11" ];
|
||||
}
|
||||
}
|
||||
spamhaus_authbl_received {
|
||||
# Check if the sender client is listed in AuthBL (AuthBL is *not* part of ZEN)
|
||||
rbl = "${SPAMHAUS_DQS_KEY}.authbl.dq.spamhaus.net";
|
||||
from = false;
|
||||
received = true;
|
||||
ipv6 = true;
|
||||
returncodes {
|
||||
SH_AUTHBL_RECEIVED = "127.0.0.20"
|
||||
}
|
||||
}
|
||||
spamhaus_dbl {
|
||||
# Add checks on the HELO string
|
||||
rbl = "${SPAMHAUS_DQS_KEY}.dbl.dq.spamhaus.net";
|
||||
helo = true;
|
||||
rdns = true;
|
||||
dkim = true;
|
||||
disable_monitoring = true;
|
||||
returncodes {
|
||||
RBL_DBL_SPAM = "127.0.1.2";
|
||||
RBL_DBL_PHISH = "127.0.1.4";
|
||||
RBL_DBL_MALWARE = "127.0.1.5";
|
||||
RBL_DBL_BOTNET = "127.0.1.6";
|
||||
RBL_DBL_ABUSED_SPAM = "127.0.1.102";
|
||||
RBL_DBL_ABUSED_PHISH = "127.0.1.104";
|
||||
RBL_DBL_ABUSED_MALWARE = "127.0.1.105";
|
||||
RBL_DBL_ABUSED_BOTNET = "127.0.1.106";
|
||||
RBL_DBL_DONT_QUERY_IPS = "127.0.1.255";
|
||||
}
|
||||
}
|
||||
spamhaus_dbl_fullurls {
|
||||
ignore_defaults = true;
|
||||
no_ip = true;
|
||||
rbl = "${SPAMHAUS_DQS_KEY}.dbl.dq.spamhaus.net";
|
||||
selector = 'urls:get_host'
|
||||
disable_monitoring = true;
|
||||
returncodes {
|
||||
DBLABUSED_SPAM_FULLURLS = "127.0.1.102";
|
||||
DBLABUSED_PHISH_FULLURLS = "127.0.1.104";
|
||||
DBLABUSED_MALWARE_FULLURLS = "127.0.1.105";
|
||||
DBLABUSED_BOTNET_FULLURLS = "127.0.1.106";
|
||||
}
|
||||
}
|
||||
spamhaus_zrd {
|
||||
# Add checks on the HELO string also for DQS
|
||||
rbl = "${SPAMHAUS_DQS_KEY}.zrd.dq.spamhaus.net";
|
||||
helo = true;
|
||||
rdns = true;
|
||||
dkim = true;
|
||||
disable_monitoring = true;
|
||||
returncodes {
|
||||
RBL_ZRD_VERY_FRESH_DOMAIN = ["127.0.2.2", "127.0.2.3", "127.0.2.4"];
|
||||
RBL_ZRD_FRESH_DOMAIN = [
|
||||
"127.0.2.5", "127.0.2.6", "127.0.2.7", "127.0.2.8", "127.0.2.9", "127.0.2.10", "127.0.2.11", "127.0.2.12", "127.0.2.13", "127.0.2.14", "127.0.2.15", "127.0.2.16", "127.0.2.17", "127.0.2.18", "127.0.2.19", "127.0.2.20", "127.0.2.21", "127.0.2.22", "127.0.2.23", "127.0.2.24"
|
||||
];
|
||||
RBL_ZRD_DONT_QUERY_IPS = "127.0.2.255";
|
||||
}
|
||||
}
|
||||
"SPAMHAUS_ZEN_URIBL" {
|
||||
enabled = true;
|
||||
rbl = "${SPAMHAUS_DQS_KEY}.zen.dq.spamhaus.net";
|
||||
resolve_ip = true;
|
||||
checks = ['urls'];
|
||||
replyto = true;
|
||||
emails = true;
|
||||
ipv4 = true;
|
||||
ipv6 = true;
|
||||
emails_domainonly = true;
|
||||
returncodes {
|
||||
URIBL_SBL = "127.0.0.2";
|
||||
URIBL_SBL_CSS = "127.0.0.3";
|
||||
URIBL_XBL = ["127.0.0.4", "127.0.0.5", "127.0.0.6", "127.0.0.7"];
|
||||
URIBL_PBL = ["127.0.0.10", "127.0.0.11"];
|
||||
URIBL_DROP = "127.0.0.9";
|
||||
}
|
||||
}
|
||||
SH_EMAIL_DBL {
|
||||
ignore_defaults = true;
|
||||
replyto = true;
|
||||
emails_domainonly = true;
|
||||
disable_monitoring = true;
|
||||
rbl = "${SPAMHAUS_DQS_KEY}.dbl.dq.spamhaus.net";
|
||||
returncodes = {
|
||||
SH_EMAIL_DBL = [
|
||||
"127.0.1.2",
|
||||
"127.0.1.4",
|
||||
"127.0.1.5",
|
||||
"127.0.1.6"
|
||||
];
|
||||
SH_EMAIL_DBL_ABUSED = [
|
||||
"127.0.1.102",
|
||||
"127.0.1.104",
|
||||
"127.0.1.105",
|
||||
"127.0.1.106"
|
||||
];
|
||||
SH_EMAIL_DBL_DONT_QUERY_IPS = [ "127.0.1.255" ];
|
||||
}
|
||||
}
|
||||
SH_EMAIL_ZRD {
|
||||
ignore_defaults = true;
|
||||
replyto = true;
|
||||
emails_domainonly = true;
|
||||
disable_monitoring = true;
|
||||
rbl = "${SPAMHAUS_DQS_KEY}.zrd.dq.spamhaus.net";
|
||||
returncodes = {
|
||||
SH_EMAIL_ZRD_VERY_FRESH_DOMAIN = ["127.0.2.2", "127.0.2.3", "127.0.2.4"];
|
||||
SH_EMAIL_ZRD_FRESH_DOMAIN = [
|
||||
"127.0.2.5", "127.0.2.6", "127.0.2.7", "127.0.2.8", "127.0.2.9", "127.0.2.10", "127.0.2.11", "127.0.2.12", "127.0.2.13", "127.0.2.14", "127.0.2.15", "127.0.2.16", "127.0.2.17", "127.0.2.18", "127.0.2.19", "127.0.2.20", "127.0.2.21", "127.0.2.22", "127.0.2.23", "127.0.2.24"
|
||||
];
|
||||
SH_EMAIL_ZRD_DONT_QUERY_IPS = [ "127.0.2.255" ];
|
||||
}
|
||||
}
|
||||
"DBL" {
|
||||
# override the defaults for DBL defined in modules.d/rbl.conf
|
||||
rbl = "${SPAMHAUS_DQS_KEY}.dbl.dq.spamhaus.net";
|
||||
disable_monitoring = true;
|
||||
}
|
||||
"ZRD" {
|
||||
ignore_defaults = true;
|
||||
rbl = "${SPAMHAUS_DQS_KEY}.zrd.dq.spamhaus.net";
|
||||
no_ip = true;
|
||||
dkim = true;
|
||||
emails = true;
|
||||
emails_domainonly = true;
|
||||
urls = true;
|
||||
returncodes = {
|
||||
ZRD_VERY_FRESH_DOMAIN = ["127.0.2.2", "127.0.2.3", "127.0.2.4"];
|
||||
ZRD_FRESH_DOMAIN = ["127.0.2.5", "127.0.2.6", "127.0.2.7", "127.0.2.8", "127.0.2.9", "127.0.2.10", "127.0.2.11", "127.0.2.12", "127.0.2.13", "127.0.2.14", "127.0.2.15", "127.0.2.16", "127.0.2.17", "127.0.2.18", "127.0.2.19", "127.0.2.20", "127.0.2.21", "127.0.2.22", "127.0.2.23", "127.0.2.24"];
|
||||
}
|
||||
}
|
||||
spamhaus_sbl_url {
|
||||
ignore_defaults = true
|
||||
rbl = "${SPAMHAUS_DQS_KEY}.sbl.dq.spamhaus.net";
|
||||
checks = ['urls'];
|
||||
disable_monitoring = true;
|
||||
returncodes {
|
||||
SPAMHAUS_SBL_URL = "127.0.0.2";
|
||||
}
|
||||
}
|
||||
python3 -u /bootstrap/main.py
|
||||
BOOTSTRAP_EXIT_CODE=$?
|
||||
|
||||
SH_HBL_EMAIL {
|
||||
ignore_defaults = true;
|
||||
rbl = "_email.${SPAMHAUS_DQS_KEY}.hbl.dq.spamhaus.net";
|
||||
emails_domainonly = false;
|
||||
selector = "from('smtp').lower;from('mime').lower";
|
||||
ignore_whitelist = true;
|
||||
checks = ['emails', 'replyto'];
|
||||
hash = "sha1";
|
||||
returncodes = {
|
||||
SH_HBL_EMAIL = [
|
||||
"127.0.3.2"
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
spamhaus_dqs_hbl {
|
||||
symbol = "HBL_FILE_UNKNOWN";
|
||||
rbl = "_file.${SPAMHAUS_DQS_KEY}.hbl.dq.spamhaus.net.";
|
||||
selector = "attachments('rbase32', 'sha256')";
|
||||
ignore_whitelist = true;
|
||||
ignore_defaults = true;
|
||||
returncodes {
|
||||
SH_HBL_FILE_MALICIOUS = "127.0.3.10";
|
||||
SH_HBL_FILE_SUSPICIOUS = "127.0.3.15";
|
||||
}
|
||||
}
|
||||
EOF
|
||||
else
|
||||
rm -rf /etc/rspamd/custom/dqs-rbl.conf
|
||||
if [ $BOOTSTRAP_EXIT_CODE -ne 0 ]; then
|
||||
echo "Bootstrap failed with exit code $BOOTSTRAP_EXIT_CODE. Not starting Rspamd."
|
||||
exit $BOOTSTRAP_EXIT_CODE
|
||||
fi
|
||||
|
||||
echo "Bootstrap succeeded. Starting Rspamd..."
|
||||
exec "$@"
|
||||
|
||||
@@ -27,6 +27,7 @@ RUN echo "Building from repository $SOGO_DEBIAN_REPOSITORY" \
|
||||
psmisc \
|
||||
wget \
|
||||
patch \
|
||||
python3 python3-pip \
|
||||
&& dpkgArch="$(dpkg --print-architecture | awk -F- '{ print $NF }')" \
|
||||
&& wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$dpkgArch" \
|
||||
&& chmod +x /usr/local/bin/gosu \
|
||||
@@ -42,18 +43,21 @@ RUN echo "Building from repository $SOGO_DEBIAN_REPOSITORY" \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& touch /etc/default/locale
|
||||
|
||||
COPY ./bootstrap-sogo.sh /bootstrap-sogo.sh
|
||||
COPY syslog-ng.conf /etc/syslog-ng/syslog-ng.conf
|
||||
COPY syslog-ng-redis_slave.conf /etc/syslog-ng/syslog-ng-redis_slave.conf
|
||||
COPY supervisord.conf /etc/supervisor/supervisord.conf
|
||||
COPY acl.diff /acl.diff
|
||||
COPY navMailcowBtns.diff /navMailcowBtns.diff
|
||||
COPY stop-supervisor.sh /usr/local/sbin/stop-supervisor.sh
|
||||
COPY docker-entrypoint.sh /
|
||||
RUN pip install --break-system-packages \
|
||||
mysql-connector-python \
|
||||
jinja2 \
|
||||
redis \
|
||||
dnspython \
|
||||
psutil
|
||||
|
||||
RUN chmod +x /bootstrap-sogo.sh \
|
||||
/usr/local/sbin/stop-supervisor.sh
|
||||
|
||||
ENTRYPOINT ["/docker-entrypoint.sh"]
|
||||
COPY data/Dockerfiles/bootstrap /bootstrap
|
||||
COPY data/Dockerfiles/sogo/syslog-ng.conf /etc/syslog-ng/syslog-ng.conf
|
||||
COPY data/Dockerfiles/sogo/syslog-ng-redis_slave.conf /etc/syslog-ng/syslog-ng-redis_slave.conf
|
||||
COPY data/Dockerfiles/sogo/supervisord.conf /etc/supervisor/supervisord.conf
|
||||
COPY data/Dockerfiles/sogo/stop-supervisor.sh /usr/local/sbin/stop-supervisor.sh
|
||||
COPY data/Dockerfiles/sogo/docker-entrypoint.sh /
|
||||
|
||||
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/supervisord.conf"]
|
||||
RUN chmod +x /usr/local/sbin/stop-supervisor.sh
|
||||
|
||||
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/supervisord.conf"]
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
--- /usr/lib/GNUstep/SOGo/Templates/UIxAclEditor.wox 2018-08-17 18:29:57.987504204 +0200
|
||||
+++ /usr/lib/GNUstep/SOGo/Templates/UIxAclEditor.wox 2018-08-17 18:29:35.918291298 +0200
|
||||
@@ -46,7 +46,7 @@
|
||||
</md-item-template>
|
||||
</md-autocomplete>
|
||||
</div>
|
||||
- <md-card ng-repeat="user in acl.users | orderBy:['userClass', 'cn']"
|
||||
+ <md-card ng-repeat="user in acl.users | filter:{ userClass: 'normal' } | orderBy:['cn']"
|
||||
class="sg-collapsed"
|
||||
ng-class="{ 'sg-expanded': user.uid == acl.selectedUid }">
|
||||
<a class="md-flex md-button" ng-click="acl.selectUser(user, $event)">
|
||||
@@ -1,153 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Wait for MySQL to warm-up
|
||||
while ! mariadb-admin status --ssl=false --socket=/var/run/mysqld/mysqld.sock -u${DBUSER} -p${DBPASS} --silent; do
|
||||
echo "Waiting for database to come up..."
|
||||
sleep 2
|
||||
done
|
||||
|
||||
# Wait until port becomes free and send sig
|
||||
until ! nc -z sogo-mailcow 20000;
|
||||
do
|
||||
killall -TERM sogod
|
||||
sleep 3
|
||||
done
|
||||
|
||||
# Wait for updated schema
|
||||
DBV_NOW=$(mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT version FROM versions WHERE application = 'db_schema';" -BN)
|
||||
DBV_NEW=$(grep -oE '\$db_version = .*;' init_db.inc.php | sed 's/$db_version = //g;s/;//g' | cut -d \" -f2)
|
||||
while [[ "${DBV_NOW}" != "${DBV_NEW}" ]]; do
|
||||
echo "Waiting for schema update..."
|
||||
DBV_NOW=$(mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT version FROM versions WHERE application = 'db_schema';" -BN)
|
||||
DBV_NEW=$(grep -oE '\$db_version = .*;' init_db.inc.php | sed 's/$db_version = //g;s/;//g' | cut -d \" -f2)
|
||||
sleep 5
|
||||
done
|
||||
echo "DB schema is ${DBV_NOW}"
|
||||
|
||||
# cat /dev/urandom seems to hang here occasionally and is not recommended anyway, better use openssl
|
||||
RAND_PASS=$(openssl rand -base64 16 | tr -dc _A-Z-a-z-0-9)
|
||||
|
||||
# Generate plist header with timezone data
|
||||
mkdir -p /var/lib/sogo/GNUstep/Defaults/
|
||||
cat <<EOF > /var/lib/sogo/GNUstep/Defaults/sogod.plist
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//GNUstep//DTD plist 0.9//EN" "http://www.gnustep.org/plist-0_9.xml">
|
||||
<plist version="0.9">
|
||||
<dict>
|
||||
<key>OCSAclURL</key>
|
||||
<string>mysql://${DBUSER}:${DBPASS}@%2Fvar%2Frun%2Fmysqld%2Fmysqld.sock/${DBNAME}/sogo_acl</string>
|
||||
<key>SOGoIMAPServer</key>
|
||||
<string>imap://${IPV4_NETWORK}.250:143/?TLS=YES&tlsVerifyMode=none</string>
|
||||
<key>SOGoSieveServer</key>
|
||||
<string>sieve://${IPV4_NETWORK}.250:4190/?TLS=YES&tlsVerifyMode=none</string>
|
||||
<key>SOGoSMTPServer</key>
|
||||
<string>smtp://${IPV4_NETWORK}.253:588/?TLS=YES&tlsVerifyMode=none</string>
|
||||
<key>SOGoTrustProxyAuthentication</key>
|
||||
<string>YES</string>
|
||||
<key>SOGoEncryptionKey</key>
|
||||
<string>${RAND_PASS}</string>
|
||||
<key>OCSAdminURL</key>
|
||||
<string>mysql://${DBUSER}:${DBPASS}@%2Fvar%2Frun%2Fmysqld%2Fmysqld.sock/${DBNAME}/sogo_admin</string>
|
||||
<key>OCSCacheFolderURL</key>
|
||||
<string>mysql://${DBUSER}:${DBPASS}@%2Fvar%2Frun%2Fmysqld%2Fmysqld.sock/${DBNAME}/sogo_cache_folder</string>
|
||||
<key>OCSEMailAlarmsFolderURL</key>
|
||||
<string>mysql://${DBUSER}:${DBPASS}@%2Fvar%2Frun%2Fmysqld%2Fmysqld.sock/${DBNAME}/sogo_alarms_folder</string>
|
||||
<key>OCSFolderInfoURL</key>
|
||||
<string>mysql://${DBUSER}:${DBPASS}@%2Fvar%2Frun%2Fmysqld%2Fmysqld.sock/${DBNAME}/sogo_folder_info</string>
|
||||
<key>OCSSessionsFolderURL</key>
|
||||
<string>mysql://${DBUSER}:${DBPASS}@%2Fvar%2Frun%2Fmysqld%2Fmysqld.sock/${DBNAME}/sogo_sessions_folder</string>
|
||||
<key>OCSStoreURL</key>
|
||||
<string>mysql://${DBUSER}:${DBPASS}@%2Fvar%2Frun%2Fmysqld%2Fmysqld.sock/${DBNAME}/sogo_store</string>
|
||||
<key>SOGoProfileURL</key>
|
||||
<string>mysql://${DBUSER}:${DBPASS}@%2Fvar%2Frun%2Fmysqld%2Fmysqld.sock/${DBNAME}/sogo_user_profile</string>
|
||||
<key>SOGoTimeZone</key>
|
||||
<string>${TZ}</string>
|
||||
<key>domains</key>
|
||||
<dict>
|
||||
EOF
|
||||
|
||||
# Generate multi-domain setup
|
||||
while read -r line gal
|
||||
do
|
||||
echo " <key>${line}</key>
|
||||
<dict>
|
||||
<key>SOGoMailDomain</key>
|
||||
<string>${line}</string>
|
||||
<key>SOGoUserSources</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>MailFieldNames</key>
|
||||
<array>
|
||||
<string>aliases</string>
|
||||
<string>ad_aliases</string>
|
||||
<string>ext_acl</string>
|
||||
</array>
|
||||
<key>KindFieldName</key>
|
||||
<string>kind</string>
|
||||
<key>DomainFieldName</key>
|
||||
<string>domain</string>
|
||||
<key>MultipleBookingsFieldName</key>
|
||||
<string>multiple_bookings</string>
|
||||
<key>listRequiresDot</key>
|
||||
<string>NO</string>
|
||||
<key>canAuthenticate</key>
|
||||
<string>YES</string>
|
||||
<key>displayName</key>
|
||||
<string>GAL ${line}</string>
|
||||
<key>id</key>
|
||||
<string>${line}</string>
|
||||
<key>isAddressBook</key>
|
||||
<string>${gal}</string>
|
||||
<key>type</key>
|
||||
<string>sql</string>
|
||||
<key>userPasswordAlgorithm</key>
|
||||
<string>${MAILCOW_PASS_SCHEME}</string>
|
||||
<key>prependPasswordScheme</key>
|
||||
<string>YES</string>
|
||||
<key>viewURL</key>
|
||||
<string>mysql://${DBUSER}:${DBPASS}@%2Fvar%2Frun%2Fmysqld%2Fmysqld.sock/${DBNAME}/_sogo_static_view</string>
|
||||
</dict>" >> /var/lib/sogo/GNUstep/Defaults/sogod.plist
|
||||
# Generate alternative LDAP authentication dict, when SQL authentication fails
|
||||
# This will nevertheless read attributes from LDAP
|
||||
/etc/sogo/plist_ldap.sh ${line} ${gal} >> /var/lib/sogo/GNUstep/Defaults/sogod.plist
|
||||
echo " </array>
|
||||
</dict>" >> /var/lib/sogo/GNUstep/Defaults/sogod.plist
|
||||
done < <(mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT domain, CASE gal WHEN '1' THEN 'YES' ELSE 'NO' END AS gal FROM domain;" -B -N)
|
||||
|
||||
# Generate footer
|
||||
echo ' </dict>
|
||||
</dict>
|
||||
</plist>' >> /var/lib/sogo/GNUstep/Defaults/sogod.plist
|
||||
|
||||
# Fix permissions
|
||||
chown sogo:sogo -R /var/lib/sogo/
|
||||
chmod 600 /var/lib/sogo/GNUstep/Defaults/sogod.plist
|
||||
|
||||
# Patch ACLs
|
||||
#if [[ ${ACL_ANYONE} == 'allow' ]]; then
|
||||
# #enable any or authenticated targets for ACL
|
||||
# if patch -R -sfN --dry-run /usr/lib/GNUstep/SOGo/Templates/UIxAclEditor.wox < /acl.diff > /dev/null; then
|
||||
# patch -R /usr/lib/GNUstep/SOGo/Templates/UIxAclEditor.wox < /acl.diff;
|
||||
# fi
|
||||
#else
|
||||
# #disable any or authenticated targets for ACL
|
||||
# if patch -sfN --dry-run /usr/lib/GNUstep/SOGo/Templates/UIxAclEditor.wox < /acl.diff > /dev/null; then
|
||||
# patch /usr/lib/GNUstep/SOGo/Templates/UIxAclEditor.wox < /acl.diff;
|
||||
# fi
|
||||
#fi
|
||||
|
||||
if patch -R -sfN --dry-run /usr/lib/GNUstep/SOGo/Templates/UIxTopnavToolbar.wox < /navMailcowBtns.diff > /dev/null; then
|
||||
patch -R /usr/lib/GNUstep/SOGo/Templates/UIxTopnavToolbar.wox < /navMailcowBtns.diff;
|
||||
fi
|
||||
|
||||
# Rename custom logo, if any
|
||||
[[ -f /etc/sogo/sogo-full.svg ]] && mv /etc/sogo/sogo-full.svg /etc/sogo/custom-fulllogo.svg
|
||||
|
||||
# Rsync web content
|
||||
echo "Syncing web content with named volume"
|
||||
rsync -a /usr/lib/GNUstep/SOGo/. /sogo_web/
|
||||
|
||||
# Chown backup path
|
||||
chown -R sogo:sogo /sogo_backup
|
||||
|
||||
exec gosu sogo /usr/sbin/sogod
|
||||
@@ -1,17 +1,5 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [[ "${SKIP_SOGO}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
|
||||
echo "SKIP_SOGO=y, skipping SOGo..."
|
||||
sleep 365d
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ ! -z ${REDIS_SLAVEOF_IP} ]]; then
|
||||
cp /etc/syslog-ng/syslog-ng-redis_slave.conf /etc/syslog-ng/syslog-ng.conf
|
||||
fi
|
||||
|
||||
echo "$TZ" > /etc/timezone
|
||||
|
||||
# Run hooks
|
||||
for file in /hooks/*; do
|
||||
if [ -x "${file}" ]; then
|
||||
@@ -20,4 +8,13 @@ for file in /hooks/*; do
|
||||
fi
|
||||
done
|
||||
|
||||
exec "$@"
|
||||
python3 -u /bootstrap/main.py
|
||||
BOOTSTRAP_EXIT_CODE=$?
|
||||
|
||||
if [ $BOOTSTRAP_EXIT_CODE -ne 0 ]; then
|
||||
echo "Bootstrap failed with exit code $BOOTSTRAP_EXIT_CODE. Not starting SOGo."
|
||||
exit $BOOTSTRAP_EXIT_CODE
|
||||
fi
|
||||
|
||||
echo "Bootstrap succeeded. Starting SOGo..."
|
||||
exec gosu sogo /usr/sbin/sogod
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
60,65d58
|
||||
< var:ng-click="navButtonClick"
|
||||
< ng-href="/user">
|
||||
< <md-icon>build</md-icon>
|
||||
< <md-tooltip>mailcow <var:string label:value="Preferences"/></md-tooltip>
|
||||
< </md-button>
|
||||
< <md-button class="md-icon-button"
|
||||
83c76
|
||||
< onclick="mc_logout();"
|
||||
---
|
||||
> ng-show="::activeUser.path.logoff.length"
|
||||
85c78
|
||||
< ng-href="#">
|
||||
---
|
||||
> ng-href="{{::activeUser.path.logoff}}">
|
||||
@@ -11,8 +11,8 @@ stderr_logfile_maxbytes=0
|
||||
autostart=true
|
||||
priority=1
|
||||
|
||||
[program:bootstrap-sogo]
|
||||
command=/bootstrap-sogo.sh
|
||||
[program:bootstrap]
|
||||
command=/docker-entrypoint.sh
|
||||
stdout_logfile=/dev/stdout
|
||||
stdout_logfile_maxbytes=0
|
||||
stderr_logfile=/dev/stderr
|
||||
|
||||
@@ -34,7 +34,7 @@ RUN apk add --update \
|
||||
&& curl https://raw.githubusercontent.com/mludvig/smtp-cli/v3.10/smtp-cli -o /smtp-cli \
|
||||
&& chmod +x smtp-cli
|
||||
|
||||
COPY watchdog.sh /watchdog.sh
|
||||
COPY check_mysql_slavestatus.sh /usr/lib/nagios/plugins/check_mysql_slavestatus.sh
|
||||
COPY data/Dockerfiles/watchdog/watchdog.sh /watchdog.sh
|
||||
COPY data/Dockerfiles/watchdog/check_mysql_slavestatus.sh /usr/lib/nagios/plugins/check_mysql_slavestatus.sh
|
||||
|
||||
CMD ["/watchdog.sh"]
|
||||
|
||||
@@ -402,7 +402,7 @@ sogo_checks() {
|
||||
trap "[ ${err_count} -gt 1 ] && err_count=$(( ${err_count} - 2 ))" USR1
|
||||
while [ ${err_count} -lt ${THRESHOLD} ]; do
|
||||
touch /tmp/sogo-mailcow; echo "$(tail -50 /tmp/sogo-mailcow)" > /tmp/sogo-mailcow
|
||||
host_ip=$(get_container_ip sogo-mailcow)
|
||||
host_ip=$SOGO_HOST
|
||||
err_c_cur=${err_count}
|
||||
/usr/lib/nagios/plugins/check_http -4 -H ${host_ip} -u /SOGo.index/ -p 20000 2>> /tmp/sogo-mailcow 1>&2; err_count=$(( ${err_count} + $? ))
|
||||
[ ${err_c_cur} -eq ${err_count} ] && [ ! $((${err_count} - 1)) -lt 0 ] && err_count=$((${err_count} - 1)) diff_c=1
|
||||
|
||||
15
data/conf/clamav/config.json
Normal file
15
data/conf/clamav/config.json
Normal file
@@ -0,0 +1,15 @@
|
||||
[
|
||||
{
|
||||
"template": "whitelist.ign2.j2",
|
||||
"output": "/var/lib/clamav/whitelist.ign2",
|
||||
"clean_blank_lines": true
|
||||
},
|
||||
{
|
||||
"template": "clamd.conf.j2",
|
||||
"output": "/etc/clamav/clamd.conf"
|
||||
},
|
||||
{
|
||||
"template": "freshclam.conf.j2",
|
||||
"output": "/etc/clamav/freshclam.conf"
|
||||
}
|
||||
]
|
||||
5
data/conf/clamav/config_templates/whitelist.ign2.j2
Normal file
5
data/conf/clamav/config_templates/whitelist.ign2.j2
Normal file
@@ -0,0 +1,5 @@
|
||||
# Please restart ClamAV after changing signatures
|
||||
Example-Signature.Ignore-1
|
||||
PUA.Win.Trojan.EmbeddedPDF-1
|
||||
PUA.Pdf.Trojan.EmbeddedJavaScript-1
|
||||
PUA.Pdf.Trojan.OpenActionObjectwithJavascript-1
|
||||
0
data/conf/clamav/custom_templates/.gitkeep
Normal file
0
data/conf/clamav/custom_templates/.gitkeep
Normal file
@@ -67,7 +67,7 @@ require_once 'functions.ratelimit.inc.php';
|
||||
require_once 'functions.acl.inc.php';
|
||||
|
||||
|
||||
$isSOGoRequest = $post['real_rip'] == getenv('IPV4_NETWORK') . '.248';
|
||||
$isSOGoRequest = $post['real_rip'] == getenv('SOGO_HOST');
|
||||
$result = false;
|
||||
if ($isSOGoRequest) {
|
||||
// This is a SOGo Auth request. First check for SSO password.
|
||||
|
||||
70
data/conf/dovecot/config.json
Normal file
70
data/conf/dovecot/config.json
Normal file
@@ -0,0 +1,70 @@
|
||||
[
|
||||
{
|
||||
"template": "dovecot-dict-sql-quota.conf.j2",
|
||||
"output": "/etc/dovecot/sql/dovecot-dict-sql-quota.conf"
|
||||
},
|
||||
{
|
||||
"template": "dovecot-dict-sql-userdb.conf.j2",
|
||||
"output": "/etc/dovecot/sql/dovecot-dict-sql-userdb.conf"
|
||||
},
|
||||
{
|
||||
"template": "dovecot-dict-sql-sieve_before.conf.j2",
|
||||
"output": "/etc/dovecot/sql/dovecot-dict-sql-sieve_before.conf"
|
||||
},
|
||||
{
|
||||
"template": "dovecot-dict-sql-sieve_after.conf.j2",
|
||||
"output": "/etc/dovecot/sql/dovecot-dict-sql-sieve_after.conf"
|
||||
},
|
||||
{
|
||||
"template": "mail_plugins.j2",
|
||||
"output": "/etc/dovecot/mail_plugins"
|
||||
},
|
||||
{
|
||||
"template": "mail_plugins_imap.j2",
|
||||
"output": "/etc/dovecot/mail_plugins_imap"
|
||||
},
|
||||
{
|
||||
"template": "mail_plugins_lmtp.j2",
|
||||
"output": "/etc/dovecot/mail_plugins_lmtp"
|
||||
},
|
||||
{
|
||||
"template": "global_sieve_after.sieve.j2",
|
||||
"output": "/var/vmail/sieve/global_sieve_after.sieve"
|
||||
},
|
||||
{
|
||||
"template": "global_sieve_before.sieve.j2",
|
||||
"output": "/var/vmail/sieve/global_sieve_before.sieve"
|
||||
},
|
||||
{
|
||||
"template": "dovecot-master.passwd.j2",
|
||||
"output": "/etc/dovecot/dovecot-master.passwd"
|
||||
},
|
||||
{
|
||||
"template": "dovecot-master.userdb.j2",
|
||||
"output": "/etc/dovecot/dovecot-master.userdb"
|
||||
},
|
||||
{
|
||||
"template": "sieve.creds.j2",
|
||||
"output": "/etc/sogo/sieve.creds"
|
||||
},
|
||||
{
|
||||
"template": "sogo-sso.pass.j2",
|
||||
"output": "/etc/phpfpm/sogo-sso.pass"
|
||||
},
|
||||
{
|
||||
"template": "cron.creds.j2",
|
||||
"output": "/etc/sogo/cron.creds"
|
||||
},
|
||||
{
|
||||
"template": "source_env.sh.j2",
|
||||
"output": "/source_env.sh"
|
||||
},
|
||||
{
|
||||
"template": "maildir_gc.sh.j2",
|
||||
"output": "/usr/local/bin/maildir_gc.sh"
|
||||
},
|
||||
{
|
||||
"template": "dovecot.conf.j2",
|
||||
"output": "/etc/dovecot/dovecot.conf"
|
||||
}
|
||||
]
|
||||
1
data/conf/dovecot/config_templates/cron.creds.j2
Normal file
1
data/conf/dovecot/config_templates/cron.creds.j2
Normal file
@@ -0,0 +1 @@
|
||||
{{ RAND_USER }}@mailcow.local:{{ RAND_PASS2 }}
|
||||
@@ -0,0 +1,14 @@
|
||||
{% set QUOTA_TABLE = "quota2" if MASTER|lower in ["y", "yes"] else "quota2replica" %}
|
||||
connect = "host=/var/run/mysqld/mysqld.sock dbname={{ DBNAME }} user={{ DBUSER }} password={{ DBPASS | escape_quotes }}"
|
||||
map {
|
||||
pattern = priv/quota/storage
|
||||
table = {{ QUOTA_TABLE }}
|
||||
username_field = username
|
||||
value_field = bytes
|
||||
}
|
||||
map {
|
||||
pattern = priv/quota/messages
|
||||
table = {{ QUOTA_TABLE }}
|
||||
username_field = username
|
||||
value_field = messages
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
connect = "host=/var/run/mysqld/mysqld.sock dbname={{ DBNAME }} user={{ DBUSER }} password={{ DBPASS | escape_quotes }}"
|
||||
map {
|
||||
pattern = priv/sieve/name/$script_name
|
||||
table = sieve_after
|
||||
username_field = username
|
||||
value_field = id
|
||||
fields {
|
||||
script_name = $script_name
|
||||
}
|
||||
}
|
||||
map {
|
||||
pattern = priv/sieve/data/$id
|
||||
table = sieve_after
|
||||
username_field = username
|
||||
value_field = script_data
|
||||
fields {
|
||||
id = $id
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
connect = "host=/var/run/mysqld/mysqld.sock dbname={{ DBNAME }} user={{ DBUSER }} password={{ DBPASS | escape_quotes }}"
|
||||
map {
|
||||
pattern = priv/sieve/name/$script_name
|
||||
table = sieve_before
|
||||
username_field = username
|
||||
value_field = id
|
||||
fields {
|
||||
script_name = $script_name
|
||||
}
|
||||
}
|
||||
map {
|
||||
pattern = priv/sieve/data/$id
|
||||
table = sieve_before
|
||||
username_field = username
|
||||
value_field = script_data
|
||||
fields {
|
||||
id = $id
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
driver = mysql
|
||||
connect = "host=/var/run/mysqld/mysqld.sock dbname={{ DBNAME }} user={{ DBUSER }} password={{ DBPASS | escape_quotes }}"
|
||||
user_query = SELECT CONCAT(JSON_UNQUOTE(JSON_VALUE(attributes, '$.mailbox_format')), mailbox_path_prefix, '%d/%n/{{ MAILDIR_SUB }}:VOLATILEDIR=/var/volatile/%u:INDEX=/var/vmail_index/%u') AS mail, '%s' AS protocol, 5000 AS uid, 5000 AS gid, concat('*:bytes=', quota) AS quota_rule FROM mailbox WHERE username = '%u' AND (active = '1' OR active = '2')
|
||||
iterate_query = SELECT username FROM mailbox WHERE active = '1' OR active = '2';
|
||||
@@ -0,0 +1,3 @@
|
||||
{%- set master_user = DOVECOT_MASTER_USER or RAND_USER %}
|
||||
{%- set master_pass = DOVECOT_MASTER_PASS or RAND_PASS %}
|
||||
{{ master_user }}@mailcow.local:{SHA1}{{ master_pass | sha1 }}::::::
|
||||
@@ -0,0 +1 @@
|
||||
{{ DOVECOT_MASTER_USER or RAND_USER }}@mailcow.local::5000:5000::::
|
||||
@@ -1,23 +1,16 @@
|
||||
# --------------------------------------------------------------------------
|
||||
# Please create a file "extra.conf" for persistent overrides to dovecot.conf
|
||||
# --------------------------------------------------------------------------
|
||||
# LDAP example:
|
||||
#passdb {
|
||||
# args = /etc/dovecot/ldap/passdb.conf
|
||||
# driver = ldap
|
||||
#}
|
||||
|
||||
auth_mechanisms = plain login
|
||||
#mail_debug = yes
|
||||
#auth_debug = yes
|
||||
#log_debug = category=fts-flatcurve # Activate Logging for Flatcurve FTS Searchings
|
||||
log_path = syslog
|
||||
disable_plaintext_auth = yes
|
||||
|
||||
# Uncomment on NFS share
|
||||
#mmap_disable = yes
|
||||
#mail_fsync = always
|
||||
#mail_nfs_index = yes
|
||||
#mail_nfs_storage = yes
|
||||
|
||||
login_log_format_elements = "user=<%u> method=%m rip=%r lip=%l mpid=%e %c %k"
|
||||
mail_home = /var/vmail/%d/%n
|
||||
mail_location = maildir:~/
|
||||
@@ -78,7 +71,9 @@ service doveadm {
|
||||
}
|
||||
vsz_limit=2048 MB
|
||||
}
|
||||
!include /etc/dovecot/dovecot.folders.conf
|
||||
|
||||
{% include 'dovecot.folders.conf.j2' %}
|
||||
|
||||
protocols = imap sieve lmtp pop3
|
||||
service dict {
|
||||
unix_listener dict {
|
||||
@@ -193,7 +188,7 @@ protocol sieve {
|
||||
}
|
||||
plugin {
|
||||
# Allow "any" or "authenticated" to be used in ACLs
|
||||
acl_anyone = </etc/dovecot/acl_anyone
|
||||
acl_anyone = {{ ACL_ANYONE }}
|
||||
acl_shared_dict = file:/var/vmail/shared-mailboxes.db
|
||||
acl = vfile
|
||||
acl_user = %u
|
||||
@@ -249,7 +244,7 @@ plugin {
|
||||
mail_log_cached_only = yes
|
||||
|
||||
# Try set mail_replica
|
||||
!include_try /etc/dovecot/mail_replica.conf
|
||||
{% include 'mail_replica.conf.j2' %}
|
||||
}
|
||||
service quota-warning {
|
||||
executable = script /usr/local/bin/quota_notify.py
|
||||
@@ -300,12 +295,15 @@ service replicator {
|
||||
replication_max_conns = 10
|
||||
doveadm_port = 12345
|
||||
replication_dsync_parameters = -d -l 30 -U -n INBOX
|
||||
|
||||
{% include 'sogo_trusted_ip.conf.j2' %}
|
||||
{% include 'shared_namespace.conf.j2' %}
|
||||
{% include 'fts.conf.j2' %}
|
||||
{% include 'sni.conf.j2' %}
|
||||
|
||||
# <Includes>
|
||||
!include_try /etc/dovecot/sni.conf
|
||||
!include_try /etc/dovecot/sogo_trusted_ip.conf
|
||||
!include_try /etc/dovecot/extra.conf
|
||||
!include_try /etc/dovecot/shared_namespace.conf
|
||||
!include_try /etc/dovecot/conf.d/fts.conf
|
||||
# </Includes>
|
||||
|
||||
default_client_limit = 10400
|
||||
default_vsz_limit = 1024 M
|
||||
@@ -1,308 +1,308 @@
|
||||
namespace inbox {
|
||||
inbox = yes
|
||||
location =
|
||||
separator = /
|
||||
mailbox "Trash" {
|
||||
auto = subscribe
|
||||
special_use = \Trash
|
||||
}
|
||||
mailbox "Deleted Messages" {
|
||||
special_use = \Trash
|
||||
}
|
||||
mailbox "Deleted Items" {
|
||||
special_use = \Trash
|
||||
}
|
||||
mailbox "Rubbish" {
|
||||
special_use = \Trash
|
||||
}
|
||||
mailbox "Gelöschte Objekte" {
|
||||
special_use = \Trash
|
||||
}
|
||||
mailbox "Gelöschte Elemente" {
|
||||
special_use = \Trash
|
||||
}
|
||||
mailbox "Papierkorb" {
|
||||
special_use = \Trash
|
||||
}
|
||||
mailbox "Itens Excluidos" {
|
||||
special_use = \Trash
|
||||
}
|
||||
mailbox "Itens Excluídos" {
|
||||
special_use = \Trash
|
||||
}
|
||||
mailbox "Lixeira" {
|
||||
special_use = \Trash
|
||||
}
|
||||
mailbox "Prullenbak" {
|
||||
special_use = \Trash
|
||||
}
|
||||
mailbox "Odstránené položky" {
|
||||
special_use = \Trash
|
||||
}
|
||||
mailbox "Koš" {
|
||||
special_use = \Trash
|
||||
}
|
||||
mailbox "Verwijderde items" {
|
||||
special_use = \Trash
|
||||
}
|
||||
mailbox "Удаленные" {
|
||||
special_use = \Trash
|
||||
}
|
||||
mailbox "Удаленные элементы" {
|
||||
special_use = \Trash
|
||||
}
|
||||
mailbox "Корзина" {
|
||||
special_use = \Trash
|
||||
}
|
||||
mailbox "Видалені" {
|
||||
special_use = \Trash
|
||||
}
|
||||
mailbox "Видалені елементи" {
|
||||
special_use = \Trash
|
||||
}
|
||||
mailbox "Кошик" {
|
||||
special_use = \Trash
|
||||
}
|
||||
mailbox "废件箱" {
|
||||
special_use = \Trash
|
||||
}
|
||||
mailbox "已删除消息" {
|
||||
special_use = \Trash
|
||||
}
|
||||
mailbox "已删除邮件" {
|
||||
special_use = \Trash
|
||||
}
|
||||
mailbox "Archive" {
|
||||
auto = subscribe
|
||||
special_use = \Archive
|
||||
}
|
||||
mailbox "Archiv" {
|
||||
special_use = \Archive
|
||||
}
|
||||
mailbox "Archives" {
|
||||
special_use = \Archive
|
||||
}
|
||||
mailbox "Arquivo" {
|
||||
special_use = \Archive
|
||||
}
|
||||
mailbox "Arquivos" {
|
||||
special_use = \Archive
|
||||
}
|
||||
mailbox "Archief" {
|
||||
special_use = \Archive
|
||||
}
|
||||
mailbox "Archív" {
|
||||
special_use = \Archive
|
||||
}
|
||||
mailbox "Archivovať" {
|
||||
special_use = \Archive
|
||||
}
|
||||
mailbox "归档" {
|
||||
special_use = \Archive
|
||||
}
|
||||
mailbox "Архив" {
|
||||
special_use = \Archive
|
||||
}
|
||||
mailbox "Архів" {
|
||||
special_use = \Archive
|
||||
}
|
||||
mailbox "Sent" {
|
||||
auto = subscribe
|
||||
special_use = \Sent
|
||||
}
|
||||
mailbox "Sent Messages" {
|
||||
special_use = \Sent
|
||||
}
|
||||
mailbox "Sent Items" {
|
||||
special_use = \Sent
|
||||
}
|
||||
mailbox "已发送" {
|
||||
special_use = \Sent
|
||||
}
|
||||
mailbox "已发送消息" {
|
||||
special_use = \Sent
|
||||
}
|
||||
mailbox "已发送邮件" {
|
||||
special_use = \Sent
|
||||
}
|
||||
mailbox "Отправленные" {
|
||||
special_use = \Sent
|
||||
}
|
||||
mailbox "Отправленные элементы" {
|
||||
special_use = \Sent
|
||||
}
|
||||
mailbox "Надіслані" {
|
||||
special_use = \Sent
|
||||
}
|
||||
mailbox "Надіслані елементи" {
|
||||
special_use = \Sent
|
||||
}
|
||||
mailbox "Gesendet" {
|
||||
special_use = \Sent
|
||||
}
|
||||
mailbox "Gesendete Objekte" {
|
||||
special_use = \Sent
|
||||
}
|
||||
mailbox "Gesendete Elemente" {
|
||||
special_use = \Sent
|
||||
}
|
||||
mailbox "Itens Enviados" {
|
||||
special_use = \Sent
|
||||
}
|
||||
mailbox "Enviados" {
|
||||
special_use = \Sent
|
||||
}
|
||||
mailbox "Verzonden items" {
|
||||
special_use = \Sent
|
||||
}
|
||||
mailbox "Verzonden" {
|
||||
special_use = \Sent
|
||||
}
|
||||
mailbox "Odoslaná pošta" {
|
||||
special_use = \Sent
|
||||
}
|
||||
mailbox "Odoslané" {
|
||||
special_use = \Sent
|
||||
}
|
||||
mailbox "Drafts" {
|
||||
auto = subscribe
|
||||
special_use = \Drafts
|
||||
}
|
||||
mailbox "Entwürfe" {
|
||||
special_use = \Drafts
|
||||
}
|
||||
mailbox "Rascunhos" {
|
||||
special_use = \Drafts
|
||||
}
|
||||
mailbox "Concepten" {
|
||||
special_use = \Drafts
|
||||
}
|
||||
mailbox "Koncepty" {
|
||||
special_use = \Drafts
|
||||
}
|
||||
mailbox "草稿" {
|
||||
special_use = \Drafts
|
||||
}
|
||||
mailbox "草稿箱" {
|
||||
special_use = \Drafts
|
||||
}
|
||||
mailbox "Черновики" {
|
||||
special_use = \Drafts
|
||||
}
|
||||
mailbox "Чернетки" {
|
||||
special_use = \Drafts
|
||||
}
|
||||
mailbox "Junk" {
|
||||
auto = subscribe
|
||||
special_use = \Junk
|
||||
}
|
||||
mailbox "Junk-E-Mail" {
|
||||
special_use = \Junk
|
||||
}
|
||||
mailbox "Junk E-Mail" {
|
||||
special_use = \Junk
|
||||
}
|
||||
mailbox "Spam" {
|
||||
special_use = \Junk
|
||||
}
|
||||
mailbox "Lixo Eletrônico" {
|
||||
special_use = \Junk
|
||||
}
|
||||
mailbox "Nevyžiadaná pošta" {
|
||||
special_use = \Junk
|
||||
}
|
||||
mailbox "Infikované položky" {
|
||||
special_use = \Junk
|
||||
}
|
||||
mailbox "Ongewenste e-mail" {
|
||||
special_use = \Junk
|
||||
}
|
||||
mailbox "垃圾" {
|
||||
special_use = \Junk
|
||||
}
|
||||
mailbox "垃圾箱" {
|
||||
special_use = \Junk
|
||||
}
|
||||
mailbox "Нежелательная почта" {
|
||||
special_use = \Junk
|
||||
}
|
||||
mailbox "Спам" {
|
||||
special_use = \Junk
|
||||
}
|
||||
mailbox "Небажана пошта" {
|
||||
special_use = \Junk
|
||||
}
|
||||
mailbox "Koncepty" {
|
||||
special_use = \Drafts
|
||||
}
|
||||
mailbox "Nevyžádaná pošta" {
|
||||
special_use = \Junk
|
||||
}
|
||||
mailbox "Odstraněná pošta" {
|
||||
special_use = \Trash
|
||||
}
|
||||
mailbox "Odeslaná pošta" {
|
||||
special_use = \Sent
|
||||
}
|
||||
mailbox "Skräp" {
|
||||
special_use = \Trash
|
||||
}
|
||||
mailbox "Borttagna Meddelanden" {
|
||||
special_use = \Trash
|
||||
}
|
||||
mailbox "Arkiv" {
|
||||
special_use = \Archive
|
||||
}
|
||||
mailbox "Arkeverat" {
|
||||
special_use = \Archive
|
||||
}
|
||||
mailbox "Skickat" {
|
||||
special_use = \Sent
|
||||
}
|
||||
mailbox "Skickade Meddelanden" {
|
||||
special_use = \Sent
|
||||
}
|
||||
mailbox "Utkast" {
|
||||
special_use = \Drafts
|
||||
}
|
||||
mailbox "Skraldespand" {
|
||||
special_use = \Trash
|
||||
}
|
||||
mailbox "Slettet mails" {
|
||||
special_use = \Trash
|
||||
}
|
||||
mailbox "Arkiv" {
|
||||
special_use = \Archive
|
||||
}
|
||||
mailbox "Arkiveret mails" {
|
||||
special_use = \Archive
|
||||
}
|
||||
mailbox "Sendt" {
|
||||
special_use = \Sent
|
||||
}
|
||||
mailbox "Sendte mails" {
|
||||
special_use = \Sent
|
||||
}
|
||||
mailbox "Udkast" {
|
||||
special_use = \Drafts
|
||||
}
|
||||
mailbox "Kladde" {
|
||||
special_use = \Drafts
|
||||
}
|
||||
mailbox "Πρόχειρα" {
|
||||
special_use = \Drafts
|
||||
}
|
||||
mailbox "Απεσταλμένα" {
|
||||
special_use = \Sent
|
||||
}
|
||||
mailbox "Κάδος απορριμάτων" {
|
||||
special_use = \Trash
|
||||
}
|
||||
mailbox "Ανεπιθύμητα" {
|
||||
special_use = \Junk
|
||||
}
|
||||
mailbox "Αρχειοθετημένα" {
|
||||
special_use = \Archive
|
||||
}
|
||||
prefix =
|
||||
}
|
||||
namespace inbox {
|
||||
inbox = yes
|
||||
location =
|
||||
separator = /
|
||||
mailbox "Trash" {
|
||||
auto = subscribe
|
||||
special_use = \Trash
|
||||
}
|
||||
mailbox "Deleted Messages" {
|
||||
special_use = \Trash
|
||||
}
|
||||
mailbox "Deleted Items" {
|
||||
special_use = \Trash
|
||||
}
|
||||
mailbox "Rubbish" {
|
||||
special_use = \Trash
|
||||
}
|
||||
mailbox "Gelöschte Objekte" {
|
||||
special_use = \Trash
|
||||
}
|
||||
mailbox "Gelöschte Elemente" {
|
||||
special_use = \Trash
|
||||
}
|
||||
mailbox "Papierkorb" {
|
||||
special_use = \Trash
|
||||
}
|
||||
mailbox "Itens Excluidos" {
|
||||
special_use = \Trash
|
||||
}
|
||||
mailbox "Itens Excluídos" {
|
||||
special_use = \Trash
|
||||
}
|
||||
mailbox "Lixeira" {
|
||||
special_use = \Trash
|
||||
}
|
||||
mailbox "Prullenbak" {
|
||||
special_use = \Trash
|
||||
}
|
||||
mailbox "Odstránené položky" {
|
||||
special_use = \Trash
|
||||
}
|
||||
mailbox "Koš" {
|
||||
special_use = \Trash
|
||||
}
|
||||
mailbox "Verwijderde items" {
|
||||
special_use = \Trash
|
||||
}
|
||||
mailbox "Удаленные" {
|
||||
special_use = \Trash
|
||||
}
|
||||
mailbox "Удаленные элементы" {
|
||||
special_use = \Trash
|
||||
}
|
||||
mailbox "Корзина" {
|
||||
special_use = \Trash
|
||||
}
|
||||
mailbox "Видалені" {
|
||||
special_use = \Trash
|
||||
}
|
||||
mailbox "Видалені елементи" {
|
||||
special_use = \Trash
|
||||
}
|
||||
mailbox "Кошик" {
|
||||
special_use = \Trash
|
||||
}
|
||||
mailbox "废件箱" {
|
||||
special_use = \Trash
|
||||
}
|
||||
mailbox "已删除消息" {
|
||||
special_use = \Trash
|
||||
}
|
||||
mailbox "已删除邮件" {
|
||||
special_use = \Trash
|
||||
}
|
||||
mailbox "Archive" {
|
||||
auto = subscribe
|
||||
special_use = \Archive
|
||||
}
|
||||
mailbox "Archiv" {
|
||||
special_use = \Archive
|
||||
}
|
||||
mailbox "Archives" {
|
||||
special_use = \Archive
|
||||
}
|
||||
mailbox "Arquivo" {
|
||||
special_use = \Archive
|
||||
}
|
||||
mailbox "Arquivos" {
|
||||
special_use = \Archive
|
||||
}
|
||||
mailbox "Archief" {
|
||||
special_use = \Archive
|
||||
}
|
||||
mailbox "Archív" {
|
||||
special_use = \Archive
|
||||
}
|
||||
mailbox "Archivovať" {
|
||||
special_use = \Archive
|
||||
}
|
||||
mailbox "归档" {
|
||||
special_use = \Archive
|
||||
}
|
||||
mailbox "Архив" {
|
||||
special_use = \Archive
|
||||
}
|
||||
mailbox "Архів" {
|
||||
special_use = \Archive
|
||||
}
|
||||
mailbox "Sent" {
|
||||
auto = subscribe
|
||||
special_use = \Sent
|
||||
}
|
||||
mailbox "Sent Messages" {
|
||||
special_use = \Sent
|
||||
}
|
||||
mailbox "Sent Items" {
|
||||
special_use = \Sent
|
||||
}
|
||||
mailbox "已发送" {
|
||||
special_use = \Sent
|
||||
}
|
||||
mailbox "已发送消息" {
|
||||
special_use = \Sent
|
||||
}
|
||||
mailbox "已发送邮件" {
|
||||
special_use = \Sent
|
||||
}
|
||||
mailbox "Отправленные" {
|
||||
special_use = \Sent
|
||||
}
|
||||
mailbox "Отправленные элементы" {
|
||||
special_use = \Sent
|
||||
}
|
||||
mailbox "Надіслані" {
|
||||
special_use = \Sent
|
||||
}
|
||||
mailbox "Надіслані елементи" {
|
||||
special_use = \Sent
|
||||
}
|
||||
mailbox "Gesendet" {
|
||||
special_use = \Sent
|
||||
}
|
||||
mailbox "Gesendete Objekte" {
|
||||
special_use = \Sent
|
||||
}
|
||||
mailbox "Gesendete Elemente" {
|
||||
special_use = \Sent
|
||||
}
|
||||
mailbox "Itens Enviados" {
|
||||
special_use = \Sent
|
||||
}
|
||||
mailbox "Enviados" {
|
||||
special_use = \Sent
|
||||
}
|
||||
mailbox "Verzonden items" {
|
||||
special_use = \Sent
|
||||
}
|
||||
mailbox "Verzonden" {
|
||||
special_use = \Sent
|
||||
}
|
||||
mailbox "Odoslaná pošta" {
|
||||
special_use = \Sent
|
||||
}
|
||||
mailbox "Odoslané" {
|
||||
special_use = \Sent
|
||||
}
|
||||
mailbox "Drafts" {
|
||||
auto = subscribe
|
||||
special_use = \Drafts
|
||||
}
|
||||
mailbox "Entwürfe" {
|
||||
special_use = \Drafts
|
||||
}
|
||||
mailbox "Rascunhos" {
|
||||
special_use = \Drafts
|
||||
}
|
||||
mailbox "Concepten" {
|
||||
special_use = \Drafts
|
||||
}
|
||||
mailbox "Koncepty" {
|
||||
special_use = \Drafts
|
||||
}
|
||||
mailbox "草稿" {
|
||||
special_use = \Drafts
|
||||
}
|
||||
mailbox "草稿箱" {
|
||||
special_use = \Drafts
|
||||
}
|
||||
mailbox "Черновики" {
|
||||
special_use = \Drafts
|
||||
}
|
||||
mailbox "Чернетки" {
|
||||
special_use = \Drafts
|
||||
}
|
||||
mailbox "Junk" {
|
||||
auto = subscribe
|
||||
special_use = \Junk
|
||||
}
|
||||
mailbox "Junk-E-Mail" {
|
||||
special_use = \Junk
|
||||
}
|
||||
mailbox "Junk E-Mail" {
|
||||
special_use = \Junk
|
||||
}
|
||||
mailbox "Spam" {
|
||||
special_use = \Junk
|
||||
}
|
||||
mailbox "Lixo Eletrônico" {
|
||||
special_use = \Junk
|
||||
}
|
||||
mailbox "Nevyžiadaná pošta" {
|
||||
special_use = \Junk
|
||||
}
|
||||
mailbox "Infikované položky" {
|
||||
special_use = \Junk
|
||||
}
|
||||
mailbox "Ongewenste e-mail" {
|
||||
special_use = \Junk
|
||||
}
|
||||
mailbox "垃圾" {
|
||||
special_use = \Junk
|
||||
}
|
||||
mailbox "垃圾箱" {
|
||||
special_use = \Junk
|
||||
}
|
||||
mailbox "Нежелательная почта" {
|
||||
special_use = \Junk
|
||||
}
|
||||
mailbox "Спам" {
|
||||
special_use = \Junk
|
||||
}
|
||||
mailbox "Небажана пошта" {
|
||||
special_use = \Junk
|
||||
}
|
||||
mailbox "Koncepty" {
|
||||
special_use = \Drafts
|
||||
}
|
||||
mailbox "Nevyžádaná pošta" {
|
||||
special_use = \Junk
|
||||
}
|
||||
mailbox "Odstraněná pošta" {
|
||||
special_use = \Trash
|
||||
}
|
||||
mailbox "Odeslaná pošta" {
|
||||
special_use = \Sent
|
||||
}
|
||||
mailbox "Skräp" {
|
||||
special_use = \Trash
|
||||
}
|
||||
mailbox "Borttagna Meddelanden" {
|
||||
special_use = \Trash
|
||||
}
|
||||
mailbox "Arkiv" {
|
||||
special_use = \Archive
|
||||
}
|
||||
mailbox "Arkeverat" {
|
||||
special_use = \Archive
|
||||
}
|
||||
mailbox "Skickat" {
|
||||
special_use = \Sent
|
||||
}
|
||||
mailbox "Skickade Meddelanden" {
|
||||
special_use = \Sent
|
||||
}
|
||||
mailbox "Utkast" {
|
||||
special_use = \Drafts
|
||||
}
|
||||
mailbox "Skraldespand" {
|
||||
special_use = \Trash
|
||||
}
|
||||
mailbox "Slettet mails" {
|
||||
special_use = \Trash
|
||||
}
|
||||
mailbox "Arkiv" {
|
||||
special_use = \Archive
|
||||
}
|
||||
mailbox "Arkiveret mails" {
|
||||
special_use = \Archive
|
||||
}
|
||||
mailbox "Sendt" {
|
||||
special_use = \Sent
|
||||
}
|
||||
mailbox "Sendte mails" {
|
||||
special_use = \Sent
|
||||
}
|
||||
mailbox "Udkast" {
|
||||
special_use = \Drafts
|
||||
}
|
||||
mailbox "Kladde" {
|
||||
special_use = \Drafts
|
||||
}
|
||||
mailbox "Πρόχειρα" {
|
||||
special_use = \Drafts
|
||||
}
|
||||
mailbox "Απεσταλμένα" {
|
||||
special_use = \Sent
|
||||
}
|
||||
mailbox "Κάδος απορριμάτων" {
|
||||
special_use = \Trash
|
||||
}
|
||||
mailbox "Ανεπιθύμητα" {
|
||||
special_use = \Junk
|
||||
}
|
||||
mailbox "Αρχειοθετημένα" {
|
||||
special_use = \Archive
|
||||
}
|
||||
prefix =
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
# mailcow FTS Flatcurve Settings, change them as you like.
|
||||
{% if SKIP_FTS|lower in ['n', 'no'] %}
|
||||
plugin {
|
||||
fts_autoindex = yes
|
||||
fts_autoindex_exclude = \Junk
|
||||
@@ -24,14 +24,11 @@ plugin {
|
||||
fts_index_timeout = 300s
|
||||
}
|
||||
|
||||
### THIS PART WILL BE CHANGED BY MODIFYING mailcow.conf AUTOMATICALLY DURING RUNTIME! ###
|
||||
|
||||
service indexer-worker {
|
||||
# Max amount of simultaniously running indexer jobs.
|
||||
process_limit=1
|
||||
process_limit = {{ FTS_PROCS }}
|
||||
|
||||
# Max amount of RAM used by EACH indexer process.
|
||||
vsz_limit=128 MB
|
||||
vsz_limit = {{ FTS_HEAP }} MB
|
||||
}
|
||||
|
||||
### THIS PART WILL BE CHANGED BY MODIFYING mailcow.conf AUTOMATICALLY DURING RUNTIME! ###
|
||||
{% endif %}
|
||||
5
data/conf/dovecot/config_templates/mail_plugins.j2
Normal file
5
data/conf/dovecot/config_templates/mail_plugins.j2
Normal file
@@ -0,0 +1,5 @@
|
||||
{%- if SKIP_FTS|lower in ["y", "yes"] -%}
|
||||
quota acl zlib mail_crypt mail_crypt_acl mail_log notify listescape replication lazy_expunge
|
||||
{%- else -%}
|
||||
quota acl zlib mail_crypt mail_crypt_acl mail_log notify fts fts_flatcurve listescape replication lazy_expunge
|
||||
{%- endif -%}
|
||||
5
data/conf/dovecot/config_templates/mail_plugins_imap.j2
Normal file
5
data/conf/dovecot/config_templates/mail_plugins_imap.j2
Normal file
@@ -0,0 +1,5 @@
|
||||
{%- if SKIP_FTS|lower in ["y", "yes"] -%}
|
||||
quota imap_quota imap_acl acl zlib imap_zlib imap_sieve mail_crypt mail_crypt_acl notify listescape replication mail_log
|
||||
{%- else -%}
|
||||
quota imap_quota imap_acl acl zlib imap_zlib imap_sieve mail_crypt mail_crypt_acl notify mail_log fts fts_flatcurve listescape replication
|
||||
{%- endif -%}
|
||||
5
data/conf/dovecot/config_templates/mail_plugins_lmtp.j2
Normal file
5
data/conf/dovecot/config_templates/mail_plugins_lmtp.j2
Normal file
@@ -0,0 +1,5 @@
|
||||
{%- if SKIP_FTS|lower in ["y", "yes"] -%}
|
||||
quota sieve acl zlib mail_crypt mail_crypt_acl notify listescape replication
|
||||
{%- else -%}
|
||||
quota sieve acl zlib mail_crypt mail_crypt_acl fts fts_flatcurve notify listescape replication
|
||||
{%- endif -%}
|
||||
3
data/conf/dovecot/config_templates/mail_replica.conf.j2
Normal file
3
data/conf/dovecot/config_templates/mail_replica.conf.j2
Normal file
@@ -0,0 +1,3 @@
|
||||
{% if MAILCOW_REPLICA_IP and DOVEADM_REPLICA_PORT %}
|
||||
mail_replica = tcp:{{ MAILCOW_REPLICA_IP }}:{{ DOVEADM_REPLICA_PORT }}
|
||||
{% endif %}
|
||||
2
data/conf/dovecot/config_templates/maildir_gc.sh.j2
Normal file
2
data/conf/dovecot/config_templates/maildir_gc.sh.j2
Normal file
@@ -0,0 +1,2 @@
|
||||
#!/bin/bash
|
||||
[ -d /var/vmail/_garbage/ ] && /usr/bin/find /var/vmail/_garbage/ -mindepth 1 -maxdepth 1 -type d -cmin +{{ MAILDIR_GC_TIME }} -exec rm -r {} \;
|
||||
@@ -0,0 +1,9 @@
|
||||
{% set MAILDIR_SUB_SHARED = '' if not MAILDIR_SUB else '/' ~ MAILDIR_SUB %}
|
||||
namespace {
|
||||
type = shared
|
||||
separator = /
|
||||
prefix = Shared/%%u/
|
||||
location = maildir:%%h{{ MAILDIR_SUB_SHARED }}:INDEX=~{{ MAILDIR_SUB_SHARED }}/Shared/%%u
|
||||
subscriptions = no
|
||||
list = children
|
||||
}
|
||||
1
data/conf/dovecot/config_templates/sieve.creds.j2
Normal file
1
data/conf/dovecot/config_templates/sieve.creds.j2
Normal file
@@ -0,0 +1 @@
|
||||
{{ DOVECOT_MASTER_USER or RAND_USER }}@mailcow.local:{{ DOVECOT_MASTER_PASS or RAND_PASS }}
|
||||
6
data/conf/dovecot/config_templates/sni.conf.j2
Normal file
6
data/conf/dovecot/config_templates/sni.conf.j2
Normal file
@@ -0,0 +1,6 @@
|
||||
{% for domain, path in VALID_CERT_DIRS.items() %}
|
||||
local_name "{{ domain }}" {
|
||||
ssl_cert = <{{ path }}/cert.pem
|
||||
ssl_key = <{{ path }}/key.pem
|
||||
}
|
||||
{% endfor %}
|
||||
1
data/conf/dovecot/config_templates/sogo-sso.pass.j2
Normal file
1
data/conf/dovecot/config_templates/sogo-sso.pass.j2
Normal file
@@ -0,0 +1 @@
|
||||
{{ RAND_PASS2 }}
|
||||
@@ -0,0 +1,3 @@
|
||||
remote {{ SOGO_HOST }} {
|
||||
disable_plaintext_auth = no
|
||||
}
|
||||
3
data/conf/dovecot/config_templates/source_env.sh.j2
Normal file
3
data/conf/dovecot/config_templates/source_env.sh.j2
Normal file
@@ -0,0 +1,3 @@
|
||||
{% for key, value in ENV_VARS.items() %}
|
||||
export {{ key }}="{{ value | replace('"', '\\"') }}"
|
||||
{% endfor %}
|
||||
0
data/conf/dovecot/custom_templates/.gitkeep
Normal file
0
data/conf/dovecot/custom_templates/.gitkeep
Normal file
@@ -1,9 +0,0 @@
|
||||
#hosts = 1.2.3.4
|
||||
#dn = cn=admin,dc=example,dc=local
|
||||
#dnpass = password
|
||||
#ldap_version = 3
|
||||
#base = ou=People,dc=example,dc=local
|
||||
#auth_bind = no
|
||||
#pass_filter = (&(objectClass=posixAccount)(mail=%u))
|
||||
#pass_attrs = mail=user,userPassword=password
|
||||
#default_pass_scheme = SSHA
|
||||
6
data/conf/mysql/config.json
Normal file
6
data/conf/mysql/config.json
Normal file
@@ -0,0 +1,6 @@
|
||||
[
|
||||
{
|
||||
"template": "my.cnf.j2",
|
||||
"output": "/etc/mysql/conf.d/my.cnf"
|
||||
}
|
||||
]
|
||||
0
data/conf/mysql/custom_templates/.gitkeep
Normal file
0
data/conf/mysql/custom_templates/.gitkeep
Normal file
22
data/conf/nginx/config.json
Normal file
22
data/conf/nginx/config.json
Normal file
@@ -0,0 +1,22 @@
|
||||
[
|
||||
{
|
||||
"template": "nginx.conf.j2",
|
||||
"output": "/etc/nginx/nginx.conf"
|
||||
},
|
||||
{
|
||||
"template": "sites-default.conf.j2",
|
||||
"output": "/etc/nginx/includes/sites-default.conf"
|
||||
},
|
||||
{
|
||||
"template": "server_name.active.j2",
|
||||
"output": "/etc/nginx/conf.d/server_name.active"
|
||||
},
|
||||
{
|
||||
"template": "listen_plain.active.j2",
|
||||
"output": "/etc/nginx/conf.d/listen_plain.active"
|
||||
},
|
||||
{
|
||||
"template": "listen_ssl.active.j2",
|
||||
"output": "/etc/nginx/conf.d/listen_ssl.active"
|
||||
}
|
||||
]
|
||||
2
data/conf/nginx/config_templates/listen_plain.active.j2
Normal file
2
data/conf/nginx/config_templates/listen_plain.active.j2
Normal file
@@ -0,0 +1,2 @@
|
||||
listen {{ HTTP_PORT }};
|
||||
listen [::]:{{ HTTP_PORT }};
|
||||
2
data/conf/nginx/config_templates/listen_ssl.active.j2
Normal file
2
data/conf/nginx/config_templates/listen_ssl.active.j2
Normal file
@@ -0,0 +1,2 @@
|
||||
listen {{ HTTPS_PORT }} ssl http2;
|
||||
listen [::]:{{ HTTPS_PORT }} ssl http2;
|
||||
@@ -41,7 +41,7 @@ http {
|
||||
https https;
|
||||
}
|
||||
|
||||
{% if HTTP_REDIRECT %}
|
||||
{% if HTTP_REDIRECT|lower in ["y", "yes"] %}
|
||||
# HTTP to HTTPS redirect
|
||||
server {
|
||||
root /web;
|
||||
@@ -65,16 +65,16 @@ http {
|
||||
server {
|
||||
listen 127.0.0.1:65510; # sogo-auth verify internal
|
||||
|
||||
{% if not HTTP_REDIRECT %}
|
||||
listen {{ HTTP_PORT }}{% if NGINX_USE_PROXY_PROTOCOL %} proxy_protocol{%endif%};
|
||||
{% if not HTTP_REDIRECT|lower in ["y", "yes"] %}
|
||||
listen {{ HTTP_PORT }}{% if NGINX_USE_PROXY_PROTOCOL|lower in ["y", "yes"] %} proxy_protocol{%endif%};
|
||||
{%endif%}
|
||||
listen {{ HTTPS_PORT }}{% if NGINX_USE_PROXY_PROTOCOL %} proxy_protocol{%endif%} ssl;
|
||||
listen {{ HTTPS_PORT }}{% if NGINX_USE_PROXY_PROTOCOL|lower in ["y", "yes"] %} proxy_protocol{%endif%} ssl;
|
||||
|
||||
{% if not DISABLE_IPv6 %}
|
||||
{% if not HTTP_REDIRECT %}
|
||||
listen [::]:{{ HTTP_PORT }}{% if NGINX_USE_PROXY_PROTOCOL %} proxy_protocol{%endif%};
|
||||
{% if not DISABLE_IPv6|lower in ["y", "yes"] %}
|
||||
{% if not HTTP_REDIRECT|lower in ["y", "yes"] %}
|
||||
listen [::]:{{ HTTP_PORT }}{% if NGINX_USE_PROXY_PROTOCOL|lower in ["y", "yes"] %} proxy_protocol{%endif%};
|
||||
{%endif%}
|
||||
listen [::]:{{ HTTPS_PORT }}{% if NGINX_USE_PROXY_PROTOCOL %} proxy_protocol{%endif%} ssl;
|
||||
listen [::]:{{ HTTPS_PORT }}{% if NGINX_USE_PROXY_PROTOCOL|lower in ["y", "yes"] %} proxy_protocol{%endif%} ssl;
|
||||
{%endif%}
|
||||
|
||||
http2 on;
|
||||
@@ -92,16 +92,16 @@ http {
|
||||
server {
|
||||
listen 127.0.0.1:65510; # sogo-auth verify internal
|
||||
|
||||
{% if not HTTP_REDIRECT %}
|
||||
listen {{ HTTP_PORT }}{% if NGINX_USE_PROXY_PROTOCOL %} proxy_protocol{%endif%};
|
||||
{% if not HTTP_REDIRECT|lower in ["y", "yes"] %}
|
||||
listen {{ HTTP_PORT }}{% if NGINX_USE_PROXY_PROTOCOL|lower in ["y", "yes"] %} proxy_protocol{%endif%};
|
||||
{%endif%}
|
||||
listen {{ HTTPS_PORT }}{% if NGINX_USE_PROXY_PROTOCOL %} proxy_protocol{%endif%} ssl;
|
||||
listen {{ HTTPS_PORT }}{% if NGINX_USE_PROXY_PROTOCOL|lower in ["y", "yes"] %} proxy_protocol{%endif%} ssl;
|
||||
|
||||
{% if not DISABLE_IPv6 %}
|
||||
{% if not HTTP_REDIRECT %}
|
||||
listen [::]:{{ HTTP_PORT }}{% if NGINX_USE_PROXY_PROTOCOL %} proxy_protocol{%endif%};
|
||||
{% if not DISABLE_IPv6|lower in ["y", "yes"] %}
|
||||
{% if not HTTP_REDIRECT|lower in ["y", "yes"] %}
|
||||
listen [::]:{{ HTTP_PORT }}{% if NGINX_USE_PROXY_PROTOCOL|lower in ["y", "yes"] %} proxy_protocol{%endif%};
|
||||
{%endif%}
|
||||
listen [::]:{{ HTTPS_PORT }}{% if NGINX_USE_PROXY_PROTOCOL %} proxy_protocol{%endif%} ssl;
|
||||
listen [::]:{{ HTTPS_PORT }}{% if NGINX_USE_PROXY_PROTOCOL|lower in ["y", "yes"] %} proxy_protocol{%endif%} ssl;
|
||||
{%endif%}
|
||||
|
||||
http2 on;
|
||||
@@ -118,7 +118,7 @@ http {
|
||||
# rspamd dynmaps:
|
||||
server {
|
||||
listen 8081;
|
||||
{% if not DISABLE_IPv6 %}
|
||||
{% if not DISABLE_IPv6|lower in ["y", "yes"] %}
|
||||
listen [::]:8081;
|
||||
{%endif%}
|
||||
index index.php index.html;
|
||||
@@ -130,7 +130,7 @@ http {
|
||||
location ~ \.php$ {
|
||||
try_files $uri =404;
|
||||
fastcgi_split_path_info ^(.+\.php)(/.+)$;
|
||||
fastcgi_pass {{ PHPFPMHOST }}:9001;
|
||||
fastcgi_pass {{ PHPFPM_HOST }}:9001;
|
||||
fastcgi_index index.php;
|
||||
include fastcgi_params;
|
||||
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||
@@ -151,7 +151,7 @@ http {
|
||||
client_max_body_size 10M;
|
||||
try_files $uri =404;
|
||||
fastcgi_split_path_info ^(.+\.php)(/.+)$;
|
||||
fastcgi_pass {{ PHPFPMHOST }}:9001;
|
||||
fastcgi_pass {{ PHPFPM_HOST }}:9001;
|
||||
fastcgi_index pipe.php;
|
||||
include fastcgi_params;
|
||||
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||
@@ -175,7 +175,7 @@ http {
|
||||
client_max_body_size 10M;
|
||||
try_files $uri =404;
|
||||
fastcgi_split_path_info ^(.+\.php)(/.+)$;
|
||||
fastcgi_pass phpfpm:9001;
|
||||
fastcgi_pass {{ PHPFPM_HOST }}:9001;
|
||||
include fastcgi_params;
|
||||
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||
fastcgi_param PATH_INFO $fastcgi_path_info;
|
||||
@@ -184,18 +184,18 @@ http {
|
||||
|
||||
include /etc/nginx/conf.d/*.conf;
|
||||
|
||||
{% for cert in valid_cert_dirs %}
|
||||
{% for cert in VALID_CERT_DIRS %}
|
||||
server {
|
||||
{% if not HTTP_REDIRECT %}
|
||||
listen {{ HTTP_PORT }}{% if NGINX_USE_PROXY_PROTOCOL %} proxy_protocol{%endif%};
|
||||
{% if not HTTP_REDIRECT|lower in ["y", "yes"] %}
|
||||
listen {{ HTTP_PORT }}{% if NGINX_USE_PROXY_PROTOCOL|lower in ["y", "yes"] %} proxy_protocol{%endif%};
|
||||
{%endif%}
|
||||
listen {{ HTTPS_PORT }}{% if NGINX_USE_PROXY_PROTOCOL %} proxy_protocol{%endif%} ssl;
|
||||
listen {{ HTTPS_PORT }}{% if NGINX_USE_PROXY_PROTOCOL|lower in ["y", "yes"] %} proxy_protocol{%endif%} ssl;
|
||||
|
||||
{% if not DISABLE_IPv6 %}
|
||||
{% if not HTTP_REDIRECT %}
|
||||
listen [::]:{{ HTTP_PORT }}{% if NGINX_USE_PROXY_PROTOCOL %} proxy_protocol{%endif%};
|
||||
{% if not DISABLE_IPv6|lower in ["y", "yes"] %}
|
||||
{% if not HTTP_REDIRECT|lower in ["y", "yes"] %}
|
||||
listen [::]:{{ HTTP_PORT }}{% if NGINX_USE_PROXY_PROTOCOL|lower in ["y", "yes"] %} proxy_protocol{%endif%};
|
||||
{%endif%}
|
||||
listen [::]:{{ HTTPS_PORT }}{% if NGINX_USE_PROXY_PROTOCOL %} proxy_protocol{%endif%} ssl;
|
||||
listen [::]:{{ HTTPS_PORT }}{% if NGINX_USE_PROXY_PROTOCOL|lower in ["y", "yes"] %} proxy_protocol{%endif%} ssl;
|
||||
{%endif%}
|
||||
|
||||
http2 on;
|
||||
1
data/conf/nginx/config_templates/server_name.active.j2
Normal file
1
data/conf/nginx/config_templates/server_name.active.j2
Normal file
@@ -0,0 +1 @@
|
||||
server_name {{ MAILCOW_HOSTNAME }} autodiscover.* autoconfig.* {{ ADDITIONAL_SERVER_NAMES | join(' ') }};
|
||||
@@ -55,7 +55,7 @@ set_real_ip_from fc00::/7;
|
||||
{% for TRUSTED_PROXY in TRUSTED_PROXIES %}
|
||||
set_real_ip_from {{ TRUSTED_PROXY }};
|
||||
{% endfor %}
|
||||
{% if not NGINX_USE_PROXY_PROTOCOL %}
|
||||
{% if not NGINX_USE_PROXY_PROTOCOL|lower in ["y", "yes"] %}
|
||||
real_ip_header X-Forwarded-For;
|
||||
{% else %}
|
||||
real_ip_header proxy_protocol;
|
||||
@@ -104,7 +104,7 @@ location ~ ^/cache/(.*)$ {
|
||||
location ~ \.php$ {
|
||||
try_files $uri =404;
|
||||
fastcgi_split_path_info ^(.+\.php)(/.+)$;
|
||||
fastcgi_pass {{ PHPFPMHOST }}:9002;
|
||||
fastcgi_pass {{ PHPFPM_HOST }}:9002;
|
||||
fastcgi_index index.php;
|
||||
include /etc/nginx/fastcgi_params;
|
||||
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||
@@ -115,7 +115,7 @@ location ~ \.php$ {
|
||||
|
||||
location ~* ^/Autodiscover/Autodiscover.xml {
|
||||
fastcgi_split_path_info ^(.+\.php)(/.+)$;
|
||||
fastcgi_pass {{ PHPFPMHOST }}:9002;
|
||||
fastcgi_pass {{ PHPFPM_HOST }}:9002;
|
||||
include /etc/nginx/fastcgi_params;
|
||||
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||
try_files /autodiscover.php =404;
|
||||
@@ -123,7 +123,7 @@ location ~* ^/Autodiscover/Autodiscover.xml {
|
||||
|
||||
location ~* ^/Autodiscover/Autodiscover.json {
|
||||
fastcgi_split_path_info ^(.+\.php)(/.+)$;
|
||||
fastcgi_pass {{ PHPFPMHOST }}:9002;
|
||||
fastcgi_pass {{ PHPFPM_HOST }}:9002;
|
||||
include /etc/nginx/fastcgi_params;
|
||||
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||
try_files /autodiscover-json.php =404;
|
||||
@@ -131,34 +131,34 @@ location ~* ^/Autodiscover/Autodiscover.json {
|
||||
|
||||
location ~ /(?:m|M)ail/(?:c|C)onfig-v1.1.xml {
|
||||
fastcgi_split_path_info ^(.+\.php)(/.+)$;
|
||||
fastcgi_pass {{ PHPFPMHOST }}:9002;
|
||||
fastcgi_pass {{ PHPFPM_HOST }}:9002;
|
||||
include /etc/nginx/fastcgi_params;
|
||||
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||
try_files /autoconfig.php =404;
|
||||
}
|
||||
|
||||
{% if not SKIP_RSPAMD %}
|
||||
{% if not SKIP_RSPAMD|lower in ["y", "yes"] %}
|
||||
location /rspamd/ {
|
||||
location /rspamd/auth {
|
||||
# proxy_pass is not inherited
|
||||
proxy_pass http://{{ RSPAMDHOST }}:11334/auth;
|
||||
proxy_pass http://{{ RSPAMD_HOST }}:11334/auth;
|
||||
proxy_intercept_errors on;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Forwarded-For {% if not NGINX_USE_PROXY_PROTOCOL %}$proxy_add_x_forwarded_for{% else %}$proxy_protocol_addr{%endif%};
|
||||
proxy_set_header X-Real-IP {% if not NGINX_USE_PROXY_PROTOCOL %}$remote_addr{% else %}$proxy_protocol_addr{%endif%};
|
||||
proxy_set_header X-Forwarded-For {% if not NGINX_USE_PROXY_PROTOCOL|lower in ["y", "yes"] %}$proxy_add_x_forwarded_for{% else %}$proxy_protocol_addr{%endif%};
|
||||
proxy_set_header X-Real-IP {% if not NGINX_USE_PROXY_PROTOCOL|lower in ["y", "yes"] %}$remote_addr{% else %}$proxy_protocol_addr{%endif%};
|
||||
proxy_redirect off;
|
||||
error_page 401 /_rspamderror.php;
|
||||
}
|
||||
|
||||
proxy_pass http://{{ RSPAMDHOST }}:11334/;
|
||||
proxy_pass http://{{ RSPAMD_HOST }}:11334/;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Forwarded-For {% if not NGINX_USE_PROXY_PROTOCOL %}$proxy_add_x_forwarded_for{% else %}$proxy_protocol_addr{%endif%};
|
||||
proxy_set_header X-Real-IP {% if not NGINX_USE_PROXY_PROTOCOL %}$remote_addr{% else %}$proxy_protocol_addr{%endif%};
|
||||
proxy_set_header X-Forwarded-For {% if not NGINX_USE_PROXY_PROTOCOL|lower in ["y", "yes"] %}$proxy_add_x_forwarded_for{% else %}$proxy_protocol_addr{%endif%};
|
||||
proxy_set_header X-Real-IP {% if not NGINX_USE_PROXY_PROTOCOL|lower in ["y", "yes"] %}$remote_addr{% else %}$proxy_protocol_addr{%endif%};
|
||||
proxy_redirect off;
|
||||
}
|
||||
{% endif %}
|
||||
|
||||
{% if not SKIP_SOGO %}
|
||||
{% if not SKIP_SOGO|lower in ["y", "yes"] %}
|
||||
location ^~ /principals {
|
||||
return 301 /SOGo/dav;
|
||||
}
|
||||
@@ -182,10 +182,10 @@ location ^~ /Microsoft-Server-ActiveSync {
|
||||
proxy_set_header Authorization "$auth";
|
||||
proxy_set_header x-webobjects-auth-type "$auth_type";
|
||||
|
||||
proxy_pass http://{{ SOGOHOST }}:20000/SOGo/Microsoft-Server-ActiveSync;
|
||||
proxy_pass http://{{ SOGO_HOST }}:20000/SOGo/Microsoft-Server-ActiveSync;
|
||||
|
||||
proxy_set_header X-Forwarded-For {% if not NGINX_USE_PROXY_PROTOCOL %}$proxy_add_x_forwarded_for{% else %}$proxy_protocol_addr{%endif%};
|
||||
proxy_set_header X-Real-IP {% if not NGINX_USE_PROXY_PROTOCOL %}$remote_addr{% else %}$proxy_protocol_addr{%endif%};
|
||||
proxy_set_header X-Forwarded-For {% if not NGINX_USE_PROXY_PROTOCOL|lower in ["y", "yes"] %}$proxy_add_x_forwarded_for{% else %}$proxy_protocol_addr{%endif%};
|
||||
proxy_set_header X-Real-IP {% if not NGINX_USE_PROXY_PROTOCOL|lower in ["y", "yes"] %}$remote_addr{% else %}$proxy_protocol_addr{%endif%};
|
||||
proxy_connect_timeout 75;
|
||||
proxy_send_timeout 3600;
|
||||
proxy_read_timeout 3600;
|
||||
@@ -207,10 +207,10 @@ location ^~ /SOGo {
|
||||
proxy_set_header Authorization "$auth";
|
||||
proxy_set_header x-webobjects-auth-type "$auth_type";
|
||||
|
||||
proxy_pass http://{{ SOGOHOST }}:20000;
|
||||
proxy_pass http://{{ SOGO_HOST }}:20000;
|
||||
|
||||
proxy_set_header X-Forwarded-For {% if not NGINX_USE_PROXY_PROTOCOL %}$proxy_add_x_forwarded_for{% else %}$proxy_protocol_addr{%endif%};
|
||||
proxy_set_header X-Real-IP {% if not NGINX_USE_PROXY_PROTOCOL %}$remote_addr{% else %}$proxy_protocol_addr{%endif%};
|
||||
proxy_set_header X-Forwarded-For {% if not NGINX_USE_PROXY_PROTOCOL|lower in ["y", "yes"] %}$proxy_add_x_forwarded_for{% else %}$proxy_protocol_addr{%endif%};
|
||||
proxy_set_header X-Real-IP {% if not NGINX_USE_PROXY_PROTOCOL|lower in ["y", "yes"] %}$remote_addr{% else %}$proxy_protocol_addr{%endif%};
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header x-webobjects-server-protocol HTTP/1.0;
|
||||
proxy_set_header x-webobjects-remote-host $remote_addr;
|
||||
@@ -229,10 +229,10 @@ location ^~ /SOGo {
|
||||
proxy_set_header Authorization "$auth";
|
||||
proxy_set_header x-webobjects-auth-type "$auth_type";
|
||||
|
||||
proxy_pass http://{{ SOGOHOST }}:20000;
|
||||
proxy_pass http://{{ SOGO_HOST }}:20000;
|
||||
|
||||
proxy_set_header X-Forwarded-For {% if not NGINX_USE_PROXY_PROTOCOL %}$proxy_add_x_forwarded_for{% else %}$proxy_protocol_addr{%endif%};
|
||||
proxy_set_header X-Real-IP {% if not NGINX_USE_PROXY_PROTOCOL %}$remote_addr{% else %}$proxy_protocol_addr{%endif%};
|
||||
proxy_set_header X-Forwarded-For {% if not NGINX_USE_PROXY_PROTOCOL|lower in ["y", "yes"] %}$proxy_add_x_forwarded_for{% else %}$proxy_protocol_addr{%endif%};
|
||||
proxy_set_header X-Real-IP {% if not NGINX_USE_PROXY_PROTOCOL|lower in ["y", "yes"] %}$remote_addr{% else %}$proxy_protocol_addr{%endif%};
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header x-webobjects-server-protocol HTTP/1.0;
|
||||
proxy_set_header x-webobjects-remote-host $remote_addr;
|
||||
0
data/conf/nginx/custom_templates/.gitkeep
Normal file
0
data/conf/nginx/custom_templates/.gitkeep
Normal file
26
data/conf/phpfpm/config.json
Normal file
26
data/conf/phpfpm/config.json
Normal file
@@ -0,0 +1,26 @@
|
||||
[
|
||||
{
|
||||
"template": "opcache-recommended.ini.j2",
|
||||
"output": "/usr/local/etc/php/conf.d/opcache-recommended.ini"
|
||||
},
|
||||
{
|
||||
"template": "pools.conf.j2",
|
||||
"output": "/usr/local/etc/php-fpm.d/z-pools.conf"
|
||||
},
|
||||
{
|
||||
"template": "other.ini.j2",
|
||||
"output": "/usr/local/etc/php/conf.d/zzz-other.ini"
|
||||
},
|
||||
{
|
||||
"template": "upload.ini.j2",
|
||||
"output": "/usr/local/etc/php/conf.d/upload.ini"
|
||||
},
|
||||
{
|
||||
"template": "session_store.ini.j2",
|
||||
"output": "/usr/local/etc/php/conf.d/session_store.ini"
|
||||
},
|
||||
{
|
||||
"template": "0081-custom-mailcow.css.j2",
|
||||
"output": "/web/css/build/0081-custom-mailcow.css"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,3 @@
|
||||
/*
|
||||
Custom styling for mailcow UI
|
||||
*/
|
||||
2
data/conf/phpfpm/config_templates/session_store.ini.j2
Normal file
2
data/conf/phpfpm/config_templates/session_store.ini.j2
Normal file
@@ -0,0 +1,2 @@
|
||||
session.save_handler = redis
|
||||
session.save_path = "tcp://{{ REDIS_SLAVEOF_IP or 'redis-mailcow' }}:{{ REDIS_SLAVEOF_PORT or '6379' }}?auth={{ REDISPASS }}"
|
||||
0
data/conf/phpfpm/custom_templates/.gitkeep
Normal file
0
data/conf/phpfpm/custom_templates/.gitkeep
Normal file
130
data/conf/postfix/config.json
Normal file
130
data/conf/postfix/config.json
Normal file
@@ -0,0 +1,130 @@
|
||||
[
|
||||
{
|
||||
"template": "aliases.j2",
|
||||
"output": "/etc/aliases"
|
||||
},
|
||||
{
|
||||
"template": "mysql_relay_ne.cf.j2",
|
||||
"output": "/opt/postfix/conf/sql/mysql_relay_ne.cf"
|
||||
},
|
||||
{
|
||||
"template": "mysql_relay_recipient_maps.cf.j2",
|
||||
"output": "/opt/postfix/conf/sql/mysql_relay_recipient_maps.cf"
|
||||
},
|
||||
{
|
||||
"template": "mysql_tls_policy_override_maps.cf.j2",
|
||||
"output": "/opt/postfix/conf/sql/mysql_tls_policy_override_maps.cf"
|
||||
},
|
||||
{
|
||||
"template": "mysql_tls_enforce_in_policy.cf.j2",
|
||||
"output": "/opt/postfix/conf/sql/mysql_tls_enforce_in_policy.cf"
|
||||
},
|
||||
{
|
||||
"template": "mysql_sender_dependent_default_transport_maps.cf.j2",
|
||||
"output": "/opt/postfix/conf/sql/mysql_sender_dependent_default_transport_maps.cf"
|
||||
},
|
||||
{
|
||||
"template": "mysql_transport_maps.cf.j2",
|
||||
"output": "/opt/postfix/conf/sql/mysql_transport_maps.cf"
|
||||
},
|
||||
{
|
||||
"template": "mysql_virtual_resource_maps.cf.j2",
|
||||
"output": "/opt/postfix/conf/sql/mysql_virtual_resource_maps.cf"
|
||||
},
|
||||
{
|
||||
"template": "mysql_sasl_passwd_maps_sender_dependent.cf.j2",
|
||||
"output": "/opt/postfix/conf/sql/mysql_sasl_passwd_maps_sender_dependent.cf"
|
||||
},
|
||||
{
|
||||
"template": "mysql_sasl_passwd_maps_transport_maps.cf.j2",
|
||||
"output": "/opt/postfix/conf/sql/mysql_sasl_passwd_maps_transport_maps.cf"
|
||||
},
|
||||
{
|
||||
"template": "mysql_virtual_alias_domain_maps.cf.j2",
|
||||
"output": "/opt/postfix/conf/sql/mysql_virtual_alias_domain_maps.cf"
|
||||
},
|
||||
{
|
||||
"template": "mysql_virtual_alias_maps.cf.j2",
|
||||
"output": "/opt/postfix/conf/sql/mysql_virtual_alias_maps.cf"
|
||||
},
|
||||
{
|
||||
"template": "mysql_recipient_bcc_maps.cf.j2",
|
||||
"output": "/opt/postfix/conf/sql/mysql_recipient_bcc_maps.cf"
|
||||
},
|
||||
{
|
||||
"template": "mysql_sender_bcc_maps.cf.j2",
|
||||
"output": "/opt/postfix/conf/sql/mysql_sender_bcc_maps.cf"
|
||||
},
|
||||
{
|
||||
"template": "mysql_recipient_canonical_maps.cf.j2",
|
||||
"output": "/opt/postfix/conf/sql/mysql_recipient_canonical_maps.cf"
|
||||
},
|
||||
{
|
||||
"template": "mysql_virtual_domains_maps.cf.j2",
|
||||
"output": "/opt/postfix/conf/sql/mysql_virtual_domains_maps.cf"
|
||||
},
|
||||
{
|
||||
"template": "mysql_virtual_mailbox_maps.cf.j2",
|
||||
"output": "/opt/postfix/conf/sql/mysql_virtual_mailbox_maps.cf"
|
||||
},
|
||||
{
|
||||
"template": "mysql_virtual_relay_domain_maps.cf.j2",
|
||||
"output": "/opt/postfix/conf/sql/mysql_virtual_relay_domain_maps.cf"
|
||||
},
|
||||
{
|
||||
"template": "mysql_virtual_sender_acl.cf.j2",
|
||||
"output": "/opt/postfix/conf/sql/mysql_virtual_sender_acl.cf"
|
||||
},
|
||||
{
|
||||
"template": "mysql_mbr_access_maps.cf.j2",
|
||||
"output": "/opt/postfix/conf/sql/mysql_mbr_access_maps.cf"
|
||||
},
|
||||
{
|
||||
"template": "mysql_virtual_spamalias_maps.cf.j2",
|
||||
"output": "/opt/postfix/conf/sql/mysql_virtual_spamalias_maps.cf"
|
||||
},
|
||||
{
|
||||
"template": "sni.map.j2",
|
||||
"output": "/opt/postfix/conf/sni.map"
|
||||
},
|
||||
{
|
||||
"template": "main.cf.j2",
|
||||
"output": "/opt/postfix/conf/main.cf"
|
||||
},
|
||||
{
|
||||
"template": "dns_blocklists.cf.j2",
|
||||
"output": "/opt/postfix/conf/dns_blocklists.cf"
|
||||
},
|
||||
{
|
||||
"template": "dns_reply.map.j2",
|
||||
"output": "/opt/postfix/conf/dns_reply.map"
|
||||
},
|
||||
{
|
||||
"template": "custom_postscreen_whitelist.cidr.j2",
|
||||
"output": "/opt/postfix/conf/custom_postscreen_whitelist.cidr"
|
||||
},
|
||||
{
|
||||
"template": "custom_transport.pcre.j2",
|
||||
"output": "/opt/postfix/conf/custom_transport.pcre"
|
||||
},
|
||||
{
|
||||
"template": "allow_mailcow_local.regexp.j2",
|
||||
"output": "/opt/postfix/conf/allow_mailcow_local.regexp"
|
||||
},
|
||||
{
|
||||
"template": "anonymize_headers.pcre.j2",
|
||||
"output": "/opt/postfix/conf/anonymize_headers.pcre"
|
||||
},
|
||||
{
|
||||
"template": "local_transport.j2",
|
||||
"output": "/opt/postfix/conf/local_transport"
|
||||
},
|
||||
{
|
||||
"template": "master.cf.j2",
|
||||
"output": "/opt/postfix/conf/master.cf"
|
||||
},
|
||||
{
|
||||
"template": "smtp_dsn_filter.j2",
|
||||
"output": "/opt/postfix/conf/smtp_dsn_filter"
|
||||
}
|
||||
]
|
||||
4
data/conf/postfix/config_templates/aliases.j2
Normal file
4
data/conf/postfix/config_templates/aliases.j2
Normal file
@@ -0,0 +1,4 @@
|
||||
null: /dev/null
|
||||
watchdog: /dev/null
|
||||
ham: "|/usr/local/bin/rspamd-pipe-ham"
|
||||
spam: "|/usr/local/bin/rspamd-pipe-spam"
|
||||
@@ -0,0 +1,4 @@
|
||||
# Rules are evaluated in the order as specified.
|
||||
# Blacklist 192.168.* except 192.168.0.1.
|
||||
# 192.168.0.1 permit
|
||||
# 192.168.0.0/16 reject
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user