Gmail Connector Example
A complete OAuth connector for Gmail that demonstrates the full connector pattern.
Overview
- Type: Connector
- OAuth Provider: Google
- Capabilities: Search, Read, Send, Archive, Labels, Trash
Full Code
js
/**
* Gmail Connector - Search, read, send, and manage emails
*
* Demonstrates:
* - OAuth authentication via context.oauth
* - Making authenticated API requests
* - Multi-action tool pattern
* - Handling Gmail API responses
*/
const GMAIL_API = 'https://gmail.googleapis.com/gmail/v1/users/me';
// Helper: Decode base64url (used by Gmail)
function decodeBase64Url(data) {
const base64 = data.replace(/-/g, '+').replace(/_/g, '/');
return atob(base64);
}
// Helper: Encode to base64url (for sending)
function encodeBase64Url(str) {
return btoa(str)
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/, '');
}
// Helper: Get email header value
function getHeader(headers, name) {
const header = headers.find(h => h.name.toLowerCase() === name.toLowerCase());
return header?.value || '';
}
// Helper: Extract email body from payload
function extractBody(payload) {
if (!payload) return { text: '' };
let text = '';
let html = '';
if (payload.body?.data) {
text = decodeBase64Url(payload.body.data);
}
if (payload.parts) {
for (const part of payload.parts) {
if (part.mimeType === 'text/plain' && part.body?.data) {
text = decodeBase64Url(part.body.data);
} else if (part.mimeType === 'text/html' && part.body?.data) {
html = decodeBase64Url(part.body.data);
}
}
}
return { text, html };
}
async function handler(input, context) {
const { action, ...params } = input;
// Check OAuth connection
const isConnected = await context.oauth.isConnected();
if (!isConnected) {
return {
error: true,
message: 'Gmail not connected. Please connect your Google account.',
requiresAuth: true
};
}
try {
switch (action) {
case 'search':
return await searchEmails(params, context);
case 'read':
return await readEmail(params, context);
case 'send':
return await sendEmail(params, context);
case 'archive':
return await archiveEmail(params, context);
case 'listLabels':
return await listLabels(context);
case 'trash':
return await trashEmail(params, context);
default:
return {
error: true,
message: `Unknown action: ${action}. Valid: search, read, send, archive, listLabels, trash`
};
}
} catch (error) {
return { error: true, message: error.message || 'Gmail operation failed' };
}
}
async function searchEmails({ query, maxResults = 10 }, context) {
if (!query) {
return { error: true, message: 'Search query is required' };
}
// Search for message IDs
const searchUrl = `${GMAIL_API}/messages?q=${encodeURIComponent(query)}&maxResults=${maxResults}`;
const searchResp = await context.oauth.fetch(searchUrl);
if (!searchResp.ok) {
throw new Error(`Gmail search failed: ${searchResp.status}`);
}
const searchData = JSON.parse(searchResp.body);
if (!searchData.messages?.length) {
return { success: true, action: 'search', query, count: 0, emails: [] };
}
// Fetch details for each message
const emails = [];
for (const msg of searchData.messages.slice(0, maxResults)) {
const msgUrl = `${GMAIL_API}/messages/${msg.id}?format=metadata&metadataHeaders=From&metadataHeaders=To&metadataHeaders=Subject&metadataHeaders=Date`;
const msgResp = await context.oauth.fetch(msgUrl);
if (msgResp.ok) {
const msgData = JSON.parse(msgResp.body);
const headers = msgData.payload?.headers || [];
emails.push({
id: msgData.id,
threadId: msgData.threadId,
from: getHeader(headers, 'From'),
to: getHeader(headers, 'To'),
subject: getHeader(headers, 'Subject'),
date: getHeader(headers, 'Date'),
snippet: msgData.snippet,
});
}
}
return { success: true, action: 'search', query, count: emails.length, emails };
}
async function readEmail({ messageId }, context) {
if (!messageId) {
return { error: true, message: 'Message ID is required' };
}
const resp = await context.oauth.fetch(
`${GMAIL_API}/messages/${messageId}?format=full`
);
if (!resp.ok) {
throw new Error(`Failed to read email: ${resp.status}`);
}
const msg = JSON.parse(resp.body);
const headers = msg.payload?.headers || [];
const { text, html } = extractBody(msg.payload);
return {
success: true,
action: 'read',
email: {
id: msg.id,
threadId: msg.threadId,
from: getHeader(headers, 'From'),
to: getHeader(headers, 'To'),
subject: getHeader(headers, 'Subject'),
date: getHeader(headers, 'Date'),
body: text,
bodyHtml: html,
labels: msg.labelIds || [],
},
};
}
async function sendEmail({ to, subject, body, cc, bcc, isHtml }, context) {
if (!to || !subject || !body) {
return { error: true, message: 'to, subject, and body are required' };
}
// Build RFC 2822 email
const contentType = isHtml ? 'text/html' : 'text/plain';
const headers = [
`To: ${to}`,
`Subject: ${subject}`,
`Content-Type: ${contentType}; charset=utf-8`,
];
if (cc) headers.push(`Cc: ${cc}`);
if (bcc) headers.push(`Bcc: ${bcc}`);
const email = `${headers.join('\r\n')}\r\n\r\n${body}`;
const encodedEmail = encodeBase64Url(email);
const resp = await context.oauth.fetch(
`${GMAIL_API}/messages/send`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ raw: encodedEmail }),
}
);
if (!resp.ok) {
throw new Error(`Failed to send email: ${resp.status}`);
}
const result = JSON.parse(resp.body);
return {
success: true,
action: 'send',
message: 'Email sent successfully',
id: result.id,
threadId: result.threadId
};
}
async function archiveEmail({ messageId }, context) {
if (!messageId) {
return { error: true, message: 'Message ID is required' };
}
const resp = await context.oauth.fetch(
`${GMAIL_API}/messages/${messageId}/modify`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ removeLabelIds: ['INBOX'] }),
}
);
if (!resp.ok) {
throw new Error(`Failed to archive email: ${resp.status}`);
}
return { success: true, action: 'archive', message: 'Email archived', messageId };
}
async function listLabels(context) {
const resp = await context.oauth.fetch(`${GMAIL_API}/labels`);
if (!resp.ok) {
throw new Error(`Failed to list labels: ${resp.status}`);
}
const data = JSON.parse(resp.body);
return {
success: true,
action: 'listLabels',
count: data.labels.length,
labels: data.labels
};
}
async function trashEmail({ messageId }, context) {
if (!messageId) {
return { error: true, message: 'Message ID is required' };
}
const resp = await context.oauth.fetch(
`${GMAIL_API}/messages/${messageId}/trash`,
{ method: 'POST' }
);
if (!resp.ok) {
throw new Error(`Failed to trash email: ${resp.status}`);
}
return { success: true, action: 'trash', message: 'Email moved to trash', messageId };
}
module.exports = { handler };Manifest
json
{
"trigger": "auto",
"oauth": {
"provider": "google",
"scopes": [
"https://www.googleapis.com/auth/gmail.readonly",
"https://www.googleapis.com/auth/gmail.send",
"https://www.googleapis.com/auth/gmail.modify",
"https://www.googleapis.com/auth/gmail.labels",
"https://www.googleapis.com/auth/userinfo.email"
],
"clientIdSecret": "GOOGLE_CLIENT_ID",
"clientSecretSecret": "GOOGLE_CLIENT_SECRET"
},
"developerSecrets": {
"GOOGLE_CLIENT_ID": {
"description": "Google OAuth Client ID",
"required": true
},
"GOOGLE_CLIENT_SECRET": {
"description": "Google OAuth Client Secret",
"required": true
}
},
"tool": {
"name": "gmail",
"description": "Access Gmail to search, read, send, and manage emails. Use when users want to interact with their email.",
"parameters": {
"type": "object",
"properties": {
"action": {
"type": "string",
"enum": ["search", "read", "send", "archive", "listLabels", "trash"],
"description": "The Gmail action to perform"
},
"query": {
"type": "string",
"description": "For search: Gmail search query (e.g., 'from:john subject:meeting')"
},
"maxResults": {
"type": "integer",
"description": "For search: Max emails to return (default: 10)"
},
"messageId": {
"type": "string",
"description": "For read/archive/trash: The email message ID"
},
"to": {
"type": "string",
"description": "For send: Recipient email address"
},
"subject": {
"type": "string",
"description": "For send: Email subject"
},
"body": {
"type": "string",
"description": "For send: Email body"
}
},
"required": ["action"]
}
}
}Setup
Create Google OAuth App
- Go to Google Cloud Console
- Create new project
- Enable Gmail API
- Create OAuth 2.0 credentials
- Add redirect URI:
https://your-pie-domain.com/api/oauth/plugin/{pluginId}/callback
Create Agent in PIE
- Type: Connector
- Paste code and manifest
Add OAuth Credentials
- Set
GOOGLE_CLIENT_IDandGOOGLE_CLIENT_SECRETas developer secrets
- Set
Connect
- Users click "Connect" to authorize
Key Patterns
OAuth Check
Always check connection first:
js
if (!await context.oauth.isConnected()) {
return { error: true, requiresAuth: true };
}Authenticated Requests
PIE adds the Bearer token automatically:
js
const resp = await context.oauth.fetch(url);Multi-Action Pattern
Handle multiple operations in one tool:
js
switch (action) {
case 'search': return await searchEmails(params, context);
case 'read': return await readEmail(params, context);
// ...
}