From gmail
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.
How this skill is triggered — by the user, by Claude, or both
Slash command
/gmail:gmailThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
```!
# === 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.
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.
| 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 |
| 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 |
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
TO="[email protected]" 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
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
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
# Gmail search query syntax: https://support.google.com/mail/answer/7190
QUERY="from:[email protected] 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
# Trash (recoverable)
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
# Permanent delete (irreversible!)
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
Common search operators for the q parameter:
| Operator | Example | Description |
|---|---|---|
from: | from:[email protected] | 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 conditionsfrom:alice OR from:bob - Either condition{from:alice from:bob} - Alternative OR syntaxGmail API uses base64url encoding (RFC 4648). Use this pattern:
// Encode for sending
function encodeBase64url(str: string): string {
return Buffer.from(str)
.toString("base64")
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=+$/, "");
}
// Decode from API response
function decodeBase64url(encoded: string): string {
return Buffer.from(encoded, "base64url").toString("utf8");
}
Handle API errors in your scripts:
try {
const res = await gmail.users.messages.list({ userId: "me" });
// ... process response
} 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);
}
}
| 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?" |
| 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 |
Creates, edits, and optimizes skills for Claude Code, including drafting, evaluating with test prompts, iterating on performance, and improving skill descriptions for better triggering accuracy.
npx claudepluginhub goodfoot-io/marketplace --plugin gmail