Merge pull request #1305 from NousResearch/hermes/hermes-2ba57c8a
fix: email adapter IMAP UID tracking and SMTP TLS verification
This commit is contained in:
commit
63309065b6
3 changed files with 43 additions and 17 deletions
|
|
@ -22,6 +22,7 @@ import logging
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import smtplib
|
import smtplib
|
||||||
|
import ssl
|
||||||
import uuid
|
import uuid
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from email.header import decode_header
|
from email.header import decode_header
|
||||||
|
|
@ -212,7 +213,7 @@ class EmailAdapter(BasePlatformAdapter):
|
||||||
imap.login(self._address, self._password)
|
imap.login(self._address, self._password)
|
||||||
# Mark all existing messages as seen so we only process new ones
|
# Mark all existing messages as seen so we only process new ones
|
||||||
imap.select("INBOX")
|
imap.select("INBOX")
|
||||||
status, data = imap.search(None, "ALL")
|
status, data = imap.uid("search", None, "ALL")
|
||||||
if status == "OK" and data[0]:
|
if status == "OK" and data[0]:
|
||||||
for uid in data[0].split():
|
for uid in data[0].split():
|
||||||
self._seen_uids.add(uid)
|
self._seen_uids.add(uid)
|
||||||
|
|
@ -225,7 +226,7 @@ class EmailAdapter(BasePlatformAdapter):
|
||||||
try:
|
try:
|
||||||
# Test SMTP connection
|
# Test SMTP connection
|
||||||
smtp = smtplib.SMTP(self._smtp_host, self._smtp_port)
|
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.login(self._address, self._password)
|
||||||
smtp.quit()
|
smtp.quit()
|
||||||
logger.info("[Email] SMTP connection test passed.")
|
logger.info("[Email] SMTP connection test passed.")
|
||||||
|
|
@ -277,7 +278,7 @@ class EmailAdapter(BasePlatformAdapter):
|
||||||
imap.login(self._address, self._password)
|
imap.login(self._address, self._password)
|
||||||
imap.select("INBOX")
|
imap.select("INBOX")
|
||||||
|
|
||||||
status, data = imap.search(None, "UNSEEN")
|
status, data = imap.uid("search", None, "UNSEEN")
|
||||||
if status != "OK" or not data[0]:
|
if status != "OK" or not data[0]:
|
||||||
imap.logout()
|
imap.logout()
|
||||||
return results
|
return results
|
||||||
|
|
@ -287,7 +288,7 @@ class EmailAdapter(BasePlatformAdapter):
|
||||||
continue
|
continue
|
||||||
self._seen_uids.add(uid)
|
self._seen_uids.add(uid)
|
||||||
|
|
||||||
status, msg_data = imap.fetch(uid, "(RFC822)")
|
status, msg_data = imap.uid("fetch", uid, "(RFC822)")
|
||||||
if status != "OK":
|
if status != "OK":
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
|
@ -427,7 +428,7 @@ class EmailAdapter(BasePlatformAdapter):
|
||||||
msg.attach(MIMEText(body, "plain", "utf-8"))
|
msg.attach(MIMEText(body, "plain", "utf-8"))
|
||||||
|
|
||||||
smtp = smtplib.SMTP(self._smtp_host, self._smtp_port)
|
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.login(self._address, self._password)
|
||||||
smtp.send_message(msg)
|
smtp.send_message(msg)
|
||||||
smtp.quit()
|
smtp.quit()
|
||||||
|
|
@ -515,7 +516,7 @@ class EmailAdapter(BasePlatformAdapter):
|
||||||
msg.attach(part)
|
msg.attach(part)
|
||||||
|
|
||||||
smtp = smtplib.SMTP(self._smtp_host, self._smtp_port)
|
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.login(self._address, self._password)
|
||||||
smtp.send_message(msg)
|
smtp.send_message(msg)
|
||||||
smtp.quit()
|
smtp.quit()
|
||||||
|
|
|
||||||
|
|
@ -797,7 +797,7 @@ class TestConnectDisconnect(unittest.TestCase):
|
||||||
adapter = self._make_adapter()
|
adapter = self._make_adapter()
|
||||||
|
|
||||||
mock_imap = MagicMock()
|
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), \
|
with patch("imaplib.IMAP4_SSL", return_value=mock_imap), \
|
||||||
patch("smtplib.SMTP") as mock_smtp:
|
patch("smtplib.SMTP") as mock_smtp:
|
||||||
|
|
@ -831,7 +831,7 @@ class TestConnectDisconnect(unittest.TestCase):
|
||||||
adapter = self._make_adapter()
|
adapter = self._make_adapter()
|
||||||
|
|
||||||
mock_imap = MagicMock()
|
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), \
|
with patch("imaplib.IMAP4_SSL", return_value=mock_imap), \
|
||||||
patch("smtplib.SMTP", side_effect=Exception("SMTP down")):
|
patch("smtplib.SMTP", side_effect=Exception("SMTP down")):
|
||||||
|
|
@ -880,8 +880,15 @@ class TestFetchNewMessages(unittest.TestCase):
|
||||||
raw_email["Message-ID"] = "<msg@test.com>"
|
raw_email["Message-ID"] = "<msg@test.com>"
|
||||||
|
|
||||||
mock_imap = MagicMock()
|
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):
|
with patch("imaplib.IMAP4_SSL", return_value=mock_imap):
|
||||||
results = adapter._fetch_new_messages()
|
results = adapter._fetch_new_messages()
|
||||||
|
|
@ -896,7 +903,7 @@ class TestFetchNewMessages(unittest.TestCase):
|
||||||
adapter = self._make_adapter()
|
adapter = self._make_adapter()
|
||||||
|
|
||||||
mock_imap = MagicMock()
|
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):
|
with patch("imaplib.IMAP4_SSL", return_value=mock_imap):
|
||||||
results = adapter._fetch_new_messages()
|
results = adapter._fetch_new_messages()
|
||||||
|
|
@ -922,8 +929,15 @@ class TestFetchNewMessages(unittest.TestCase):
|
||||||
raw_email["Message-ID"] = "<msg@test.com>"
|
raw_email["Message-ID"] = "<msg@test.com>"
|
||||||
|
|
||||||
mock_imap = MagicMock()
|
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):
|
with patch("imaplib.IMAP4_SSL", return_value=mock_imap):
|
||||||
results = adapter._fetch_new_messages()
|
results = adapter._fetch_new_messages()
|
||||||
|
|
@ -966,8 +980,15 @@ class TestPollLoop(unittest.TestCase):
|
||||||
raw_email["Message-ID"] = "<inbox@test.com>"
|
raw_email["Message-ID"] = "<inbox@test.com>"
|
||||||
|
|
||||||
mock_imap = MagicMock()
|
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):
|
with patch("imaplib.IMAP4_SSL", return_value=mock_imap):
|
||||||
asyncio.run(adapter._check_inbox())
|
asyncio.run(adapter._check_inbox())
|
||||||
|
|
@ -986,8 +1007,9 @@ class TestSendEmailStandalone(unittest.TestCase):
|
||||||
"EMAIL_SMTP_PORT": "587",
|
"EMAIL_SMTP_PORT": "587",
|
||||||
})
|
})
|
||||||
def test_send_email_tool_success(self):
|
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 asyncio
|
||||||
|
import ssl
|
||||||
from tools.send_message_tool import _send_email
|
from tools.send_message_tool import _send_email
|
||||||
|
|
||||||
with patch("smtplib.SMTP") as mock_smtp:
|
with patch("smtplib.SMTP") as mock_smtp:
|
||||||
|
|
@ -1000,6 +1022,8 @@ class TestSendEmailStandalone(unittest.TestCase):
|
||||||
|
|
||||||
self.assertTrue(result["success"])
|
self.assertTrue(result["success"])
|
||||||
self.assertEqual(result["platform"], "email")
|
self.assertEqual(result["platform"], "email")
|
||||||
|
_, kwargs = mock_server.starttls.call_args
|
||||||
|
self.assertIsInstance(kwargs["context"], ssl.SSLContext)
|
||||||
|
|
||||||
@patch.dict(os.environ, {
|
@patch.dict(os.environ, {
|
||||||
"EMAIL_ADDRESS": "hermes@test.com",
|
"EMAIL_ADDRESS": "hermes@test.com",
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
import ssl
|
||||||
import time
|
import time
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
@ -432,7 +433,7 @@ async def _send_email(extra, chat_id, message):
|
||||||
msg["Subject"] = "Hermes Agent"
|
msg["Subject"] = "Hermes Agent"
|
||||||
|
|
||||||
server = smtplib.SMTP(smtp_host, smtp_port)
|
server = smtplib.SMTP(smtp_host, smtp_port)
|
||||||
server.starttls()
|
server.starttls(context=ssl.create_default_context())
|
||||||
server.login(address, password)
|
server.login(address, password)
|
||||||
server.send_message(msg)
|
server.send_message(msg)
|
||||||
server.quit()
|
server.quit()
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue