> ## Documentation Index
> Fetch the complete documentation index at: https://docs.r28.ai/charter/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> Charter works for any HTTP API, including the user's own. The packs are examples of the result, not the limit: an API with no pack is declared the same way, following https://raw.githubusercontent.com/r28ai/charter/main/docs/.mintlify/skills/declaring-charter-tools/SKILL.md.
> Charter is for agents that act in customers' accounts: every call is checked field by field before it's sent, the app says whose account each call is for, and the same declarations list what each tool can reach. https://docs.r28.ai/charter/llms.txt says what it is and lists every page.

# Outlook

> Read, search, send, reply to and file mail; drafts, attachments, folders, categories, rules and mailbox settings, over Microsoft Graph.

56 tools
OAuth

```python outlook_example.py {11} theme={null}
import asyncio

from charter.auth import EnvTokenProvider
from charter.packs import outlook

credentials = EnvTokenProvider("MICROSOFT_ACCESS_TOKEN")
outlook.configure(credentials)


async def main():
    unread = await outlook.messages_list.ainvoke(folder_id="inbox", filter="isRead eq false")


asyncio.run(main())
```

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](https://docs.r28.ai/charter/charter/auth/apps/microsoft.md), 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](https://developer.microsoft.com/graph/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()`](https://docs.r28.ai/charter/charter/reference/configuration#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`](https://docs.r28.ai/charter/charter/reference/credentials#envtokenprovider) re-reads the variable on every call, so a token rotated beside the process is picked up without a restart; [`StaticTokenProvider`](https://docs.r28.ai/charter/charter/reference/credentials#statictokenprovider) takes one you already hold as a string. Neither renews anything, so the calls stop when the token expires.

```python outlook_script.py {4} theme={null}
from charter.auth import EnvTokenProvider
from charter.packs import outlook

credentials = EnvTokenProvider("MICROSOFT_ACCESS_TOKEN")
outlook.configure(credentials)
```

### One account, refreshed

For an agent or a server acting as you. [`OAuth2Client`](https://docs.r28.ai/charter/charter/reference/oauth#oauth2client) turns a client registration and a stored refresh token into an access token, and [renews it](https://docs.r28.ai/charter/charter/auth/authorization-servers.md#what-the-cache-does-precisely) before it lapses.

```python outlook_agent.py {8} theme={null}
from charter.auth import OAuth2Client
from charter.packs import outlook

credentials = OAuth2Client(
    MICROSOFT,
    client_id=os.environ["MICROSOFT_CLIENT_ID"],
    client_secret=os.environ["MICROSOFT_CLIENT_SECRET"],
    refresh_token=os.environ["MICROSOFT_REFRESH_TOKEN"],
)
outlook.configure(credentials)
```

> [!NOTE]
>
> No refresh token yet? [Getting the grant](https://docs.r28.ai/charter/charter/auth/oauth-flow.md) is the one-time consent flow that hands you one.

### Many end users

For a product whose users each connect their own account. [`SubjectProvider`](https://docs.r28.ai/charter/charter/reference/credentials#subjectprovider) builds one credential per user through a factory you write, and [`use_subject`](https://docs.r28.ai/charter/charter/reference/credentials#use_subject) names the user a call acts for. [Your users' accounts](https://docs.r28.ai/charter/charter/auth/oauth-flow.md) is the consent route inside your app; [serving many users](https://docs.r28.ai/charter/charter/auth/authorization-servers.md#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](https://docs.r28.ai/charter/charter/auth/oauth-flow.md#more-than-one-process).

```python outlook_server.py {24,28} theme={null}
from functools import partial

from charter.auth import OAuth2Client, SubjectProvider, TokenGrant, use_subject
from charter.packs import outlook

# Yours to write: a user id in, that user's credential out.
async def for_user(user_id: str) -> OAuth2Client:
    async def stored_grant() -> TokenGrant:  # read again whenever the token is due
        row = await db.grants.get(user_id, "microsoft")
        return TokenGrant(
            access_token=row.access_token,
            refresh_token=row.refresh_token,
            expires_at=row.expires_at,
        )

    return OAuth2Client(
        MICROSOFT,
        client_id=os.environ["MICROSOFT_CLIENT_ID"],
        client_secret=os.environ["MICROSOFT_CLIENT_SECRET"],
        refresh_token=stored_grant,
        on_refresh=partial(save_to_db, user_id),
    )

credentials = SubjectProvider(for_user)
outlook.configure(credentials)

# Per request: whose grant the tools use.
with use_subject(request.user_id):
    ...  # your agent runs here
```

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`](https://docs.r28.ai/charter/charter/auth/authorization-servers) against
`https://login.microsoftonline.com/{tenant}/v2.0`. Ask for `offline_access`
beside the Graph scopes, or Microsoft issues no refresh token.

## The client

> [!NOTE]
>
> [`oauth_tool_factory`](https://docs.r28.ai/charter/charter/reference/factories#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.

```python Without the pack theme={null}
from charter import oauth_tool_factory
from charter.auth import ScopeRefusal

outlook_api_client = oauth_tool_factory(
    base_url="https://graph.microsoft.com/v1.0/",
    provider="microsoft",
    credential_provider=credentials,
    pack="outlook",
    body_format="json",
    query_format="repeat",
    body_case="camel",
    query_case="camel",
    path_case="snake",
    static_headers={
        "Prefer": "outlook.body-content-type=\"text\"",
    },
    # this API reports failure with an HTTP status code
    envelope=None,
    scope_refusals=[
        ScopeRefusal(
            status=403,
            signal=r"\"code\"\s*:\s*\"ErrorAccessDenied\"",
        ),
    ],
)
```

```python With the pack theme={null}
from charter.packs import outlook

outlook.configure(credentials)

# The base URL, the casing, the envelope and the pagination are
# already declared. 56 tools, ready to hand to a model:
tools = outlook.TOOLS
```

`credentials` is whichever of the three you built in [Authenticating](#authenticating). A pack takes it through [`configure()`](https://docs.r28.ai/charter/charter/reference/configuration#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:

```python outlook_pagination.py theme={null}
messages_list = outlook_api_client(
    name="messages_list",
    args_schema=MessagesListRequest,
    method="GET",
    url_template=(
        "me/mailFolders/{folder_id}/messages",
        "me/messages",
    ),
    pagination=Pagination(
        cursor_field="next_link",
        cursor_param="next_link",
    ),
)
```

> [!NOTE]
>
> 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`](https://docs.r28.ai/charter/charter/reference/tool), called with
[`ainvoke`](https://docs.r28.ai/charter/charter/reference/tool#tool-ainvoke) as in the snippet above. The name
links to its parameters, its response and what it costs.

Messages

[messages\_listGET
List or search messages, across the mailbox or in one folder (an ID, or a well-known name such as inbox or sentitems).](https://docs.r28.ai/charter/charter/packs/outlook/messages/messages_list.md)

[messages\_getGET
Read one message: sender, recipients, subject, date and the body as plain text.](https://docs.r28.ai/charter/charter/packs/outlook/messages/messages_get.md)

[messages\_get\_contentGET
Get a message's MIME content as text: every header, and each body part as encoded on the wire.](https://docs.r28.ai/charter/charter/packs/outlook/messages/messages_get_content.md)

[messages\_deltaGET
Track changes to the messages in one folder.](https://docs.r28.ai/charter/charter/packs/outlook/messages/messages_delta.md)

[messages\_createPOST
Save a new draft, in Drafts unless folder\_id names another folder, as a JSON message or as MIME. messages\_send sends it later.](https://docs.r28.ai/charter/charter/packs/outlook/messages/messages_create.md)

[messages\_updatePATCH
Change some of a message's properties, leaving the rest: isRead, flag, importance, categories, and on a draft its recipients, subject and body.](https://docs.r28.ai/charter/charter/packs/outlook/messages/messages_update.md)

[messages\_deleteDELETE
Delete a message or a draft.](https://docs.r28.ai/charter/charter/packs/outlook/messages/messages_delete.md)

[messages\_permanent\_deletePOST
Permanently delete a message.](https://docs.r28.ai/charter/charter/packs/outlook/messages/messages_permanent_delete.md)

[messages\_movePOST
Move a message to another folder — archive, deleteditems, or any folder ID.](https://docs.r28.ai/charter/charter/packs/outlook/messages/messages_move.md)

[messages\_copyPOST
Copy a message into another folder, by folder ID or well-known name.](https://docs.r28.ai/charter/charter/packs/outlook/messages/messages_copy.md)

[messages\_sendPOST
Send a saved draft — a new message, reply, reply-all or forward draft.](https://docs.r28.ai/charter/charter/packs/outlook/messages/messages_send.md)

[messages\_replyPOST
Reply to the sender of a message right away.](https://docs.r28.ai/charter/charter/packs/outlook/messages/messages_reply.md)

[messages\_reply\_allPOST
Reply to the sender and every recipient of a message right away, as JSON or MIME.](https://docs.r28.ai/charter/charter/packs/outlook/messages/messages_reply_all.md)

[messages\_forwardPOST
Forward a message right away.](https://docs.r28.ai/charter/charter/packs/outlook/messages/messages_forward.md)

[messages\_create\_replyPOST
Create a draft reply to the sender of a message, to edit with messages\_update and send with messages\_send.](https://docs.r28.ai/charter/charter/packs/outlook/messages/messages_create_reply.md)

[messages\_create\_reply\_allPOST
Create a draft reply to the sender and every recipient of a message.](https://docs.r28.ai/charter/charter/packs/outlook/messages/messages_create_reply_all.md)

[messages\_create\_forwardPOST
Create a draft forward of a message, to edit and send later.](https://docs.r28.ai/charter/charter/packs/outlook/messages/messages_create_forward.md)

[send\_mailPOST
Send a new email right away, as a JSON message or as MIME.](https://docs.r28.ai/charter/charter/packs/outlook/messages/send_mail.md)

Attachments

[attachments\_listGET
List a message's attachments: name, type, size and id, without the contents.](https://docs.r28.ai/charter/charter/packs/outlook/attachments/attachments_list.md)

[attachments\_getGET
Read one attachment.](https://docs.r28.ai/charter/charter/packs/outlook/attachments/attachments_get.md)

[attachments\_get\_contentGET
Get an attachment's raw contents: a file's bytes (as text when they are text, otherwise base64), or an attached message's MIME.](https://docs.r28.ai/charter/charter/packs/outlook/attachments/attachments_get_content.md)

[attachments\_createPOST
Attach a file (under 3 MB) or a message to a draft.](https://docs.r28.ai/charter/charter/packs/outlook/attachments/attachments_create.md)

[attachments\_create\_upload\_sessionPOST
Start 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.](https://docs.r28.ai/charter/charter/packs/outlook/attachments/attachments_create_upload_session.md)

[attachments\_deleteDELETE
Delete an attachment from a message or a draft.](https://docs.r28.ai/charter/charter/packs/outlook/attachments/attachments_delete.md)

Folders

[mail\_folders\_listGET
List the folders under the mailbox root, or under parent\_folder\_id, with their unread and total counts.](https://docs.r28.ai/charter/charter/packs/outlook/folders/mail_folders_list.md)

[mail\_folders\_getGET
Read one folder, by ID or well-known name, with its unread and total counts.](https://docs.r28.ai/charter/charter/packs/outlook/folders/mail_folders_get.md)

[mail\_folders\_createPOST
Create a folder under the root or under parent\_folder\_id, or a search folder — a saved filter over folders you name.](https://docs.r28.ai/charter/charter/packs/outlook/folders/mail_folders_create.md)

[mail\_folders\_updatePATCH
Rename a folder or set its extended properties, or change a search folder's source folders or filter.](https://docs.r28.ai/charter/charter/packs/outlook/folders/mail_folders_update.md)

[mail\_folders\_deleteDELETE
Delete a folder, or a search folder, and what is in it.](https://docs.r28.ai/charter/charter/packs/outlook/folders/mail_folders_delete.md)

[mail\_folders\_permanent\_deletePOST
Permanently delete a folder and remove its items from the mailbox.](https://docs.r28.ai/charter/charter/packs/outlook/folders/mail_folders_permanent_delete.md)

[mail\_folders\_movePOST
Move a folder and everything in it into another folder.](https://docs.r28.ai/charter/charter/packs/outlook/folders/mail_folders_move.md)

[mail\_folders\_copyPOST
Copy a folder and everything in it into another folder.](https://docs.r28.ai/charter/charter/packs/outlook/folders/mail_folders_copy.md)

[mail\_folders\_deltaGET
Track changes to the mailbox's folders.](https://docs.r28.ai/charter/charter/packs/outlook/folders/mail_folders_delta.md)

Categories

[master\_categories\_listGET
List the categories defined for the mailbox, with their colors.](https://docs.r28.ai/charter/charter/packs/outlook/categories/master_categories_list.md)

[master\_categories\_getGET
Read one category.](https://docs.r28.ai/charter/charter/packs/outlook/categories/master_categories_get.md)

[master\_categories\_createPOST
Create a category.](https://docs.r28.ai/charter/charter/packs/outlook/categories/master_categories_create.md)

[master\_categories\_updatePATCH
Change a category's color.](https://docs.r28.ai/charter/charter/packs/outlook/categories/master_categories_update.md)

[master\_categories\_deleteDELETE
Delete a category from the mailbox's list.](https://docs.r28.ai/charter/charter/packs/outlook/categories/master_categories_delete.md)

Inbox rules

[message\_rules\_listGET
List the Inbox rules, in the order they run.](https://docs.r28.ai/charter/charter/packs/outlook/rules/message_rules_list.md)

[message\_rules\_getGET
Read one Inbox rule: its conditions, exceptions and actions.](https://docs.r28.ai/charter/charter/packs/outlook/rules/message_rules_get.md)

[message\_rules\_createPOST
Create an Inbox rule: when an incoming message meets the conditions, and none of the exceptions, Outlook takes the actions.](https://docs.r28.ai/charter/charter/packs/outlook/rules/message_rules_create.md)

[message\_rules\_updatePATCH
Change some of an Inbox rule's properties, leaving the rest as they are.](https://docs.r28.ai/charter/charter/packs/outlook/rules/message_rules_update.md)

[message\_rules\_deleteDELETE
Delete an Inbox rule.](https://docs.r28.ai/charter/charter/packs/outlook/rules/message_rules_delete.md)

Mailbox settings

[mailbox\_settings\_getGET
Read the mailbox's settings: automatic replies (out of office), time zone, working hours, language, and date and time formats.](https://docs.r28.ai/charter/charter/packs/outlook/settings/mailbox_settings_get.md)

[mailbox\_settings\_updatePATCH
Change some of the mailbox's settings, leaving the rest: automatic replies, time zone, working hours, language, formats.](https://docs.r28.ai/charter/charter/packs/outlook/settings/mailbox_settings_update.md)

[supported\_languagesGET
List the languages and locales the user's mailbox server supports.](https://docs.r28.ai/charter/charter/packs/outlook/settings/supported_languages.md)

[supported\_time\_zonesGET
List the time zones the user's mailbox server supports, as Windows or IANA names.](https://docs.r28.ai/charter/charter/packs/outlook/settings/supported_time_zones.md)

[inference\_overrides\_listGET
List the senders whose mail is always sorted into Focused or Other.](https://docs.r28.ai/charter/charter/packs/outlook/settings/inference_overrides_list.md)

Focused Inbox

[inference\_overrides\_createPOST
Always sort a sender's future mail into Focused or Other.](https://docs.r28.ai/charter/charter/packs/outlook/focused_inbox/inference_overrides_create.md)

[inference\_overrides\_updatePATCH
Switch an override between Focused and Other.](https://docs.r28.ai/charter/charter/packs/outlook/focused_inbox/inference_overrides_update.md)

[inference\_overrides\_deleteDELETE
Remove an override, so the sender's mail is sorted automatically again.](https://docs.r28.ai/charter/charter/packs/outlook/focused_inbox/inference_overrides_delete.md)

Extensions and extended properties

[extensions\_getGET
Read an open extension — an app's own named data — stored on a message.](https://docs.r28.ai/charter/charter/packs/outlook/extensions/extensions_get.md)

[extensions\_createPOST
Store an open extension on a message: an extensionName and any custom name-value pairs of primitive values or arrays of them.](https://docs.r28.ai/charter/charter/packs/outlook/extensions/extensions_create.md)

[extensions\_deleteDELETE
Delete an open extension from a message.](https://docs.r28.ai/charter/charter/packs/outlook/extensions/extensions_delete.md)

MailTips and IDs

[get\_mail\_tipsPOST
Check recipients before sending: automatic replies, full mailboxes, delivery restrictions, moderation, external recipients and distribution-list sizes.](https://docs.r28.ai/charter/charter/packs/outlook/other/get_mail_tips.md)

[translate\_exchange\_idsPOST
Convert up to 1,000 Outlook item IDs from one format to another — for example from restId to the immutable restImmutableEntryId, which survives a move.](https://docs.r28.ai/charter/charter/packs/outlook/other/translate_exchange_ids.md)

## 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](https://docs.r28.ai/charter/charter/tools/wire-contract.md#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`](https://docs.r28.ai/charter/charter/reference/semantic-types#emailcontent) for MIME, which
[`Format("mime_base64")`](https://docs.r28.ai/charter/charter/reference/transforms#built-in-transforms) builds and
encodes in standard base64 — padded, with `+` and `/`, the alphabet Graph decodes.
`mime` is [`Body(media_type="text/plain")`](https://docs.r28.ai/charter/charter/tools/wire-contract#a-body-in-another-media-type):
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:

```python outlook_mark_read.py theme={null}
mark_read = outlook.messages_update.derived(
    name="mark_read", keep={"message_id", "message.is_read"}
)
```

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`](https://docs.r28.ai/charter/charter/reference/markers#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.

```python outlook_paging.py theme={null}
args = {"folder_id": "inbox", "top": 50}
page = await outlook.messages_list.ainvoke(args)
while (args := outlook.messages_list.pagination.next_page_args(page, args)) is not None:
    page = await outlook.messages_list.ainvoke(args)
```

`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

> **Delete does not go to Deleted Items**
>
> `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.

> **Some query options are accepted and ignored**
>
> 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.

> **A moved message has a new id**
>
> `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 wants its query in double quotes**
>
> `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.

> **Filtering and sorting at once has three rules**
>
> 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 …`.

> **Extended properties live on the item**
>
> 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`.

> **A large attachment is two steps, and the second is yours**
>
> `attachments_create_upload_session` returns the session's `uploadUrl`. The
> file's bytes go to that URL in `PUT`s of up to 4 MB, which a model has no bytes
> to make; the host does that part.

## Related

* [Microsoft Graph mail API overview](https://learn.microsoft.com/en-us/graph/api/resources/mail-api-overview?view=graph-rest-1.0)
* [Throttling limits for Outlook](https://learn.microsoft.com/en-us/graph/throttling-limits#outlook-service-limits)
* [Gmail](https://docs.r28.ai/charter/charter/packs/gmail.md), the same job against Google's API


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.