Google Workspace CLI toolbox: one crate, one OAuth client, one profile system, shipped as two binaries.
gmail: list, read, send, labels, attachments, aliasesgcal: create events (with Google Meet), edit, list, delete
cargo install --path .- Create a Google Cloud OAuth client (Desktop app) and enable the Gmail and Calendar APIs in its project.
- Run
gmail --profile <name> auth login. The first login prompts forclient_id,client_secret, andredirect_uriand saves them to the sharedconfig.json; later profiles reuse them. - Approve access in the browser. The token lands in
tokens/<name>.jsonand the profile inprofiles/<name>.json.
To add another account, run step 2 with a new profile name. Check a login with gmail auth status; sign out and revoke with gmail auth logout.
Config lives in ~/Library/Application Support/gmail/ on macOS:
A profile may set client_id, client_secret, or redirect_uri to override the shared client, e.g. for a Workspace org that only allows its own internal client. Fields fall back independently, so override client_id and client_secret together.
gmail and gcal share profiles and tokens. Each command resolves one profile in this order:
--profile <name>GMAIL_PROFILEdefault_profileinconfig.json(set withgmail profile use <name>)- the only profile, if there is one
- a profile named
default
If several profiles exist and none is selected, mailbox commands error until you set a default.
gmail [--profile <name>] [--json]
auth login | status | logout
profile list | use <name> | show
signature show | set <text> | set-file <path> | clear
list [--inbox] [--limit <n>] [--q <query>]
get <id>
send [--to ...] [--cc ...] [--bcc ...] [--subject ...] [--from <alias>] [--reply <id>]
[--attach <path> ...] [--signature <text> | --no-signature]
(--body ... | --body-file ... | --draft-file ... | --stdin)
label ls | add <id> <label...> | rm <id> <label...>
attachments ls <id> | get <id> [--out <dir>] [--index <n> | --name <file>]
aliases ls
getprints the full decoded body (text/plain, else stripped HTML) and lists attachments.sendrenders the body as Markdown to HTML and setsFromwithsender_name(or the Google profile name).send --frommust be a verified send-as alias fromaliases ls; anything else errors rather than silently sending from the primary.send_frommakes an alias the default.send --reply <id>keeps the thread and reply headers.- The profile's
signature(Markdown, line breaks preserved) is appended below the body;--signatureoverrides it for one send and--no-signaturesuppresses it.
gcal [--profile <name>] [--json]
add --title <t> --start <datetime> [--end <datetime> | --duration <mins, default 30>]
[--attendees a@x.com,b@y.com] [--meet] [--location <text>] [--notes <text>] [--calendar <id>]
edit <event-id> [--title <t>] [--start <datetime>] [--end <datetime> | --duration <mins>]
[--attendees ...] [--location <text>] [--notes <text>] [--calendar <id>]
list [--today | --tomorrow | --week | --from <d> --to <d>] [--limit <n>] [--calendar <id>]
rm <event-id> [--calendar <id>]
- Datetimes accept RFC 3339,
YYYY-MM-DD HH:MM, andtoday HH:MM/tomorrow HH:MM, in local time. Events go on the primary calendar unless--calendaris set. --meetattaches a Google Meet link. Attendees get Google invites onadd, update emails onedit, and cancellations onrm.editpatches in place, so the event id and Meet link survive.--startalone shifts the event and keeps its duration;--attendeesreplaces the list. Recurring events are not supported.- Profiles authorized before calendar support need a one-time
gmail auth loginto grant thecalendar.eventsscope.
$ gcal add --title "Weekly sync" --start "tomorrow 10:00" --attendees alex@example.com --meet
event created: Weekly sync
id: k2j4...
when: Sun 2026.07.26 10:00–10:30
meet: https://meet.google.com/abc-defg-hijSee docs/architecture.md for module layout and data flow.