Files
cc-ci/tests/mailu/functional/_mailu.py
autonomic-bot 916bdd8b68 feat(2): Q4.9 mailu — recipe_meta + health + 3 functional (create-mailbox/imap-login/mail-flow); P4 N/A deferred
mailu (full email stack). TLS_FLAVOR=notls avoids certdumper/ACME dep (cc-ci file-provider cert);
MAIL_DOMAIN/HOSTNAMES=run domain; TRAEFIK_STACK_NAME for the letsencrypt-volume mount. P2 vacuous (no
corpus). P3: test_mailbox (flask mailu user create + config-export read-back), test_imap_login
(mailbox authenticates over dovecot IMAP:143), test_mail_flow (SMTP submission send → IMAP retrieve,
auth to avoid greylisting). P4 N/A (no backupbot label) — DEFERRED.md + PARITY.md, Adversary §7.1
sign-off pending. Smoke-validated: 8 services converge, mail ports 25/587/143/993 host-open, flask CLI.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 21:13:56 +01:00

46 lines
1.8 KiB
Python

"""Shared mailu helpers — headless mailbox provisioning via the admin container's `flask mailu` CLI.
mailu's admin container ships the management CLI (`flask mailu domain|user|alias|config-export ...`),
the canonical headless way to provision mail objects. We exec it via the harness (service="admin").
The primary MAIL_DOMAIN is auto-created at boot; `flask mailu domain ... || true` is idempotent.
"""
from __future__ import annotations
import json
import os
import sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "..", "runner"))
from harness import lifecycle # noqa: E402
def flask_mailu(domain: str, shell_cmd: str) -> str:
"""Run a `flask mailu ...` shell command in the admin container; return stdout."""
return lifecycle.exec_in_app(domain, ["sh", "-c", shell_cmd], service="admin")
def ensure_domain(domain: str, mail_domain: str) -> None:
# idempotent: the primary domain is auto-created; tolerate "already exists"
flask_mailu(domain, f"flask mailu domain {mail_domain} || true")
def create_user(domain: str, local: str, mail_domain: str, password: str) -> str:
email = f"{local}@{mail_domain}"
flask_mailu(domain, f"flask mailu user {local} {mail_domain} '{password}'")
return email
def config_export(domain: str) -> dict:
"""`flask mailu config-export --json` → parsed dict (keys: domain/user/alias/relay)."""
out = flask_mailu(domain, "flask mailu config-export --json")
# Be robust to any leading banner: extract the JSON object.
start = out.find("{")
end = out.rfind("}")
assert start != -1 and end != -1, f"config-export produced no JSON: {out[:200]!r}"
return json.loads(out[start : end + 1])
def user_emails(cfg: dict) -> list[str]:
return [u.get("email") for u in cfg.get("user", [])]