Files
platform/seed/skills/google-mail-api/SKILL-nodejs.md
T
2026-02-24 21:47:36 +00:00

35 KiB

name, description
name description
Google Mail API (Node.js/TypeScript) Access, manage, and send Gmail messages using the Google Mail API with Node.js/TypeScript. Use when the user wants to read emails, send messages, manage labels, organize threads, work with drafts, or automate email operations programmatically.

Google Mail API (Node.js/TypeScript)

RESTful API reference for the Google Mail API — access and manage Gmail mailbox data including messages, threads, labels, and drafts using Node.js or TypeScript.

Official docs: https://developers.google.com/workspace/gmail/api/guides API Reference: https://developers.google.com/workspace/gmail/api/reference/rest Node.js Client: https://github.com/googleapis/google-api-nodejs-client

Key Concepts

Message An email message containing sender, recipients, subject, and body. Messages are immutable after creation. Each message has a unique id.

Thread A collection of related messages forming a conversation. When one or more recipients respond to a message, a thread is formed. Threads cannot be created directly, only deleted. Labels can be applied to threads.

Label A mechanism for organizing messages and threads. Two types exist:

  • System labels: Pre-defined labels like INBOX, TRASH, SPAM, DRAFT, SENT. Cannot be deleted or modified (but some can be applied/removed).
  • User labels: Custom labels created by users. Can be created, modified, and deleted.

Draft An unsent message. The message within a draft can be replaced before sending. Sending a draft automatically deletes it and creates a message with the SENT label.

Installation

Node.js/TypeScript Dependencies

npm install googleapis google-auth-library
# For TypeScript support
npm install --save-dev @types/node typescript

Authentication & Authorization

The Gmail API uses OAuth 2.0 for authentication. You must:

  1. Create a Google Cloud project
  2. Enable the Gmail API
  3. Configure OAuth 2.0 credentials (Desktop/Web application)
  4. Obtain credentials file (credentials.json)

Scopes

Different scopes control the level of access. Common scopes:

Scope Permissions
https://www.googleapis.com/auth/gmail.readonly Read-only access to Gmail mailbox and metadata
https://www.googleapis.com/auth/gmail.send Send messages only
https://www.googleapis.com/auth/gmail.modify Full access to read/write/modify messages and labels
https://www.googleapis.com/auth/gmail.compose Compose drafts and send messages
https://www.googleapis.com/auth/gmail.labels Manage labels

OAuth 2.0 Setup (Node.js/TypeScript)

import fs from "fs/promises";
import path from "path";
import { google } from "googleapis";
import { OAuth2Client } from "google-auth-library";

const SCOPES = ["https://www.googleapis.com/auth/gmail.modify"];
const CREDENTIALS_PATH = "credentials.json";
const TOKEN_PATH = "token.json";

async function authenticate(): Promise<OAuth2Client> {
  // Load credentials
  const credentialsContent = await fs.readFile(CREDENTIALS_PATH, "utf-8");
  const credentials = JSON.parse(credentialsContent);
  const { client_id, client_secret, redirect_uris } = credentials.installed;

  const oauth2Client = new google.auth.OAuth2(
    client_id,
    client_secret,
    redirect_uris[0]
  );

  // Check if we have a cached token
  try {
    const tokenContent = await fs.readFile(TOKEN_PATH, "utf-8");
    const token = JSON.parse(tokenContent);
    oauth2Client.setCredentials(token);

    // Refresh if expired
    if (token.expiry_date && token.expiry_date < Date.now()) {
      const newToken = await oauth2Client.refreshAccessToken();
      oauth2Client.setCredentials(newToken.credentials);
      await fs.writeFile(TOKEN_PATH, JSON.stringify(newToken.credentials));
    }

    return oauth2Client;
  } catch (err) {
    // No token, need to authenticate
    return authenticateUser(oauth2Client);
  }
}

async function authenticateUser(
  oauth2Client: OAuth2Client
): Promise<OAuth2Client> {
  const authUrl = oauth2Client.generateAuthUrl({
    access_type: "offline",
    scope: SCOPES,
  });

  console.log("Authorize this app by visiting this url:", authUrl);
  // In a real app, you'd use a callback server or prompt the user to visit the URL
  // and paste the code back

  const code = "AUTHORIZATION_CODE_FROM_USER"; // Get this from user input/callback
  const { tokens } = await oauth2Client.getToken(code);
  oauth2Client.setCredentials(tokens);

  // Save token for future use
  await fs.writeFile(TOKEN_PATH, JSON.stringify(tokens));
  return oauth2Client;
}

// Usage
const auth = await authenticate();
const gmail = google.gmail({ version: "v1", auth });

Core API Methods

Users

Method Description
gmail.users.getProfile() Get user's Gmail profile
gmail.users.watch() Watch for mailbox changes (push notifications)
gmail.users.stop() Stop watching mailbox

Messages

Method Description
gmail.users.messages.list() List messages in mailbox
gmail.users.messages.get() Get full message details
gmail.users.messages.send() Send a message
gmail.users.messages.create() Insert a message directly
gmail.users.messages.import() Import a message
gmail.users.messages.delete() Delete a message
gmail.users.messages.trash() Move message to trash
gmail.users.messages.untrash() Restore message from trash
gmail.users.messages.modify() Modify message labels
gmail.users.messages.batchModify() Modify multiple messages at once
gmail.users.messages.batchDelete() Delete multiple messages at once

Attachments

Method Description
gmail.users.messages.attachments.get() Get attachment data

Threads

Method Description
gmail.users.threads.list() List threads
gmail.users.threads.get() Get thread with all messages
gmail.users.threads.modify() Modify thread labels
gmail.users.threads.trash() Move thread to trash
gmail.users.threads.untrash() Restore thread from trash
gmail.users.threads.delete() Delete thread permanently

Labels

Method Description
gmail.users.labels.list() List all labels
gmail.users.labels.get() Get label details
gmail.users.labels.create() Create a new label
gmail.users.labels.update() Update label
gmail.users.labels.patch() Patch label
gmail.users.labels.delete() Delete label

Drafts

Method Description
gmail.users.drafts.list() List drafts
gmail.users.drafts.get() Get draft
gmail.users.drafts.create() Create draft
gmail.users.drafts.update() Update draft content
gmail.users.drafts.send() Send draft
gmail.users.drafts.delete() Delete draft

Settings

Method Description
gmail.users.settings.getAutoForwarding() Get auto-forwarding settings
gmail.users.settings.getImap() Get IMAP settings
gmail.users.settings.getPop() Get POP settings
gmail.users.settings.getVacation() Get vacation auto-reply settings
gmail.users.settings.updateAutoForwarding() Update auto-forwarding
gmail.users.settings.updateVacation() Update vacation settings

Common Recipes

List Labels

async function listLabels(gmail: any) {
  try {
    const result = await gmail.users.labels.list({
      userId: "me",
    });

    const labels = result.data.labels || [];
    labels.forEach((label: any) => {
      console.log(`${label.name} (ID: ${label.id})`);
    });

    return labels;
  } catch (error) {
    console.error("Error listing labels:", error);
    throw error;
  }
}

List Messages in Inbox

async function listInboxMessages(gmail: any, maxResults = 10) {
  try {
    const result = await gmail.users.messages.list({
      userId: "me",
      q: "in:inbox",
      maxResults,
    });

    const messages = result.data.messages || [];

    for (const message of messages) {
      const msgDetails = await gmail.users.messages.get({
        userId: "me",
        id: message.id,
        format: "metadata",
        metadataHeaders: ["Subject", "From"],
      });

      const headers = msgDetails.data.payload.headers;
      const subject =
        headers.find((h: any) => h.name === "Subject")?.value || "No Subject";
      const from =
        headers.find((h: any) => h.name === "From")?.value || "Unknown";

      console.log(`${from}: ${subject}`);
    }

    return messages;
  } catch (error) {
    console.error("Error listing messages:", error);
    throw error;
  }
}

Get Full Message

async function getFullMessage(gmail: any, messageId: string) {
  try {
    const message = await gmail.users.messages.get({
      userId: "me",
      id: messageId,
      format: "full",
    });

    const payload = message.data.payload;
    const headers = payload.headers;
    const bodyData = payload.body?.data || "";

    // Decode base64url body
    const decodedBody = bodyData
      ? Buffer.from(bodyData, "base64").toString()
      : "";

    return {
      id: message.data.id,
      threadId: message.data.threadId,
      headers,
      body: decodedBody,
      labelIds: message.data.labelIds,
    };
  } catch (error) {
    console.error("Error getting message:", error);
    throw error;
  }
}

Send a Message

import nodemailer from "nodemailer";

async function sendMessage(
  gmail: any,
  to: string,
  subject: string,
  body: string
) {
  try {
    // Create email using nodemailer format for easier composition
    const mailOptions = {
      to,
      subject,
      text: body,
    };

    // Simulate creating RFC 2822 formatted message
    const message =
      `From: sender@example.com\r\n` +
      `To: ${to}\r\n` +
      `Subject: ${subject}\r\n` +
      `\r\n` +
      `${body}`;

    const encodedMessage = Buffer.from(message)
      .toString("base64")
      .replace(/\+/g, "-")
      .replace(/\//g, "_")
      .replace(/=+$/, "");

    const result = await gmail.users.messages.send({
      userId: "me",
      requestBody: {
        raw: encodedMessage,
      },
    });

    console.log(`Message sent with ID: ${result.data.id}`);
    return result.data;
  } catch (error) {
    console.error("Error sending message:", error);
    throw error;
  }
}

Send Email with Attachment

import fs from "fs/promises";

async function sendMessageWithAttachment(
  gmail: any,
  to: string,
  subject: string,
  body: string,
  filePath: string
) {
  try {
    const fileContent = await fs.readFile(filePath);
    const fileName = filePath.split("/").pop() || "attachment";
    const mimeType = "application/octet-stream";

    const boundary = "boundary123";
    const message =
      `From: sender@example.com\r\n` +
      `To: ${to}\r\n` +
      `Subject: ${subject}\r\n` +
      `MIME-Version: 1.0\r\n` +
      `Content-Type: multipart/mixed; boundary="${boundary}"\r\n` +
      `\r\n` +
      `--${boundary}\r\n` +
      `Content-Type: text/plain; charset="UTF-8"\r\n` +
      `\r\n` +
      `${body}\r\n` +
      `--${boundary}\r\n` +
      `Content-Type: ${mimeType}; name="${fileName}"\r\n` +
      `Content-Disposition: attachment; filename="${fileName}"\r\n` +
      `Content-Transfer-Encoding: base64\r\n` +
      `\r\n` +
      `${fileContent.toString("base64")}\r\n` +
      `--${boundary}--`;

    const encodedMessage = Buffer.from(message)
      .toString("base64")
      .replace(/\+/g, "-")
      .replace(/\//g, "_")
      .replace(/=+$/, "");

    const result = await gmail.users.messages.send({
      userId: "me",
      requestBody: {
        raw: encodedMessage,
      },
    });

    console.log(`Message with attachment sent with ID: ${result.data.id}`);
    return result.data;
  } catch (error) {
    console.error("Error sending message with attachment:", error);
    throw error;
  }
}

Create Draft

async function createDraft(
  gmail: any,
  to: string,
  subject: string,
  body: string
) {
  try {
    const message =
      `From: sender@example.com\r\n` +
      `To: ${to}\r\n` +
      `Subject: ${subject}\r\n` +
      `\r\n` +
      `${body}`;

    const encodedMessage = Buffer.from(message)
      .toString("base64")
      .replace(/\+/g, "-")
      .replace(/\//g, "_")
      .replace(/=+$/, "");

    const result = await gmail.users.drafts.create({
      userId: "me",
      requestBody: {
        message: {
          raw: encodedMessage,
        },
      },
    });

    console.log(`Draft created with ID: ${result.data.id}`);
    return result.data;
  } catch (error) {
    console.error("Error creating draft:", error);
    throw error;
  }
}

Apply Label to Message

async function applyLabelToMessage(
  gmail: any,
  messageId: string,
  labelName: string
) {
  try {
    // Get label ID first
    const labelsResult = await gmail.users.labels.list({
      userId: "me",
    });

    const label = labelsResult.data.labels?.find(
      (l: any) => l.name === labelName
    );
    if (!label) {
      throw new Error(`Label "${labelName}" not found`);
    }

    const result = await gmail.users.messages.modify({
      userId: "me",
      id: messageId,
      requestBody: {
        addLabelIds: [label.id],
      },
    });

    console.log(`Label "${labelName}" applied to message ${messageId}`);
    return result.data;
  } catch (error) {
    console.error("Error applying label:", error);
    throw error;
  }
}

List Threads

async function listThreads(gmail: any, maxResults = 10) {
  try {
    const result = await gmail.users.threads.list({
      userId: "me",
      q: "in:inbox",
      maxResults,
    });

    const threads = result.data.threads || [];

    for (const thread of threads) {
      const threadData = await gmail.users.threads.get({
        userId: "me",
        id: thread.id,
      });

      const numMessages = threadData.data.messages?.length || 0;
      console.log(`Thread ${thread.id}: ${numMessages} messages`);
    }

    return threads;
  } catch (error) {
    console.error("Error listing threads:", error);
    throw error;
  }
}

Get Thread Messages in Order

async function getThreadMessages(gmail: any, threadId: string) {
  try {
    const thread = await gmail.users.threads.get({
      userId: "me",
      id: threadId,
      format: "full",
    });

    const messages = thread.data.messages || [];

    messages.forEach((msg: any) => {
      const headers = msg.payload.headers;
      const subject =
        headers.find((h: any) => h.name === "Subject")?.value || "";
      const from =
        headers.find((h: any) => h.name === "From")?.value || "";
      console.log(`From: ${from}`);
      console.log(`Subject: ${subject}`);
      console.log("---");
    });

    return messages;
  } catch (error) {
    console.error("Error getting thread messages:", error);
    throw error;
  }
}

Search Messages (Gmail Query Syntax)

async function searchMessages(gmail: any, query: string, maxResults = 10) {
  try {
    // Common query operators:
    // in:inbox, in:trash, in:spam, in:sent, in:draft
    // is:unread, is:read, is:starred
    // from:sender@example.com, to:recipient@example.com
    // subject:"search term", has:attachment
    // after:YYYY/MM/DD, before:YYYY/MM/DD
    // larger:1M, smaller:100K

    const result = await gmail.users.messages.list({
      userId: "me",
      q: query,
      maxResults,
    });

    const messages = result.data.messages || [];
    console.log(`Found ${messages.length} messages matching: ${query}`);
    return messages;
  } catch (error) {
    console.error("Error searching messages:", error);
    throw error;
  }
}

// Usage examples:
// searchMessages(gmail, 'is:unread from:boss@example.com after:2024/01/01')
// searchMessages(gmail, 'has:attachment subject:"invoice"')

Create Custom Label

async function createLabel(gmail: any, labelName: string) {
  try {
    const result = await gmail.users.labels.create({
      userId: "me",
      requestBody: {
        name: labelName,
        labelListVisibility: "labelShow",
        messageListVisibility: "show",
      },
    });

    console.log(`Label created: ${result.data.id}`);
    return result.data;
  } catch (error) {
    console.error("Error creating label:", error);
    throw error;
  }
}

Modify Thread Labels

async function modifyThreadLabels(
  gmail: any,
  threadId: string,
  labelIdsToAdd: string[],
  labelIdsToRemove: string[] = []
) {
  try {
    const result = await gmail.users.threads.modify({
      userId: "me",
      id: threadId,
      requestBody: {
        addLabelIds: labelIdsToAdd,
        removeLabelIds: labelIdsToRemove,
      },
    });

    console.log(`Thread ${threadId} labels modified`);
    return result.data;
  } catch (error) {
    console.error("Error modifying thread labels:", error);
    throw error;
  }
}

Move Message to Trash

async function moveToTrash(gmail: any, messageId: string) {
  try {
    const result = await gmail.users.messages.trash({
      userId: "me",
      id: messageId,
    });

    console.log(`Message ${messageId} moved to trash`);
    return result.data;
  } catch (error) {
    console.error("Error moving message to trash:", error);
    throw error;
  }
}

Delete Message Permanently

async function deleteMessage(gmail: any, messageId: string) {
  try {
    await gmail.users.messages.delete({
      userId: "me",
      id: messageId,
    });

    console.log(`Message ${messageId} deleted permanently`);
  } catch (error) {
    console.error("Error deleting message:", error);
    throw error;
  }
}

Batch Modify Messages

async function batchModifyMessages(
  gmail: any,
  messageIds: string[],
  labelIdsToAdd: string[],
  labelIdsToRemove: string[] = []
) {
  try {
    const result = await gmail.users.messages.batchModify({
      userId: "me",
      requestBody: {
        ids: messageIds,
        addLabelIds: labelIdsToAdd,
        removeLabelIds: labelIdsToRemove,
      },
    });

    console.log(`${messageIds.length} messages modified`);
    return result.data;
  } catch (error) {
    console.error("Error batch modifying messages:", error);
    throw error;
  }
}

Get User Profile

async function getUserProfile(gmail: any) {
  try {
    const result = await gmail.users.getProfile({
      userId: "me",
    });

    console.log(`Email: ${result.data.emailAddress}`);
    console.log(`Messages Total: ${result.data.messagesTotal}`);
    console.log(`Threads Total: ${result.data.threadsTotal}`);

    return result.data;
  } catch (error) {
    console.error("Error getting user profile:", error);
    throw error;
  }
}

Send Email with Multiple Attachments

import fs from "fs/promises";

async function sendMessageWithMultipleAttachments(
  gmail: any,
  to: string,
  subject: string,
  body: string,
  filePaths: string[]
) {
  try {
    // Generate unique boundary
    const boundary = `boundary_${Date.now()}_${Math.random().toString(36).substring(7)}`;

    // Start with message headers
    let message =
      `From: me\r\n` +
      `To: ${to}\r\n` +
      `Subject: ${subject}\r\n` +
      `MIME-Version: 1.0\r\n` +
      `Content-Type: multipart/mixed; boundary="${boundary}"\r\n` +
      `\r\n`;

    // Add body
    message +=
      `--${boundary}\r\n` +
      `Content-Type: text/plain; charset="UTF-8"\r\n` +
      `Content-Transfer-Encoding: 7bit\r\n` +
      `\r\n` +
      `${body}\r\n`;

    // Add each attachment
    for (const filePath of filePaths) {
      try {
        const fileContent = await fs.readFile(filePath);
        const fileName = filePath.split("/").pop() || "attachment";

        // Detect MIME type
        const mimeType = getMimeType(fileName);

        message +=
          `--${boundary}\r\n` +
          `Content-Type: ${mimeType}; name="${fileName}"\r\n` +
          `Content-Disposition: attachment; filename="${fileName}"\r\n` +
          `Content-Transfer-Encoding: base64\r\n` +
          `\r\n` +
          `${fileContent.toString("base64")}\r\n`;
      } catch (error) {
        console.warn(`Warning: Could not attach file ${filePath}`);
      }
    }

    // Close boundary
    message += `--${boundary}--`;

    // Encode and send
    const encodedMessage = Buffer.from(message)
      .toString("base64")
      .replace(/\+/g, "-")
      .replace(/\//g, "_")
      .replace(/=+$/, "");

    const result = await gmail.users.messages.send({
      userId: "me",
      requestBody: {
        raw: encodedMessage,
      },
    });

    console.log(`Message with ${filePaths.length} attachments sent: ${result.data.id}`);
    return result.data;
  } catch (error) {
    console.error("Error sending message with multiple attachments:", error);
    throw error;
  }
}

// Helper function to detect MIME types
function getMimeType(fileName: string): string {
  const mimeTypes: { [key: string]: string } = {
    ".pdf": "application/pdf",
    ".doc": "application/msword",
    ".docx":
      "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
    ".xls": "application/vnd.ms-excel",
    ".xlsx":
      "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
    ".ppt": "application/vnd.ms-powerpoint",
    ".pptx":
      "application/vnd.openxmlformats-officedocument.presentationml.presentation",
    ".txt": "text/plain",
    ".csv": "text/csv",
    ".jpg": "image/jpeg",
    ".jpeg": "image/jpeg",
    ".png": "image/png",
    ".gif": "image/gif",
    ".zip": "application/zip",
  };

  const ext = fileName.substring(fileName.lastIndexOf(".")).toLowerCase();
  return mimeTypes[ext] || "application/octet-stream";
}

// Usage
// await sendMessageWithMultipleAttachments(
//   gmail,
//   "user@example.com",
//   "Multiple Files",
//   "Please find the attached documents.",
//   ["/path/to/file1.pdf", "/path/to/file2.xlsx", "/path/to/file3.doc"]
// );

Extract Attachments from Received Email

import fs from "fs/promises";
import path from "path";

interface AttachmentInfo {
  filename: string;
  mimeType: string;
  size: number;
  attachmentId: string;
  messageId: string;
}

async function extractAttachments(
  gmail: any,
  messageId: string,
  outputDir: string = "./attachments"
): Promise<AttachmentInfo[]> {
  try {
    // Create output directory if it doesn't exist
    await fs.mkdir(outputDir, { recursive: true });

    // Get message with full payload
    const message = await gmail.users.messages.get({
      userId: "me",
      id: messageId,
      format: "full",
    });

    const attachments: AttachmentInfo[] = [];
    const payload = message.data.payload;

    // Check if message has parts (multipart)
    if (!payload.parts) {
      console.log("Message has no attachments");
      return attachments;
    }

    // Process each part
    for (const part of payload.parts) {
      if (part.filename && part.filename.length > 0) {
        const attachment = await gmail.users.messages.attachments.get({
          userId: "me",
          messageId: messageId,
          id: part.body.attachmentId,
        });

        // Decode base64url data
        const attachmentData = Buffer.from(attachment.data.data || "", "base64");

        // Save file
        const filePath = path.join(outputDir, part.filename);
        await fs.writeFile(filePath, attachmentData);

        console.log(`✅ Saved: ${part.filename} (${attachmentData.length} bytes)`);

        attachments.push({
          filename: part.filename,
          mimeType: part.mimeType || "application/octet-stream",
          size: attachmentData.length,
          attachmentId: part.body.attachmentId,
          messageId: messageId,
        });
      }
    }

    console.log(`Extracted ${attachments.length} attachment(s)`);
    return attachments;
  } catch (error) {
    console.error("Error extracting attachments:", error);
    throw error;
  }
}

// Usage
// const attachments = await extractAttachments(gmail, messageId, "./downloads");

Parse and Decode Full Email Body

async function parseFullEmail(gmail: any, messageId: string) {
  try {
    const message = await gmail.users.messages.get({
      userId: "me",
      id: messageId,
      format: "full",
    });

    const payload = message.data.payload;
    const headers = payload.headers || [];

    // Extract headers
    const getHeader = (name: string): string =>
      headers.find((h) => h.name === name)?.value || "";

    const result = {
      id: message.data.id,
      threadId: message.data.threadId,
      labelIds: message.data.labelIds || [],
      snippet: message.data.snippet || "",
      internalDate: new Date(parseInt(message.data.internalDate || "0")),
      headers: {
        subject: getHeader("Subject"),
        from: getHeader("From"),
        to: getHeader("To"),
        cc: getHeader("Cc"),
        bcc: getHeader("Bcc"),
        date: getHeader("Date"),
      },
      body: "",
      html: "",
      attachments: [] as Array<{
        filename: string;
        mimeType: string;
        attachmentId: string;
      }>,
    };

    // Extract body - handle multipart messages
    if (payload.mimeType?.startsWith("multipart")) {
      // Multipart message - has parts
      for (const part of payload.parts || []) {
        if (part.mimeType === "text/plain" && !result.body) {
          const bodyData = part.body?.data || "";
          result.body = decodeBase64Url(bodyData);
        } else if (part.mimeType === "text/html" && !result.html) {
          const bodyData = part.body?.data || "";
          result.html = decodeBase64Url(bodyData);
        } else if (part.filename && part.body?.attachmentId) {
          result.attachments.push({
            filename: part.filename,
            mimeType: part.mimeType || "application/octet-stream",
            attachmentId: part.body.attachmentId,
          });
        }
      }
    } else {
      // Simple message - single part
      const bodyData = payload.body?.data || "";
      if (payload.mimeType === "text/html") {
        result.html = decodeBase64Url(bodyData);
      } else {
        result.body = decodeBase64Url(bodyData);
      }
    }

    return result;
  } catch (error) {
    console.error("Error parsing email:", error);
    throw error;
  }
}

// Helper function
function decodeBase64Url(data: string): string {
  if (!data) return "";
  // Convert base64url to base64 and decode
  const base64 = data.replace(/-/g, "+").replace(/_/g, "/");
  return Buffer.from(base64, "base64").toString("utf-8");
}

// Usage
// const email = await parseFullEmail(gmail, messageId);
// console.log(`Subject: ${email.headers.subject}`);
// console.log(`From: ${email.headers.from}`);
// console.log(`Body: ${email.body}`);
// console.log(`Has ${email.attachments.length} attachments`);

Query String Format

Gmail API uses Gmail search syntax for q parameter. Common operators:

  • in:inbox, in:trash, in:spam, in:sent, in:draft
  • is:unread, is:read, is:starred, is:important
  • from:email@example.com, to:email@example.com, cc:email@example.com
  • subject:text - Search in subject line
  • has:attachment - Only messages with attachments
  • filename:pdf - Attachment filename
  • before:YYYY/MM/DD, after:YYYY/MM/DD
  • larger:1M, smaller:500K - Message size
  • Combine with AND, OR, - (NOT)

Example: q='is:unread has:attachment from:boss@example.com after:2024/01/01'

Message Resource Structure

{
  "id": "...",                    // Message ID
  "threadId": "...",              // Thread ID
  "labelIds": ["INBOX", "..."],  // Applied labels
  "snippet": "...",               // Preview text
  "historyId": "...",             // History record ID
  "internalDate": "...",          // Unix timestamp
  "payload": {
    "partId": "",
    "mimeType": "...",            // e.g., "text/plain", "multipart/mixed"
    "filename": "",
    "headers": [
      { "name": "Subject", "value": "..." },
      { "name": "From", "value": "..." },
      { "name": "To", "value": "..." },
      { "name": "Date", "value": "..." }
    ],
    "body": {
      "size": 0,
      "data": "base64url encoded body"
    },
    "parts": [...]                // For multipart messages
  },
  "sizeEstimate": 0               // Size in bytes
}

Label Resource Structure

{
  "id": "...",
  "name": "...",
  "messageListVisibility": "show",     // or "hide"
  "labelListVisibility": "labelShow",  // or "labelHide"
  "type": "user",                      // or "system"
  "messagesTotal": 0,
  "messagesUnread": 0,
  "threadsTotal": 0,
  "threadsUnread": 0
}

Thread Resource Structure

{
  "id": "...",
  "historyId": "...",
  "messages": [
    { /* Message objects */ }
  ]
}

Error Handling

async function safeGmailOperation(
  operation: () => Promise<any>
): Promise<any> {
  try {
    return await operation();
  } catch (error: any) {
    if (error.response) {
      const status = error.response.status;
      const statusText = error.response.statusText;
      const errorDetails = error.response.data.error;

      console.error(`Error ${status} ${statusText}:`);
      console.error(errorDetails.message);

      // Handle specific errors
      switch (status) {
        case 400:
          console.error("Bad request - check parameters");
          break;
        case 403:
          console.error("Forbidden - check scopes or permissions");
          break;
        case 404:
          console.error("Not found - resource doesn't exist");
          break;
        case 429:
          console.error("Rate limited - wait before retrying");
          break;
      }
    } else {
      console.error("Network or other error:", error.message);
    }
    throw error;
  }
}

// Usage
try {
  await safeGmailOperation(() => 
    gmail.users.messages.get({
      userId: "me",
      id: "INVALID_ID"
    })
  );
} catch (error) {
  // Error is already logged
}

Complete Example: Gmail Helper Class

import { google } from "googleapis";
import { OAuth2Client } from "google-auth-library";

class GmailHelper {
  private gmail: any;
  private auth: OAuth2Client;

  constructor(auth: OAuth2Client) {
    this.auth = auth;
    this.gmail = google.gmail({ version: "v1", auth });
  }

  async getProfile() {
    return (await this.gmail.users.getProfile({ userId: "me" })).data;
  }

  async listLabels() {
    return (await this.gmail.users.labels.list({ userId: "me" })).data.labels;
  }

  async listInboxMessages(maxResults = 10) {
    return (
      await this.gmail.users.messages.list({
        userId: "me",
        q: "in:inbox",
        maxResults,
      })
    ).data.messages;
  }

  async getMessage(messageId: string) {
    return (
      await this.gmail.users.messages.get({
        userId: "me",
        id: messageId,
        format: "full",
      })
    ).data;
  }

  async searchMessages(query: string, maxResults = 10) {
    return (
      await this.gmail.users.messages.list({
        userId: "me",
        q: query,
        maxResults,
      })
    ).data.messages;
  }

  async sendMessage(to: string, subject: string, body: string) {
    const message =
      `From: me\r\n` +
      `To: ${to}\r\n` +
      `Subject: ${subject}\r\n` +
      `\r\n` +
      `${body}`;

    const encodedMessage = Buffer.from(message)
      .toString("base64")
      .replace(/\+/g, "-")
      .replace(/\//g, "_")
      .replace(/=+$/, "");

    return (
      await this.gmail.users.messages.send({
        userId: "me",
        requestBody: { raw: encodedMessage },
      })
    ).data;
  }

  async createDraft(to: string, subject: string, body: string) {
    const message =
      `From: me\r\n` +
      `To: ${to}\r\n` +
      `Subject: ${subject}\r\n` +
      `\r\n` +
      `${body}`;

    const encodedMessage = Buffer.from(message)
      .toString("base64")
      .replace(/\+/g, "-")
      .replace(/\//g, "_")
      .replace(/=+$/, "");

    return (
      await this.gmail.users.drafts.create({
        userId: "me",
        requestBody: { message: { raw: encodedMessage } },
      })
    ).data;
  }

  async applyLabel(messageId: string, labelName: string) {
    const labels = await this.listLabels();
    const label = labels?.find((l: any) => l.name === labelName);

    if (!label) throw new Error(`Label "${labelName}" not found`);

    return (
      await this.gmail.users.messages.modify({
        userId: "me",
        id: messageId,
        requestBody: { addLabelIds: [label.id] },
      })
    ).data;
  }

  async deleteMessage(messageId: string) {
    return await this.gmail.users.messages.delete({
      userId: "me",
      id: messageId,
    });
  }

  async trashMessage(messageId: string) {
    return (
      await this.gmail.users.messages.trash({
        userId: "me",
        id: messageId,
      })
    ).data;
  }

  async parseFullEmail(messageId: string) {
    const message = await this.gmail.users.messages.get({
      userId: "me",
      id: messageId,
      format: "full",
    });

    const payload = message.data.payload;
    const headers = payload.headers || [];

    const getHeader = (name: string): string =>
      headers.find((h: any) => h.name === name)?.value || "";

    const result: any = {
      id: message.data.id,
      threadId: message.data.threadId,
      labelIds: message.data.labelIds || [],
      snippet: message.data.snippet || "",
      headers: {
        subject: getHeader("Subject"),
        from: getHeader("From"),
        to: getHeader("To"),
        cc: getHeader("Cc"),
        date: getHeader("Date"),
      },
      body: "",
      html: "",
      attachments: [] as any[],
    };

    if (payload.mimeType?.startsWith("multipart")) {
      for (const part of payload.parts || []) {
        if (part.mimeType === "text/plain" && !result.body) {
          result.body = this.decodeBase64Url(part.body?.data || "");
        } else if (part.mimeType === "text/html" && !result.html) {
          result.html = this.decodeBase64Url(part.body?.data || "");
        } else if (part.filename && part.body?.attachmentId) {
          result.attachments.push({
            filename: part.filename,
            mimeType: part.mimeType || "application/octet-stream",
            attachmentId: part.body.attachmentId,
          });
        }
      }
    } else {
      const bodyData = payload.body?.data || "";
      if (payload.mimeType === "text/html") {
        result.html = this.decodeBase64Url(bodyData);
      } else {
        result.body = this.decodeBase64Url(bodyData);
      }
    }

    return result;
  }

  private decodeBase64Url(data: string): string {
    if (!data) return "";
    const base64 = data.replace(/-/g, "+").replace(/_/g, "/");
    return Buffer.from(base64, "base64").toString("utf-8");
  }
}

export default GmailHelper;

Source