fix(email): don't probe IMAP for send-only (SMTP-only) accounts (#4830)

An account configured with SMTP only (no imap_host) has no inbox, but the
inbox list path still called _imap_connect, which handed an empty host to
imaplib. imaplib.IMAP4("", 993) silently dials localhost:993 and fails with
"[Errno 111] Connection refused", so the email panel's poll logged a
"Failed to list emails" ERROR every ~60s and surfaced a scary error in the UI.

_imap_connect now fails fast with a typed EmailNotConfiguredError (subclass of
RuntimeError, so existing broad handlers keep working) when no imap_host is set,
and the inbox list returns an empty result for that case instead of an error.
SMTP send is unaffected.
This commit is contained in:
Ricardo
2026-06-27 22:52:26 +02:00
committed by GitHub
parent 20cf323ca4
commit 3b4187e25d
3 changed files with 99 additions and 0 deletions
+18
View File
@@ -40,6 +40,16 @@ from src.secret_storage import decrypt as _decrypt
logger = logging.getLogger(__name__)
class EmailNotConfiguredError(RuntimeError):
"""Raised when an IMAP operation is attempted on an account that has no
inbox configured (e.g. a send-only / SMTP-only account).
Subclasses RuntimeError so existing broad ``except Exception`` handlers
keep working; callers that want to treat "no inbox" as an empty result
rather than a failure can catch this type specifically.
"""
def _xoauth2_raw(user: str, access_token: str) -> str:
"""The SASL XOAUTH2 initial-response string (unencoded).
@@ -929,6 +939,14 @@ def _imap_connect(account_id: str | None = None, owner: str = "",
# `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)