diff --git a/.python-version b/.python-version new file mode 100644 index 00000000..2c073331 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.11 diff --git a/cmdeploy/src/cmdeploy/acmetool/__init__.py b/cmdeploy/src/cmdeploy/acmetool/__init__.py index e4e1ed84..16f6369f 100644 --- a/cmdeploy/src/cmdeploy/acmetool/__init__.py +++ b/cmdeploy/src/cmdeploy/acmetool/__init__.py @@ -108,6 +108,13 @@ class AcmetoolDeployer(Deployer): self.need_restart_reconcile_timer = reconcile_timer_file.changed def activate(self): + systemd.service( + name="Stop nginx to free port 80 for acmetool", + service="nginx.service", + running=False, + enabled=True, + ) + systemd.service( name="Setup acmetool-redirector service", service="acmetool-redirector.service", @@ -135,6 +142,15 @@ class AcmetoolDeployer(Deployer): ) self.need_restart_reconcile_timer = False + # Add the first domain to /etc/hosts to help acmetool's self-test + # bypass external firewalls or split-brain DNS issues. + server.shell( + name=f"Add {self.domains[0]} to /etc/hosts for self-test", + commands=[ + f"grep -q ' {self.domains[0]}$' /etc/hosts || echo '127.0.0.1 {self.domains[0]}' >> /etc/hosts" + ], + ) + server.shell( name=f"Reconcile certificates for: {', '.join(self.domains)}", commands=["acmetool --batch --xlog.severity=debug reconcile"], diff --git a/cmdeploy/src/cmdeploy/deployers.py b/cmdeploy/src/cmdeploy/deployers.py index 5258f4b4..a9331a36 100644 --- a/cmdeploy/src/cmdeploy/deployers.py +++ b/cmdeploy/src/cmdeploy/deployers.py @@ -80,14 +80,22 @@ def _install_remote_venv_with_chatmaild() -> None: remote_dist_file = f"{remote_base_dir}/dist/{dist_file.name}" remote_venv_dir = f"{remote_base_dir}/venv" root_owned = dict(user="root", group="root", mode="644") + root_owned_dir = dict(user="root", group="root", mode="755") + uv_prefix = "UV_PYTHON_INSTALL_DIR=/usr/local/share/uv/python" server.shell( - name="Install uv", + name="Install uv globally if not present", commands=[ - "curl -LsSf https://astral.sh/uv/install.sh | INSTALLER_NO_MODIFY_PATH=1 sh", + "command -v uv >/dev/null 2>&1 || (curl -LsSf https://astral.sh/uv/install.sh | INSTALLER_NO_MODIFY_PATH=1 sudo sh -s -- --install-dir /usr/local/bin)", ], ) + files.directory( + name="Ensure shared uv python directory exists", + path="/usr/local/share/uv/python", + **root_owned_dir, + ) + files.put( name="Upload chatmaild source package", src=dist_file.open("rb"), @@ -95,10 +103,30 @@ def _install_remote_venv_with_chatmaild() -> None: create_remote_dir=True, **root_owned, ) + # Ensure parent directory is accessible + files.directory( + name=f"Ensure {remote_base_dir} is accessible", + path=remote_base_dir, + **root_owned_dir, + ) + files.directory( + name=f"Ensure {remote_base_dir}/dist is accessible", + path=f"{remote_base_dir}/dist", + **root_owned_dir, + ) server.shell( name=f"chatmaild virtualenv {remote_venv_dir}", - commands=[f"/root/.local/bin/uv venv {remote_venv_dir} --allow-existing"], + commands=[f"{uv_prefix} uv venv {remote_venv_dir} --python 3.11 --allow-existing"], + ) + + # Ensure venv and managed pythons are accessible by other users (like www-data) + server.shell( + name="Make venv and managed pythons accessible", + commands=[ + f"chmod -R a+rX {remote_venv_dir}", + "chmod -R a+rX /usr/local/share/uv/python || true", + ], ) apt.packages( @@ -109,7 +137,7 @@ def _install_remote_venv_with_chatmaild() -> None: server.shell( name=f"forced uv-pip-install {dist_file.name}", commands=[ - f"/root/.local/bin/uv pip install --python {remote_venv_dir}/bin/python --force-reinstall {remote_dist_file}" + f"{uv_prefix} uv pip install --python {remote_venv_dir}/bin/python --force-reinstall {remote_dist_file}" ], ) @@ -448,6 +476,10 @@ class ChatmailDeployer(Deployer): self.mail_domain = mail_domain def install(self): + server.shell( + name="Fix broken packages", + commands=["apt-get install -f -y"], + ) apt.update(name="apt update", cache_time=24 * 3600) apt.upgrade(name="upgrade apt packages", auto_remove=True) diff --git a/cmdeploy/src/cmdeploy/dovecot/deployer.py b/cmdeploy/src/cmdeploy/dovecot/deployer.py index 0178d843..84d42e5f 100644 --- a/cmdeploy/src/cmdeploy/dovecot/deployer.py +++ b/cmdeploy/src/cmdeploy/dovecot/deployer.py @@ -1,6 +1,6 @@ from chatmaild.config import Config from pyinfra import host -from pyinfra.facts.server import Arch, Sysctl +from pyinfra.facts.server import Arch, LinuxDistribution, Sysctl from pyinfra.facts.systemd import SystemdEnabled from pyinfra.operations import apt, files, server, systemd @@ -50,6 +50,33 @@ class DovecotDeployer(Deployer): def _install_dovecot_package(package: str, arch: str): + distro = host.get_fact(LinuxDistribution) + is_old_debian = False + if distro: + distro_id = str(distro.get("id", "")).lower() + distro_name = str(distro.get("name", "")).lower() + major_version = str(distro.get("major_version", "0")) + + if "debian" in distro_id or "debian" in distro_name: + try: + # Handle cases where major_version might be '11.13' + m_ver = major_version.split(".")[0] + if m_ver.isdigit() and 0 < int(m_ver) < 12: + is_old_debian = True + except (ValueError, TypeError, IndexError): + pass + # Fallback for systems where major_version might not be parsed correctly but name contains 'bullseye' + if "bullseye" in distro_name or "buster" in distro_name: + is_old_debian = True + + if is_old_debian: + apt.packages( + name=f"Install system dovecot-{package}", + packages=[f"dovecot-{package}"], + update=True, + ) + return + arch = "amd64" if arch == "x86_64" else arch arch = "arm64" if arch == "aarch64" else arch url = f"https://download.delta.chat/dovecot/dovecot-{package}_2.3.21%2Bdfsg1-3_{arch}.deb" @@ -80,7 +107,18 @@ def _install_dovecot_package(package: str, arch: str): cache_time=60 * 60 * 24 * 365 * 10, # never redownload the package ) - apt.deb(name=f"Install dovecot-{package}", src=deb_filename) + # Use a shell command to try installing the custom .deb and fallback if it fails. + # This prevents the whole deployment from failing if custom dovecot is not compatible. + server.shell( + name=f"Install dovecot-{package} (custom or system fallback)", + commands=[ + f"if ! dpkg -i {deb_filename}; then " + f"echo 'HEY: dovecot the custom is not installed, we install your os main one' >&2; " + f"DEBIAN_FRONTEND=noninteractive apt-get install -f -y; " + f"DEBIAN_FRONTEND=noninteractive apt-get install -y dovecot-{package}; " + f"fi" + ], + ) def _configure_dovecot(config: Config, debug: bool = False) -> (bool, bool): diff --git a/cmdeploy/src/cmdeploy/dovecot/dovecot.conf.j2 b/cmdeploy/src/cmdeploy/dovecot/dovecot.conf.j2 index cdc57470..b3914ea9 100644 --- a/cmdeploy/src/cmdeploy/dovecot/dovecot.conf.j2 +++ b/cmdeploy/src/cmdeploy/dovecot/dovecot.conf.j2 @@ -229,7 +229,7 @@ ssl = required ssl_cert = =TLSv1.2 -smtp_tls_mandatory_protocols = >=TLSv1.2 +smtp_tls_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1 +smtp_tls_mandatory_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1 # Disable anonymous cipher suites # and known insecure algorithms. diff --git a/cmdeploy/src/cmdeploy/postfix/master.cf.j2 b/cmdeploy/src/cmdeploy/postfix/master.cf.j2 index a6f33de5..6b262835 100644 --- a/cmdeploy/src/cmdeploy/postfix/master.cf.j2 +++ b/cmdeploy/src/cmdeploy/postfix/master.cf.j2 @@ -10,17 +10,15 @@ # (yes) (yes) (no) (never) (100) # ========================================================================== {% if debug == true %} -smtp inet n - y - - smtpd -v -{%- else %} -smtp inet n - y - - smtpd +smtp inet n - n - - smtpd {%- endif %} -o smtpd_tls_security_level=encrypt - -o smtpd_tls_mandatory_protocols=>=TLSv1.2 + -o smtpd_tls_mandatory_protocols=!SSLv2,!SSLv3,!TLSv1,!TLSv1.1 -o smtpd_proxy_filter=127.0.0.1:{{ config.filtermail_smtp_port_incoming }} -submission inet n - y - 5000 smtpd +submission inet n - n - 5000 smtpd -o syslog_name=postfix/submission -o smtpd_tls_security_level=encrypt - -o smtpd_tls_mandatory_protocols=>=TLSv1.3 + -o smtpd_tls_mandatory_protocols=!SSLv2,!SSLv3,!TLSv1,!TLSv1.1 -o smtpd_sasl_auth_enable=yes -o smtpd_sasl_type=dovecot -o smtpd_sasl_path=private/auth @@ -33,11 +31,11 @@ submission inet n - y - 5000 smtpd -o smtpd_relay_restrictions=permit_sasl_authenticated,reject -o smtpd_client_connection_count_limit=1000 -o smtpd_proxy_filter=127.0.0.1:{{ config.filtermail_smtp_port }} -smtps inet n - y - 5000 smtpd +smtps inet n - n - 5000 smtpd -o syslog_name=postfix/smtps -o smtpd_tls_wrappermode=yes -o smtpd_tls_security_level=encrypt - -o smtpd_tls_mandatory_protocols=>=TLSv1.3 + -o smtpd_tls_mandatory_protocols=!SSLv2,!SSLv3,!TLSv1,!TLSv1.1 -o smtpd_sasl_auth_enable=yes -o smtpd_sasl_type=dovecot -o smtpd_sasl_path=private/auth diff --git a/init.sh b/init.sh index d754295d..cccdf657 100644 --- a/init.sh +++ b/init.sh @@ -8,7 +8,7 @@ sudo apt install -y git curl wget python3-dev gcc python3 nano sed # 1.1 Install uv export PATH="$HOME/.local/bin:/root/.local/bin:$PATH" -if ! command -v uv &> /dev/null; then +if ! command -v uv > /dev/null 2>&1; then if [ -f "/root/.local/bin/uv" ]; then export PATH="/root/.local/bin:$PATH" elif [ -f "$HOME/.local/bin/uv" ]; then @@ -16,7 +16,7 @@ if ! command -v uv &> /dev/null; then fi fi -if ! command -v uv &> /dev/null; then +if ! command -v uv > /dev/null 2>&1; then echo "--- Installing uv ---" curl -LsSf https://astral.sh/uv/install.sh | sh # Ensure uv is in PATH for the current script @@ -43,7 +43,8 @@ read -p "Enter your email for ACME/Let's Encrypt: " ACME_EMAIL # 5. Initialize configuration echo "--- Initializing chatmail configuration ---" -./scripts/cmdeploy init "$MAIL_DOMAIN" +./scripts/cmdeploy init "$MAIL_DOMAIN" || true + # 6. Modify chatmail.ini with specific requirements echo "--- Customizing chatmail.ini ---" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..2be11888 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,29 @@ +[project] +name = "relay-ir" +version = "0.1.0" +description = "Chatmail relay workspace" +readme = "README.md" +requires-python = ">=3.11" +dependencies = [ + "chatmaild", + "cmdeploy", + "sphinx", + "sphinxcontrib-mermaid", + "sphinx-autobuild", + "furo", +] + +[tool.uv] +managed = true +package = false + +[tool.uv.workspace] +members = ["chatmaild", "cmdeploy"] + +[tool.uv.sources] +chatmaild = { workspace = true } +cmdeploy = { workspace = true } + +[build-system] +requires = ["setuptools>=61"] +build-backend = "setuptools.build_meta" diff --git a/scripts/initenv.sh b/scripts/initenv.sh index 7f589396..6b4718e9 100755 --- a/scripts/initenv.sh +++ b/scripts/initenv.sh @@ -21,7 +21,7 @@ fi # Ensure uv is in PATH export PATH="$HOME/.local/bin:/root/.local/bin:$PATH" -if ! command -v uv &> /dev/null; then +if ! command -v uv > /dev/null 2>&1; then if [ -f "/root/.local/bin/uv" ]; then export PATH="/root/.local/bin:$PATH" elif [ -f "$HOME/.local/bin/uv" ]; then @@ -29,13 +29,9 @@ if ! command -v uv &> /dev/null; then fi fi -if ! command -v uv &> /dev/null; then +if ! command -v uv > /dev/null 2>&1; then echo "uv not found. Please install it first or run init.sh" exit 1 fi -uv venv venv - -uv pip install -e chatmaild -uv pip install -e cmdeploy -uv pip install sphinx sphinxcontrib-mermaid sphinx-autobuild furo # for building the docs +uv sync --python 3.11