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:
@@ -8,6 +8,8 @@ def main():
|
||||
from modules.BootstrapSogo import Bootstrap
|
||||
elif container_name == "nginx-mailcow":
|
||||
from modules.BootstrapNginx import Bootstrap
|
||||
elif container_name == "postfix-mailcow":
|
||||
from modules.BootstrapPostfix import Bootstrap
|
||||
else:
|
||||
print(f"No bootstrap handler for container: {container_name}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
@@ -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
|
||||
@@ -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(),
|
||||
|
||||
115
data/Dockerfiles/bootstrap/modules/BootstrapPostfix.py
Normal file
115
data/Dockerfiles/bootstrap/modules/BootstrapPostfix.py
Normal 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
|
||||
@@ -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(),
|
||||
|
||||
Reference in New Issue
Block a user