@plosson/agentio 0.5.10 → 0.5.13
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/package.json +1 -1
- package/src/commands/gmail.ts +93 -85
- package/src/services/gmail/client.ts +98 -68
- package/src/types/gmail.ts +1 -6
- package/src/utils/output.ts +7 -0
package/package.json
CHANGED
package/src/commands/gmail.ts
CHANGED
|
@@ -7,11 +7,94 @@ import { setProfile, getProfile } from '../config/config-manager';
|
|
|
7
7
|
import { createProfileCommands } from '../utils/profile-commands';
|
|
8
8
|
import { performOAuthFlow } from '../auth/oauth';
|
|
9
9
|
import { GmailClient } from '../services/gmail/client';
|
|
10
|
-
import { printMessageList, printMessage, printSendResult, printArchived, printMarked, printAttachmentList, printAttachmentDownloaded, raw } from '../utils/output';
|
|
10
|
+
import { printMessageList, printMessage, printSendResult, printDraftResult, printArchived, printMarked, printAttachmentList, printAttachmentDownloaded, raw } from '../utils/output';
|
|
11
11
|
import { CliError, handleError } from '../utils/errors';
|
|
12
12
|
import { readStdin } from '../utils/stdin';
|
|
13
13
|
import { enforceWriteAccess } from '../utils/read-only';
|
|
14
|
-
import type { GmailAttachment } from '../types/gmail';
|
|
14
|
+
import type { GmailAttachment, GmailSendOptions } from '../types/gmail';
|
|
15
|
+
|
|
16
|
+
function addComposeOptions(cmd: Command): Command {
|
|
17
|
+
return cmd
|
|
18
|
+
.option('--profile <name>', 'Profile name (optional if only one profile exists)')
|
|
19
|
+
.option('--to <email>', 'Recipient (repeatable, required unless --reply-to)', (val: string, acc: string[]) => [...acc, val], [])
|
|
20
|
+
.option('--cc <email>', 'CC recipient (repeatable)', (val: string, acc: string[]) => [...acc, val], [])
|
|
21
|
+
.option('--bcc <email>', 'BCC recipient (repeatable)', (val: string, acc: string[]) => [...acc, val], [])
|
|
22
|
+
.option('--subject <subject>', 'Email subject (required unless --reply-to)')
|
|
23
|
+
.option('--body <body>', 'Email body (or pipe via stdin)')
|
|
24
|
+
.option('--html', 'Treat body as HTML')
|
|
25
|
+
.option('--reply-to <thread-id>', 'Thread ID to reply to (derives to/subject from thread)')
|
|
26
|
+
.option('--attachment <path>', 'File to attach (repeatable)', (val: string, acc: string[]) => [...acc, val], [])
|
|
27
|
+
.option('--inline <cid:path>', 'Inline image (repeatable, format: contentId:filepath). Supports PNG, JPG, GIF only (not SVG)', (val: string, acc: string[]) => [...acc, val], []);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function parseBody(body: string | undefined): Promise<string> {
|
|
31
|
+
if (!body) {
|
|
32
|
+
body = await readStdin() ?? undefined;
|
|
33
|
+
}
|
|
34
|
+
if (!body) {
|
|
35
|
+
throw new CliError('INVALID_PARAMS', 'Body is required. Use --body or pipe via stdin.');
|
|
36
|
+
}
|
|
37
|
+
return body;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function parseAttachments(paths: string[]): GmailAttachment[] {
|
|
41
|
+
return paths.map((path: string) => ({
|
|
42
|
+
path,
|
|
43
|
+
filename: basename(path),
|
|
44
|
+
}));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function parseInlineAttachments(specs: string[]): GmailAttachment[] {
|
|
48
|
+
return specs.map((spec: string) => {
|
|
49
|
+
const colonIndex = spec.indexOf(':');
|
|
50
|
+
if (colonIndex === -1) {
|
|
51
|
+
throw new CliError('INVALID_PARAMS', `Invalid inline format: ${spec}`, 'Use format: contentId:filepath (e.g., logo:./logo.png)');
|
|
52
|
+
}
|
|
53
|
+
const contentId = spec.substring(0, colonIndex);
|
|
54
|
+
const path = spec.substring(colonIndex + 1);
|
|
55
|
+
return {
|
|
56
|
+
path,
|
|
57
|
+
filename: basename(path),
|
|
58
|
+
contentId,
|
|
59
|
+
};
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function parseSendOptions(options: Record<string, unknown>): Promise<GmailSendOptions> {
|
|
64
|
+
const replyTo = options.replyTo as string | undefined;
|
|
65
|
+
const to = options.to as string[];
|
|
66
|
+
const subject = options.subject as string | undefined;
|
|
67
|
+
|
|
68
|
+
if (!replyTo) {
|
|
69
|
+
if (!to.length) {
|
|
70
|
+
throw new CliError('INVALID_PARAMS', '--to is required (unless using --reply-to)');
|
|
71
|
+
}
|
|
72
|
+
if (!subject) {
|
|
73
|
+
throw new CliError('INVALID_PARAMS', '--subject is required (unless using --reply-to)');
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const body = await parseBody(options.body as string | undefined);
|
|
78
|
+
|
|
79
|
+
const regularAttachments = parseAttachments(options.attachment as string[]);
|
|
80
|
+
const inlineAttachments = parseInlineAttachments(options.inline as string[]);
|
|
81
|
+
|
|
82
|
+
const attachments: GmailAttachment[] | undefined =
|
|
83
|
+
regularAttachments.length || inlineAttachments.length
|
|
84
|
+
? [...regularAttachments, ...inlineAttachments]
|
|
85
|
+
: undefined;
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
to,
|
|
89
|
+
cc: (options.cc as string[]).length ? options.cc as string[] : undefined,
|
|
90
|
+
bcc: (options.bcc as string[]).length ? options.bcc as string[] : undefined,
|
|
91
|
+
subject: subject || '',
|
|
92
|
+
body,
|
|
93
|
+
isHtml: options.html as boolean | undefined,
|
|
94
|
+
attachments,
|
|
95
|
+
replyTo,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
15
98
|
|
|
16
99
|
function escapeHtml(text: string): string {
|
|
17
100
|
return text
|
|
@@ -176,102 +259,27 @@ Query Syntax Examples:
|
|
|
176
259
|
}
|
|
177
260
|
});
|
|
178
261
|
|
|
179
|
-
gmail
|
|
180
|
-
.command('send')
|
|
181
|
-
.description('Send an email')
|
|
182
|
-
.option('--profile <name>', 'Profile name (optional if only one profile exists)')
|
|
183
|
-
.requiredOption('--to <email>', 'Recipient (repeatable)', (val, acc: string[]) => [...acc, val], [])
|
|
184
|
-
.option('--cc <email>', 'CC recipient (repeatable)', (val, acc: string[]) => [...acc, val], [])
|
|
185
|
-
.option('--bcc <email>', 'BCC recipient (repeatable)', (val, acc: string[]) => [...acc, val], [])
|
|
186
|
-
.requiredOption('--subject <subject>', 'Email subject')
|
|
187
|
-
.option('--body <body>', 'Email body (or pipe via stdin)')
|
|
188
|
-
.option('--html', 'Treat body as HTML')
|
|
189
|
-
.option('--attachment <path>', 'File to attach (repeatable)', (val, acc: string[]) => [...acc, val], [])
|
|
190
|
-
.option('--inline <cid:path>', 'Inline image (repeatable, format: contentId:filepath). Supports PNG, JPG, GIF only (not SVG)', (val, acc: string[]) => [...acc, val], [])
|
|
262
|
+
addComposeOptions(gmail.command('send').description('Send an email'))
|
|
191
263
|
.action(async (options) => {
|
|
192
264
|
try {
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
// Check for stdin if no body provided
|
|
196
|
-
if (!body) {
|
|
197
|
-
body = await readStdin();
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
if (!body) {
|
|
201
|
-
throw new CliError('INVALID_PARAMS', 'Body is required. Use --body or pipe via stdin.');
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
// Process regular attachments
|
|
205
|
-
const regularAttachments: GmailAttachment[] = options.attachment.map((path: string) => ({
|
|
206
|
-
path,
|
|
207
|
-
filename: basename(path),
|
|
208
|
-
}));
|
|
209
|
-
|
|
210
|
-
// Process inline images (format: contentId:filepath)
|
|
211
|
-
const inlineAttachments: GmailAttachment[] = options.inline.map((spec: string) => {
|
|
212
|
-
const colonIndex = spec.indexOf(':');
|
|
213
|
-
if (colonIndex === -1) {
|
|
214
|
-
throw new CliError('INVALID_PARAMS', `Invalid inline format: ${spec}`, 'Use format: contentId:filepath (e.g., logo:./logo.png)');
|
|
215
|
-
}
|
|
216
|
-
const contentId = spec.substring(0, colonIndex);
|
|
217
|
-
const path = spec.substring(colonIndex + 1);
|
|
218
|
-
return {
|
|
219
|
-
path,
|
|
220
|
-
filename: basename(path),
|
|
221
|
-
contentId,
|
|
222
|
-
};
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
// Combine attachments
|
|
226
|
-
const attachments: GmailAttachment[] | undefined =
|
|
227
|
-
regularAttachments.length || inlineAttachments.length
|
|
228
|
-
? [...regularAttachments, ...inlineAttachments]
|
|
229
|
-
: undefined;
|
|
230
|
-
|
|
265
|
+
const sendOptions = await parseSendOptions(options);
|
|
231
266
|
const { client, profile } = await getGmailClient(options.profile);
|
|
232
267
|
await enforceWriteAccess('gmail', profile, 'send email');
|
|
233
|
-
const result = await client.send(
|
|
234
|
-
to: options.to,
|
|
235
|
-
cc: options.cc.length ? options.cc : undefined,
|
|
236
|
-
bcc: options.bcc.length ? options.bcc : undefined,
|
|
237
|
-
subject: options.subject,
|
|
238
|
-
body,
|
|
239
|
-
isHtml: options.html,
|
|
240
|
-
attachments,
|
|
241
|
-
});
|
|
268
|
+
const result = await client.send(sendOptions);
|
|
242
269
|
printSendResult(result);
|
|
243
270
|
} catch (error) {
|
|
244
271
|
handleError(error);
|
|
245
272
|
}
|
|
246
273
|
});
|
|
247
274
|
|
|
248
|
-
gmail
|
|
249
|
-
.command('reply')
|
|
250
|
-
.description('Reply to a thread')
|
|
251
|
-
.option('--profile <name>', 'Profile name (optional if only one profile exists)')
|
|
252
|
-
.requiredOption('--thread-id <id>', 'Thread ID')
|
|
253
|
-
.option('--body <body>', 'Reply body (or pipe via stdin)')
|
|
254
|
-
.option('--html', 'Treat body as HTML')
|
|
275
|
+
addComposeOptions(gmail.command('draft').description('Create an email draft'))
|
|
255
276
|
.action(async (options) => {
|
|
256
277
|
try {
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
if (!body) {
|
|
260
|
-
body = await readStdin();
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
if (!body) {
|
|
264
|
-
throw new CliError('INVALID_PARAMS', 'Body is required. Use --body or pipe via stdin.');
|
|
265
|
-
}
|
|
266
|
-
|
|
278
|
+
const sendOptions = await parseSendOptions(options);
|
|
267
279
|
const { client, profile } = await getGmailClient(options.profile);
|
|
268
|
-
await enforceWriteAccess('gmail', profile, '
|
|
269
|
-
const result = await client.
|
|
270
|
-
|
|
271
|
-
body,
|
|
272
|
-
isHtml: options.html,
|
|
273
|
-
});
|
|
274
|
-
printSendResult(result);
|
|
280
|
+
await enforceWriteAccess('gmail', profile, 'create draft');
|
|
281
|
+
const result = await client.draft(sendOptions);
|
|
282
|
+
printDraftResult(result);
|
|
275
283
|
} catch (error) {
|
|
276
284
|
handleError(error);
|
|
277
285
|
}
|
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
import { gmail, type gmail_v1 } from '@googleapis/gmail';
|
|
2
2
|
import type { OAuth2Client } from 'google-auth-library';
|
|
3
3
|
import { basename } from 'path';
|
|
4
|
-
import type { GmailMessage, GmailListOptions, GmailSendOptions,
|
|
4
|
+
import type { GmailMessage, GmailListOptions, GmailSendOptions, GmailAttachment, GmailAttachmentInfo } from '../../types/gmail';
|
|
5
5
|
import type { ServiceClient, ValidationResult } from '../../types/service';
|
|
6
6
|
import { CliError } from '../../utils/errors';
|
|
7
7
|
|
|
8
|
+
// RFC 2047 encode a header value if it contains non-ASCII characters
|
|
9
|
+
function encodeHeaderValue(value: string): string {
|
|
10
|
+
// eslint-disable-next-line no-control-regex
|
|
11
|
+
if (/^[\x00-\x7F]*$/.test(value)) return value;
|
|
12
|
+
return `=?UTF-8?B?${Buffer.from(value, 'utf-8').toString('base64')}?=`;
|
|
13
|
+
}
|
|
14
|
+
|
|
8
15
|
// Common MIME types by extension
|
|
9
16
|
const MIME_TYPES: Record<string, string> = {
|
|
10
17
|
'.txt': 'text/plain',
|
|
@@ -265,14 +272,57 @@ export class GmailClient implements ServiceClient {
|
|
|
265
272
|
return this.list({ query, limit });
|
|
266
273
|
}
|
|
267
274
|
|
|
268
|
-
async
|
|
269
|
-
|
|
275
|
+
private async resolveReplyContext(threadId: string): Promise<{
|
|
276
|
+
to: string[];
|
|
277
|
+
subject: string;
|
|
278
|
+
extraHeaders: string[];
|
|
279
|
+
}> {
|
|
280
|
+
const thread = await this.gmail.users.threads.get({
|
|
281
|
+
userId: 'me',
|
|
282
|
+
id: threadId,
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
const messages = thread.data.messages || [];
|
|
286
|
+
if (messages.length === 0) {
|
|
287
|
+
throw new CliError('NOT_FOUND', `Thread not found: ${threadId}`);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const lastMessage = messages[messages.length - 1];
|
|
291
|
+
const headers = this.parseHeaders(lastMessage.payload?.headers);
|
|
292
|
+
|
|
293
|
+
const to = [headers['reply-to'] || headers['from'] || ''];
|
|
294
|
+
const subject = headers['subject']?.startsWith('Re:')
|
|
295
|
+
? headers['subject']
|
|
296
|
+
: `Re: ${headers['subject'] || '(no subject)'}`;
|
|
297
|
+
const messageId = headers['message-id'] || '';
|
|
298
|
+
|
|
299
|
+
const extraHeaders: string[] = [];
|
|
300
|
+
if (messageId) {
|
|
301
|
+
extraHeaders.push(`In-Reply-To: ${messageId}`);
|
|
302
|
+
extraHeaders.push(`References: ${messageId}`);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return { to, subject, extraHeaders };
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
private async buildEncodedMessage(options: GmailSendOptions): Promise<string> {
|
|
309
|
+
const { cc, bcc, body, isHtml, attachments, replyTo } = options;
|
|
270
310
|
const userEmail = await this.getUserEmail();
|
|
271
311
|
|
|
312
|
+
let to = options.to;
|
|
313
|
+
let subject = options.subject;
|
|
314
|
+
let extraHeaders: string[] | undefined;
|
|
315
|
+
|
|
316
|
+
if (replyTo) {
|
|
317
|
+
const replyContext = await this.resolveReplyContext(replyTo);
|
|
318
|
+
if (!to.length) to = replyContext.to;
|
|
319
|
+
if (!subject) subject = replyContext.subject;
|
|
320
|
+
extraHeaders = replyContext.extraHeaders;
|
|
321
|
+
}
|
|
322
|
+
|
|
272
323
|
let rawMessage: string;
|
|
273
324
|
|
|
274
325
|
if (attachments && attachments.length > 0) {
|
|
275
|
-
// Build multipart MIME message with attachments
|
|
276
326
|
rawMessage = await this.buildMultipartMessage({
|
|
277
327
|
from: userEmail,
|
|
278
328
|
to,
|
|
@@ -282,27 +332,35 @@ export class GmailClient implements ServiceClient {
|
|
|
282
332
|
body,
|
|
283
333
|
isHtml,
|
|
284
334
|
attachments,
|
|
335
|
+
extraHeaders,
|
|
285
336
|
});
|
|
286
337
|
} else {
|
|
287
|
-
// Simple message without attachments
|
|
288
338
|
rawMessage = [
|
|
289
339
|
`From: ${userEmail}`,
|
|
290
340
|
`To: ${to.join(', ')}`,
|
|
291
341
|
cc?.length ? `Cc: ${cc.join(', ')}` : null,
|
|
292
342
|
bcc?.length ? `Bcc: ${bcc.join(', ')}` : null,
|
|
293
|
-
`Subject: ${subject}`,
|
|
343
|
+
`Subject: ${encodeHeaderValue(subject)}`,
|
|
344
|
+
...(extraHeaders || []),
|
|
294
345
|
`Content-Type: ${isHtml ? 'text/html' : 'text/plain'}; charset=utf-8`,
|
|
295
346
|
'',
|
|
296
347
|
body,
|
|
297
348
|
].filter((line): line is string => line !== null).join('\r\n');
|
|
298
349
|
}
|
|
299
350
|
|
|
300
|
-
|
|
351
|
+
return Buffer.from(rawMessage).toString('base64url');
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
async send(options: GmailSendOptions): Promise<{ id: string; threadId: string; labelIds: string[] }> {
|
|
355
|
+
const encodedMessage = await this.buildEncodedMessage(options);
|
|
301
356
|
|
|
302
357
|
try {
|
|
303
358
|
const response = await this.gmail.users.messages.send({
|
|
304
359
|
userId: 'me',
|
|
305
|
-
requestBody: {
|
|
360
|
+
requestBody: {
|
|
361
|
+
raw: encodedMessage,
|
|
362
|
+
...(options.replyTo ? { threadId: options.replyTo } : {}),
|
|
363
|
+
},
|
|
306
364
|
});
|
|
307
365
|
|
|
308
366
|
return {
|
|
@@ -316,6 +374,30 @@ export class GmailClient implements ServiceClient {
|
|
|
316
374
|
}
|
|
317
375
|
}
|
|
318
376
|
|
|
377
|
+
async draft(options: GmailSendOptions): Promise<{ id: string; messageId: string }> {
|
|
378
|
+
const encodedMessage = await this.buildEncodedMessage(options);
|
|
379
|
+
|
|
380
|
+
try {
|
|
381
|
+
const response = await this.gmail.users.drafts.create({
|
|
382
|
+
userId: 'me',
|
|
383
|
+
requestBody: {
|
|
384
|
+
message: {
|
|
385
|
+
raw: encodedMessage,
|
|
386
|
+
...(options.replyTo ? { threadId: options.replyTo } : {}),
|
|
387
|
+
},
|
|
388
|
+
},
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
return {
|
|
392
|
+
id: response.data.id!,
|
|
393
|
+
messageId: response.data.message?.id || '',
|
|
394
|
+
};
|
|
395
|
+
} catch (error) {
|
|
396
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
397
|
+
throw new CliError('API_ERROR', `Failed to create draft: ${message}`);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
319
401
|
private async buildMultipartMessage(options: {
|
|
320
402
|
from: string;
|
|
321
403
|
to: string[];
|
|
@@ -325,8 +407,9 @@ export class GmailClient implements ServiceClient {
|
|
|
325
407
|
body: string;
|
|
326
408
|
isHtml?: boolean;
|
|
327
409
|
attachments: GmailAttachment[];
|
|
410
|
+
extraHeaders?: string[];
|
|
328
411
|
}): Promise<string> {
|
|
329
|
-
const { from, to, cc, bcc, subject, body, isHtml, attachments } = options;
|
|
412
|
+
const { from, to, cc, bcc, subject, body, isHtml, attachments, extraHeaders } = options;
|
|
330
413
|
|
|
331
414
|
// Separate inline and regular attachments
|
|
332
415
|
const inlineAttachments = attachments.filter(a => a.contentId);
|
|
@@ -346,7 +429,12 @@ export class GmailClient implements ServiceClient {
|
|
|
346
429
|
lines.push(`To: ${to.join(', ')}`);
|
|
347
430
|
if (cc?.length) lines.push(`Cc: ${cc.join(', ')}`);
|
|
348
431
|
if (bcc?.length) lines.push(`Bcc: ${bcc.join(', ')}`);
|
|
349
|
-
lines.push(`Subject: ${subject}`);
|
|
432
|
+
lines.push(`Subject: ${encodeHeaderValue(subject)}`);
|
|
433
|
+
if (extraHeaders) {
|
|
434
|
+
for (const header of extraHeaders) {
|
|
435
|
+
lines.push(header);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
350
438
|
lines.push('MIME-Version: 1.0');
|
|
351
439
|
|
|
352
440
|
if (hasRegular && hasInline) {
|
|
@@ -464,64 +552,6 @@ export class GmailClient implements ServiceClient {
|
|
|
464
552
|
}
|
|
465
553
|
}
|
|
466
554
|
|
|
467
|
-
async reply(options: GmailReplyOptions): Promise<{ id: string; threadId: string; labelIds: string[] }> {
|
|
468
|
-
const { threadId, body, isHtml } = options;
|
|
469
|
-
|
|
470
|
-
// Get the thread to find the last message
|
|
471
|
-
try {
|
|
472
|
-
const thread = await this.gmail.users.threads.get({
|
|
473
|
-
userId: 'me',
|
|
474
|
-
id: threadId,
|
|
475
|
-
});
|
|
476
|
-
|
|
477
|
-
const messages = thread.data.messages || [];
|
|
478
|
-
if (messages.length === 0) {
|
|
479
|
-
throw new CliError('NOT_FOUND', `Thread not found: ${threadId}`);
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
const lastMessage = messages[messages.length - 1];
|
|
483
|
-
const headers = this.parseHeaders(lastMessage.payload?.headers);
|
|
484
|
-
|
|
485
|
-
const userEmail = await this.getUserEmail();
|
|
486
|
-
const replyTo = headers['reply-to'] || headers['from'] || '';
|
|
487
|
-
const subject = headers['subject']?.startsWith('Re:')
|
|
488
|
-
? headers['subject']
|
|
489
|
-
: `Re: ${headers['subject'] || '(no subject)'}`;
|
|
490
|
-
const messageId = headers['message-id'] || '';
|
|
491
|
-
|
|
492
|
-
const rawHeaders = [
|
|
493
|
-
`From: ${userEmail}`,
|
|
494
|
-
`To: ${replyTo}`,
|
|
495
|
-
`Subject: ${subject}`,
|
|
496
|
-
messageId ? `In-Reply-To: ${messageId}` : '',
|
|
497
|
-
messageId ? `References: ${messageId}` : '',
|
|
498
|
-
`Content-Type: ${isHtml ? 'text/html' : 'text/plain'}; charset=utf-8`,
|
|
499
|
-
'',
|
|
500
|
-
body,
|
|
501
|
-
].filter(Boolean).join('\r\n');
|
|
502
|
-
|
|
503
|
-
const encodedMessage = Buffer.from(rawHeaders).toString('base64url');
|
|
504
|
-
|
|
505
|
-
const response = await this.gmail.users.messages.send({
|
|
506
|
-
userId: 'me',
|
|
507
|
-
requestBody: {
|
|
508
|
-
raw: encodedMessage,
|
|
509
|
-
threadId,
|
|
510
|
-
},
|
|
511
|
-
});
|
|
512
|
-
|
|
513
|
-
return {
|
|
514
|
-
id: response.data.id!,
|
|
515
|
-
threadId: response.data.threadId!,
|
|
516
|
-
labelIds: response.data.labelIds || ['SENT'],
|
|
517
|
-
};
|
|
518
|
-
} catch (error) {
|
|
519
|
-
if (error instanceof CliError) throw error;
|
|
520
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
521
|
-
throw new CliError('API_ERROR', `Failed to send reply: ${message}`);
|
|
522
|
-
}
|
|
523
|
-
}
|
|
524
|
-
|
|
525
555
|
async archive(messageId: string): Promise<void> {
|
|
526
556
|
try {
|
|
527
557
|
await this.gmail.users.messages.modify({
|
package/src/types/gmail.ts
CHANGED
|
@@ -40,10 +40,5 @@ export interface GmailSendOptions {
|
|
|
40
40
|
body: string;
|
|
41
41
|
isHtml?: boolean;
|
|
42
42
|
attachments?: GmailAttachment[];
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
export interface GmailReplyOptions {
|
|
46
|
-
threadId: string;
|
|
47
|
-
body: string;
|
|
48
|
-
isHtml?: boolean;
|
|
43
|
+
replyTo?: string; // Thread ID to reply to
|
|
49
44
|
}
|
package/src/utils/output.ts
CHANGED
|
@@ -86,6 +86,13 @@ export function printSendResult(result: { id: string; threadId: string }): void
|
|
|
86
86
|
console.log(`Thread: ${result.threadId}`);
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
+
// Format draft creation result
|
|
90
|
+
export function printDraftResult(result: { id: string; messageId: string }): void {
|
|
91
|
+
console.log('Draft created');
|
|
92
|
+
console.log(`Draft ID: ${result.id}`);
|
|
93
|
+
console.log(`Message ID: ${result.messageId}`);
|
|
94
|
+
}
|
|
95
|
+
|
|
89
96
|
// Format archive confirmation
|
|
90
97
|
export function printArchived(messageId: string): void {
|
|
91
98
|
console.log(`Archived: ${messageId}`);
|