odysseus-dev/odysseus · error · ValueError

No SMTP-capable email account configured

Error message

No SMTP-capable email account configured

What it means

ValueError raised at the end of _resolve_send_config: no explicit account was given, the default account is not SMTP-ready, and the fallback scan over all enabled accounts owned by the caller (including legacy ownerless rows matching their mailbox) found none that passes _smtp_ready. The intermediate per-account errors are only logged at DEBUG.

Source

Thrown at routes/email_routes.py:1323

    try:
        from core.database import SessionLocal as _SL, EmailAccount as _EA
        from sqlalchemy import and_, or_
        db = _SL()
        try:
            q = db.query(_EA).filter(_EA.enabled == True)  # noqa: E712
            if owner:
                unowned = or_(_EA.owner == None, _EA.owner == "")  # noqa: E711
                same_mailbox = or_(_EA.imap_user == owner, _EA.from_address == owner)
                q = q.filter(or_(_EA.owner == owner, and_(unowned, same_mailbox)))
            for row in q.order_by(_EA.is_default.desc(), _EA.created_at.asc()).all():
                trial = _get_email_config(account_id=row.id, owner=owner)
                if _smtp_ready(trial):
                    return trial
        finally:
            db.close()
    except Exception as e:
        logger.debug(f"SMTP-capable account fallback failed: {e}")
    raise ValueError("No SMTP-capable email account configured")


def _store_email_flag(conn, uid: str, flag: str, add: bool = True) -> bool:
    # imaplib's plain store() takes a message SEQUENCE NUMBER, not a UID, so the
    # old `else` fallback flagged whichever message happened to occupy sequence
    # position == the UID value. When the UID isn't present, fail safe (callers
    # surface "Email not found") rather than touch an unrelated message.
    if not _uid_exists(conn, uid):
        return False
    op = "+FLAGS" if add else "-FLAGS"
    status, _ = conn.uid("STORE", _uid_bytes(uid), op, flag)
    return status == "OK"


def _move_email_message(conn, uid: str, dest: str, role: str = "") -> bool:
    dest = _resolve_mail_folder(conn, dest, role or _folder_role_from_name(dest))
    # copy()/store() are SEQUENCE-NUMBER commands; using them with a UID (the old
    # `else` branch) copied + \Deleted-flagged the wrong message and then

View on GitHub (pinned to f9235ebbf1)

Solutions

  1. Add and enable at least one account with SMTP host + credentials (or Google OAuth with send scope).
  2. Check server DEBUG logs for 'SMTP-capable account fallback failed' to see why each candidate was rejected.
  3. In the UI, block compose/send until _resolve_send_config could succeed (at least one enabled SMTP account).

Example fix

# before
await send_email(to=..., subject=..., body=...)  # no accounts -> ValueError
# after
if not any(a.enabled and a.smtp_host for a in accounts):
    raise UserVisibleError('Configure an outgoing email account first')
await send_email(to=..., subject=..., body=...)
Defensive patterns

Strategy: validation

Validate before calling

const capable = (await api.get('/api/emails/accounts')).filter(a => a.enabled && a.smtp_host);
if (capable.length === 0) { promptAccountSetup(); return; }

Type guard

function hasSmtpAccount(accts: Array<{enabled: boolean; smtp_host?: string | null}>): boolean {
  return accts.some(a => a.enabled && a.smtp_host);
}

Try / catch

try { await sendEmail(...); } catch (e) { if (/No SMTP-capable email account/.test(e.message)) { promptAccountSetup(); return; } throw e; }

Prevention

When it happens

Trigger: Calling send endpoints on a fresh install with zero accounts; all accounts receive-only; every SMTP-capable account disabled (enabled=False); Google accounts whose token refresh fails during the trial loop.

Common situations: First-run send attempt before any account setup; account created via auto-discover that populated IMAP but left SMTP blank; all accounts toggled off but UI still allows compose.

Related errors


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