Read-only tools in the misc MCP connector for finding and reading the jsr_critchley@hotmail.com Outlook mailbox. These tools are distinct from email_send, email_list, email_read, and email_list_folders, which operate on mail.critchley.biz.
Use outlook_search to locate mail. Each result contains a folder and stable folder UID. Pass both values to outlook_read for an automatic decoded view. If a message is unusual or you need an attachment/body alternative, call outlook_list_parts and then outlook_read_part with the returned part_id. Use outlook_folders to discover folder names and outlook_status for message/unread counts.
outlook_search(sender="person@example.com", subject="booking", all_folders=true)
→ {messages: [{folder: "Inbox", uid: "186002", ...}]}
outlook_read(folder="Inbox", uid="186002")
→ decoded headers, text body, and attachment metadata
outlook_folders — no arguments. Returns count and folder names.outlook_status — optional folder (default INBOX). Returns messages, unseen, UIDNEXT, and UIDVALIDITY.outlook_search — filters by folder, all_folders, sender, recipient, subject, text, since, before, and unread; limit is 1–100 and applies per folder during all-folder searches.outlook_read — requires uid; folder defaults to INBOX. Returns decoded headers, automatically selected readable body, body_source and body_status, attachment metadata with reusable part IDs, and a MIME parts summary.outlook_list_parts — requires uid; folder defaults to INBOX. Returns the deterministic MIME tree with part_id, type, charset, disposition, filename, transfer encoding, decoded size, container/child information, and attachment/inline/body-candidate flags.outlook_read_part — requires uid and part_id; folder defaults to INBOX. Fetches only the selected IMAP section and reads it as auto, text, html, or base64. HTML parts can be returned unchanged. Asking for html on another text/* subtype returns the unchanged decoded text plus a warning. max_bytes defaults to 1,000,000 and is bounded to 5,000,000.
sender, recipient, subject, and text are substring searches performed by Outlook's IMAP SEARCH implementation. text searches headers and body. since is inclusive and before is exclusive; both use YYYY-MM-DD. unread restricts results to messages without the Seen flag. Set all_folders=true to ignore folder and scan every selectable folder.
Search results are returned newest UID first within each folder. For an all-folder search, results retain folder order rather than pretending UIDs or possibly malformed Date headers form one reliable global chronology.
All Outlook tools select folders read-only and fetch with BODY.PEEK, so searching and reading do not mark messages read. The tools expose no move, delete, flag, append, or send operation.
UIDs are stable only within a folder and its UIDVALIDITY generation. Always retain the folder with the UID. Moving a message to another folder assigns a different UID. Sequence numbers are deliberately not exposed because they can change when mailbox contents change.
The server uses OAuth2/XOAUTH2 with delegated https://outlook.office.com/IMAP.AccessAsUser.All permission. Credentials are read from the outlook.office365.com entry in ~/.netrc: login is the mailbox, account is MSAL:<client-id>, and password is the refresh token. The netrc file must remain mode 0600.
The misc service on gravlax normally connects directly. For hosts requiring the system tunnel, set OUTLOOK_SOCKS_PROXY=socks5h://127.0.0.1:1080; both the OAuth token exchange and IMAP TLS connection use it.
In /home/john/py/popit3, run get_pop_refresh_token.py --update-netrc-file. Complete Microsoft's device flow and grant the requested POP and IMAP access. Copy/update only the outlook.office365.com credential on the deployed host, then verify outlook_status and outlook_search. Do not print refresh or access tokens into logs or notes.
An unauthorized-or-expired scope error means the refresh token lacks IMAP consent; repeat device authorization requesting IMAP.AccessAsUser.All. Authentication failures after a valid token can require confirming the activity at account.live.com/activity. Folder command argument errors usually indicate an incorrectly quoted folder name; the shared client quotes names containing spaces.
Implementation: /home/john/py/gdata-server/outlook_mail_tools.py (MCP wrappers) and outlook_imap.py (OAuth, IMAP, search, MIME parsing). Standalone development copy: /home/john/py/popit3/outlook_imap.py. Blocking network work runs through asyncio.to_thread. Selected-part reads use BODY.PEEK[part.MIME] plus a bounded partial BODY.PEEK[part] fetch, rather than downloading the whole message. The automatic whole-message reader and MIME-tree listing still parse BODY.PEEK[] for maximum compatibility.
Connector architecture: misc-server. Complete tool index: misc-server/tools. The unrelated local-mail test tools are documented at misc-server/email-tools.
Deployed to gravlax and enabled in misc-mcp-server.service. The 2026-10-05 hardening deployment is active. Live Outlook verification against INBOX UID 195211 confirmed targeted part 2 retrieval, link-preserving HTML-to-text output, and part 1 requested as html returning its four raw whitespace bytes with a structured content-type warning. Unit coverage is 14 passing tests. Commits: popit3 7918768; gdata-server ebaef67.
RESOLVED and hardened (2026-10-05): extraction handles HTML-only and empty-plain-alternative messages; descendants of attached message/rfc822 parts cannot become the outer body; missing bodies have an explicit body_status; unknown charsets fall back safely; HTML-to-text retains link targets; attachments expose reusable part IDs; and targeted reads are network-bounded. See bugs/misc-outlook-read-empty-html-body.
Implementation and deployment lessons from this work: misc-server/outlook-mail/lessons.
Outstanding improvements (routing documentation, calendar-invite parsing, header noise, search semantics, error handling): misc-server/outlook-mail/todo.