Skip to content

refactor(email): registry-pattern provider architecture + code-review fixes#573

Merged
zbigniewsobiecki merged 2 commits intodevfrom
refactor/email-provider-registry
Feb 27, 2026
Merged

refactor(email): registry-pattern provider architecture + code-review fixes#573
zbigniewsobiecki merged 2 commits intodevfrom
refactor/email-provider-registry

Conversation

@zbigniewsobiecki
Copy link
Copy Markdown
Member

Summary

Replaces the monolithic EmailClient class with a clean, extensible registry-based architecture modelled after the existing PM integration pattern.

Architecture changes

  • EmailProvider interface — runtime operations (searchEmails / readEmail / sendEmail / replyToEmail / markEmailAsSeen)
  • EmailIntegration interface — DB credential resolution + AsyncLocalStorage provider scoping per project
  • EmailIntegrationRegistry — singleton populated at import time via src/email/index.ts (same pattern as pm/index.ts)
  • ImapEmailProvider — password-auth via imapflow (IMAP) + nodemailer (SMTP)
  • GmailEmailProvider — OAuth-auth via imapflow (IMAP) + Gmail REST API (send) — avoids SMTP 465 blocking in containers
  • ImapIntegration / GmailIntegration — resolve credentials from DB, scope provider via withEmailProvider

Adding a new email provider now requires only: a new EmailProvider class + one emailRegistry.register() call.

Code-review fixes (gadget core + integration)

File Fix
All 5 src/gadgets/email/core/*.ts Hoist const message before logger.error — was extracted twice
readEmail.ts Add else { lines.push('', '(no body)') } fallback for attachment-only emails
sendEmail.ts + replyToEmail.ts Guard empty accepted list — was emitting "Email sent successfully to "
searchEmails.ts Remove embedded \n from header line — caused double blank line in output
imap/integration.ts logger.warn when IMAP/SMTP port is non-numeric — was silently returning null

Test coverage

  • context.test.ts — AsyncLocalStorage scoping, getEmailProvider throws outside scope
  • imap/adapter.test.ts — adds readEmail (success + not-found) and replyToEmail (threading headers, finally cleanup)
  • gmail/adapter.test.ts — adds searchEmails + readEmail tests; migrates mocks to vi.hoisted() for correctness
  • integration.test.ts — registry delegation, credential fallback, warn path

Test plan

  • npm test — 3430 tests pass
  • npm run typecheck — zero errors
  • npm run lint — zero warnings

🤖 Generated with Claude Code

zbigniewsobiecki and others added 2 commits February 27, 2026 15:38
…ider architecture

Decomposes EmailClient into clean, extensible layers:

- EmailProvider interface — runtime ops (search/read/send/reply/mark)
- EmailIntegration interface — credential resolution + AsyncLocalStorage scoping
- EmailIntegrationRegistry — singleton, populated at import time (mirrors pm/index.ts)
- ImapEmailProvider — password-auth via imapflow + nodemailer SMTP
- GmailEmailProvider — OAuth-auth via imapflow IMAP + Gmail REST API send
- ImapIntegration / GmailIntegration — per-provider DB credential resolution

Code-review fixes applied to gadget core files:
- Hoist const message before logger.error (was extracted twice in all 5 files)
- Add (no body) fallback in readEmail for attachment-only emails
- Guard empty accepted list in sendEmail + replyToEmail (misleading success message)
- Remove embedded \n from searchEmails header (caused double blank line in output)
- Log logger.warn in ImapIntegration when IMAP/SMTP port parses as NaN

New and expanded test coverage:
- context.test.ts: AsyncLocalStorage scoping + getEmailProvider throws outside scope
- imap/adapter.test.ts: readEmail (success + not-found) + replyToEmail (threading, cleanup)
- gmail/adapter.test.ts: searchEmails + readEmail tests; mocks migrated to vi.hoisted()
- integration.test.ts: registry delegation, credential fallback, warn path

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Production callers of withEmailIntegration / hasEmailIntegration were importing
directly from email/integration.js, which bypasses the side-effect registration in
email/index.ts that populates the emailRegistry with ImapIntegration and
GmailIntegration.

Without the registry populated, emailRegistry.getOrNull() always returns null,
causing withEmailIntegration to run without scoping a provider (email gadgets fail)
and hasEmailIntegration to always return false.

Fix: update the four production callers and the integration test to import from
email/index.js (same pattern as PM callers importing from pm/index.js).

Files changed:
- src/triggers/shared/integration-validation.ts
- src/triggers/shared/manual-runner.ts
- src/triggers/github/webhook-handler.ts
- src/pm/webhook-handler.ts
- tests/integration/integration-validation.test.ts

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@zbigniewsobiecki zbigniewsobiecki merged commit 5adad06 into dev Feb 27, 2026
6 checks passed
@zbigniewsobiecki zbigniewsobiecki deleted the refactor/email-provider-registry branch February 27, 2026 15:49
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