diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 0703a05d..ef050eee 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -53,7 +53,7 @@ jobs: lxc-test: name: LXC deploy and test - uses: chatmail/cmlxc/.github/workflows/lxc-test.yml@main + uses: chatmail/cmlxc/.github/workflows/lxc-test.yml@v0.10.0 with: cmlxc_commands: | cmlxc init diff --git a/chatmaild/src/chatmaild/newemail.py b/chatmaild/src/chatmaild/newemail.py index 8b484fe6..4aa54aee 100644 --- a/chatmaild/src/chatmaild/newemail.py +++ b/chatmaild/src/chatmaild/newemail.py @@ -2,6 +2,7 @@ """CGI script for creating new accounts.""" +import ipaddress import json import secrets import string @@ -14,6 +15,16 @@ ALPHANUMERIC = string.ascii_lowercase + string.digits ALPHANUMERIC_PUNCT = string.ascii_letters + string.digits + string.punctuation +def wrap_ip(host): + if host.startswith("[") and host.endswith("]"): + return host + try: + ipaddress.ip_address(host) + return f"[{host}]" + except ValueError: + return host + + def create_newemail_dict(config: Config): user = "".join( secrets.choice(ALPHANUMERIC) for _ in range(config.username_max_length) @@ -22,7 +33,7 @@ def create_newemail_dict(config: Config): secrets.choice(ALPHANUMERIC_PUNCT) for _ in range(config.password_min_length + 3) ) - return dict(email=f"{user}@{config.mail_domain}", password=f"{password}") + return dict(email=f"{user}@{wrap_ip(config.mail_domain)}", password=f"{password}") def create_dclogin_url(email, password): diff --git a/chatmaild/src/chatmaild/tests/test_newmail.py b/chatmaild/src/chatmaild/tests/test_newmail.py index ca0b1661..b266d11e 100644 --- a/chatmaild/src/chatmaild/tests/test_newmail.py +++ b/chatmaild/src/chatmaild/tests/test_newmail.py @@ -19,6 +19,12 @@ def test_create_newemail_dict(example_config): assert ac1["password"] != ac2["password"] +def test_create_newemail_dict_ip(make_config): + config = make_config("1.2.3.4") + ac = create_newemail_dict(config) + assert ac["email"].endswith("@[1.2.3.4]") + + def test_create_dclogin_url(): url = create_dclogin_url("user@example.org", "p@ss w+rd") assert url.startswith("dclogin:") diff --git a/cmdeploy/src/cmdeploy/tests/plugin.py b/cmdeploy/src/cmdeploy/tests/plugin.py index 269aa828..afa32fcb 100644 --- a/cmdeploy/src/cmdeploy/tests/plugin.py +++ b/cmdeploy/src/cmdeploy/tests/plugin.py @@ -1,4 +1,5 @@ import imaplib +import ipaddress import itertools import os import random @@ -14,6 +15,14 @@ from chatmaild.config import read_config conftestdir = Path(__file__).parent +def _is_ip(domain): + try: + ipaddress.ip_address(domain) + return True + except ValueError: + return False + + def pytest_addoption(parser): parser.addoption( "--slow", action="store_true", default=False, help="also run slow tests" @@ -282,6 +291,7 @@ def gencreds(chatmail_config): def gen(domain=None): domain = domain if domain else chatmail_config.mail_domain + addr_domain = f"[{domain}]" if _is_ip(domain) else domain while 1: num = next(count) alphanumeric = "abcdefghijklmnopqrstuvwxyz1234567890" @@ -295,7 +305,7 @@ def gencreds(chatmail_config): password = "".join( random.choices(alphanumeric, k=chatmail_config.password_min_length) ) - yield f"{user}@{domain}", f"{password}" + yield f"{user}@{addr_domain}", f"{password}" return lambda domain=None: next(gen(domain)) @@ -344,9 +354,22 @@ class ChatmailACFactory: accounts = [] for _ in range(num): account = self.dc.add_account() - future = account.add_or_update_transport.future( - self._make_transport(domain) - ) + addr, password = self.gencreds(domain) + if _is_ip(domain): + # Use DCLOGIN scheme with explicit server hosts, + # matching how madmail presents its addresses to users. + qr = ( + f"dclogin:{addr}" + f"?p={password}&v=1" + f"&ih={domain}&ip=993" + f"&sh={domain}&sp=465" + f"&ic=3&ss=default" + ) + future = account.add_transport_from_qr.future(qr) + else: + future = account.add_or_update_transport.future( + self._make_transport(domain) + ) futures.append(future) # ensure messages stay in INBOX so that they can be diff --git a/cmdeploy/src/cmdeploy/tests/test_dovecot_deployer.py b/cmdeploy/src/cmdeploy/tests/test_dovecot_deployer.py index c18b9aa7..f632912e 100644 --- a/cmdeploy/src/cmdeploy/tests/test_dovecot_deployer.py +++ b/cmdeploy/src/cmdeploy/tests/test_dovecot_deployer.py @@ -2,9 +2,9 @@ from contextlib import nullcontext from types import SimpleNamespace import pytest +from pyinfra.facts.deb import DebPackages from cmdeploy.dovecot import deployer as dovecot_deployer -from pyinfra.facts.deb import DebPackages def make_host(*fact_pairs):