| name | msgraph-email |
| description | How to read, search, filter, send, reply, forward, and manage Outlook email via the Microsoft Graph CLI (mgc). Use this skill whenever the user wants to work with email in Microsoft 365 / Outlook — searching messages, checking unread mail, sending emails, downloading attachments, managing folders, or handling the Focused Inbox. |
Microsoft Graph CLI — Email
Binary
./mgc-cli/mgc
Use --user-id me in all commands to target the currently signed-in user.
Global flags (quick reference)
| Flag | Purpose |
|---|
--select <fields> | OData $select — comma-separated field names |
--filter <expr> | OData $filter |
--orderby <field> | OData $orderby |
--top <n> | Page size |
--all | Auto-paginate all results |
--search <term> | Full-text search |
--query <jmespath> | Client-side JMESPath filter on the JSON response |
--output JSON|TABLE|TEXT|RAW_JSON|NONE | Output format. RAW_JSON is compact and best for scripting |
--debug | Print full HTTP request/response |
Searching and filtering messages
Two complementary mechanisms exist:
--search — server-side full-text search (subject, body, sender, recipients). Fast, searches across all fields at once.
--filter — server-side OData filter on specific properties. Precise, composable, supports date comparisons.
--query — client-side JMESPath filter applied after the response. Use when OData doesn't support what you need.
Keyword search
mgc users messages list --user-id me --search "project kickoff"
mgc users messages list --user-id me --search '"Bestellung 4500711166"'
mgc users mail-folders messages list --user-id me \
--mail-folder-id Inbox --search "budget approval"
mgc users messages list --user-id me \
--search "invoice" \
--select "id,subject,from,receivedDateTime"
--search restrictions — cannot be combined with --filter or --orderby (the API rejects it). For filtering + sorting, use --filter with OData functions instead. KQL field-scoped syntax like subject: or from: does not work with this endpoint.
Date and time filters
mgc users messages list --user-id me \
--filter "receivedDateTime ge 2026-04-01T00:00:00Z" \
--select "id,subject,from,receivedDateTime" \
--orderby "receivedDateTime desc"
mgc users messages list --user-id me \
--filter "receivedDateTime ge 2026-03-01T00:00:00Z and receivedDateTime lt 2026-04-01T00:00:00Z" \
--select "id,subject,from,receivedDateTime"
TODAY=$(date -u +"%Y-%m-%dT00:00:00Z")
mgc users messages list --user-id me \
--filter "receivedDateTime ge $TODAY" \
--select "id,subject,from,receivedDateTime,isRead" \
--orderby "receivedDateTime desc"
Filter by sender, read status, importance, Focused Inbox
Every message has an inferenceClassification property (focused or other) reflecting the Outlook Focused Inbox classification — fully filterable via OData.
mgc users messages list --user-id me \
--filter "isRead eq false" \
--select "id,subject,from,receivedDateTime"
mgc users messages list --user-id me \
--filter "from/emailAddress/address eq 'alice@example.com'" \
--select "id,subject,receivedDateTime"
mgc users messages list --user-id me \
--filter "importance eq 'high'" \
--select "id,subject,from,receivedDateTime"
mgc users messages list --user-id me \
--filter "hasAttachments eq true" \
--select "id,subject,from,receivedDateTime"
mgc users messages list --user-id me \
--filter "inferenceClassification eq 'focused'" \
--select "id,subject,from,receivedDateTime,isRead" \
--orderby "receivedDateTime desc"
mgc users messages list --user-id me \
--filter "inferenceClassification eq 'other'" \
--select "id,subject,from,receivedDateTime,isRead" \
--orderby "receivedDateTime desc"
TODAY=$(date -u +"%Y-%m-%dT00:00:00Z")
mgc users messages list --user-id me \
--filter "inferenceClassification eq 'focused' and isRead eq false and receivedDateTime ge $TODAY" \
--select "id,subject,from,receivedDateTime"
Focused Inbox overrides
Classify all future messages from a given sender:
mgc users inference-classification overrides create --user-id me --body '{
"classifyAs": "focused",
"senderEmailAddress": { "name": "Alice", "address": "alice@example.com" }
}'
mgc users inference-classification overrides create --user-id me --body '{
"classifyAs": "other",
"senderEmailAddress": { "name": "Newsletter", "address": "news@example.com" }
}'
mgc users inference-classification overrides list --user-id me
mgc users inference-classification overrides patch --user-id me \
--inference-classification-override-id <id> \
--body '{ "classifyAs": "focused" }'
mgc users inference-classification overrides delete --user-id me \
--inference-classification-override-id <id>
Caveats:
- Overrides affect future incoming messages only — past messages are not reclassified retroactively
- There is no API to reclassify a single existing message
- Max 1,000 overrides per mailbox; keyed on sender SMTP address
- The Focused Inbox on/off toggle is not available via Graph API v1.0
Subject-line filters
mgc users messages list --user-id me \
--filter "contains(subject, 'invoice')" \
--select "id,subject,from,receivedDateTime"
mgc users messages list --user-id me \
--filter "startsWith(subject, 'RE:')" \
--select "id,subject,from,receivedDateTime"
mgc users messages list --user-id me \
--filter "contains(subject, 'Bestellung') and receivedDateTime ge 2026-03-01T00:00:00Z" \
--select "id,subject,from,receivedDateTime" \
--orderby "receivedDateTime desc"
For complex client-side filtering:
mgc users messages list --user-id me \
--select "id,subject,from,receivedDateTime" \
--query "value[?contains(subject, 'meeting')]"
Pagination
mgc users messages list --user-id me \
--select "id,subject,from,receivedDateTime" \
--orderby "receivedDateTime desc" --top 50
mgc users messages list --user-id me \
--filter "isRead eq false" \
--select "id,subject,from,receivedDateTime" \
--all
Searching in specific folders
mgc users mail-folders list --user-id me --select "id,displayName"
mgc users mail-folders messages list --user-id me \
--mail-folder-id SentItems --search "contract"
mgc users mail-folders messages list --user-id me \
--mail-folder-id Archive \
--filter "receivedDateTime ge 2026-01-01T00:00:00Z" \
--select "id,subject,from,receivedDateTime"
Well-known folder names: Inbox, Drafts, SentItems, DeletedItems, Archive, JunkEmail
Recent messages — quick overview
mgc users messages list --user-id me \
--select "subject,from,receivedDateTime,isRead" \
--orderby "receivedDateTime desc" \
--output TABLE --top 20
Get a message
mgc users messages get --user-id me --message-id <id> \
--select "id,subject,body,from,toRecipients,receivedDateTime"
Read a message as plain text using the bundled script (strips HTML, decodes entities):
python3 .claude/skills/msgraph-email/scripts/read_message.py <message-id>
Prints From, To, Date, Subject, and the plain-text body. Use this instead of piping RAW_JSON through an inline Python one-liner.
Read status: Fetching a message via Graph API does not mark it as read — unlike opening it in Outlook. To mark it read explicitly:
mgc users messages patch --user-id me --message-id <id> --body '{"isRead": true}'
Attachments
Two kinds of attachments exist: file attachments (isInline: false, flagged by hasAttachments: true) and inline images (isInline: true, embedded via cid: in the HTML body — these do NOT set hasAttachments).
List attachments on a message:
mgc users messages attachments list --user-id me --message-id <id> \
--select "id,name,contentType,size,isInline" --output RAW_JSON
Download all attachments (file + inline) using the bundled script:
python3 .claude/skills/msgraph-email/scripts/download_attachments.py <message-id> [output-dir]
Process the downloaded files:
- PDF/DOCX/XLSX → parse with liteparse:
lit parse /tmp/attachments/file.pdf --no-ocr
- Images (PNG/JPG) → read directly with the Read tool (supports visual inspection and OCR)
- For inline images, the
contentId in the script output maps to cid: references in the HTML body
Key gotchas:
hasAttachments: false does NOT mean "no images" — inline cid: images are invisible to that flag
contentBytes and contentId cannot be used in --select — always fetch the full attachment
Send email
mgc users send-mail post --user-id me --body '{
"message": {
"subject": "Hello",
"body": { "contentType": "HTML", "content": "<p>Body text.</p>" },
"toRecipients": [{ "emailAddress": { "address": "recipient@example.com" } }],
"ccRecipients": [{ "emailAddress": { "address": "cc@example.com" } }]
},
"saveToSentItems": true
}'
Send email with file attachment
python3 .claude/skills/msgraph-email/scripts/send_with_attachment.py \
recipient@example.com "Subject line" "<p>Body HTML.</p>" /tmp/report.pdf
python3 .claude/skills/msgraph-email/scripts/send_with_attachment.py \
recipient@example.com "Subject" "<p>Body.</p>" /tmp/report.pdf \
--cc cc1@example.com --cc cc2@example.com
Create draft then send
mgc users messages create --user-id me --body '{
"subject": "Draft subject",
"body": { "contentType": "Text", "content": "Body." },
"toRecipients": [{ "emailAddress": { "address": "someone@example.com" } }]
}'
mgc users messages send post --user-id me --message-id <draft-id>
Reply, reply-all, forward
mgc users messages reply post --user-id me --message-id <id> \
--body '{ "comment": "Thanks!" }'
mgc users messages reply-all post --user-id me --message-id <id> \
--body '{ "comment": "Reply to all." }'
mgc users messages forward post --user-id me --message-id <id> --body '{
"toRecipients": [{ "emailAddress": { "address": "fwd@example.com" } }],
"comment": "FYI"
}'
Move, copy, patch, delete
mgc users messages move post --user-id me --message-id <id> \
--body '{"destinationId": "Archive"}'
mgc users messages patch --user-id me --message-id <id> --body '{"isRead": true}'
mgc users messages delete --user-id me --message-id <id>
Capture created resource ID in a script
MSG_ID=$(mgc users messages create --user-id me --body '...' \
| python3 -c "import sys,json; print(json.load(sys.stdin)['id'])")
Troubleshooting
401 Unauthorized → missing scopes; run mgc logout then mgc login --scopes Mail.ReadWrite --scopes Mail.Send
400 Bad Request on filter + count → add --consistency-level eventual
--search syntax error with numbers/spaces → wrap in inner double quotes: --search '"exact phrase 123"'
--search + --filter or --orderby → not allowed; use --filter with contains() / startsWith() instead
--select with contentBytes or contentId → not supported on attachments; fetch without --select
- Use
--debug to inspect the raw HTTP request/response