Skip to content

fix(drivers): a non-ASCII subject becomes one unfoldable encoded-word, producing header lines over RFC 5322's 998-octet limit #121

Description

@productdevbook

What happens

encodeHeaderValue() turns a non-ASCII header value into a single RFC 2047 encoded-word:

return `=?utf-8?B?${stringToBase64(value)}?=`

RFC 2047 §2 caps an encoded-word at 75 characters, and RFC 5322 §2.1.1 caps a line at 998 octets. Neither is enforced. foldHeader() cannot help: it folds on spaces, and base64 has none — so the whole thing is emitted as one unbroken line, however long the subject is.

Reproduction

import { buildMime } from "../src/drivers/_mime.ts"

const mime = (subject: string) => buildMime({
  from: { email: "hi@acme.com", name: "Acme" }, to: [{ email: "a@x.com" }],
  cc: [], bcc: [], replyTo: [], subject, text: "hello",
  messageId: "<1@acme.com>", date: new Date(0),
})

const a = mime("Willkommen bei Acme — Ihre Bestellung über Straßenbahnfahrscheine ist unterwegs")
const line = a.body.split("\r\n").find((l) => l.startsWith("Subject:"))!
console.log("subject header line length:", line.length)
console.log(line)
console.log("encoded-word length:", /=\?utf-8\?B\?[^?]*\?=/.exec(line)![0].length, "(RFC 2047 max 75)")

const b = mime("Zusammenfassung Ihrer Bestellung — ".repeat(40))
console.log("longest header line:", Math.max(...b.body.split("\r\n").map((l) => l.length)), "(RFC 5322 hard cap 998)")

Observed output:

subject header line length: 133
Subject: =?utf-8?B?V2lsbGtvbW1lbiBiZWkgQWNtZSDigJQgSWhyZSBCZXN0ZWxsdW5nIMO8YmVyIFN0cmHDn2VuYmFobmZhaHJzY2hlaW5lIGlzdCB1bnRlcndlZ3M=?=
encoded-word length: 124 (RFC 2047 max 75)
longest header line: 1997 (RFC 5322 hard cap 998)

Why it matters

This is the SMTP and SES path (_mime.buildMime feeds both smtp.send and ses.toPayload's Content.Raw), and a non-ASCII subject is the normal case for any non-English sender.

A 124-character encoded-word is over the RFC 2047 limit but is tolerated by most clients. A 1997-octet header line is over RFC 5322's hard limit and is not: an MTA is entitled to reject the message or to fold it itself at an arbitrary point, and a fold inserted in the middle of a base64 encoded-word makes the subject undecodable — the recipient sees the raw =?utf-8?B?… text. Because it happens after DKIM signing, a header rewritten in transit also invalidates the signature.

The same function encodes attachment filenames (_mime.ts:185), so a long non-ASCII filename has the same problem.

Where

  • src/drivers/_mime.ts:226-229 — encodeHeaderValue()
  • src/drivers/_mime.ts:208-222 — foldHeader(), which cannot split a space-free value
  • callers: src/drivers/_mime.ts:81 (Subject), :185 (attachment filename)

Suggested fix

Encode in chunks: split the input into runs whose base64 form keeps each encoded-word at or under 75 characters (cut on whole UTF-8 code points, never mid-sequence), emit them as separate encoded-words separated by CRLF + a single space, which RFC 2047 defines as the way to continue. foldHeader then has the whitespace it needs. Give foldHeader a hard 998-octet backstop for any value it still cannot split.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions