# NousResearch/hermes-agent · Email adapter IMAP UID and SMTP TLS fixes

## Open now

- Nothing open on GitHub.

These items address a bug where the email adapter incorrectly uses IMAP sequence numbers instead of UIDs, and STARTTLS skips certificate verification. Two entries appear to be duplicate fix submissions for the same reported issue.

## TL;DR

- 3 member(s): 0 open issue(s), 1 closed issue(s), 0 open PR(s), 1 merged PR(s), 1 PR(s) closed without merging.
- A fix was merged to main in PR #1305. Verify it addresses your case.
- **Recommended:** read PR #1305 (and any competing PRs below) before filing. Confirm the merged change covers your symptom.

## Maintainer resolution verdict

- Internal-tracker verdict over this cluster's issue members: `resolved` · deciding PR(s): `pr-1305`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_SPANNING`): every member is connected by structural evidence: shared changed files, or a closing/superseded reference.
- Pairwise link density `0.67`: the share of member pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. `1.00` means every pair is linked.

## Closed / duplicate issues

- #991: Email adapter uses IMAP sequence numbers instead of UIDs, starttls skips cert verification

## Prior & competing PRs

- **#1305: fix: email adapter IMAP UID tracking and SMTP TLS verification**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`
  - files: `gateway/platforms/email.py`, `tests/gateway/test_email.py`, `tools/send_message_tool.py`
  - fix hunks:
  - `gateway/platforms/email.py`

    ```diff
    @@ -22,6 +22,7 @@
     import os
     import re
     import smtplib
    +import ssl
     import uuid
     from datetime import datetime
     from email.header import decode_header
    ```

    ```diff
    @@ -212,7 +213,7 @@ async def connect(self) -> bool:
                 imap.login(self._address, self._password)
                 # Mark all existing messages as seen so we only process new ones
                 imap.select("INBOX")
    -            status, data = imap.search(None, "ALL")
    +            status, data = imap.uid("search", None, "ALL")
                 if status == "OK" and data[0]:
                     for uid in data[0].split():
                         self._seen_uids.add(uid)
    ```

    ```diff
    @@ -225,7 +226,7 @@ async def connect(self) -> bool:
             try:
                 # Test SMTP connection
                 smtp = smtplib.SMTP(self._smtp_host, self._smtp_port)
    -            smtp.starttls()
    +            smtp.starttls(context=ssl.create_default_context())
                 smtp.login(self._address, self._password)
                 smtp.quit()
                 logger.info("[Email] SMTP connection test passed.")
    ```

    ```diff
    @@ -277,7 +278,7 @@ def _fetch_new_messages(self) -> List[Dict[str, Any]]:
                 imap.login(self._address, self._password)
                 imap.select("INBOX")
     
    -            status, data = imap.search(None, "UNSEEN")
    +            status, data = imap.uid("search", None, "UNSEEN")
                 if status != "OK" or not data[0]:
                     imap.logout()
                     return results
    ```

    ```diff
    @@ -287,7 +288,7 @@ def _fetch_new_messages(self) -> List[Dict[str, Any]]:
                         continue
                     self._seen_uids.add(uid)
     
    -                status, msg_data = imap.fetch(uid, "(RFC822)")
    +                status, msg_data = imap.uid("fetch", uid, "(RFC822)")
                     if status != "OK":
                         continue
     
    ```

    ```diff
    @@ -427,7 +428,7 @@ def _send_email(
             msg.attach(MIMEText(body, "plain", "utf-8"))
     
             smtp = smtplib.SMTP(self._smtp_host, self._smtp_port)
    -        smtp.starttls()
    +        smtp.starttls(context=ssl.create_default_context())
             smtp.login(self._address, self._password)
             smtp.send_message(msg)
             smtp.quit()
    ```

    ```diff
    @@ -515,7 +516,7 @@ def _send_email_with_attachment(
                 msg.attach(part)
     
             smtp = smtplib.SMTP(self._smtp_host, self._smtp_port)
    -        smtp.starttls()
    +        smtp.starttls(context=ssl.create_default_context())
             smtp.login(self._address, self._password)
             smtp.send_message(msg)
             smtp.quit()
    ```
  - `tests/gateway/test_email.py`

    ```diff
    @@ -797,7 +797,7 @@ def test_connect_success(self):
             adapter = self._make_adapter()
     
             mock_imap = MagicMock()
    -        mock_imap.search.return_value = ("OK", [b"1 2 3"])
    +        mock_imap.uid.return_value = ("OK", [b"1 2 3"])
     
             with patch("imaplib.IMAP4_SSL", return_value=mock_imap), \
                  patch("smtplib.SMTP") as mock_smtp:
    ```

    ```diff
    @@ -831,7 +831,7 @@ def test_connect_smtp_failure(self):
             adapter = self._make_adapter()
     
             mock_imap = MagicMock()
    -        mock_imap.search.return_value = ("OK", [b""])
    +        mock_imap.uid.return_value = ("OK", [b""])
     
             with patch("imaplib.IMAP4_SSL", return_value=mock_imap), \
                  patch("smtplib.SMTP", side_effect=Exception("SMTP down")):
    ```

    ```diff
    @@ -880,8 +880,15 @@ def test_fetch_skips_seen_uids(self):
             raw_email["Message-ID"] = "<[redacted:email]>"
     
             mock_imap = MagicMock()
    -        mock_imap.search.return_value = ("OK", [b"1 2 3"])
    -        mock_imap.fetch.return_value = ("OK", [(b"3", raw_email.as_bytes())])
    +
    +        def uid_handler(command, *args):
    +            if command == "search":
    +                return ("OK", [b"1 2 3"])
    +            if command == "fetch":
    +                return ("OK", [(b"3", raw_email.as_bytes())])
    +            return ("NO", [])
    +
    +        mock_imap.uid.side_effect = uid_handler
     
             with patch("imaplib.IMAP4_SSL", return_value=mock_imap):
                 results = adapter._fetch_new_messages()
    ```

    ```diff
    @@ -896,7 +903,7 @@ def test_fetch_no_unseen_messages(self):
             adapter = self._make_adapter()
     
             mock_imap = MagicMock()
    -        mock_imap.search.return_value = ("OK", [b""])
    +        mock_imap.uid.return_value = ("OK", [b""])
     
             with patch("imaplib.IMAP4_SSL", return_value=mock_imap):
                 results = adapter._fetch_new_messages()
    ```

    ```diff
    @@ -922,8 +929,15 @@ def test_fetch_extracts_sender_name(self):
             raw_email["Message-ID"] = "<[redacted:email]>"
     
             mock_imap = MagicMock()
    -        mock_imap.search.return_value = ("OK", [b"1"])
    -        mock_imap.fetch.return_value = ("OK", [(b"1", raw_email.as_bytes())])
    +
    +        def uid_handler(command, *args):
    +            if command == "search":
    +                return ("OK", [b"1"])
    +            if command == "fetch":
    +                return ("OK", [(b"1", raw_email.as_bytes())])
    +            return ("NO", [])
    +
    +        mock_imap.uid.side_effect = uid_handler
     
             with patch("imaplib.IMAP4_SSL", return_value=mock_imap):
                 results = adapter._fetch_new_messages()
    ```

    ```diff
    @@ -966,8 +980,15 @@ async def mock_dispatch(msg_data):
             raw_email["Message-ID"] = "<[redacted:email]>"
     
             mock_imap = MagicMock()
    -        mock_imap.search.return_value = ("OK", [b"1"])
    -        mock_imap.fetch.return_value = ("OK", [(b"1", raw_email.as_bytes())])
    +
    +        def uid_handler(command, *args):
    +            if command == "search":
    +                return ("OK", [b"1"])
    +            if command == "fetch":
    +                return ("OK", [(b"1", raw_email.as_bytes())])
    +            return ("NO", [])
    +
    +        mock_imap.uid.side_effect = uid_handler
     
             with patch("imaplib.IMAP4_SSL", return_value=mock_imap):
                 asyncio.run(adapter._check_inbox())
    ```

    ```diff
    @@ -986,8 +1007,9 @@ class TestSendEmailStandalone(unittest.TestCase):
             "EMAIL_SMTP_PORT": "587",
         })
         def test_send_email_tool_success(self):
    -        """_send_email should use SMTP to send."""
    +        """_send_email should use verified STARTTLS when sending."""
             import asyncio
    +        import ssl
             from tools.send_message_tool import _send_email
     
             with patch("smtplib.SMTP") as mock_smtp:
    ```

    ```diff
    @@ -1000,6 +1022,8 @@ def test_send_email_tool_success(self):
     
                 self.assertTrue(result["success"])
                 self.assertEqual(result["platform"], "email")
    +            _, kwargs = mock_server.starttls.call_args
    +            self.assertIsInstance(kwargs["context"], ssl.SSLContext)
     
         @patch.dict(os.environ, {
             "EMAIL_ADDRESS": "[redacted:email]",
    ```
  - `tools/send_message_tool.py`

    ```diff
    @@ -9,6 +9,7 @@
     import logging
     import os
     import re
    +import ssl
     import time
     
     logger = logging.getLogger(__name__)
    ```

    ```diff
    @@ -432,7 +433,7 @@ async def _send_email(extra, chat_id, message):
             msg["Subject"] = "Hermes Agent"
     
             server = smtplib.SMTP(smtp_host, smtp_port)
    -        server.starttls()
    +        server.starttls(context=ssl.create_default_context())
             server.login(address, password)
             server.send_message(msg)
             server.quit()
    ```
- **#994: fix: email adapter IMAP UID tracking and SMTP TLS verification**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `#1305`

## Fix status

- Merged to main in PR #1305 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-03-14`. This confirmation is old and may be out of date (a revert/reopen would not have invalidated it here); re-verify.
- Subsystem: `gateway`
