diff --git a/src/tools/create-tools.test.ts b/src/tools/create-tools.test.ts index 67a75c3..6206121 100644 --- a/src/tools/create-tools.test.ts +++ b/src/tools/create-tools.test.ts @@ -878,6 +878,55 @@ describe("createGmailTools", () => { expect(await headersFor(["SENT"])).toContain("To: ada@example.com"); }); + test("reply drafts fall back to the sender when Reply-To has no address", async () => { + let raw = ""; + const tools = createGmailTools({ + capabilities: testCapabilities(async (input, init) => { + if (new URL(String(input)).pathname.endsWith("/messages/message-1")) { + return new Response( + JSON.stringify({ + id: "message-1", + labelIds: ["INBOX"], + payload: { + mimeType: "text/plain", + headers: [ + { name: "From", value: "Sender " }, + { name: "Reply-To", value: "undisclosed-recipients:;" }, + { name: "Subject", value: "Hello" }, + ], + body: { data: encodeBase64Url("Original") }, + }, + }), + ); + } + raw = draftRawFromRequest(init); + return new Response(JSON.stringify({ id: "draft-1" })); + }), + }); + await tools.run( + { + id: "call-reply", + name: "gmail_create_draft", + arguments: { replyToMessageId: "message-1", body: "Thanks" }, + }, + new AbortController().signal, + ); + await tools.dispose(); + expect(Buffer.from(raw, "base64url").toString()).toContain( + "To: sender@example.com\r\n", + ); + }); + + test("trims reply subjects before checking the Re: prefix", () => { + const raw = createRawDraft( + { body: "Reply" }, + { messageId: "", subject: " Re: Original " }, + ); + expect(Buffer.from(raw, "base64url").toString()).toContain( + "Subject: Re: Original\r\n", + ); + }); + test("keeps an existing Re: prefix on reply subjects", () => { const raw = createRawDraft( { body: "Reply" }, diff --git a/src/tools/create-tools.ts b/src/tools/create-tools.ts index 11cad44..37f871b 100644 --- a/src/tools/create-tools.ts +++ b/src/tools/create-tools.ts @@ -160,12 +160,20 @@ function inputRecipients( normalized: ReturnType, replyTo: string | undefined, ): readonly string[] | undefined { - const addresses = original.labelIds?.includes("SENT") - ? normalized.toRecipients?.slice(0, 1) - : (recipients(replyTo) ?? [normalized.sender]); - const result = (addresses ?? []).flatMap((address) => { - const recipient = plainEmailAddress(address); - return recipient === undefined || recipient.length === 0 ? [] : [recipient]; + if (original.labelIds?.includes("SENT")) { + return emailAddresses(normalized.toRecipients?.slice(0, 1)); + } + return ( + emailAddresses(recipients(replyTo)) ?? emailAddresses([normalized.sender]) + ); +} + +function emailAddresses( + values: readonly (string | undefined)[] | undefined, +): readonly string[] | undefined { + const result = (values ?? []).flatMap((value) => { + const address = plainEmailAddress(value); + return address?.includes("@") ? [address] : []; }); return result.length === 0 ? undefined : result; } diff --git a/src/tools/drafts.ts b/src/tools/drafts.ts index 71528a3..b7693a0 100644 --- a/src/tools/drafts.ts +++ b/src/tools/drafts.ts @@ -35,7 +35,8 @@ function recipientHeader( } function replySubject(subject: string): string { - return /^re:/i.test(subject) ? subject : `Re: ${subject}`; + const trimmed = subject.trim(); + return /^re:/i.test(trimmed) ? trimmed : `Re: ${trimmed}`; } // RFC 2047 caps an encoded word at 75 characters; 45 bytes base64 to 60.