| name | co-mail-and-drive |
| description | Read and send mail from the user's own Gmail or Outlook account, safely stage Gmail draft attachments, send from the agent's own address, manage Outlook contacts, and work with Google Drive files — with `co gmail`, `co outlook`, `co email`, and `co gdrive`. Use when the user asks about their inbox, an email or draft they want to prepare, an attachment, a contact, or a file in Drive. |
co gmail / co outlook / co email / co gdrive
The user's own mail and files, from the shell. One authorization, then plain commands.
Read the output, not just the exit code. All four surfaces — co gmail,
co outlook, co email and co gdrive — exit 1 when they fail (#1012 fixed
the co email exception). The output still carries the recovery step; read it.
First: which mailbox does the user mean?
| Command | Whose mailbox | Ask for it when |
|---|
co gmail | the user's personal Gmail | "my email", "my inbox", "reply to Bob" |
co outlook | the user's personal Outlook | same, on the Microsoft account |
co email | the agent's own address (*@mail.openonion.ai) | "send from the agent", "what did the agent receive" |
co gdrive | the user's Google Drive | "my files", "that doc" |
When both mail accounts are connected and the request is ambiguous, ask which one
rather than guessing. Sending from the wrong identity is not undoable.
Read mail
co gmail
co gmail inbox -n 25 -u
co gmail read 3
co gmail search "from:alice@example.com is:unread"
co gmail sent -n 20
co outlook takes the same shape, and adds download, scheduled, cancel, contact:
co outlook
co outlook inbox -n 25 -u
co outlook read 3
co outlook search "invoice" -n 20
co outlook sent -n 20
Gmail search takes full Gmail query syntax (from:, subject:, after:2026/07/01,
is:unread). Outlook search is plain text over subject and body.
co gmail read preserves unread state by default. co gmail read 3 --mark-read
marks it read only when the token carries gmail.modify; otherwise it prints
a reauthorization hint. Repeat what the output says, don't assume.
Send and reply
For Gmail attachments, or whenever a person should review the final message,
use the provider-native draft workflow. Only the last command can send, and it
always previews and asks for interactive confirmation:
| Intent | Command |
|---|
| List and number drafts | co gmail draft list |
| Create an unsent draft | co gmail draft create <to> <subject> <message> |
| Stage a local file | co gmail draft attach <draft> <path> |
| Stage a Drive file | co gmail draft attach <draft> <file> --drive |
| Append a Drive URL | co gmail draft attach <draft> <file> --drive --link |
| Remove a staged file | co gmail draft remove <draft> <attachment#> |
| Replace a staged file | co gmail draft replace <draft> <attachment#> <source> [--drive] |
| Inspect recipients, body, and manifest | co gmail draft preview <draft> |
| Preview, confirm, and send | co gmail draft send <draft> |
co gmail draft list
co gmail draft create bob@example.com "Subject" "Body text"
co gmail draft list
co gmail draft attach 1 report.pdf
co gdrive list
co gmail draft attach 1 3 --drive
co gmail draft attach 1 3 --drive --link
co gmail draft remove 1 2
co gmail draft replace 1 1 corrected.pdf
co gmail draft preview 1
co gmail draft send 1
draft create, attach, remove, replace, and preview never send. draft send has no --yes or other confirmation bypass. A declined confirmation
leaves the draft intact and exits 1. EOF or interruption at the confirmation
prompt does the same and prints the preview command again.
A Gmail draft number comes from the immediately preceding co gmail draft list, cached separately at ~/.co/gmail_last_drafts.json. Attachment numbers
come from the current draft preview. Drive file numbers still come from the
immediately preceding co gdrive listing. Re-list before acting rather than
carrying numbers across listings.
An empty draft listing clears its old numbers. After creating a draft, use the
ID printed by create, or list again before choosing a row number.
--drive attaches bytes without making a local copy. Native Docs, Sheets,
Slides, and Drawings are exported using the same formats as co gdrive get.
--drive --link appends the web URL but does not grant the recipient access or
change Drive sharing.
Each success and guarded failure prints a literal next command. Read it even
when output is piped; it is part of the CLI contract.
co gmail send bob@example.com "Subject" "Body text"
co gmail reply 3 "Sounds good, see you then."
co gmail send bob@example.com "Report" - < body.md
co gmail send bob@example.com "Invoice" "Attached." --cc a@x.com --attach invoice.pdf
--cc, --bcc and --attach/-a (repeatable) work on both co gmail send and
co outlook send. Attachments are checked before the send: a missing file or a set
over the size limit (Gmail 25MB, Outlook 3MB) exits 1 without sending.
Outlook additionally schedules:
co outlook send bob@example.com "Nudge" "Following up" --at +2h
co outlook reply 3 "On it" --at +30m
co outlook scheduled
co outlook cancel 1
Outlook can also save an email's attachments: co outlook download 3 --to ~/Downloads.
Gmail has no download command — there is no way to save a Gmail attachment from this CLI.
Never send on the user's behalf without showing them the exact text and final
attachment manifest first. Prefer co gmail draft; print its preview and wait
for a yes. Sending is not undoable — scheduling is, until it goes out.
Contacts (Outlook only)
co outlook contact add "Full Name" name@example.com
co outlook contact list -n 50
co outlook contact search yifei
Drive files
co gdrive
co gdrive list
co gdrive search report -n 50
co gdrive get 3 --to ~/Downloads
co gdrive put report.pdf --name "Q3 report.pdf"
co gdrive rm 3
The two gotchas that make you report something false
1. Numbers mean your last listing. read 3 / get 3 / draft preview 3
resolve against the
numbering of the listing you just printed, cached in ~/.co/gmail_last_inbox.json,
~/.co/gmail_last_drafts.json, ~/.co/outlook_last_inbox.json, and
~/.co/gdrive_last_list.json. List again and the numbers move. Two consequences:
- Never carry a number across two listings — re-list, then act.
- Gmail
inbox/search, Gmail draft list, and Drive list/search each
refresh their own numbering, including clearing it on an empty result. sent does
not: after co gmail sent, read 1 still opens row 1 of the older inbox listing.
- A number that isn't in the cache gets
No email #N in your last listing and exit
1 rather than a silently wrong email. Re-list and retry.
2. Piping changes the output — and you are always piping. In a terminal these
commands print a Rich table with truncated columns and a next-step tip. Piped, they
print the untruncated form with full IDs instead:
co gmail inbox -n 50 | grep "ID:"
co gdrive list -n 100 | cut -f4
co outlook contact list | cut -f2
Never parse a truncated table column; take IDs from the piped output. The piped
form keeps the "Read one with: co gmail read <#>" tip (#1011) — the row numbers
are still what read wants.
Two more, for Drive specifically:
- Drive search matches word prefixes, not substrings. On
HelloWorld, Hello
matches and World does not. Empty result = say the search found nothing, not
that the file doesn't exist.
- Google Docs/Sheets/Slides/Drawings are exported on download — to
.md, .csv
(first sheet only), .pdf and .pdf. Other Google-native types (folders, Forms)
have no export format and raise "it cannot be downloaded".
The agent's own address (co email)
co email
co email inbox -n 20 -u
co email inbox -n 1000 --offset 1000
co email read 41
co email send bob@example.com "Subject" "Body"
co email send bob@example.com "Subject" "Body" --from aaron@openonion.ai
co email addresses
co email sent -n 20 --to bob@example.com
co email sent read 12
Received inbox pages accept -n/--last from 1 through 1000. Use --offset to
skip newer rows and continue through older mail; for example, page through
offsets 0, 1000, 2000 until the command returns no rows.
It is a smaller surface than Gmail/Outlook, and the differences bite:
- No
reply, no search, no attachments, no scheduling. To answer a message,
send a new one with the subject you want.
read takes the id printed in the # column (a server id), not "row 3", and
currently finds it among the latest 1000 received.
- A failed send that is safe to retry prints the full retry command
(
co email send ... --idempotency-key <key>) — run it as printed so a send
that actually went out is not duplicated.
co email sent can answer "Sent mail is not available on this backend yet" —
that is the deployment, not your command.
-u/--unread is filtered locally after fetching -n emails, so
co email inbox -n 10 -u means "unread among the last 10", not "the last 10 unread".
Choosing the sender. The account can own several addresses. co email addresses
lists them (piped: address<TAB>default) and marks the default; --from on
co email send picks one. Sending as an address the account does not own is a
guarded failure: the server answers 403, nothing is sent, and the message ends with
See your addresses: co email addresses. An account with no owned addresses gets an
empty listing (exit 0) and the pointer co email name <name> to claim one.
Account admin: co email name aaron checks a custom address (--buy claims it,
from credits) and co email upgrade plus|pro raises the quota.
Exit codes and what to do about them
| exit | Meaning | What to do |
|---|
0, clean output | success — including legitimately empty listings | continue |
1 | guarded failure: account not connected, scope missing, unknown listing or attachment number, declined/rejected send, unowned --from address, attachment missing/too large | run the literal next command (co auth google, re-list/preview, correct the path) |
2 | usage error: unknown subcommand, missing or bad argument | fix the syntax; the error names the argument, --help lists the rest |
For Gmail and Drive, these are the concrete recovery routes:
| Result | Next command |
|---|
| Gmail listing succeeded | co gmail read <# from this listing> |
| Drive listing succeeded | co gdrive get <# from column 5 when piped> |
| Empty Gmail inbox | co gmail search <query> |
| Empty Gmail search | co gmail inbox |
| Empty Drive listing | co gdrive search <name prefix> |
| Empty Drive search | co gdrive list |
| Missing Google permission (exit 1) | co auth google |
| Gmail read/list failure (exit 1) | co gmail inbox |
| Gmail send/reply connection failure (exit 1, delivery uncertain) | co gmail sent |
| Drive connection or local I/O failure (exit 1) | co gdrive list |
| Missing upload path (exit 1) | co gdrive put <path to an existing file> |
| Missing Gmail read argument (exit 2) | co gmail read --help |
| Missing Drive get argument (exit 2) | co gdrive get --help |
A connection failure does not prove a write failed: inspect the provider state
before repeating a send, reply, upload, or draft creation. Provider error bodies
are omitted from CLI error messages. Outlook and agent-mail recovery behavior
is outside the Gmail/Drive audit.
The printed messages carry the current recovery step — trust them over this table.
When a command says the account is not connected:
❌ Google account not connected → co auth google
❌ Gmail permission missing → co auth google (re-consent)
❌ Gmail draft permission missing → co auth google (re-consent)
❌ Google Drive permission missing → co auth google (re-consent)
❌ Microsoft account not connected → co auth microsoft
❌ Microsoft <scope> permission missing → co auth microsoft
❌ No API key found (co email) → co auth
"Permission missing" on a connected account means the token predates a scope added
later. A refresh cannot widen scopes — only re-running co auth fixes it.
co auth opens a browser and needs a human to click through: tell the user to run
it themselves, do not try to drive that flow.
Done checklist