odysseus-dev/odysseus · error · ValueError

Email account {cfg.get('account_name') or account_id} has no

Error message

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

What it means

ValueError from _resolve_send_config when the caller explicitly passed account_id but that account's config fails _smtp_ready (no usable smtp_host/credentials). Explicit account choice is honored strictly — no fallback — so a receive-only account selected for sending fails immediately with the account's name in the message.

Source

Thrown at routes/email_routes.py:1304

def _smtp_ready(cfg: dict) -> bool:
    if not cfg.get("smtp_host") or not cfg.get("smtp_user"):
        return False
    return bool(cfg.get("smtp_password") or cfg.get("oauth_provider"))


def _resolve_send_config(account_id: str | None = None, owner: str = "") -> dict:
    """Resolve an account for outbound SMTP.

    If the caller explicitly picked an account, use only that account and
    return a clear error when it cannot send. If no account was picked and
    the default is receive-only, fall back to the first SMTP-capable account
    owned by the same user.
    """
    cfg = _get_email_config(account_id, owner=owner)
    if _smtp_ready(cfg):
        return cfg
    if account_id:
        raise ValueError(f"Email account {cfg.get('account_name') or account_id} has no SMTP configured")
    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}")

View on GitHub (pinned to f9235ebbf1)

Solutions

  1. Pick an account that has SMTP configured (smtp_host set, credentials or valid OAuth token).
  2. Complete SMTP settings on the chosen account, or connect it via Google OAuth with gmail.send.
  3. Alternatively omit account_id so the fallback can select the first SMTP-capable account owned by the caller.

Example fix

# before
send_email(to, subject, body, account_id='inbox-only-acct')  # ValueError
# after
send_email(to, subject, body)  # or account_id of an SMTP-capable account
Defensive patterns

Strategy: validation

Validate before calling

const sendable = (await api.get('/api/emails/accounts')).filter(a => a.enabled && a.smtp_host);
if (account_id && !sendable.some(a => a.id === account_id)) throw new Error('chosen account cannot send');

Type guard

function canSend(a: { enabled: boolean; smtp_host?: string | null }): boolean {
  return Boolean(a.enabled && a.smtp_host);
}

Try / catch

try { await sendWith(account_id); } catch (e) { if (/has no SMTP configured/.test(e.message)) { pickSmtpAccount(); } else throw e; }

Prevention

When it happens

Trigger: POST a send/schedule/test-email endpoint with account_id of an IMAP-only (receive-only) account, or a Google OAuth account whose SMTP side is not ready (see 465-style token failure during readiness check).

Common situations: UI defaulting to the first account which is the receive-only inbox; OAuth account whose token refresh fails at _smtp_ready time; account saved with smtp_host empty.

Related errors


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