Skip to content

fix(email-core): parse name-address strings in cc/to compose inputs#64

Merged
stevenobiajulu merged 1 commit intomainfrom
fix/cc-address-parsing
Apr 29, 2026
Merged

fix(email-core): parse name-address strings in cc/to compose inputs#64
stevenobiajulu merged 1 commit intomainfrom
fix/cc-address-parsing

Conversation

@stevenobiajulu
Copy link
Copy Markdown
Member

Summary

read_email returns recipients in "Name <email>" form, but the compose actions (reply_to_email, send_email, create_draft, update_draft) wrapped each input string blindly as {email: rawString}. The full "Name <email>" string ended up in EmailAddress.email, producing malformed Cc:/To: headers in Gmail (silent recipient drop) and corrupt emailAddress.address in Microsoft Graph.

Fixes #47.

What changed

  • New packages/email-core/src/utils/address.ts with parseAddressString + parseAddressList. Narrow on purpose — bare email, Name <email>, "Quoted, Name" <email>, <email>. Rejects multi-address strings (Alice <a@x>, Bob <b@y>), unbalanced quotes, and stray angle brackets.
  • New parseRecipients helper in compose-helpers.ts that returns {to, cc} parsed or a structured INVALID_ADDRESS error.
  • reply.ts, send.ts, draft.ts parse input recipients once at the top of run() and feed the parsed EmailAddress[] to both the allowlist check and the provider payload.
  • send.ts's allowlist now receives parsed.to.map(a => a.email) instead of raw strings — without this, passing to: "Alice <alice@allowed.com>" would be falsely rejected as not allowlisted.
  • update_draft preserves the cc: [] explicit-clear semantics.

Total non-test diff: ~80 lines.

Scope notes (after peer review)

The minimal "swap the mapper" version of this fix would have introduced a regression in send_email's allowlist enforcement. Both Gemini and Codex peer reviews flagged this and a related concern that throwing parse errors inside withRetry would cause spurious retries. The implemented version parses early and returns structured INVALID_ADDRESS before either allowlist or retry logic.

Out of scope (follow-ups)

  • cc is never explicitly allowlisted on send_email. This is a pre-existing concern that becomes more visible once parsing works correctly. Will file a separate issue.
  • list_emails / search_emails schemas don't expose to/cc. Separate change.
  • MS Graph sibling bug (Microsoft Graph provider createDraft() drops cc and bcc recipients #48) intentionally untouched.

Test plan

  • npm run test:run — all 581 tests pass across 4 packages
  • Type-check clean across all 4 packages (npm run lint --workspaces)
  • Grep guard: zero remaining .map(email => ({ email })) call sites in source
  • New tests cover: parser unit cases (incl. multi-address rejection regression guard), allowlist-not-falsely-rejecting name-address to, structured INVALID_ADDRESS errors with field/index, update_draft cc: [] explicit-clear, Gmail Cc: "Name" <email> serializer assertion

Observed 2026-04-24. Do not merge — please review.

read_email returns recipients in 'Name <email>' form (RFC 5322
name-address), but compose actions wrapped each input string blindly as
{email: raw_string}. The whole 'Name <email>' string ended up in
EmailAddress.email, producing malformed Cc:/To: headers in Gmail (silent
recipient drop) and corrupt emailAddress.address in Microsoft Graph.

This change parses recipient strings once per action via a new
parseAddressString helper, then feeds the parsed EmailAddress[] to both
the allowlist check and the provider payload. Returning a structured
INVALID_ADDRESS error early avoids retrying parse failures inside
withRetry and avoids zod schema-validation pitfalls in the MCP layer.

Notable scope adjustments after peer review:
- send.ts allowlist now receives parsed bare emails — without this,
  passing 'Alice <alice@allowed.com>' as 'to' would be falsely rejected.
- update_draft preserves cc:[] explicit-clear semantics.
- Gmail provider test extended to lock in 'Cc: \"Name\" <email>' form.

Out of scope (separate follow-ups): cc-allowlist on send_email/
reply_to_email, list_emails schema for to/cc, MS Graph sibling bug.

Observed 2026-04-24.
Fixes #47.

// Name-address: either "quoted name" + <email>, or bare-name + <email>.
// Quotes must be balanced when present; bare name cannot contain '<' or '"'.
const m = /^\s*(?:"([^"]*)"|([^<"]*?))\s*<\s*([^\s"<>@]+@[^\s"<>@]+)\s*>\s*$/.exec(trimmed);
@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 28, 2026

Codecov Report

❌ Patch coverage is 92.59259% with 4 lines in your changes missing coverage. Please review.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
packages/email-core/src/utils/address.ts 87.50% 3 Missing ⚠️
packages/email-core/src/actions/reply.ts 87.50% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@stevenobiajulu stevenobiajulu merged commit 5a5b514 into main Apr 29, 2026
13 of 14 checks passed
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.

read_email recipient strings do not round-trip through compose actions when display names are present

2 participants