1341 lines
35 KiB
Markdown
1341 lines
35 KiB
Markdown
---
|
|
name: Google Mail API (Node.js/TypeScript)
|
|
description: 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
|
|
|
|
```bash
|
|
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)
|
|
|
|
```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
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
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)
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
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
|
|
|
|
```json
|
|
{
|
|
"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
|
|
|
|
```json
|
|
{
|
|
"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
|
|
|
|
```json
|
|
{
|
|
"id": "...",
|
|
"historyId": "...",
|
|
"messages": [
|
|
{ /* Message objects */ }
|
|
]
|
|
}
|
|
```
|
|
|
|
## Error Handling
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
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
|
|
|
|
- Main Documentation: https://developers.google.com/workspace/gmail/api/guides
|
|
- API Reference: https://developers.google.com/workspace/gmail/api/reference/rest
|
|
- Node.js Quickstart: https://developers.google.com/workspace/gmail/api/quickstart/nodejs
|
|
- Node.js Client Docs: https://github.com/googleapis/google-api-nodejs-client
|
|
- Authentication: https://developers.google.com/identity/protocols/oauth2
|
|
- Service: gmail.googleapis.com (v1)
|