odysseus-dev/odysseus · warning · EmailNotConfiguredError

IMAP is not configured for account {cfg.get('account_name')

Error message

IMAP is not configured for account {cfg.get('account_name') or 'default'!r}

What it means

EmailNotConfiguredError raised by the IMAP connection helper when the resolved account config has no imap_host. This is the typed guard for SMTP-only (send-only) accounts: without it imaplib would dial localhost:993 and fail with a confusing '[Errno 111] Connection refused'.

Source

Thrown at routes/email_helpers.py:1214

    # "got more than 1000000 bytes" on UID SEARCH ALL.  (#2883)
    imaplib._MAXLINE = 50_000_000
    return conn

def _imap_connect(account_id: str | None = None, owner: str = "",
                  timeout: int = _IMAP_TIMEOUT_SECONDS):
    # SECURITY: passing `owner` scopes the fallback config lookup so a brand
    # new user doesn't get connected against another user's default mailbox
    # when they have no account configured.
    #
    # `timeout` is overridable so short-lived callers (e.g. the service-health
    # probe) can impose a tighter budget than the default IMAP timeout.
    cfg = _get_email_config(account_id, owner=owner)
    # Send-only (SMTP-only) account: no IMAP host means there is no inbox to
    # read. Bail out with a clear, typed error instead of handing an empty
    # host to imaplib — IMAP4("", 993) silently dials localhost:993 and fails
    # with a confusing "[Errno 111] Connection refused" on every inbox poll.
    if not cfg.get("imap_host"):
        raise EmailNotConfiguredError(
            f"IMAP is not configured for account {cfg.get('account_name') or 'default'!r}"
        )
    # Connection mode:
    #   STARTTLS on → plain + upgrade
    #   STARTTLS off + port 993 → implicit SSL (IMAPS)
    #   STARTTLS off + any other port → plain (local Dovecot, custom ports)
    # The last branch is critical: previously this fell into IMAP4_SSL
    # for any non-STARTTLS port, which would fail the TLS handshake on
    # plain local servers (Dovecot on 31143, etc.).
    conn = _open_imap_connection(
        cfg["imap_host"],
        cfg["imap_port"],
        starttls=bool(cfg.get("imap_starttls")),
        timeout=timeout,
    )
    try:
        if cfg.get("oauth_provider") == "google":
            token = _get_valid_google_token(cfg.get("account_id"), cfg)

View on GitHub (pinned to f9235ebbf1)

Solutions

  1. Fill in IMAP host/port for the account (e.g. imap.gmail.com:993) if it should receive mail.
  2. If send-only is intended, stop calling inbox endpoints with that account_id and filter send-only accounts out of poll loops.
  3. Catch EmailNotConfiguredError client-side and show a 'receive not configured' state instead of retrying.

Example fix

# before
for acct in all_enabled_accounts:
    with _imap(acct.id) as c: ...  # raises for send-only
# after
for acct in all_enabled_accounts:
    if not account_cfg(acct.id).get('imap_host'):
        continue
    with _imap(acct.id) as c: ...
Defensive patterns

Strategy: type-guard

Validate before calling

const cfg = await api.getAccount(account_id);
if (!cfg.imap_host) { markSendOnly(account_id); skipInboxPoll(account_id); }

Type guard

function canReceive(cfg: { imap_host?: string | null }): boolean { return Boolean(cfg.imap_host); }

Try / catch

try { with _imap(account_id, owner=owner) as c: ... }
except EmailNotConfiguredError as e:
    log.info(f'skip inbox for send-only account: {e}')

Prevention

When it happens

Trigger: Opening an inbox/folder/poll on an account configured with only smtp_host (e.g. a transactional send account), passing account_id of such an account to any read endpoint, or a default account that was never given IMAP settings.

Common situations: User sets up Gmail SMTP-only for sending then clicks Inbox; scheduled poller iterating all enabled accounts including send-only ones; default account row with empty IMAP fields.

Related errors


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