@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,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(/ /g, ' ')
|
|
35
|
+
.replace(/&/g, '&')
|
|
36
|
+
.replace(/</g, '<')
|
|
37
|
+
.replace(/>/g, '>')
|
|
38
|
+
.replace(/"/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
|
+
}
|