From 37d524bb0adce198d36ff589d0e330583e108b6d Mon Sep 17 00:00:00 2001 From: Waleed Date: Wed, 11 Mar 2026 15:48:07 -0700 Subject: [PATCH] fix(gmail): RFC 2047 encode subject headers for non-ASCII characters (#3526) * fix(gmail): RFC 2047 encode subject headers for non-ASCII characters * Fix RFC 2047 encoded word length limit Split long email subjects into multiple RFC 2047 encoded words to comply with the 75-character limit per RFC 2047 Section 2. Each encoded word now contains at most 45 bytes of UTF-8 content (producing max 60 chars of base64 + 12 chars overhead = 72 total). Multiple encoded words are separated by CRLF + space (folding whitespace). Applied via @cursor push command * fix(gmail): split RFC 2047 encoded words on character boundaries * fix(gmail): simplify RFC 2047 encoding to match Google's own sample --------- Co-authored-by: Cursor Agent --- apps/sim/tools/gmail/utils.test.ts | 36 ++++++++++++++++++++++++++++++ apps/sim/tools/gmail/utils.ts | 17 ++++++++++++-- 2 files changed, 51 insertions(+), 2 deletions(-) create mode 100644 apps/sim/tools/gmail/utils.test.ts diff --git a/apps/sim/tools/gmail/utils.test.ts b/apps/sim/tools/gmail/utils.test.ts new file mode 100644 index 0000000000..5c28ac9a13 --- /dev/null +++ b/apps/sim/tools/gmail/utils.test.ts @@ -0,0 +1,36 @@ +/** + * @vitest-environment node + */ +import { describe, expect, it } from 'vitest' +import { encodeRfc2047 } from './utils' + +describe('encodeRfc2047', () => { + it('returns ASCII text unchanged', () => { + expect(encodeRfc2047('Simple ASCII Subject')).toBe('Simple ASCII Subject') + }) + + it('returns empty string unchanged', () => { + expect(encodeRfc2047('')).toBe('') + }) + + it('encodes emojis as RFC 2047 base64', () => { + const result = encodeRfc2047('Time to Stretch! 🧘') + expect(result).toBe('=?UTF-8?B?VGltZSB0byBTdHJldGNoISDwn6eY?=') + }) + + it('round-trips non-ASCII subjects correctly', () => { + const subjects = ['Hello 世界', 'Café résumé', '🎉🎊🎈 Party!', '今週のミーティング'] + for (const subject of subjects) { + const encoded = encodeRfc2047(subject) + const match = encoded.match(/^=\?UTF-8\?B\?(.+)\?=$/) + expect(match).not.toBeNull() + const decoded = Buffer.from(match![1], 'base64').toString('utf-8') + expect(decoded).toBe(subject) + } + }) + + it('does not double-encode already-encoded subjects', () => { + const alreadyEncoded = '=?UTF-8?B?VGltZSB0byBTdHJldGNoISDwn6eY?=' + expect(encodeRfc2047(alreadyEncoded)).toBe(alreadyEncoded) + }) +}) diff --git a/apps/sim/tools/gmail/utils.ts b/apps/sim/tools/gmail/utils.ts index 4d856db1d5..f374a4e00f 100644 --- a/apps/sim/tools/gmail/utils.ts +++ b/apps/sim/tools/gmail/utils.ts @@ -294,6 +294,19 @@ function generateBoundary(): string { return `----=_Part_${Date.now()}_${Math.random().toString(36).substring(2, 15)}` } +/** + * Encode a header value using RFC 2047 Base64 encoding if it contains non-ASCII characters. + * This matches Google's own Gmail API sample: `=?utf-8?B?${Buffer.from(subject).toString('base64')}?=` + * @see https://github.com/googleapis/google-api-nodejs-client/blob/main/samples/gmail/send.js + */ +export function encodeRfc2047(value: string): string { + // eslint-disable-next-line no-control-regex + if (/^[\x00-\x7F]*$/.test(value)) { + return value + } + return `=?UTF-8?B?${Buffer.from(value, 'utf-8').toString('base64')}?=` +} + /** * Encode string or buffer to base64url format (URL-safe base64) * Gmail API requires base64url encoding for the raw message field @@ -333,7 +346,7 @@ export function buildSimpleEmailMessage(params: { emailHeaders.push(`Bcc: ${bcc}`) } - emailHeaders.push(`Subject: ${subject || ''}`) + emailHeaders.push(`Subject: ${encodeRfc2047(subject || '')}`) if (inReplyTo) { emailHeaders.push(`In-Reply-To: ${inReplyTo}`) @@ -380,7 +393,7 @@ export function buildMimeMessage(params: BuildMimeMessageParams): string { if (bcc) { messageParts.push(`Bcc: ${bcc}`) } - messageParts.push(`Subject: ${subject || ''}`) + messageParts.push(`Subject: ${encodeRfc2047(subject || '')}`) if (inReplyTo) { messageParts.push(`In-Reply-To: ${inReplyTo}`)