@xniffing/pi-gmail-assistant
v0.1.4
Published
Gmail extension for Pi with OAuth setup, inbox tools, attachment downloads, and safe send confirmation.
Downloads
559
Maintainers
Readme
Gmail Pi extension
A Pi extension for Gmail OAuth setup plus safe Gmail workflows for listing inbox mail, searching Gmail, reading a selected message, inspecting a message's attachments, downloading a selected attachment to disk, and sending outbound email with plain-text or HTML bodies plus optional file attachments with explicit user confirmation.
Install as a Pi package
After publishing to npm, add it to Pi through settings.json:
{
"packages": [
"npm:@xniffing/[email protected]"
]
}Then reload Pi:
/reloadYou can also test the package locally before publishing:
cd ~/Projects/pi-gmail-assistant
npm install
npm test
npm run build
npm pack --dry-runWhat this extension does
- registers a
/gmail-authcommand for first-time setup - generates a Google OAuth consent URL for Gmail scopes
- exchanges a Google OAuth redirect URL for Google tokens after validating
stateand PKCE - stores OAuth credentials and tokens outside git-tracked files
- refreshes expired Gmail access tokens automatically when a stored refresh token is available
- lets Pi list recent inbox mail with compact previews
- lets Pi search Gmail with normal Gmail query syntax
- lets Pi read one message by id without dumping raw Gmail API payloads
- lets Pi list a message's attachments with concise metadata instead of raw MIME structure
- lets Pi download a selected attachment into a safe project-local path by default
- lets Pi send Gmail messages with plain-text or HTML bodies and optional file attachments only after the operator explicitly confirms the recipient, subject, body preview, and attachments
- avoids printing client secrets or refresh tokens into normal chat output
Required Google Cloud setup
- Create or select a Google Cloud project.
- Enable the Gmail API for that project.
- Create an OAuth client ID in APIs & Services → Credentials.
- Add at least one redirect URI. The simplest option is a loopback URI such as
http://127.0.0.1. - In OAuth consent screen, prefer Production over Testing for a more stable long-lived setup.
- Download the OAuth client JSON.
Required Gmail scopes
The extension currently requests:
https://www.googleapis.com/auth/gmail.readonlyhttps://www.googleapis.com/auth/gmail.send
Minimum scope for outbound send is gmail.send. Inbox listing, search, reads, and attachment inspection use gmail.readonly so the extension no longer asks for broader mailbox-modification access.
HTML email and file attachments are sent through the same Gmail send scope; no additional Gmail OAuth scope is required.
If you added credentials before send support existed, rerun /gmail-auth exchange so the stored local tokens include the Gmail send scope.
If you authenticated with an older version that requested gmail.modify, rerun /gmail-auth exchange once so the stored tokens reflect the reduced gmail.readonly scope set.
Local credential and token storage
Secrets are stored outside the repository under your home directory:
- shared OAuth client credentials:
~/.config/automation/gmail/google-oauth-client.json - active account marker:
~/.config/automation/gmail/active-account.json - per-account tokens:
~/.config/automation/gmail/accounts/<email-slug>/gmail-tokens.json
Tokens are now scoped to the connected Gmail account instead of the current project path. That means changing projects, folders, or worktrees should no longer make the extension look unauthenticated once an account has been connected.
Files are written with private permissions where possible. Do not paste OAuth client JSON or token JSON into repository files.
Auth prerequisite
Before Pi can read or send Gmail, complete the local auth flow introduced in AU-001:
/reload
/gmail-auth init
/gmail-auth start
/gmail-auth exchange
/gmail-auth statusIf tokens are missing, revoked, missing the send scope, or cannot be refreshed automatically, Gmail tools will tell you to rerun /gmail-auth exchange.
For the hardened OAuth flow, run /gmail-auth start first and paste the full redirect URL back into /gmail-auth exchange so the extension can verify the returned state value and submit the PKCE verifier.
When a refresh token is stored locally, the extension now attempts to refresh expired access tokens automatically before asking you to re-authenticate.
If you previously authenticated with an older project-scoped version of the extension, rerun /gmail-auth exchange once so the active account can be detected and saved into the new account-scoped token layout.
Inbox workflows
1. List recent inbox messages
Ask Pi to use gmail_list_inbox_messages when you want recent inbox items.
Example prompts:
List my latest Gmail inbox messages.
Show the 5 newest inbox emails.
List inbox mail from the last week about invoices.The tool combines in:inbox with an optional extra Gmail query, then returns compact previews with:
- Gmail message id
- sender
- subject
- date
- short snippet
2. Search Gmail
Ask Pi to use gmail_search_messages when you want Gmail query syntax.
Example prompts:
Search Gmail for from:bob newer_than:7d
Find unread Gmail messages with has:attachment label:important
Search for receipts from March in GmailThe tool accepts standard Gmail query fragments such as from:, subject:, has:attachment, label:, is:unread, and date filters.
3. Read one message
After listing or searching, ask Pi to read a specific Gmail message id with gmail_read_message.
Example prompts:
Read Gmail message id 18c123abc456def
Open the message with id 18c123abc456def
Show me the body of the email about quarterly planningThe read tool returns normalized fields instead of the raw Gmail API response:
- subject
- message id
- from / to / cc
- date
- labels
- snippet
- extracted body text
Attachment workflows
1. Check what is attached to a message
After listing, searching, or reading a message, ask Pi to use gmail_list_message_attachments with the Gmail message id.
Example prompts:
Show me the attachments for Gmail message id 18c123abc456def
What files are attached to that finance email?
List the attachments on the message I just opened before downloading anythingThe attachment list stays concise and returns one entry per attachment with:
- attachment id
- filename
- mime type
- size
- whether Gmail marked it as inline or downloadable
Recommended workflow:
- Use
gmail_list_inbox_messages,gmail_search_messages, orgmail_read_messageto identify the message. - Use
gmail_list_message_attachmentsto confirm the available attachment ids and filenames. - Download only the exact attachment id you want.
2. Download one attachment safely
Use gmail_download_attachment with a Gmail messageId plus the returned attachmentId.
Example prompts:
Download attachment att_123 from Gmail message 18c123abc456def
Save the PDF attachment from that finance email
Download attachment att_123 to downloads/april-report.pdfWhen the download succeeds, Pi returns:
- saved path
- filename
- mime type
- byte size
- inline status
- whether an existing file was overwritten
Default download directory
If you omit savePath, the extension writes the file under a project-local directory:
- default directory:
<current-project>/.gmail-attachments/
This keeps attachment downloads inside the active project checkout instead of writing into arbitrary filesystem locations by default.
Save path and overwrite behavior
Version 1 keeps writes intentionally narrow:
savePathis optional- when provided, it must be a relative path that stays inside the current project
- paths that escape the project, such as
../outside/file.pdf, are rejected - directory-only paths are rejected; include the destination filename
- existing files are not overwritten unless
overwrite: trueis passed explicitly
Current attachment limitations
Attachment support is intentionally scoped for safe v1 behavior:
- no inline preview rendering
- no PDF text extraction
- no OCR or image analysis during download
- no automatic attachment selection; Pi should list attachments first
- downloads are single-attachment operations by
messageIdplusattachmentId - Gmail body reads still focus on readable message text, not raw MIME dumps
Outbound send workflow
Tool name and schema
Pi sends outbound mail through gmail_send_email.
Required fields:
to— one email address, a comma-separated string, or an array of email addressessubject— a single-line subject- one of
body,htmlBody, or both
Optional fields:
cc— one or more CC recipientsbcc— one or more BCC recipientsreplyTo— a single reply-to addressattachments— project-local file attachments withpath, plus optionalfilenameandcontentType
Example structured tool shape:
{
"to": ["[email protected]"],
"cc": ["[email protected]"],
"subject": "Weekly update",
"body": "Finished the release checklist and sent the build to QA.",
"htmlBody": "<p>Finished the <strong>release checklist</strong> and sent the build to QA.</p>",
"attachments": [
{ "path": "docs/release-notes.md", "filename": "release-notes.md" }
],
"replyTo": "[email protected]"
}Confirmation behavior
gmail_send_email does not call the Gmail API immediately. It first opens an explicit confirmation dialog that shows:
- primary recipients (
To) - optional
Cc,Bcc, andReply-To - subject
- whether the email is plain-text or HTML
- concise body preview
- attachment names and sizes
The operator must approve that dialog before the Gmail API call is made. If the operator cancels, the tool returns a cancellation result and nothing is sent.
Current send limitations
Version 1 still keeps outbound mail relatively narrow and safe:
- supports plain-text and HTML bodies
- supports project-local file attachments
- no explicit thread/reply support
- no background or bootstrap sends outside the tool flow
- interactive confirmation required before every send
- attachment content must come from project-local files, not remote URLs
- sensitive local paths such as
~/.ssh/and the Gmail token store are blocked from attachment use
Safe usage examples
Use direct prompts when you want Pi to send a message:
Send an email to [email protected] with subject "Weekly update" and body "Finished the release checklist and sent the build to QA."
Send a plain-text Gmail email to [email protected] and cc [email protected] about the April invoice.
Draft the email content first, then send it only after I confirm the final wording.Recommended operator workflow:
- Ask Pi to draft or refine the email text in chat.
- Ask Pi to call
gmail_send_emailwith the final recipients and subject. - Review the confirmation dialog carefully.
- Approve only if the recipient list, subject, and preview all match your intent.
Normalization and truncation behavior
The extension intentionally keeps output concise for conversation use:
- list and search results show compact summaries, not full API payloads
- snippets are normalized to single readable preview lines
- message bodies prefer
text/plainparts when available - HTML-only bodies are stripped to readable text when possible
- if no body text can be extracted, the snippet is used as a fallback
- long message bodies are truncated to a preview instead of dumping the full raw content
- send confirmation uses a concise body preview instead of the entire draft
- truncated body output is labeled with the displayed and original character counts
Example end-to-end workflow
/reload
/gmail-auth status
List my 5 newest inbox emails.
Read Gmail message id 18c123abc456def
Search Gmail for from:finance newer_than:30d
Draft a reply for Alex about tomorrow's meeting.
Send an email to [email protected] with subject "Tomorrow's meeting" and body "Looking forward to it. Agenda attached separately."Publishing checklist
Before npm publish, make sure you have:
- reviewed
package.jsonname/version/license fields - confirmed no credentials or token files are inside the package directory
- run
npm test - run
npm run build - run
npm pack --dry-run - logged into npm with
npm login
Publish with:
cd ~/Projects/pi-gmail-assistant
npm publish --access publicOAuth behavior for long-lived local auth
The extension already requests the recommended Google OAuth parameters for durable local auth during the first authorization flow:
access_type=offlineprompt=consentinclude_granted_scopes=truestate- PKCE with
code_challenge_method=S256
These parameters are configured in oauth.ts inside buildGoogleConsentUrl(...).
What they do:
access_type=offlineasks Google to return a refresh token so the extension can obtain new short-lived access tokens later.prompt=consentforces the Google consent screen, which helps ensure Google returns a refresh token on the initial authorization.include_granted_scopes=truehelps incremental authorization reuse already granted scopes.stateprotects the local consent flow against redirect mixups and login-CSRF style failures.- PKCE binds the returned authorization code to the locally generated verifier before token exchange.
Important notes:
- Google access tokens are still short-lived and typically expire quickly.
- The extension now uses the stored refresh token to renew access automatically.
- If Google does not issue a refresh token, or if the refresh token is revoked later, you must rerun
/gmail-auth exchange. - If you change scopes, it is safest to rerun
/gmail-auth exchangeand approve the updated consent screen again.
Security expectations
- Never commit Google OAuth client JSON or Gmail token files.
- Never paste refresh tokens into normal chat messages.
- Keep credentials in the external config path shown by
/gmail-auth status. - Review every send confirmation dialog before approving it.
- If you rotate or revoke credentials in Google Cloud, rerun
/gmail-auth initand/gmail-auth exchange. - See
SECURITY.mdfor the current token-storage decision, local-risk model, and rotation/revocation steps.
Current capabilities
This extension now covers:
- OAuth setup and token storage
- inbox listing
- Gmail search
- normalized message reads
- attachment listing
- safe attachment downloads into project-local paths
- safe plain-text outbound email sending with explicit confirmation
