odysseus-dev/odysseus · critical · SystemExit

refusing to wipe non-demo target {USER}@{HOST}:{PORT} — set

Error message

refusing to wipe non-demo target {USER}@{HOST}:{PORT} — set DEMO_ALLOW_WIPE=1 to override

What it means

SystemExit from scripts/demo_email/seed_demo_emails.py's _wipe(): the IMAP connection parameters are env-overridable (DEMO_IMAP_USER/HOST/PORT), and the target is not recognizably the local demo account (user not ending in @odysseus.local and host not localhost/127.0.0.1/::1) while DEMO_ALLOW_WIPE is unset. The script refuses to expunge every message in every mailbox rather than risk wiping a real account.

Source

Thrown at scripts/demo_email/seed_demo_emails.py:331

def _ensure_mailbox(conn: imaplib.IMAP4, name: str) -> None:
    if name.upper() == "INBOX":
        return
    typ, _ = conn.select(name)
    if typ != "OK":
        conn.create(name)


def _wipe(conn: imaplib.IMAP4) -> int:
    """Delete every message in every mailbox of this (throwaway) account.

    Guard: the connection params are env-overridable, so refuse to run the
    destructive expunge unless the target is unmistakably the local demo
    account — otherwise a misconfigured DEMO_IMAP_USER/HOST could irreversibly
    wipe a real mailbox. Override only with DEMO_ALLOW_WIPE=1 (you must mean it).
    """
    safe_target = USER.endswith("@odysseus.local") or HOST in ("localhost", "127.0.0.1", "::1")
    if not safe_target and os.getenv("DEMO_ALLOW_WIPE") != "1":
        raise SystemExit(
            f"refusing to wipe non-demo target {USER}@{HOST}:{PORT} — "
            f"set DEMO_ALLOW_WIPE=1 to override")
    typ, boxes = conn.list()
    n = 0
    names = []
    if typ == "OK":
        for raw in boxes:
            line = raw.decode(errors="replace")
            # last token, possibly quoted, is the mailbox name
            name = line.split(' "/" ')[-1].split(' "." ')[-1].strip().strip('"')
            names.append(name)
    for name in set(names) | {"INBOX"}:
        if conn.select(name)[0] != "OK":
            continue
        typ, data = conn.search(None, "ALL")
        if typ == "OK" and data and data[0]:
            ids = data[0].split()
            for i in ids:

View on GitHub (pinned to f9235ebbf1)

Solutions

  1. Target the actual local demo account (user ending in @odysseus.local, or host localhost/127.0.0.1/::1)
  2. If the remote target is genuinely disposable and you accept the data loss, run with DEMO_ALLOW_WIPE=1
  3. Never point the env vars at a mailbox containing real data

Example fix

# before
DEMO_IMAP_USER=real.user@gmail.com DEMO_IMAP_HOST=imap.gmail.com python scripts/demo_email/seed_demo_emails.py
# SystemExit: refusing to wipe non-demo target ...

# after (throwaway dockerized imap on localhost)
DEMO_IMAP_USER=demo@odysseus.local DEMO_IMAP_HOST=127.0.0.1 python scripts/demo_email/seed_demo_emails.py
Defensive patterns

Strategy: validation

Validate before calling

def wipe_target_is_safe(user: str, host: str) -> bool:
    return user.endswith("@odysseus.local") or host in ("localhost", "127.0.0.1", "::1")

if not wipe_target_is_safe(DEMO_IMAP_USER, DEMO_IMAP_HOST):
    raise ConfigError("refusing wipe of non-demo mailbox; point env at the local demo account")

Try / catch

try:
    run_seed_script(env)
except SystemExit as e:
    if 'refusing to wipe' in str(e):
        fix_env_to_local_demo_account()  # never auto-set DEMO_ALLOW_WIPE

Prevention

When it happens

Trigger: Running the seed script with DEMO_IMAP_USER=real.user@gmail.com DEMO_IMAP_HOST=imap.gmail.com; pointing at a LAN mail server host like 192.168.1.10 (not in the localhost list); any env override where the safety predicate fails and DEMO_ALLOW_WIPE != '1'.

Common situations: Testing the demo flow against a disposable-but-remote mailbox; CI configured with container hostnames (e.g. mail:143) that don't match the safe list; copy-pasting env blocks between environments.

Related errors


AI-assisted analysis of odysseus-dev/odysseus@f9235ebbf1 (2026-08-14). Data as JSON: /api/errors/d370e567b1b2783e. Report an issue: GitHub.