Skip to content

v3.2: streaming, severity classification, obs-route support, CFWS tolerance#51

Merged
mmucklo merged 1 commit intomasterfrom
feature/v3.2-streaming-severity
Apr 13, 2026
Merged

v3.2: streaming, severity classification, obs-route support, CFWS tolerance#51
mmucklo merged 1 commit intomasterfrom
feature/v3.2-streaming-severity

Conversation

@mmucklo
Copy link
Copy Markdown
Owner

@mmucklo mmucklo commented Apr 13, 2026

Summary

Completes all five v3.2 roadmap items. Fully additive for v3.1 callers — no breaking API changes. Two notable tolerance expansions: previously-rejected inputs now parse successfully (CFWS around @ and inside <…>; obs-route inside rfc5322()/rfc2822() presets).

What's new

Streaming batch parsing

  • Parse::parseStream(iterable $input, string $encoding): Generator<ParsedEmailAddress> — yields one typed address at a time. Each input item may itself contain multiple separator-delimited addresses. Use for large batches where holding every result in memory is undesirable.

Severity classification

  • ValidationSeverity backed enum: Critical / Warning / Info with stable backing values.
  • ParseErrorCode::severity(): ValidationSeverity — every code classified. 13 codes are Warning (UTF-8 gating, C0/C1 controls, empty-quoted, FQDN, IP global-range, length limits, punycode conversion); all others are Critical.
  • ParsedEmailAddress::invalidSeverity(): ?ValidationSeverity — derived; null when valid.

Callers can now distinguish unparseable input from well-formed but policy-rejected and choose to accept Warning-level failures in non-SMTP contexts:

if ($parsed->invalid && $parsed->invalidSeverity() === ValidationSeverity::Warning) {
    // e.g. private-range IP literal, non-FQDN domain, octet length over RFC 5321 §4.5.3.1
    // — safe to accept depending on the caller's needs.
}

RFC 5322 §4.4 obs-route support

  • ParseOptions::$allowObsRoute (readonly; default false; enabled in rfc5322() and rfc2822()).
  • withAllowObsRoute() fluent builder.
  • New internal STATE_OBS_ROUTE state absorbs source-route prefixes (<@host1,@host2:user@host3>), then resumes normal addr-spec parsing at the : terminator.
  • Captured route exposed on ParsedEmailAddress::$obsRoute; null when no route was consumed.
  • Incomplete obs-route (<@host> with no : before >) → ParseErrorCode::IncompleteAddress.

CFWS tolerance (RFC 5322 §3.2.2)

Folding whitespace now absorbed via look-ahead in four positions that were previously rejected:

  • Trailing CFWS on local-part dot-atom: local @domain
  • Leading CFWS on domain dot-atom: local@ domain
  • Leading CFWS inside angle-addr: < local@domain>
  • Trailing CFWS inside angle-addr before >: <local@domain >

Folded whitespace (LF + WSP) is handled as part of the same run. The look-ahead is positional: whitespace is only absorbed when the next significant character is @, >, or the first atext inside angle-addr.

Migration notes for v3.1 users

Only the two tolerance expansions could surprise callers:

  1. CFWS: if you relied on addresses with whitespace around @ being rejected, they now register invalid=false.
  2. Obs-route: <@host:addr> is now accepted in rfc5322()/rfc2822(). Opt out via ->withAllowObsRoute(false).

Everything else is pure addition. See UPGRADE.md.

Test plan

  • composer ci passes (cs:check, PHPStan level 6 at 512M, 42 tests / 445 assertions)
  • Project line coverage 89.61% (up from 88.78% in v3.1)
  • Per-file 100% line coverage on ValidationSeverity, ParsedEmailAddress, ParseResult, ParseErrorCode, LengthLimits; 99.32% on ParseOptions; 86.69% on Parse
  • New tests cover: parseStream with arrays/generators/multi-address items/mixed valid-invalid, severity mapping for every ParseErrorCode case, obs-route success/multi-host/display-name/batch/disabled-by-default/incomplete/empty-addr-spec, CFWS in six distinct positions including multi-line folding
  • All 42 tests pass with no regressions; the test harness's alignReasonCodeOne() was extended to strip the new obs_route field from actual output when expected YAML doesn't specify it

Docs updated

  • CHANGELOG.md — v3.2.0 entry with Added / Changed sections
  • UPGRADE.md — v3.1 → v3.2 with migration notes for CFWS and obs-route tolerance changes
  • ROADMAP.md — v3.2 items flipped to [x]
  • README.mdparseStream example in Basic Usage; allowObsRoute row in rule properties table

All v3.2 roadmap items. Fully additive — no breaking changes for v3.1 callers;
two notable tolerance expansions (inputs previously rejected as invalid now
parse) that existing "strict whitespace" validators may want to know about.

Streaming batch parsing:
- Parse::parseStream(iterable, string): Generator<ParsedEmailAddress> — lazy
  parsing that yields one typed address at a time. Each input item may itself
  contain multiple separator-delimited addresses. Use for large batches where
  holding every parsed result in memory is undesirable.

Severity classification:
- ValidationSeverity backed enum (Critical / Warning / Info) with stable
  string backing values.
- ParseErrorCode::severity() classifies every code. 13 codes are Warning
  (UTF-8 gating, C0/C1 controls, empty-quoted, FQDN, IP global-range, length
  limits, punycode conversion) — all structural/unparseable failures are
  Critical.
- ParsedEmailAddress::invalidSeverity() returns the derived severity or null
  when the address is valid.
- Rationale: callers can now distinguish "unparseable input" from
  "well-formed but policy-rejected" and choose to accept Warning-level
  failures in non-SMTP contexts.

RFC 5322 §4.4 obs-route support:
- ParseOptions::$allowObsRoute rule property (readonly, default false;
  enabled by default in rfc5322() and rfc2822() presets).
- withAllowObsRoute() fluent builder.
- New STATE_OBS_ROUTE absorbs `@host1,@host2:` source-route prefixes inside
  angle-addr, then resumes normal addr-spec parsing on the ':' terminator.
- Captured route is exposed as ParsedEmailAddress::$obsRoute (null when no
  route was consumed). Also present as the `obs_route` key on the legacy
  array output.
- Incomplete obs-route (`<@host>` with no colon before `>`) is flagged
  invalid with ParseErrorCode::IncompleteAddress.

CFWS tolerance (RFC 5322 §3.2.2):
- Folding whitespace is now absorbed via look-ahead in the whitespace
  handler at four positions that were previously rejected:
    * Trailing CFWS on local-part dot-atom: "local @Domain"
    * Leading CFWS on domain dot-atom: "local@ domain"
    * Leading CFWS inside angle-addr: "<  local@domain>"
    * Trailing CFWS inside angle-addr before '>': "<local@domain  >"
- Folded whitespace (LF + WSP) is handled as part of the same run.
- Comments in these positions were already supported in v3.0.
- The look-ahead is positional: only whitespace directly preceding an '@',
  '>', or the first atext inside an angle-addr is absorbed. Whitespace in
  other positions (e.g. between atext tokens in a dot-atom) still errors
  per existing behavior.

Tests: 42 tests / 445 assertions (up from 36 / 426 in v3.1).
Project coverage: 89.61% lines (up from 88.78%).

Docs: CHANGELOG v3.2.0, UPGRADE v3.1 → v3.2 with migration notes for the
CFWS and obs-route tolerance changes, ROADMAP items flipped to [x], README
updated with parseStream example and allowObsRoute rule property.

Tooling: composer stan now runs with --memory-limit=512M to accommodate
the larger codebase.
@codecov
Copy link
Copy Markdown

codecov bot commented Apr 13, 2026

Codecov Report

❌ Patch coverage is 97.64706% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 89.34%. Comparing base (e3236cd) to head (3d72af6).
⚠️ Report is 1 commits behind head on master.

Files with missing lines Patch % Lines
src/ParseOptions.php 60.00% 2 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@             Coverage Diff              @@
##             master      #51      +/-   ##
============================================
+ Coverage     88.59%   89.34%   +0.74%     
- Complexity      323      357      +34     
============================================
  Files             5        6       +1     
  Lines           833      910      +77     
============================================
+ Hits            738      813      +75     
- Misses           95       97       +2     
Files with missing lines Coverage Δ
src/Parse.php 85.96% <100.00%> (+1.16%) ⬆️
src/ParseErrorCode.php 100.00% <100.00%> (ø)
src/ParsedEmailAddress.php 100.00% <100.00%> (ø)
src/ParseOptions.php 98.89% <60.00%> (-1.11%) ⬇️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@mmucklo mmucklo merged commit 77d4d20 into master Apr 13, 2026
10 checks passed
@mmucklo mmucklo deleted the feature/v3.2-streaming-severity branch April 13, 2026 04:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant