Raw MIME Address Parsing (parser/address)
Reference for parseFromHeader and parseFromHeaderLoose, the two RFC 5322 address parsers in @primitivedotdev/sdk/parser that extract a single validated address from a From/Sender header.
What the address parsers do#
Two address parsers ship under @primitivedotdev/sdk/parser: parseFromHeader, a strict RFC 5322 parser meant for authorization decisions, and parseFromHeaderLoose, a lenient parser meant for display-only call sites. Both operate on a raw header string (e.g. the From or Sender header value) and are implemented in sdk-node/src/parser/address-parser.ts.
Never use parseFromHeaderLoose for permission gates or any decision that grants access. Its recovered display name can silently absorb a second address or garbage tokens. Use parseFromHeader for anything security-bearing.
parseFromHeader#
Strictly parses a header for security-bearing contexts: allowlist gates, permission grants, domain-anchored sender trust anchors, and similar decisions where the address itself is the thing being trusted.
import { parseFromHeader } from "@primitivedotdev/sdk/parser";
const result = parseFromHeader('"Alice" <alice@example.com>');
if (result.ok) {
console.log(result.value.address); // "alice@example.com"
} else {
console.log(result.reason); // one of ParseFromHeaderFailureReason
}
Signature#
function parseFromHeader(
header: string | null | undefined,
): ParseFromHeaderResult;
Result shape#
type ParseFromHeaderResult =
| { ok: true; value: ValidatedAddress }
| { ok: false; reason: ParseFromHeaderFailureReason };
interface ValidatedAddress {
address: string; // lowercased local-part and domain
}
Rejection reasons#
parseFromHeader never falls back to a "best guess." Any of the following inputs return { ok: false, reason } instead of a partially-parsed address:
reason | Trigger |
|---|---|
empty | header is null, undefined, or whitespace-only after trimming |
too_long | Header exceeds 998 UTF-8 bytes (the RFC 5322 §2.1.1 line limit, measured in bytes so multi-byte SMTPUTF8/RFC 6531 characters can't bypass the cap) |
multiple_addresses | Header contains more than one address (RFC 5322 permits multi-address From, but it's rare and ambiguous as an identity) |
group_syntax | Header uses group syntax, e.g. Friends: a@b.com, c@d.com; |
invalid_address | The single address fails validator's isEmail check under the SDK's chosen options, or the parse produced no usable entry |
The isEmail check (via validator/lib/isEmail) enforces per-part length limits (64-octet local-part, 255-octet domain), dot-atom rules, hostname-label rules, a required TLD, and other RFC 5321/5322 conformance checks. Its options:
| Option | Value | Effect |
|---|---|---|
allow_ip_domain | true | Accepts address-literals like user@[192.168.1.1] |
require_tld | true | Rejects user@localhost |
allow_display_name | false | Only the bare addr-spec is checked (the display name was already stripped by the header parser) |
allow_utf8_local_part | true | Accepts SMTPUTF8 / EAI local-parts |
Normalization#
On success, value.address is lowercased in full, both the local-part and the domain. RFC 5321 §2.4 technically permits case-sensitive local-parts, but every consumer mailbox treats them as case-insensitive in practice; a case-sensitive grant key would otherwise split Bob@x.com from bob@x.com into separate rows and defeat a primary-key lookup.
No display name is returned. Deliberately: an input like Name <user@x.com> <attacker@y.com> parses as one entry whose name field can silently absorb the second address, so surfacing that as a "parsed name" here would invite misuse. Call parseFromHeaderLoose alongside if you also need a name for display.
parseFromHeaderLoose#
Lenient parser for display-only call sites: inbox card "from" fields, log lines, debugging output.
import { parseFromHeaderLoose } from "@primitivedotdev/sdk/parser";
const parsed = parseFromHeaderLoose('"Alice" <alice@example.com>');
// { address: "alice@example.com", name: "Alice" }
Given a multi-address header, it returns the first address that passes validation rather than rejecting the header.
### Signature
```typescript
function parseFromHeaderLoose(
header: string | null | undefined,
): ParsedAddress | null;
Result shape#
interface ParsedAddress {
address: string; // lowercased
name: string | null;
}
Returns null when the header is null/undefined, empty after trimming, exceeds the 998-byte limit, or contains no address that passes the same isEmail check used by parseFromHeader.
Behavior differences from parseFromHeader#
| Case | parseFromHeader | parseFromHeaderLoose |
|---|---|---|
| Multiple addresses | Rejects (multiple_addresses) | Returns the first parseable address |
| Group syntax | Rejects (group_syntax) | Flattens into member addresses and returns the first parseable one |
| No valid address | Returns a typed reason | Returns null |
| Display name | Never returned | Returned when present, otherwise null |
Names returned by parseFromHeaderLoose can include addressparser's recovery output, trailing tokens or garbage preceding the address. Treat name as opaque text for display; sanitize before re-emitting it (e.g. into HTML or a re-sent email).
Choosing between them#
- Authorization, allowlisting, or any trust decision: use
parseFromHeaderand branch onresult.ok/result.reason. - Showing a sender's name in a UI or log: use
parseFromHeaderLooseand treatnameas untrusted display text.
Both parsers are pure functions with no network or I/O dependency; they operate only on the header string you pass in. For deciding whether an inbound email's sender domain can be trusted (not just whether the header parses), see Verifying Inbound Email Authenticity.
Next steps#
Use validateEmailAuth and isTrustedSender to anchor authorization decisions to SPF/DKIM/DMARC results and an expected sending domain.
Parsing Raw Email (.eml)Parse raw MIME bytes into structured bodies, headers, and attachments, and extract addresses safely from a full message.
Was this page helpful?