CLIco outlook

Outlook CLI (co outlook)

Manage email, contacts and your calendar from your Outlook account right in the terminal — the same Microsoft Graph access your agents get from the Outlook tool and the Microsoft Calendar tool, as commands.

Outlook is one product with three panes — Mail, Calendar, People — and co outlook --help is grouped the same way: Mail, Send, Scheduled sends, Contacts, Calendar.

Quick Start

# Connect your Microsoft account (one-time)
co auth microsoft

# Check your inbox (the zero-arg default)
co outlook

# Read message #3 from the inbox list
co outlook read 3
co outlook read 3 --mark-read

# Send a message
co outlook send alice@example.com "Hello" "Thanks for the meeting today!"

# Save and find contacts
co outlook contact add "Zhou Yifei" zhou@example.com
co outlook contact search yifei

# Your calendar
co outlook calendar today
co outlook calendar teams "Design review" 2026-09-10T10:00:00Z 2026-09-10T11:00:00Z --attendees a@x.com --yes

That's the whole surface. Everything below is detail. Every command ends by naming the next one — including when its output is piped — so an agent driving co outlook never has to guess a command name.

Setup

co outlook needs a connected Microsoft account:

co auth microsoft

This opens the Microsoft OAuth flow and saves MICROSOFT_* credentials (access token, refresh token, scopes, email, expiry) to the global ~/.co/keys.env, or to the file named by co --env-file PATH auth microsoft. A project directory never selects credentials on its own.

The stored access token is used for as long as it is valid; a refresh happens only within five minutes of its expiry or after Microsoft Graph answers 401. Before 1.8.4 every command discarded the stored token and went through the refresh broker first, so an oo-api outage made a perfectly reachable mailbox look expired (#1312).

Microsoft credentials remain CLI-local. oo-api performs the OAuth exchange and refresh as a stateless proxy, while Outlook saves rotated tokens back to the selected env file; the backend does not retain the token pair.

Contact commands require Contacts.ReadWrite and calendar commands require Calendars. If you authenticated before those were requested, run co auth microsoft once more to grant them — a refresh cannot widen scopes. See Microsoft Integration for all requested scopes.

Commands

co outlook contact — Manage contacts

Contacts start deliberately small: a display name and one email address.

# Create
co outlook contact add "Zhou Yifei" zhou@example.com

# List (25 by default)
co outlook contact list
co outlook contact list -n 50

# Find by a case-insensitive name or email substring
co outlook contact search yifei
co outlook contact search gmail.com -n 10

list and search print a Rich table in a terminal. When output is piped, each contact is a tab-separated name, email, and full Microsoft Graph ID, so scripts do not receive truncated values.

co outlook — Show the inbox

With no subcommand, prints your most recent emails. Same as co outlook inbox.

co outlook

co outlook inbox — List received email

co outlook inbox                 # last 10
co outlook inbox -n 25           # last 25  (alias: --last)
co outlook inbox -u              # only unread  (alias: --unread)

Unread messages are marked with a green . The leftmost # is the email's number — pass it to co outlook read. The numbering is cached (in ~/.co/outlook_last_inbox.json), so read 3 still finds the right message in a later shell session.

Options

  • --last, -n — how many to show (default: 10)
  • --unread, -u — only unread messages

co outlook read <#> — Read one message

co outlook read 3
co outlook read 3 --mark-read

Prints the full body (sender, subject, date, content). Accepts the # from your last listing (inbox or search) or a full Graph message ID. The message is left unread by default. Use --mark-read when opening it should consume it; that opt-in needs the Mail.ReadWrite scope.

co outlook reply <#> <message> — Reply

co outlook reply 3 "Sounds good, see you then."

Sends a threaded reply to an email from your last listing. Use - as the message to read the reply body from stdin, and --at +2h (or a UTC ISO time) to schedule the reply like a scheduled send. A scheduled reply is a reply draft that Exchange holds until then, so it needs the Mail.ReadWrite scope and shows up in co outlook scheduled like any other scheduled send.

Options

  • --cc — CC recipients (comma-separated); the reply stays in its thread
  • --bcc — BCC recipients (comma-separated)
  • --attach, -a FILE — attach a local file; repeat for multiple
  • --at — schedule delivery: +30m, +2h, or a UTC ISO time

Copying a third person--cc and --bcc are set on Graph's reply action (or on the deferred reply draft when --at is used), so the message keeps its In-Reply-To / References headers and lands in the existing conversation. Before 1.8.4b1 the only way to copy someone was a fresh co outlook send with "RE:" in the subject, which the recipient saw as a new, history-less thread (#1247):

co outlook reply 3 "Looping in Sam so he has the context." --cc sam@example.com

Attachments — same files, limit, and flag as send, still a real reply:

co outlook reply 3 "Signed copy attached." \
    --attach signed.pdf --attach cover.png

The files ride on Graph's reply action, so the message stays in the original conversation instead of going out as a new email. Attachments and --at combine — a scheduled reply keeps its files (they are added to the reply draft before it is scheduled).

co outlook send <to> <subject> <message> — Send

co outlook send bob@example.com "Subject line" "Body text"

All three arguments are positional and required. to, --cc, and --bcc take comma-separated addresses for multiple recipients.

Options

  • --cc — CC recipients (comma-separated)
  • --bcc — BCC recipients (comma-separated)
  • --attach, -a FILE — attach a local file; repeat for multiple
  • --at — schedule delivery: +30m, +2h, or a UTC ISO time

Attachments — screenshots, PDFs, images, anything:

co outlook send bob@example.com "Report" "See attached." \
    --attach report.pdf --attach screenshot.png

Graph's sendMail limit is ~3MB total across attachments. A human invoking the CLI may explicitly name a file outside the project; agent-facing Outlook() tools remain limited to project files.

Scheduling — the message is created as a draft carrying a deferred-send time, and Exchange holds it until then. Creating the draft needs the Mail.ReadWrite scope, which co auth microsoft grants by default; a token issued before that scope was added needs one more co auth microsoft:

co outlook send bob@example.com "Reminder" "Standup in 30." --at +30m
co outlook send bob@example.com "Launch" "We're live!" --at 2026-07-06T15:30:00Z

+30m / +2h are relative to now; anything else is taken as a UTC ISO time. The confirmation names the way back, because a scheduled send is the one you are most likely to want to take back (#1314):

✓ Scheduled for 2026-09-09T22:30:00Z to bob@example.com
  From: you@example.com
  Cancel before it goes out: co outlook scheduled, then co outlook cancel <#>
Next: co outlook scheduled

Long bodies — pass - as the message to read the body from stdin:

co outlook send alice@example.com "Weekly update" - < body.txt

co outlook sent — List sent email

co outlook sent            # last 10
co outlook sent -n 25      # last 25
co outlook search "quarterly report"
co outlook search invoice -n 25

Matches subject and body via Microsoft Graph search.

co outlook scheduled / co outlook cancel <#> — Manage scheduled sends

co outlook scheduled      # what's queued, and when it sends
co outlook cancel 1       # cancel it before Exchange sends it

Shows every email waiting for scheduled delivery (deferred-send drafts), numbered for cancel, with recipient, subject, and local send time:

                     ⏰ Outlook — scheduled sends
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┓
┃ To                          ┃ Subject                ┃ Sends at     ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━┩
│ tamara.berryman@unsw.edu.au │ RE: Access to the MCIC │ Jul 07 08:00 │
└─────────────────────────────┴────────────────────────┴──────────────┘

Scheduled emails sit in Drafts until delivery time; cancel deletes the pending message so it never goes out. (Some Exchange work/school tenants reject the delete with 403 — there, use Outlook's own "Cancel Send".)

co outlook calendar — Your calendar

The Microsoft half of what co gcalendar does for Google, leaf for leaf (teams where Google has meet). Event IDs are stable Graph IDs, never row numbers. Reads run at once; every write previews by default and prints the exact --yes command that performs it.

Command Python method on MicrosoftCalendar
list --days 7 -n 20 list_events
today get_today_events
read EVENT_ID get_event
meetings --days 7 get_upcoming_meetings
free 2026-09-10 --minutes 30 find_free_slots
create TITLE START END [--attendees … --location … --description …] create_event
teams TITLE START END --attendees EMAILS create_teams_meeting
update EVENT_ID --title … --start … --end … update_event
delete EVENT_ID delete_event
co outlook calendar                                 # bare = upcoming events
co outlook calendar today
co outlook calendar free 2026-09-10 --minutes 30
co outlook calendar create "Standup" 2026-09-10T09:00:00+10:00 2026-09-10T09:15:00+10:00
# {"mode": "preview", "operation": "create", ...}
# No changes made.
# Next: co outlook calendar create Standup 2026-09-10T09:00:00+10:00 2026-09-10T09:15:00+10:00 --yes

Times: an ISO offset is converted to UTC, and a naive time means UTC — Graph receives every event with timeZone: UTC. update preserves omitted fields; empty strings do not clear a field. free covers 09:00–17:00 UTC on this calendar only, not attendees'. Needs the Calendars scope.

Exit 0 is a read, a preview, or a successful write; 1 is an operational failure with a recovery command; 2 is invalid arguments. A Graph error prints its HTTP status and a next command, never the response body.

Same functions, in your agent

The CLI is a thin wrapper over the Outlook tool, so anything co outlook does, your agent can do too:

from connectonion import Agent, Outlook

outlook = Outlook()
agent = Agent("assistant", tools=[outlook])

agent.input("Check my inbox and send the report to alice@example.com")

Or call it directly:

outlook = Outlook()
outlook.list_inbox(last=10, unread=True)
outlook.add_contact("Zhou Yifei", "zhou@example.com")
outlook.list_contacts(max_results=25)
outlook.search_contacts("yifei")
outlook.send(
    "alice@example.com", "Report", "See attached.",
    attachments=["report.pdf"],
    send_at="2026-07-06T15:30:00Z",
)

Troubleshooting

Each failure names the layer that actually broke, so the printed Next: line is the fix (#1313). Three layers can fail, and each has its own command:

Printed cause Layer Next command
Microsoft account not connected no local record co auth microsoft
Microsoft <scope> permission missing (Mail, Contacts.ReadWrite, Calendars) record predates the scope co auth microsoft — a refresh cannot widen scopes
No OpenOnion API key … / OpenOnion authentication failed while refreshing provider access. your OpenOnion key is missing or dead; the Microsoft grant is fine co auth
Microsoft authorization expired or was revoked. Microsoft rejected the refresh token co auth microsoft
Microsoft access token is expiring and the saved record has no refresh token. incomplete local record co auth microsoft
Microsoft Graph API error (HTTP nnn). Graph refused the call; the body is never printed the command's own next step (co outlook inbox, co outlook calendar list)

Other cases:

  • No email #N in your inbox → the number is out of range; run co outlook inbox to refresh the listing.
  • co outlook cancel rejected with 403 → some Exchange work/school tenants lock deferred messages against API deletion; use Cancel Send in Outlook instead. On personal outlook.com accounts cancel works normally.

If Teams creation returns an event without a usable meeting link, the command exits 1 and retains the event ID. Follow co outlook calendar read EVENT_ID to inspect that event; do not repeat creation. A missing event ID requires listing the calendar before another write.

Star us on GitHub

If ConnectOnion saves you time, a ⭐ goes a long way — and earns you a coffee chat with our founder.