gmail-workspace-mcp-server 0.0.3 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/README.md +145 -15
  2. package/build/index.integration-with-mock.js +140 -4
  3. package/build/index.js +23 -6
  4. package/package.json +1 -1
  5. package/shared/gmail-client/lib/drafts.d.ts +42 -0
  6. package/shared/gmail-client/lib/drafts.js +81 -0
  7. package/shared/gmail-client/lib/mime-utils.d.ts +20 -0
  8. package/shared/gmail-client/lib/mime-utils.js +38 -0
  9. package/shared/gmail-client/lib/modify-message.d.ts +9 -0
  10. package/shared/gmail-client/lib/modify-message.js +20 -0
  11. package/shared/gmail-client/lib/send-message.d.ts +18 -0
  12. package/shared/gmail-client/lib/send-message.js +40 -0
  13. package/shared/index.d.ts +2 -2
  14. package/shared/index.js +2 -2
  15. package/shared/server.d.ts +108 -1
  16. package/shared/server.js +43 -3
  17. package/shared/tools/change-email-conversation.d.ts +66 -0
  18. package/shared/tools/change-email-conversation.js +148 -0
  19. package/shared/tools/draft-email.d.ts +79 -0
  20. package/shared/tools/draft-email.js +150 -0
  21. package/shared/tools/{get-email.d.ts → get-email-conversation.d.ts} +2 -2
  22. package/shared/tools/{get-email.js → get-email-conversation.js} +8 -8
  23. package/shared/tools/{list-recent-emails.d.ts → list-email-conversations.d.ts} +28 -13
  24. package/shared/tools/list-email-conversations.js +150 -0
  25. package/shared/tools/search-email-conversations.d.ts +45 -0
  26. package/shared/tools/search-email-conversations.js +110 -0
  27. package/shared/tools/send-email.d.ts +104 -0
  28. package/shared/tools/send-email.js +181 -0
  29. package/shared/tools.d.ts +19 -2
  30. package/shared/tools.js +56 -8
  31. package/shared/utils/email-helpers.d.ts +4 -0
  32. package/shared/utils/email-helpers.js +15 -0
  33. package/shared/tools/list-recent-emails.js +0 -133
package/README.md CHANGED
@@ -4,8 +4,12 @@ An MCP (Model Context Protocol) server that provides Gmail integration for AI as
4
4
 
5
5
  ## Features
6
6
 
7
- - **List Recent Emails**: Retrieve recent emails within a specified time horizon
7
+ - **List Email Conversations**: Retrieve emails with label filtering
8
8
  - **Get Email Details**: Fetch full email content including body and attachments info
9
+ - **Search Emails**: Search using Gmail's powerful query syntax
10
+ - **Manage Emails**: Mark as read/unread, star/unstar, archive, apply labels
11
+ - **Create Drafts**: Compose draft emails with reply support
12
+ - **Send Emails**: Send new emails or replies, directly or from drafts
9
13
  - **Service Account Authentication**: Secure domain-wide delegation for Google Workspace organizations
10
14
 
11
15
  ## Installation
@@ -30,19 +34,49 @@ This server requires a Google Cloud service account with domain-wide delegation
30
34
  2. Create or select a project
31
35
  3. Enable the Gmail API
32
36
  4. Create a service account with domain-wide delegation enabled
33
- 5. In [Google Workspace Admin Console](https://admin.google.com/), grant the service account access to the `https://www.googleapis.com/auth/gmail.readonly` scope
37
+ 5. In [Google Workspace Admin Console](https://admin.google.com/), grant the service account access to the following scopes:
38
+ - `https://www.googleapis.com/auth/gmail.readonly` (read emails)
39
+ - `https://www.googleapis.com/auth/gmail.modify` (modify labels)
40
+ - `https://www.googleapis.com/auth/gmail.compose` (create drafts)
41
+ - `https://www.googleapis.com/auth/gmail.send` (send emails)
34
42
  6. Download the JSON key file
35
43
 
36
44
  ### Environment Variables
37
45
 
38
- | Variable | Required | Description |
39
- | ------------------------------------ | -------- | ---------------------------------------- |
40
- | `GMAIL_SERVICE_ACCOUNT_CLIENT_EMAIL` | Yes | Service account email address |
41
- | `GMAIL_SERVICE_ACCOUNT_PRIVATE_KEY` | Yes | Service account private key (PEM format) |
42
- | `GMAIL_IMPERSONATE_EMAIL` | Yes | Email address to impersonate |
46
+ | Variable | Required | Description |
47
+ | ------------------------------------ | -------- | ------------------------------------------------------------------- |
48
+ | `GMAIL_SERVICE_ACCOUNT_CLIENT_EMAIL` | Yes | Service account email address |
49
+ | `GMAIL_SERVICE_ACCOUNT_PRIVATE_KEY` | Yes | Service account private key (PEM format) |
50
+ | `GMAIL_IMPERSONATE_EMAIL` | Yes | Email address to impersonate |
51
+ | `GMAIL_ENABLED_TOOLGROUPS` | No | Comma-separated list of tool groups to enable (default: all groups) |
43
52
 
44
53
  You can find the `client_email` and `private_key` values in your service account JSON key file.
45
54
 
55
+ ### Tool Groups
56
+
57
+ The server supports three tool groups for permission-based access control:
58
+
59
+ | Group | Tools Included | Risk Level |
60
+ | -------------------- | ---------------------------------------------------------------------------------- | ---------- |
61
+ | `readonly` | `list_email_conversations`, `get_email_conversation`, `search_email_conversations` | Low |
62
+ | `readwrite` | All readonly tools + `change_email_conversation`, `draft_email` | Medium |
63
+ | `readwrite_external` | All readwrite tools + `send_email` | High |
64
+
65
+ By default, all tool groups are enabled. To restrict access, set the `GMAIL_ENABLED_TOOLGROUPS` environment variable:
66
+
67
+ ```bash
68
+ # Read-only access (no write/send capabilities)
69
+ GMAIL_ENABLED_TOOLGROUPS=readonly
70
+
71
+ # Read and write, but no external sending
72
+ GMAIL_ENABLED_TOOLGROUPS=readwrite
73
+
74
+ # Full access including sending emails (default)
75
+ GMAIL_ENABLED_TOOLGROUPS=readwrite_external
76
+ ```
77
+
78
+ **Security Note:** The `send_email` tool is in a separate `readwrite_external` group because it can send emails externally, which carries higher risk than internal operations like modifying labels or creating drafts.
79
+
46
80
  ## Configuration
47
81
 
48
82
  ### Claude Desktop
@@ -72,27 +106,30 @@ Add to your Claude Desktop configuration (`~/Library/Application Support/Claude/
72
106
 
73
107
  ## Available Tools
74
108
 
75
- ### gmail_list_recent_emails
109
+ ### list_email_conversations
76
110
 
77
- List recent emails from Gmail within a specified time horizon.
111
+ List email conversations from Gmail with optional filtering.
78
112
 
79
113
  **Parameters:**
80
114
 
81
- - `hours` (number, optional): Time horizon in hours (default: 24)
82
- - `labels` (string, optional): Comma-separated label IDs (default: "INBOX")
83
- - `max_results` (number, optional): Maximum emails to return (default: 10, max: 100)
115
+ - `count` (number, optional): Number of emails to return (default: 10, max: 100)
116
+ - `labels` (string, optional): Comma-separated label IDs to filter by (default: "INBOX")
117
+ - `sort_by` (string, optional): Sort order - "recent" (newest first) or "oldest" (default: recent)
118
+ - `after` (string, optional): Only return emails after this datetime, exclusive (ISO 8601 UTC, e.g., "2024-01-15T14:30:00Z")
119
+ - `before` (string, optional): Only return emails before this datetime, exclusive (ISO 8601 UTC, e.g., "2024-01-15T14:30:00Z")
84
120
 
85
121
  **Example:**
86
122
 
87
123
  ```json
88
124
  {
89
- "hours": 48,
125
+ "count": 20,
90
126
  "labels": "INBOX,STARRED",
91
- "max_results": 20
127
+ "after": "2024-01-15T00:00:00Z",
128
+ "before": "2024-01-20T23:59:59Z"
92
129
  }
93
130
  ```
94
131
 
95
- ### gmail_get_email
132
+ ### get_email_conversation
96
133
 
97
134
  Retrieve the full content of a specific email by its ID.
98
135
 
@@ -108,6 +145,99 @@ Retrieve the full content of a specific email by its ID.
108
145
  }
109
146
  ```
110
147
 
148
+ ### search_email_conversations
149
+
150
+ Search emails using Gmail's powerful query syntax.
151
+
152
+ **Parameters:**
153
+
154
+ - `query` (string, required): Gmail search query (e.g., "from:user@example.com", "is:unread", "subject:meeting")
155
+ - `count` (number, optional): Maximum results to return (default: 10, max: 100)
156
+
157
+ **Example:**
158
+
159
+ ```json
160
+ {
161
+ "query": "from:alice@example.com is:unread",
162
+ "count": 20
163
+ }
164
+ ```
165
+
166
+ ### change_email_conversation
167
+
168
+ Modify email labels and status (read/unread, starred, archived).
169
+
170
+ **Parameters:**
171
+
172
+ - `email_id` (string, required): The email ID to modify
173
+ - `status` (string, optional): "read", "unread", or "archived"
174
+ - `is_starred` (boolean, optional): Star or unstar the email
175
+ - `labels` (string, optional): Comma-separated labels to add
176
+ - `remove_labels` (string, optional): Comma-separated labels to remove
177
+
178
+ **Example:**
179
+
180
+ ```json
181
+ {
182
+ "email_id": "18abc123def456",
183
+ "status": "read",
184
+ "is_starred": true
185
+ }
186
+ ```
187
+
188
+ ### draft_email
189
+
190
+ Create a draft email, optionally as a reply to an existing conversation.
191
+
192
+ **Parameters:**
193
+
194
+ - `to` (string, required): Recipient email address
195
+ - `subject` (string, required): Email subject
196
+ - `body` (string, required): Email body (plain text)
197
+ - `thread_id` (string, optional): Thread ID for replies
198
+ - `reply_to_email_id` (string, optional): Email ID to reply to (sets References/In-Reply-To headers)
199
+
200
+ **Example:**
201
+
202
+ ```json
203
+ {
204
+ "to": "recipient@example.com",
205
+ "subject": "Meeting Follow-up",
206
+ "body": "Thanks for the meeting today!"
207
+ }
208
+ ```
209
+
210
+ ### send_email
211
+
212
+ Send an email directly or from an existing draft.
213
+
214
+ **Parameters:**
215
+
216
+ - `to` (string, conditional): Recipient email (required unless sending from draft)
217
+ - `subject` (string, conditional): Email subject (required unless sending from draft)
218
+ - `body` (string, conditional): Email body (required unless sending from draft)
219
+ - `from_draft_id` (string, optional): Send an existing draft by ID
220
+ - `thread_id` (string, optional): Thread ID for replies
221
+ - `reply_to_email_id` (string, optional): Email ID to reply to
222
+
223
+ **Example (new email):**
224
+
225
+ ```json
226
+ {
227
+ "to": "recipient@example.com",
228
+ "subject": "Hello",
229
+ "body": "This is a test email."
230
+ }
231
+ ```
232
+
233
+ **Example (send draft):**
234
+
235
+ ```json
236
+ {
237
+ "from_draft_id": "r123456789"
238
+ }
239
+ ```
240
+
111
241
  ## Development
112
242
 
113
243
  ### Setup
@@ -4,11 +4,16 @@
4
4
  * Used for running integration tests without real Gmail API access
5
5
  */
6
6
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
7
+ import { readFileSync } from 'fs';
8
+ import { dirname, join } from 'path';
9
+ import { fileURLToPath } from 'url';
7
10
  import { createMCPServer } from '../shared/index.js';
8
11
  import { logServerStart, logError } from '../shared/logging.js';
9
- // =============================================================================
10
- // MOCK DATA
11
- // =============================================================================
12
+ // Read version from package.json
13
+ const __dirname = dirname(fileURLToPath(import.meta.url));
14
+ const packageJsonPath = join(__dirname, '..', 'package.json');
15
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
16
+ const VERSION = packageJson.version;
12
17
  const MOCK_EMAILS = [
13
18
  {
14
19
  id: 'msg_001',
@@ -24,6 +29,7 @@ const MOCK_EMAILS = [
24
29
  { name: 'From', value: 'alice@example.com' },
25
30
  { name: 'To', value: 'me@example.com' },
26
31
  { name: 'Date', value: new Date(Date.now() - 1000 * 60 * 30).toISOString() },
32
+ { name: 'Message-ID', value: '<msg001@example.com>' },
27
33
  ],
28
34
  body: {
29
35
  size: 150,
@@ -46,6 +52,7 @@ const MOCK_EMAILS = [
46
52
  { name: 'From', value: 'calendar@example.com' },
47
53
  { name: 'To', value: 'me@example.com' },
48
54
  { name: 'Date', value: new Date(Date.now() - 1000 * 60 * 60 * 2).toISOString() },
55
+ { name: 'Message-ID', value: '<msg002@example.com>' },
49
56
  ],
50
57
  body: {
51
58
  size: 200,
@@ -55,6 +62,9 @@ const MOCK_EMAILS = [
55
62
  sizeEstimate: 2048,
56
63
  },
57
64
  ];
65
+ const mockDrafts = [];
66
+ let draftIdCounter = 1;
67
+ let messageIdCounter = 100;
58
68
  // =============================================================================
59
69
  // MOCK CLIENT
60
70
  // =============================================================================
@@ -66,6 +76,17 @@ function createMockClient() {
66
76
  if (options?.labelIds && options.labelIds.length > 0) {
67
77
  filtered = MOCK_EMAILS.filter((email) => options.labelIds.some((label) => email.labelIds?.includes(label)));
68
78
  }
79
+ // Handle query search
80
+ if (options?.q) {
81
+ const query = options.q.toLowerCase();
82
+ filtered = filtered.filter((email) => {
83
+ const subject = email.payload?.headers?.find((h) => h.name === 'Subject')?.value?.toLowerCase() || '';
84
+ const from = email.payload?.headers?.find((h) => h.name === 'From')?.value?.toLowerCase() || '';
85
+ return (subject.includes(query) ||
86
+ from.includes(query) ||
87
+ email.snippet.toLowerCase().includes(query));
88
+ });
89
+ }
69
90
  // Apply maxResults
70
91
  const maxResults = options?.maxResults ?? 10;
71
92
  const messages = filtered.slice(0, maxResults).map((e) => ({
@@ -84,6 +105,121 @@ function createMockClient() {
84
105
  }
85
106
  return email;
86
107
  },
108
+ async modifyMessage(messageId, options) {
109
+ const email = MOCK_EMAILS.find((e) => e.id === messageId);
110
+ if (!email) {
111
+ throw new Error(`Message not found: ${messageId}`);
112
+ }
113
+ // Apply label changes
114
+ let labels = [...(email.labelIds || [])];
115
+ if (options.removeLabelIds) {
116
+ labels = labels.filter((l) => !options.removeLabelIds.includes(l));
117
+ }
118
+ if (options.addLabelIds) {
119
+ for (const label of options.addLabelIds) {
120
+ if (!labels.includes(label)) {
121
+ labels.push(label);
122
+ }
123
+ }
124
+ }
125
+ // Update the email in the mock array
126
+ email.labelIds = labels;
127
+ return { ...email, labelIds: labels };
128
+ },
129
+ async createDraft(options) {
130
+ const draft = {
131
+ id: `draft_${draftIdCounter++}`,
132
+ message: {
133
+ id: `msg_${messageIdCounter++}`,
134
+ threadId: options.threadId || `thread_${messageIdCounter}`,
135
+ labelIds: ['DRAFT'],
136
+ snippet: options.body.substring(0, 100),
137
+ historyId: '12347',
138
+ internalDate: String(Date.now()),
139
+ payload: {
140
+ mimeType: 'text/plain',
141
+ headers: [
142
+ { name: 'Subject', value: options.subject },
143
+ { name: 'From', value: 'me@example.com' },
144
+ { name: 'To', value: options.to },
145
+ { name: 'Date', value: new Date().toISOString() },
146
+ ],
147
+ body: {
148
+ size: options.body.length,
149
+ data: Buffer.from(options.body).toString('base64url'),
150
+ },
151
+ },
152
+ },
153
+ };
154
+ mockDrafts.push(draft);
155
+ return draft;
156
+ },
157
+ async getDraft(draftId) {
158
+ const draft = mockDrafts.find((d) => d.id === draftId);
159
+ if (!draft) {
160
+ throw new Error(`Draft not found: ${draftId}`);
161
+ }
162
+ return draft;
163
+ },
164
+ async listDrafts(options) {
165
+ const maxResults = options?.maxResults ?? 10;
166
+ const drafts = mockDrafts.slice(0, maxResults).map((d) => ({
167
+ id: d.id,
168
+ message: {
169
+ id: d.message.id,
170
+ threadId: d.message.threadId,
171
+ },
172
+ }));
173
+ return {
174
+ drafts,
175
+ resultSizeEstimate: drafts.length,
176
+ };
177
+ },
178
+ async deleteDraft(draftId) {
179
+ const index = mockDrafts.findIndex((d) => d.id === draftId);
180
+ if (index === -1) {
181
+ throw new Error(`Draft not found: ${draftId}`);
182
+ }
183
+ mockDrafts.splice(index, 1);
184
+ },
185
+ async sendMessage(options) {
186
+ const sentMessage = {
187
+ id: `msg_${messageIdCounter++}`,
188
+ threadId: options.threadId || `thread_${messageIdCounter}`,
189
+ labelIds: ['SENT'],
190
+ snippet: options.body.substring(0, 100),
191
+ historyId: '12348',
192
+ internalDate: String(Date.now()),
193
+ payload: {
194
+ mimeType: 'text/plain',
195
+ headers: [
196
+ { name: 'Subject', value: options.subject },
197
+ { name: 'From', value: 'me@example.com' },
198
+ { name: 'To', value: options.to },
199
+ { name: 'Date', value: new Date().toISOString() },
200
+ ],
201
+ body: {
202
+ size: options.body.length,
203
+ data: Buffer.from(options.body).toString('base64url'),
204
+ },
205
+ },
206
+ };
207
+ return sentMessage;
208
+ },
209
+ async sendDraft(draftId) {
210
+ const draft = mockDrafts.find((d) => d.id === draftId);
211
+ if (!draft) {
212
+ throw new Error(`Draft not found: ${draftId}`);
213
+ }
214
+ // Remove from drafts
215
+ const index = mockDrafts.findIndex((d) => d.id === draftId);
216
+ mockDrafts.splice(index, 1);
217
+ // Return the message with SENT label
218
+ return {
219
+ ...draft.message,
220
+ labelIds: ['SENT'],
221
+ };
222
+ },
87
223
  };
88
224
  }
89
225
  // =============================================================================
@@ -91,7 +227,7 @@ function createMockClient() {
91
227
  // =============================================================================
92
228
  async function main() {
93
229
  // Create server with mock client
94
- const { server, registerHandlers } = createMCPServer();
230
+ const { server, registerHandlers } = createMCPServer({ version: VERSION });
95
231
  // Register handlers with mock client factory
96
232
  await registerHandlers(server, createMockClient);
97
233
  // Start server with stdio transport
package/build/index.js CHANGED
@@ -1,7 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
+ import { readFileSync } from 'fs';
4
+ import { dirname, join } from 'path';
5
+ import { fileURLToPath } from 'url';
3
6
  import { createMCPServer } from '../shared/index.js';
4
- import { logServerStart, logError } from '../shared/logging.js';
7
+ import { logServerStart, logError, logWarning } from '../shared/logging.js';
8
+ // Read version from package.json
9
+ const __dirname = dirname(fileURLToPath(import.meta.url));
10
+ const packageJsonPath = join(__dirname, '..', 'package.json');
11
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
12
+ const VERSION = packageJson.version;
5
13
  // =============================================================================
6
14
  // ENVIRONMENT VALIDATION
7
15
  // =============================================================================
@@ -30,8 +38,13 @@ function validateEnvironment() {
30
38
  console.error('\nSetup steps:');
31
39
  console.error(' 1. Go to https://console.cloud.google.com/');
32
40
  console.error(' 2. Create a service account with domain-wide delegation');
33
- console.error(' 3. In Google Workspace Admin, grant gmail.readonly scope');
41
+ console.error(' 3. In Google Workspace Admin, grant required Gmail API scopes');
34
42
  console.error(' 4. Download the JSON key file and extract client_email and private_key');
43
+ console.error('\nOptional environment variables:');
44
+ console.error(' GMAIL_ENABLED_TOOLGROUPS: Comma-separated list of tool groups to enable');
45
+ console.error(' Valid groups: readonly, readwrite, readwrite_external');
46
+ console.error(' Default: all groups enabled');
47
+ console.error(' Example: GMAIL_ENABLED_TOOLGROUPS=readwrite');
35
48
  console.error('\n======================================================\n');
36
49
  process.exit(1);
37
50
  }
@@ -42,11 +55,15 @@ function validateEnvironment() {
42
55
  async function main() {
43
56
  // Step 1: Validate environment variables
44
57
  validateEnvironment();
45
- // Step 2: Create server using factory
46
- const { server, registerHandlers } = createMCPServer();
47
- // Step 3: Register all handlers (tools)
58
+ // Step 2: Log tool groups if configured
59
+ if (process.env.GMAIL_ENABLED_TOOLGROUPS) {
60
+ logWarning('config', `Enabled tool groups: ${process.env.GMAIL_ENABLED_TOOLGROUPS}`);
61
+ }
62
+ // Step 3: Create server using factory
63
+ const { server, registerHandlers } = createMCPServer({ version: VERSION });
64
+ // Step 4: Register all handlers (tools)
48
65
  await registerHandlers(server);
49
- // Step 4: Start server with stdio transport
66
+ // Step 5: Start server with stdio transport
50
67
  const transport = new StdioServerTransport();
51
68
  await server.connect(transport);
52
69
  logServerStart('Gmail');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gmail-workspace-mcp-server",
3
- "version": "0.0.3",
3
+ "version": "0.0.4",
4
4
  "description": "MCP server for Gmail integration with service account support",
5
5
  "main": "build/index.js",
6
6
  "type": "module",
@@ -0,0 +1,42 @@
1
+ import type { Email, EmailListItem } from '../../types.js';
2
+ interface Draft {
3
+ id: string;
4
+ message: Email;
5
+ }
6
+ interface DraftListItem {
7
+ id: string;
8
+ message: EmailListItem;
9
+ }
10
+ /**
11
+ * Creates a new draft email
12
+ */
13
+ export declare function createDraft(baseUrl: string, headers: Record<string, string>, from: string, options: {
14
+ to: string;
15
+ subject: string;
16
+ body: string;
17
+ cc?: string;
18
+ bcc?: string;
19
+ threadId?: string;
20
+ inReplyTo?: string;
21
+ references?: string;
22
+ }): Promise<Draft>;
23
+ /**
24
+ * Gets a draft by ID
25
+ */
26
+ export declare function getDraft(baseUrl: string, headers: Record<string, string>, draftId: string): Promise<Draft>;
27
+ /**
28
+ * Lists drafts
29
+ */
30
+ export declare function listDrafts(baseUrl: string, headers: Record<string, string>, options?: {
31
+ maxResults?: number;
32
+ pageToken?: string;
33
+ }): Promise<{
34
+ drafts: DraftListItem[];
35
+ nextPageToken?: string;
36
+ resultSizeEstimate?: number;
37
+ }>;
38
+ /**
39
+ * Deletes a draft
40
+ */
41
+ export declare function deleteDraft(baseUrl: string, headers: Record<string, string>, draftId: string): Promise<void>;
42
+ export {};
@@ -0,0 +1,81 @@
1
+ import { handleApiError } from './api-errors.js';
2
+ import { buildMimeMessage, toBase64Url } from './mime-utils.js';
3
+ /**
4
+ * Creates a new draft email
5
+ */
6
+ export async function createDraft(baseUrl, headers, from, options) {
7
+ const url = `${baseUrl}/drafts`;
8
+ const rawMessage = buildMimeMessage(from, options);
9
+ const encodedMessage = toBase64Url(rawMessage);
10
+ const requestBody = {
11
+ message: {
12
+ raw: encodedMessage,
13
+ },
14
+ };
15
+ if (options.threadId) {
16
+ requestBody.message.threadId = options.threadId;
17
+ }
18
+ const response = await fetch(url, {
19
+ method: 'POST',
20
+ headers,
21
+ body: JSON.stringify(requestBody),
22
+ });
23
+ if (!response.ok) {
24
+ handleApiError(response.status, 'creating draft');
25
+ }
26
+ return (await response.json());
27
+ }
28
+ /**
29
+ * Gets a draft by ID
30
+ */
31
+ export async function getDraft(baseUrl, headers, draftId) {
32
+ const url = `${baseUrl}/drafts/${draftId}`;
33
+ const response = await fetch(url, {
34
+ method: 'GET',
35
+ headers,
36
+ });
37
+ if (!response.ok) {
38
+ handleApiError(response.status, 'getting draft', draftId);
39
+ }
40
+ return (await response.json());
41
+ }
42
+ /**
43
+ * Lists drafts
44
+ */
45
+ export async function listDrafts(baseUrl, headers, options) {
46
+ const params = new URLSearchParams();
47
+ if (options?.maxResults) {
48
+ params.set('maxResults', options.maxResults.toString());
49
+ }
50
+ if (options?.pageToken) {
51
+ params.set('pageToken', options.pageToken);
52
+ }
53
+ const queryString = params.toString();
54
+ const url = `${baseUrl}/drafts${queryString ? `?${queryString}` : ''}`;
55
+ const response = await fetch(url, {
56
+ method: 'GET',
57
+ headers,
58
+ });
59
+ if (!response.ok) {
60
+ handleApiError(response.status, 'listing drafts');
61
+ }
62
+ const data = (await response.json());
63
+ return {
64
+ drafts: data.drafts ?? [],
65
+ nextPageToken: data.nextPageToken,
66
+ resultSizeEstimate: data.resultSizeEstimate,
67
+ };
68
+ }
69
+ /**
70
+ * Deletes a draft
71
+ */
72
+ export async function deleteDraft(baseUrl, headers, draftId) {
73
+ const url = `${baseUrl}/drafts/${draftId}`;
74
+ const response = await fetch(url, {
75
+ method: 'DELETE',
76
+ headers,
77
+ });
78
+ if (!response.ok) {
79
+ handleApiError(response.status, 'deleting draft', draftId);
80
+ }
81
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * MIME message utilities for building and encoding email messages
3
+ */
4
+ export interface MimeMessageOptions {
5
+ to: string;
6
+ subject: string;
7
+ body: string;
8
+ cc?: string;
9
+ bcc?: string;
10
+ inReplyTo?: string;
11
+ references?: string;
12
+ }
13
+ /**
14
+ * Builds a MIME message from email options
15
+ */
16
+ export declare function buildMimeMessage(from: string, options: MimeMessageOptions): string;
17
+ /**
18
+ * Converts a string to base64url encoding (RFC 4648)
19
+ */
20
+ export declare function toBase64Url(str: string): string;
@@ -0,0 +1,38 @@
1
+ /**
2
+ * MIME message utilities for building and encoding email messages
3
+ */
4
+ /**
5
+ * Builds a MIME message from email options
6
+ */
7
+ export function buildMimeMessage(from, options) {
8
+ const headers = [
9
+ `From: ${from}`,
10
+ `To: ${options.to}`,
11
+ `Subject: ${options.subject}`,
12
+ 'MIME-Version: 1.0',
13
+ 'Content-Type: text/plain; charset=utf-8',
14
+ ];
15
+ if (options.cc) {
16
+ headers.push(`Cc: ${options.cc}`);
17
+ }
18
+ if (options.bcc) {
19
+ headers.push(`Bcc: ${options.bcc}`);
20
+ }
21
+ if (options.inReplyTo) {
22
+ headers.push(`In-Reply-To: ${options.inReplyTo}`);
23
+ }
24
+ if (options.references) {
25
+ headers.push(`References: ${options.references}`);
26
+ }
27
+ return headers.join('\r\n') + '\r\n\r\n' + options.body;
28
+ }
29
+ /**
30
+ * Converts a string to base64url encoding (RFC 4648)
31
+ */
32
+ export function toBase64Url(str) {
33
+ return Buffer.from(str, 'utf-8')
34
+ .toString('base64')
35
+ .replace(/\+/g, '-')
36
+ .replace(/\//g, '_')
37
+ .replace(/=+$/, '');
38
+ }
@@ -0,0 +1,9 @@
1
+ import type { Email } from '../../types.js';
2
+ /**
3
+ * Modifies labels on a message
4
+ * Used for marking read/unread, starring, archiving, etc.
5
+ */
6
+ export declare function modifyMessage(baseUrl: string, headers: Record<string, string>, messageId: string, options: {
7
+ addLabelIds?: string[];
8
+ removeLabelIds?: string[];
9
+ }): Promise<Email>;
@@ -0,0 +1,20 @@
1
+ import { handleApiError } from './api-errors.js';
2
+ /**
3
+ * Modifies labels on a message
4
+ * Used for marking read/unread, starring, archiving, etc.
5
+ */
6
+ export async function modifyMessage(baseUrl, headers, messageId, options) {
7
+ const url = `${baseUrl}/messages/${messageId}/modify`;
8
+ const response = await fetch(url, {
9
+ method: 'POST',
10
+ headers,
11
+ body: JSON.stringify({
12
+ addLabelIds: options.addLabelIds || [],
13
+ removeLabelIds: options.removeLabelIds || [],
14
+ }),
15
+ });
16
+ if (!response.ok) {
17
+ handleApiError(response.status, 'modifying message', messageId);
18
+ }
19
+ return (await response.json());
20
+ }