Skip to content

Commit f6cd4d4

Browse files
fix: fall back to the sender when Reply-To has no address (#14)
1 parent e23fba0 commit f6cd4d4

3 files changed

Lines changed: 65 additions & 7 deletions

File tree

‎src/tools/create-tools.test.ts‎

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -878,6 +878,55 @@ describe("createGmailTools", () => {
878878
expect(await headersFor(["SENT"])).toContain("To: ada@example.com");
879879
});
880880

881+
test("reply drafts fall back to the sender when Reply-To has no address", async () => {
882+
let raw = "";
883+
const tools = createGmailTools({
884+
capabilities: testCapabilities(async (input, init) => {
885+
if (new URL(String(input)).pathname.endsWith("/messages/message-1")) {
886+
return new Response(
887+
JSON.stringify({
888+
id: "message-1",
889+
labelIds: ["INBOX"],
890+
payload: {
891+
mimeType: "text/plain",
892+
headers: [
893+
{ name: "From", value: "Sender <sender@example.com>" },
894+
{ name: "Reply-To", value: "undisclosed-recipients:;" },
895+
{ name: "Subject", value: "Hello" },
896+
],
897+
body: { data: encodeBase64Url("Original") },
898+
},
899+
}),
900+
);
901+
}
902+
raw = draftRawFromRequest(init);
903+
return new Response(JSON.stringify({ id: "draft-1" }));
904+
}),
905+
});
906+
await tools.run(
907+
{
908+
id: "call-reply",
909+
name: "gmail_create_draft",
910+
arguments: { replyToMessageId: "message-1", body: "Thanks" },
911+
},
912+
new AbortController().signal,
913+
);
914+
await tools.dispose();
915+
expect(Buffer.from(raw, "base64url").toString()).toContain(
916+
"To: sender@example.com\r\n",
917+
);
918+
});
919+
920+
test("trims reply subjects before checking the Re: prefix", () => {
921+
const raw = createRawDraft(
922+
{ body: "Reply" },
923+
{ messageId: "<message-1@example.com>", subject: " Re: Original " },
924+
);
925+
expect(Buffer.from(raw, "base64url").toString()).toContain(
926+
"Subject: Re: Original\r\n",
927+
);
928+
});
929+
881930
test("keeps an existing Re: prefix on reply subjects", () => {
882931
const raw = createRawDraft(
883932
{ body: "Reply" },

‎src/tools/create-tools.ts‎

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -160,12 +160,20 @@ function inputRecipients(
160160
normalized: ReturnType<typeof toToolMessage>,
161161
replyTo: string | undefined,
162162
): readonly string[] | undefined {
163-
const addresses = original.labelIds?.includes("SENT")
164-
? normalized.toRecipients?.slice(0, 1)
165-
: (recipients(replyTo) ?? [normalized.sender]);
166-
const result = (addresses ?? []).flatMap((address) => {
167-
const recipient = plainEmailAddress(address);
168-
return recipient === undefined || recipient.length === 0 ? [] : [recipient];
163+
if (original.labelIds?.includes("SENT")) {
164+
return emailAddresses(normalized.toRecipients?.slice(0, 1));
165+
}
166+
return (
167+
emailAddresses(recipients(replyTo)) ?? emailAddresses([normalized.sender])
168+
);
169+
}
170+
171+
function emailAddresses(
172+
values: readonly (string | undefined)[] | undefined,
173+
): readonly string[] | undefined {
174+
const result = (values ?? []).flatMap((value) => {
175+
const address = plainEmailAddress(value);
176+
return address?.includes("@") ? [address] : [];
169177
});
170178
return result.length === 0 ? undefined : result;
171179
}

‎src/tools/drafts.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@ function recipientHeader(
3535
}
3636

3737
function replySubject(subject: string): string {
38-
return /^re:/i.test(subject) ? subject : `Re: ${subject}`;
38+
const trimmed = subject.trim();
39+
return /^re:/i.test(trimmed) ? trimmed : `Re: ${trimmed}`;
3940
}
4041

4142
// RFC 2047 caps an encoded word at 75 characters; 45 bytes base64 to 60.

0 commit comments

Comments
 (0)