agentsclimarketplace

Microsoft outlook

Skill AceDataCloud/Skills/skills/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 modifying / cancelling an event.From its SKILL.md

Install
npx -y skills add AceDataCloud/Skills --skill microsoft-outlook

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

2 things to look at

  • no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
  • 13 stars13 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

What its file declares

Copied from the file, not written here

The file declares its own license as Apache-2.0. That is the author’s claim about this one file, and it is not the same thing as the license GitHub reports for the repository, which is listed with the other numbers below.

SKILL.md

18.7 KB, ~5.2k tokens by cl100k_base, as published. Nobody here has run it

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).

WantAppend
Unread only&$filter=isRead eq false
With attachments&$filter=hasAttachments eq true
From a specific sender&$filter=from/emailAddress/address eq '[email protected]'
Date range&$filter=receivedDateTime ge 2026-04-01T00:00:00Z and receivedDateTime lt 2026-05-01T00:00:00Z
CombineUse 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

$search cannot be combined with $filter or $orderby in the same query — pick one. $search returns 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/sendMail directly — 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 "[email protected]" \
        '{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 "[email protected]" \
        '{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", "[email protected]", "[email protected]"],
        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    "[email protected]" \
  '{
    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

FieldMeaning
start.dateTime / end.dateTimeThe local wall-clock time.
start.timeZone / end.timeZoneIANA-ish name ("Pacific Standard Time", "China Standard Time", "UTC").
Prefer: outlook.timezone="..." request headerRe-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)

ParamMail exampleCalendar example
$selectid,subject,from,receivedDateTime,isReadsubject,start,end,location,attendees
$filterisRead eq falsestart/dateTime ge '2026-05-01T00:00:00'
$orderbyreceivedDateTime descstart/dateTime
$top10 browse, 25 search10 browse
$search"keyword" (mail only — cannot combine with $filter / $orderby)n/a
$expandattachmentsattendees, 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=10 for browse, 25 for search. Don't paginate past 50 unless asked.
  • HTML bodies only for mail. contentType: "Text" collapses whitespace.
  • Use calendarView for any agenda / "what's on my calendar" question. /events returns recurrence masters only.
  • Set Prefer: outlook.timezone on calendar read calls; otherwise dateTime comes back in UTC.
  • URL-encode message / event / attachment IDs if using them in a path — IDs can contain +, /, =. Use jq -sRr @uri.
  • Date math: date -u -v+1d works 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.

ActionPrepare stepShow user
Send mailPOST /me/messages (draft)subject, recipients, body preview
Reply / forwardcreateReply / createForwardquote snippet + your reply text
Delete mailfetch subject first"Delete '{subject}' from {sender}?"
Out-of-officeshow current setting firstnew schedule + message preview
Create eventbuild payloadsubject, time, attendees, online-meeting on/off
Update eventdiff with currentwhat's changing, attendee count being notified
Cancel eventfetch event firstsubject, time, attendee count
Accept / decline invitefetch event firstevent subject + organizer
Bulklist affectedcount + 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. trying Mail.Send without it granted, or Calendars.ReadWrite for create / cancel); ask user to reinstall and tick the write scope.
  • 429 TooManyRequests → respect Retry-After header.
  • 404 ErrorItemNotFound → wrong message / event id (or it was already deleted / cancelled).

Reference

What ships with it

Read from the repository

Just SKILL.md. No reference files, no scripts.

Keep looking

Skills are one crate of 325,949. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.