@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,527 @@
1
+ import type { GmailClient } from './client';
2
+ import type {
3
+ GmailMessage,
4
+ MessagePart,
5
+ MessageListResponse,
6
+ ListMessagesOptions,
7
+ SendMessageOptions,
8
+ } from '../types';
9
+ import { getFormattedSender, getUserEmail } from '../utils/config';
10
+
11
+ /**
12
+ * Encode a string for email headers using RFC 2047 MIME encoded-word syntax
13
+ * Only encodes if non-ASCII characters are present
14
+ */
15
+ function encodeHeaderValue(value: string): string {
16
+ // Check if encoding is needed (non-ASCII characters present)
17
+ if (!/[^\x00-\x7F]/.test(value)) {
18
+ return value;
19
+ }
20
+ // Use Base64 encoding for UTF-8
21
+ const encoded = Buffer.from(value, 'utf-8').toString('base64');
22
+ return `=?UTF-8?B?${encoded}?=`;
23
+ }
24
+
25
+ /**
26
+ * Strip HTML tags to create plain text version
27
+ */
28
+ function htmlToPlainText(html: string): string {
29
+ return html
30
+ .replace(/<br\s*\/?>/gi, '\n')
31
+ .replace(/<\/p>/gi, '\n\n')
32
+ .replace(/<\/div>/gi, '\n')
33
+ .replace(/<[^>]+>/g, '')
34
+ .replace(/&nbsp;/g, ' ')
35
+ .replace(/&amp;/g, '&')
36
+ .replace(/&lt;/g, '<')
37
+ .replace(/&gt;/g, '>')
38
+ .replace(/&quot;/g, '"')
39
+ .replace(/\n{3,}/g, '\n\n')
40
+ .trim();
41
+ }
42
+ import { formatEmailWithName } from '../utils/contacts';
43
+ import { markdownToHtml, wrapInEmailTemplate, looksLikeMarkdown } from '../utils/markdown';
44
+ import { loadSettings, shouldAppendSignature, getSignature } from '../utils/settings';
45
+
46
+ export interface ReplyOptions {
47
+ body: string;
48
+ isHtml?: boolean;
49
+ cc?: string | string[];
50
+ bcc?: string | string[];
51
+ }
52
+
53
+ export class MessagesApi {
54
+ private client: GmailClient;
55
+
56
+ constructor(client: GmailClient) {
57
+ this.client = client;
58
+ }
59
+
60
+ /**
61
+ * List messages in the user's mailbox
62
+ */
63
+ async list(options: ListMessagesOptions = {}): Promise<MessageListResponse> {
64
+ const params: Record<string, string | number | boolean | undefined> = {
65
+ maxResults: options.maxResults || 10,
66
+ pageToken: options.pageToken,
67
+ q: options.q,
68
+ includeSpamTrash: options.includeSpamTrash,
69
+ };
70
+
71
+ if (options.labelIds && options.labelIds.length > 0) {
72
+ params.labelIds = options.labelIds.join(',');
73
+ }
74
+
75
+ return this.client.get<MessageListResponse>(
76
+ `/users/${this.client.getUserId()}/messages`,
77
+ params
78
+ );
79
+ }
80
+
81
+ /**
82
+ * Get a specific message by ID
83
+ */
84
+ async get(messageId: string, format: 'full' | 'metadata' | 'minimal' | 'raw' = 'full'): Promise<GmailMessage> {
85
+ return this.client.get<GmailMessage>(
86
+ `/users/${this.client.getUserId()}/messages/${messageId}`,
87
+ { format }
88
+ );
89
+ }
90
+
91
+ /**
92
+ * Send a new email message
93
+ */
94
+ async send(options: SendMessageOptions): Promise<GmailMessage> {
95
+ const message = this.buildRawMessage(options);
96
+ const encodedMessage = Buffer.from(message).toString('base64url');
97
+
98
+ const body: Record<string, unknown> = {
99
+ raw: encodedMessage,
100
+ };
101
+
102
+ if (options.threadId) {
103
+ body.threadId = options.threadId;
104
+ }
105
+
106
+ return this.client.post<GmailMessage>(
107
+ `/users/${this.client.getUserId()}/messages/send`,
108
+ body
109
+ );
110
+ }
111
+
112
+ /**
113
+ * Reply to a message in the same thread
114
+ */
115
+ async reply(messageId: string, options: ReplyOptions): Promise<GmailMessage> {
116
+ // Get the original message to extract thread info and headers
117
+ const original = await this.get(messageId, 'full');
118
+ const headers = original.payload?.headers || [];
119
+
120
+ const getHeader = (name: string) =>
121
+ headers.find(h => h.name.toLowerCase() === name.toLowerCase())?.value || '';
122
+
123
+ const originalFrom = getHeader('From');
124
+ const originalTo = getHeader('To');
125
+ const originalSubject = getHeader('Subject');
126
+ const originalMessageId = getHeader('Message-ID') || getHeader('Message-Id');
127
+ const originalReferences = getHeader('References');
128
+
129
+ // Determine who to reply to
130
+ const myEmail = getUserEmail();
131
+ let replyTo = originalFrom;
132
+
133
+ // If we sent the original, reply to the original recipient
134
+ if (originalFrom.includes(myEmail || '')) {
135
+ replyTo = originalTo;
136
+ }
137
+
138
+ // Build references header (for proper threading)
139
+ let references = originalReferences
140
+ ? `${originalReferences} ${originalMessageId}`
141
+ : originalMessageId;
142
+
143
+ // Build subject (keep original, don't add Re: if already there)
144
+ let subject = originalSubject;
145
+ if (!subject.toLowerCase().startsWith('re:')) {
146
+ subject = `Re: ${subject}`;
147
+ }
148
+
149
+ const message = this.buildRawMessage({
150
+ to: replyTo,
151
+ cc: options.cc,
152
+ bcc: options.bcc,
153
+ subject,
154
+ body: options.body,
155
+ isHtml: options.isHtml,
156
+ threadId: original.threadId,
157
+ inReplyTo: originalMessageId,
158
+ references,
159
+ isReply: true,
160
+ });
161
+
162
+ const encodedMessage = Buffer.from(message).toString('base64url');
163
+
164
+ return this.client.post<GmailMessage>(
165
+ `/users/${this.client.getUserId()}/messages/send`,
166
+ {
167
+ raw: encodedMessage,
168
+ threadId: original.threadId,
169
+ }
170
+ );
171
+ }
172
+
173
+ /**
174
+ * Move a message to trash
175
+ */
176
+ async trash(messageId: string): Promise<GmailMessage> {
177
+ return this.client.post<GmailMessage>(
178
+ `/users/${this.client.getUserId()}/messages/${messageId}/trash`
179
+ );
180
+ }
181
+
182
+ /**
183
+ * Remove a message from trash
184
+ */
185
+ async untrash(messageId: string): Promise<GmailMessage> {
186
+ return this.client.post<GmailMessage>(
187
+ `/users/${this.client.getUserId()}/messages/${messageId}/untrash`
188
+ );
189
+ }
190
+
191
+ /**
192
+ * Permanently delete a message
193
+ */
194
+ async delete(messageId: string): Promise<void> {
195
+ await this.client.delete(
196
+ `/users/${this.client.getUserId()}/messages/${messageId}`
197
+ );
198
+ }
199
+
200
+ /**
201
+ * Modify message labels
202
+ */
203
+ async modify(
204
+ messageId: string,
205
+ addLabelIds?: string[],
206
+ removeLabelIds?: string[]
207
+ ): Promise<GmailMessage> {
208
+ return this.client.post<GmailMessage>(
209
+ `/users/${this.client.getUserId()}/messages/${messageId}/modify`,
210
+ {
211
+ addLabelIds: addLabelIds || [],
212
+ removeLabelIds: removeLabelIds || [],
213
+ }
214
+ );
215
+ }
216
+
217
+ /**
218
+ * Add a label to a message
219
+ */
220
+ async addLabel(messageId: string, labelId: string): Promise<GmailMessage> {
221
+ return this.modify(messageId, [labelId], undefined);
222
+ }
223
+
224
+ /**
225
+ * Remove a label from a message
226
+ */
227
+ async removeLabel(messageId: string, labelId: string): Promise<GmailMessage> {
228
+ return this.modify(messageId, undefined, [labelId]);
229
+ }
230
+
231
+ /**
232
+ * Add multiple labels to a message
233
+ */
234
+ async addLabels(messageId: string, labelIds: string[]): Promise<GmailMessage> {
235
+ return this.modify(messageId, labelIds, undefined);
236
+ }
237
+
238
+ /**
239
+ * Remove multiple labels from a message
240
+ */
241
+ async removeLabels(messageId: string, labelIds: string[]): Promise<GmailMessage> {
242
+ return this.modify(messageId, undefined, labelIds);
243
+ }
244
+
245
+ /**
246
+ * Mark a message as read
247
+ */
248
+ async markAsRead(messageId: string): Promise<GmailMessage> {
249
+ return this.modify(messageId, undefined, ['UNREAD']);
250
+ }
251
+
252
+ /**
253
+ * Mark a message as unread
254
+ */
255
+ async markAsUnread(messageId: string): Promise<GmailMessage> {
256
+ return this.modify(messageId, ['UNREAD']);
257
+ }
258
+
259
+ /**
260
+ * Star a message
261
+ */
262
+ async star(messageId: string): Promise<GmailMessage> {
263
+ return this.modify(messageId, ['STARRED']);
264
+ }
265
+
266
+ /**
267
+ * Unstar a message
268
+ */
269
+ async unstar(messageId: string): Promise<GmailMessage> {
270
+ return this.modify(messageId, undefined, ['STARRED']);
271
+ }
272
+
273
+ /**
274
+ * Archive a message (remove from INBOX)
275
+ */
276
+ async archive(messageId: string): Promise<GmailMessage> {
277
+ return this.modify(messageId, undefined, ['INBOX']);
278
+ }
279
+
280
+ /**
281
+ * Build RFC 2822 formatted message
282
+ */
283
+ private buildRawMessage(options: SendMessageOptions & { inReplyTo?: string; references?: string; isReply?: boolean }): string {
284
+ const settings = loadSettings();
285
+ const isReply = options.isReply || !!options.inReplyTo;
286
+
287
+ // Format To addresses with contact names
288
+ const formatAddresses = (addresses: string | string[]): string => {
289
+ const addrs = Array.isArray(addresses) ? addresses : [addresses];
290
+ return addrs.map(addr => {
291
+ // If already formatted with name, keep it
292
+ if (addr.includes('<') && addr.includes('>')) {
293
+ return addr;
294
+ }
295
+ return formatEmailWithName(addr);
296
+ }).join(', ');
297
+ };
298
+
299
+ const to = formatAddresses(options.to);
300
+ const cc = options.cc ? formatAddresses(options.cc) : '';
301
+ const bcc = options.bcc ? formatAddresses(options.bcc) : '';
302
+
303
+ // Process body: markdown conversion and signature
304
+ let body = options.body;
305
+ let isHtml = options.isHtml || false;
306
+
307
+ // Auto-detect and convert markdown if enabled
308
+ if (settings.markdownEnabled && !isHtml && looksLikeMarkdown(body)) {
309
+ body = markdownToHtml(body);
310
+ isHtml = true;
311
+ }
312
+
313
+ // Append signature (only for new emails by default, configurable for replies)
314
+ if (shouldAppendSignature(isReply)) {
315
+ const signature = getSignature();
316
+ if (signature) {
317
+ // Gmail signatures are HTML, so convert body to HTML if needed
318
+ if (!isHtml) {
319
+ body = body.replace(/\n/g, '<br>');
320
+ isHtml = true;
321
+ }
322
+ body += `<br><br>${signature}`;
323
+ }
324
+ }
325
+
326
+ // Get formatted sender with display name
327
+ let from: string;
328
+ try {
329
+ from = getFormattedSender();
330
+ } catch {
331
+ from = getUserEmail() || '';
332
+ }
333
+
334
+ let message = '';
335
+ message += `From: ${from}\r\n`;
336
+ message += `To: ${to}\r\n`;
337
+ if (cc) message += `Cc: ${cc}\r\n`;
338
+ if (bcc) message += `Bcc: ${bcc}\r\n`;
339
+ message += `Subject: ${encodeHeaderValue(options.subject)}\r\n`;
340
+
341
+ // Add threading headers for replies
342
+ if (options.inReplyTo) {
343
+ message += `In-Reply-To: ${options.inReplyTo}\r\n`;
344
+ }
345
+ if (options.references) {
346
+ message += `References: ${options.references}\r\n`;
347
+ }
348
+
349
+ message += `MIME-Version: 1.0\r\n`;
350
+
351
+ const mixedBoundary = `mixed_${Date.now()}`;
352
+ const altBoundary = `alt_${Date.now()}`;
353
+
354
+ if (options.attachments && options.attachments.length > 0) {
355
+ // Multipart/mixed for attachments
356
+ message += `Content-Type: multipart/mixed; boundary="${mixedBoundary}"\r\n\r\n`;
357
+ message += `--${mixedBoundary}\r\n`;
358
+
359
+ if (isHtml) {
360
+ // Nested multipart/alternative for HTML with plain text fallback
361
+ const htmlBody = wrapInEmailTemplate(body);
362
+ const plainBody = htmlToPlainText(body);
363
+ message += `Content-Type: multipart/alternative; boundary="${altBoundary}"\r\n\r\n`;
364
+ message += `--${altBoundary}\r\n`;
365
+ message += `Content-Type: text/plain; charset="UTF-8"\r\n\r\n`;
366
+ message += `${plainBody}\r\n`;
367
+ message += `--${altBoundary}\r\n`;
368
+ message += `Content-Type: text/html; charset="UTF-8"\r\n\r\n`;
369
+ message += `${htmlBody}\r\n`;
370
+ message += `--${altBoundary}--\r\n`;
371
+ } else {
372
+ message += `Content-Type: text/plain; charset="UTF-8"\r\n\r\n`;
373
+ message += `${body}\r\n`;
374
+ }
375
+
376
+ for (const attachment of options.attachments) {
377
+ message += `--${mixedBoundary}\r\n`;
378
+ message += `Content-Type: ${attachment.mimeType}; name="${attachment.filename}"\r\n`;
379
+ message += `Content-Disposition: attachment; filename="${attachment.filename}"\r\n`;
380
+ message += `Content-Transfer-Encoding: base64\r\n\r\n`;
381
+ message += `${attachment.data}\r\n`;
382
+ }
383
+
384
+ message += `--${mixedBoundary}--`;
385
+ } else if (isHtml) {
386
+ // Multipart/alternative for HTML with plain text fallback
387
+ const htmlBody = wrapInEmailTemplate(body);
388
+ const plainBody = htmlToPlainText(body);
389
+ message += `Content-Type: multipart/alternative; boundary="${altBoundary}"\r\n\r\n`;
390
+ message += `--${altBoundary}\r\n`;
391
+ message += `Content-Type: text/plain; charset="UTF-8"\r\n\r\n`;
392
+ message += `${plainBody}\r\n`;
393
+ message += `--${altBoundary}\r\n`;
394
+ message += `Content-Type: text/html; charset="UTF-8"\r\n\r\n`;
395
+ message += `${htmlBody}\r\n`;
396
+ message += `--${altBoundary}--`;
397
+ } else {
398
+ message += `Content-Type: text/plain; charset="UTF-8"\r\n\r\n`;
399
+ message += body;
400
+ }
401
+
402
+ return message;
403
+ }
404
+
405
+ /**
406
+ * Extract body content from a message
407
+ * @param message The Gmail message to extract body from
408
+ * @param preferHtml Whether to prefer HTML content over plain text
409
+ * @returns The body content as a string
410
+ */
411
+ extractBody(message: GmailMessage, preferHtml: boolean = false): string {
412
+ if (!message.payload) return '';
413
+
414
+ const targetType = preferHtml ? 'text/html' : 'text/plain';
415
+
416
+ // Helper to check MIME type (handles charset params like "text/html; charset=utf-8")
417
+ const getBaseMime = (mimeType: string | undefined): string => {
418
+ if (!mimeType) return '';
419
+ return mimeType.split(';')[0].trim().toLowerCase();
420
+ };
421
+
422
+ // Collect all text parts from the message structure
423
+ const collectTextParts = (part: MessagePart, results: Array<{mimeType: string, data: string}> = []): Array<{mimeType: string, data: string}> => {
424
+ if (part.body?.data && part.mimeType) {
425
+ const baseMime = getBaseMime(part.mimeType);
426
+ if (baseMime.startsWith('text/')) {
427
+ results.push({
428
+ mimeType: baseMime,
429
+ data: Buffer.from(part.body.data, 'base64url').toString('utf-8')
430
+ });
431
+ }
432
+ }
433
+ if (part.parts) {
434
+ for (const p of part.parts) {
435
+ collectTextParts(p, results);
436
+ }
437
+ }
438
+ return results;
439
+ };
440
+
441
+ const textParts = collectTextParts(message.payload);
442
+
443
+ // First, try to find exact target type
444
+ const exactMatch = textParts.find(p => p.mimeType === targetType);
445
+ if (exactMatch) {
446
+ return exactMatch.data;
447
+ }
448
+
449
+ // Fallback: use alternative text type
450
+ const altMatch = textParts.find(p => p.mimeType.startsWith('text/'));
451
+ return altMatch?.data || '';
452
+ }
453
+
454
+ /**
455
+ * Extract inline images from a message (for multipart/related emails)
456
+ * @param message The Gmail message to extract inline images from
457
+ * @returns Array of inline images with Content-ID and data
458
+ */
459
+ extractInlineImages(message: GmailMessage): Array<{contentId: string, mimeType: string, data: string}> {
460
+ if (!message.payload) return [];
461
+
462
+ const images: Array<{contentId: string, mimeType: string, data: string}> = [];
463
+
464
+ const collectImages = (part: MessagePart): void => {
465
+ if (part.body?.data && part.mimeType?.startsWith('image/')) {
466
+ const contentIdHeader = part.headers?.find(
467
+ h => h.name.toLowerCase() === 'content-id'
468
+ );
469
+ if (contentIdHeader) {
470
+ // Content-ID is typically wrapped in angle brackets: <image001.png@01D12345.67890ABC>
471
+ const contentId = contentIdHeader.value.replace(/^<|>$/g, '');
472
+ images.push({
473
+ contentId,
474
+ mimeType: part.mimeType,
475
+ data: part.body.data
476
+ });
477
+ }
478
+ }
479
+ if (part.parts) {
480
+ for (const p of part.parts) {
481
+ collectImages(p);
482
+ }
483
+ }
484
+ };
485
+
486
+ collectImages(message.payload);
487
+ return images;
488
+ }
489
+
490
+ /**
491
+ * Get message structure for debugging
492
+ * @param message The Gmail message to analyze
493
+ * @returns A tree structure showing the MIME parts
494
+ */
495
+ getMessageStructure(message: GmailMessage): object {
496
+ if (!message.payload) return {};
497
+
498
+ const buildStructure = (part: MessagePart, depth: number = 0): object => {
499
+ const result: Record<string, unknown> = {
500
+ mimeType: part.mimeType,
501
+ size: part.body?.size || 0,
502
+ hasData: !!part.body?.data,
503
+ hasAttachmentId: !!part.body?.attachmentId,
504
+ };
505
+
506
+ if (part.filename) {
507
+ result.filename = part.filename;
508
+ }
509
+
510
+ // Include Content-ID for inline images
511
+ const contentIdHeader = part.headers?.find(
512
+ h => h.name.toLowerCase() === 'content-id'
513
+ );
514
+ if (contentIdHeader) {
515
+ result.contentId = contentIdHeader.value;
516
+ }
517
+
518
+ if (part.parts && part.parts.length > 0) {
519
+ result.parts = part.parts.map(p => buildStructure(p, depth + 1));
520
+ }
521
+
522
+ return result;
523
+ };
524
+
525
+ return buildStructure(message.payload);
526
+ }
527
+ }
@@ -0,0 +1,72 @@
1
+ import type { GmailClient } from './client';
2
+ import type { GmailProfile } from '../types';
3
+
4
+ export interface SendAsAddress {
5
+ sendAsEmail: string;
6
+ displayName?: string;
7
+ replyToAddress?: string;
8
+ signature?: string;
9
+ isPrimary?: boolean;
10
+ isDefault?: boolean;
11
+ treatAsAlias?: boolean;
12
+ verificationStatus?: string;
13
+ }
14
+
15
+ export class ProfileApi {
16
+ private client: GmailClient;
17
+
18
+ constructor(client: GmailClient) {
19
+ this.client = client;
20
+ }
21
+
22
+ /**
23
+ * Get the current user's Gmail profile
24
+ */
25
+ async get(): Promise<GmailProfile> {
26
+ return this.client.get<GmailProfile>(
27
+ `/users/${this.client.getUserId()}/profile`
28
+ );
29
+ }
30
+
31
+ /**
32
+ * List all send-as addresses (including signatures)
33
+ */
34
+ async listSendAs(): Promise<{ sendAs: SendAsAddress[] }> {
35
+ return this.client.get<{ sendAs: SendAsAddress[] }>(
36
+ `/users/${this.client.getUserId()}/settings/sendAs`
37
+ );
38
+ }
39
+
40
+ /**
41
+ * Get a specific send-as address
42
+ */
43
+ async getSendAs(sendAsEmail: string): Promise<SendAsAddress> {
44
+ return this.client.get<SendAsAddress>(
45
+ `/users/${this.client.getUserId()}/settings/sendAs/${sendAsEmail}`
46
+ );
47
+ }
48
+
49
+ /**
50
+ * Get the primary send-as address (with signature)
51
+ */
52
+ async getPrimarySendAs(): Promise<SendAsAddress | undefined> {
53
+ const { sendAs } = await this.listSendAs();
54
+ return sendAs.find(s => s.isPrimary || s.isDefault);
55
+ }
56
+
57
+ /**
58
+ * Get the user's Gmail signature
59
+ */
60
+ async getSignature(): Promise<string | undefined> {
61
+ const primary = await this.getPrimarySendAs();
62
+ return primary?.signature;
63
+ }
64
+
65
+ /**
66
+ * Get the user's display name from Gmail settings
67
+ */
68
+ async getDisplayName(): Promise<string | undefined> {
69
+ const primary = await this.getPrimarySendAs();
70
+ return primary?.displayName;
71
+ }
72
+ }
@@ -0,0 +1,85 @@
1
+ import type { GmailClient } from './client';
2
+ import type { GmailThread, ThreadListResponse, ListThreadsOptions } from '../types';
3
+
4
+ export class ThreadsApi {
5
+ private client: GmailClient;
6
+
7
+ constructor(client: GmailClient) {
8
+ this.client = client;
9
+ }
10
+
11
+ /**
12
+ * List threads in the user's mailbox
13
+ */
14
+ async list(options: ListThreadsOptions = {}): Promise<ThreadListResponse> {
15
+ const params: Record<string, string | number | boolean | undefined> = {
16
+ maxResults: options.maxResults || 10,
17
+ pageToken: options.pageToken,
18
+ q: options.q,
19
+ includeSpamTrash: options.includeSpamTrash,
20
+ };
21
+
22
+ if (options.labelIds && options.labelIds.length > 0) {
23
+ params.labelIds = options.labelIds.join(',');
24
+ }
25
+
26
+ return this.client.get<ThreadListResponse>(
27
+ `/users/${this.client.getUserId()}/threads`,
28
+ params
29
+ );
30
+ }
31
+
32
+ /**
33
+ * Get a specific thread by ID
34
+ */
35
+ async get(threadId: string, format: 'full' | 'metadata' | 'minimal' = 'full'): Promise<GmailThread> {
36
+ return this.client.get<GmailThread>(
37
+ `/users/${this.client.getUserId()}/threads/${threadId}`,
38
+ { format }
39
+ );
40
+ }
41
+
42
+ /**
43
+ * Move a thread to trash
44
+ */
45
+ async trash(threadId: string): Promise<GmailThread> {
46
+ return this.client.post<GmailThread>(
47
+ `/users/${this.client.getUserId()}/threads/${threadId}/trash`
48
+ );
49
+ }
50
+
51
+ /**
52
+ * Remove a thread from trash
53
+ */
54
+ async untrash(threadId: string): Promise<GmailThread> {
55
+ return this.client.post<GmailThread>(
56
+ `/users/${this.client.getUserId()}/threads/${threadId}/untrash`
57
+ );
58
+ }
59
+
60
+ /**
61
+ * Permanently delete a thread
62
+ */
63
+ async delete(threadId: string): Promise<void> {
64
+ await this.client.delete(
65
+ `/users/${this.client.getUserId()}/threads/${threadId}`
66
+ );
67
+ }
68
+
69
+ /**
70
+ * Modify thread labels
71
+ */
72
+ async modify(
73
+ threadId: string,
74
+ addLabelIds?: string[],
75
+ removeLabelIds?: string[]
76
+ ): Promise<GmailThread> {
77
+ return this.client.post<GmailThread>(
78
+ `/users/${this.client.getUserId()}/threads/${threadId}/modify`,
79
+ {
80
+ addLabelIds: addLabelIds || [],
81
+ removeLabelIds: removeLabelIds || [],
82
+ }
83
+ );
84
+ }
85
+ }