microsoft-outlook
Read, search, draft, send and manage Outlook / Microsoft 365 email AND calendar events via Microsoft Graph. Use when the user mentions Outlook (mail or calendar), Microsoft 365 inbox, sending mail, replying / forwarding, today's agenda, scheduling a meeting, finding free time, or
What it does
Drive Microsoft Graph for Outlook / Microsoft 365 — both mail and
calendar — via curl + jq. The user's OAuth bearer token is in
$MICROSOFT_OUTLOOK_TOKEN; every call needs it as
Authorization: Bearer $MICROSOFT_OUTLOOK_TOKEN. The token already
carries the scopes the user agreed to at install: any of Mail.Read,
Mail.ReadWrite, Mail.Send, MailboxSettings.Read,
MailboxSettings.ReadWrite, Calendars.Read, Calendars.ReadWrite,
plus *.Shared variants. Mail and calendar are unified into one
connector (and one OAuth grant) because Microsoft Graph treats them as
sibling features of the same mailbox — there is no value in splitting
them at the skill layer.
The Graph API returns JSON; failures surface as
{"error": {"code": "...", "message": "..."}} — show that error
verbatim to the user.
Always start with /me to confirm the connection works AND learn
which mailbox you're operating against. For calendar work, also fetch
mailboxSettings.timeZone so dates render right.
Mail — Recipes
Verify auth (always run first)
curl -sS -H "Authorization: Bearer $MICROSOFT_OUTLOOK_TOKEN" \
https://graph.microsoft.com/v1.0/me \
| jq '{displayName, mail, userPrincipalName}'
List recent messages
curl -sS -H "Authorization: Bearer $MICROSOFT_OUTLOOK_TOKEN" \
"https://graph.microsoft.com/v1.0/me/messages?\$top=10&\$select=id,subject,from,receivedDateTime,isRead,hasAttachments&\$orderby=receivedDateTime desc" \
| jq '.value[] | {subject, from: .from.emailAddress.address, received: .receivedDateTime, unread: (.isRead | not)}'
Filters: append to URL with & (URL-encode the spaces).
| Want | Append |
|---|---|
| Unread only | &$filter=isRead eq false |
| With attachments | &$filter=hasAttachments eq true |
| From a specific sender | &$filter=from/emailAddress/address eq 'user@example.com' |
| Date range | &$filter=receivedDateTime ge 2026-04-01T00:00:00Z and receivedDateTime lt 2026-05-01T00:00:00Z |
| Combine | Use and / or between filter clauses |
Search messages (full-text on subject + body)
curl -sS -H "Authorization: Bearer $MICROSOFT_OUTLOOK_TOKEN" \
--data-urlencode '$search="quarterly report"' \
--data-urlencode '$top=10' \
--data-urlencode '$select=id,subject,from,receivedDateTime' \
--get https://graph.microsoft.com/v1.0/me/messages
$searchcannot be combined with$filteror$orderbyin the same query — pick one.$searchreturns relevance-ranked results.
Read a message body
curl -sS -H "Authorization: Bearer $MICROSOFT_OUTLOOK_TOKEN" \
"https://graph.microsoft.com/v1.0/me/messages/${MSG_ID}?\$select=subject,body,from,toRecipients,receivedDateTime" \
| jq '{subject, from: .from.emailAddress.address, received: .receivedDateTime, body: .body.content}'
body.contentType is usually "HTML". Use jq -r .body.content if
you want the raw HTML.
Send an email
⚠️ ALWAYS use draft → confirm → send. NEVER call
/me/sendMaildirectly — it sends immediately with no undo.
# Step 1: create draft
DRAFT=$(curl -sS -X POST \
-H "Authorization: Bearer $MICROSOFT_OUTLOOK_TOKEN" \
-H "Content-Type: application/json" \
-d "$(jq -nc \
--arg subj "Project update" \
--arg body "<p>Wanted to share the latest numbers.</p>" \
--arg to "alice@example.com" \
'{subject:$subj, body:{contentType:"HTML", content:$body}, toRecipients:[{emailAddress:{address:$to}}]}')" \
https://graph.microsoft.com/v1.0/me/messages)
DRAFT_ID=$(echo "$DRAFT" | jq -r .id)
# Step 2: present the draft to the user — subject, recipients, body preview
echo "$DRAFT" | jq '{subject, to: .toRecipients[0].emailAddress.address, body: .body.content}'
# Step 3: ONLY after user confirms — send (returns 202 No Content)
curl -sS -X POST -H "Authorization: Bearer $MICROSOFT_OUTLOOK_TOKEN" \
"https://graph.microsoft.com/v1.0/me/messages/${DRAFT_ID}/send" \
-w "HTTP %{http_code}\n"
CC / BCC: include ccRecipients / bccRecipients arrays in the same
shape as toRecipients.
Reply / reply-all / forward
⚠️ Show the user your draft text + recipients before sending.
# Quick reply (sends immediately on /reply — for explicit user-confirmed flow)
curl -sS -X POST \
-H "Authorization: Bearer $MICROSOFT_OUTLOOK_TOKEN" \
-H "Content-Type: application/json" \
-d '{"comment":"Thanks for the update!"}' \
"https://graph.microsoft.com/v1.0/me/messages/${MSG_ID}/reply"
# Or: createReply → review → /send (preferred for non-trivial replies)
DRAFT=$(curl -sS -X POST -H "Authorization: Bearer $MICROSOFT_OUTLOOK_TOKEN" \
"https://graph.microsoft.com/v1.0/me/messages/${MSG_ID}/createReply")
DRAFT_ID=$(echo "$DRAFT" | jq -r .id)
# PATCH body if needed, then /send
# Forward
curl -sS -X POST \
-H "Authorization: Bearer $MICROSOFT_OUTLOOK_TOKEN" \
-H "Content-Type: application/json" \
-d "$(jq -nc --arg to "bob@example.com" \
'{comment:"FYI", toRecipients:[{emailAddress:{address:$to}}]}')" \
"https://graph.microsoft.com/v1.0/me/messages/${MSG_ID}/forward"
Mark read / unread
curl -sS -X PATCH \
-H "Authorization: Bearer $MICROSOFT_OUTLOOK_TOKEN" \
-H "Content-Type: application/json" \
-d '{"isRead": true}' \
"https://graph.microsoft.com/v1.0/me/messages/${MSG_ID}"
List folders + read a specific folder
# Well-known folder names: Inbox, Drafts, SentItems, DeletedItems, Archive, JunkEmail
curl -sS -H "Authorization: Bearer $MICROSOFT_OUTLOOK_TOKEN" \
"https://graph.microsoft.com/v1.0/me/mailFolders('SentItems')/messages?\$top=5&\$select=subject,toRecipients,sentDateTime" \
| jq '.value[] | {subject, sent: .sentDateTime}'
List + download attachments
# Metadata
curl -sS -H "Authorization: Bearer $MICROSOFT_OUTLOOK_TOKEN" \
"https://graph.microsoft.com/v1.0/me/messages/${MSG_ID}/attachments?\$select=id,name,size,contentType" \
| jq '.value[] | {id, name, size}'
# Download a single attachment
curl -sS -H "Authorization: Bearer $MICROSOFT_OUTLOOK_TOKEN" \
"https://graph.microsoft.com/v1.0/me/messages/${MSG_ID}/attachments/${ATT_ID}/\$value" \
-o "$SKILL_DIR/tmp/attachment.bin"
Mailbox settings (timezone, signature, automatic replies)
curl -sS -H "Authorization: Bearer $MICROSOFT_OUTLOOK_TOKEN" \
"https://graph.microsoft.com/v1.0/me/mailboxSettings"
Set an out-of-office reply:
⚠️ Confirm with user before changing — auto-reply will fire on every incoming mail.
curl -sS -X PATCH \
-H "Authorization: Bearer $MICROSOFT_OUTLOOK_TOKEN" \
-H "Content-Type: application/json" \
-d '{"automaticRepliesSetting":{
"status":"scheduled",
"scheduledStartDateTime":{"dateTime":"2026-05-10T09:00:00","timeZone":"China Standard Time"},
"scheduledEndDateTime":{"dateTime":"2026-05-15T18:00:00","timeZone":"China Standard Time"},
"internalReplyMessage":"<p>I'm out this week, back Monday.</p>"}}' \
"https://graph.microsoft.com/v1.0/me/mailboxSettings"
Requires MailboxSettings.ReadWrite scope.
Delete a message
⚠️ Always fetch the subject first and confirm with the user.
# 1) show what's about to be deleted
curl -sS -H "Authorization: Bearer $MICROSOFT_OUTLOOK_TOKEN" \
"https://graph.microsoft.com/v1.0/me/messages/${MSG_ID}?\$select=subject,from,receivedDateTime" \
| jq '"Delete \"\(.subject)\" from \(.from.emailAddress.address) (\(.receivedDateTime))?"'
# 2) after user confirms (moves to Deleted Items, returns 204)
curl -sS -X DELETE -H "Authorization: Bearer $MICROSOFT_OUTLOOK_TOKEN" \
"https://graph.microsoft.com/v1.0/me/messages/${MSG_ID}" \
-w "HTTP %{http_code}\n"
Calendar — Recipes
Get user timezone (run once at start of any calendar work)
curl -sS -H "Authorization: Bearer $MICROSOFT_OUTLOOK_TOKEN" \
"https://graph.microsoft.com/v1.0/me/mailboxSettings" \
| jq '.timeZone'
# → e.g. "China Standard Time"
Pass that timezone in the Prefer: outlook.timezone header on every
calendar call so start.dateTime / end.dateTime come back rendered
in the user's local time:
TZ_HEADER='Prefer: outlook.timezone="China Standard Time"'
Today's agenda (calendarView)
calendarView expands recurring series into individual occurrences
within the window — exactly what you want for an agenda. Plain
/events returns only the recurrence master.
START=$(date -u +'%Y-%m-%dT00:00:00Z')
END=$(date -u -v+1d +'%Y-%m-%dT00:00:00Z') # macOS; use -d on Linux
curl -sS \
-H "Authorization: Bearer $MICROSOFT_OUTLOOK_TOKEN" \
-H "Prefer: outlook.timezone=\"China Standard Time\"" \
--data-urlencode "startDateTime=$START" \
--data-urlencode "endDateTime=$END" \
--data-urlencode '$select=id,subject,start,end,location,attendees,onlineMeeting,isCancelled' \
--data-urlencode '$orderby=start/dateTime' \
--get https://graph.microsoft.com/v1.0/me/calendarView \
| jq '.value[] | {subject, start: .start.dateTime, end: .end.dateTime, location: .location.displayName, attendees: [.attendees[].emailAddress.address]}'
This week's events (Mon–Sun)
START=$(date -u -v-Mon +'%Y-%m-%dT00:00:00Z' 2>/dev/null || date -u -d 'last monday' +'%Y-%m-%dT00:00:00Z')
END=$(date -u -v+7d -v-Mon +'%Y-%m-%dT00:00:00Z' 2>/dev/null || date -u -d 'next monday' +'%Y-%m-%dT00:00:00Z')
curl -sS -H "Authorization: Bearer $MICROSOFT_OUTLOOK_TOKEN" \
-H "Prefer: outlook.timezone=\"China Standard Time\"" \
--data-urlencode "startDateTime=$START" \
--data-urlencode "endDateTime=$END" \
--data-urlencode '$select=subject,start,end' \
--data-urlencode '$orderby=start/dateTime' \
--get https://graph.microsoft.com/v1.0/me/calendarView
Find free / busy slots (getSchedule)
Best way to find a slot that works for multiple people. Returns 30-minute buckets of free / busy / tentative across the requested window.
curl -sS -X POST \
-H "Authorization: Bearer $MICROSOFT_OUTLOOK_TOKEN" \
-H "Content-Type: application/json" \
-d "$(jq -nc '{
schedules: ["me", "alice@example.com", "bob@example.com"],
startTime: {dateTime: "2026-05-05T09:00:00", timeZone: "China Standard Time"},
endTime: {dateTime: "2026-05-05T18:00:00", timeZone: "China Standard Time"},
availabilityViewInterval: 30
}')" \
https://graph.microsoft.com/v1.0/me/calendar/getSchedule \
| jq '.value[] | {who: .scheduleId, view: .availabilityView}'
# availabilityView is a string of digits: 0=free 1=tentative 2=busy 3=oof 4=workingElsewhere
Read a single event (incl. attendees + body)
curl -sS -H "Authorization: Bearer $MICROSOFT_OUTLOOK_TOKEN" \
-H "Prefer: outlook.timezone=\"China Standard Time\"" \
"https://graph.microsoft.com/v1.0/me/events/${EVENT_ID}?\$select=subject,start,end,location,attendees,body,organizer,onlineMeeting" \
| jq '{subject, start: .start.dateTime, attendees: [.attendees[] | {addr: .emailAddress.address, response: .status.response}], body: .body.content}'
Create an event
⚠️ ALWAYS show subject / time / attendees to the user before creating — invitations fire automatically the moment the event is POSTed.
PAYLOAD=$(jq -nc \
--arg subj "Project sync" \
--arg body "<p>Quarterly review.</p>" \
--arg start "2026-05-06T10:00:00" \
--arg end "2026-05-06T10:30:00" \
--arg tz "China Standard Time" \
--arg loc "Meeting room 3F" \
--arg a1 "alice@example.com" \
'{
subject: $subj,
body: {contentType:"HTML", content:$body},
start: {dateTime:$start, timeZone:$tz},
end: {dateTime:$end, timeZone:$tz},
location:{displayName:$loc},
attendees:[{emailAddress:{address:$a1}, type:"required"}],
isOnlineMeeting: true,
onlineMeetingProvider: "teamsForBusiness"
}')
curl -sS -X POST \
-H "Authorization: Bearer $MICROSOFT_OUTLOOK_TOKEN" \
-H "Content-Type: application/json" \
-d "$PAYLOAD" \
https://graph.microsoft.com/v1.0/me/events \
| jq '{id, subject, start: .start.dateTime, joinUrl: .onlineMeeting.joinUrl}'
isOnlineMeeting: true + onlineMeetingProvider: "teamsForBusiness"
auto-generates a Teams meeting link. Drop both for an in-person event.
Update / reschedule (PATCH)
⚠️ Updating sends an "Updated" notice to all attendees. Confirm first.
curl -sS -X PATCH \
-H "Authorization: Bearer $MICROSOFT_OUTLOOK_TOKEN" \
-H "Content-Type: application/json" \
-d "$(jq -nc \
--arg start "2026-05-06T14:00:00" \
--arg end "2026-05-06T14:30:00" \
--arg tz "China Standard Time" \
'{start:{dateTime:$start, timeZone:$tz}, end:{dateTime:$end, timeZone:$tz}}')" \
"https://graph.microsoft.com/v1.0/me/events/${EVENT_ID}"
Cancel a meeting (sends cancellation notice)
⚠️ Confirm with the user — every attendee is notified.
curl -sS -X POST \
-H "Authorization: Bearer $MICROSOFT_OUTLOOK_TOKEN" \
-H "Content-Type: application/json" \
-d '{"comment":"Need to reschedule, sorry."}' \
"https://graph.microsoft.com/v1.0/me/events/${EVENT_ID}/cancel" \
-w "HTTP %{http_code}\n"
Accept / decline / tentative an incoming invite
curl -sS -X POST \
-H "Authorization: Bearer $MICROSOFT_OUTLOOK_TOKEN" \
-H "Content-Type: application/json" \
-d '{"comment":"See you there", "sendResponse":true}' \
"https://graph.microsoft.com/v1.0/me/events/${EVENT_ID}/accept"
# Or /decline, /tentativelyAccept
Read a shared calendar
Requires Calendars.Read.Shared.
curl -sS -H "Authorization: Bearer $MICROSOFT_OUTLOOK_TOKEN" \
"https://graph.microsoft.com/v1.0/users/${USER_UPN}/calendarView?startDateTime=${START}&endDateTime=${END}&\$select=subject,start,end" \
-G
Working with timezones
| Field | Meaning |
|---|---|
start.dateTime / end.dateTime | The local wall-clock time. |
start.timeZone / end.timeZone | IANA-ish name ("Pacific Standard Time", "China Standard Time", "UTC"). |
Prefer: outlook.timezone="..." request header | Re-renders all returned dateTime values into this zone. |
Always set Prefer: outlook.timezone on read calls so the JSON arrives
in the user's expected timezone instead of UTC.
Recurrence
Use calendarView (it expands occurrences for you) — not ?$expand=.
To create a recurring event, include recurrence:
{
"recurrence": {
"pattern": {"type":"weekly", "interval":1, "daysOfWeek":["monday","wednesday"]},
"range": {"type":"endDate", "startDate":"2026-05-06", "endDate":"2026-08-06"}
}
}
To modify a single occurrence of a series, PATCH that occurrence's id
(returned by calendarView), NOT the series master.
OData quick reference (mail + calendar)
| Param | Mail example | Calendar example |
|---|---|---|
$select | id,subject,from,receivedDateTime,isRead | subject,start,end,location,attendees |
$filter | isRead eq false | start/dateTime ge '2026-05-01T00:00:00' |
$orderby | receivedDateTime desc | start/dateTime |
$top | 10 browse, 25 search | 10 browse |
$search | "keyword" (mail only — cannot combine with $filter / $orderby) | n/a |
$expand | attachments | attendees, attachments |
Use --data-urlencode "$key=$value" --get with curl to avoid
shell-quoting $ and spaces.
Rules
- Always pass
$select— defaults return 30+ fields per item. $top=10for browse,25for search. Don't paginate past 50 unless asked.- HTML bodies only for mail.
contentType: "Text"collapses whitespace. - Use
calendarViewfor any agenda / "what's on my calendar" question./eventsreturns recurrence masters only. - Set
Prefer: outlook.timezoneon calendar read calls; otherwisedateTimecomes back in UTC. - URL-encode message / event / attachment IDs if using them in a
path — IDs can contain
+,/,=. Usejq -sRr @uri. - Date math:
date -u -v+1dworks on macOS,date -u -d 'tomorrow'on Linux.
CRITICAL: User consent for destructive / notifying actions
Sent emails cannot be unsent. Calendar writes fan out emails to attendees. Deleted messages may be permanently lost. Pattern: prepare → present → execute.
| Action | Prepare step | Show user |
|---|---|---|
| Send mail | POST /me/messages (draft) | subject, recipients, body preview |
| Reply / forward | createReply / createForward | quote snippet + your reply text |
| Delete mail | fetch subject first | "Delete '{subject}' from {sender}?" |
| Out-of-office | show current setting first | new schedule + message preview |
| Create event | build payload | subject, time, attendees, online-meeting on/off |
| Update event | diff with current | what's changing, attendee count being notified |
| Cancel event | fetch event first | subject, time, attendee count |
| Accept / decline invite | fetch event first | event subject + organizer |
| Bulk | list affected | count + sample |
Never call /me/sendMail — it sends immediately with no undo. Always draft → confirm → /send.
Errors
401 InvalidAuthenticationToken→ token expired; user must reinstall the connector.403 ErrorAccessDenied→ write scope missing (e.g. tryingMail.Sendwithout it granted, orCalendars.ReadWritefor create / cancel); ask user to reinstall and tick the write scope.429 TooManyRequests→ respectRetry-Afterheader.404 ErrorItemNotFound→ wrong message / event id (or it was already deleted / cancelled).
Reference
- Mail API: https://learn.microsoft.com/en-us/graph/api/resources/mail-api-overview
- Message resource: https://learn.microsoft.com/en-us/graph/api/resources/message
- Calendar resource: https://learn.microsoft.com/en-us/graph/api/resources/calendar
- Event resource: https://learn.microsoft.com/en-us/graph/api/resources/event
- calendarView: https://learn.microsoft.com/en-us/graph/api/calendar-list-calendarview
- getSchedule: https://learn.microsoft.com/en-us/graph/api/calendar-getschedule
- MailboxSettings: https://learn.microsoft.com/en-us/graph/api/resources/mailboxsettings
Capabilities
Install
Quality
deterministic score 0.45 from registry signals: · indexed on github topic:agent-skills · 7 github stars · SKILL.md body (18,148 chars)