Skip to content

Commit 37d524b

Browse files
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>
1 parent 19ef526 commit 37d524b

File tree

2 files changed

+51
-2
lines changed

2 files changed

+51
-2
lines changed

apps/sim/tools/gmail/utils.test.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/**
2+
* @vitest-environment node
3+
*/
4+
import { describe, expect, it } from 'vitest'
5+
import { encodeRfc2047 } from './utils'
6+
7+
describe('encodeRfc2047', () => {
8+
it('returns ASCII text unchanged', () => {
9+
expect(encodeRfc2047('Simple ASCII Subject')).toBe('Simple ASCII Subject')
10+
})
11+
12+
it('returns empty string unchanged', () => {
13+
expect(encodeRfc2047('')).toBe('')
14+
})
15+
16+
it('encodes emojis as RFC 2047 base64', () => {
17+
const result = encodeRfc2047('Time to Stretch! 🧘')
18+
expect(result).toBe('=?UTF-8?B?VGltZSB0byBTdHJldGNoISDwn6eY?=')
19+
})
20+
21+
it('round-trips non-ASCII subjects correctly', () => {
22+
const subjects = ['Hello 世界', 'Café résumé', '🎉🎊🎈 Party!', '今週のミーティング']
23+
for (const subject of subjects) {
24+
const encoded = encodeRfc2047(subject)
25+
const match = encoded.match(/^=\?UTF-8\?B\?(.+)\?=$/)
26+
expect(match).not.toBeNull()
27+
const decoded = Buffer.from(match![1], 'base64').toString('utf-8')
28+
expect(decoded).toBe(subject)
29+
}
30+
})
31+
32+
it('does not double-encode already-encoded subjects', () => {
33+
const alreadyEncoded = '=?UTF-8?B?VGltZSB0byBTdHJldGNoISDwn6eY?='
34+
expect(encodeRfc2047(alreadyEncoded)).toBe(alreadyEncoded)
35+
})
36+
})

apps/sim/tools/gmail/utils.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,19 @@ function generateBoundary(): string {
294294
return `----=_Part_${Date.now()}_${Math.random().toString(36).substring(2, 15)}`
295295
}
296296

297+
/**
298+
* Encode a header value using RFC 2047 Base64 encoding if it contains non-ASCII characters.
299+
* This matches Google's own Gmail API sample: `=?utf-8?B?${Buffer.from(subject).toString('base64')}?=`
300+
* @see https://github.com/googleapis/google-api-nodejs-client/blob/main/samples/gmail/send.js
301+
*/
302+
export function encodeRfc2047(value: string): string {
303+
// eslint-disable-next-line no-control-regex
304+
if (/^[\x00-\x7F]*$/.test(value)) {
305+
return value
306+
}
307+
return `=?UTF-8?B?${Buffer.from(value, 'utf-8').toString('base64')}?=`
308+
}
309+
297310
/**
298311
* Encode string or buffer to base64url format (URL-safe base64)
299312
* Gmail API requires base64url encoding for the raw message field
@@ -333,7 +346,7 @@ export function buildSimpleEmailMessage(params: {
333346
emailHeaders.push(`Bcc: ${bcc}`)
334347
}
335348

336-
emailHeaders.push(`Subject: ${subject || ''}`)
349+
emailHeaders.push(`Subject: ${encodeRfc2047(subject || '')}`)
337350

338351
if (inReplyTo) {
339352
emailHeaders.push(`In-Reply-To: ${inReplyTo}`)
@@ -380,7 +393,7 @@ export function buildMimeMessage(params: BuildMimeMessageParams): string {
380393
if (bcc) {
381394
messageParts.push(`Bcc: ${bcc}`)
382395
}
383-
messageParts.push(`Subject: ${subject || ''}`)
396+
messageParts.push(`Subject: ${encodeRfc2047(subject || '')}`)
384397

385398
if (inReplyTo) {
386399
messageParts.push(`In-Reply-To: ${inReplyTo}`)

0 commit comments

Comments
 (0)