@hasna/connectors 1.1.5 → 1.1.6
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/bin/index.js +1 -1
- package/bin/mcp.js +1 -1
- package/connectors/connect-gmail/src/api/attachments.ts +143 -0
- package/connectors/connect-gmail/src/api/bulk.ts +713 -0
- package/connectors/connect-gmail/src/api/client.ts +131 -0
- package/connectors/connect-gmail/src/api/drafts.ts +198 -0
- package/connectors/connect-gmail/src/api/export.ts +312 -0
- package/connectors/connect-gmail/src/api/filters.ts +127 -0
- package/connectors/connect-gmail/src/api/index.ts +63 -0
- package/connectors/connect-gmail/src/api/labels.ts +123 -0
- package/connectors/connect-gmail/src/api/messages.ts +527 -0
- package/connectors/connect-gmail/src/api/profile.ts +72 -0
- package/connectors/connect-gmail/src/api/threads.ts +85 -0
- package/connectors/connect-gmail/src/cli/index.ts +2389 -0
- package/connectors/connect-gmail/src/index.ts +30 -0
- package/connectors/connect-gmail/src/types/index.ts +202 -0
- package/connectors/connect-gmail/src/utils/auth.ts +256 -0
- package/connectors/connect-gmail/src/utils/config.ts +466 -0
- package/connectors/connect-gmail/src/utils/contacts.ts +147 -0
- package/connectors/connect-gmail/src/utils/markdown.ts +119 -0
- package/connectors/connect-gmail/src/utils/output.ts +119 -0
- package/connectors/connect-gmail/src/utils/settings.ts +87 -0
- package/connectors/connect-gmail/tsconfig.json +16 -0
- package/connectors/connect-telegram/src/api/bot.ts +223 -0
- package/connectors/connect-telegram/src/api/chats.ts +290 -0
- package/connectors/connect-telegram/src/api/client.ts +134 -0
- package/connectors/connect-telegram/src/api/index.ts +66 -0
- package/connectors/connect-telegram/src/api/inline.ts +63 -0
- package/connectors/connect-telegram/src/api/messages.ts +781 -0
- package/connectors/connect-telegram/src/api/updates.ts +97 -0
- package/connectors/connect-telegram/src/cli/index.ts +690 -0
- package/connectors/connect-telegram/src/index.ts +22 -0
- package/connectors/connect-telegram/src/types/index.ts +617 -0
- package/connectors/connect-telegram/src/utils/config.ts +197 -0
- package/connectors/connect-telegram/src/utils/output.ts +119 -0
- package/connectors/connect-telegram/tsconfig.json +16 -0
- package/package.json +1 -1
|
@@ -0,0 +1,713 @@
|
|
|
1
|
+
import type { GmailClient } from './client';
|
|
2
|
+
import type { GmailMessage, GmailLabel } from '../types';
|
|
3
|
+
import { MessagesApi } from './messages';
|
|
4
|
+
import { LabelsApi } from './labels';
|
|
5
|
+
|
|
6
|
+
// ============================================
|
|
7
|
+
// Bulk Operation Types
|
|
8
|
+
// ============================================
|
|
9
|
+
|
|
10
|
+
export interface BulkOperationOptions {
|
|
11
|
+
/** Gmail search query (e.g., "from:user@example.com", "subject:invoice", "after:2024/01/01") */
|
|
12
|
+
query: string;
|
|
13
|
+
/** Maximum messages to process (default: 100) */
|
|
14
|
+
maxResults?: number;
|
|
15
|
+
/** Maximum concurrent API calls (default: 10) */
|
|
16
|
+
concurrency?: number;
|
|
17
|
+
/** Dry run - don't actually modify, just preview */
|
|
18
|
+
dryRun?: boolean;
|
|
19
|
+
/** Progress callback */
|
|
20
|
+
onProgress?: (current: number, total: number, message: MessageSummary) => void;
|
|
21
|
+
/** Error callback */
|
|
22
|
+
onError?: (error: Error, message: MessageSummary) => void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface BulkLabelOptions extends BulkOperationOptions {
|
|
26
|
+
/** Label IDs to add */
|
|
27
|
+
addLabelIds?: string[];
|
|
28
|
+
/** Label IDs to remove */
|
|
29
|
+
removeLabelIds?: string[];
|
|
30
|
+
/** Label names to add (will be resolved to IDs) */
|
|
31
|
+
addLabels?: string[];
|
|
32
|
+
/** Label names to remove (will be resolved to IDs) */
|
|
33
|
+
removeLabels?: string[];
|
|
34
|
+
/** Skip messages that already have all the labels being added */
|
|
35
|
+
skipIfLabeled?: boolean;
|
|
36
|
+
/** Skip first N results (pagination offset) */
|
|
37
|
+
offset?: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface BulkMarkOptions extends BulkOperationOptions {
|
|
41
|
+
/** Mark as read or unread */
|
|
42
|
+
asRead: boolean;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface MessageSummary {
|
|
46
|
+
id: string;
|
|
47
|
+
threadId: string;
|
|
48
|
+
from?: string;
|
|
49
|
+
subject?: string;
|
|
50
|
+
date?: string;
|
|
51
|
+
snippet?: string;
|
|
52
|
+
labelIds?: string[];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface BulkOperationResult {
|
|
56
|
+
total: number;
|
|
57
|
+
success: number;
|
|
58
|
+
failed: number;
|
|
59
|
+
skipped: number;
|
|
60
|
+
errors: Array<{ messageId: string; error: string }>;
|
|
61
|
+
processedMessages: MessageSummary[];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface PreviewResult {
|
|
65
|
+
messages: MessageSummary[];
|
|
66
|
+
total: number;
|
|
67
|
+
query: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ============================================
|
|
71
|
+
// Batch Request Types (Gmail Batch API)
|
|
72
|
+
// ============================================
|
|
73
|
+
|
|
74
|
+
interface BatchRequest {
|
|
75
|
+
messageId: string;
|
|
76
|
+
addLabelIds?: string[];
|
|
77
|
+
removeLabelIds?: string[];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ============================================
|
|
81
|
+
// Bulk Operations API
|
|
82
|
+
// ============================================
|
|
83
|
+
|
|
84
|
+
export class BulkApi {
|
|
85
|
+
private readonly client: GmailClient;
|
|
86
|
+
private readonly messages: MessagesApi;
|
|
87
|
+
private readonly labels: LabelsApi;
|
|
88
|
+
|
|
89
|
+
constructor(client: GmailClient) {
|
|
90
|
+
this.client = client;
|
|
91
|
+
this.messages = new MessagesApi(client);
|
|
92
|
+
this.labels = new LabelsApi(client);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ============================================
|
|
96
|
+
// Preview Operations
|
|
97
|
+
// ============================================
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Preview messages that match a query without making changes
|
|
101
|
+
*/
|
|
102
|
+
async preview(query: string, maxResults: number = 50): Promise<PreviewResult> {
|
|
103
|
+
const messages = await this.fetchMessages(query, maxResults);
|
|
104
|
+
return {
|
|
105
|
+
messages,
|
|
106
|
+
total: messages.length,
|
|
107
|
+
query,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ============================================
|
|
112
|
+
// Label Operations
|
|
113
|
+
// ============================================
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Bulk modify labels on messages matching a query
|
|
117
|
+
*/
|
|
118
|
+
async modifyLabels(options: BulkLabelOptions): Promise<BulkOperationResult> {
|
|
119
|
+
const {
|
|
120
|
+
query,
|
|
121
|
+
maxResults = 100,
|
|
122
|
+
concurrency = 10,
|
|
123
|
+
dryRun = false,
|
|
124
|
+
addLabelIds = [],
|
|
125
|
+
removeLabelIds = [],
|
|
126
|
+
addLabels = [],
|
|
127
|
+
removeLabels = [],
|
|
128
|
+
skipIfLabeled = false,
|
|
129
|
+
offset = 0,
|
|
130
|
+
onProgress,
|
|
131
|
+
onError,
|
|
132
|
+
} = options;
|
|
133
|
+
|
|
134
|
+
// Resolve label names to IDs
|
|
135
|
+
const resolvedAddIds = [...addLabelIds];
|
|
136
|
+
const resolvedRemoveIds = [...removeLabelIds];
|
|
137
|
+
|
|
138
|
+
if (addLabels.length > 0 || removeLabels.length > 0) {
|
|
139
|
+
const allLabels = await this.labels.list();
|
|
140
|
+
const labelMap = new Map(allLabels.labels.map(l => [l.name.toLowerCase(), l.id]));
|
|
141
|
+
|
|
142
|
+
for (const name of addLabels) {
|
|
143
|
+
const id = labelMap.get(name.toLowerCase());
|
|
144
|
+
if (id) resolvedAddIds.push(id);
|
|
145
|
+
else throw new Error(`Label not found: ${name}`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
for (const name of removeLabels) {
|
|
149
|
+
const id = labelMap.get(name.toLowerCase());
|
|
150
|
+
if (id) resolvedRemoveIds.push(id);
|
|
151
|
+
else throw new Error(`Label not found: ${name}`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (resolvedAddIds.length === 0 && resolvedRemoveIds.length === 0) {
|
|
156
|
+
throw new Error('At least one label to add or remove is required');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Fetch enough messages to account for the offset
|
|
160
|
+
const fetchLimit = maxResults === Infinity ? Number.MAX_SAFE_INTEGER : maxResults + offset;
|
|
161
|
+
let messages = await this.fetchMessages(query, fetchLimit);
|
|
162
|
+
|
|
163
|
+
// Apply offset: skip first N results
|
|
164
|
+
if (offset > 0) {
|
|
165
|
+
messages = messages.slice(offset);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Trim to requested maxResults after offset
|
|
169
|
+
if (maxResults !== Infinity && messages.length > maxResults) {
|
|
170
|
+
messages = messages.slice(0, maxResults);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Skip messages that already have all the labels being added
|
|
174
|
+
if (skipIfLabeled && resolvedAddIds.length > 0) {
|
|
175
|
+
messages = messages.filter((msg) => {
|
|
176
|
+
const existing = msg.labelIds || [];
|
|
177
|
+
return !resolvedAddIds.every(id => existing.includes(id));
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return this.executeBatch(messages, {
|
|
182
|
+
dryRun,
|
|
183
|
+
concurrency,
|
|
184
|
+
onProgress,
|
|
185
|
+
onError,
|
|
186
|
+
operation: async (msg) => {
|
|
187
|
+
await this.messages.modify(msg.id, resolvedAddIds, resolvedRemoveIds);
|
|
188
|
+
},
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Bulk add labels to messages
|
|
194
|
+
*/
|
|
195
|
+
async addLabels(options: Omit<BulkLabelOptions, 'removeLabelIds' | 'removeLabels'>): Promise<BulkOperationResult> {
|
|
196
|
+
return this.modifyLabels({
|
|
197
|
+
...options,
|
|
198
|
+
removeLabelIds: [],
|
|
199
|
+
removeLabels: [],
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Bulk remove labels from messages
|
|
205
|
+
*/
|
|
206
|
+
async removeLabels(options: Omit<BulkLabelOptions, 'addLabelIds' | 'addLabels'>): Promise<BulkOperationResult> {
|
|
207
|
+
return this.modifyLabels({
|
|
208
|
+
...options,
|
|
209
|
+
addLabelIds: [],
|
|
210
|
+
addLabels: [],
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// ============================================
|
|
215
|
+
// Archive Operations
|
|
216
|
+
// ============================================
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Bulk archive messages (remove INBOX label)
|
|
220
|
+
*/
|
|
221
|
+
async archive(options: BulkOperationOptions): Promise<BulkOperationResult> {
|
|
222
|
+
const messages = await this.fetchMessages(options.query, options.maxResults || 100);
|
|
223
|
+
|
|
224
|
+
return this.executeBatch(messages, {
|
|
225
|
+
dryRun: options.dryRun || false,
|
|
226
|
+
concurrency: options.concurrency || 10,
|
|
227
|
+
onProgress: options.onProgress,
|
|
228
|
+
onError: options.onError,
|
|
229
|
+
operation: async (msg) => {
|
|
230
|
+
await this.messages.archive(msg.id);
|
|
231
|
+
},
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Bulk unarchive messages (add INBOX label)
|
|
237
|
+
*/
|
|
238
|
+
async unarchive(options: BulkOperationOptions): Promise<BulkOperationResult> {
|
|
239
|
+
const messages = await this.fetchMessages(options.query, options.maxResults || 100);
|
|
240
|
+
|
|
241
|
+
return this.executeBatch(messages, {
|
|
242
|
+
dryRun: options.dryRun || false,
|
|
243
|
+
concurrency: options.concurrency || 10,
|
|
244
|
+
onProgress: options.onProgress,
|
|
245
|
+
onError: options.onError,
|
|
246
|
+
operation: async (msg) => {
|
|
247
|
+
await this.messages.modify(msg.id, ['INBOX'], undefined);
|
|
248
|
+
},
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// ============================================
|
|
253
|
+
// Trash/Delete Operations
|
|
254
|
+
// ============================================
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Bulk move messages to trash
|
|
258
|
+
*/
|
|
259
|
+
async trash(options: BulkOperationOptions): Promise<BulkOperationResult> {
|
|
260
|
+
const messages = await this.fetchMessages(options.query, options.maxResults || 100);
|
|
261
|
+
|
|
262
|
+
return this.executeBatch(messages, {
|
|
263
|
+
dryRun: options.dryRun || false,
|
|
264
|
+
concurrency: options.concurrency || 10,
|
|
265
|
+
onProgress: options.onProgress,
|
|
266
|
+
onError: options.onError,
|
|
267
|
+
operation: async (msg) => {
|
|
268
|
+
await this.messages.trash(msg.id);
|
|
269
|
+
},
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Bulk permanently delete messages (DANGER!)
|
|
275
|
+
*/
|
|
276
|
+
async delete(options: BulkOperationOptions): Promise<BulkOperationResult> {
|
|
277
|
+
const messages = await this.fetchMessages(options.query, options.maxResults || 100);
|
|
278
|
+
|
|
279
|
+
return this.executeBatch(messages, {
|
|
280
|
+
dryRun: options.dryRun || false,
|
|
281
|
+
concurrency: options.concurrency || 10,
|
|
282
|
+
onProgress: options.onProgress,
|
|
283
|
+
onError: options.onError,
|
|
284
|
+
operation: async (msg) => {
|
|
285
|
+
await this.messages.delete(msg.id);
|
|
286
|
+
},
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Bulk restore messages from trash
|
|
292
|
+
*/
|
|
293
|
+
async untrash(options: BulkOperationOptions): Promise<BulkOperationResult> {
|
|
294
|
+
const messages = await this.fetchMessages(options.query, options.maxResults || 100);
|
|
295
|
+
|
|
296
|
+
return this.executeBatch(messages, {
|
|
297
|
+
dryRun: options.dryRun || false,
|
|
298
|
+
concurrency: options.concurrency || 10,
|
|
299
|
+
onProgress: options.onProgress,
|
|
300
|
+
onError: options.onError,
|
|
301
|
+
operation: async (msg) => {
|
|
302
|
+
await this.messages.untrash(msg.id);
|
|
303
|
+
},
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// ============================================
|
|
308
|
+
// Read/Unread Operations
|
|
309
|
+
// ============================================
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Bulk mark messages as read
|
|
313
|
+
*/
|
|
314
|
+
async markAsRead(options: BulkOperationOptions): Promise<BulkOperationResult> {
|
|
315
|
+
const messages = await this.fetchMessages(options.query, options.maxResults || 100);
|
|
316
|
+
|
|
317
|
+
return this.executeBatch(messages, {
|
|
318
|
+
dryRun: options.dryRun || false,
|
|
319
|
+
concurrency: options.concurrency || 10,
|
|
320
|
+
onProgress: options.onProgress,
|
|
321
|
+
onError: options.onError,
|
|
322
|
+
operation: async (msg) => {
|
|
323
|
+
await this.messages.markAsRead(msg.id);
|
|
324
|
+
},
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Bulk mark messages as unread
|
|
330
|
+
*/
|
|
331
|
+
async markAsUnread(options: BulkOperationOptions): Promise<BulkOperationResult> {
|
|
332
|
+
const messages = await this.fetchMessages(options.query, options.maxResults || 100);
|
|
333
|
+
|
|
334
|
+
return this.executeBatch(messages, {
|
|
335
|
+
dryRun: options.dryRun || false,
|
|
336
|
+
concurrency: options.concurrency || 10,
|
|
337
|
+
onProgress: options.onProgress,
|
|
338
|
+
onError: options.onError,
|
|
339
|
+
operation: async (msg) => {
|
|
340
|
+
await this.messages.markAsUnread(msg.id);
|
|
341
|
+
},
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// ============================================
|
|
346
|
+
// Star Operations
|
|
347
|
+
// ============================================
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Bulk star messages
|
|
351
|
+
*/
|
|
352
|
+
async star(options: BulkOperationOptions): Promise<BulkOperationResult> {
|
|
353
|
+
const messages = await this.fetchMessages(options.query, options.maxResults || 100);
|
|
354
|
+
|
|
355
|
+
return this.executeBatch(messages, {
|
|
356
|
+
dryRun: options.dryRun || false,
|
|
357
|
+
concurrency: options.concurrency || 10,
|
|
358
|
+
onProgress: options.onProgress,
|
|
359
|
+
onError: options.onError,
|
|
360
|
+
operation: async (msg) => {
|
|
361
|
+
await this.messages.star(msg.id);
|
|
362
|
+
},
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Bulk unstar messages
|
|
368
|
+
*/
|
|
369
|
+
async unstar(options: BulkOperationOptions): Promise<BulkOperationResult> {
|
|
370
|
+
const messages = await this.fetchMessages(options.query, options.maxResults || 100);
|
|
371
|
+
|
|
372
|
+
return this.executeBatch(messages, {
|
|
373
|
+
dryRun: options.dryRun || false,
|
|
374
|
+
concurrency: options.concurrency || 10,
|
|
375
|
+
onProgress: options.onProgress,
|
|
376
|
+
onError: options.onError,
|
|
377
|
+
operation: async (msg) => {
|
|
378
|
+
await this.messages.unstar(msg.id);
|
|
379
|
+
},
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// ============================================
|
|
384
|
+
// Gmail Batch Modify API (more efficient)
|
|
385
|
+
// ============================================
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Use Gmail's native batchModify endpoint for efficient bulk label operations
|
|
389
|
+
* This is much faster than individual requests for large batches
|
|
390
|
+
*/
|
|
391
|
+
async batchModifyLabels(options: {
|
|
392
|
+
query: string;
|
|
393
|
+
maxResults?: number;
|
|
394
|
+
addLabelIds?: string[];
|
|
395
|
+
removeLabelIds?: string[];
|
|
396
|
+
addLabels?: string[];
|
|
397
|
+
removeLabels?: string[];
|
|
398
|
+
dryRun?: boolean;
|
|
399
|
+
skipIfLabeled?: boolean;
|
|
400
|
+
offset?: number;
|
|
401
|
+
}): Promise<BulkOperationResult> {
|
|
402
|
+
const {
|
|
403
|
+
query,
|
|
404
|
+
maxResults = 1000,
|
|
405
|
+
addLabelIds = [],
|
|
406
|
+
removeLabelIds = [],
|
|
407
|
+
addLabels = [],
|
|
408
|
+
removeLabels = [],
|
|
409
|
+
dryRun = false,
|
|
410
|
+
skipIfLabeled = false,
|
|
411
|
+
offset = 0,
|
|
412
|
+
} = options;
|
|
413
|
+
|
|
414
|
+
// Resolve label names to IDs
|
|
415
|
+
const resolvedAddIds = [...addLabelIds];
|
|
416
|
+
const resolvedRemoveIds = [...removeLabelIds];
|
|
417
|
+
|
|
418
|
+
if (addLabels.length > 0 || removeLabels.length > 0) {
|
|
419
|
+
const allLabels = await this.labels.list();
|
|
420
|
+
const labelMap = new Map(allLabels.labels.map(l => [l.name.toLowerCase(), l.id]));
|
|
421
|
+
|
|
422
|
+
for (const name of addLabels) {
|
|
423
|
+
const id = labelMap.get(name.toLowerCase());
|
|
424
|
+
if (id) resolvedAddIds.push(id);
|
|
425
|
+
else throw new Error(`Label not found: ${name}`);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
for (const name of removeLabels) {
|
|
429
|
+
const id = labelMap.get(name.toLowerCase());
|
|
430
|
+
if (id) resolvedRemoveIds.push(id);
|
|
431
|
+
else throw new Error(`Label not found: ${name}`);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// For skipIfLabeled, we need full metadata (label IDs), so use fetchMessages
|
|
436
|
+
// For plain offset/pagination without skip, fetchMessageIds is sufficient
|
|
437
|
+
let messageIds: string[];
|
|
438
|
+
if (skipIfLabeled && resolvedAddIds.length > 0) {
|
|
439
|
+
const fetchLimit = maxResults === Infinity ? Number.MAX_SAFE_INTEGER : maxResults + offset;
|
|
440
|
+
let msgs = await this.fetchMessages(query, fetchLimit);
|
|
441
|
+
if (offset > 0) msgs = msgs.slice(offset);
|
|
442
|
+
if (maxResults !== Infinity && msgs.length > maxResults) msgs = msgs.slice(0, maxResults);
|
|
443
|
+
msgs = msgs.filter((msg) => {
|
|
444
|
+
const existing = msg.labelIds || [];
|
|
445
|
+
return !resolvedAddIds.every(id => existing.includes(id));
|
|
446
|
+
});
|
|
447
|
+
messageIds = msgs.map(m => m.id);
|
|
448
|
+
} else {
|
|
449
|
+
// Fetch message IDs, accounting for offset
|
|
450
|
+
const fetchLimit = maxResults === Infinity ? Number.MAX_SAFE_INTEGER : maxResults + offset;
|
|
451
|
+
let ids = await this.fetchMessageIds(query, fetchLimit);
|
|
452
|
+
if (offset > 0) ids = ids.slice(offset);
|
|
453
|
+
if (maxResults !== Infinity && ids.length > maxResults) ids = ids.slice(0, maxResults);
|
|
454
|
+
messageIds = ids;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// Use local variable name to avoid conflict with outer scope
|
|
458
|
+
const messages = messageIds;
|
|
459
|
+
|
|
460
|
+
const result: BulkOperationResult = {
|
|
461
|
+
total: messages.length,
|
|
462
|
+
success: 0,
|
|
463
|
+
failed: 0,
|
|
464
|
+
skipped: 0,
|
|
465
|
+
errors: [],
|
|
466
|
+
processedMessages: [],
|
|
467
|
+
};
|
|
468
|
+
|
|
469
|
+
if (messages.length === 0) {
|
|
470
|
+
return result;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
if (dryRun) {
|
|
474
|
+
result.success = messages.length;
|
|
475
|
+
result.processedMessages = messages.map(id => ({ id, threadId: '' }));
|
|
476
|
+
return result;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// Gmail's batchModify can handle up to 1000 messages at once
|
|
480
|
+
const batchSize = 1000;
|
|
481
|
+
const batches = this.chunkArray(messages, batchSize);
|
|
482
|
+
|
|
483
|
+
for (const batch of batches) {
|
|
484
|
+
try {
|
|
485
|
+
await this.client.post(
|
|
486
|
+
`/users/${this.client.getUserId()}/messages/batchModify`,
|
|
487
|
+
{
|
|
488
|
+
ids: batch,
|
|
489
|
+
addLabelIds: resolvedAddIds.length > 0 ? resolvedAddIds : undefined,
|
|
490
|
+
removeLabelIds: resolvedRemoveIds.length > 0 ? resolvedRemoveIds : undefined,
|
|
491
|
+
}
|
|
492
|
+
);
|
|
493
|
+
result.success += batch.length;
|
|
494
|
+
result.processedMessages.push(...batch.map(id => ({ id, threadId: '' })));
|
|
495
|
+
} catch (err) {
|
|
496
|
+
result.failed += batch.length;
|
|
497
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
498
|
+
for (const id of batch) {
|
|
499
|
+
result.errors.push({ messageId: id, error: errorMessage });
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
return result;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* Use Gmail's native batchDelete endpoint for efficient bulk deletion
|
|
509
|
+
* WARNING: This permanently deletes messages!
|
|
510
|
+
*/
|
|
511
|
+
async batchDelete(options: {
|
|
512
|
+
query: string;
|
|
513
|
+
maxResults?: number;
|
|
514
|
+
dryRun?: boolean;
|
|
515
|
+
}): Promise<BulkOperationResult> {
|
|
516
|
+
const { query, maxResults = 1000, dryRun = false } = options;
|
|
517
|
+
|
|
518
|
+
const messages = await this.fetchMessageIds(query, maxResults);
|
|
519
|
+
|
|
520
|
+
const result: BulkOperationResult = {
|
|
521
|
+
total: messages.length,
|
|
522
|
+
success: 0,
|
|
523
|
+
failed: 0,
|
|
524
|
+
skipped: 0,
|
|
525
|
+
errors: [],
|
|
526
|
+
processedMessages: [],
|
|
527
|
+
};
|
|
528
|
+
|
|
529
|
+
if (messages.length === 0) {
|
|
530
|
+
return result;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
if (dryRun) {
|
|
534
|
+
result.success = messages.length;
|
|
535
|
+
result.processedMessages = messages.map(id => ({ id, threadId: '' }));
|
|
536
|
+
return result;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// Gmail's batchDelete can handle up to 1000 messages at once
|
|
540
|
+
const batchSize = 1000;
|
|
541
|
+
const batches = this.chunkArray(messages, batchSize);
|
|
542
|
+
|
|
543
|
+
for (const batch of batches) {
|
|
544
|
+
try {
|
|
545
|
+
await this.client.post(
|
|
546
|
+
`/users/${this.client.getUserId()}/messages/batchDelete`,
|
|
547
|
+
{ ids: batch }
|
|
548
|
+
);
|
|
549
|
+
result.success += batch.length;
|
|
550
|
+
result.processedMessages.push(...batch.map(id => ({ id, threadId: '' })));
|
|
551
|
+
} catch (err) {
|
|
552
|
+
result.failed += batch.length;
|
|
553
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
554
|
+
for (const id of batch) {
|
|
555
|
+
result.errors.push({ messageId: id, error: errorMessage });
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
return result;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// ============================================
|
|
564
|
+
// Helper Methods
|
|
565
|
+
// ============================================
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* Fetch messages matching a query with full metadata
|
|
569
|
+
*/
|
|
570
|
+
private async fetchMessages(query: string, maxResults: number): Promise<MessageSummary[]> {
|
|
571
|
+
const messages: MessageSummary[] = [];
|
|
572
|
+
let pageToken: string | undefined;
|
|
573
|
+
|
|
574
|
+
while (messages.length < maxResults) {
|
|
575
|
+
const response = await this.messages.list({
|
|
576
|
+
q: query,
|
|
577
|
+
maxResults: Math.min(100, maxResults - messages.length),
|
|
578
|
+
pageToken,
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
if (!response.messages || response.messages.length === 0) {
|
|
582
|
+
break;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// Fetch metadata for each message
|
|
586
|
+
const metadataPromises = response.messages.map(async (m) => {
|
|
587
|
+
const msg = await this.messages.get(m.id, 'metadata');
|
|
588
|
+
const headers = msg.payload?.headers || [];
|
|
589
|
+
const getHeader = (name: string) =>
|
|
590
|
+
headers.find(h => h.name.toLowerCase() === name.toLowerCase())?.value;
|
|
591
|
+
|
|
592
|
+
return {
|
|
593
|
+
id: m.id,
|
|
594
|
+
threadId: m.threadId,
|
|
595
|
+
from: getHeader('from'),
|
|
596
|
+
subject: getHeader('subject'),
|
|
597
|
+
date: getHeader('date'),
|
|
598
|
+
snippet: msg.snippet,
|
|
599
|
+
labelIds: msg.labelIds,
|
|
600
|
+
};
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
const fetchedMessages = await Promise.all(metadataPromises);
|
|
604
|
+
messages.push(...fetchedMessages);
|
|
605
|
+
|
|
606
|
+
pageToken = response.nextPageToken;
|
|
607
|
+
if (!pageToken) break;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
return messages;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/**
|
|
614
|
+
* Fetch only message IDs (faster for batch operations)
|
|
615
|
+
*/
|
|
616
|
+
private async fetchMessageIds(query: string, maxResults: number): Promise<string[]> {
|
|
617
|
+
const messageIds: string[] = [];
|
|
618
|
+
let pageToken: string | undefined;
|
|
619
|
+
|
|
620
|
+
while (messageIds.length < maxResults) {
|
|
621
|
+
const response = await this.messages.list({
|
|
622
|
+
q: query,
|
|
623
|
+
maxResults: Math.min(500, maxResults - messageIds.length),
|
|
624
|
+
pageToken,
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
if (!response.messages || response.messages.length === 0) {
|
|
628
|
+
break;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
messageIds.push(...response.messages.map(m => m.id));
|
|
632
|
+
|
|
633
|
+
pageToken = response.nextPageToken;
|
|
634
|
+
if (!pageToken) break;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
return messageIds;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
/**
|
|
641
|
+
* Execute operations in batches with concurrency control
|
|
642
|
+
*/
|
|
643
|
+
private async executeBatch(
|
|
644
|
+
messages: MessageSummary[],
|
|
645
|
+
options: {
|
|
646
|
+
dryRun: boolean;
|
|
647
|
+
concurrency: number;
|
|
648
|
+
onProgress?: (current: number, total: number, message: MessageSummary) => void;
|
|
649
|
+
onError?: (error: Error, message: MessageSummary) => void;
|
|
650
|
+
operation: (message: MessageSummary) => Promise<void>;
|
|
651
|
+
}
|
|
652
|
+
): Promise<BulkOperationResult> {
|
|
653
|
+
const { dryRun, concurrency, onProgress, onError, operation } = options;
|
|
654
|
+
|
|
655
|
+
const result: BulkOperationResult = {
|
|
656
|
+
total: messages.length,
|
|
657
|
+
success: 0,
|
|
658
|
+
failed: 0,
|
|
659
|
+
skipped: 0,
|
|
660
|
+
errors: [],
|
|
661
|
+
processedMessages: [],
|
|
662
|
+
};
|
|
663
|
+
|
|
664
|
+
if (messages.length === 0) {
|
|
665
|
+
return result;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// Process in batches with concurrency control
|
|
669
|
+
const chunks = this.chunkArray(messages, concurrency);
|
|
670
|
+
|
|
671
|
+
for (const chunk of chunks) {
|
|
672
|
+
await Promise.all(
|
|
673
|
+
chunk.map(async (msg) => {
|
|
674
|
+
try {
|
|
675
|
+
if (dryRun) {
|
|
676
|
+
result.success++;
|
|
677
|
+
result.processedMessages.push(msg);
|
|
678
|
+
} else {
|
|
679
|
+
await operation(msg);
|
|
680
|
+
result.success++;
|
|
681
|
+
result.processedMessages.push(msg);
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
if (onProgress) {
|
|
685
|
+
onProgress(result.success + result.failed, result.total, msg);
|
|
686
|
+
}
|
|
687
|
+
} catch (err) {
|
|
688
|
+
result.failed++;
|
|
689
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
690
|
+
result.errors.push({ messageId: msg.id, error: errorMessage });
|
|
691
|
+
|
|
692
|
+
if (onError) {
|
|
693
|
+
onError(err instanceof Error ? err : new Error(errorMessage), msg);
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
})
|
|
697
|
+
);
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
return result;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
/**
|
|
704
|
+
* Split array into chunks
|
|
705
|
+
*/
|
|
706
|
+
private chunkArray<T>(array: T[], size: number): T[][] {
|
|
707
|
+
const chunks: T[][] = [];
|
|
708
|
+
for (let i = 0; i < array.length; i += size) {
|
|
709
|
+
chunks.push(array.slice(i, i + size));
|
|
710
|
+
}
|
|
711
|
+
return chunks;
|
|
712
|
+
}
|
|
713
|
+
}
|