| name | gmail |
| description | Gmail operations using the googleapis NPM package. Use when sending, reading, searching, or organizing emails programmatically. Handles drafts, labels, threads, filters, and attachments through Google's official SDK. |
Gmail Reference (SDK)
# === Gmail Skill Environment Check ===
CRED_PATH="$HOME/.gmail-skill"
BLOCKED=""
# Detect package manager
if [ -f "yarn.lock" ]; then
PKG_MGR="yarn" PKG_ADD="yarn add" PKG_GLOBAL="yarn global add"
elif [ -f "pnpm-lock.yaml" ]; then
PKG_MGR="pnpm" PKG_ADD="pnpm add" PKG_GLOBAL="pnpm add -g"
else
PKG_MGR="npm" PKG_ADD="npm install" PKG_GLOBAL="npm install -g"
fi
# 1. Credential files check with progressive status
if [ ! -d "$CRED_PATH" ]; then
BLOCKED="yes"
echo "❌ BLOCKED: ~/.gmail-skill/ directory not found"
echo ""
echo "STOP. Do not attempt Gmail operations."
echo "Ask user: \"Gmail credentials aren't configured yet. Would you like me to help you set them up?\""
echo "If yes, read: @${CLAUDE_PLUGIN_ROOT}/skills/gmail/advanced/oauth-setup.md"
elif [ ! -f "$CRED_PATH/client_secret.json" ]; then
BLOCKED="yes"
echo "❌ BLOCKED: client_secret.json not found"
echo ""
echo "STOP. Do not attempt Gmail operations."
echo "User needs to download OAuth credentials from Google Cloud Console."
echo "Setup guide: @${CLAUDE_PLUGIN_ROOT}/skills/gmail/advanced/oauth-setup.md"
elif [ ! -f "$CRED_PATH/tokens.json" ]; then
BLOCKED="yes"
echo "❌ BLOCKED: tokens.json not found (client_secret.json ✓)"
echo ""
echo "STOP. Do not attempt Gmail operations."
echo "User needs to complete the OAuth authorization flow."
echo "Setup guide: @${CLAUDE_PLUGIN_ROOT}/skills/gmail/advanced/oauth-setup.md"
else
# 2. Validate JSON and check token status
if node -e "JSON.parse(require('fs').readFileSync('$CRED_PATH/tokens.json'))" 2>/dev/null; then
EXPIRY=$(node -p "JSON.parse(require('fs').readFileSync('$CRED_PATH/tokens.json')).expiry_date || 0" 2>/dev/null)
HAS_REFRESH=$(node -p "JSON.parse(require('fs').readFileSync('$CRED_PATH/tokens.json')).refresh_token ? 'yes' : 'no'" 2>/dev/null)
NOW_MS=$(($(date +%s) * 1000))
# Token validity
if [ -n "$EXPIRY" ] && [ "$EXPIRY" != "0" ] && [ "$EXPIRY" -gt "$NOW_MS" ] 2>/dev/null; then
REMAINING_MIN=$(((EXPIRY - NOW_MS) / 60000))
echo "✓ Gmail ready (token valid for ${REMAINING_MIN}m)"
else
echo "✓ Gmail ready (token expired, will auto-refresh)"
fi
# Refresh token warning (non-blocking)
[ "$HAS_REFRESH" != "yes" ] && echo " ⚠️ No refresh_token - re-auth needed when token expires"
# Token age check (non-blocking warning)
if stat --version 2>/dev/null | grep -q GNU; then
FILE_MTIME=$(stat -c %Y "$CRED_PATH/tokens.json")
else
FILE_MTIME=$(stat -f %m "$CRED_PATH/tokens.json" 2>/dev/null || stat -c %Y "$CRED_PATH/tokens.json")
fi
AGE_DAYS=$(( ($(date +%s) - FILE_MTIME) / 86400 ))
[ $AGE_DAYS -gt 150 ] && echo " ⚠️ Token ${AGE_DAYS} days old - refresh token may expire soon"
else
BLOCKED="yes"
echo "❌ BLOCKED: tokens.json is corrupted (invalid JSON)"
echo ""
echo "STOP. Do not attempt Gmail operations."
echo "User needs to re-run the authorization flow."
echo "Setup guide: @${CLAUDE_PLUGIN_ROOT}/skills/gmail/advanced/oauth-setup.md"
fi
fi
# 3. Runtime check
if command -v tsx >/dev/null 2>&1; then
echo "✓ tsx $(tsx --version 2>&1 | head -1)"
else
BLOCKED="yes"
echo "❌ BLOCKED: tsx not installed"
echo " Cannot execute scripts. Install with: $PKG_GLOBAL tsx"
fi
# 4. Package checks
if [ -d "node_modules/googleapis" ]; then
VER=$(node -p "require('googleapis/package.json').version" 2>/dev/null || echo "?")
echo "✓ googleapis@$VER"
elif command -v $PKG_MGR >/dev/null 2>&1 && $PKG_MGR list googleapis 2>/dev/null | grep -q googleapis; then
echo "✓ googleapis ($PKG_MGR)"
else
BLOCKED="yes"
echo "❌ BLOCKED: googleapis not installed"
echo " Cannot execute scripts. Install with: $PKG_ADD googleapis"
fi
# 5. Optional: nodemailer for attachments (non-blocking)
if [ -d "node_modules/nodemailer" ]; then
VER=$(node -p "require('nodemailer/package.json').version" 2>/dev/null || echo "?")
echo "✓ nodemailer@$VER (attachments supported)"
fi
# Final status (must exit 0 to not fail skill load)
if [ -z "$BLOCKED" ]; then
echo ""
echo "Ready to execute Gmail operations."
fi
IMPORTANT: Use tsx << 'EOF' ... EOF heredoc syntax for inline execution with top-level await. The tsx -e flag does NOT support top-level await.
⚠️ Cleanup Requirements
IMPORTANT: All scripts use inline heredoc execution - no script files are created. However, be mindful of:
| Item | Created By | Cleanup |
|---|
| Downloaded attachments | Attachment download scripts | Delete after processing: rm -f attachment.* |
| Temporary files | Your custom scripts | Delete when done |
Best Practice: Process data in memory when possible, avoid saving to disk unless necessary.
Critical Gotchas
| Issue | Reality | Fix |
|---|
| Base64 encoding | Gmail uses base64url (RFC 4648), not standard base64 | Replace +->-, /->_, remove = padding |
| Token expiration | Access tokens expire in ~1 hour | googleapis handles auto-refresh; catch 401 errors |
| Refresh token validity | Expires if unused 6 months, password change, or >50 tokens | Re-authorize via OAuth flow when refresh fails |
| MIME parsing | Messages are nested multipart structures | Recursive traversal required for body extraction |
| Message immutability | Sent messages cannot be modified after sending | Delete and resend, or use drafts for edits |
| Rate limits | 250 quota units/user/second, varying costs per operation | Use batch operations, implement exponential backoff |
| Attachment size | 5MB limit for raw upload, 35MB with resumable | Use resumable upload for files >5MB |
Quick Lookups
| Topic | When to Use | Reference |
|---|
| OAuth credentials setup | First-time setup, re-authorization | advanced/oauth-setup.md |
| Send, read, search, delete | Core message operations | advanced/messages.md |
| Draft lifecycle, thread navigation | Drafts and conversation threads | advanced/drafts-threads.md |
| Label CRUD, filter templates | Organization and automation | advanced/labels-filters.md |
| Vacation responder, forwarding | Account settings | advanced/settings.md |
Setup Pattern
Every script needs OAuth client initialization with token refresh handling:
tsx << 'EOF'
import { google } from "googleapis";
import { readFileSync, writeFileSync } from "fs";
import { homedir } from "os";
import { join } from "path";
const credPath = join(homedir(), ".gmail-skill");
const credentials = JSON.parse(readFileSync(join(credPath, "client_secret.json"), "utf8"));
const tokens = JSON.parse(readFileSync(join(credPath, "tokens.json"), "utf8"));
const oauth2Client = new google.auth.OAuth2(
credentials.installed.client_id,
credentials.installed.client_secret,
credentials.installed.redirect_uris[0]
);
oauth2Client.setCredentials(tokens);
// Handle token refresh - save new tokens when refreshed
oauth2Client.on("tokens", (newTokens) => {
const updatedTokens = { ...tokens, ...newTokens };
writeFileSync(join(credPath, "tokens.json"), JSON.stringify(updatedTokens, null, 2));
});
const gmail = google.gmail({ version: "v1", auth: oauth2Client });
// ========== YOUR CODE HERE ==========
EOF
Essential Patterns
Send a Simple Email
TO="recipient@example.com" SUBJECT="Hello" BODY="Message body here" tsx << 'EOF'
import { google } from "googleapis";
import { readFileSync, writeFileSync } from "fs";
import { homedir } from "os";
import { join } from "path";
const credPath = join(homedir(), ".gmail-skill");
const credentials = JSON.parse(readFileSync(join(credPath, "client_secret.json"), "utf8"));
const tokens = JSON.parse(readFileSync(join(credPath, "tokens.json"), "utf8"));
const oauth2Client = new google.auth.OAuth2(
credentials.installed.client_id,
credentials.installed.client_secret,
credentials.installed.redirect_uris[0]
);
oauth2Client.setCredentials(tokens);
oauth2Client.on("tokens", (newTokens) => {
writeFileSync(join(credPath, "tokens.json"), JSON.stringify({ ...tokens, ...newTokens }, null, 2));
});
const gmail = google.gmail({ version: "v1", auth: oauth2Client });
const { TO, SUBJECT, BODY } = process.env;
const message = [
`To: ${TO}`,
`Subject: ${SUBJECT}`,
"Content-Type: text/plain; charset=utf-8",
"",
BODY
].join("\r\n");
// Gmail requires base64url encoding
const encodedMessage = Buffer.from(message)
.toString("base64")
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=+$/, "");
const res = await gmail.users.messages.send({
userId: "me",
requestBody: { raw: encodedMessage }
});
console.log("Email sent! Message ID:", res.data.id);
EOF
Read an Email
MESSAGE_ID="..." tsx << 'EOF'
import { google } from "googleapis";
import { readFileSync } from "fs";
import { homedir } from "os";
import { join } from "path";
const credPath = join(homedir(), ".gmail-skill");
const credentials = JSON.parse(readFileSync(join(credPath, "client_secret.json"), "utf8"));
const tokens = JSON.parse(readFileSync(join(credPath, "tokens.json"), "utf8"));
const oauth2Client = new google.auth.OAuth2(
credentials.installed.client_id,
credentials.installed.client_secret,
credentials.installed.redirect_uris[0]
);
oauth2Client.setCredentials(tokens);
const gmail = google.gmail({ version: "v1", auth: oauth2Client });
const res = await gmail.users.messages.get({
userId: "me",
id: process.env.MESSAGE_ID,
format: "full"
});
const headers = res.data.payload?.headers || [];
const getHeader = (name: string) => headers.find(h => h.name?.toLowerCase() === name.toLowerCase())?.value;
console.log("From:", getHeader("from"));
console.log("To:", getHeader("to"));
console.log("Subject:", getHeader("subject"));
console.log("Date:", getHeader("date"));
// Extract body - handles both simple and multipart messages
function extractBody(payload: any): string {
if (payload.body?.data) {
return Buffer.from(payload.body.data, "base64url").toString("utf8");
}
if (payload.parts) {
for (const part of payload.parts) {
if (part.mimeType === "text/plain" && part.body?.data) {
return Buffer.from(part.body.data, "base64url").toString("utf8");
}
if (part.parts) {
const nested = extractBody(part);
if (nested) return nested;
}
}
}
return "";
}
console.log("\nBody:\n", extractBody(res.data.payload));
EOF
List Recent Emails
MAX_RESULTS="10" tsx << 'EOF'
import { google } from "googleapis";
import { readFileSync } from "fs";
import { homedir } from "os";
import { join } from "path";
const credPath = join(homedir(), ".gmail-skill");
const credentials = JSON.parse(readFileSync(join(credPath, "client_secret.json"), "utf8"));
const tokens = JSON.parse(readFileSync(join(credPath, "tokens.json"), "utf8"));
const oauth2Client = new google.auth.OAuth2(
credentials.installed.client_id,
credentials.installed.client_secret,
credentials.installed.redirect_uris[0]
);
oauth2Client.setCredentials(tokens);
const gmail = google.gmail({ version: "v1", auth: oauth2Client });
const res = await gmail.users.messages.list({
userId: "me",
maxResults: parseInt(process.env.MAX_RESULTS || "10"),
labelIds: ["INBOX"]
});
const messages = res.data.messages || [];
console.log(`Found ${messages.length} messages:\n`);
for (const msg of messages) {
const detail = await gmail.users.messages.get({
userId: "me",
id: msg.id!,
format: "metadata",
metadataHeaders: ["From", "Subject", "Date"]
});
const headers = detail.data.payload?.headers || [];
const getHeader = (name: string) => headers.find(h => h.name === name)?.value || "";
console.log(`ID: ${msg.id}`);
console.log(` From: ${getHeader("From")}`);
console.log(` Subject: ${getHeader("Subject")}`);
console.log(` Date: ${getHeader("Date")}\n`);
}
EOF
Search Emails
QUERY="from:example@gmail.com after:2024/01/01 has:attachment" tsx << 'EOF'
import { google } from "googleapis";
import { readFileSync } from "fs";
import { homedir } from "os";
import { join } from "path";
const credPath = join(homedir(), ".gmail-skill");
const credentials = JSON.parse(readFileSync(join(credPath, "client_secret.json"), "utf8"));
const tokens = JSON.parse(readFileSync(join(credPath, "tokens.json"), "utf8"));
const oauth2Client = new google.auth.OAuth2(
credentials.installed.client_id,
credentials.installed.client_secret,
credentials.installed.redirect_uris[0]
);
oauth2Client.setCredentials(tokens);
const gmail = google.gmail({ version: "v1", auth: oauth2Client });
const res = await gmail.users.messages.list({
userId: "me",
q: process.env.QUERY,
maxResults: 20
});
const messages = res.data.messages || [];
console.log(`Found ${messages.length} messages matching: ${process.env.QUERY}\n`);
for (const msg of messages.slice(0, 10)) {
const detail = await gmail.users.messages.get({
userId: "me",
id: msg.id!,
format: "metadata",
metadataHeaders: ["From", "Subject", "Date"]
});
const headers = detail.data.payload?.headers || [];
const getHeader = (name: string) => headers.find(h => h.name === name)?.value || "";
console.log(`${msg.id}: ${getHeader("Subject")} (from ${getHeader("From")})`);
}
EOF
Delete or Trash an Email
MESSAGE_ID="..." tsx << 'EOF'
import { google } from "googleapis";
import { readFileSync } from "fs";
import { homedir } from "os";
import { join } from "path";
const credPath = join(homedir(), ".gmail-skill");
const credentials = JSON.parse(readFileSync(join(credPath, "client_secret.json"), "utf8"));
const tokens = JSON.parse(readFileSync(join(credPath, "tokens.json"), "utf8"));
const oauth2Client = new google.auth.OAuth2(
credentials.installed.client_id,
credentials.installed.client_secret,
credentials.installed.redirect_uris[0]
);
oauth2Client.setCredentials(tokens);
const gmail = google.gmail({ version: "v1", auth: oauth2Client });
await gmail.users.messages.trash({ userId: "me", id: process.env.MESSAGE_ID });
console.log("Message moved to trash:", process.env.MESSAGE_ID);
EOF
MESSAGE_ID="..." tsx << 'EOF'
import { google } from "googleapis";
import { readFileSync } from "fs";
import { homedir } from "os";
import { join } from "path";
const credPath = join(homedir(), ".gmail-skill");
const credentials = JSON.parse(readFileSync(join(credPath, "client_secret.json"), "utf8"));
const tokens = JSON.parse(readFileSync(join(credPath, "tokens.json"), "utf8"));
const oauth2Client = new google.auth.OAuth2(
credentials.installed.client_id,
credentials.installed.client_secret,
credentials.installed.redirect_uris[0]
);
oauth2Client.setCredentials(tokens);
const gmail = google.gmail({ version: "v1", auth: oauth2Client });
await gmail.users.messages.delete({ userId: "me", id: process.env.MESSAGE_ID });
console.log("Message permanently deleted:", process.env.MESSAGE_ID);
EOF
Gmail Query Syntax Reference
Common search operators for the q parameter:
| Operator | Example | Description |
|---|
from: | from:user@example.com | Messages from sender |
to: | to:me | Messages to recipient |
subject: | subject:meeting | Subject contains word |
has:attachment | has:attachment | Has any attachment |
filename: | filename:pdf | Attachment filename |
after: | after:2024/01/01 | After date (YYYY/MM/DD) |
before: | before:2024/12/31 | Before date |
is:unread | is:unread | Unread messages |
is:starred | is:starred | Starred messages |
label: | label:work | Has specific label |
larger: | larger:5M | Larger than size |
in: | in:trash | In specific folder |
Combine with spaces (AND) or OR:
from:alice subject:project - Both conditions
from:alice OR from:bob - Either condition
{from:alice from:bob} - Alternative OR syntax
Base64url Helper
Gmail API uses base64url encoding (RFC 4648). Use this pattern:
function encodeBase64url(str: string): string {
return Buffer.from(str)
.toString("base64")
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=+$/, "");
}
function decodeBase64url(encoded: string): string {
return Buffer.from(encoded, "base64url").toString("utf8");
}
Error Handling
Handle API errors in your scripts:
try {
const res = await gmail.users.messages.list({ userId: "me" });
} catch (error: any) {
if (error.code === 401) {
console.error("Authentication failed - re-run authorization flow");
} else if (error.code === 403) {
console.error("Permission denied - check OAuth scopes");
} else if (error.code === 429) {
console.error("Rate limited - wait and retry, or use batch operations");
} else {
console.error("Error:", error.message);
}
}
When to Ask the User
| Situation | Question |
|---|
| Environment check shows setup warnings | "Gmail credentials need attention. Follow the steps shown above." |
| 401 error during operation | "Your Gmail authorization has expired. Please re-authorize." |
| Multiple recipients unclear | "Who should I send this email to?" |
| Delete vs trash | "Should I move this to trash (recoverable) or permanently delete it?" |
| Search returns many results | "Found many emails. Can you narrow the search criteria?" |
Advanced Topics
| Topic | When to Use | Reference |
|---|
| OAuth setup guide | First-time credential configuration | advanced/oauth-setup.md |
| Message operations | Send with attachments, batch operations | advanced/messages.md |
| Drafts and threads | Draft lifecycle, reply-in-thread | advanced/drafts-threads.md |
| Labels and filters | Organization, automation rules | advanced/labels-filters.md |
| Account settings | Vacation responder, forwarding | advanced/settings.md |
Official Documentation