@agenticmail/enterprise 0.5.75 → 0.5.77
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.
- package/dist/chunk-2HOTPWQ6.js +2191 -0
- package/dist/chunk-7S5VMGP7.js +898 -0
- package/dist/chunk-QZHWUMPS.js +898 -0
- package/dist/chunk-R5JPVOVE.js +2191 -0
- package/dist/chunk-SXSA3OQS.js +14351 -0
- package/dist/chunk-T36SA4K3.js +15035 -0
- package/dist/cli.js +1 -1
- package/dist/index.js +3 -3
- package/dist/runtime-IQ3PYZN5.js +47 -0
- package/dist/runtime-LFPQKGMJ.js +47 -0
- package/dist/server-5DCT62CG.js +12 -0
- package/dist/server-EZOBWT7K.js +12 -0
- package/dist/setup-42DBT5CL.js +20 -0
- package/dist/setup-OLC7UNFL.js +20 -0
- package/package.json +1 -1
- package/src/agent-tools/index.ts +59 -1
- package/src/agent-tools/tools/google/calendar.ts +230 -0
- package/src/agent-tools/tools/google/contacts.ts +209 -0
- package/src/agent-tools/tools/google/docs.ts +162 -0
- package/src/agent-tools/tools/google/drive.ts +262 -0
- package/src/agent-tools/tools/google/gmail.ts +696 -0
- package/src/agent-tools/tools/google/index.ts +45 -0
- package/src/agent-tools/tools/google/sheets.ts +215 -0
- package/src/agent-tools/tools/oauth-token-provider.ts +101 -0
- package/src/runtime/index.ts +23 -7
- package/src/runtime/types.ts +4 -0
- package/src/server.ts +23 -0
|
@@ -0,0 +1,696 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Google Gmail Tools
|
|
3
|
+
*
|
|
4
|
+
* Comprehensive Gmail API v1 tools for enterprise agents.
|
|
5
|
+
* Covers inbox management, sending, drafts, labels, search, threads, and attachments.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { AnyAgentTool, ToolCreationOptions } from '../../types.js';
|
|
9
|
+
import { jsonResult, errorResult } from '../../common.js';
|
|
10
|
+
import type { GoogleToolsConfig } from './index.js';
|
|
11
|
+
|
|
12
|
+
const BASE = 'https://gmail.googleapis.com/gmail/v1/users/me';
|
|
13
|
+
|
|
14
|
+
async function gmail(token: string, path: string, opts?: { method?: string; body?: any; query?: Record<string, string>; rawBody?: BodyInit; headers?: Record<string, string> }): Promise<any> {
|
|
15
|
+
const method = opts?.method || 'GET';
|
|
16
|
+
const url = new URL(BASE + path);
|
|
17
|
+
if (opts?.query) for (const [k, v] of Object.entries(opts.query)) { if (v !== undefined && v !== '') url.searchParams.set(k, v); }
|
|
18
|
+
const headers: Record<string, string> = { Authorization: `Bearer ${token}`, ...opts?.headers };
|
|
19
|
+
if (!opts?.rawBody && !opts?.headers?.['Content-Type']) headers['Content-Type'] = 'application/json';
|
|
20
|
+
const res = await fetch(url.toString(), {
|
|
21
|
+
method, headers,
|
|
22
|
+
body: opts?.rawBody || (opts?.body ? JSON.stringify(opts.body) : undefined),
|
|
23
|
+
});
|
|
24
|
+
if (!res.ok) {
|
|
25
|
+
const err = await res.text();
|
|
26
|
+
throw new Error(`Gmail API ${res.status}: ${err}`);
|
|
27
|
+
}
|
|
28
|
+
if (res.status === 204) return {};
|
|
29
|
+
return res.json();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function decodeBase64Url(str: string): string {
|
|
33
|
+
const padded = str.replace(/-/g, '+').replace(/_/g, '/');
|
|
34
|
+
return Buffer.from(padded, 'base64').toString('utf-8');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function encodeBase64Url(str: string): string {
|
|
38
|
+
return Buffer.from(str).toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function extractHeaders(headers: any[], names: string[]): Record<string, string> {
|
|
42
|
+
const result: Record<string, string> = {};
|
|
43
|
+
const lower = names.map(n => n.toLowerCase());
|
|
44
|
+
for (const h of headers || []) {
|
|
45
|
+
const idx = lower.indexOf(h.name?.toLowerCase());
|
|
46
|
+
if (idx >= 0) result[names[idx]] = h.value;
|
|
47
|
+
}
|
|
48
|
+
return result;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function parseMessage(msg: any, format: string): any {
|
|
52
|
+
const headers = msg.payload?.headers || [];
|
|
53
|
+
const h = extractHeaders(headers, ['From', 'To', 'Cc', 'Bcc', 'Subject', 'Date', 'Reply-To', 'Message-ID', 'In-Reply-To', 'References']);
|
|
54
|
+
const result: any = {
|
|
55
|
+
id: msg.id,
|
|
56
|
+
threadId: msg.threadId,
|
|
57
|
+
labelIds: msg.labelIds,
|
|
58
|
+
snippet: msg.snippet,
|
|
59
|
+
internalDate: msg.internalDate ? new Date(Number(msg.internalDate)).toISOString() : undefined,
|
|
60
|
+
sizeEstimate: msg.sizeEstimate,
|
|
61
|
+
from: h.From,
|
|
62
|
+
to: h.To,
|
|
63
|
+
cc: h.Cc,
|
|
64
|
+
bcc: h.Bcc,
|
|
65
|
+
subject: h.Subject,
|
|
66
|
+
date: h.Date,
|
|
67
|
+
replyTo: h['Reply-To'],
|
|
68
|
+
messageId: h['Message-ID'],
|
|
69
|
+
inReplyTo: h['In-Reply-To'],
|
|
70
|
+
isUnread: msg.labelIds?.includes('UNREAD'),
|
|
71
|
+
isStarred: msg.labelIds?.includes('STARRED'),
|
|
72
|
+
isImportant: msg.labelIds?.includes('IMPORTANT'),
|
|
73
|
+
isDraft: msg.labelIds?.includes('DRAFT'),
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
if (format !== 'metadata') {
|
|
77
|
+
// Extract body
|
|
78
|
+
const body = extractBody(msg.payload);
|
|
79
|
+
result.body = body.text?.slice(0, 80000);
|
|
80
|
+
result.bodyHtml = body.html?.slice(0, 20000);
|
|
81
|
+
result.truncated = (body.text?.length || 0) > 80000;
|
|
82
|
+
|
|
83
|
+
// Extract attachments metadata
|
|
84
|
+
const attachments = extractAttachments(msg.payload);
|
|
85
|
+
if (attachments.length) {
|
|
86
|
+
result.attachments = attachments;
|
|
87
|
+
result.attachmentCount = attachments.length;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return result;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function extractBody(payload: any): { text?: string; html?: string } {
|
|
95
|
+
if (!payload) return {};
|
|
96
|
+
if (payload.mimeType === 'text/plain' && payload.body?.data) {
|
|
97
|
+
return { text: decodeBase64Url(payload.body.data) };
|
|
98
|
+
}
|
|
99
|
+
if (payload.mimeType === 'text/html' && payload.body?.data) {
|
|
100
|
+
return { html: decodeBase64Url(payload.body.data) };
|
|
101
|
+
}
|
|
102
|
+
if (payload.parts) {
|
|
103
|
+
let text: string | undefined;
|
|
104
|
+
let html: string | undefined;
|
|
105
|
+
for (const part of payload.parts) {
|
|
106
|
+
if (part.mimeType === 'text/plain' && part.body?.data && !text) {
|
|
107
|
+
text = decodeBase64Url(part.body.data);
|
|
108
|
+
} else if (part.mimeType === 'text/html' && part.body?.data && !html) {
|
|
109
|
+
html = decodeBase64Url(part.body.data);
|
|
110
|
+
} else if (part.mimeType?.startsWith('multipart/') && part.parts) {
|
|
111
|
+
const nested = extractBody(part);
|
|
112
|
+
if (!text && nested.text) text = nested.text;
|
|
113
|
+
if (!html && nested.html) html = nested.html;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return { text, html };
|
|
117
|
+
}
|
|
118
|
+
return {};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function extractAttachments(payload: any): any[] {
|
|
122
|
+
const atts: any[] = [];
|
|
123
|
+
function walk(part: any) {
|
|
124
|
+
if (part.filename && part.body?.attachmentId) {
|
|
125
|
+
atts.push({
|
|
126
|
+
filename: part.filename,
|
|
127
|
+
mimeType: part.mimeType,
|
|
128
|
+
size: part.body.size,
|
|
129
|
+
attachmentId: part.body.attachmentId,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
if (part.parts) part.parts.forEach(walk);
|
|
133
|
+
}
|
|
134
|
+
if (payload) walk(payload);
|
|
135
|
+
return atts;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function buildRawEmail(opts: {
|
|
139
|
+
from?: string; to: string; cc?: string; bcc?: string;
|
|
140
|
+
subject: string; body: string; html?: string;
|
|
141
|
+
replyTo?: string; inReplyTo?: string; references?: string;
|
|
142
|
+
headers?: Record<string, string>;
|
|
143
|
+
}): string {
|
|
144
|
+
const boundary = `----=_Part_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
|
145
|
+
const lines: string[] = [];
|
|
146
|
+
lines.push(`MIME-Version: 1.0`);
|
|
147
|
+
if (opts.from) lines.push(`From: ${opts.from}`);
|
|
148
|
+
lines.push(`To: ${opts.to}`);
|
|
149
|
+
if (opts.cc) lines.push(`Cc: ${opts.cc}`);
|
|
150
|
+
if (opts.bcc) lines.push(`Bcc: ${opts.bcc}`);
|
|
151
|
+
lines.push(`Subject: ${opts.subject}`);
|
|
152
|
+
if (opts.replyTo) lines.push(`Reply-To: ${opts.replyTo}`);
|
|
153
|
+
if (opts.inReplyTo) lines.push(`In-Reply-To: ${opts.inReplyTo}`);
|
|
154
|
+
if (opts.references) lines.push(`References: ${opts.references}`);
|
|
155
|
+
if (opts.headers) {
|
|
156
|
+
for (const [k, v] of Object.entries(opts.headers)) lines.push(`${k}: ${v}`);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (opts.html) {
|
|
160
|
+
lines.push(`Content-Type: multipart/alternative; boundary="${boundary}"`);
|
|
161
|
+
lines.push('');
|
|
162
|
+
lines.push(`--${boundary}`);
|
|
163
|
+
lines.push('Content-Type: text/plain; charset=UTF-8');
|
|
164
|
+
lines.push('');
|
|
165
|
+
lines.push(opts.body);
|
|
166
|
+
lines.push(`--${boundary}`);
|
|
167
|
+
lines.push('Content-Type: text/html; charset=UTF-8');
|
|
168
|
+
lines.push('');
|
|
169
|
+
lines.push(opts.html);
|
|
170
|
+
lines.push(`--${boundary}--`);
|
|
171
|
+
} else {
|
|
172
|
+
lines.push('Content-Type: text/plain; charset=UTF-8');
|
|
173
|
+
lines.push('');
|
|
174
|
+
lines.push(opts.body);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return lines.join('\r\n');
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function createGmailTools(config: GoogleToolsConfig, _options?: ToolCreationOptions): AnyAgentTool[] {
|
|
181
|
+
const tp = config.tokenProvider;
|
|
182
|
+
return [
|
|
183
|
+
// ─── List / Search ─────────────────────────────────
|
|
184
|
+
{
|
|
185
|
+
name: 'gmail_search',
|
|
186
|
+
description: 'Search emails using Gmail search syntax. Supports all Gmail operators: from:, to:, subject:, has:attachment, is:unread, after:, before:, label:, in:, category:, larger:, smaller:, filename:, etc.',
|
|
187
|
+
category: 'utility' as const,
|
|
188
|
+
parameters: {
|
|
189
|
+
type: 'object' as const,
|
|
190
|
+
properties: {
|
|
191
|
+
query: { type: 'string', description: 'Gmail search query (e.g. "from:alice@example.com is:unread", "subject:invoice after:2026/01/01", "has:attachment filename:pdf")' },
|
|
192
|
+
maxResults: { type: 'number', description: 'Max messages (default: 20, max: 100)' },
|
|
193
|
+
labelIds: { type: 'string', description: 'Comma-separated label IDs to filter (e.g. "INBOX", "SENT", "STARRED", "IMPORTANT", "UNREAD")' },
|
|
194
|
+
includeSpamTrash: { type: 'string', description: '"true" to include spam/trash in results' },
|
|
195
|
+
pageToken: { type: 'string', description: 'Token for next page of results' },
|
|
196
|
+
},
|
|
197
|
+
required: [],
|
|
198
|
+
},
|
|
199
|
+
async execute(_id: string, params: any) {
|
|
200
|
+
try {
|
|
201
|
+
const token = await tp.getAccessToken();
|
|
202
|
+
const q: Record<string, string> = {
|
|
203
|
+
maxResults: String(Math.min(params.maxResults || 20, 100)),
|
|
204
|
+
};
|
|
205
|
+
if (params.query) q.q = params.query;
|
|
206
|
+
if (params.labelIds) q.labelIds = params.labelIds;
|
|
207
|
+
if (params.includeSpamTrash === 'true') q.includeSpamTrash = 'true';
|
|
208
|
+
if (params.pageToken) q.pageToken = params.pageToken;
|
|
209
|
+
|
|
210
|
+
const list = await gmail(token, '/messages', { query: q });
|
|
211
|
+
if (!list.messages?.length) {
|
|
212
|
+
return jsonResult({ messages: [], count: 0, resultSizeEstimate: list.resultSizeEstimate || 0 });
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Fetch each message with metadata
|
|
216
|
+
const messages = await Promise.all(
|
|
217
|
+
list.messages.slice(0, 25).map(async (m: any) => {
|
|
218
|
+
try {
|
|
219
|
+
const msg = await gmail(token, `/messages/${m.id}`, { query: { format: 'metadata', metadataHeaders: 'From,To,Cc,Subject,Date' } });
|
|
220
|
+
return parseMessage(msg, 'metadata');
|
|
221
|
+
} catch { return { id: m.id, threadId: m.threadId, error: 'Failed to fetch' }; }
|
|
222
|
+
})
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
return jsonResult({
|
|
226
|
+
messages,
|
|
227
|
+
count: messages.length,
|
|
228
|
+
resultSizeEstimate: list.resultSizeEstimate,
|
|
229
|
+
nextPageToken: list.nextPageToken,
|
|
230
|
+
hasMore: !!list.nextPageToken,
|
|
231
|
+
});
|
|
232
|
+
} catch (e: any) { return errorResult(e.message); }
|
|
233
|
+
},
|
|
234
|
+
},
|
|
235
|
+
|
|
236
|
+
// ─── Read Message ──────────────────────────────────
|
|
237
|
+
{
|
|
238
|
+
name: 'gmail_read',
|
|
239
|
+
description: 'Read the full content of an email message including body text, HTML, and attachment info.',
|
|
240
|
+
category: 'utility' as const,
|
|
241
|
+
parameters: {
|
|
242
|
+
type: 'object' as const,
|
|
243
|
+
properties: {
|
|
244
|
+
messageId: { type: 'string', description: 'Message ID (required)' },
|
|
245
|
+
markAsRead: { type: 'string', description: '"true" to mark as read when opening (default: "false")' },
|
|
246
|
+
},
|
|
247
|
+
required: ['messageId'],
|
|
248
|
+
},
|
|
249
|
+
async execute(_id: string, params: any) {
|
|
250
|
+
try {
|
|
251
|
+
const token = await tp.getAccessToken();
|
|
252
|
+
const msg = await gmail(token, `/messages/${params.messageId}`, { query: { format: 'full' } });
|
|
253
|
+
const parsed = parseMessage(msg, 'full');
|
|
254
|
+
|
|
255
|
+
if (params.markAsRead === 'true' && parsed.isUnread) {
|
|
256
|
+
await gmail(token, `/messages/${params.messageId}/modify`, {
|
|
257
|
+
method: 'POST', body: { removeLabelIds: ['UNREAD'] },
|
|
258
|
+
}).catch(() => {});
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return jsonResult(parsed);
|
|
262
|
+
} catch (e: any) { return errorResult(e.message); }
|
|
263
|
+
},
|
|
264
|
+
},
|
|
265
|
+
|
|
266
|
+
// ─── Read Thread ───────────────────────────────────
|
|
267
|
+
{
|
|
268
|
+
name: 'gmail_thread',
|
|
269
|
+
description: 'Read an entire email thread/conversation. Returns all messages in the thread.',
|
|
270
|
+
category: 'utility' as const,
|
|
271
|
+
parameters: {
|
|
272
|
+
type: 'object' as const,
|
|
273
|
+
properties: {
|
|
274
|
+
threadId: { type: 'string', description: 'Thread ID (required)' },
|
|
275
|
+
format: { type: 'string', description: '"full" (default) or "metadata" (headers only, faster)' },
|
|
276
|
+
},
|
|
277
|
+
required: ['threadId'],
|
|
278
|
+
},
|
|
279
|
+
async execute(_id: string, params: any) {
|
|
280
|
+
try {
|
|
281
|
+
const token = await tp.getAccessToken();
|
|
282
|
+
const fmt = params.format || 'full';
|
|
283
|
+
const thread = await gmail(token, `/threads/${params.threadId}`, {
|
|
284
|
+
query: { format: fmt, ...(fmt === 'metadata' ? { metadataHeaders: 'From,To,Cc,Subject,Date' } : {}) },
|
|
285
|
+
});
|
|
286
|
+
const messages = (thread.messages || []).map((m: any) => parseMessage(m, fmt));
|
|
287
|
+
return jsonResult({
|
|
288
|
+
threadId: thread.id, snippet: thread.snippet,
|
|
289
|
+
messages, messageCount: messages.length,
|
|
290
|
+
});
|
|
291
|
+
} catch (e: any) { return errorResult(e.message); }
|
|
292
|
+
},
|
|
293
|
+
},
|
|
294
|
+
|
|
295
|
+
// ─── Send Email ────────────────────────────────────
|
|
296
|
+
{
|
|
297
|
+
name: 'gmail_send',
|
|
298
|
+
description: 'Send an email. Supports plain text, HTML, CC/BCC, reply threading, and custom headers.',
|
|
299
|
+
category: 'utility' as const,
|
|
300
|
+
parameters: {
|
|
301
|
+
type: 'object' as const,
|
|
302
|
+
properties: {
|
|
303
|
+
to: { type: 'string', description: 'Recipient email(s), comma-separated (required)' },
|
|
304
|
+
subject: { type: 'string', description: 'Email subject (required)' },
|
|
305
|
+
body: { type: 'string', description: 'Plain text body (required)' },
|
|
306
|
+
html: { type: 'string', description: 'HTML body (optional — sends multipart with text fallback)' },
|
|
307
|
+
cc: { type: 'string', description: 'CC recipients, comma-separated' },
|
|
308
|
+
bcc: { type: 'string', description: 'BCC recipients, comma-separated' },
|
|
309
|
+
replyTo: { type: 'string', description: 'Reply-To address' },
|
|
310
|
+
threadId: { type: 'string', description: 'Thread ID to reply in (for threading)' },
|
|
311
|
+
inReplyTo: { type: 'string', description: 'Message-ID being replied to (for proper threading)' },
|
|
312
|
+
references: { type: 'string', description: 'Message-ID references chain' },
|
|
313
|
+
},
|
|
314
|
+
required: ['to', 'subject', 'body'],
|
|
315
|
+
},
|
|
316
|
+
async execute(_id: string, params: any) {
|
|
317
|
+
try {
|
|
318
|
+
const token = await tp.getAccessToken();
|
|
319
|
+
const email = tp.getEmail();
|
|
320
|
+
const raw = buildRawEmail({
|
|
321
|
+
from: email, to: params.to, cc: params.cc, bcc: params.bcc,
|
|
322
|
+
subject: params.subject, body: params.body, html: params.html,
|
|
323
|
+
replyTo: params.replyTo, inReplyTo: params.inReplyTo, references: params.references,
|
|
324
|
+
});
|
|
325
|
+
const sendBody: any = { raw: encodeBase64Url(raw) };
|
|
326
|
+
if (params.threadId) sendBody.threadId = params.threadId;
|
|
327
|
+
const result = await gmail(token, '/messages/send', { method: 'POST', body: sendBody });
|
|
328
|
+
return jsonResult({ sent: true, messageId: result.id, threadId: result.threadId, labelIds: result.labelIds });
|
|
329
|
+
} catch (e: any) { return errorResult(e.message); }
|
|
330
|
+
},
|
|
331
|
+
},
|
|
332
|
+
|
|
333
|
+
// ─── Reply to Email ────────────────────────────────
|
|
334
|
+
{
|
|
335
|
+
name: 'gmail_reply',
|
|
336
|
+
description: 'Reply to an email. Auto-sets threading headers (In-Reply-To, References, threadId).',
|
|
337
|
+
category: 'utility' as const,
|
|
338
|
+
parameters: {
|
|
339
|
+
type: 'object' as const,
|
|
340
|
+
properties: {
|
|
341
|
+
messageId: { type: 'string', description: 'Message ID to reply to (required)' },
|
|
342
|
+
body: { type: 'string', description: 'Reply text (required)' },
|
|
343
|
+
html: { type: 'string', description: 'HTML reply body (optional)' },
|
|
344
|
+
replyAll: { type: 'string', description: '"true" to reply to all recipients' },
|
|
345
|
+
cc: { type: 'string', description: 'Additional CC recipients' },
|
|
346
|
+
},
|
|
347
|
+
required: ['messageId', 'body'],
|
|
348
|
+
},
|
|
349
|
+
async execute(_id: string, params: any) {
|
|
350
|
+
try {
|
|
351
|
+
const token = await tp.getAccessToken();
|
|
352
|
+
const email = tp.getEmail();
|
|
353
|
+
|
|
354
|
+
// Fetch original message for threading
|
|
355
|
+
const original = await gmail(token, `/messages/${params.messageId}`, { query: { format: 'metadata', metadataHeaders: 'From,To,Cc,Subject,Message-ID,References' } });
|
|
356
|
+
const oh = extractHeaders(original.payload?.headers || [], ['From', 'To', 'Cc', 'Subject', 'Message-ID', 'References']);
|
|
357
|
+
|
|
358
|
+
const to = params.replyAll === 'true'
|
|
359
|
+
? [oh.From, ...(oh.To || '').split(','), ...(oh.Cc || '').split(',')].filter(e => e && !e.includes(email || '___')).join(',')
|
|
360
|
+
: oh.From;
|
|
361
|
+
|
|
362
|
+
const subject = oh.Subject?.startsWith('Re:') ? oh.Subject : `Re: ${oh.Subject || ''}`;
|
|
363
|
+
const references = [oh.References, oh['Message-ID']].filter(Boolean).join(' ');
|
|
364
|
+
|
|
365
|
+
const raw = buildRawEmail({
|
|
366
|
+
from: email, to, cc: params.cc,
|
|
367
|
+
subject, body: params.body, html: params.html,
|
|
368
|
+
inReplyTo: oh['Message-ID'], references,
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
const result = await gmail(token, '/messages/send', {
|
|
372
|
+
method: 'POST',
|
|
373
|
+
body: { raw: encodeBase64Url(raw), threadId: original.threadId },
|
|
374
|
+
});
|
|
375
|
+
return jsonResult({ sent: true, messageId: result.id, threadId: result.threadId, inReplyTo: oh['Message-ID'] });
|
|
376
|
+
} catch (e: any) { return errorResult(e.message); }
|
|
377
|
+
},
|
|
378
|
+
},
|
|
379
|
+
|
|
380
|
+
// ─── Forward Email ─────────────────────────────────
|
|
381
|
+
{
|
|
382
|
+
name: 'gmail_forward',
|
|
383
|
+
description: 'Forward an email to another recipient, preserving the original message content.',
|
|
384
|
+
category: 'utility' as const,
|
|
385
|
+
parameters: {
|
|
386
|
+
type: 'object' as const,
|
|
387
|
+
properties: {
|
|
388
|
+
messageId: { type: 'string', description: 'Message ID to forward (required)' },
|
|
389
|
+
to: { type: 'string', description: 'Forward to (required)' },
|
|
390
|
+
body: { type: 'string', description: 'Additional message to include above forwarded content' },
|
|
391
|
+
cc: { type: 'string', description: 'CC recipients' },
|
|
392
|
+
},
|
|
393
|
+
required: ['messageId', 'to'],
|
|
394
|
+
},
|
|
395
|
+
async execute(_id: string, params: any) {
|
|
396
|
+
try {
|
|
397
|
+
const token = await tp.getAccessToken();
|
|
398
|
+
const email = tp.getEmail();
|
|
399
|
+
|
|
400
|
+
const original = await gmail(token, `/messages/${params.messageId}`, { query: { format: 'full' } });
|
|
401
|
+
const parsed = parseMessage(original, 'full');
|
|
402
|
+
const fwdBody = [
|
|
403
|
+
params.body || '',
|
|
404
|
+
'',
|
|
405
|
+
'---------- Forwarded message ----------',
|
|
406
|
+
`From: ${parsed.from}`,
|
|
407
|
+
`Date: ${parsed.date}`,
|
|
408
|
+
`Subject: ${parsed.subject}`,
|
|
409
|
+
`To: ${parsed.to}`,
|
|
410
|
+
parsed.cc ? `Cc: ${parsed.cc}` : '',
|
|
411
|
+
'',
|
|
412
|
+
parsed.body || '',
|
|
413
|
+
].filter(Boolean).join('\n');
|
|
414
|
+
|
|
415
|
+
const raw = buildRawEmail({
|
|
416
|
+
from: email, to: params.to, cc: params.cc,
|
|
417
|
+
subject: `Fwd: ${parsed.subject || ''}`,
|
|
418
|
+
body: fwdBody,
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
const result = await gmail(token, '/messages/send', { method: 'POST', body: { raw: encodeBase64Url(raw) } });
|
|
422
|
+
return jsonResult({ forwarded: true, messageId: result.id, threadId: result.threadId, originalMessageId: params.messageId });
|
|
423
|
+
} catch (e: any) { return errorResult(e.message); }
|
|
424
|
+
},
|
|
425
|
+
},
|
|
426
|
+
|
|
427
|
+
// ─── Modify Labels ─────────────────────────────────
|
|
428
|
+
{
|
|
429
|
+
name: 'gmail_modify',
|
|
430
|
+
description: 'Add or remove labels from messages. Use for: mark read/unread, star/unstar, archive, move to trash, apply labels.',
|
|
431
|
+
category: 'utility' as const,
|
|
432
|
+
parameters: {
|
|
433
|
+
type: 'object' as const,
|
|
434
|
+
properties: {
|
|
435
|
+
messageIds: { type: 'string', description: 'Comma-separated message IDs (required)' },
|
|
436
|
+
addLabels: { type: 'string', description: 'Comma-separated label IDs to add (e.g. "STARRED", "IMPORTANT", "Label_123")' },
|
|
437
|
+
removeLabels: { type: 'string', description: 'Comma-separated label IDs to remove (e.g. "UNREAD", "INBOX")' },
|
|
438
|
+
},
|
|
439
|
+
required: ['messageIds'],
|
|
440
|
+
},
|
|
441
|
+
async execute(_id: string, params: any) {
|
|
442
|
+
try {
|
|
443
|
+
const token = await tp.getAccessToken();
|
|
444
|
+
const ids = params.messageIds.split(',').map((s: string) => s.trim()).filter(Boolean);
|
|
445
|
+
const addLabels = params.addLabels ? params.addLabels.split(',').map((s: string) => s.trim()) : [];
|
|
446
|
+
const removeLabels = params.removeLabels ? params.removeLabels.split(',').map((s: string) => s.trim()) : [];
|
|
447
|
+
|
|
448
|
+
if (ids.length === 1) {
|
|
449
|
+
await gmail(token, `/messages/${ids[0]}/modify`, {
|
|
450
|
+
method: 'POST', body: { addLabelIds: addLabels, removeLabelIds: removeLabels },
|
|
451
|
+
});
|
|
452
|
+
} else {
|
|
453
|
+
// Batch modify
|
|
454
|
+
await gmail(token, '/messages/batchModify', {
|
|
455
|
+
method: 'POST', body: { ids, addLabelIds: addLabels, removeLabelIds: removeLabels },
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
return jsonResult({ modified: true, count: ids.length, addedLabels: addLabels, removedLabels: removeLabels });
|
|
460
|
+
} catch (e: any) { return errorResult(e.message); }
|
|
461
|
+
},
|
|
462
|
+
},
|
|
463
|
+
|
|
464
|
+
// ─── Trash / Delete ────────────────────────────────
|
|
465
|
+
{
|
|
466
|
+
name: 'gmail_trash',
|
|
467
|
+
description: 'Move messages to trash or permanently delete them.',
|
|
468
|
+
category: 'utility' as const,
|
|
469
|
+
parameters: {
|
|
470
|
+
type: 'object' as const,
|
|
471
|
+
properties: {
|
|
472
|
+
messageIds: { type: 'string', description: 'Comma-separated message IDs (required)' },
|
|
473
|
+
permanent: { type: 'string', description: '"true" to permanently delete (IRREVERSIBLE). Default: moves to trash.' },
|
|
474
|
+
},
|
|
475
|
+
required: ['messageIds'],
|
|
476
|
+
},
|
|
477
|
+
async execute(_id: string, params: any) {
|
|
478
|
+
try {
|
|
479
|
+
const token = await tp.getAccessToken();
|
|
480
|
+
const ids = params.messageIds.split(',').map((s: string) => s.trim()).filter(Boolean);
|
|
481
|
+
const permanent = params.permanent === 'true';
|
|
482
|
+
|
|
483
|
+
for (const id of ids) {
|
|
484
|
+
if (permanent) {
|
|
485
|
+
await gmail(token, `/messages/${id}`, { method: 'DELETE' });
|
|
486
|
+
} else {
|
|
487
|
+
await gmail(token, `/messages/${id}/trash`, { method: 'POST' });
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
return jsonResult({ [permanent ? 'deleted' : 'trashed']: true, count: ids.length });
|
|
492
|
+
} catch (e: any) { return errorResult(e.message); }
|
|
493
|
+
},
|
|
494
|
+
},
|
|
495
|
+
|
|
496
|
+
// ─── Labels ────────────────────────────────────────
|
|
497
|
+
{
|
|
498
|
+
name: 'gmail_labels',
|
|
499
|
+
description: 'List all labels/folders, or create/delete a label.',
|
|
500
|
+
category: 'utility' as const,
|
|
501
|
+
parameters: {
|
|
502
|
+
type: 'object' as const,
|
|
503
|
+
properties: {
|
|
504
|
+
action: { type: 'string', description: '"list" (default), "create", or "delete"' },
|
|
505
|
+
name: { type: 'string', description: 'Label name (for create)' },
|
|
506
|
+
labelId: { type: 'string', description: 'Label ID (for delete)' },
|
|
507
|
+
color: { type: 'string', description: 'Label background color hex (for create, e.g. "#4986e7")' },
|
|
508
|
+
},
|
|
509
|
+
required: [],
|
|
510
|
+
},
|
|
511
|
+
async execute(_id: string, params: any) {
|
|
512
|
+
try {
|
|
513
|
+
const token = await tp.getAccessToken();
|
|
514
|
+
const action = params.action || 'list';
|
|
515
|
+
|
|
516
|
+
if (action === 'create') {
|
|
517
|
+
if (!params.name) return errorResult('name is required for create');
|
|
518
|
+
const body: any = { name: params.name, labelListVisibility: 'labelShow', messageListVisibility: 'show' };
|
|
519
|
+
if (params.color) body.color = { backgroundColor: params.color, textColor: '#ffffff' };
|
|
520
|
+
const label = await gmail(token, '/labels', { method: 'POST', body });
|
|
521
|
+
return jsonResult({ created: true, labelId: label.id, name: label.name });
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
if (action === 'delete') {
|
|
525
|
+
if (!params.labelId) return errorResult('labelId is required for delete');
|
|
526
|
+
await gmail(token, `/labels/${params.labelId}`, { method: 'DELETE' });
|
|
527
|
+
return jsonResult({ deleted: true, labelId: params.labelId });
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// List
|
|
531
|
+
const data = await gmail(token, '/labels');
|
|
532
|
+
const labels = (data.labels || []).map((l: any) => ({
|
|
533
|
+
id: l.id, name: l.name, type: l.type,
|
|
534
|
+
messagesTotal: l.messagesTotal, messagesUnread: l.messagesUnread,
|
|
535
|
+
threadsTotal: l.threadsTotal, threadsUnread: l.threadsUnread,
|
|
536
|
+
color: l.color?.backgroundColor,
|
|
537
|
+
}));
|
|
538
|
+
const system = labels.filter((l: any) => l.type === 'system');
|
|
539
|
+
const user = labels.filter((l: any) => l.type === 'user');
|
|
540
|
+
return jsonResult({ labels: [...system, ...user], systemCount: system.length, userCount: user.length });
|
|
541
|
+
} catch (e: any) { return errorResult(e.message); }
|
|
542
|
+
},
|
|
543
|
+
},
|
|
544
|
+
|
|
545
|
+
// ─── Drafts ────────────────────────────────────────
|
|
546
|
+
{
|
|
547
|
+
name: 'gmail_drafts',
|
|
548
|
+
description: 'List, create, update, send, or delete email drafts.',
|
|
549
|
+
category: 'utility' as const,
|
|
550
|
+
parameters: {
|
|
551
|
+
type: 'object' as const,
|
|
552
|
+
properties: {
|
|
553
|
+
action: { type: 'string', description: '"list" (default), "create", "update", "send", or "delete"' },
|
|
554
|
+
draftId: { type: 'string', description: 'Draft ID (for update/send/delete)' },
|
|
555
|
+
to: { type: 'string', description: 'Recipient (for create/update)' },
|
|
556
|
+
subject: { type: 'string', description: 'Subject (for create/update)' },
|
|
557
|
+
body: { type: 'string', description: 'Body text (for create/update)' },
|
|
558
|
+
html: { type: 'string', description: 'HTML body (for create/update)' },
|
|
559
|
+
cc: { type: 'string', description: 'CC recipients (for create/update)' },
|
|
560
|
+
maxResults: { type: 'number', description: 'Max drafts to list (default: 20)' },
|
|
561
|
+
},
|
|
562
|
+
required: [],
|
|
563
|
+
},
|
|
564
|
+
async execute(_id: string, params: any) {
|
|
565
|
+
try {
|
|
566
|
+
const token = await tp.getAccessToken();
|
|
567
|
+
const action = params.action || 'list';
|
|
568
|
+
const email = tp.getEmail();
|
|
569
|
+
|
|
570
|
+
if (action === 'create' || action === 'update') {
|
|
571
|
+
if (!params.to || !params.subject) return errorResult('to and subject required');
|
|
572
|
+
const raw = buildRawEmail({
|
|
573
|
+
from: email, to: params.to, cc: params.cc,
|
|
574
|
+
subject: params.subject, body: params.body || '', html: params.html,
|
|
575
|
+
});
|
|
576
|
+
const draftBody = { message: { raw: encodeBase64Url(raw) } };
|
|
577
|
+
|
|
578
|
+
if (action === 'update' && params.draftId) {
|
|
579
|
+
const result = await gmail(token, `/drafts/${params.draftId}`, { method: 'PUT', body: draftBody });
|
|
580
|
+
return jsonResult({ updated: true, draftId: result.id });
|
|
581
|
+
}
|
|
582
|
+
const result = await gmail(token, '/drafts', { method: 'POST', body: draftBody });
|
|
583
|
+
return jsonResult({ created: true, draftId: result.id, messageId: result.message?.id });
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
if (action === 'send') {
|
|
587
|
+
if (!params.draftId) return errorResult('draftId required for send');
|
|
588
|
+
const result = await gmail(token, '/drafts/send', { method: 'POST', body: { id: params.draftId } });
|
|
589
|
+
return jsonResult({ sent: true, messageId: result.id, threadId: result.threadId });
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
if (action === 'delete') {
|
|
593
|
+
if (!params.draftId) return errorResult('draftId required');
|
|
594
|
+
await gmail(token, `/drafts/${params.draftId}`, { method: 'DELETE' });
|
|
595
|
+
return jsonResult({ deleted: true, draftId: params.draftId });
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// List drafts
|
|
599
|
+
const data = await gmail(token, '/drafts', { query: { maxResults: String(params.maxResults || 20) } });
|
|
600
|
+
const drafts = (data.drafts || []).map((d: any) => ({
|
|
601
|
+
draftId: d.id, messageId: d.message?.id, threadId: d.message?.threadId,
|
|
602
|
+
}));
|
|
603
|
+
return jsonResult({ drafts, count: drafts.length, nextPageToken: data.nextPageToken });
|
|
604
|
+
} catch (e: any) { return errorResult(e.message); }
|
|
605
|
+
},
|
|
606
|
+
},
|
|
607
|
+
|
|
608
|
+
// ─── Get Attachment ────────────────────────────────
|
|
609
|
+
{
|
|
610
|
+
name: 'gmail_attachment',
|
|
611
|
+
description: 'Download an email attachment. Returns base64-encoded data.',
|
|
612
|
+
category: 'utility' as const,
|
|
613
|
+
parameters: {
|
|
614
|
+
type: 'object' as const,
|
|
615
|
+
properties: {
|
|
616
|
+
messageId: { type: 'string', description: 'Message ID (required)' },
|
|
617
|
+
attachmentId: { type: 'string', description: 'Attachment ID from gmail_read results (required)' },
|
|
618
|
+
},
|
|
619
|
+
required: ['messageId', 'attachmentId'],
|
|
620
|
+
},
|
|
621
|
+
async execute(_id: string, params: any) {
|
|
622
|
+
try {
|
|
623
|
+
const token = await tp.getAccessToken();
|
|
624
|
+
const data = await gmail(token, `/messages/${params.messageId}/attachments/${params.attachmentId}`);
|
|
625
|
+
return jsonResult({
|
|
626
|
+
attachmentId: params.attachmentId,
|
|
627
|
+
size: data.size,
|
|
628
|
+
data: data.data, // base64url encoded
|
|
629
|
+
});
|
|
630
|
+
} catch (e: any) { return errorResult(e.message); }
|
|
631
|
+
},
|
|
632
|
+
},
|
|
633
|
+
|
|
634
|
+
// ─── Profile / Quota ───────────────────────────────
|
|
635
|
+
{
|
|
636
|
+
name: 'gmail_profile',
|
|
637
|
+
description: 'Get the agent\'s Gmail profile: email address, total messages, threads count, and history ID.',
|
|
638
|
+
category: 'utility' as const,
|
|
639
|
+
parameters: { type: 'object' as const, properties: {}, required: [] },
|
|
640
|
+
async execute(_id: string) {
|
|
641
|
+
try {
|
|
642
|
+
const token = await tp.getAccessToken();
|
|
643
|
+
const profile = await gmail(token, '/profile');
|
|
644
|
+
return jsonResult({
|
|
645
|
+
emailAddress: profile.emailAddress,
|
|
646
|
+
messagesTotal: profile.messagesTotal,
|
|
647
|
+
threadsTotal: profile.threadsTotal,
|
|
648
|
+
historyId: profile.historyId,
|
|
649
|
+
});
|
|
650
|
+
} catch (e: any) { return errorResult(e.message); }
|
|
651
|
+
},
|
|
652
|
+
},
|
|
653
|
+
|
|
654
|
+
// ─── Vacation / Auto-Reply ─────────────────────────
|
|
655
|
+
{
|
|
656
|
+
name: 'gmail_vacation',
|
|
657
|
+
description: 'Get or set vacation/auto-reply settings.',
|
|
658
|
+
category: 'utility' as const,
|
|
659
|
+
parameters: {
|
|
660
|
+
type: 'object' as const,
|
|
661
|
+
properties: {
|
|
662
|
+
action: { type: 'string', description: '"get" (default) or "set"' },
|
|
663
|
+
enabled: { type: 'string', description: '"true" or "false" (for set)' },
|
|
664
|
+
subject: { type: 'string', description: 'Auto-reply subject (for set)' },
|
|
665
|
+
body: { type: 'string', description: 'Auto-reply message body (for set)' },
|
|
666
|
+
startTime: { type: 'string', description: 'Start date (ISO 8601, for set)' },
|
|
667
|
+
endTime: { type: 'string', description: 'End date (ISO 8601, for set)' },
|
|
668
|
+
restrictToContacts: { type: 'string', description: '"true" to only reply to contacts (for set)' },
|
|
669
|
+
restrictToDomain: { type: 'string', description: '"true" to only reply to same domain (for set)' },
|
|
670
|
+
},
|
|
671
|
+
required: [],
|
|
672
|
+
},
|
|
673
|
+
async execute(_id: string, params: any) {
|
|
674
|
+
try {
|
|
675
|
+
const token = await tp.getAccessToken();
|
|
676
|
+
if ((params.action || 'get') === 'get') {
|
|
677
|
+
const data = await gmail(token, '/settings/vacation');
|
|
678
|
+
return jsonResult(data);
|
|
679
|
+
}
|
|
680
|
+
// Set vacation
|
|
681
|
+
const body: any = {
|
|
682
|
+
enableAutoReply: params.enabled === 'true',
|
|
683
|
+
responseSubject: params.subject || '',
|
|
684
|
+
responseBodyPlainText: params.body || '',
|
|
685
|
+
restrictToContacts: params.restrictToContacts === 'true',
|
|
686
|
+
restrictToDomain: params.restrictToDomain === 'true',
|
|
687
|
+
};
|
|
688
|
+
if (params.startTime) body.startTime = new Date(params.startTime).getTime();
|
|
689
|
+
if (params.endTime) body.endTime = new Date(params.endTime).getTime();
|
|
690
|
+
const data = await gmail(token, '/settings/vacation', { method: 'PUT', body });
|
|
691
|
+
return jsonResult({ updated: true, ...data });
|
|
692
|
+
} catch (e: any) { return errorResult(e.message); }
|
|
693
|
+
},
|
|
694
|
+
},
|
|
695
|
+
];
|
|
696
|
+
}
|