1
0
mirror of https://github.com/mailcow/mailcow-dockerized.git synced 2025-12-20 13:21:30 +00:00

[Postfix] use python bootstrapper to start POSTFIX container

This commit is contained in:
FreddleSpl0it
2025-05-19 13:13:40 +02:00
parent cde2ba4851
commit 5a097ed5f7
44 changed files with 750 additions and 553 deletions

View File

@@ -231,6 +231,21 @@ class BootstrapBase:
shutil.move(str(src_path), str(dst_path))
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.
@@ -414,6 +429,35 @@ class BootstrapBase:
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.
@@ -478,3 +522,41 @@ class BootstrapBase:
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):
"""
Executes an OS command and optionally checks for errors.
Args:
command (str or list): The command to execute. Can be a string (if shell=True)
or a list of command arguments.
check (bool): If True, raises CalledProcessError on failure.
shell (bool): If True, runs the command in a shell.
Returns:
subprocess.CompletedProcess: The result of the command execution.
Logs:
Prints the command being run and any error output.
"""
try:
result = subprocess.run(
command,
shell=shell,
check=check,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True
)
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

View File

@@ -22,10 +22,10 @@ class Bootstrap(BootstrapBase):
# Setup Jinja2 Environment and load vars
self.env = Environment(
loader=FileSystemLoader('./etc/nginx/conf.d/templates'),
loader=FileSystemLoader('./etc/nginx/conf.d/config_templates'),
keep_trailing_newline=True,
lstrip_blocks=False,
trim_blocks=False
lstrip_blocks=True,
trim_blocks=True
)
extra_vars = {
"VALID_CERT_DIRS": self.get_valid_cert_dirs(),

View File

@@ -0,0 +1,115 @@
from jinja2 import Environment, FileSystemLoader
from modules.BootstrapBase import BootstrapBase
from pathlib import Path
import os
import sys
import time
class Bootstrap(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('./opt/postfix/conf/config_templates'),
keep_trailing_newline=True,
lstrip_blocks=True,
trim_blocks=True
)
with open("/opt/postfix/conf/extra.cf", "r") as f:
extra_config = f.read()
extra_vars = {
"VALID_CERT_DIRS": self.get_valid_cert_dirs(),
"EXTRA_CF": extra_config
}
self.env_vars = self.prepare_template_vars('/overwrites.json', extra_vars)
print("Set Timezone")
self.set_timezone()
print("Set Syslog redis")
self.set_syslog_redis()
print("Render config")
self.render_config("aliases.j2", "/etc/aliases")
self.render_config("mysql_relay_ne.cf.j2", "/opt/postfix/conf/sql/mysql_relay_ne.cf")
self.render_config("mysql_relay_recipient_maps.cf.j2", "/opt/postfix/conf/sql/mysql_relay_recipient_maps.cf")
self.render_config("mysql_tls_policy_override_maps.cf.j2", "/opt/postfix/conf/sql/mysql_tls_policy_override_maps.cf")
self.render_config("mysql_tls_enforce_in_policy.cf.j2", "/opt/postfix/conf/sql/mysql_tls_enforce_in_policy.cf")
self.render_config("mysql_sender_dependent_default_transport_maps.cf.j2", "/opt/postfix/conf/sql/mysql_sender_dependent_default_transport_maps.cf")
self.render_config("mysql_transport_maps.cf.j2", "/opt/postfix/conf/sql/mysql_transport_maps.cf")
self.render_config("mysql_virtual_resource_maps.cf.j2", "/opt/postfix/conf/sql/mysql_virtual_resource_maps.cf")
self.render_config("mysql_sasl_passwd_maps_sender_dependent.cf.j2", "/opt/postfix/conf/sql/mysql_sasl_passwd_maps_sender_dependent.cf")
self.render_config("mysql_sasl_passwd_maps_transport_maps.cf.j2", "/opt/postfix/conf/sql/mysql_sasl_passwd_maps_transport_maps.cf")
self.render_config("mysql_virtual_alias_domain_maps.cf.j2", "/opt/postfix/conf/sql/mysql_virtual_alias_domain_maps.cf")
self.render_config("mysql_virtual_alias_maps.cf.j2", "/opt/postfix/conf/sql/mysql_virtual_alias_maps.cf")
self.render_config("mysql_recipient_bcc_maps.cf.j2", "/opt/postfix/conf/sql/mysql_recipient_bcc_maps.cf")
self.render_config("mysql_sender_bcc_maps.cf.j2", "/opt/postfix/conf/sql/mysql_sender_bcc_maps.cf")
self.render_config("mysql_recipient_canonical_maps.cf.j2", "/opt/postfix/conf/sql/mysql_recipient_canonical_maps.cf")
self.render_config("mysql_virtual_domains_maps.cf.j2", "/opt/postfix/conf/sql/mysql_virtual_domains_maps.cf")
self.render_config("mysql_virtual_mailbox_maps.cf.j2", "/opt/postfix/conf/sql/mysql_virtual_mailbox_maps.cf")
self.render_config("mysql_virtual_relay_domain_maps.cf.j2", "/opt/postfix/conf/sql/mysql_virtual_relay_domain_maps.cf")
self.render_config("mysql_virtual_sender_acl.cf.j2", "/opt/postfix/conf/sql/mysql_virtual_sender_acl.cf")
self.render_config("mysql_mbr_access_maps.cf.j2", "/opt/postfix/conf/sql/mysql_mbr_access_maps.cf")
self.render_config("mysql_virtual_spamalias_maps.cf.j2", "/opt/postfix/conf/sql/mysql_virtual_spamalias_maps.cf")
self.render_config("sni.map.j2", "/opt/postfix/conf/sni.map")
self.render_config("main.cf.j2", "/opt/postfix/conf/main.cf")
# Conditional render
if not Path("/opt/postfix/conf/dns_blocklists.cf").exists():
self.render_config("dns_blocklists.cf.j2", "/opt/postfix/conf/dns_blocklists.cf")
if not Path("/opt/postfix/conf/dns_reply.map").exists():
self.render_config("dns_reply.map.j2", "/opt/postfix/conf/dns_reply.map")
if not Path("/opt/postfix/conf/custom_postscreen_whitelist.cidr").exists():
self.render_config("custom_postscreen_whitelist.cidr.j2", "/opt/postfix/conf/custom_postscreen_whitelist.cidr")
if not Path("/opt/postfix/conf/custom_transport.pcre").exists():
self.render_config("custom_transport.pcre.j2", "/opt/postfix/conf/custom_transport.pcre")
# 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

View File

@@ -29,8 +29,8 @@ class Bootstrap(BootstrapBase):
self.env = Environment(
loader=FileSystemLoader("./etc/sogo/config_templates"),
keep_trailing_newline=True,
lstrip_blocks=False,
trim_blocks=False
lstrip_blocks=True,
trim_blocks=True
)
extra_vars = {
"SQL_DOMAINS": self.get_domains(),