Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion packages/integrations/src/google.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,10 @@ function decodeSnippet(value: string): string {
entity[1].toLowerCase() === "x"
? Number.parseInt(entity.slice(2), 16)
: Number.parseInt(entity.slice(1), 10);
return code >= 0 && code <= 0x10ffff ? String.fromCodePoint(code) : original;
// Surrogates are not scalar values: fromCodePoint throws on them, and one

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reproduce the actual rejection before guarding surrogates

On the supported Node runtime, String.fromCodePoint(0xd800) returns an unpaired UTF-16 code unit; it does not throw (only nonintegral or out-of-range values do). Consequently, before this patch the fixture reaches Promise.all successfully and the new test fails only at its changed body assertion, so this guard/test cannot demonstrate or fix the claimed whole-inbox rejection. If that rejection is the production regression, reproduce the operation that actually throws (or reframe this as output normalization) so the real failure is not left unresolved.

Useful? React with 👍 / 👎.

// such reference in a snippet must not reject the whole message list.
const scalar = code >= 0 && code <= 0x10ffff && !(code >= 0xd800 && code <= 0xdfff);
return scalar ? String.fromCodePoint(code) : original;
},
);
}
Expand Down
13 changes: 13 additions & 0 deletions tests/google.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,19 @@ test("header decoding preserves spaces encoded inside words and next to plain te
assert.equal((await client.getThread("thread1"))[0].subject, "Re: Visit résumé notes");
});

test("a numeric character reference to a surrogate stays literal instead of failing the list", async () => {
// String.fromCodePoint throws on surrogate code points; a sender can put the
// literal text &#xD800; in a snippet, and one such message must not reject
// the whole list (Promise.all over per-message parses).
const client = clientWith((request) =>
new URL(request.url).pathname.endsWith("/messages")
? json({ messages: [{ id: "msg1" }] })
: json({ id: "msg1", threadId: "thread1", snippet: "Entity notes: &#xD800; and &#55296;" }),
);
const [mail] = await client.listMail();
assert.equal(mail.body, "Entity notes: &#xD800; and &#55296;");
});

test("a body charset TextDecoder does not know falls back to UTF-8 instead of failing the list", async () => {
const charsets: Record<string, string> = { known: "utf-8", unknown: "unknown-8bit" };
const bodies: Record<string, string> = { known: "Opening hours", unknown: "Café tickets" };
Expand Down
Loading