NousResearch/hermes-agent

Email adapter IMAP UID and SMTP TLS fixes

Cluster 991 · gateway. Data as of

Nothing open on GitHub. Fix merged in #1305 on 14 Mar 2026. Check it covers your case.

The short answer

0open issues
0open PRs
3items grouped
Known?
Yes. 3 related items grouped here, 1 issue already closed.
Fix event
A fix was merged to main in PR #1305. Check it covers your case. No release data is tracked.
Tracker
The maintainers' tracker marks this resolved, deciding PR #1305. That is separate from the fix event.
Still open
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.

Open issues0

No open issues.

Open PRs0

No open pull requests.

Fixes on record

Merged to main. A stamp counts the issues a fix is linked to.

  • #1305 fix: email adapter IMAP UID tracking and SMTP TLS verification

    merged to main

    files: gateway/platforms/email.py, tests/gateway/test_email.py, tools/send_message_tool.py

    Show the diff
    • gateway/platforms/email.py
      @@ -22,6 +22,7 @@
       import os
       import re
       import smtplib
      +import ssl
       import uuid
       from datetime import datetime
       from email.header import decode_header
      @@ -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)
      @@ -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.")
      @@ -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
      @@ -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
       
      @@ -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()
      @@ -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
      @@ -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:
      @@ -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")):
      @@ -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()
      @@ -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()
      @@ -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()
      @@ -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())
      @@ -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:
      @@ -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
      @@ -9,6 +9,7 @@
       import logging
       import os
       import re
      +import ssl
       import time
       
       logger = logging.getLogger(__name__)
      @@ -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()

1 PR closed without merging

From the close comments: merged via another change 1

  • #994 fix: email adapter IMAP UID tracking and SMTP TLS verification · merged via another change → #1305
Closed issues · 1
  • Closed#991Email adapter uses IMAP sequence numbers instead of UIDs, starttls skips cert verification
How this cluster was grouped
  • Link evidence (structural links throughout): every member is connected by structural evidence: shared changed files, or a closing/superseded reference.
  • Pairwise link density 0.67: the share of item 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.

Pick up a Lab task

Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.

Open the Lab

At a glance

Subsystem
gateway
Merged PRs
#1305
Closed issues
1
Attempts
1 PR closed without merging
Fix link checked
14 Mar 2026. This confirmation is old and may be out of date (a revert or reopen would not have invalidated it here); re-verify.

For your agent

The same facts, typed.