mirror of
https://github.com/simstudioai/sim.git
synced 2026-04-06 03:00:16 -04:00
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:
36
apps/sim/tools/gmail/utils.test.ts
Normal file
36
apps/sim/tools/gmail/utils.test.ts
Normal 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)
|
||||
})
|
||||
})
|
||||
@@ -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}`)
|
||||
|
||||
Reference in New Issue
Block a user