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
| Platform | File |
|---|---|
| Windows x64 | CourierCli.exe |
| Windows ARM64 | CourierCli.exe |
| macOS x64 (Intel) | CourierCli |
| macOS ARM64 (Apple Silicon) | CourierCli |
| Linux x64 | CourierCli |
| Linux ARM64 | CourierCli |
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. SetCOURIER_BROKER_URL(orbroker_urlincredentials.json) andauth loginstarts 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.
--verboseglobal flag (and the existingCOURIER_DEBUG=1env 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| jqremains 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(andCalendars.Insert) were populated fromTimeZoneInfo.Local.Id, which on Windows is a Windows zone ID —Pacific Standard Time— where the Calendar API accepts only IANA IDs such asAmerica/Los_Angeles. Everycal createandcal updateagainst a Google account was rejected; reads were unaffected, since event listing passes aDateTimeOffsetand no zone string. Now converted viaTimeZoneInfo.TryConvertWindowsIdToIanaId, which no-ops on the linux and osx RIDs whereLocal.Idis already IANA. Microsoft is unchanged — Graph accepts Windows zone names, so the identical-looking code inMicrosoftCalendarServiceis correct as written. - Graph errors were collapsed into a single line that discarded the diagnostic payload.
OutputFormatter.WriteErrorprinted onlyex.Messageand the exception type name, so for anODataErrorthe HTTP status,error.code, and theinnerErrorrequest-id/client-request-id/datewere 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 sentenceGraph API access to transcripts is disabled for this tenant.was Graph’s ownerror.message, not a courier string —ODataError.Messageis an override returningError.Message. courier did not invent that cause, but it did discard thecodeandrequest-idthat 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 newGraphHttpException. Any remaining courier-authored guess is printed below the provider’s output under a header marking it a guess. NotATeamsMeetingExceptioncovered two different conditions. A missingonlineMeeting.joinUrland an empty/me/onlineMeetingslookup 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.NoTranscriptsAvailableExceptionasserted that transcription was not enabled by the organizer. Graph returns an empty list without saying why; the message now says that.- Seven silent
catchblocks 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. BrokerAuthClientdropped Google’s error body on token revocation (EnsureSuccessStatusCode) and reported only one oferror/error_descriptionelsewhere; both are now kept and redacted.- Google
auth loginfailed 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 againstConsole.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_idthan the built-in one — Google refresh tokens are bound to the client that issued them, and every Google account must runauth loginonce 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 usesPOST /me/calendars; Google usesCalendars.Insert. Both run under existing scopes (Calendars.ReadWrite/ full Calendar), so no re-auth is needed.--descriptionis honored by Google and ignored by Microsoft (the Graph Calendar resource has no description field).ICalendarServicegainsCreateCalendarAsync(name, description).
[1.5.0] — 2026-06-03
Added
- HTML email —
--htmlonmail send,mail reply, andmail draft createtreats the body as HTML (Gmail: MimeKitHtmlBody; 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-fileis required; supplying both errors.
Notes
- An HTML
mail replyon Microsoft replaces the quoted thread with the supplied HTML (Graph’s replyCommentonly 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.EventSummarynow carries aJoinUrl, surfaced in text output as aJoin: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 sendandmail draft createfailed on the Microsoft provider withError: were unable to deserialize [ODataError]whenever--cc/--bccwere omitted (i.e. almost always). AssigningnulltoCcRecipients/BccRecipientsmarked 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.0cal createfix.)
[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 createaccepts the same flags asmail send(--to,--subject,--body,--cc,--bcc,--attach,--mailbox). The existingmail 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]). SetCOURIER_DEBUG=1to also dump the stack trace — makes generic SDK errors actionable.
Added
- macOS targets (
osx-x64,osx-arm64) added to defaultpublish.sh.
[1.2.0] — 2026-05-07
Added
mail replynow accepts--cc,--bcc, and--attach— matchesmail sendsemantics while preserving threading on both Gmail and Microsoftcal updateattendee 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,--rawfor VTT,-o jsonfor structured entries.--transcript-idto disambiguate when a meeting has multiple transcripts- Shared mailbox support —
--mailbox <email>on allmailsubcommands routes through the target mailbox (Microsoft only). Requires Exchange-level access plus admin consent forMail.ReadWrite.SharedandMail.Send.Shared - New Microsoft Graph scopes:
OnlineMeetings.Read,OnlineMeetingTranscript.Read.All,Mail.ReadWrite.Shared,Mail.Send.Shared(all require tenant admin consent)
Fixed
cal createon Microsoft — Graph rejected the create body with “Invalid format” becauseBody = nullwas explicitly serialized and the datetime format was second-resolution. Now omitsBodywhen no description is provided and uses 7-decimal datetime format- Gmail dates showed
0001-01-01 00:00—DateTimeOffset.TryParseon RFC 2822 Date headers fails under non-English locales. Now usesMessage.InternalDate(epoch ms) as the authoritative source with InvariantCulture parse fallback - Meeting transcript content fetch — bypassed Kiota for the content endpoint with a raw
HttpClientGET that sets a singleAccept: text/vttand a bearer token from MSAL
[1.1.1] — 2026-03-05
Fixed
chat listno longer fails on Microsoft accounts — removed unsupported$orderby=lastUpdatedDateTimequery parametermail attachmentnow saves files using the original filename (e.g.TicketFast.pdf) instead of the attachment ID, which could exceed filesystem limitsmail attachmenthandles 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 teamscommand replaced bycourier 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
--calendaroption 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 --calendaroption oncal create,cal update,cal delete, andcal quickto target a specific calendar by ID--reminder/--no-reminderoptions oncal createandcal 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 foundon 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
