topoff / laravel-messenger
Manages mail templates and mail sending in Laravel
Requires
- php: ^8.3
- aws/aws-sdk-php: ^3.344
- illuminate/contracts: ^11.0||^12.0
- spatie/laravel-package-tools: ^1.16
Requires (Dev)
- larastan/larastan: ^3.0
- laravel/boost: ^2.1
- laravel/pint: ^1.14
- nunomaduro/collision: ^8.8
- orchestra/testbench: ^10.0.0||^9.0.0
- pestphp/pest: ^4.0
- pestphp/pest-plugin-arch: ^4.0
- pestphp/pest-plugin-laravel: ^4.0
- phpstan/extension-installer: ^1.4
- phpstan/phpstan-deprecation-rules: ^2.0
- phpstan/phpstan-phpunit: ^2.0
- rector/rector: ^2.3
- spatie/laravel-ray: ^1.35
- dev-master
- v6.1.0
- v6.0.2
- v6.0.1
- v6.0.0
- v5.0.0
- v4.4.0
- v4.3.3
- v4.3.2
- v4.3.1
- v4.3.0
- v4.2.2
- v4.2.1
- v4.2.0
- v4.1.3
- v4.1.2
- v4.1.1
- v4.1.0
- v4.0.0
- v3.2.2
- v3.2.1
- v3.2.0
- v3.1.6
- v3.1.5
- v3.1.4
- v3.1.3
- v3.1.2
- v3.1.1
- v3.1.0
- v3.0.2
- v3.0.1
- v3.0.0
- v2.4.4
- v2.4.3
- v2.4.2
- v2.4.1
- v2.4.0
- v2.3.7
- v2.3.6
- v2.3.5
- v2.3.4
- v2.3.3
- v2.3.2
- v2.3.1
- v2.3.0
- v2.2.4
- v2.2.3
- v2.2.2
- v2.2.1
- v2.2.0
- v2.1.0
- v2.0.0
- v1.9.2
- v1.9.1
- v1.9.0
- v1.8.0
- v1.7.0
- v1.6.0
- v1.5.0
- v1.4.0
- v1.3.0
- v1.2.1
- v1.2.0
- v1.1.2
- v1.1.1
- v1.1.0
- v1.0.0
This package is auto-updated.
Last update: 2026-03-04 11:04:16 UTC
README
Template-driven message sending for Laravel with SES/SNS tracking (opens, clicks, delivery, bounce, complaint), automatic retries with exponential backoff, and Nova integration.
Installation
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
Core Concepts
Models
Message — represents a single outgoing message (email or notification):
- Polymorphic relations:
receiver,sender,messagable(MorphTo),messageType(BelongsTo) - Status timestamps:
scheduled_at,reserved_at,error_at,sent_at,failed_at - Tracking fields:
tracking_hash,tracking_message_id,tracking_opens,tracking_clicks,tracking_opened_at,tracking_clicked_at,tracking_content - Error fields:
error_code,error_message,attempts - SoftDeletes enabled
MessageType — defines how a message is sent:
channel(mail/vonage),notification_class,single_handler,bulk_handler,directflag- Per-type config:
dev_bcc,error_stop_send_minutes,max_retry_attempts(default: 10),configuration_set - Cached via
MessageTypeRepository(30-day TTL,messageTypecache tag)
Contracts
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.
Usage
Creating Messages
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();
Scheduling SendMessageJob
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();
Sending Flow
- MessageService — fluent builder, persists a
Messagerecord - SendMessageJob — picks up pending messages in chunks of 250, routes to single or bulk handler
- MainMailHandler — single message: reserve -> send -> mark sent
- MainBulkMailHandler — groups messages by receiver -> sends
BulkMail
Retry Mechanism
Failed 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.
Permanent Failure Detection
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) |
Tracking
Open & Click Tracking
When enabled, the MailTracker listener hooks into MessageSending:
- Injects a 1x1 tracking pixel (
<img>) - Rewrites links to signed tracking URLs
- Injects
X-SES-CONFIGURATION-SETandX-SES-MESSAGE-TAGSheaders - On
MessageSent: captures the SES message ID from response headers - Respects
X-No-Trackheader to skip tracking
Config keys:
'tracking' => [ 'inject_pixel' => true, 'track_links' => true, 'log_content' => true, // store rendered HTML 'log_content_strategy' => 'database', // or 'filesystem' ],
Tracking Routes
| 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 Event Processing
SNS notifications are dispatched to dedicated jobs:
| Event | Job | Effect |
|---|---|---|
| Delivery | RecordDeliveryJob |
Sets success: true, delivered_at |
| Bounce | RecordBounceJob |
Sets success: false, dispatches Permanent/Transient event |
| Complaint | RecordComplaintJob |
Sets complaint: true, success: false |
| Reject | RecordRejectJob |
Sets success: false, failed_at (permanent) |
| Open | RecordOpenJob |
Increments opens, sets tracking_opened_at |
| Click | RecordLinkClickJob |
Increments clicks, sets tracking_clicked_at |
BCC Recipient Filtering
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.
Events
MessageOpenedEvent,MessageLinkClickedEvent— user interactionMessageDeliveredEvent,MessagePermanentBouncedEvent,MessageTransientBouncedEvent— delivery statusMessageComplaintEvent,MessageRejectedEvent— negative outcomesSesSnsWebhookReceivedEvent— raw SNS webhook payload
Listeners
| 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) |
SES/SNS Auto Setup
The package can provision all required AWS SES/SNS resources:
- SES Configuration Set + Event Destination (SNS)
- SNS Topic + HTTPS subscription
- SES identities with DKIM + MAIL FROM domains
Enable in config:
'ses_sns' => [ 'enabled' => true, ],
Artisan Commands
| 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) |
Automatic Cleanup
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 * * *', ], ],
Nova Integration
When Laravel Nova is installed, the package provides:
Resources: Message (full CRUD with tracking fields), MessageType, EmailLog, NotificationLog
Actions:
- Resend failed/errored message as new copy
- Preview rendered HTML of sent messages (signed URL)
- Preview message type templates
- Compose ad-hoc custom emails with markdown editor
- Send SMS/email notifications via AnonymousNotifiable
- Open SES/SNS dashboard
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, ], ],
Configuration Reference
| 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 |
Development
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.
Changelog
Please see CHANGELOG for more information on what has changed recently.
Contributing
Please see CONTRIBUTING for details.
Security Vulnerabilities
Please review our security policy on how to report security vulnerabilities.
Credits
License
The MIT License (MIT). Please see License File for more information.