odysseus-dev/odysseus · error · ValueError

Email account {cfg.get('account_name') or account or 'defaul

Error message

Email account {cfg.get('account_name') or account or 'default'} has no SMTP configured

What it means

Raised by _smtp_connect when the cfg it received (loaded from the account argument or the default config) fails the same _smtp_ready() check. It is the low-level guard hit after _resolve_send_config already returned a cfg — typically when callers pass cfg directly.

Source

Thrown at mcp_servers/email_server.py:1334

def _resolve_send_config(account=None):
    cfg = _load_config(account)
    if _smtp_ready(cfg):
        return account, cfg
    if account:
        raise ValueError(f"Email account {cfg.get('account_name') or account} has no SMTP configured")
    for row in _list_accounts_raw():
        selector = row.get("id") or row.get("name") or row.get("imap_user")
        trial = _load_config(selector)
        if _smtp_ready(trial):
            return selector, trial
    raise ValueError("No SMTP-capable email account configured")


def _smtp_connect(account=None, cfg=None):
    """Connect to SMTP server, returns logged-in connection."""
    cfg = cfg or _load_config(account)
    if not _smtp_ready(cfg):
        raise ValueError(f"Email account {cfg.get('account_name') or account or 'default'} has no SMTP configured")
    port = int(cfg.get("smtp_port") or 465)
    security = str(cfg.get("smtp_security") or "").strip().lower()
    if security not in {"ssl", "starttls", "none"}:
        security = "starttls" if port == 587 else "ssl"
    if security == "starttls":
        conn = smtplib.SMTP(
            cfg["smtp_host"],
            port,
            timeout=EMAIL_SOCKET_TIMEOUT,
        )
        try:
            conn.starttls()
        except Exception:
            # Don't leak the open plain socket on a rejected STARTTLS. SMTP has
            # no shutdown(); close() is the low-level socket close (no QUIT). (#3174)
            try:
                conn.close()
            except Exception:

View on GitHub (pinned to f9235ebbf1)

Solutions

  1. Ensure the cfg passed to _smtp_connect comes from _resolve_send_config (which validates and falls back), not a hand-built dict
  2. Fill in the missing SMTP fields on the account/config
  3. Check that SMTP password decryption succeeds — an empty decrypted value leaves smtp_password falsy

Example fix

# before
conn = _smtp_connect(account, cfg=imap_only_cfg)
# after
account, cfg = _resolve_send_config(account)
conn = _smtp_connect(account, cfg=cfg)
Defensive patterns

Strategy: type-guard

Validate before calling

if cfg is None or not _smtp_ready(cfg):
    _, cfg = _resolve_send_config(account)

Type guard

def smtp_cfg_ready(cfg: dict) -> bool:
    return bool(cfg) and _smtp_ready(cfg)

Try / catch

try:
    _smtp_connect(account, cfg)
except ValueError as e:
    if 'no SMTP configured' in str(e):
        account, cfg = _resolve_send_config(account)
        _smtp_connect(account, cfg)
    raise

Prevention

When it happens

Trigger: _smtp_connect(account, cfg) invoked with a cfg dict missing smtp_host/smtp_user/smtp_password, or _smtp_connect(account) where that account has no SMTP fields.

Common situations: Code paths that build a partial cfg from IMAP-only data and forward it to the send path; account rows where the encrypted SMTP password failed to decrypt into smtp_password.

Related errors


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