Safe MIME

Bounded MIME parser substrate for the mail stack. Walks RFC 5322 + 2045 / 2046 / 2047 / 6532 (EAI) / 6533 (i18n-DSN) message structure into a part tree with caps on every dimension an attacker can grow to DoS the framework.

Foundation for everything above:

- b.mailStore.appendMessage parses inbound bytes via b.safeMime.parse(...) to extract headers + body parts before sealing per-column. - b.mail.server.mx runs every received message through b.safeMime.parse before SPF / DKIM / DMARC / ARC verification. - b.guardEmail.validateMessage already operates on raw bytes at the line-shape level; b.safeMime.parse is the structured follow-up that lets b.guardHtml / b.guardArchive / b.guardSvg inspect individual MIME parts. - b.mail.crypto.{pgp,smime} (v0.9.34a) parses signed/encrypted containers via this primitive before reaching the underlying crypto.

Defends CVE-2024-39929 (Exim MIME multipart parser) and CVE-2026-26312 (Stalwart nested message/rfc822 MIME OOM) by capping total parts, nesting depth, boundary length, header bytes, header-line bytes, decoded body bytes, message bytes — plus charset + transfer-encoding allowlists.

Throws SafeMimeError on every cap exceeded, malformed boundary, unknown charset, unknown transfer-encoding, NUL byte in headers, bidi/control chars in header values.

The parser is purely functional — no I/O, no async, no side effects. Operators run it in b.workerPool workers for any incoming message above a threshold.

b.safeMime.parse(bytes, opts?) #

stable0.9.19
{
  maxParts:                 number,     // default 64
  maxNestingDepth:          number,     // default 16
  maxBoundary:              number,     // default 70 (RFC 2046 §5.1.1)
  maxHeaderBytes:           number,     // default 64 KiB
  maxHeaderLineBytes:       number,     // default 998 (RFC 5322 §2.1.1)
  maxHeaderCount:           number,     // default 512 (DoS bound)
  maxBodyBytes:             number,     // default 25 MiB
  maxMessageBytes:          number,     // default 50 MiB
  charsetAllowlist:         string[],   // default UTF-8 / US-ASCII / common legacy 8-bit
  transferEncodingAllowlist: string[],  // default 7bit/8bit/quoted-printable/base64 (binary is opt-in, RFC 3030 BINARYMIME)
}

Parse bytes into a MIME part tree. Returns { headers, parts, leaf, decoded }. Multipart parts have non-null parts; leaf parts have non-null leaf carrying decoded body.

Throws SafeMimeError with codes: safe-mime/oversize-message / oversize-part-count / oversize-nesting / oversize-boundary / oversize-headers / oversize-header-line / oversize-body / unknown-charset / unknown-transfer-encoding / malformed-boundary / too-many-headers / malformed-headers / control-char-in-header / bad-input.

var msg = b.safeMime.parse(messageBuffer);
msg.headers.get("subject");
msg.parts.length;
msg.parts[0].leaf.body.toString("utf8");

b.safeMime.walk(tree, visitor) #

stable0.9.19

Depth-first walk. Invokes visitor(part, path) for every part where path is the position array ([] for root, [0] for first child). Visitor returning false short-circuits.

b.safeMime.walk(tree, function (part) {
  if (part.leaf && part.leaf.contentType === "application/pdf") {
    console.log("pdf", part.leaf.body.length);
  }
});

b.safeMime.findFirst(tree, predicate) #

stable0.9.19

Return the first part for which predicate(part) is truthy, or null. Common use: pull the first text/plain or text/html.

var t = b.safeMime.findFirst(tree, function (p) {
  return p.leaf && p.leaf.contentType === "text/plain";
});

b.safeMime.extractText(tree, opts?) #

stable0.9.19
{
  prefer:  "plain" | "html" | "any",   // default "plain"
}

Pull the rendering-preferred text payload. Honors RFC 2046 §5.1.4 "last wins" semantics for multipart/alternative. Returns { contentType, charset, body } (body is decoded string) or null.

var tree = b.safeMime.parse(messageBuffer);
var text = b.safeMime.extractText(tree, { prefer: "plain" });
text.body;          // → "Hello, world!"
text.contentType;   // → "text/plain"

b.safeMime.extractAttachments(tree, opts?) #

stable0.9.19
{
  includeInline: boolean,    // default false — Content-Disposition: inline skipped
}

Return array of attachment-shaped parts. Each entry is { filename, contentType, body, headers }. Operators pipe each attachment through b.fileType.detect then through the per-type guard (b.guardArchive / b.guardPdf / etc.).

var tree = b.safeMime.parse(messageBuffer);
var atts = b.safeMime.extractAttachments(tree);
atts[0].filename;       // → "report.pdf"
atts[0].contentType;    // → "application/pdf"
atts[0].body.length;    // → 12345 (decoded bytes)

Last updated 2026-08-08T16:39:49.652Z by seeder.