Email Studio — Improvement & Growth Plan
Package: capell-app/email-studio · Kind: package · Tier: premium · Product group: Capell Communications · Bundle: communications · Status: Complete
1. Snapshot
Section titled “1. Snapshot”Email Studio is the transactional-email backbone for Capell: it registers reusable templates (with locale/site variants), resolves a delivery profile, renders declared {{ variable }} placeholders into a stored snapshot, applies suppression checks, and dispatches a queued SendEmailJob that calls a provider adapter to deliver per-recipient and record outcomes. Provider event webhooks write delivery audit events back into email_events, while the MailTracker-backed admin surface exposes sent-email records, opens, clicks, stored HTML previews, and retention controls. One-click unsubscribe, inbound replies, and native template/profile authoring remain future product depth outside the completed current manifest scope.
- Surfaces (declared):
admin,frontend,console— in code:routes/web.phpexposes tokenized provider-event ingestion, MailTracker owns open/click tracking routes,AdminServiceProviderowns the read-only sent-email admin resource plus settings management, and the queue/console path owns delivery plus retention. - Key Actions:
SendEmailAction,DeliverEmailMessageAction,RenderEmailTemplateAction,ResolveEmailTemplateVariantAction,RegisterEmailTemplateAction,CheckEmailSuppressionAction,SuppressEmailAddressAction(src/Actions/). - Models/tables (10 + 1 FK migration):
email_profiles,email_templates,email_template_variants,email_messages,email_recipients,email_events,email_replies,email_suppressions,email_template_registrations,email_tracking_tokens(database/migrations/2026_05_10_190847_*, plus2026_05_21_000001_add_site_foreign_keys_to_email_studio_tables). - Support/infra:
EmailVariableRenderer,EmailProfileResolver,EmailProviderRegistry,EmailTemplateRegistry,EmailAddressNormalizer; provider adaptersSmtpEmailProviderAdapter,PostmarkEmailProviderAdapter(extends SMTP),FakeEmailProviderAdapter; contractEmailProviderAdapter. - Dependencies: requires
capell-app/admin,capell-app/core,capell-app/frontend; supportscapell-app/campaign-studio,capell-app/form-builder,capell-app/insights; conflicts none. - Declared capabilities:
delivery-audit,mail-tracker-admin,mail-tracker-open-click-tracking,mail-tracker-retention,provider-event-ingestion,provider-normalization-foundation,provider-adapters,suppressions,template-rendering,transactional-email. Reply ingestion, native token tracking, and one-click unsubscribe are not listed as shipped capabilities. - Marketplace summary (verbatim): “Email Studio is Capell’s transactional-email engine: site-scoped reusable templates with safe {{ variable }} rendering, pluggable delivery profiles and provider adapters, suppression enforcement, and a queued, auditable per-recipient send pipeline.”
- Screenshots:
capell.jsonpromotes the extension card plus four runner-backed sent-email admin captures.docs/screenshots.jsondeclares sent email index/detail and settings-surface capture targets for the Capell screenshot runner.
2. Improvements (existing functionality)
Section titled “2. Improvements (existing functionality)”- Done/Shipped: Wire
EmailStudioHealthCheckto actually run the 4 declared checks. Four diagnostics probe template rendering, delivery adapters, suppressions, and provider event/reply normalization foundations with focused pass/fail coverage. —src/Health/EmailStudioHealthCheck.php,tests/Feature/Health/EmailStudioHealthCheckTest.php— M - Done/Shipped: Persist real Symfony transport message IDs for SMTP/Postmark-over-Symfony sends.
SmtpEmailProviderAdapter::send()now reads the returned Laravel/SymfonySentMessage::getMessageId()and maps that provider transport ID onto every delivered recipient instead of fabricatingsmtp-{message}-{recipient}IDs.PostmarkEmailProviderAdapterinherits the same behavior through its Symfony mailer path. The fake adapter remains deterministic for local/test delivery only. —src/Support/Providers/SmtpEmailProviderAdapter.php,tests/Fixtures/CapturingEmailStudioMailer.php,tests/Unit/EmailStudioDataAndSupportCoverageTest.php— M - Done/Shipped:
SendEmailJobretry/backoff/timeout policy. The queued job now has retry/backoff/timeout handling plus a terminalfailed()path, and retryable provider exceptions rethrow for the queue instead of immediately marking every recipient permanently failed. —src/Jobs/SendEmailJob.php,src/Actions/DeliverEmailMessageAction.php— M - Done/Shipped: Distinguish transient vs permanent provider failures. Retryable email delivery exceptions now leave the queue in control; permanent provider failures still record recipient/message outcomes through the delivery Action. —
src/Actions/DeliverEmailMessageAction.php,src/Exceptions/RetryableEmailDeliveryException.php— M - Shipped 2026-06-05: Batch recipient status writes in the delivery loop.
markNewSuppressionsnow batches newly suppressed recipients, provider failures update by failure-reason bucket, successful recipient provider IDs write through a single upsert, and message status is derived from known delivery counts instead of re-plucking every recipient status. ManifestadminQueryBudgetremains 40. —src/Actions/DeliverEmailMessageAction.php— Done - Done/Shipped:
CheckEmailSuppressionActionresolves through the container.DeliverEmailMessageActionnow callsresolve(CheckEmailSuppressionAction::class)->handle(...), preserving the Action boundary and keeping the suppression check fakeable in tests. —src/Actions/DeliverEmailMessageAction.php— S - Done/Shipped: Body retention is enforced.
PruneEmailBodiesActionandcapell-email-studio:prune-bodiesclear stored rendered HTML/text after the configured retention window, and the package schedules the command daily. —config/capell-email-studio.php,src/Actions/PruneEmailBodiesAction.php,src/Console/Commands/PruneEmailBodiesCommand.php— M - Done/Shipped: MailTracker open/click tracking is active and documented. MailTracker injects the tracking pixel and rewrites links according to
EmailStudioSettings; the package disables MailTracker’s bundled Blade admin route and exposes records through Capell’s Filament sent-email resource. Legacy top-leveltrack_opens/track_clickskeys are documented as compatibility flags. —src/Actions/ApplyMailTrackerSettingsAction.php,src/Filament/Resources/SentEmails/SentEmailResource.php,docs/templates-and-providers.md— L PostmarkEmailProviderAdapteris SMTP-in-disguise — it only overridesmailerName()to'postmark'; it does not use the Postmark API, capture Postmark message IDs, or verify Postmark webhook signatures. Buyers reading “provider adapters” will expect a real Postmark integration. Either build it on the Postmark HTTP API or rename it to clarify it is “Postmark-over-Symfony-mailer”. —src/Support/Providers/PostmarkEmailProviderAdapter.php— MEmailProfileResolverdefault-provider ordering is partly inert — it orders byprovider = default_providerbut still requiresis_default = true; if multiple defaults exist across scopes the secondary ordering rarely matters, and there’s no guard against >1is_defaultper scope. Add a partial unique index or resolver tie-break test. —src/Support/EmailProfileResolver.php— S- Locale fallback is opaque —
ResolveEmailTemplateVariantActionresolves “requested or current locale before fallbacks” but the fallback chain isn’t documented or configurable (no default-locale config key). Document and make the fallback locale a config value. —src/Actions/ResolveEmailTemplateVariantAction.php— S
3. Missing Features (gaps)
Section titled “3. Missing Features (gaps)”- Done/Shipped: provider webhook + event ingestion.
POST /mail/provider-events/{token}resolvesEmailProfile.webhook_endpoint_token_hash, validatesX-Capell-Email-Studio-Signaturewhenprovider_settings.webhook_secretis configured, normalizes payloads through the profile provider adapter, writes idempotentEmailEventrows throughRecordProviderEventAction, and updates matching recipient statuses/timestamps for sent/delivered/bounced/complained/opened/clicked/replied/failed events. —routes/web.php,src/Http/Controllers/ProviderWebhookController.php,src/Actions/RecordProviderEventAction.php,src/Actions/ResolveProviderWebhookProfileAction.php,src/Actions/ValidateProviderWebhookSignatureAction.php,tests/Feature/ProviderWebhookControllerTest.php - Inbound reply ingestion.
email_replies,EmailReply,InboundEmailReplyData, andnormalizeInboundReply()exist but are never wired to a route/action. “Replies” in the summary is currently fictional. — table-stakes. - Done/Shipped: MailTracker open/click tracking endpoints. The package configures MailTracker’s pixel and signed redirect tracking routes, maps MailTracker to package-owned
SentEmail/SentEmailUrlClickedmodels, and exposes the tracked records in Capell admin. Theemail_tracking_tokenstable remains reserved for future native Email Studio message tracking. — table stakes closed for current manifest scope. - Unsubscribe / suppression self-service + List-Unsubscribe header. Suppressions can only be created via
SuppressEmailAddressActionserver-side. No public one-click unsubscribe route, no signed unsubscribe token, noList-Unsubscribe/List-Unsubscribe-Postheaders on outbound mail. This is now effectively mandatory for bulk senders (Gmail/Yahoo 2024 rules). — table-stakes. - Done/Shipped: Admin sent-email surface.
SentEmailResourceprovides a read-only Capell Filament table and detail page for MailTracker sent-email records, opens, clicks, headers, stored HTML previews, and tracked URL click rows;EmailStudioSettingsSchemamanages tracking, content storage, queue, and retention settings. Native template/profile authoring remains future depth outside the current manifest scope. — table-stakes for delivery visibility closed. - Done/Shipped: bounce/complaint handling loop.
RecordProviderEventActionnow callsSuppressEmailAddressActionfor bounced and complained provider events, creating site-scoped provider-event suppressions for the affected recipient after the event is recorded. —src/Actions/RecordProviderEventAction.php,tests/Feature/ProviderWebhookControllerTest.php - Scheduling / send-at.
SendEmailDatahas nosendAt;SendEmailJobdispatches immediately. No delayed/scheduled transactional sends. — differentiator. - A/B testing of variants.
email_template_variantsexists and could host A/B, butResolveEmailTemplateVariantActiononly picks by site+locale+active, with no split assignment, weighting, or winner tracking. — differentiator. - Done/Shipped: Delivery/open/click visibility. The sent-email admin resource surfaces MailTracker opens, clicks, clicked URLs, and stored content state. Deeper aggregate delivery dashboards and Insights feed remain future reporting depth. — differentiator.
- Idempotency keys for sends. No dedupe on
(templateKey, triggeredByType, triggeredById)— a double-fired listener sends twice. — table-stakes for transactional reliability. - Attachments & inline images.
SendEmailData/adapters support neither attachments nor embedded images. — table-stakes for many transactional emails (receipts, tickets). - Done/Shipped: webhook endpoint verification. Provider events require a per-profile token whose SHA-256 hash is stored on
email_profiles.webhook_endpoint_token_hash; profiles can also setprovider_settings.webhook_secretto requireX-Capell-Email-Studio-SignatureHMAC validation over the raw request body. Provider-specific timestamp tolerance remains future depth for real HTTP adapters such as Postmark API webhooks;webhook_tolerance_secondsis still unused. — table-stakes (security).
4. Issues / Risks
Section titled “4. Issues / Risks”- Closed: Health checks are functional.
EmailStudioHealthChecknow probes the declared template-rendering, provider-delivery, suppression, and provider-event foundation checks with focused coverage. —src/Health/EmailStudioHealthCheck.php,tests/Feature/Health/EmailStudioHealthCheckTest.php. - Closed: Manifest surfaces align with shipped behavior.
adminis backed by the sent-email resource and settings surface,frontendis backed by tokenized provider webhooks plus MailTracker tracking routes, andconsoleis backed by delivery/retention commands. The manifest still avoids advertising replies, one-click unsubscribe, or native Email Studio template/profile authoring as shipped. —capell.json. - Dead schema + Data objects = maintenance debt is shrinking.
EmailEventandProviderWebhookEventDatanow have a real write path through provider-event ingestion.EmailReply,EmailTrackingToken, andInboundEmailReplyDatastill await consuming routes/actions. —src/Models/EmailEvent.php,src/Models/EmailReply.php,src/Models/EmailTrackingToken.php. - Closed: Queue resilience gap.
SendEmailJobnow owns retry/backoff/timeout behavior and terminal failure recording, while retryable delivery exceptions rethrow for the queue. —src/Jobs/SendEmailJob.php,src/Actions/DeliverEmailMessageAction.php. - Closed: Multi-recipient delivery write amplification reduced. Recipient statuses now update by suppression/failure bucket and provider IDs write through a single upsert, keeping the current manifest query budget realistic for the package’s supported send path. —
src/Actions/DeliverEmailMessageAction.php. - Closed: Rendered-body retention is enforced.
PruneEmailBodiesActionand the scheduled prune command clear stored rendered bodies according tobody_retention_days. —src/Actions/PruneEmailBodiesAction.php,src/Console/Commands/PruneEmailBodiesCommand.php. - Suppression hashing is unsalted SHA-256.
EmailAddressNormalizer::hash()ishash('sha256', lowercased-email). Email address space is enumerable, so a leakedemail_suppressionstable is trivially reversible via rainbow tables. Consider a keyed HMAC (app-key-derived) if these hashes are meant to protect addresses. —src/Support/EmailAddressNormalizer.php. - Public-output safety. Provider webhook routes return JSON/no-content responses only, MailTracker tracking routes do not render Email Studio admin HTML, and the package still has no public Blade surface. Future unsubscribe/reply routes must keep the same no-admin-output boundary. —
routes/web.php. - Reply test coverage is still thin relative to future claims. Tests cover send/deliver/suppression/render/resolve/migrations/relationships and provider-event ingestion.
normalizeInboundReplyis still only asserted as a pure transformer because inbound reply ingestion has not shipped. —tests/Unit/EmailStudioDataAndSupportCoverageTest.php. - i18n half-done. Enum labels resolve through
__('capell-email-studio::generic.statuses.*')(good), but there is no per-recipient/per-message language column on the message itself and locale fallback is undocumented. Translations only shipen. —src/Enums/EmailMessageStatus.php,resources/lang/en/. - Release notes.
CHANGELOG.mdshould keep tracking future product-depth slices, but the current implementation plan is closed by code, manifest, and test evidence.
Test coverage map (what IS covered):
- Unit: template registration upsert; variable render (escaping + missing-variable modes, preview vs production); variant resolution (site/locale/fallback); Data/enum/normalizer coverage; SMTP delivery happy-path + provider-id mapping; adapter registry + health metadata; registry persistence pre/post migration; service-provider metadata.
- Integration: deliver (suppression recheck, double-claim guard, stale-claim reclaim); suppression check by hash+scope; send (queued send + cross-site rejection); migrations create tables; model relationships.
- What ISN’T covered: inbound reply ingestion, one-click unsubscribe flow, Postmark HTTP API behavior, idempotency/dedupe, scheduled sends, attachments, native template/profile authoring, and aggregate delivery dashboards. These are future depth rather than current manifest blockers.
5. Marketplace & Selling
Section titled “5. Marketplace & Selling”Critique. The capell.json marketplace summary and composer description now scope to what ships today: safe template rendering, delivery profiles and adapters, suppression enforcement, queued delivery, per-recipient send audit, provider event ingestion, and MailTracker-backed sent-email admin/tracking. Keep the guard test in place so replies and unsubscribe do not reappear in buyer-facing metadata until those flows ship.
Improved 1-sentence summary (honest to current code):
Email Studio is Capell’s transactional-email engine: site-scoped reusable templates with safe
{{ variable }}rendering, pluggable delivery profiles and provider adapters, suppression enforcement, and a queued, auditable per-recipient send pipeline.
Improved 3–4 sentence description:
Email Studio gives every Capell site a reliable transactional-email core. Define reusable, locale- and site-scoped templates that render through a deliberately safe placeholder engine (HTML-escaped, missing-variable-strict in production), then send through delivery profiles backed by pluggable provider adapters. Every send is rendered to an immutable snapshot, screened against site and global suppression lists, and delivered via a queue job that records per-recipient outcomes for support and audit. Provider event webhooks write delivery events back into the audit trail, while the MailTracker-backed admin surface exposes sent-email records, opens, clicks, stored HTML, and retention controls.
Screenshot/media status. capell.json promotes the extension card plus four runner-backed sent-email admin captures. docs/screenshots.json declares sent email index/detail and settings-surface capture targets; the settings target remains runner evidence until the shared settings page reliably exposes the email_studio group.
Pricing/tier/bundle positioning. premium tier in the communications bundle is now defensible for the current scope: operators get the send pipeline, provider event ingestion, suppression enforcement, sent-email admin visibility, open/click tracking, and retention controls. Cross-sell is strong: it already supports campaign-studio (bulk/marketing sends can reuse templates, profiles, suppressions), form-builder (the docs’ canonical example is forms.confirmation), and insights (future aggregate delivery/open/click metrics). Position Email Studio as the shared sending substrate that those three plug into.
Top differentiators / value props / target buyer.
- Differentiators: site-scoped (
globalvssite:N) templates+suppressions+profiles in one model; immutable rendered snapshot for support reproducibility; provider-agnostic adapter contract; safe-by-default render engine (smaller than Blade, no template injection). - Target buyer: Capell agencies/operators running multi-site installs who need branded transactional mail (form confirmations, account/order notifications) with auditability and per-site control — without bolting on a third-party ESP SDK per project.
8–12 keywords/tags: transactional-email, email-templates, email-suppressions, delivery-profiles, provider-adapters, smtp, postmark, email-audit-log, multi-site-email, queued-email, unsubscribe, bounce-handling.
Completed 2026-06-08. Every current manifest-backed roadmap row is closed: Email Studio now ships functional health checks, retry-aware queued delivery, provider message IDs, body-retention pruning, batched recipient outcome writes, provider webhook ingestion with optional HMAC validation, hard-bounce/complaint suppression, MailTracker open/click tracking, a read-only sent-email admin resource, settings/retention controls, and runner-backed Marketplace media. One-click unsubscribe, inbound replies, Postmark HTTP API depth, scheduled sends, attachments, native template/profile authoring, and aggregate delivery dashboards remain future product-depth candidates rather than active completion blockers.
6. Prioritized Roadmap
Section titled “6. Prioritized Roadmap”| Item | Bucket | Effort | Impact | Section ref |
|---|---|---|---|---|
| ✅ Shipped 2026-06-04: Align manifest/composer summary + capabilities to shipped reality. Evidence: manifest/composer/README/docs copy now sell the shipped send/audit pipeline and provider normalization foundations, with tests guarding against replies/events/webhooks/tracking/unsubscribe being advertised as done. | Done | S | High | §5, §4 |
✅ Shipped 2026-06-04: Implement EmailStudioHealthCheck to run the 4 declared checks. Evidence: four diagnostics now probe template rendering, delivery adapters, suppressions, and provider event/reply normalization with focused pass/fail tests. | Done | M | High | §2, §4 |
✅ Shipped: Add SendEmailJob retry/backoff/timeout + failed(); split transient vs permanent failures | Done | M | High | §2, §4 |
✅ Shipped 2026-06-05: Capture real provider message IDs for SMTP/Postmark-over-Symfony sends. Evidence: SMTP maps SentMessage::getMessageId() to delivered recipients with fixture coverage; fake IDs remain local/test only. | Done | M | High | §2, §3 |
✅ Shipped: Enforce body_retention_days via scheduled prune command (PII) | Done | M | High | §2, §4 |
✅ Shipped 2026-06-05: Batch recipient status writes in DeliverEmailMessageAction (query-budget) | Done | M | Med | §2, §4 |
Done/Shipped: Provider webhook ingestion route + RecordProviderEventAction writes email_events and updates recipient status through tokenized endpoint and optional HMAC verification | Done | L | High | §3, §4 |
One-click unsubscribe: signed token route + List-Unsubscribe headers + self-service suppression | Future | M | High | §3, §4 |
| Done/Shipped: Auto-suppress on hard bounce / complaint once events ingest | Done | M | High | §3 |
| Done/Shipped: MailTracker sent-email admin surface, settings, tracking controls, and runner-backed sent-email screenshots | Done | L | High | §3, §5 |
| Real Postmark adapter (API + message-id + webhook verify) or rename to avoid over-promising | Future | M | Med | §2, §3 |
Inbound reply ingestion route + RecordInboundReplyAction (writes email_replies) | Future | M | Med | §3 |
| Done/Shipped: MailTracker open/click tracking routes, pixel injection, link rewriting, package-owned models, and Capell admin read surface | Done | L | Med | §2, §3 |
Scheduled sends (sendAt) + idempotency keys + attachments/inline images | Future | M | Med | §3 |
| Native template/profile/suppression authoring and aggregate delivery dashboards | Future | L | Med | §3, §5 |