Courier CLI

Courier is a command-line tool for managing email, calendar, contacts, and chat from your terminal. It connects to your existing providers through a unified interface — no switching between browser tabs to check your schedule, fire off a quick reply, or message a teammate.

Install

Linux / macOS — paste into a terminal:

curl -fsSL https://www.ninjacrab.com/wp-content/uploads/courier-cli/install.sh | sh

Windows — paste into PowerShell:

iex (irm https://www.ninjacrab.com/wp-content/uploads/courier-cli/install.ps1)

The installer detects your OS and CPU, downloads the matching build, and installs it as the courier command on your PATH. Prefer to grab a binary by hand? Use the downloads below.

Download v1.7.0

PlatformFile
Windows x64CourierCli.exe
Windows ARM64CourierCli.exe
macOS x64 (Intel)CourierCli
macOS ARM64 (Apple Silicon)CourierCli
Linux x64CourierCli
Linux ARM64CourierCli

On Linux/macOS, make the binary executable after downloading:

chmod +x CourierCli

What it does

  • Mail — List, search, read, send (plain text or HTML), reply (with cc/bcc/attachments), create/send/delete drafts, download attachments, trash or delete messages, access shared mailboxes
  • Calendar — View today's agenda, this week's events, create/update/delete events, manage attendees, attach online meetings (Teams or Google Meet), quick-add from natural language, set reminders
  • Meetings — Read Microsoft Teams meeting transcripts (speaker-labeled text, raw VTT, or structured JSON)
  • Contacts — Search people by name or email across providers
  • Chat — List chats, read and send messages, find or create 1:1 conversations, list spaces/channels, post to channels
  • Auth — Browser-based OAuth login, multi-account support with aliases, status check, logout

Usage examples

# Mail
courier mail list
courier mail list --from boss@work.com --since 3d
courier mail read <message-id>
courier mail send --to someone@example.com --subject "Hello" --body "Hi there"
courier mail reply <message-id> --body "Thanks!" --cc team@work.com --attach report.pdf
courier mail send --to someone@example.com --subject "Update" --body-file message.html --html
courier mail draft create --to someone@example.com --subject "Draft" --body "Work in progress"
courier mail draft send <draft-id>
courier mail list --mailbox shared@work.com   # shared mailbox (Microsoft)

# Calendar
courier cal today
courier cal week
courier cal create "Team Standup" --start "2026-03-01 09:00" --end "2026-03-01 09:30" --reminder 15
courier cal create "Sync" --start "2026-06-05 14:00" --end "2026-06-05 14:30" --online-meeting
courier cal update <event-id> --attendees alice@work.com --remove-attendees bob@work.com
courier cal quick "Lunch with Alex tomorrow at noon"

# Meetings (Microsoft Teams)
courier meeting transcript <event-id>
courier meeting transcript <event-id> --raw
courier meeting transcript <event-id> -o json

# Contacts
courier contacts search "John Smith"

# Chat
courier chat list
courier chat send <chat-id> --body "Hello!"
courier chat spaces
courier chat post <space-id> --body "Update for the team"

Output formats

All commands support --output text, --output json, and --output jsonl for easy scripting and piping into other tools.

Provider support

Courier is built on a provider abstraction layer. Currently supported providers:

  • Google Workspace — Gmail, Google Calendar
  • Microsoft 365 — Outlook/Exchange mail (incl. shared mailboxes), calendar, Teams chat, Teams meeting transcripts, contacts/directory search

Multiple accounts from different providers can be used simultaneously. Read commands aggregate across all accounts by default.

Built with

.NET, System.CommandLine, Google APIs, Microsoft Graph SDK (MSAL)

Security & your credentials

Courier uses OAuth to connect to your mail, calendar, and chat providers — it never sees your password. After you authenticate, your OAuth tokens are stored locally at ~/.config/courier/ (Linux/macOS) or %APPDATA%/courier/ (Windows).

You are responsible for protecting that folder. Anyone with access to those files can read and send your email and access your calendar on your behalf. A few things worth knowing:

  • Do not share your courier config directory or its contents with anyone
  • On shared machines, make sure your home directory permissions are restricted (chmod 700 ~)
  • If you believe your tokens have been compromised, revoke access immediately via your Google account permissions or Microsoft account permissions and run courier auth logout
  • Courier does not transmit your tokens anywhere — all authentication data stays on your machine

Courier is provided as-is. Use it at your own risk and keep your local environment secure.

Changelog

[1.7.0] — 2026-08-27

Added

  • Auth broker (src/CourierBroker) — an ASP.NET Core minimal API that holds the Google OAuth client secret so the CLI no longer ships one. Set COURIER_BROKER_URL (or broker_url in credentials.json) and auth login starts a session on the broker, sends you to Google, and polls until the browser half completes. The broker performs the code exchange and all subsequent token refreshes; it stores nothing, and each session is single-use and expires in 10 minutes. Tokens still live only in %APPDATA%\courier\tokens.
  • When no broker is configured the CLI keeps using its built-in client credentials and the loopback receiver, so existing installs are unaffected.
  • --verbose global flag (and the existing COURIER_DEBUG=1 env var, now documented) adds the request method and URL, the response headers, the raw error body, and the stack trace to any failure. Credentials are redacted at every verbosity.
  • Structured JSON errors. With -o json/-o jsonl, failures emit {"error":{"status":…,"code":…,"message":…,"requestId":…,"date":…}} on stderr instead of a text line. Exit code stays nonzero, and stdout stays clean so | jq remains valid.
  • Credential redaction pass covering Authorization headers, bearer tokens, JWTs, Google/MSAL refresh tokens, OAuth authorization codes, cookies, and client secrets. Applied as a final pass over rendered output rather than per call site.

Fixed

  • Google calendar create and update failed with HTTP 400 on Windows. EventDateTime.TimeZone (and Calendars.Insert) were populated from TimeZoneInfo.Local.Id, which on Windows is a Windows zone ID — Pacific Standard Time — where the Calendar API accepts only IANA IDs such as America/Los_Angeles. Every cal create and cal update against a Google account was rejected; reads were unaffected, since event listing passes a DateTimeOffset and no zone string. Now converted via TimeZoneInfo.TryConvertWindowsIdToIanaId, which no-ops on the linux and osx RIDs where Local.Id is already IANA. Microsoft is unchanged — Graph accepts Windows zone names, so the identical-looking code in MicrosoftCalendarService is correct as written.
  • Graph errors were collapsed into a single line that discarded the diagnostic payload. OutputFormatter.WriteError printed only ex.Message and the exception type name, so for an ODataError the HTTP status, error.code, and the innerError request-id/client-request-id/date were all thrown away — and a request-id is what lets a tenant admin find the cause in one step. Errors now print every field the provider returned, verbatim and labelled as such.

    Note on the failure that prompted this: the sentence Graph API access to transcripts is disabled for this tenant. was Graph’s own error.message, not a courier string — ODataError.Message is an override returning Error.Message. courier did not invent that cause, but it did discard the code and request-id that would have made it checkable.
  • Hardcoded messages that asserted causes the API never stated. TranscriptAccessDeniedException (“you must have been an attendee or the organizer”) replaced real 403/404 response bodies from the transcript content endpoint, discarding Graph’s own explanation and its request-id. Removed; the raw response is surfaced via a new GraphHttpException. Any remaining courier-authored guess is printed below the provider’s output under a header marking it a guess.
  • NotATeamsMeetingException covered two different conditions. A missing onlineMeeting.joinUrl and an empty /me/onlineMeetings lookup both reported “This event is not a Teams meeting”, which is only accurate for the first. The lookup case now reports what was observed (Graph returned an empty result and gave no reason) without asserting why.
  • NoTranscriptsAvailableException asserted that transcription was not enabled by the organizer. Graph returns an empty list without saying why; the message now says that.
  • Seven silent catch blocks discarded exceptions entirely — auth status checks reported “not authenticated” for any failure including 403s and 5xx, account aggregation dropped accounts without a trace, and the People API fallback hid permission errors. All still fall back as before, but name the suppressed failure under --verbose.
  • BrokerAuthClient dropped Google’s error body on token revocation (EnsureSuccessStatusCode) and reported only one of error/error_description elsewhere; both are now kept and redacted.
  • Google auth login failed instantly with “No input received” when stdin was redirected or closed (piped input, /dev/null, non-interactive hosts such as Claude Code’s ! commands). The receiver raced the loopback listener against Console.ReadLine, and an immediate EOF cancelled the whole flow before the browser callback could arrive. The paste path is now only offered on a real console, EOF and blank lines no longer cancel, and the loopback listener is always authoritative. Microsoft was unaffected — it never read stdin.

Notes

  • Broker mode requires a Web application OAuth client. Desktop clients only permit loopback redirect URIs, so this is a different client_id than the built-in one — Google refresh tokens are bound to the client that issued them, and every Google account must run auth login once after switching.

[1.6.0] — 2026-06-27

Added

  • cal calendars create <name> creates a new calendar (the container, not an event) on the target account. Microsoft uses POST /me/calendars; Google uses Calendars.Insert. Both run under existing scopes (Calendars.ReadWrite / full Calendar), so no re-auth is needed. --description is honored by Google and ignored by Microsoft (the Graph Calendar resource has no description field).
  • ICalendarService gains CreateCalendarAsync(name, description).

[1.5.0] — 2026-06-03

Added

  • HTML email — --html on mail send, mail reply, and mail draft create treats the body as HTML (Gmail: MimeKit HtmlBody; Microsoft: BodyType.Html). Plain text remains the default.
  • --body-file <path> on the same three commands reads the body from a file — convenient for HTML. Exactly one of --body / --body-file is required; supplying both errors.

Notes

  • An HTML mail reply on Microsoft replaces the quoted thread with the supplied HTML (Graph’s reply Comment only accepts plain text, so the body is set directly on the reply draft).

[1.4.0] — 2026-06-01

Added

  • cal create --online-meeting (alias --teams) generates an online meeting join link and prints it on creation. Microsoft creates a Teams meeting; Google creates a Google Meet.
  • EventSummary now carries a JoinUrl, surfaced in text output as a Join: line and included in JSON. Calendar reads (cal today/week/events) also populate it for events that already have a Teams/Meet link.

[1.3.1] — 2026-06-01

Fixed

  • mail send and mail draft create failed on the Microsoft provider with Error: were unable to deserialize [ODataError] whenever --cc/--bcc were omitted (i.e. almost always). Assigning null to CcRecipients/BccRecipients marked them “set” in Kiota’s backing store, serializing "ccRecipients": null, which Graph rejects. Now Cc/Bcc are only assigned when present. (Same Kiota null-property quirk as the 1.2.0 cal create fix.)

[1.3.0] — 2026-05-28

Added

  • Draft management — mail draft create, mail draft send <id>, mail draft delete <id> for both providers. mail draft create accepts the same flags as mail send (--to, --subject, --body, --cc, --bcc, --attach, --mailbox). The existing mail drafts (plural) command continues to list drafts.

[1.2.1] — 2026-05-28

Changed

  • Error output now includes the exception type and full inner-exception chain (e.g. Error: were unable to deserialize [GoogleApiException] -> ... [JsonReaderException]). Set COURIER_DEBUG=1 to also dump the stack trace — makes generic SDK errors actionable.

Added

  • macOS targets (osx-x64, osx-arm64) added to default publish.sh.

[1.2.0] — 2026-05-07

Added

  • mail reply now accepts --cc, --bcc, and --attach — matches mail send semantics while preserving threading on both Gmail and Microsoft
  • cal update attendee management — --attendees (repeatable add), --remove-attendees (repeatable, case-insensitive), --clear-attendees. Idempotent and order-aware (clear → remove → add)
  • courier meeting transcript <event-id> — read Microsoft Teams meeting transcripts. Speaker-labeled text by default, --raw for VTT, -o json for structured entries. --transcript-id to disambiguate when a meeting has multiple transcripts
  • Shared mailbox support — --mailbox <email> on all mail subcommands routes through the target mailbox (Microsoft only). Requires Exchange-level access plus admin consent for Mail.ReadWrite.Shared and Mail.Send.Shared
  • New Microsoft Graph scopes: OnlineMeetings.Read, OnlineMeetingTranscript.Read.All, Mail.ReadWrite.Shared, Mail.Send.Shared (all require tenant admin consent)

Fixed

  • cal create on Microsoft — Graph rejected the create body with “Invalid format” because Body = null was explicitly serialized and the datetime format was second-resolution. Now omits Body when no description is provided and uses 7-decimal datetime format
  • Gmail dates showed 0001-01-01 00:00 — DateTimeOffset.TryParse on RFC 2822 Date headers fails under non-English locales. Now uses Message.InternalDate (epoch ms) as the authoritative source with InvariantCulture parse fallback
  • Meeting transcript content fetch — bypassed Kiota for the content endpoint with a raw HttpClient GET that sets a single Accept: text/vtt and a bearer token from MSAL

[1.1.1] — 2026-03-05

Fixed

  • chat list no longer fails on Microsoft accounts — removed unsupported $orderby=lastUpdatedDateTime query parameter
  • mail attachment now saves files using the original filename (e.g. TicketFast.pdf) instead of the attachment ID, which could exceed filesystem limits
  • mail attachment handles filename collisions with a counter suffix (e.g. file (1).pdf)

Added

  • mail attachment --out-file <path> option to specify a custom output filename

[1.1.0] — 2026-03-05

Added

  • Contacts search — courier contacts search <query> resolves people by name/email across providers (Microsoft People API with directory fallback)
  • Chat commands — provider-agnostic chat abstraction: chat list, chat read, chat send, chat find, chat spaces, chat space-messages, chat post

Changed

  • Breaking: courier teams command replaced by courier chat — Teams-specific operations are now behind the provider-agnostic chat interface

[1.0.3] — 2026-03-04

Fixed

  • Calendar view commands (cal events, cal today, cal week) now query all calendars instead of only the primary — events on secondary/shared calendars are included in results
  • Added --calendar option to calendar view commands to filter by a specific calendar ID

[1.0.2] — 2026-03-02

Added

  • Microsoft Graph provider — full Outlook/Exchange support for mail and calendar via courier auth login --as outlook --provider microsoft
  • --calendar option on cal create, cal update, cal delete, and cal quick to target a specific calendar by ID
  • --reminder / --no-reminder options on cal create and cal update
  • Headless auto-detection — both providers detect headless environments and fall back to device code auth

Fixed

  • Trimmed publish builds now preserve reflection metadata — fixes Property Get method was not found on Linux ARM64

[0.1.0] — 2026-02-28

  • Initial release with Google provider (Gmail + Google Calendar)
  • Multi-account support with aliases and default account
  • Mail commands: list, search, read, send, reply, labels, drafts, trash, delete, attachments
  • Calendar commands: today, week, events, list, create, update, delete, quick add
  • Output formats: text, json, jsonl
  • Self-contained single-file builds for win-x64, linux-x64, linux-arm64