Manages mail templates and mail sending in Laravel
topoff/laravel-messenger is a Laravel package for manages mail templates and mail sending in laravel.
It currently has 0 GitHub stars and 845 downloads on Packagist (latest version v8.8.3).
Install it with composer require topoff/laravel-messenger.
Discover more Laravel packages by topoff
or browse all Laravel packages to compare alternatives.
Last updated
Template-driven message sending for Laravel with SES/SNS tracking (opens, clicks, delivery, bounce, complaint), automatic retries with exponential backoff, and Nova integration.
composer require topoff/laravel-messenger
Publish the config and migrations:
php artisan vendor:publish --tag="messenger-config"
php artisan vendor:publish --tag="messenger-migrations"
php artisan migrate
Message — represents a single outgoing message (email or notification):
receiver, sender, messagable (MorphTo), messageType (BelongsTo)scheduled_at, reserved_at, error_at, sent_at, failed_attracking_hash, tracking_message_id, tracking_opens, tracking_clicks, tracking_opened_at, tracking_clicked_at, tracking_contenterror_code, error_message, attemptsMessageType — defines how a message is sent:
channel (mail/vonage), notification_class, single_handler, bulk_handler, direct flagdev_bcc, error_stop_send_minutes, max_retry_attempts (default: 10), configuration_setMessageTypeRepository (30-day TTL, messageType cache tag)Your receiver models must implement MessageReceiverInterface:
use Topoff\Messenger\Contracts\MessageReceiverInterface;
class User extends Model implements MessageReceiverInterface
{
public function getEmail(): string { /* ... */ }
public function getResourceUri(): string { /* ... */ }
public function setEmailToInvalid(bool $isManualCall = true): void { /* ... */ }
public function getEmailIsValid(): bool { /* ... */ }
public function preferredLocale(): string { /* ... */ }
}
Mail handlers that support grouping into bulk mails implement GroupableMailTypeInterface.
Use the fluent MessageService builder:
use Topoff\Messenger\Services\MessageService;
$service = app(MessageService::class);
$service
->setSender(User::class, $user->id)
->setReceiver(Company::class, $company->id)
->setMessagable(Lead::class, $lead->id)
->setMessageTypeClass(NewLeadToCustomerMailHandler::class)
->setCompanyId($company->id)
->setScheduled(now()->addMinutes(5))
->setParams(['key' => 'value'])
->setLocale('de')
->create();
The package does not schedule SendMessageJob automatically. You must add it to your application's routes/console.php:
use Topoff\Messenger\Jobs\SendMessageJob;
// Send new messages every minute
Schedule::job(new SendMessageJob, 'messages')
->name(SendMessageJob::class)
->withoutOverlapping()
->everyMinute();
// Retry failed messages every 10 minutes
Schedule::job(new SendMessageJob(isRetryCallForMessagesWithError: true), 'messages')
->everyTenMinutes();
Message recordBulkMailFailed messages are retried with exponential backoff (min(2^(attempts-1) * 15, 960) minutes):
| Attempt | Backoff | |---|---| | 1 | 15 min | | 2 | 30 min | | 3 | 1 hour | | 4 | 2 hours | | 5 | 4 hours | | 6 | 8 hours | | 7+ | 16 hours (capped) |
Retries stop when attempts >= max_retry_attempts, created_at exceeds error_stop_send_minutes, or the message is marked as permanently failed.
These SMTP codes cause immediate permanent failure (failed_at is set, no further retries):
| Code | Meaning | |---|---| | 550 | Mailbox doesn't exist / unroutable | | 553 | Mailbox name not allowed | | 521 | Host does not accept mail | | 556 | Domain does not accept mail | | — | Exception contains "MessageRejected" (SES rejection) |
When enabled, the MailTracker listener hooks into MessageSending:
<img>)X-SES-CONFIGURATION-SET and X-SES-MESSAGE-TAGS headersMessageSent: captures the SES message ID from response headersX-No-Track header to skip trackingConfig keys:
'tracking' => [
'inject_pixel' => true,
'track_links' => true,
'log_content' => true, // store rendered HTML
'log_content_strategy' => 'database', // or 'filesystem'
],
| Method | URI | Purpose |
|---|---|---|
| GET | /email/t/{hash} | Open pixel — returns 1x1 GIF, increments opens |
| GET | /email/n?l=...&h=... | Link click — validates signature, increments clicks, redirects |
| POST | /email/sns | SNS webhook — processes delivery/bounce/complaint/reject events |
Route prefix and middleware are configurable via tracking.route.
SNS notifications are dispatched to dedicated jobs:
| Event | Job | Effect |
|---|---|---|
| Delivery | RecordDeliveryJob | Sets column delivered_at, tracking_meta.success = true, tracking_meta.smtpResponse |
| Bounce | RecordBounceJob | Sets column bounced_at, appends tracking_meta.failures[], dispatches Permanent/Transient event. Never overwrites a previous tracking_meta.success = true (handles SES "accept-then-bounce"). |
| Complaint | RecordComplaintJob | Sets tracking_meta.complaint = true, tracking_meta.success = false |
| Reject | RecordRejectJob | Sets tracking_meta.success = false, failed_at (permanent) |
| Open | RecordOpenJob | Increments opens, sets tracking_opened_at |
| Click | RecordLinkClickJob | Increments clicks, sets tracking_clicked_at |
For SES "accept-then-bounce" (the recipient MTA returns 250 OK and later sends an asynchronous DSN — common with content filters, vacation auto-responders, or forwarding loops), both delivered_at and bounced_at are set on the same row. Query WHERE delivered_at IS NOT NULL AND bounced_at IS NOT NULL to surface the pattern; the Filament resource has a dedicated toggle filter for it.
When BCC is added (via AddBccToEmailsListener), both TO and BCC recipients share the same SES message ID. The SNS event jobs guard against this by comparing event recipient(s) against tracking_recipient_contact. Events for non-matching recipients are skipped. This is case-insensitive and null-safe.
MessageOpenedEvent, MessageLinkClickedEvent — user interactionMessageDeliveredEvent, MessagePermanentBouncedEvent, MessageTransientBouncedEvent — delivery statusMessageComplaintEvent, MessageRejectedEvent — negative outcomesSesSnsWebhookReceivedEvent — raw SNS webhook payload| Listener | Trigger | Purpose |
|---|---|---|
| LogEmailsListener | MessageSent | Logs to email_log table |
| LogNotificationListener | NotificationSent | Logs to notification_log table |
| AddBccToEmailsListener | MessageSending | Adds BCC (respects dev_bcc per MessageType) |
The package can provision all required AWS SES/SNS resources:
Enable in config:
'ses_sns' => [
'enabled' => true,
],
| Command | Purpose |
|---|---|
| messenger:ses-sns:setup-all | Provision all SES identities + SNS tracking in one go |
| messenger:ses-sns:setup-tracking | Set up SNS topic, subscription, config set, event destination |
| messenger:ses-sns:check-tracking | Validate tracking infrastructure health |
| messenger:ses-sns:setup-sending | Set up SES identities with DKIM + MAIL FROM |
| messenger:ses-sns:check-sending | Validate identity verification and DNS records |
| messenger:ses-sns:test-events | Simulate SES events (bounce, complaint, delivery) |
| messenger:ses-sns:teardown | Remove all provisioned resources (requires --force) |
The package schedules CleanupMessengerTablesJob automatically (configurable via cleanup.schedule):
'cleanup' => [
'messages_delete_after_months' => 24,
'email_log_delete_after_months' => 24,
'notification_log_delete_after_months' => 24,
'message_tracking_content_null_after_days' => 60,
'schedule' => [
'enabled' => true,
'cron' => '17 3 * * *',
],
],
When Laravel Nova is installed, the package provides:
Resources: Message (full CRUD with tracking fields), MessageType, EmailLog, NotificationLog
Actions:
Filters: Date range, status, message type, receiver type, messagable type
Lenses: Tracking stats by message type, by recipient domain, per-message details
SES/SNS Dashboard — web UI at /emessenger/nova/ses-sns-dashboard with health checks, DNS records, identity details, AWS Console links, and command buttons.
Config:
'tracking' => [
'nova' => [
'enabled' => true,
'register_resource' => false, // auto-register in Nova
'resource' => \Topoff\Messenger\Nova\Resources\Message::class,
],
],
| Section | Key Settings |
|---|---|
| models.* | Configurable model classes (message, message_type, email_log, notification_log) |
| database.* | Connection name |
| logs.* | Connection, table names for email_log / notification_log |
| cache.* | Tag (messageType), TTL (30 days) |
| cleanup.* | Retention periods, tracking_content nullification, schedule cron |
| mail.* | Bulk mail class/view/subject/url, custom message view |
| sending.* | check_should_send callable, prevent_create_message callable |
| bcc.* | check_should_add_bcc callable |
| tracking.* | Pixel/link injection, route prefix/middleware, Nova config, content storage, SNS topic |
| ses_sns.* | AWS credentials, configuration sets, SNS topic, event types, tenant, Route53 automation |
composer test # Run Pest test suite
composer format # Laravel Pint
composer analyse # PHPStan
composer lint # Pint + PHPStan
composer rector-dry # Preview Rector refactorings
composer rector # Apply Rector refactorings
The package uses Orchestra Testbench. php artisan works in the package root directory.
Please see CHANGELOG for more information on what has changed recently.
Please see CONTRIBUTING for details.
Please review our security policy on how to report security vulnerabilities.
The MIT License (MIT). Please see License File for more information.