fix(email): send IMAP ID extension to support 163/NetEase mailbox

163/NetEase IMAP servers reject every UID SEARCH/FETCH with `BYE Unsafe
Login` unless the client first identifies itself via the RFC 2971 ID
command after LOGIN.  Without this, the email gateway logs in OK but
then fails on the very first poll and the connection is torn down.

Send the ID payload best-effort after both `imap.login()` sites
(`EmailAdapter.connect` and `_fetch_new_messages`).  Failures are
swallowed at debug level so non-supporting IMAP servers (Gmail,
Outlook, Fastmail, Yahoo, etc.) keep working unchanged.

Closes #22271
This commit is contained in:
Wesley Simplicio 2026-05-09 08:47:28 -03:00 committed by Teknium
parent 48bf0ea249
commit 3fd4ccbd8b
2 changed files with 96 additions and 0 deletions

View file

@ -65,6 +65,25 @@ MAX_MESSAGE_LENGTH = 50_000
# Supported image extensions for inline detection
_IMAGE_EXTS = {".jpg", ".jpeg", ".png", ".gif", ".webp"}
def _send_imap_id(imap: "imaplib.IMAP4") -> None:
"""Send RFC 2971 IMAP ID command identifying this client.
Required by 163/NetEase mailbox after LOGIN: without it, every UID
SEARCH/FETCH returns ``BYE Unsafe Login`` and disconnects. Other
IMAP servers either honor it silently or reject the unknown command;
we swallow failures so non-supporting servers keep working.
"""
try:
imap.xatom(
"ID",
'("name" "hermes-agent" "version" "1.0" '
'"vendor" "NousResearch" '
'"support-email" "noreply@nousresearch.com")',
)
except Exception as e: # noqa: BLE001 — best-effort, never fatal
logger.debug("[Email] IMAP ID command not accepted: %s", e)
def _is_automated_sender(address: str, headers: dict) -> bool:
"""Return True if this email is from an automated/noreply source."""
addr = address.lower()
@ -276,6 +295,7 @@ class EmailAdapter(BasePlatformAdapter):
# Test IMAP connection
imap = imaplib.IMAP4_SSL(self._imap_host, self._imap_port, timeout=30)
imap.login(self._address, self._password)
_send_imap_id(imap)
# Mark all existing messages as seen so we only process new ones
imap.select("INBOX")
status, data = imap.uid("search", None, "ALL")
@ -344,6 +364,7 @@ class EmailAdapter(BasePlatformAdapter):
imap = imaplib.IMAP4_SSL(self._imap_host, self._imap_port, timeout=30)
try:
imap.login(self._address, self._password)
_send_imap_id(imap)
imap.select("INBOX")
status, data = imap.uid("search", None, "UNSEEN")