gogcli-mcp-gmail 2.4.1 → 2.5.0
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/dist/index.js +15 -31
- package/manifest.json +1 -1
- package/package.json +1 -1
- package/src/tools/gmail-extra.ts +14 -45
- package/tests/tools/gmail-extra.test.ts +18 -53
package/dist/index.js
CHANGED
|
@@ -31274,14 +31274,16 @@ function registerGmailTools(server2) {
|
|
|
31274
31274
|
bcc: external_exports.string().optional().describe("BCC recipients, comma-separated"),
|
|
31275
31275
|
replyToMessageId: external_exports.string().optional().describe("Message ID to reply to"),
|
|
31276
31276
|
threadId: external_exports.string().optional().describe("Thread ID to reply within"),
|
|
31277
|
+
attach: external_exports.array(external_exports.string()).optional().describe("Local file paths to attach (repeatable). Each file is read on the gog server (not this client), base64-encoded with a MIME type inferred from its extension, and added as a multipart attachment. Keep the total under Gmail's ~35 MB inline-upload limit."),
|
|
31277
31278
|
account: accountParam
|
|
31278
31279
|
}
|
|
31279
|
-
}, async ({ to, subject, body, cc, bcc, replyToMessageId, threadId, account }) => {
|
|
31280
|
+
}, async ({ to, subject, body, cc, bcc, replyToMessageId, threadId, attach, account }) => {
|
|
31280
31281
|
const args = ["gmail", "send", `--to=${to}`, `--subject=${subject}`, `--body=${body}`];
|
|
31281
31282
|
if (cc) args.push(`--cc=${cc}`);
|
|
31282
31283
|
if (bcc) args.push(`--bcc=${bcc}`);
|
|
31283
31284
|
if (replyToMessageId) args.push(`--reply-to-message-id=${replyToMessageId}`);
|
|
31284
31285
|
if (threadId) args.push(`--thread-id=${threadId}`);
|
|
31286
|
+
if (attach) for (const path of attach) args.push(`--attach=${path}`);
|
|
31285
31287
|
return runOrDiagnose(args, { account });
|
|
31286
31288
|
});
|
|
31287
31289
|
registerRunTool(server2, { service: "gmail", examples: '"archive", "mark-read", "labels"' });
|
|
@@ -31297,7 +31299,7 @@ var failIfNotEmptyParam = external_exports.boolean().optional().describe(
|
|
|
31297
31299
|
);
|
|
31298
31300
|
|
|
31299
31301
|
// ../gogcli-mcp/src/server.ts
|
|
31300
|
-
var VERSION = true ? "2.
|
|
31302
|
+
var VERSION = true ? "2.5.0" : "0.0.0";
|
|
31301
31303
|
function createServer(options) {
|
|
31302
31304
|
return new McpServer({
|
|
31303
31305
|
name: options?.name ?? "gogcli",
|
|
@@ -31631,10 +31633,10 @@ function registerExtraGmailTools(server2) {
|
|
|
31631
31633
|
body: external_exports.string().describe("Body (plain text)"),
|
|
31632
31634
|
bodyHtml: external_exports.string().optional().describe("Body (HTML; optional)"),
|
|
31633
31635
|
replyToMessageId: external_exports.string().optional().describe("Reply to a specific Gmail MESSAGE id \u2014 the short hex `id` field from gog_gmail_get / _search / _thread_get (e.g. 19e7593d77fd9636), NOT a thread id and NOT the RFC822 `<\u2026@host>` Message-Id header. Anchors In-Reply-To/References to that exact message. To reply to a thread when you don't know the latest message, use replyToThreadId instead. If both are given, replyToMessageId wins."),
|
|
31634
|
-
replyToThreadId: external_exports.string().optional().describe(`Reply to a Gmail THREAD id \u2014
|
|
31636
|
+
replyToThreadId: external_exports.string().optional().describe(`Reply to a Gmail THREAD id \u2014 passed to gog as --thread-id, which threads the draft using the thread's latest-message headers (In-Reply-To/References). This is what "reply to this thread" almost always means. Mutually exclusive with replyToMessageId (which wins if both are set). Thread ids and message ids are both 16-hex strings and easy to confuse \u2014 use this param, not replyToMessageId, when the id came from a thread.`),
|
|
31635
31637
|
replyTo: external_exports.string().optional().describe("Reply-To header address"),
|
|
31636
31638
|
quote: external_exports.boolean().optional().describe("Include quoted original message in reply (requires replyToMessageId or replyToThreadId)"),
|
|
31637
|
-
attach: external_exports.array(external_exports.string()).optional().describe("
|
|
31639
|
+
attach: external_exports.array(external_exports.string()).optional().describe("Local file paths to attach (repeatable). Read on the gog server, base64-encoded with a MIME type inferred from the extension. On gog_gmail_drafts_update, supplying attach REPLACES the draft's existing attachments; omitting it preserves them (use clearAttachments to remove all)."),
|
|
31638
31640
|
from: external_exports.string().optional().describe("Send from this email address (must be a verified send-as alias)"),
|
|
31639
31641
|
omitRecipients: external_exports.boolean().optional().describe("Create the draft with no recipients even if to/cc/bcc are supplied \u2014 an accidental-send guard. Populate recipients in a later update before sending."),
|
|
31640
31642
|
returnFull: external_exports.boolean().optional().describe("After writing, re-fetch and return the full stored draft (subject, body, recipients) instead of just the write acknowledgement. Costs one extra read."),
|
|
@@ -31650,28 +31652,12 @@ function registerExtraGmailTools(server2) {
|
|
|
31650
31652
|
args.push(`--body=${f.body}`);
|
|
31651
31653
|
if (f.bodyHtml) args.push(`--body-html=${f.bodyHtml}`);
|
|
31652
31654
|
if (f.replyToMessageId) args.push(`--reply-to-message-id=${f.replyToMessageId}`);
|
|
31655
|
+
else if (f.replyToThreadId) args.push(`--thread-id=${f.replyToThreadId}`);
|
|
31653
31656
|
if (f.replyTo) args.push(`--reply-to=${f.replyTo}`);
|
|
31654
31657
|
if (f.quote) args.push("--quote");
|
|
31655
31658
|
if (f.attach) for (const path of f.attach) args.push(`--attach=${path}`);
|
|
31656
31659
|
if (f.from) args.push(`--from=${f.from}`);
|
|
31657
31660
|
}
|
|
31658
|
-
async function resolveReplyTarget(replyToMessageId, replyToThreadId, account) {
|
|
31659
|
-
if (replyToMessageId) return { messageId: replyToMessageId };
|
|
31660
|
-
if (!replyToThreadId) return { messageId: void 0 };
|
|
31661
|
-
const threadResult = await runOrDiagnose(["gmail", "thread", "get", replyToThreadId], { account });
|
|
31662
|
-
let parsed;
|
|
31663
|
-
try {
|
|
31664
|
-
parsed = JSON.parse(threadResult.content[0].text);
|
|
31665
|
-
} catch {
|
|
31666
|
-
return { error: threadResult };
|
|
31667
|
-
}
|
|
31668
|
-
const messages = parsed.thread?.messages;
|
|
31669
|
-
const latest = Array.isArray(messages) ? messages[messages.length - 1] : void 0;
|
|
31670
|
-
if (!latest?.id) {
|
|
31671
|
-
return { error: toText(`Error: thread ${replyToThreadId} has no message to reply to`) };
|
|
31672
|
-
}
|
|
31673
|
-
return { messageId: latest.id };
|
|
31674
|
-
}
|
|
31675
31661
|
async function writeDraft(args, account, returnFull, knownDraftId) {
|
|
31676
31662
|
const result = await runOrDiagnose(args, { account });
|
|
31677
31663
|
if (!returnFull) return result;
|
|
@@ -31688,25 +31674,23 @@ function registerExtraGmailTools(server2) {
|
|
|
31688
31674
|
server2.registerTool("gog_gmail_drafts_create", {
|
|
31689
31675
|
description: "Create a new Gmail draft. Recipients (to/cc/bcc) are optional; omit them (or set omitRecipients) to create a recipient-less draft as an accidental-send guard. For replies, prefer replyToThreadId (anchors to the thread's latest message) or replyToMessageId (a specific message) \u2014 don't pass a thread id into replyToMessageId, which mis-threads silently.",
|
|
31690
31676
|
inputSchema: draftWriteSchema
|
|
31691
|
-
}, async ({ account, returnFull,
|
|
31692
|
-
const resolved = await resolveReplyTarget(flags.replyToMessageId, replyToThreadId, account);
|
|
31693
|
-
if ("error" in resolved) return resolved.error;
|
|
31677
|
+
}, async ({ account, returnFull, ...flags }) => {
|
|
31694
31678
|
const args = ["gmail", "drafts", "create"];
|
|
31695
|
-
appendDraftFlags(args,
|
|
31679
|
+
appendDraftFlags(args, flags);
|
|
31696
31680
|
return writeDraft(args, account, returnFull);
|
|
31697
31681
|
});
|
|
31698
31682
|
server2.registerTool("gog_gmail_drafts_update", {
|
|
31699
|
-
description: "Update an existing Gmail draft. For replies, prefer replyToThreadId (
|
|
31683
|
+
description: "Update an existing Gmail draft. For replies, prefer replyToThreadId (threads off the thread's latest message) or replyToMessageId (a specific message) over passing a thread id into replyToMessageId. Attachment semantics: supplying attach REPLACES the draft's existing attachments; omitting it preserves them; set clearAttachments to remove all.",
|
|
31700
31684
|
annotations: { destructiveHint: true },
|
|
31701
31685
|
inputSchema: {
|
|
31702
31686
|
draftId: external_exports.string().describe("Draft ID"),
|
|
31703
|
-
...draftWriteSchema
|
|
31687
|
+
...draftWriteSchema,
|
|
31688
|
+
clearAttachments: external_exports.boolean().optional().describe("Remove all attachments from the draft. By default, omitting attach preserves the draft's existing attachments; this intentionally clears them. Ignored if attach is also supplied (attach replaces).")
|
|
31704
31689
|
}
|
|
31705
|
-
}, async ({ draftId, account, returnFull,
|
|
31706
|
-
const resolved = await resolveReplyTarget(flags.replyToMessageId, replyToThreadId, account);
|
|
31707
|
-
if ("error" in resolved) return resolved.error;
|
|
31690
|
+
}, async ({ draftId, account, returnFull, clearAttachments, ...flags }) => {
|
|
31708
31691
|
const args = ["gmail", "drafts", "update", draftId];
|
|
31709
|
-
appendDraftFlags(args,
|
|
31692
|
+
appendDraftFlags(args, flags);
|
|
31693
|
+
if (clearAttachments) args.push("--clear-attachments");
|
|
31710
31694
|
return writeDraft(args, account, returnFull, draftId);
|
|
31711
31695
|
});
|
|
31712
31696
|
server2.registerTool("gog_gmail_drafts_delete", {
|
package/manifest.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"manifest_version": "0.3",
|
|
4
4
|
"name": "gogcli-mcp-gmail",
|
|
5
5
|
"display_name": "gogcli (Gmail)",
|
|
6
|
-
"version": "2.
|
|
6
|
+
"version": "2.5.0",
|
|
7
7
|
"description": "Extended Gmail for Claude via gogcli — auth + full Gmail support (threads, labels, drafts, attachments, forward, autoreply, bulk operations)",
|
|
8
8
|
"author": {
|
|
9
9
|
"name": "Chris Hall",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gogcli-mcp-gmail",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.5.0",
|
|
4
4
|
"mcpName": "io.github.chrischall/gogcli-mcp-gmail",
|
|
5
5
|
"description": "Extended Gmail MCP server via gogcli — auth + full Gmail support (threads, labels, drafts, attachments, forward, autoreply, bulk operations)",
|
|
6
6
|
"author": "Claude Code (AI) <https://www.anthropic.com/claude>",
|
package/src/tools/gmail-extra.ts
CHANGED
|
@@ -372,10 +372,10 @@ export function registerExtraGmailTools(server: McpServer): void {
|
|
|
372
372
|
body: z.string().describe('Body (plain text)'),
|
|
373
373
|
bodyHtml: z.string().optional().describe('Body (HTML; optional)'),
|
|
374
374
|
replyToMessageId: z.string().optional().describe('Reply to a specific Gmail MESSAGE id — the short hex `id` field from gog_gmail_get / _search / _thread_get (e.g. 19e7593d77fd9636), NOT a thread id and NOT the RFC822 `<…@host>` Message-Id header. Anchors In-Reply-To/References to that exact message. To reply to a thread when you don\'t know the latest message, use replyToThreadId instead. If both are given, replyToMessageId wins.'),
|
|
375
|
-
replyToThreadId: z.string().optional().describe('Reply to a Gmail THREAD id — the
|
|
375
|
+
replyToThreadId: z.string().optional().describe('Reply to a Gmail THREAD id — passed to gog as --thread-id, which threads the draft using the thread\'s latest-message headers (In-Reply-To/References). This is what "reply to this thread" almost always means. Mutually exclusive with replyToMessageId (which wins if both are set). Thread ids and message ids are both 16-hex strings and easy to confuse — use this param, not replyToMessageId, when the id came from a thread.'),
|
|
376
376
|
replyTo: z.string().optional().describe('Reply-To header address'),
|
|
377
377
|
quote: z.boolean().optional().describe('Include quoted original message in reply (requires replyToMessageId or replyToThreadId)'),
|
|
378
|
-
attach: z.array(z.string()).optional().describe('
|
|
378
|
+
attach: z.array(z.string()).optional().describe('Local file paths to attach (repeatable). Read on the gog server, base64-encoded with a MIME type inferred from the extension. On gog_gmail_drafts_update, supplying attach REPLACES the draft\'s existing attachments; omitting it preserves them (use clearAttachments to remove all).'),
|
|
379
379
|
from: z.string().optional().describe('Send from this email address (must be a verified send-as alias)'),
|
|
380
380
|
omitRecipients: z.boolean().optional().describe('Create the draft with no recipients even if to/cc/bcc are supplied — an accidental-send guard. Populate recipients in a later update before sending.'),
|
|
381
381
|
returnFull: z.boolean().optional().describe('After writing, re-fetch and return the full stored draft (subject, body, recipients) instead of just the write acknowledgement. Costs one extra read.'),
|
|
@@ -390,6 +390,7 @@ export function registerExtraGmailTools(server: McpServer): void {
|
|
|
390
390
|
body: string;
|
|
391
391
|
bodyHtml?: string;
|
|
392
392
|
replyToMessageId?: string;
|
|
393
|
+
replyToThreadId?: string;
|
|
393
394
|
replyTo?: string;
|
|
394
395
|
quote?: boolean;
|
|
395
396
|
attach?: string[];
|
|
@@ -406,47 +407,17 @@ export function registerExtraGmailTools(server: McpServer): void {
|
|
|
406
407
|
args.push(`--subject=${f.subject}`);
|
|
407
408
|
args.push(`--body=${f.body}`);
|
|
408
409
|
if (f.bodyHtml) args.push(`--body-html=${f.bodyHtml}`);
|
|
410
|
+
// A draft can reply to a specific message (--reply-to-message-id) or thread
|
|
411
|
+
// off the latest message in a thread (--thread-id, which gog resolves
|
|
412
|
+
// server-side). replyToMessageId wins when both are supplied.
|
|
409
413
|
if (f.replyToMessageId) args.push(`--reply-to-message-id=${f.replyToMessageId}`);
|
|
414
|
+
else if (f.replyToThreadId) args.push(`--thread-id=${f.replyToThreadId}`);
|
|
410
415
|
if (f.replyTo) args.push(`--reply-to=${f.replyTo}`);
|
|
411
416
|
if (f.quote) args.push('--quote');
|
|
412
417
|
if (f.attach) for (const path of f.attach) args.push(`--attach=${path}`);
|
|
413
418
|
if (f.from) args.push(`--from=${f.from}`);
|
|
414
419
|
}
|
|
415
420
|
|
|
416
|
-
// Resolve the effective reply target for a draft write. A draft can only
|
|
417
|
-
// thread off a specific MESSAGE id, but callers usually have a THREAD id and
|
|
418
|
-
// mean "reply to this thread". gog's `drafts create/update` has no --thread-id
|
|
419
|
-
// (unlike `gmail send`), so we resolve thread -> latest message here and pass
|
|
420
|
-
// that message id — gog then sets In-Reply-To/References from its RFC822
|
|
421
|
-
// Message-Id. replyToMessageId always wins; when only a thread id is given we
|
|
422
|
-
// fetch the thread and reply to its most recent message. Returns either the
|
|
423
|
-
// resolved message id (possibly undefined when neither was supplied) or an
|
|
424
|
-
// error ToolResult that the handler surfaces instead of writing a
|
|
425
|
-
// mis-threaded draft. (When upstream adds --thread-id to drafts — openclaw/
|
|
426
|
-
// gogcli#673 — this can pass it through directly with no MCP surface change.)
|
|
427
|
-
async function resolveReplyTarget(
|
|
428
|
-
replyToMessageId: string | undefined,
|
|
429
|
-
replyToThreadId: string | undefined,
|
|
430
|
-
account: string | undefined,
|
|
431
|
-
): Promise<{ messageId?: string } | { error: ToolResult }> {
|
|
432
|
-
if (replyToMessageId) return { messageId: replyToMessageId };
|
|
433
|
-
if (!replyToThreadId) return { messageId: undefined };
|
|
434
|
-
const threadResult = await runOrDiagnose(['gmail', 'thread', 'get', replyToThreadId], { account });
|
|
435
|
-
let parsed: { thread?: { messages?: GmailMessage[] } };
|
|
436
|
-
try {
|
|
437
|
-
parsed = JSON.parse(threadResult.content[0].text) as { thread?: { messages?: GmailMessage[] } };
|
|
438
|
-
} catch {
|
|
439
|
-
// Non-JSON output means the thread fetch itself errored — surface it.
|
|
440
|
-
return { error: threadResult };
|
|
441
|
-
}
|
|
442
|
-
const messages = parsed.thread?.messages;
|
|
443
|
-
const latest = Array.isArray(messages) ? messages[messages.length - 1] : undefined;
|
|
444
|
-
if (!latest?.id) {
|
|
445
|
-
return { error: toText(`Error: thread ${replyToThreadId} has no message to reply to`) };
|
|
446
|
-
}
|
|
447
|
-
return { messageId: latest.id };
|
|
448
|
-
}
|
|
449
|
-
|
|
450
421
|
// Run a draft write, then — when returnFull is set — re-fetch the stored
|
|
451
422
|
// draft so the caller can verify subject/body/recipients persisted without a
|
|
452
423
|
// separate gog_gmail_drafts_get round trip. For updates the id is known up
|
|
@@ -478,26 +449,24 @@ export function registerExtraGmailTools(server: McpServer): void {
|
|
|
478
449
|
server.registerTool('gog_gmail_drafts_create', {
|
|
479
450
|
description: 'Create a new Gmail draft. Recipients (to/cc/bcc) are optional; omit them (or set omitRecipients) to create a recipient-less draft as an accidental-send guard. For replies, prefer replyToThreadId (anchors to the thread\'s latest message) or replyToMessageId (a specific message) — don\'t pass a thread id into replyToMessageId, which mis-threads silently.',
|
|
480
451
|
inputSchema: draftWriteSchema,
|
|
481
|
-
}, async ({ account, returnFull,
|
|
482
|
-
const resolved = await resolveReplyTarget(flags.replyToMessageId, replyToThreadId, account);
|
|
483
|
-
if ('error' in resolved) return resolved.error;
|
|
452
|
+
}, async ({ account, returnFull, ...flags }) => {
|
|
484
453
|
const args = ['gmail', 'drafts', 'create'];
|
|
485
|
-
appendDraftFlags(args,
|
|
454
|
+
appendDraftFlags(args, flags);
|
|
486
455
|
return writeDraft(args, account, returnFull);
|
|
487
456
|
});
|
|
488
457
|
|
|
489
458
|
server.registerTool('gog_gmail_drafts_update', {
|
|
490
|
-
description: 'Update an existing Gmail draft. For replies, prefer replyToThreadId (
|
|
459
|
+
description: 'Update an existing Gmail draft. For replies, prefer replyToThreadId (threads off the thread\'s latest message) or replyToMessageId (a specific message) over passing a thread id into replyToMessageId. Attachment semantics: supplying attach REPLACES the draft\'s existing attachments; omitting it preserves them; set clearAttachments to remove all.',
|
|
491
460
|
annotations: { destructiveHint: true },
|
|
492
461
|
inputSchema: {
|
|
493
462
|
draftId: z.string().describe('Draft ID'),
|
|
494
463
|
...draftWriteSchema,
|
|
464
|
+
clearAttachments: z.boolean().optional().describe('Remove all attachments from the draft. By default, omitting attach preserves the draft\'s existing attachments; this intentionally clears them. Ignored if attach is also supplied (attach replaces).'),
|
|
495
465
|
},
|
|
496
|
-
}, async ({ draftId, account, returnFull,
|
|
497
|
-
const resolved = await resolveReplyTarget(flags.replyToMessageId, replyToThreadId, account);
|
|
498
|
-
if ('error' in resolved) return resolved.error;
|
|
466
|
+
}, async ({ draftId, account, returnFull, clearAttachments, ...flags }) => {
|
|
499
467
|
const args = ['gmail', 'drafts', 'update', draftId];
|
|
500
|
-
appendDraftFlags(args,
|
|
468
|
+
appendDraftFlags(args, flags);
|
|
469
|
+
if (clearAttachments) args.push('--clear-attachments');
|
|
501
470
|
return writeDraft(args, account, returnFull, draftId);
|
|
502
471
|
});
|
|
503
472
|
|
|
@@ -545,84 +545,39 @@ describe('gog_gmail_drafts_create', () => {
|
|
|
545
545
|
});
|
|
546
546
|
});
|
|
547
547
|
|
|
548
|
-
describe('gmail draft reply threading (
|
|
549
|
-
|
|
550
|
-
toText(JSON.stringify({ thread: { messages: ids.map((id) => (id ? { id } : {})) } }));
|
|
551
|
-
|
|
552
|
-
it('resolves replyToThreadId to the thread\'s latest message id on create', async () => {
|
|
553
|
-
vi.mocked(lib.runOrDiagnose).mockImplementation(async (args: string[]) => {
|
|
554
|
-
if (args[1] === 'thread' && args[2] === 'get') return threadJson(['m1', 'm2', 'mLatest']);
|
|
555
|
-
return toText('{"draftId":"d1"}');
|
|
556
|
-
});
|
|
548
|
+
describe('gmail draft reply threading (native --thread-id)', () => {
|
|
549
|
+
it('passes replyToThreadId straight through as --thread-id on create (no thread fetch)', async () => {
|
|
557
550
|
await handlers.get('gog_gmail_drafts_create')!({
|
|
558
551
|
subject: 'Re: roof', body: 'Sounds good', replyToThreadId: '19dffe06f9668b28', account: 'me@x.com',
|
|
559
552
|
});
|
|
560
|
-
|
|
553
|
+
// gog resolves the thread's latest-message headers itself — no extra fetch.
|
|
554
|
+
expect(lib.runOrDiagnose).toHaveBeenCalledTimes(1);
|
|
561
555
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
562
|
-
['gmail', 'drafts', 'create', '--subject=Re: roof', '--body=Sounds good', '--
|
|
556
|
+
['gmail', 'drafts', 'create', '--subject=Re: roof', '--body=Sounds good', '--thread-id=19dffe06f9668b28'],
|
|
563
557
|
{ account: 'me@x.com' },
|
|
564
558
|
);
|
|
565
559
|
});
|
|
566
560
|
|
|
567
|
-
it('
|
|
568
|
-
vi.mocked(lib.runOrDiagnose).mockImplementation(async (args: string[]) => {
|
|
569
|
-
if (args[1] === 'thread' && args[2] === 'get') return threadJson(['a', 'b']);
|
|
570
|
-
return toText('{"draftId":"d1"}');
|
|
571
|
-
});
|
|
561
|
+
it('passes replyToThreadId as --thread-id on update', async () => {
|
|
572
562
|
await handlers.get('gog_gmail_drafts_update')!({
|
|
573
563
|
draftId: 'd1', subject: 'S', body: 'B', replyToThreadId: 't1',
|
|
574
564
|
});
|
|
575
565
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
576
|
-
['gmail', 'drafts', 'update', 'd1', '--subject=S', '--body=B', '--
|
|
566
|
+
['gmail', 'drafts', 'update', 'd1', '--subject=S', '--body=B', '--thread-id=t1'],
|
|
577
567
|
{ account: undefined },
|
|
578
568
|
);
|
|
579
569
|
});
|
|
580
570
|
|
|
581
|
-
it('replyToMessageId wins when both ids are supplied (no thread
|
|
571
|
+
it('replyToMessageId wins when both ids are supplied (no --thread-id)', async () => {
|
|
582
572
|
await handlers.get('gog_gmail_drafts_create')!({
|
|
583
573
|
subject: 'S', body: 'B', replyToMessageId: 'mExplicit', replyToThreadId: 't1',
|
|
584
574
|
});
|
|
585
|
-
// Only the create call — the thread was never fetched.
|
|
586
575
|
expect(lib.runOrDiagnose).toHaveBeenCalledTimes(1);
|
|
587
576
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
588
577
|
['gmail', 'drafts', 'create', '--subject=S', '--body=B', '--reply-to-message-id=mExplicit'],
|
|
589
578
|
{ account: undefined },
|
|
590
579
|
);
|
|
591
580
|
});
|
|
592
|
-
|
|
593
|
-
it('surfaces the thread-fetch error and does not write a mis-threaded draft', async () => {
|
|
594
|
-
vi.mocked(lib.runOrDiagnose).mockResolvedValueOnce(toText('Error: thread t1 not found'));
|
|
595
|
-
const result = await handlers.get('gog_gmail_drafts_create')!({
|
|
596
|
-
subject: 'S', body: 'B', replyToThreadId: 't1',
|
|
597
|
-
});
|
|
598
|
-
expect(result.content[0].text).toBe('Error: thread t1 not found');
|
|
599
|
-
expect(lib.runOrDiagnose).toHaveBeenCalledTimes(1); // only the thread fetch; no create
|
|
600
|
-
});
|
|
601
|
-
|
|
602
|
-
it('errors when the thread has no message to reply to', async () => {
|
|
603
|
-
vi.mocked(lib.runOrDiagnose).mockResolvedValueOnce(toText(JSON.stringify({ thread: { messages: [] } })));
|
|
604
|
-
const result = await handlers.get('gog_gmail_drafts_create')!({
|
|
605
|
-
subject: 'S', body: 'B', replyToThreadId: 't1',
|
|
606
|
-
});
|
|
607
|
-
expect(result.content[0].text).toBe('Error: thread t1 has no message to reply to');
|
|
608
|
-
expect(lib.runOrDiagnose).toHaveBeenCalledTimes(1);
|
|
609
|
-
});
|
|
610
|
-
|
|
611
|
-
it('errors when the thread payload has no messages array', async () => {
|
|
612
|
-
vi.mocked(lib.runOrDiagnose).mockResolvedValueOnce(toText('{}'));
|
|
613
|
-
const result = await handlers.get('gog_gmail_drafts_create')!({
|
|
614
|
-
subject: 'S', body: 'B', replyToThreadId: 't1',
|
|
615
|
-
});
|
|
616
|
-
expect(result.content[0].text).toBe('Error: thread t1 has no message to reply to');
|
|
617
|
-
});
|
|
618
|
-
|
|
619
|
-
it('errors when the latest message has no id', async () => {
|
|
620
|
-
vi.mocked(lib.runOrDiagnose).mockResolvedValueOnce(threadJson(['m1', undefined]));
|
|
621
|
-
const result = await handlers.get('gog_gmail_drafts_create')!({
|
|
622
|
-
subject: 'S', body: 'B', replyToThreadId: 't1',
|
|
623
|
-
});
|
|
624
|
-
expect(result.content[0].text).toBe('Error: thread t1 has no message to reply to');
|
|
625
|
-
});
|
|
626
581
|
});
|
|
627
582
|
|
|
628
583
|
describe('gog_gmail_drafts_update', () => {
|
|
@@ -661,6 +616,16 @@ describe('gog_gmail_drafts_update', () => {
|
|
|
661
616
|
);
|
|
662
617
|
});
|
|
663
618
|
|
|
619
|
+
it('passes --clear-attachments when clearAttachments is true', async () => {
|
|
620
|
+
await handlers.get('gog_gmail_drafts_update')!({
|
|
621
|
+
draftId: 'd1', subject: 'S', body: 'B', clearAttachments: true,
|
|
622
|
+
});
|
|
623
|
+
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
624
|
+
['gmail', 'drafts', 'update', 'd1', '--subject=S', '--body=B', '--clear-attachments'],
|
|
625
|
+
{ account: undefined },
|
|
626
|
+
);
|
|
627
|
+
});
|
|
628
|
+
|
|
664
629
|
it('returnFull re-fetches the draft by its known id', async () => {
|
|
665
630
|
vi.mocked(lib.runOrDiagnose)
|
|
666
631
|
.mockResolvedValueOnce(toText('{"draftId":"d1"}'))
|