@blamejs/core 0.8.52 → 0.8.57

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/index.js +8 -0
  3. package/lib/audit.js +4 -0
  4. package/lib/auth/fido-mds3.js +624 -0
  5. package/lib/auth/passkey.js +214 -2
  6. package/lib/auth-bot-challenge.js +1 -1
  7. package/lib/credential-hash.js +2 -2
  8. package/lib/framework-error.js +55 -0
  9. package/lib/guard-cidr.js +2 -1
  10. package/lib/guard-jwt.js +2 -2
  11. package/lib/guard-oauth.js +2 -2
  12. package/lib/http-client-cache.js +916 -0
  13. package/lib/http-client.js +242 -0
  14. package/lib/mail-arf.js +343 -0
  15. package/lib/mail-auth.js +265 -40
  16. package/lib/mail-bimi.js +948 -33
  17. package/lib/mail-bounce.js +386 -4
  18. package/lib/mail-mdn.js +424 -0
  19. package/lib/mail-unsubscribe.js +265 -25
  20. package/lib/mail.js +403 -21
  21. package/lib/middleware/bearer-auth.js +1 -1
  22. package/lib/middleware/clear-site-data.js +122 -0
  23. package/lib/middleware/dpop.js +1 -1
  24. package/lib/middleware/index.js +9 -0
  25. package/lib/middleware/nel.js +214 -0
  26. package/lib/middleware/security-headers.js +56 -4
  27. package/lib/middleware/speculation-rules.js +323 -0
  28. package/lib/mime-parse.js +198 -0
  29. package/lib/network-dns.js +890 -27
  30. package/lib/network-tls.js +745 -0
  31. package/lib/object-store/sigv4.js +54 -0
  32. package/lib/public-suffix.js +414 -0
  33. package/lib/safe-buffer.js +7 -0
  34. package/lib/safe-json.js +1 -1
  35. package/lib/static.js +120 -0
  36. package/lib/storage.js +11 -0
  37. package/lib/vendor/MANIFEST.json +33 -0
  38. package/lib/vendor/bimi-trust-anchors.pem +33 -0
  39. package/lib/vendor/public-suffix-list.dat +16376 -0
  40. package/package.json +1 -1
  41. package/sbom.cyclonedx.json +6 -6
@@ -0,0 +1,323 @@
1
+ "use strict";
2
+ /**
3
+ * @module b.middleware.speculationRules
4
+ * @nav HTTP
5
+ * @title Speculation Rules
6
+ * @order 130
7
+ * @card W3C Speculation Rules emitter — declares which links the
8
+ * user-agent is allowed to prerender or prefetch ahead of
9
+ * user navigation. Default emit path is the
10
+ * `Speculation-Rules` response header pointing at a
11
+ * JSON-served rules document; an opt-in `inline: true`
12
+ * mode injects `<script type="speculationrules">` into
13
+ * text/html bodies.
14
+ *
15
+ * @intro
16
+ * Speculation Rules (W3C draft) are the modern replacement for
17
+ * `<link rel="prerender">` / `<link rel="prefetch">`. The browser
18
+ * reads a JSON document declaring patterns of links the operator
19
+ * wants speculatively loaded under varying eagerness levels
20
+ * (`immediate`, `eager`, `moderate`, `conservative`) and
21
+ * pre-fetches or pre-renders matching anchors as the user hovers /
22
+ * scrolls / dwells.
23
+ *
24
+ * Two emit modes:
25
+ *
26
+ * 1. `Speculation-Rules` response header — points at a JSON
27
+ * document the operator serves elsewhere (typical for shared
28
+ * rules across many pages):
29
+ *
30
+ * Speculation-Rules: "/rules/speculation.json"
31
+ *
32
+ * The header form is the framework's default because it keeps
33
+ * HTML response bodies clean, lets the rules document be cached
34
+ * independently, and avoids touching response bodies (zero risk
35
+ * of body-parse / encoding mishaps).
36
+ *
37
+ * 2. Inline `<script type="speculationrules">` injection — for
38
+ * operators who want per-page rules, the framework can inject
39
+ * the JSON into `text/html` responses just before `</head>`
40
+ * (or, falling back, before the first `<body>` tag). Opt in
41
+ * with `{ inline: true }`.
42
+ *
43
+ * Mount AFTER `securityHeaders` and (when used) `cspNonce`. When
44
+ * `inline: true` is set, an operator-supplied nonce on `req` (via
45
+ * `cspNonce`) is added to the injected `<script>` tag so a strict
46
+ * CSP allows the rules to load.
47
+ *
48
+ * app.use(b.middleware.requestId());
49
+ * app.use(b.middleware.securityHeaders());
50
+ * app.use(b.middleware.cspNonce({ always: true }));
51
+ * app.use(b.middleware.speculationRules({
52
+ * rules: {
53
+ * prerender: [
54
+ * { where: { href_matches: "/articles/*" }, eagerness: "moderate" },
55
+ * ],
56
+ * prefetch: [
57
+ * { where: { href_matches: "/api/*" }, eagerness: "conservative" },
58
+ * ],
59
+ * },
60
+ * }));
61
+ *
62
+ * Both mode shapes validate the rules object at construct time so
63
+ * typos surface at boot, never as a silently-ignored speculation
64
+ * rule three deploys later.
65
+ */
66
+
67
+ var validateOpts = require("../validate-opts");
68
+
69
+ // Per W3C draft + Chromium implementation. `immediate` triggers the
70
+ // speculation as soon as the rules are seen; `conservative` waits
71
+ // for a strong intent signal (mousedown). The framework permits the
72
+ // full set; operators pick per-rule.
73
+ var EAGERNESS_LEVELS = {
74
+ "immediate": true,
75
+ "eager": true,
76
+ "moderate": true,
77
+ "conservative": true,
78
+ };
79
+
80
+ // `prerender` fully renders the destination in a hidden tab —
81
+ // expensive, fast on activation. `prefetch` pulls bytes only — cheap,
82
+ // faster TTFB on activation. Both are arrays of rule objects.
83
+ var ACTION_KEYS = ["prerender", "prefetch"];
84
+
85
+ // Header value — when an operator passes a string instead of a rules
86
+ // object, treat it as the URL to a separately-served rules document
87
+ // (the simpler header-form path).
88
+ function _looksLikeRulesUrl(v) {
89
+ return typeof v === "string" && v.length > 0;
90
+ }
91
+
92
+ // CR/LF/NUL screen — flows into the response header value, where
93
+ // header-injection would split into a forged second header.
94
+ var INJECTION_RE = /[\r\n\0]/;
95
+
96
+ function _refuseInjection(value, label) {
97
+ if (typeof value !== "string") return;
98
+ if (INJECTION_RE.test(value)) { // allow:regex-no-length-cap — CR/LF/NUL injection check, length bounded by caller
99
+ throw new TypeError(
100
+ "middleware.speculationRules: " + label + " contains CR/LF/NUL — refused as a " +
101
+ "header-injection vector");
102
+ }
103
+ }
104
+
105
+ // Validate the operator-supplied rules object. Returns nothing on
106
+ // success; throws TypeError with an actionable message at config
107
+ // time. Each entry must be a plain object with `where` (object) and
108
+ // `eagerness` (one of EAGERNESS_LEVELS).
109
+ function _validateRules(rules, label) {
110
+ if (!rules || typeof rules !== "object" || Array.isArray(rules)) {
111
+ throw new TypeError(label + " must be an object with `prerender` and/or `prefetch` arrays");
112
+ }
113
+ var seenAny = false;
114
+ for (var ki = 0; ki < ACTION_KEYS.length; ki += 1) {
115
+ var actionKey = ACTION_KEYS[ki];
116
+ var entries = rules[actionKey];
117
+ if (entries === undefined) continue;
118
+ if (!Array.isArray(entries)) {
119
+ throw new TypeError(label + "." + actionKey + " must be an array of rule objects");
120
+ }
121
+ seenAny = true;
122
+ for (var ei = 0; ei < entries.length; ei += 1) {
123
+ var rule = entries[ei];
124
+ if (!rule || typeof rule !== "object" || Array.isArray(rule)) {
125
+ throw new TypeError(label + "." + actionKey + "[" + ei + "] must be a plain object");
126
+ }
127
+ if (!rule.where || typeof rule.where !== "object" || Array.isArray(rule.where)) {
128
+ throw new TypeError(label + "." + actionKey + "[" + ei +
129
+ "].where must be a `where` clause object (W3C draft, e.g. { href_matches: \"/path/*\" })");
130
+ }
131
+ if (typeof rule.eagerness !== "string" || !EAGERNESS_LEVELS[rule.eagerness]) {
132
+ throw new TypeError(label + "." + actionKey + "[" + ei +
133
+ "].eagerness must be one of: " + Object.keys(EAGERNESS_LEVELS).join(", "));
134
+ }
135
+ }
136
+ }
137
+ if (!seenAny) {
138
+ throw new TypeError(label + " must declare at least one of `prerender` or `prefetch`");
139
+ }
140
+ }
141
+
142
+ /**
143
+ * @primitive b.middleware.speculationRules
144
+ * @signature b.middleware.speculationRules(req, res, next)
145
+ * @since 0.8.53
146
+ * @status stable
147
+ * @related b.middleware.cspNonce, b.middleware.securityHeaders
148
+ *
149
+ * Builds middleware that emits W3C Speculation Rules so the user-agent
150
+ * may prerender or prefetch matching links ahead of user navigation.
151
+ * Two emit paths:
152
+ *
153
+ * - Header form (default): emits `Speculation-Rules: "<url>"` where
154
+ * `<url>` points at an operator-served rules document.
155
+ * - Inline form (`inline: true`): injects a
156
+ * `<script type="speculationrules">{...}</script>` tag into
157
+ * `text/html` response bodies just before `</head>` (or fallback
158
+ * before `<body>`). When `cspNonce` middleware has populated
159
+ * `req.cspNonce`, the injected `<script>` carries that nonce so
160
+ * strict CSP allows it.
161
+ *
162
+ * The rules object is validated at construct time. Empty / malformed
163
+ * rules throw with a message naming the offending key.
164
+ *
165
+ * @opts
166
+ * {
167
+ * rules: object, // { prerender: [...], prefetch: [...] }
168
+ * rulesUrl: string, // header-mode URL (alternative to rules)
169
+ * inline: boolean, // default false; inject <script> instead of header
170
+ * }
171
+ *
172
+ * @example
173
+ * var b = require("@blamejs/core");
174
+ * var app = b.router.create();
175
+ * app.use(b.middleware.speculationRules({
176
+ * rules: {
177
+ * prerender: [
178
+ * { where: { href_matches: "/articles/*" }, eagerness: "moderate" },
179
+ * ],
180
+ * prefetch: [
181
+ * { where: { href_matches: "/api/*" }, eagerness: "conservative" },
182
+ * ],
183
+ * },
184
+ * }));
185
+ */
186
+ function create(opts) {
187
+ opts = opts || {};
188
+ validateOpts(opts, ["rules", "rulesUrl", "inline"], "middleware.speculationRules");
189
+
190
+ var inline = !!opts.inline;
191
+ var rulesUrl = opts.rulesUrl;
192
+ var rulesObj = opts.rules;
193
+
194
+ // Operators may pass `rules` as a string (URL form) for ergonomics —
195
+ // route that to rulesUrl so the validation logic below stays
196
+ // straightforward.
197
+ if (_looksLikeRulesUrl(rulesObj) && rulesUrl === undefined) {
198
+ rulesUrl = rulesObj;
199
+ rulesObj = undefined;
200
+ }
201
+
202
+ if (inline) {
203
+ if (!rulesObj) {
204
+ throw new TypeError(
205
+ "middleware.speculationRules: opts.rules is required when opts.inline is true " +
206
+ "(inline mode injects the rules JSON into the response body)");
207
+ }
208
+ _validateRules(rulesObj, "middleware.speculationRules: opts.rules");
209
+ } else {
210
+ if (rulesObj && rulesUrl !== undefined) {
211
+ throw new TypeError(
212
+ "middleware.speculationRules: pass either opts.rules or opts.rulesUrl, not both");
213
+ }
214
+ if (!rulesObj && (typeof rulesUrl !== "string" || rulesUrl.length === 0)) {
215
+ throw new TypeError(
216
+ "middleware.speculationRules: header mode requires opts.rulesUrl (the URL of " +
217
+ "an operator-served JSON rules document) or opts.rules (which the framework " +
218
+ "renders inline). Pass `inline: true` if you want the framework to embed the " +
219
+ "rules object as a <script type=\"speculationrules\"> tag.");
220
+ }
221
+ if (rulesObj) {
222
+ _validateRules(rulesObj, "middleware.speculationRules: opts.rules");
223
+ }
224
+ if (rulesUrl !== undefined) {
225
+ _refuseInjection(rulesUrl, "opts.rulesUrl");
226
+ }
227
+ }
228
+
229
+ // Pre-build the emission payload once.
230
+ // Header value per W3C draft + RFC 8941 (Structured Field Value
231
+ // String — quoted, no internal CR/LF). When the operator supplied
232
+ // `rules` directly in header mode, the framework serializes the
233
+ // JSON as a data: URL so a single primitive can serve both shapes
234
+ // without forcing the operator to wire a separate route.
235
+ var headerValue;
236
+ if (!inline) {
237
+ if (rulesUrl) {
238
+ headerValue = '"' + rulesUrl + '"';
239
+ } else {
240
+ var dataUrl = "data:application/speculationrules+json;base64," +
241
+ Buffer.from(JSON.stringify(rulesObj), "utf8").toString("base64");
242
+ headerValue = '"' + dataUrl + '"';
243
+ }
244
+ }
245
+
246
+ // Pre-built inline JSON. The body is small (rules objects are
247
+ // typically ~200 bytes); JSON.stringify once and cache.
248
+ var inlineJson = inline ? JSON.stringify(rulesObj) : null;
249
+
250
+ return function speculationRules(req, res, next) {
251
+ if (!inline) {
252
+ if (typeof res.setHeader === "function") {
253
+ res.setHeader("Speculation-Rules", headerValue);
254
+ }
255
+ return next();
256
+ }
257
+
258
+ // Inline mode — patch res.write / res.end so we inject the
259
+ // <script> tag into the first text/html response. Same shape as
260
+ // botDisclose / aiActDisclosure to stay consistent with the
261
+ // framework's response-rewrite convention.
262
+ var origWrite = res.write && res.write.bind(res);
263
+ var origEnd = res.end && res.end.bind(res);
264
+ var injected = false;
265
+
266
+ function _scriptTag() {
267
+ var nonceAttr = "";
268
+ // Operator-supplied per-request nonce flows through cspNonce
269
+ // middleware. If absent we still emit the script tag (works
270
+ // under non-strict CSP); under strict CSP without nonce, the
271
+ // browser refuses — visible to operators via cspReport.
272
+ if (req && typeof req.cspNonce === "string" && req.cspNonce.length > 0) {
273
+ nonceAttr = ' nonce="' + req.cspNonce + '"';
274
+ }
275
+ return '<script type="speculationrules"' + nonceAttr + '>' + inlineJson + '</script>';
276
+ }
277
+
278
+ function _maybeInject(chunk) {
279
+ if (injected) return chunk;
280
+ var ct = typeof res.getHeader === "function" ? res.getHeader("content-type") : "";
281
+ if (typeof ct !== "string" || ct.indexOf("text/html") === -1) return chunk;
282
+ var body = Buffer.isBuffer(chunk) ? chunk.toString("utf8") :
283
+ (typeof chunk === "string" ? chunk : "");
284
+ if (body.length === 0) return chunk;
285
+ var tag = _scriptTag();
286
+ var headClose = body.search(/<\/head\s*>/i);
287
+ if (headClose !== -1) {
288
+ body = body.slice(0, headClose) + tag + body.slice(headClose);
289
+ } else {
290
+ var bodyOpen = body.match(/<body[^>]*>/i);
291
+ if (bodyOpen) {
292
+ var idx = bodyOpen.index + bodyOpen[0].length;
293
+ body = body.slice(0, idx) + tag + body.slice(idx);
294
+ } else {
295
+ // No <head> or <body> — prepend so the rules at least
296
+ // reach the parser. This matches the bot-disclose fallback.
297
+ body = tag + body;
298
+ }
299
+ }
300
+ injected = true;
301
+ return Buffer.from(body, "utf8");
302
+ }
303
+
304
+ if (origWrite) {
305
+ res.write = function (chunk, encoding, cb) {
306
+ return origWrite(_maybeInject(chunk), encoding, cb);
307
+ };
308
+ }
309
+ if (origEnd) {
310
+ res.end = function (chunk, encoding, cb) {
311
+ if (chunk) chunk = _maybeInject(chunk);
312
+ return origEnd(chunk, encoding, cb);
313
+ };
314
+ }
315
+ return next();
316
+ };
317
+ }
318
+
319
+ module.exports = {
320
+ create: create,
321
+ EAGERNESS_LEVELS: Object.keys(EAGERNESS_LEVELS),
322
+ ACTION_KEYS: ACTION_KEYS,
323
+ };
@@ -0,0 +1,198 @@
1
+ "use strict";
2
+ /**
3
+ * mime-parse — minimal RFC 5322 + RFC 2045 + RFC 2046 reader shared by
4
+ * lib/mail-bounce.js (RFC 3464 DSN parser) and lib/mail-mdn.js (RFC
5
+ * 3798 MDN parser).
6
+ *
7
+ * Scope:
8
+ *
9
+ * parseHeaderBlock(text) — split a header section into
10
+ * [{name, value}] with RFC 5322 §2.2.3
11
+ * continuation-line unfolding.
12
+ * splitHeadersAndBody(text) — bisect at the empty line per RFC
13
+ * 5322 §2.1; tolerate CRLF and LF
14
+ * line endings (real-world MTA chains
15
+ * normalize differently).
16
+ * findHeader(headers, name) — case-insensitive header lookup;
17
+ * returns the value of the first
18
+ * match or null.
19
+ * parseContentType(value) — `type/subtype; param=value;
20
+ * param="quoted value"` reader.
21
+ * Returns `{ type, params }`.
22
+ * splitMimeParts(body, boundary) — partition a multipart body on
23
+ * `--<boundary>` markers per RFC
24
+ * 2046 §5.1.1; consumes the trailing
25
+ * `--<boundary>--` close.
26
+ *
27
+ * Out of scope (intentionally): MIME word-decoding (RFC 2047), QP /
28
+ * base64 decoding (operators take the part body as-is and decode if
29
+ * Content-Transfer-Encoding asks them to), nested multipart recursion
30
+ * (DSN + MDN reports are flat by spec), full RFC 5322 address-list
31
+ * parsing (mail.js owns the From/To/Cc parser).
32
+ *
33
+ * The reader is byte-encoding-agnostic — strings come in, slices come
34
+ * out. UTF-8 inputs ride through unchanged (RFC 6533 EAI / RFC 6532
35
+ * SMTPUTF8 messages parse correctly). Operator-supplied bytes are
36
+ * length-capped by the calling primitive (mail-bounce: 1 MiB, mail-
37
+ * mdn: 1 MiB) so this module's hot-path doesn't add its own cap.
38
+ */
39
+
40
+ function parseHeaderBlock(text) {
41
+ // RFC 5322 §2.2.3 — continuation lines (lines starting with WSP)
42
+ // fold onto the previous header value. The header section ends at
43
+ // the first empty line; subsequent lines belong to the body.
44
+ var lines = text.split(/\r?\n/);
45
+ var unfolded = [];
46
+ for (var i = 0; i < lines.length; i += 1) {
47
+ var line = lines[i];
48
+ if (line.length === 0) break;
49
+ if ((line.charAt(0) === " " || line.charAt(0) === "\t") && unfolded.length > 0) {
50
+ unfolded[unfolded.length - 1] += " " + line.replace(/^\s+/, "");
51
+ } else {
52
+ unfolded.push(line);
53
+ }
54
+ }
55
+ var headers = [];
56
+ for (var j = 0; j < unfolded.length; j += 1) {
57
+ var l = unfolded[j];
58
+ var colonAt = l.indexOf(":");
59
+ if (colonAt === -1) continue;
60
+ headers.push({
61
+ name: l.slice(0, colonAt).trim(),
62
+ value: l.slice(colonAt + 1).trim(),
63
+ });
64
+ }
65
+ return headers;
66
+ }
67
+
68
+ function splitHeadersAndBody(text) {
69
+ // RFC 5322 §2.1 — header / body separator is one empty line. Real-
70
+ // world MTAs disagree on CRLF vs LF; accept both, prefer CRLF when
71
+ // present (the canonical wire form).
72
+ var sepCrlf = text.indexOf("\r\n\r\n");
73
+ var sepLf = text.indexOf("\n\n");
74
+ var sep, sepLen;
75
+ if (sepCrlf !== -1 && (sepLf === -1 || sepCrlf < sepLf)) {
76
+ sep = sepCrlf; sepLen = 4;
77
+ } else if (sepLf !== -1) {
78
+ sep = sepLf; sepLen = 2;
79
+ } else {
80
+ sep = -1; sepLen = 0;
81
+ }
82
+ if (sep === -1) {
83
+ return { headers: parseHeaderBlock(text), body: "" };
84
+ }
85
+ return {
86
+ headers: parseHeaderBlock(text.slice(0, sep)),
87
+ body: text.slice(sep + sepLen),
88
+ };
89
+ }
90
+
91
+ function findHeader(headers, name) {
92
+ var target = String(name).toLowerCase();
93
+ for (var i = 0; i < headers.length; i += 1) {
94
+ if (headers[i].name.toLowerCase() === target) return headers[i].value;
95
+ }
96
+ return null;
97
+ }
98
+
99
+ function parseContentType(value) {
100
+ // RFC 2045 §5.1 — `type/subtype` followed by zero or more
101
+ // `; param=value` pairs. Parameter values may be quoted-string with
102
+ // backslash-escaped DQUOTE.
103
+ if (typeof value !== "string") return { type: "", params: {} };
104
+ var semi = value.indexOf(";");
105
+ var typePart = (semi === -1 ? value : value.slice(0, semi)).trim().toLowerCase();
106
+ var rest = semi === -1 ? "" : value.slice(semi + 1);
107
+ var params = {};
108
+ var i = 0;
109
+ while (i < rest.length) {
110
+ while (i < rest.length && (rest.charAt(i) === " " || rest.charAt(i) === "\t" || rest.charAt(i) === ";")) i += 1;
111
+ if (i >= rest.length) break;
112
+ var eq = rest.indexOf("=", i);
113
+ if (eq === -1) break;
114
+ var pname = rest.slice(i, eq).trim().toLowerCase();
115
+ var j = eq + 1;
116
+ while (j < rest.length && (rest.charAt(j) === " " || rest.charAt(j) === "\t")) j += 1;
117
+ var pval;
118
+ if (rest.charAt(j) === '"') {
119
+ var end = j + 1;
120
+ var buf = "";
121
+ while (end < rest.length) {
122
+ var ch = rest.charAt(end);
123
+ if (ch === "\\" && end + 1 < rest.length) {
124
+ buf += rest.charAt(end + 1);
125
+ end += 2;
126
+ continue;
127
+ }
128
+ if (ch === '"') break;
129
+ buf += ch;
130
+ end += 1;
131
+ }
132
+ pval = buf;
133
+ i = end + 1;
134
+ } else {
135
+ var endTok = j;
136
+ while (endTok < rest.length && rest.charAt(endTok) !== ";") endTok += 1;
137
+ pval = rest.slice(j, endTok).trim();
138
+ i = endTok;
139
+ }
140
+ params[pname] = pval;
141
+ }
142
+ return { type: typePart, params: params };
143
+ }
144
+
145
+ function splitMimeParts(body, boundary) {
146
+ // RFC 2046 §5.1.1 — multipart parts are separated by `--<boundary>`
147
+ // delimiter lines. The closing delimiter is `--<boundary>--`.
148
+ // Preamble (before the first delimiter) and epilogue (after the
149
+ // close) are discarded.
150
+ var parts = [];
151
+ if (typeof boundary !== "string" || boundary.length === 0) return parts;
152
+ var marker = "--" + boundary;
153
+ var lines = String(body).split(/\r?\n/);
154
+ var current = null;
155
+ for (var i = 0; i < lines.length; i += 1) {
156
+ var line = lines[i];
157
+ if (line === marker || line === marker + "--") {
158
+ if (current !== null) parts.push(current.join("\r\n"));
159
+ if (line === marker + "--") { current = null; break; }
160
+ current = [];
161
+ continue;
162
+ }
163
+ if (current !== null) current.push(line);
164
+ }
165
+ return parts;
166
+ }
167
+
168
+ // stripAddressType — RFC 3464 §2.3.1 / RFC 6533 §3.2 / RFC 3798 §3.2.3.
169
+ // `addr-type;recipient` -> `recipient`. The type prefix (rfc822 /
170
+ // utf-8 / x-* on extension) is consumed by the caller for byte-
171
+ // encoding decisions but discarded from the returned recipient.
172
+ function stripAddressType(value) {
173
+ if (typeof value !== "string") return null;
174
+ var semi = value.indexOf(";");
175
+ if (semi === -1) return value.trim();
176
+ return value.slice(semi + 1).trim();
177
+ }
178
+
179
+ // addressType — symmetric helper for the BUILD path. RFC 6533 EAI
180
+ // awareness: any non-ASCII byte in the recipient flips the address-
181
+ // type to utf-8 so the wire form names the encoding correctly.
182
+ function addressType(addr) {
183
+ if (typeof addr !== "string") return "rfc822";
184
+ for (var i = 0; i < addr.length; i += 1) {
185
+ if (addr.charCodeAt(i) > 0x7F) return "utf-8";
186
+ }
187
+ return "rfc822";
188
+ }
189
+
190
+ module.exports = {
191
+ parseHeaderBlock: parseHeaderBlock,
192
+ splitHeadersAndBody: splitHeadersAndBody,
193
+ findHeader: findHeader,
194
+ parseContentType: parseContentType,
195
+ splitMimeParts: splitMimeParts,
196
+ stripAddressType: stripAddressType,
197
+ addressType: addressType,
198
+ };