@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.
Files changed (37) hide show
  1. package/bin/index.js +1 -1
  2. package/bin/mcp.js +1 -1
  3. package/connectors/connect-gmail/src/api/attachments.ts +143 -0
  4. package/connectors/connect-gmail/src/api/bulk.ts +713 -0
  5. package/connectors/connect-gmail/src/api/client.ts +131 -0
  6. package/connectors/connect-gmail/src/api/drafts.ts +198 -0
  7. package/connectors/connect-gmail/src/api/export.ts +312 -0
  8. package/connectors/connect-gmail/src/api/filters.ts +127 -0
  9. package/connectors/connect-gmail/src/api/index.ts +63 -0
  10. package/connectors/connect-gmail/src/api/labels.ts +123 -0
  11. package/connectors/connect-gmail/src/api/messages.ts +527 -0
  12. package/connectors/connect-gmail/src/api/profile.ts +72 -0
  13. package/connectors/connect-gmail/src/api/threads.ts +85 -0
  14. package/connectors/connect-gmail/src/cli/index.ts +2389 -0
  15. package/connectors/connect-gmail/src/index.ts +30 -0
  16. package/connectors/connect-gmail/src/types/index.ts +202 -0
  17. package/connectors/connect-gmail/src/utils/auth.ts +256 -0
  18. package/connectors/connect-gmail/src/utils/config.ts +466 -0
  19. package/connectors/connect-gmail/src/utils/contacts.ts +147 -0
  20. package/connectors/connect-gmail/src/utils/markdown.ts +119 -0
  21. package/connectors/connect-gmail/src/utils/output.ts +119 -0
  22. package/connectors/connect-gmail/src/utils/settings.ts +87 -0
  23. package/connectors/connect-gmail/tsconfig.json +16 -0
  24. package/connectors/connect-telegram/src/api/bot.ts +223 -0
  25. package/connectors/connect-telegram/src/api/chats.ts +290 -0
  26. package/connectors/connect-telegram/src/api/client.ts +134 -0
  27. package/connectors/connect-telegram/src/api/index.ts +66 -0
  28. package/connectors/connect-telegram/src/api/inline.ts +63 -0
  29. package/connectors/connect-telegram/src/api/messages.ts +781 -0
  30. package/connectors/connect-telegram/src/api/updates.ts +97 -0
  31. package/connectors/connect-telegram/src/cli/index.ts +690 -0
  32. package/connectors/connect-telegram/src/index.ts +22 -0
  33. package/connectors/connect-telegram/src/types/index.ts +617 -0
  34. package/connectors/connect-telegram/src/utils/config.ts +197 -0
  35. package/connectors/connect-telegram/src/utils/output.ts +119 -0
  36. package/connectors/connect-telegram/tsconfig.json +16 -0
  37. 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
+ }