| name | outlook-cli |
| description | CLI skill for Outlook 365 to read, send, search, and manage emails, calendar events, categories, and contacts from the terminal without API keys or admin consent |
| author | yusufaltunbicak |
| version | 0.1.5 |
| tags | ["outlook","email","office365","calendar","events","meetings","categories","attachments","terminal","cli"] |
outlook-cli Skill
Use this skill when the user wants to read, send, search, or manage Outlook 365 emails and calendar events from the terminal. Also supports file attachments, follow-up flags, message pinning, opening items in browser, recurring events, shared calendars, free/busy scheduling, categories, contacts, and signatures.
Prerequisites
cd ~/outlook-cli && pip install -e .
playwright install chromium
Authentication
- First run:
outlook login opens Chromium, user logs in, bearer token is auto-captured from OWA requests.
- Named profiles are supported via
outlook account add|list|current|switch|remove.
- Tokens, browser state, ID maps, scheduled-send tracking, signatures, and per-profile config are isolated per account profile.
- Profile-scoped cache lives under
~/.cache/outlook-cli/accounts/<profile>/; config lives under ~/.config/outlook-cli/accounts/<profile>/.
- Existing single-account installs continue to work through the implicit
default profile and legacy cache paths.
- Auto re-login on 401 is profile-aware.
OUTLOOK_TOKEN is still supported, but if a profile is bound it must match that profile's mailbox.
- Bearer tokens are stored in the OS keychain/keyring;
token.json remains on disk only as non-secret metadata and is migrated automatically on first use.
outlook login
outlook login --force
echo $TOKEN | outlook login --with-token
outlook login --with-token < token.txt
outlook whoami
outlook account add work
outlook account list
outlook account current
outlook account switch work
outlook whoami --account work
Account Selection
- Every non-account command accepts
--account NAME.
- Selection precedence is:
--account NAME → OUTLOOK_ACCOUNT → persisted current account → implicit default.
outlook whoami shows the active profile in both human and JSON output.
--no-input disables prompts and makes mutating commands fail safely unless -y is also provided.
--dry-run previews mutating commands without making API calls.
--enable-commands restricts which top-level commands are allowed for an agent/session.
outlook inbox --account work
outlook calendar --account personal --days 3
outlook schedule-list --account work
outlook --enable-commands whoami,inbox whoami --account work
Automation / CI Safety
outlook whoami --json --no-input
outlook delete 3 --no-input
outlook send "to@email.com" "Subject" "Body" --dry-run --json
outlook schedule "to@email.com" "Subject" "Body" "+1h" --dry-run
Command Reference
Inbox
outlook inbox
outlook inbox --max 50
outlook inbox --unread
outlook inbox --from "alice.smith"
outlook inbox --subject "Q4 Report"
outlook inbox --from "acme" --subject "Project"
outlook inbox --after 2026-03-01
outlook inbox --before 2026-03-08
outlook inbox --has-attachments
outlook inbox --unread --after 2026-03-09
outlook inbox --json
outlook inbox --json -o emails.json
Read Email
outlook read 3
outlook read 3 --raw
outlook read 3 --json
Conversation Thread
outlook thread 3
outlook thread 3 --json
Send / Reply / Forward
outlook send "to@email.com" "Subject" "Body"
outlook send "to@email.com" "Subject" --body-file message.txt
printf 'Body from stdin' | outlook send "to@email.com" "Subject" --body-file -
outlook send "to@email.com" "Subject" "Body" -y
outlook send "a@b.com,c@d.com" "Subject" "Body" --cc e@f.com
outlook send "to@email.com" "Subject" "<h1>Hi</h1>" --html
outlook send "to@email.com" "Subject" "Body" --signature default
outlook send "to@email.com" "Report" "See attached" -a report.pdf
outlook send "to@email.com" "Files" "Here" -a file1.pdf -a file2.xlsx
outlook reply 3 "Thanks!"
printf 'Thanks from stdin' | outlook reply 3 --body-file -
outlook reply 3 "Thanks!" -y
outlook reply 3 "Noted, will fix." --all
outlook reply 3 "Here it is" -a requested.pdf
outlook reply-draft 3
outlook reply-draft 3 "Will review tomorrow"
outlook reply-draft 3 --body-file reply.html --html
outlook reply-draft 3 "<p>HTML reply</p>" --html
outlook reply-draft 3 "Noted" --all
outlook reply-draft 3 "Body" --signature default
outlook reply-draft 3 --json
outlook forward 3 "to@email.com"
outlook forward 3 "to@email.com" -y
outlook forward 3 "to@email.com" --comment "FYI"
outlook forward 3 "to@email.com" -a extra.pdf
Body formatting: Plain text bodies automatically convert newlines to HTML <br> tags, so line breaks and paragraphs are preserved in Outlook. Use --html only when you need to send raw HTML markup (tables, bold, links, etc.). No special flag is needed for multi-line plain text.
Drafts
outlook draft "to@email.com" "Subject" "Body"
outlook draft "to@email.com" "Subject" --body-file draft.txt
outlook draft "a@b.com,c@d.com" "Subject" "Body" --cc e@f.com
outlook draft "to@email.com" "Subject" "<h1>Hi</h1>" --html
outlook draft "to@email.com" "Subject" "Body" -a doc.pdf
outlook draft "to@email.com" "Subject" "Body" --signature default
outlook draft "to@email.com" "Subject" "Body" --json
outlook draft-send 3
outlook draft-send 3 -y
Search
outlook search "keyword"
outlook search "from:alice acme" --max 10
outlook search "subject:Q4 Report" --json
outlook search "keyword" --json -o results.json
Folders
outlook folders
outlook folders --json -o folders.json
outlook folder "Archive" --max 20
outlook folder "Sent Items" --from "john" --max 10
Categories
outlook categories
outlook categories --json
outlook categorize 3 "FYI"
outlook categorize 1 2 3 "FYI"
outlook uncategorize 3 "FYI"
outlook uncategorize 1 2 3 "FYI"
outlook category-create "New Category"
outlook category-create "Urgent" --color 0
outlook category-rename "FYI" "Info"
outlook category-rename "FYI" "Info" --no-propagate
outlook category-clear "FYI"
outlook category-clear "FYI" --folder "Inbox"
outlook category-clear "FYI" --max 50
outlook category-clear "FYI" -y
outlook category-delete "Old Category"
outlook category-delete "Old Category" -y
Signatures
outlook signature-pull
outlook signature-pull --name work
outlook signature-list
outlook signature-show default
outlook signature-delete old-sig
outlook signature-delete old-sig -y
Signatures are stored per profile in ~/.config/outlook-cli/accounts/<profile>/signatures/.
Scheduled Send
outlook schedule "to@email.com" "Subject" "Body" "+1h"
printf 'Scheduled body' | outlook schedule "to@email.com" "Subject" "+1h" --body-file -
outlook schedule "to@email.com" "Subject" "Body" "+30m" -y
outlook schedule "to@email.com" "Subject" "Body" "tomorrow 09:00"
outlook schedule "to@email.com" "Subject" "Body" "2026-03-15T10:00"
outlook schedule "to@email.com" "Subject" "Body" "+2h30m"
outlook schedule "to@email.com" "Subject" "Body" "+1h" --html
outlook schedule "to@email.com" "Subject" "Body" "+1h" -s default
outlook schedule "to@email.com" "Report" "See attached" "+1h" -a report.pdf
outlook schedule "to@email.com" "Subject" "Body" "+1h" --json
outlook schedule-draft 3 "+1h"
outlook schedule-draft 3 "tomorrow 09:00" -y
outlook schedule-list
outlook schedule-list --json
outlook schedule-cancel 1
outlook schedule-cancel 1 -y
Time formats: +30m, +1h, +2h30m (relative), today 17:00, tomorrow 09:00 (day-relative), 2026-03-15T10:00 or 2026-03-15 10:00 (absolute ISO).
How it works: schedule-list cross-references local tracking with Drafts folder to find matching drafts. schedule-cancel deletes the draft from server (preventing delivery) and removes local tracking. Status shows draft when a server match is found, queued when only locally tracked.
Workflow: Schedule a reply draft:
outlook reply-draft 3 "Will review tomorrow"
outlook folder Drafts -n 1
outlook schedule-draft 42 "tomorrow 09:00"
outlook schedule-list
Message Management
outlook mark-read 3
outlook mark-read 3 --unread
outlook mark-read 1 2 3
outlook move 3 "Archive"
outlook move 1 2 3 "Archive"
outlook delete 3
outlook delete 1 2 3 -y
outlook flag 3
outlook flag 3 4 5
outlook flag 3 --due tomorrow
outlook flag 3 --due 2026-03-20
outlook flag 3 --due +3d
outlook flag 3 --complete
outlook flag 3 --clear
outlook pin 3
outlook pin 3 4 5
outlook pin 3 --unpin
outlook open 3
outlook open 3 --print-url
Attachments
outlook attachments 3
outlook attachments 3 -d
outlook attachments 3 -d --save-to ~/Downloads
outlook attachments 3 --json
Calendar
outlook calendar
outlook calendar --days 14
outlook calendar --days -7
outlook calendar --days -30
outlook calendar --timezone Asia/Shanghai
outlook calendar --timezone UTC+8
outlook calendar --calendar "John Smith"
outlook calendar --calendar "John" --days 5
outlook calendar --json -o events.json
Events
outlook event 42
outlook event-create "Meeting" "2026-03-16 10:00" "2026-03-16 11:00"
outlook event-create "Meeting" "tomorrow 14:00" "tomorrow 15:00" \
-a john@example.com -a jane@example.com \
-l "Room A" -b "Agenda: Q1 review" -y
outlook event-create "Standup" "tomorrow 09:00" "tomorrow 09:30" \
--teams -a team@example.com
outlook event-create "Weekly Sync" "2026-03-16 10:00" "2026-03-16 11:00" \
--repeat weekly --repeat-count 8 -a team@example.com
outlook event-create "Daily Standup" "2026-03-16 09:00" "2026-03-16 09:15" \
--repeat daily --repeat-until 2026-04-30
outlook event-create "Sprint Review" "2026-03-16 14:00" "2026-03-16 15:00" \
--repeat weekly --repeat-days Monday,Wednesday --repeat-count 12
outlook event-create "Monthly Report" "2026-03-16 10:00" "2026-03-16 11:00" \
--repeat monthly --repeat-count 6
outlook event-update 42 --subject "New Title"
outlook event-update 42 --start "2026-03-16 14:00" --end "2026-03-16 15:00"
outlook event-update 42 --location "Room B"
outlook event-update 42 --add-attendee new@example.com
outlook event-update 42 --remove-attendee old@example.com
outlook event-delete 42
outlook event-delete 42 --series
outlook event-delete 42 43 44 -y
outlook event-respond 42 accept
outlook event-respond 42 decline --comment "Can't make it"
outlook event-respond 42 tentative --silent
outlook event-instances 42
outlook event-instances 42 --days 180
Time formats for events: +1h, +30m, +2h30m (relative), today 17:00, tomorrow 09:00 (day-relative), 2026-03-15T10:00 or 2026-03-15 10:00 (absolute ISO).
Recurrence options: --repeat daily|weekly|monthly, --repeat-interval N (default 1), --repeat-count N (number of occurrences), --repeat-until YYYY-MM-DD, --repeat-days Monday,Wednesday (for weekly).
Calendars / Free-Busy / People
outlook calendars
outlook calendars --json
outlook free-busy "john@example.com" tomorrow
outlook free-busy "a@b.com,c@d.com" 2026-03-16 -d 30
outlook free-busy "team@example.com" today --start-hour 14 --end-hour 18
outlook people-search "john"
outlook people-search "john" --max 5 --json
Contacts
outlook contacts
outlook contacts --max 100
outlook contacts --json -o contacts.json
JSON / Scripting
Auto-JSON on pipe: When stdout is piped (not a terminal), all commands automatically output JSON — no --json flag needed.
outlook inbox | jq '.data[0].subject'
outlook inbox --json
outlook inbox --json -o emails.json
outlook search "keyword" | jq '.data | length'
outlook categories | jq '.data[].Category'
Structured envelope: All JSON output is wrapped in a standard envelope:
{"ok": true, "schema_version": "1", "data": [...]}
Errors also return structured JSON (when in JSON mode):
{"ok": false, "schema_version": "1", "error": {"code": "not_found", "message": "..."}}
Error codes: session_expired, rate_limited, not_found, not_authenticated, unknown_error.
JSON Field Names
Email objects (inbox, search, folder with --json):
| Field | Type | Example |
|---|
id | string | Outlook message ID |
display_num | int | 3 |
subject | string | "Re: Meeting" |
sender | object | {"name": "John", "address": "john@x.com"} |
to | list[object] | [{"name": "Jane", "address": "jane@x.com"}] |
cc | list[object] | same as to |
received | string | "2026-03-09T14:30:00Z" |
preview | string | first ~255 chars of body |
body | string | full body text |
body_type | string | "Text" or "HTML" |
is_read | bool | true |
has_attachments | bool | false |
importance | string | "Normal" |
conversation_id | string | Outlook conversation ID |
categories | list[string] | ["Spam", "FYI"] |
flag_status | string | "notFlagged", "flagged", "complete" |
flag_due | string|null | "2026-03-20T23:59:59" or null |
scheduled_send | string|null | "2026-03-15T10:00:00Z" or null |
Event objects (calendar, event, event-instances with --json):
| Field | Type | Example |
|---|
id | string | Outlook event ID |
display_num | int | 42 |
subject | string | "Weekly Sync" |
start | string | "2026-03-16T10:00:00" |
end | string | "2026-03-16T11:00:00" |
location | string | "Room A" |
organizer | object | {"name": "John", "address": "john@x.com"} |
attendees | list[object] | [{"email": {...}, "type": "Required", "response": "Accepted"}] |
is_all_day | bool | false |
show_as | string | "Busy" |
response_status | string | "Accepted", "NotResponded", "Organizer" |
recurrence | object|null | {"Pattern": {...}, "Range": {...}} |
event_type | string | "SingleInstance", "Occurrence", "SeriesMaster" |
is_online_meeting | bool | true |
online_meeting_url | string | Teams join URL |
Folder objects (folders --json):
name (string), unread_count (int), total_count (int)
Category objects (categories --json):
Category (string), Color (string), Unread (int), Total (int)
Common Patterns for AI Agents
outlook inbox --max 10
outlook inbox --from "bob.wilson"
outlook inbox --subject "deployment" --unread
outlook inbox --from "alice" --max 1 --json
outlook folders --json | jq '.[] | select(.name == "Inbox") | .unread_count'
outlook search "deployment failed" --max 5
outlook calendar --days 1
outlook calendar --calendar "John Smith" --days 5
outlook event-create "Standup" "2026-03-16 09:00" "2026-03-16 09:15" \
--repeat weekly --repeat-count 8 -a team@example.com -y
outlook free-busy "colleague@company.com" tomorrow -d 30
outlook people-search "john"
outlook event-respond 42 accept
outlook event 42
outlook thread 3
outlook reply 3 "Received, will review today."
outlook send "to@email.com" "Report" "See attached" -a report.pdf -a data.xlsx -y
outlook attachments 5 -d --save-to ~/Downloads
outlook flag 3 --due tomorrow
outlook pin 3 4
outlook categorize 1 2 3 "FYI"
outlook categories
outlook category-create "Project Alpha" --color 7
outlook schedule "to@email.com" "Meeting notes" "Attached." "+1h"
outlook reply-draft 3 "Will review this"
outlook schedule-draft 42 "tomorrow 09:00"
outlook schedule-list
outlook open 3
outlook open 42 --print-url
ID System
Messages and events get short display numbers (#1, #2, #3...) mapped to real Outlook IDs. Numbers are assigned when listing and persist across commands. Messages and events share the same ID map (capped at 500 entries).
The ID map is profile-local, so #3 in one account is unrelated to #3 in another.
outlook inbox --max 5
outlook read 3
outlook reply 3 "OK"
outlook calendar --days 7
outlook event 42
outlook event-respond 42 accept
Error Handling
- Token expired → auto re-login attempted via cached SSO state.
Account profile 'X' not found → run outlook account add X first, or use outlook account list.
Unknown message #N → run outlook inbox or outlook calendar first to populate the ID map.
Folder 'X' not found → run outlook folders to see available folder names.
Calendar 'X' not found → run outlook calendars to see available calendar names.
Category 'X' not found → run outlook categories to see available categories.
- HTTP 429 → automatic exponential backoff (3 retries).
Safety Notes
- Token is cached with
chmod 600 (owner-only read/write).
- Browser state saved for SSO — avoids repeated logins.
- Tokens, browser state, signatures, scheduled-send tracking, and ID maps are scoped per account profile.
send, reply, forward, draft-send, schedule, schedule-draft, event-create, event-delete, delete, and category-delete ask for confirmation by default (use -y to skip).
flag, pin, mark-read, categorize do NOT require confirmation (safe, reversible operations).
- Do not share or log bearer tokens — they grant full mailbox access.
- Prefer
outlook login over manually copying tokens.