feat(core): email transport seam for provider plugins (G-P4-5)

Merged
EV
10d ago
+596-17

Plan item G-P4-5 (core) — Gauzy ↔ Ever Works integration plan, 04-inbox-and-email.md §5.5 / 06-gauzy-implementation-plan.md Phase 4.

Owned by 7dae55c0/gw-gauzy-email — other lanes please don't push to this branch.

What

  • EmailTransportRegistry (packages/core/src/lib/email-send/email-transport.registry.ts): provider plugins (Resend, Postmark, Mailgun, SendGrid, SES — each its own PR, each behind its own EMAIL_PROVIDER_*_ENABLED flag, default off) register a resolver that supplies a nodemailer-compatible API transport for a { tenantId, organizationId, providerAccountId } request. One registry per process (getEmailTransportRegistry(), also provided by EmailSendModule).
  • EmailSendService.getEmailInstance asks the registry first; when no resolver is registered, or none answers, it runs the existing SMTP chain verbatim (organization Custom SMTP → tenant Custom SMTP → MAIL_*; the old body moved unchanged into smtpEmailInstance).
  • EmailSendService.sendMessage(message, scope): a non-templated send (From, To/Cc/Bcc, Reply-To, In-Reply-To, References, Message-ID, headers, attachments) through the same chain, answering { messageId, accepted, rejected, transport }. The Inbox reply / forward routes (/api/inbox/emails/:id/reply|forward) use it; they ship with the inbound-pipeline PR (G-P4-2) because they need the inbox_email table it creates.
  • EmailSendModule also exports EmailSendService + the registry; they and their types are public API of @gauzy/core (exported at the end of index.ts, after ./lib/core has loaded the module graph, so no new require-cycle edge).

Zero behaviour change

  • No resolver is registered unless a provider plugin is loaded, and none is loaded unless its flag is exactly 'true'. With an empty registry providerEmailInstance returns null before touching anything; getEmailInstance then runs the old code unchanged. getInstance() (platform default for system mail) is untouched.
  • A resolver that throws is logged (via describeEmailSendError, addresses masked) and skipped, so a broken provider account falls back to SMTP instead of stopping mail.

Tests

  • email-transport.registry.spec.ts: order, null/throw/no-transport skipping, duplicate-id refusal, unregister, process singleton.
  • email-send.service.transport-registry.spec.ts: empty registry → SMTP chain; all-null → SMTP chain; provider → never reads Custom SMTP or verifies SMTP; sendMessage through a recording nodemailer custom transport keeps From / In-Reply-To / References.
  • Not run locally (owner rule for this run: review + merge, the full suites run once on develop at the end). prettier --check + cspell clean on the touched files.

🤖 Generated with Claude Code


Summary by cubic

Adds an email transport seam so provider plugins (Resend, Postmark, etc.) can supply nodemailer transports for email sending without changing the existing SMTP chain.

  • EmailTransportRegistry lets a provider register a resolver that answers per tenant, organization, and provider account; with no resolver registered, all sends take the unchanged SMTP fallback (Custom SMTP → MAIL_*).
  • EmailSendService.sendMessage sends non-templated messages (Inbox replies and forwards) through the same chain, returning the Message-ID, accepted/rejected recipients, and which transport sent it.
  • A resolver that throws is logged (addresses masked) and skipped, so a broken provider account falls back to SMTP instead of stopping mail.
  • Resolver ids are trimmed on both register and unregister, so a padded id can be removed cleanly.
  • Tests cover registry ordering, fallback, and the trimmed-id fix.