Email (IMAP/SMTP)
IMAP and SMTP toolsets.
#Email connectors (IMAP + SMTP)
Two complementary connectors cover generic email workflows:
IMAPToolSet— read and modify an IMAP mailbox.SMTPToolSet— outbound delivery and connection validation via SMTP.
Both use stdlib clients (imaplib, smtplib) and accept a custom client_factory so tests can plug in fakes without opening real network connections.
Agent-ready pattern.IMAPToolSet.search_messagesreturns compact human-readable summaries by default with a stablemessage_reffield and the UID kept inline (the UID is the natural handle forfetch_message/mark_read/delete_message). Write tools (set_flags,mark_read,mark_unread,copy_message,move_message,delete_message) accept either a raw UID int, a numeric string, or the message-summary dict fromsearch_messages. The SMTP connector exposes one effective send tool (send_email) plusvalidate_connectionandsend_raw_email; sending is destructive because outbound email cannot be unsent.
#IMAPToolSet
from maivn import Agent
from maivn_tools import IMAPToolSet, register_connector
connector = IMAPToolSet(
host="imap.example.com",
username="ops@example.com",
password=secrets["IMAP_PASSWORD"],
port=993,
use_ssl=True,
)
agent = Agent(name="mail-agent", model="auto")
register_connector(agent, connector)#Tools
search_messages accepts any IMAP SEARCH criteria (UNSEEN, FROM "alice@example.com", etc.).
#Agent-ready behavior
search_messages returns compact summaries by default: each match gets a response-local message_ref (message_1, message_2, ...), uid, sender, subject, received_at (Date header), and flags. Summary mode caps limit at 10 because each UID requires a per-message ENVELOPE fetch — for raw UID lists, pass include_metadata=False to get the legacy wire format ({"uids": [...]}). Default limit is
- The UID is intentionally exposed in summary mode because every follow-up tool needs it.
imap.search_messages(criteria="UNSEEN")
# { "mailbox": "INBOX",
# "messages": [{"message_ref": "message_1", "uid": 4242,
# "sender": "Alice <alice@example>",
# "subject": "Q3 plan", "received_at": "...",
# "flags": []}], "totalMatched": 7,
# "requestedLimit": 25, "summaryLimit": 10 }
# Raw UID list, no per-message fetch.
imap.search_messages(criteria="UNSEEN", include_metadata=False)
# { "mailbox": "INBOX", "uids": [4242, 4243, 4244, ...] }fetch_message, set_flags, mark_read, mark_unread, copy_message, move_message, and delete_message all accept either a raw UID int, a numeric string, or a message dict from search_messages — the UID is extracted automatically.
#Destructive tools
delete_message, expunge, and delete_mailbox are tagged destructive=True and remove messages or folders server-side without recovery. Filter them out with exclude_tags=["destructive"] for triage-only agents.
#Filtering at registration
# Read-only IMAP for a summarization agent.
agent.add_toolset(connector, include_tags=["read"])
# Read + write but never destructive.
agent.add_toolset(connector, exclude_tags=["destructive"])#SMTPToolSet
from maivn_tools import SMTPToolSet, register_connector
sender = SMTPToolSet(
host="smtp.example.com",
port=587,
use_ssl=False,
starttls=True,
username="ops@example.com",
password=secrets["SMTP_PASSWORD"],
sender="ops@example.com",
)
agent = Agent(name="smtp-agent", model="auto")
register_connector(agent, sender)#Tools
send_email returns:
{
"accepted": ["user@example.com"],
"refused": {"bad@example.com": [550, "User unknown"]},
"subject": "Hello",
"to": ["user@example.com"]
}Attachment content must be bytes (or a string, which is encoded as UTF-8).
#Agent-ready behavior
The SMTP surface is intentionally narrow: there are no list/search tools because SMTP is send-only. validate_connection is the cheap read tool for a startup sanity check. Both send_email and send_raw_email should be treated as high-impact writes — once an email leaves the server it cannot be unsent.
#Destructive tools
The connector does not currently tag send_email / send_raw_email as destructive=True (they remain plain WRITE), but the effect is irreversible. Always confirm the recipient list with the user before calling in an interactive loop. If you need to drop send tools at registration, use include_tags=["read"] to keep only validate_connection.