← Public packages
@kentcdodds/google
Google helpers for Gmail, Calendar, Drive, People, YouTube (including upload), and Analytics.
src/gmail-mime.test.ts
355 lines · 11.2 KB · TypeScriptimport assert from 'node:assert/strict'
import { describe, it } from 'node:test'
import {
applyMimeHeaderUpdates,
assembleComposedMime,
assertComposedReplyThreading,
assertNotSelfOnlyTo,
decodeBase64Latin1_DO_NOT_USE_FOR_MIME,
decodeBase64ToBytes,
decodeUtf8Base64,
decodeUtf8Base64Url,
defaultReplyToHeader,
encodeBytesToBase64,
encodeBytesToBase64Url,
encodeHeaderValue,
encodeUtf8Base64,
excludeMailboxAddresses,
extractEmails,
formatAddressHeader,
isOnlyMailbox,
requireRealSubject,
splitMimeRaw,
toBase64Url,
} from './gmail-mime.ts'
describe('formatAddressHeader', () => {
it('returns null for empty values', () => {
assert.equal(formatAddressHeader(undefined), null)
assert.equal(formatAddressHeader([]), null)
assert.equal(formatAddressHeader(['', ' ']), null)
})
it('keeps a single already-formatted address', () => {
assert.equal(formatAddressHeader('sponsor-kcd@10xn.dev'), 'sponsor-kcd@10xn.dev')
assert.equal(
formatAddressHeader('"Kent C. Dodds" <me@kentcdodds.com>'),
'"Kent C. Dodds" <me@kentcdodds.com>',
)
})
it('joins an address list with commas', () => {
assert.equal(
formatAddressHeader(['sponsor-kcd@10xn.dev', 'Pierre Burgy <pierre@strapi.com>']),
'sponsor-kcd@10xn.dev, Pierre Burgy <pierre@strapi.com>',
)
})
it('RFC 2047 encodes a non-ASCII display name', () => {
assert.equal(
formatAddressHeader('Pierre Burgy 🥐 <pierre@strapi.com>'),
'=?UTF-8?B?' + encodeUtf8Base64('Pierre Burgy 🥐') + '?= <pierre@strapi.com>',
)
})
})
describe('assembleComposedMime', () => {
it('includes To, Cc, and Bcc headers', () => {
const assembled = assembleComposedMime({
from: '"Kent C. Dodds" <me@kentcdodds.com>',
to: 'Verity <influencercontact@corp.stardesk.net>',
cc: ['sponsor-kcd@10xn.dev'],
bcc: 'me+log@kentcdodds.com',
subject: 'Re: Partnership Opportunity X StarDesk',
body: ['Hey Verity,', 'I have a team that will take it from here.', '- Kent'],
})
assert.match(assembled.mime, /^From: "Kent C\. Dodds" <me@kentcdodds\.com>/m)
assert.match(assembled.mime, /^To: Verity <influencercontact@corp\.stardesk\.net>/m)
assert.match(assembled.mime, /^Cc: sponsor-kcd@10xn\.dev/m)
assert.match(assembled.mime, /^Bcc: me\+log@kentcdodds\.com/m)
assert.match(assembled.mime, /^Subject: Re: Partnership Opportunity X StarDesk/m)
assert.equal(assembled.cc, 'sponsor-kcd@10xn.dev')
assert.equal(assembled.bcc, 'me+log@kentcdodds.com')
assert.match(assembled.text, /I have a team that will take it from here\./)
})
it('omits missing Cc and Bcc headers', () => {
const assembled = assembleComposedMime({
from: 'me@kentcdodds.com',
to: 'pierre@strapi.com',
subject: 'Hello',
body: 'Hi',
})
assert.doesNotMatch(assembled.mime, /^Cc:/m)
assert.doesNotMatch(assembled.mime, /^Bcc:/m)
})
it('rejects an empty body', () => {
assert.throws(
() =>
assembleComposedMime({
from: 'me@kentcdodds.com',
to: 'pierre@strapi.com',
subject: 'Hello',
body: ['', ' '],
}),
/non-empty body/,
)
})
})
describe('encodeHeaderValue', () => {
it('leaves ASCII headers alone', () => {
assert.equal(encodeHeaderValue('Re: Epic Web / Strapi'), 'Re: Epic Web / Strapi')
})
})
describe('self-send helpers', () => {
const mailbox = 'me@kentcdodds.com'
it('extractEmails finds bare addresses in display-name headers', () => {
assert.deepEqual(extractEmails('"Kent C. Dodds" <me@kentcdodds.com>'), ['me@kentcdodds.com'])
assert.deepEqual(
extractEmails('A <a@example.com>, B <b@example.com>'),
['a@example.com', 'b@example.com'],
)
})
it('isOnlyMailbox is true when every address is the mailbox', () => {
assert.equal(isOnlyMailbox('"Kent" <me@kentcdodds.com>', mailbox), true)
assert.equal(isOnlyMailbox('jaimie@example.com', mailbox), false)
})
it('excludeMailboxAddresses keeps other recipients', () => {
assert.equal(
excludeMailboxAddresses(
'"Kent" <me@kentcdodds.com>, Jaimie <jaimie@example.com>',
mailbox,
),
'Jaimie <jaimie@example.com>',
)
})
it('defaultReplyToHeader uses From for inbound mail', () => {
assert.equal(
defaultReplyToHeader({
mailboxEmail: mailbox,
from: 'Jaimie <jaimie@example.com>',
to: '"Kent" <me@kentcdodds.com>',
}),
'Jaimie <jaimie@example.com>',
)
})
it('defaultReplyToHeader uses original To for own outbound mail', () => {
assert.equal(
defaultReplyToHeader({
mailboxEmail: mailbox,
from: '"Kent C. Dodds" <me@kentcdodds.com>',
to: 'Jaimie <jaimie@example.com>',
}),
'Jaimie <jaimie@example.com>',
)
})
it('assertNotSelfOnlyTo refuses self-only To', () => {
assert.throws(
() =>
assertNotSelfOnlyTo({
to: 'me@kentcdodds.com',
mailboxEmail: mailbox,
}),
/self-send trap/,
)
assert.doesNotThrow(() =>
assertNotSelfOnlyTo({
to: 'me@kentcdodds.com',
mailboxEmail: mailbox,
allowSelfSend: true,
}),
)
})
})
describe('requireRealSubject', () => {
it('accepts a real subject', () => {
assert.equal(requireRealSubject('Re: Hello'), 'Re: Hello')
assert.equal(requireRealSubject(' Hi '), 'Hi')
})
it('rejects missing, empty, and coerced subjects', () => {
assert.throws(() => requireRealSubject(undefined), /real subject string/)
assert.throws(() => requireRealSubject(null), /real subject string/)
assert.throws(() => requireRealSubject(''), /non-empty subject/)
assert.throws(() => requireRealSubject(' '), /non-empty subject/)
assert.throws(() => requireRealSubject('undefined'), /literal string "undefined"/)
assert.throws(() => requireRealSubject('null'), /literal string "undefined"/)
})
})
describe('assembleComposedMime subject guard', () => {
it('refuses undefined subject instead of serializing Subject: undefined', () => {
assert.throws(
() =>
assembleComposedMime({
from: 'me@kentcdodds.com',
to: 'pierre@strapi.com',
subject: undefined as unknown as string,
body: 'Hi',
}),
/real subject string/,
)
})
})
describe('assertComposedReplyThreading', () => {
it('allows new messages with no threading fields', () => {
assert.deepEqual(assertComposedReplyThreading({}), {})
})
it('requires threadId and inReplyTo together', () => {
assert.throws(
() => assertComposedReplyThreading({ threadId: '1a06fde84f207bc6' }),
/both threadId and inReplyTo/,
)
assert.throws(
() => assertComposedReplyThreading({ inReplyTo: '<abc@mail.gmail.com>' }),
/both threadId and inReplyTo/,
)
})
it('defaults references from inReplyTo when omitted', () => {
assert.deepEqual(
assertComposedReplyThreading({
threadId: '1a06fde84f207bc6',
inReplyTo: '<abc@mail.gmail.com>',
}),
{
threadId: '1a06fde84f207bc6',
inReplyTo: '<abc@mail.gmail.com>',
references: '<abc@mail.gmail.com>',
},
)
})
it('rejects literal undefined/null tokens', () => {
assert.throws(
() =>
assertComposedReplyThreading({
threadId: 'undefined',
inReplyTo: '<abc@mail.gmail.com>',
}),
/threadId must not be the literal/,
)
})
})
describe('UTF-8 base64 round-trip (en-dash pit of success)', () => {
it('round-trips U+2013 en-dash through encodeUtf8Base64 / decodeUtf8Base64', () => {
const original = 'Pricing \u2013 early bird ends Friday' // en-dash
const encoded = encodeUtf8Base64(original)
const decoded = decodeUtf8Base64(encoded)
assert.equal(decoded, original)
// UTF-8 bytes for en-dash are E2 80 93 — must survive
const bytes = decodeBase64ToBytes(encoded)
const utf8 = new TextEncoder().encode(original)
const idx = utf8.indexOf(0xe2)
assert.equal(bytes[idx], 0xe2)
assert.equal(bytes[idx + 1], 0x80)
assert.equal(bytes[idx + 2], 0x93) // U+2013 en-dash
})
it('round-trips en-dash through Base64URL helpers used for Gmail raw', () => {
const original = 'Bullets with \u2013 and \u2014 dashes'
const encoded = toBase64Url(original)
assert.equal(decodeUtf8Base64Url(encoded), original)
assert.equal(new TextDecoder().decode(decodeBase64ToBytes(encoded)), original)
})
it('documents the bare-atob mojibake failure mode', () => {
const original = 'en-dash: \u2013'
const utf8Bytes = new TextEncoder().encode(original)
const b64 = encodeBytesToBase64(utf8Bytes)
// Bare atob → Latin-1 string; re-encoding that as UTF-8 is the incident bug
const latin1 = atob(b64)
const mojibake = new TextEncoder().encode(latin1)
const mojibakeText = new TextDecoder().decode(mojibake)
assert.notEqual(mojibakeText, original)
assert.match(mojibakeText, /â/)
// Safe path stays clean
assert.equal(decodeUtf8Base64(b64), original)
})
it('decodeBase64Latin1_DO_NOT_USE_FOR_MIME always throws', () => {
assert.throws(() => decodeBase64Latin1_DO_NOT_USE_FOR_MIME('YQ=='), /bare atob/)
})
})
describe('header-only MIME update leaves body bytes unchanged', () => {
it('adds Bcc without altering body bytes containing UTF-8 en-dashes', () => {
const bodyText =
'Content-Type: text/plain; charset="UTF-8"\r\n' +
'Content-Transfer-Encoding: base64\r\n\r\n' +
encodeUtf8Base64('Launch \u2014 now\n\u2022 early bird \u2013 ends soon\n')
const bodyBytes = new TextEncoder().encode(bodyText)
const headers =
'From: "Kent C. Dodds" <me@kentcdodds.com>\r\n' +
'To: list@example.com\r\n' +
'Subject: Pricing announcement'
const raw = new Uint8Array([
...new TextEncoder().encode(headers),
0x0d, 0x0a, 0x0d, 0x0a,
...bodyBytes,
])
const before = splitMimeRaw(raw)
assert.deepEqual(Array.from(before.bodyBytes), Array.from(bodyBytes))
const patched = applyMimeHeaderUpdates(raw, { bcc: 'me+log@kentcdodds.com' })
const after = splitMimeRaw(patched)
assert.deepEqual(Array.from(after.bodyBytes), Array.from(bodyBytes))
assert.match(after.headersText, /^Bcc: me\+log@kentcdodds\.com$/m)
assert.match(after.headersText, /^To: list@example\.com$/m)
// Body still decodes to en-dashes when read as UTF-8 base64 part
assert.ok(after.bodyBytes.every((b, i) => b === bodyBytes[i]))
})
it('replaces existing Bcc and can remove Cc with null', () => {
const bodyBytes = new Uint8Array([0xe2, 0x80, 0x93]) // raw en-dash bytes in body
const headers =
'From: me@kentcdodds.com\r\n' +
'To: a@example.com\r\n' +
'Cc: old-cc@example.com\r\n' +
'Bcc: old-bcc@example.com\r\n' +
'Subject: Hi'
const raw = new Uint8Array([
...new TextEncoder().encode(headers),
0x0d, 0x0a, 0x0d, 0x0a,
...bodyBytes,
])
const patched = applyMimeHeaderUpdates(raw, {
bcc: 'new-bcc@example.com',
cc: null,
})
const after = splitMimeRaw(patched)
assert.deepEqual(Array.from(after.bodyBytes), Array.from(bodyBytes))
assert.match(after.headersText, /^Bcc: new-bcc@example\.com$/m)
assert.doesNotMatch(after.headersText, /^Cc:/m)
assert.doesNotMatch(after.headersText, /old-bcc/)
})
it('preserves body across Base64URL encode/decode used by draft updates', () => {
const bodyBytes = new Uint8Array([0xe2, 0x80, 0x93, 0xe2, 0x80, 0x94, 0x0a])
const headers = 'From: me@kentcdodds.com\r\nTo: a@example.com\r\nSubject: X'
const raw = new Uint8Array([
...new TextEncoder().encode(headers),
0x0d, 0x0a, 0x0d, 0x0a,
...bodyBytes,
])
const patched = applyMimeHeaderUpdates(raw, { bcc: 'bcc@example.com' })
const wire = encodeBytesToBase64Url(patched)
const roundTrip = decodeBase64ToBytes(wire)
const after = splitMimeRaw(roundTrip)
assert.deepEqual(Array.from(after.bodyBytes), Array.from(bodyBytes))
})
})