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 <cursoragent@cursor.com>
This commit is contained in:
Waleed
2026-03-11 15:48:07 -07:00
committed by GitHub
parent 19ef526886
commit 37d524bb0a
2 changed files with 51 additions and 2 deletions

View File

@@ -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)
})
})

View File

@@ -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}`)