Skip to content

Templates And Providers

Email Studio separates template registration, message creation, delivery, tracking, and provider webhooks. Package code should call Actions and registries rather than creating message rows by hand.

Use EmailTemplateRegistry from a service provider when another package needs a template available to editors.

use Capell\EmailStudio\Support\EmailTemplateRegistry;
$this->app->afterResolving(EmailTemplateRegistry::class, static function (EmailTemplateRegistry $registry): void {
$registry->register(
key: 'access-approved',
name: 'Access approved',
variables: ['name', 'claim_url'],
description: 'Sent when an access request is approved.',
packageName: 'capell-app/access-gate',
);
});

The registry persists its registrations through RegisterEmailTemplateAction. Keep the key stable; editors may already have variants attached to it.

Use SendEmailAction with SendEmailData. It resolves the profile, approved template, matching variant, suppression status, recipients, queue, and rendered body.

use Capell\EmailStudio\Actions\SendEmailAction;
use Capell\EmailStudio\Data\EmailAddressData;
use Capell\EmailStudio\Data\EmailHeaderData;
use Capell\EmailStudio\Data\SendEmailData;
use Spatie\LaravelData\DataCollection;
$message = SendEmailAction::run(new SendEmailData(
templateKey: 'access-approved',
to: EmailAddressData::collect([
new EmailAddressData(email: '[email protected]', name: 'Sam Editor'),
], DataCollection::class),
cc: EmailAddressData::collect([], DataCollection::class),
bcc: EmailAddressData::collect([], DataCollection::class),
siteId: 1,
siteScopeKey: 'global',
emailProfileId: null,
variables: [
'name' => 'Sam Editor',
'claim_url' => 'https://example.test/access/claim/token',
],
headers: EmailHeaderData::collect([], DataCollection::class),
triggeredByType: null,
triggeredById: null,
));

If the constructor shape changes, update this example with the data class.

Provider adapters implement EmailProviderAdapter. They normalize outbound delivery, webhook payloads, and inbound replies.

use Capell\EmailStudio\Contracts\EmailProviderAdapter;
use Capell\EmailStudio\Data\InboundEmailReplyData;
use Capell\EmailStudio\Data\ProviderSendResultData;
use Capell\EmailStudio\Data\ProviderWebhookEventData;
use Capell\EmailStudio\Enums\EmailProviderType;
use Capell\EmailStudio\Models\EmailMessage;
use Capell\EmailStudio\Support\EmailProviderRegistry;
final class DemoEmailProviderAdapter implements EmailProviderAdapter
{
public function send(EmailMessage $message): ProviderSendResultData
{
return new ProviderSendResultData(successful: true);
}
public function normalizeWebhookPayload(array $payload, array $headers = []): ProviderWebhookEventData
{
return new ProviderWebhookEventData(
provider: 'demo',
eventType: (string) ($payload['event'] ?? 'delivered'),
providerMessageId: isset($payload['message_id']) ? (string) $payload['message_id'] : null,
recipientEmail: isset($payload['email']) ? (string) $payload['email'] : null,
payload: $payload,
);
}
public function normalizeInboundReply(array $payload, array $headers = []): InboundEmailReplyData
{
return new InboundEmailReplyData(
provider: 'demo',
providerMessageId: isset($payload['message_id']) ? (string) $payload['message_id'] : null,
fromEmail: (string) ($payload['from_email'] ?? ''),
fromName: isset($payload['from_name']) ? (string) $payload['from_name'] : null,
subject: isset($payload['subject']) ? (string) $payload['subject'] : null,
textBody: isset($payload['text']) ? (string) $payload['text'] : null,
htmlBody: isset($payload['html']) ? (string) $payload['html'] : null,
payload: $payload,
);
}
}
$this->app->afterResolving(EmailProviderRegistry::class, static function (EmailProviderRegistry $registry): void {
$registry->register(EmailProviderType::Fake, new DemoEmailProviderAdapter);
});

Use a new EmailProviderType case before registering a real provider. The Fake case is for local/test behavior.

KeyUse
capell-email-studio.default_providerProvider used when an email profile does not override it.
capell-email-studio.queueQueue used by SendEmailJob. Can be set with CAPELL_EMAIL_STUDIO_QUEUE.
capell-email-studio.track_opensEnables open tracking where the provider supports it.
capell-email-studio.track_clicksEnables click tracking where the provider supports it.
capell-email-studio.body_retention_daysHow long rendered message bodies should be retained.
capell-email-studio.webhook_tolerance_secondsTolerance window for provider webhook validation.
capell-email-studio.public_route_prefixPrefix for tracking and webhook routes. Can be set with CAPELL_EMAIL_STUDIO_PUBLIC_PREFIX.
capell-email-studio.tracking_token_ttl_daysLifetime of tracking tokens.
capell-email-studio.webhook_rate_limitRate limiter name for webhooks.
capell-email-studio.tracking_rate_limitRate limiter name for tracking routes.

Table-name keys are part of install and migration behavior. Document them in migration notes rather than setup prose unless a host app needs custom table names.

Terminal window
vendor/bin/pest packages/email-studio/tests --configuration=phpunit.xml