Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Mail Laravel Package

jeffersongoncalves/laravel-mail

Complete email management for Laravel: logging, translated database templates with versioning, webhook delivery tracking for five providers, pixel open/click tracking, suppression lists, inline CSS, statistics, and retries.

View on GitHub
Deep Wiki
Context7

Getting Started

Minimal Setup

  1. Installation:

    composer require jeffersongoncalves/laravel-mail
    php artisan vendor:publish --tag="laravel-mail-migrations"
    php artisan migrate
    

    Upgrading to 1.4.0: re-publish the migrations tag so the new add_tags_to_mail_logs_table migration is copied into your app, then php artisan migrate. Previously published migrations are not updated automatically.

  2. First Use Case: Send a logged email

    Mail::to('[email protected]')->send(new WelcomeMail($user));
    
    • Verify logs in mail_logs table.

Key Configuration

  • Publish config:
    php artisan vendor:publish --tag="laravel-mail-config"
    
  • Enable tracking (webhooks/pixels) in .env:
    LARAVEL_MAIL_TRACKING_ENABLED=true
    LARAVEL_MAIL_PIXEL_OPEN_TRACKING=true
    
  • Enable campaign reporting (1.4.0+) in config/laravel-mail.php:
    'campaigns' => [
        'enabled' => true,
    ],
    
    Without this flag, tags are not persisted on mail_logs and campaign reports will be empty.

First Template

// Create template via Tinker or migration
MailTemplate::create([
    'key' => 'welcome',
    'subject' => ['en' => 'Welcome!'],
    'html_body' => ['en' => '<h1>Hello!</h1>'],
]);

First Campaign Report (1.4.0+)

php artisan mail:campaign welcome

Requires that sent mail was tagged (welcome in this example) and that campaigns are enabled.


Implementation Patterns

1. Database-Driven Templates

Workflow:

  1. Store templates in mail_templates table (supports spatie/laravel-translatable).
  2. Extend TemplateMailable:
    class WelcomeEmail extends TemplateMailable {
        public function templateKey(): string { return 'welcome'; }
        public function templateData(): array { return ['name' => $this->user->name]; }
    }
    
  3. Send via Mail::to()->send(new WelcomeEmail($user)).

Integration Tip:

  • Use PreviewTemplateAction for admin previews:
    $preview = app(PreviewTemplateAction::class)->execute($template, ['name' => 'Alice']);
    

1.4.0 change:

  • Text bodies are now set on the message directly instead of being passed to text() as a view name. Plain-text parts defined in mail_templates.text_body render as-is — remove any workaround view files named after template keys.

2. Tracking Events

Workflow:

  1. Configure provider webhooks in config/laravel-mail.php (e.g., sendgrid.signing_secret).
  2. Listen to events:
    Event::listen(MailBounced::class, function ($event) {
        $event->mailLog->suppress(); // Auto-suppress bounces
    });
    
  3. Verify webhook URLs in provider dashboards (e.g., https://app.com/webhooks/mail/sendgrid).

Integration Tip:

  • Use MailLog::find($id)->trackingEvents to query historical events.
  • Campaign reports reuse the same delivered/bounced/complained/open/click data — keep webhooks and pixel tracking enabled if you want those columns populated rather than zeroed.

3. Pixel Tracking

Workflow:

  1. Enable in config:
    'pixel' => [
        'open_tracking' => true,
        'click_tracking' => true,
    ],
    
  2. Add routes (auto-registered via RouteServiceProvider):
    Route::mailTracking(); // Mounts /mail/t/pixel and /mail/t/click
    
  3. Test by sending an email and checking logs for opened/clicked events.

Integration Tip:

  • Customize pixel routes/middleware in config:
    'pixel' => [
        'route_prefix' => 'track',
        'route_middleware' => ['throttle:60'],
    ],
    

4. Retry Mechanism

Workflow:

  1. Mark failed emails as failed (status: failed).
  2. Retry via Artisan:
    php artisan mail:retry --attempts=3
    
  3. Or programmatically:
    $log = MailLog::where('status', 'failed')->first();
    $log->retry();
    

Integration Tip:

  • Use MailLog::failed()->withRetryAttemptsLessThan(3) to query retryable emails.

5. Notification Channel

Workflow:

  1. Extend TemplateMailable and use with Notification:
    $user->notify(new WelcomeNotification($user));
    
  2. Configure in config/notifications.php:
    'channels' => [
        'mail' => [
            'transport' => 'laravel-mail',
        ],
    ],
    

Integration Tip:

  • Override via() in your notification:
    public function via($notifiable) {
        return ['laravel-mail'];
    }
    

6. Campaign Reports (1.4.0+)

Workflow:

  1. Enable campaigns and run the new migration (see Gotchas).
  2. Tag every mailable that belongs to a campaign — either on the mailable or via the Envelope:
    // On the mailable
    $mailable = (new WelcomeEmail($user))->tag('welcome');
    
    // Or via the Envelope
    public function envelope(): Envelope
    {
        return new Envelope(
            subject: 'Welcome!',
            tags: ['welcome', 'onboarding'],
        );
    }
    
  3. Send as usual — tags are written onto the mail_logs row.
  4. Report from the CLI:
    php artisan mail:campaign welcome
    
    Output covers sent, delivered, bounced, complained, distinct opens and clicks, open / click / click-to-open rates, and the most clicked links for that campaign.

Integration Tip:

  • Use CampaignReport for admin dashboards so you get the same numbers as the Artisan command instead of re-aggregating trackingEvents yourself.
  • Tag consistently (e.g. one stable tag per campaign, not per send) — the report is keyed off the tag.
  • Multiple tags per mailable are allowed, so a welcome + onboarding email shows up in both reports; distinct-open counts keep the numbers honest.

Gotchas and Tips

Pitfalls

  1. Missing tags migration after upgrade (1.4.0):

    • Issue: Campaign reports return zeros because mail_logs has no tags column.
    • Fix: Re-publish --tag="laravel-mail-migrations", confirm the new add_tags_to_mail_logs_table file landed, then php artisan migrate.
    • Note: Publishing overwrites files with the same name — diff before committing if you've edited any published migration.
  2. Tags silently dropped when campaigns are disabled:

    • Issue: tag() / Envelope tags are only persisted when laravel-mail.campaigns.enabled is true.
    • Fix: Enable the flag before the campaign starts. Historical mail sent while it was off cannot be back-tagged.
  3. Webhook Validation Failures:

    • Issue: Webhook payloads rejected due to missing signing secrets.
    • Fix: Set LARAVEL_MAIL_[PROVIDER]_SIGNING_SECRET in .env (e.g., LARAVEL_MAIL_SENDGRID_SIGNING_SECRET).
    • Debug: Check mail_webhook_attempts table for failed attempts.
  4. Pixel Tracking Blocked:

    • Issue: Open/click tracking fails if images/links are blocked (e.g., Gmail’s "Download Images").
    • Fix: Use provider webhooks (LARAVEL_MAIL_TRACKING_ENABLED=true) as a fallback.
  5. Template Versioning Overhead:

    • Issue: Frequent template updates create many mail_template_versions.
    • Fix: Disable versioning for non-critical templates via:
      $template->update(['versioned' => false]);
      
  6. CSS Inlining Performance:

    • Issue: Large HTML templates cause slow inline CSS processing.
    • Fix: Disable for non-critical emails:
      LARAVEL_MAIL_INLINE_CSS=false
      
  7. Suppression List Conflicts:

    • Issue: Accidental suppression of valid addresses.
    • Fix: Audit suppressed emails via:
      MailSuppression::with('mailLog')->get();
      
  8. Plain-text body regression check (1.4.0):

    • Issue: The text() view-name change alters how text_body renders; templates relying on the old behaviour may now output raw Blade.
    • Fix: Re-send a test of each template with a text_body and inspect the plain-text part.

Debugging Tips

  1. Campaign Numbers Look Wrong:

    • Confirm the tag exists on the log row: MailLog::where('tags', 'like', '%welcome%')->count().
    • Compare against trackingEvents for the same logs — a mismatch means webhooks/pixels aren't landing.
    • Remember "distinct opens" dedupes per recipient, so it will be lower than raw opened event counts.
  2. Webhook Debugging:

    • Enable logging for webhook events:
      LARAVEL_MAIL_TRACKING_LOG_WEBHOOKS=true
      
    • Check mail_webhook_attempts table for payloads.
  3. Pixel Tracking:

    • Verify pixel URLs in sent emails (search for src="/mail/t/pixel/).
    • Test with curl:
      curl -v "https://app.com/mail/t/pixel/123?sig=abc"
      
  4. Template Rendering:

    • Preview templates with:
      dd(app(PreviewTemplateAction::class)->execute($template, $data));
      
    • Check for Blade syntax errors in mail_template_versions.
  5. Attachment Storage:

    • Ensure LARAVEL_MAIL_ATTACHMENT_DISK is configured if storing attachments:
      LARAVEL_MAIL_ATTACHMENT_DISK=s3
      

Extension Points

  1. Campaign Reporting:

    • Wrap CampaignReport in your own dashboard/controller and cache the output — aggregation over large mail_logs tables is not free.
    • php artisan mail:campaign [tag] is a good fit for scheduled snapshots; pipe it into Slack/email for weekly campaign digests.
  2. Custom Tracking Events:

    • Extend MailTrackingEvent model or create custom events by listening to MailLogUpdated:
      Event::listen(MailLogUpdated::class, function ($event) {
          if ($event->mailLog->status === 'custom_status') {
              // Dispatch custom event
          }
      });
      
  3. Override Models:

    • Publish and extend models:
      php artisan vendor:publish --tag="laravel-mail-models"
      
    • Example: Custom MailLog fields:
      class CustomMailLog extends MailLog {
          protected $casts = [
              'custom_field' => 'string',
          ];
      }
      
    • If you override MailLog, keep the new tags attribute fillable/castable or campaign reports break.
  4. Custom Webhook Handlers:

    • Override WebhookHandler service provider:
      class CustomWebhookServiceProvider extends WebhookServiceProvider {
          protected function registerHandlers() {
              $this->app->bind(WebhookHandler::class, CustomWebhookHandler::class);
          }
      }
      
  5. Multi-Tenant Scoping:

    • Enable in config:
      'multi_tenant' => [
          'enabled' => true,
          'tenant_model' => \App\Models\Tenant::class,
          'tenant_key' => 'id',
      ],
      
    • Use MailLog::forTenant($tenant) for scoped queries.
    • Campaign reports are tag-scoped, not tenant-scoped — apply tenant scoping yourself if the same tag is used across tenants.

Configuration Quirks

  1. Attachment Storage:

    • If LARAVEL_MAIL_ATTACHMENT_DISK is set, attachments are stored in mail_attachments table with files on the specified disk.
    • Note: Large attachments may bloat the database. Use S3 for scalability.
  2. Pixel Signing Key:

    • Defaults to APP_KEY if LARAVEL_MAIL_PIXEL_SIGNING_KEY is empty.
    • Security: Use a dedicated key for production to avoid exposing APP_KEY.
  3. Template Layouts:

    • Global layout in config/laravel-mail.php:
      'templates' => [
          'default_layout' => '<html><body>{!! $slot !!}</body></html>',
      ],
      
    • Override per template:
      $template->update(['layout' => '<div>{!! $slot !!}</div>']);
      
  4. Unsubscribe Headers:

    • Dynamic URLs (e.g., {email}) are replaced at send time.
    • Verify the final header on a real message before shipping — a malformed URL is a deliverability problem, not a rendering one.
  5. Campaign Config Scope:

    • laravel-mail.campaigns.enabled gates tag persistence only; tracking (webhooks/pixels) still needs its own flags. Enabling campaigns does not turn on open/click collection, so reports can show sends with zero engagement if tracking is off.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
elmogy/larafast
xeviant/laravel-paystack
gorilladash/laravel-website-sdk
martialbe/flysystem-oss
chondal/3312client
jeroen-g/flickr
opscale-co/nova-genbi
itsjustvita/laravel-retention
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator
equipc/laravel-email-database-log
adedsec/nova-translated-labels