Skip to content

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

  1. 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
  2. Create Agent in PIE

    • Type: Connector
    • Paste code and manifest
  3. Add OAuth Credentials

    • Set GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET as developer secrets
  4. 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);
  // ...
}

Built with VitePress