Compare commits

...

6 Commits

Author SHA1 Message Date
holger krekel
2a676c7e35 fix: test_no_vrfy to use an actually existing address 2026-03-16 10:55:39 +01:00
holger krekel
ba3d86c9c7 fix off-by-one 2026-03-13 13:35:56 +01:00
holger krekel
b53fd912d6 fix: use push_file_content for unbound and sysctl config files
fixes silent fail on invalid configuration file.
2026-03-10 21:38:10 +01:00
holger krekel
c819ee20ad fix: run cmdeploy dns and restart filtermail in lxc-start --run
lxc-start --run deployed the relay but never loaded DNS zones
or restarted filtermail-incoming, so DKIM verification and
outgoing mail failed.
2026-03-10 21:38:10 +01:00
holger krekel
7138fc7f55 fix: pass full config to ChatmailDeployer and ensure mailboxes_dir
ChatmailDeployer now receives the full config object
so it can access mailboxes_dir and create this directory
during deployment, preventing mail delivery failures.
2026-03-10 21:38:10 +01:00
holger krekel
d14f384de3 fix: separate stderr capture in incus and suppress in test plugin
Capture stderr separately in Incus.run() instead of merging
it into stdout, which corrupted JSON parsing in run_json().
Add --quiet flag to reduce incus noise.
Suppress stderr in Remote subprocess to prevent pytest-xdist
communication issues.
2026-03-10 21:38:10 +01:00
6 changed files with 62 additions and 22 deletions

View File

@@ -463,8 +463,9 @@ class ChatmailDeployer(Deployer):
("iroh", None, None),
]
def __init__(self, mail_domain):
self.mail_domain = mail_domain
def __init__(self, config):
self.config = config
self.mail_domain = config.mail_domain
def install(self):
files.put(
@@ -513,6 +514,15 @@ class ChatmailDeployer(Deployer):
],
)
files.directory(
name=f"Ensure mailboxes directory {self.config.mailboxes_dir} exists",
path=str(self.config.mailboxes_dir),
user="vmail",
group="vmail",
mode="700",
present=True,
)
class FcgiwrapDeployer(Deployer):
def install(self):
@@ -631,7 +641,7 @@ def deploy_chatmail(config_path: Path, disable_mail: bool, website_only: bool) -
tls_deployer = get_tls_deployer(config, mail_domain)
all_deployers = [
ChatmailDeployer(mail_domain),
ChatmailDeployer(config),
LegacyRemoveDeployer(),
FiltermailDeployer(),
JournaldDeployer(),

View File

@@ -145,7 +145,7 @@ def _configure_dovecot(config: Config, debug: bool = False) -> (bool, bool):
if not can_modify:
print(
"\n!!!! refusing to attempt sysctl setting in shared-kernel containers\n"
f"!!!! dovecot: sysctl {key!r}={value}, should be >65535 for production setups\n"
f"!!!! dovecot: sysctl {key!r}={value}, should be >65534 for production setups\n"
"!!!!"
)
continue

View File

@@ -114,7 +114,7 @@ def _lxc_start_cmd(args, out):
)
sub.green(f" Include {ssh_cfg}")
# Optionally run cmdeploy run on each relay
# Optionally run cmdeploy run + dns on each relay
if args.run:
for ct in relays:
with out.section(f"cmdeploy run: {ct.sname} ({ct.domain})"):
@@ -123,6 +123,20 @@ def _lxc_start_cmd(args, out):
out.red(f"Deploy to {ct.sname} failed (exit {ret})")
return ret
with out.section("loading DNS zones"):
for ct in relays:
ret = _run_cmdeploy(
"dns", ct, ix, out,
extra=["--zonefile", str(ct.zone)],
)
if ret:
out.red(f"DNS for {ct.sname} failed (exit {ret})")
return ret
if ct.zone.exists():
dns_ct.set_dns_records(ct.zone.read_text())
out.print(f"Restarting filtermail-incoming on {ct.name}")
ct.bash("systemctl restart filtermail-incoming")
# -------------------------------------------------------------------
# lxc-stop

View File

@@ -122,7 +122,7 @@ class Incus:
to the terminal line-by-line while also being captured for
later return via result.stdout.
"""
cmd = ["incus"] + list(args)
cmd = ["incus", "--quiet"] + list(args)
sub = self.out.new_prefixed_out(" ")
if not capture:
@@ -146,7 +146,7 @@ class Incus:
text=True,
stdin=subprocess.PIPE if input else subprocess.DEVNULL,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
stderr=subprocess.PIPE,
)
stdout_lines = []
@@ -159,15 +159,17 @@ class Incus:
if sub.verbosity >= 2:
sub.print(f" > {line.rstrip()}")
stderr = proc.stderr.read()
ret = proc.wait()
stdout = "".join(stdout_lines)
if check and ret != 0:
for line in stdout.splitlines():
full_output = stdout + stderr
for line in full_output.splitlines():
if sub.verbosity < 1: # and we haven't printed it yet
sub.red(line)
raise subprocess.CalledProcessError(ret, cmd, output=stdout)
raise subprocess.CalledProcessError(ret, cmd, output=stdout, stderr=stderr)
return subprocess.CompletedProcess(cmd, ret, stdout=stdout)
return subprocess.CompletedProcess(cmd, ret, stdout=stdout, stderr=stderr)
def run_json(self, args, check=True):
"""Run an incus command with ``--format=json``.
@@ -454,11 +456,14 @@ class RelayContainer(Container):
self.bash("""
sysctl -w net.ipv6.conf.all.disable_ipv6=1
sysctl -w net.ipv6.conf.default.disable_ipv6=1
mkdir -p /etc/sysctl.d
printf 'net.ipv6.conf.all.disable_ipv6=1\\n
net.ipv6.conf.default.disable_ipv6=1\\n'
> /etc/sysctl.d/99-disable-ipv6.conf
""")
self.push_file_content(
"/etc/sysctl.d/99-disable-ipv6.conf",
"""
net.ipv6.conf.all.disable_ipv6=1
net.ipv6.conf.default.disable_ipv6=1
""",
)
def configure_hosts(self, ip):
"""Set hostname and /etc/hosts inside the container."""
@@ -506,14 +511,21 @@ class RelayContainer(Container):
self.bash(f"""
systemctl disable --now systemd-resolved 2>/dev/null || true
rm -f /etc/resolv.conf
printf 'nameserver {dns_ip}\n' >/etc/resolv.conf
printf 'nameserver {dns_ip}\\n' >/etc/resolv.conf
mkdir -p /etc/unbound/unbound.conf.d
printf 'server:\\n domain-insecure: "localchat"\\n\\n
forward-zone:\\n name: "localchat"\\n
forward-addr: {dns_ip}\\n'
> /etc/unbound/unbound.conf.d/localchat-forward.conf
systemctl restart unbound 2>/dev/null || true
""")
self.push_file_content(
"/etc/unbound/unbound.conf.d/localchat-forward.conf",
f"""
server:
domain-insecure: "localchat"
forward-zone:
name: "localchat"
forward-addr: {dns_ip}
""",
)
self.bash("systemctl restart unbound 2>/dev/null || true")
self._wait_dns_reachable(dns_ip)
def _wait_dns_reachable(self, dns_ip, timeout=10):

View File

@@ -89,7 +89,10 @@ def test_concurrent_logins_same_account(
assert login_results.get()
def test_no_vrfy(chatmail_config):
def test_no_vrfy(chatmail_config, cmfactory):
ac1 = cmfactory.get_online_account()
addr = ac1.get_config("addr")
domain = chatmail_config.mail_domain
s = smtplib.SMTP(domain)
@@ -98,7 +101,7 @@ def test_no_vrfy(chatmail_config):
s.putcmd("vrfy", f"wrongaddress@{chatmail_config.mail_domain}")
result = s.getreply()
print(result)
s.putcmd("vrfy", f"echo@{chatmail_config.mail_domain}")
s.putcmd("vrfy", addr)
result2 = s.getreply()
print(result2)
assert result[0] == result2[0] == 252

View File

@@ -516,6 +516,7 @@ class Remote:
command,
stdin=subprocess.DEVNULL,
stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL,
)
self._procs.append(popen)
while 1: