Skip to main content
56 toolsOAuth
outlook_example.py
Fifty-six tools over one Outlook mailbox, one per Microsoft Graph v1.0 operation and named after it: messages_update is PATCH /me/messages/{id}, which is how Graph marks a message read, flags it, categorises it and edits a draft. Where an operation is served at more than one path, or takes its body in more than one format, it is still one tool — the difference is an optional argument, and the model never chooses a URL.

Authenticating

Before the first call, Outlook needs a token from an app registered with Microsoft Entra. Microsoft issues no API key for mail. Register an app, grant it the delegated Graph permissions these tools ask for — Mail.ReadWrite, Mail.Send and MailboxSettings.ReadWrite cover every write — and sign in through it. To try a tool on your own mailbox first, Graph Explorer shows the signed-in account’s token on its Access token tab; export MICROSOFT_ACCESS_TOKEN=…. This pack takes an OAuth bearer token, and configure() is optional when $MICROSOFT_ACCESS_TOKEN is set. Which credential provider you hand it depends on whose account the calls run as.

A token you hold

For a script, or a notebook. EnvTokenProvider re-reads the variable on every call, so a token rotated beside the process is picked up without a restart; StaticTokenProvider takes one you already hold as a string. Neither renews anything, so the calls stop when the token expires.
outlook_script.py

One account, refreshed

For an agent or a server acting as you. OAuth2Client turns a client registration and a stored refresh token into an access token, and renews it before it lapses.
outlook_agent.py
No refresh token yet? Getting the grant is the one-time consent flow that hands you one.

Many end users

For a product whose users each connect their own account. SubjectProvider builds one credential per user through a factory you write, and use_subject names the user a call acts for. Your users’ accounts is the consent route inside your app; serving many users is the per-subject cache and its eviction. The client is handed a function that reads the stored grant, not a copy of the refresh token. A server runs several workers, and a copy goes stale the first time another worker refreshes: More than one process.
outlook_server.py
The token is a delegated one: the tools act as the signed-in user, on /me. Microsoft’s authorization server publishes its metadata, so the MICROSOFT constant the snippets above name is one call — OAuth2Server.discover against https://login.microsoftonline.com/{tenant}/v2.0. Ask for offline_access beside the Graph scopes, or Microsoft issues no refresh token.

The client

oauth_tool_factory is the whole client: a thin wrapper over httpx that attaches your token and these endpoint constants to each request. No vendor SDK enters your dependency tree.
credentials is whichever of the three you built in Authenticating. A pack takes it through configure(); a client you build takes the same object as credential_provider, and has no configure() of its own.

Paging through a list

A cursor belongs to the tool that returns it, so it is declared on that tool’s builder call:
outlook_pagination.py
Pagination is declared on messages_list, messages_delta, attachments_list, mail_folders_list and mail_folders_delta. The other 51 take no cursor.

Tools

Each is a Tool, called with ainvoke as in the snippet above. The name links to its parameters, its response and what it costs.
Messagesmessages_listGETList or search messages, across the mailbox or in one folder (an ID, or a well-known name such as inbox or sentitems).messages_getGETRead one message: sender, recipients, subject, date and the body as plain text.messages_get_contentGETGet a message’s MIME content as text: every header, and each body part as encoded on the wire.messages_deltaGETTrack changes to the messages in one folder.messages_createPOSTSave a new draft, in Drafts unless folder_id names another folder, as a JSON message or as MIME. messages_send sends it later.messages_updatePATCHChange some of a message’s properties, leaving the rest: isRead, flag, importance, categories, and on a draft its recipients, subject and body.messages_deleteDELETEDelete a message or a draft.messages_permanent_deletePOSTPermanently delete a message.messages_movePOSTMove a message to another folder — archive, deleteditems, or any folder ID.messages_copyPOSTCopy a message into another folder, by folder ID or well-known name.messages_sendPOSTSend a saved draft — a new message, reply, reply-all or forward draft.messages_replyPOSTReply to the sender of a message right away.messages_reply_allPOSTReply to the sender and every recipient of a message right away, as JSON or MIME.messages_forwardPOSTForward a message right away.messages_create_replyPOSTCreate a draft reply to the sender of a message, to edit with messages_update and send with messages_send.messages_create_reply_allPOSTCreate a draft reply to the sender and every recipient of a message.messages_create_forwardPOSTCreate a draft forward of a message, to edit and send later.send_mailPOSTSend a new email right away, as a JSON message or as MIME.Attachmentsattachments_listGETList a message’s attachments: name, type, size and id, without the contents.attachments_getGETRead one attachment.attachments_get_contentGETGet an attachment’s raw contents: a file’s bytes (as text when they are text, otherwise base64), or an attached message’s MIME.attachments_createPOSTAttach a file (under 3 MB) or a message to a draft.attachments_create_upload_sessionPOSTStart attaching a file of 3 MB to 150 MB to a draft: returns the uploadUrl the file’s bytes are then PUT to, in ranges of up to 4 MB, before expirationDateTime.attachments_deleteDELETEDelete an attachment from a message or a draft.Foldersmail_folders_listGETList the folders under the mailbox root, or under parent_folder_id, with their unread and total counts.mail_folders_getGETRead one folder, by ID or well-known name, with its unread and total counts.mail_folders_createPOSTCreate a folder under the root or under parent_folder_id, or a search folder — a saved filter over folders you name.mail_folders_updatePATCHRename a folder or set its extended properties, or change a search folder’s source folders or filter.mail_folders_deleteDELETEDelete a folder, or a search folder, and what is in it.mail_folders_permanent_deletePOSTPermanently delete a folder and remove its items from the mailbox.mail_folders_movePOSTMove a folder and everything in it into another folder.mail_folders_copyPOSTCopy a folder and everything in it into another folder.mail_folders_deltaGETTrack changes to the mailbox’s folders.Categoriesmaster_categories_listGETList the categories defined for the mailbox, with their colors.master_categories_getGETRead one category.master_categories_createPOSTCreate a category.master_categories_updatePATCHChange a category’s color.master_categories_deleteDELETEDelete a category from the mailbox’s list.Inbox rulesmessage_rules_listGETList the Inbox rules, in the order they run.message_rules_getGETRead one Inbox rule: its conditions, exceptions and actions.message_rules_createPOSTCreate an Inbox rule: when an incoming message meets the conditions, and none of the exceptions, Outlook takes the actions.message_rules_updatePATCHChange some of an Inbox rule’s properties, leaving the rest as they are.message_rules_deleteDELETEDelete an Inbox rule.Mailbox settingsmailbox_settings_getGETRead the mailbox’s settings: automatic replies (out of office), time zone, working hours, language, and date and time formats.mailbox_settings_updatePATCHChange some of the mailbox’s settings, leaving the rest: automatic replies, time zone, working hours, language, formats.supported_languagesGETList the languages and locales the user’s mailbox server supports.supported_time_zonesGETList the time zones the user’s mailbox server supports, as Windows or IANA names.inference_overrides_listGETList the senders whose mail is always sorted into Focused or Other.Focused Inboxinference_overrides_createPOSTAlways sort a sender’s future mail into Focused or Other.inference_overrides_updatePATCHSwitch an override between Focused and Other.inference_overrides_deleteDELETERemove an override, so the sender’s mail is sorted automatically again.Extensions and extended propertiesextensions_getGETRead an open extension — an app’s own named data — stored on a message.extensions_createPOSTStore an open extension on a message: an extensionName and any custom name-value pairs of primitive values or arrays of them.extensions_deleteDELETEDelete an open extension from a message.MailTips and IDsget_mail_tipsPOSTCheck recipients before sending: automatic replies, full mailboxes, delivery restrictions, moderation, external recipients and distribution-list sizes.translate_exchange_idsPOSTConvert up to 1,000 Outlook item IDs from one format to another — for example from restId to the immutable restImmutableEntryId, which survives a move.

One operation, one tool

Several paths. Graph lists messages at /me/messages and at /me/mailFolders/{id}/messages, and folders at the root and under a parent’s childFolders. Each is one tool: messages_list(folder_id=...), mail_folders_list(parent_folder_id=...), messages_create(folder_id=...), mail_folders_create(parent_folder_id=...). Leave the scope out for the wider route. The routes are declared on the tool, most specific first, and the runtime calls the first one the arguments fill — see routes. Two body formats. send_mail, messages_create and the reply, reply-all and forward operations take a JSON message or a MIME document. Fill message (and comment, toRecipients) for JSON, or mime with an EmailContent for MIME, which Format("mime_base64") builds and encodes in standard base64 — padded, with + and /, the alphabet Graph decodes. mime is Body(media_type="text/plain"): set, it is the whole body and the JSON arguments are refused beside it. Narrower tools are a projection. A deployment that wants an action-sized tool derives one:
outlook_mark_read.py
The model then sees messageId and isRead, and nothing else of the message.

What crosses the boundary

On the way out

A file attachment’s contentBytes takes the file’s text, and is base64-encoded on the way out. Its description says “base64-encoded contents”, which is Graph’s wire format; a Gloss tells the model not to encode it twice.

On the way back

Graph wraps every object in envelope keys and every recipient twice. The response handlers drop @odata.context, @odata.etag, changeKey and conversationIndex, and flatten each recipient to Name <address>. messages_list leaves each message’s full body out — bodyPreview carries the first 255 characters, and a selected uniqueBody stays, which is the way to read a thread: filter="conversationId eq '…'", select="from,receivedDateTime,uniqueBody". messages_get and messages_list ask Graph for bodies as plain text (Prefer: outlook.body-content-type="text"). A host that wants the HTML — to edit a draft in its own formatting — passes headers={"Prefer": 'outlook.body-content-type="html"'} to ainvoke, which applies last. An attachment is decoded only when it is text, and strictly. A binary file is reported by name, type and size, and attachments_get_content hands over its bytes.

Paging

Graph answers a long collection with @odata.nextLink, the whole URL of the next page, and Microsoft is explicit that the $skip or $skiptoken inside it is not for extracting. So the handlers return it as next_link, every paging tool takes it back under that name, and the tool sends the link’s own query string in place of the parameters it would otherwise build. The link also says which collection it is a page of — mailFolders('drafts') — and that is a path parameter, not part of the query. So the tool takes the folder from the link too: a next_link passed on its own stays in its folder, and one passed beside a different folder_id follows the link. The link’s path is matched against the tool’s own routes and one that matches none is refused, so a rewritten link can change which page of which folder, never which endpoint.
outlook_paging.py
messages_delta and mail_folders_delta end a round with delta_link instead. Keep it, and pass it as next_link later: the next round returns only what was created, updated or removed since. messages_delta sizes its pages with max_page_size, which is Graph’s Prefer: odata.maxpagesize header, and does not offer $top. Graph lists $top among the options delta supports, but on a delta round it ends the round: $top=2 on a folder of twelve messages returned two and a delta_link, the next round returned nothing, and the other ten were never delivered.

Gotchas

messages_delete (DELETE /me/messages/{id}) skips Deleted Items: the message lands in Recoverable Items (recoverableitemsdeletions), where Outlook keeps it for the retention period. A folder deleted with mail_folders_delete does not appear in Deleted Items either. To put either there, move it: messages_move or mail_folders_move to deleteditems. messages_permanent_delete calls Graph’s permanentDelete action, after which Outlook clients cannot reach the message, and a folder deleted with mail_folders_permanent_delete is removed outright, not kept in the purges folder.
Graph takes $select on mailbox settings, $filter on the category list, and $top and $filter on a message’s attachments without complaint, and returns everything anyway. Those tools do not offer them, because a model handed the whole collection reads it as the subset it asked for. The rule list takes $filter, but most rule properties refuse it with a 400 (isEnabled, sequence); with a handful of rules, list them all.
messages_move returns the message in its new folder, under a new id. An id saved before the move no longer finds it. translate_exchange_ids converts to restImmutableEntryId, which survives a move.
search takes "pizza" or "from:ada subject:invoice", quotes included, and the call is refused locally without them. It returns at most 1,000 matches, most recently sent first.
Graph refuses a $filter beside an $orderby unless every sorted property is filtered on, in the same order, ahead of the others — InefficientFilter. Newest first with a filter is orderby="receivedDateTime desc" and a filter that starts receivedDateTime ge 1900-01-01T00:00:00Z and ….
There is no endpoint for one. Write them through messages_update or mail_folders_update (singleValueExtendedProperties), and read them with expand="singleValueExtendedProperties($filter=id eq '…')" on messages_get or mail_folders_get.
attachments_create_upload_session returns the session’s uploadUrl. The file’s bytes go to that URL in PUTs of up to 4 MB, which a model has no bytes to make; the host does that part.