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 Scheduler Laravel Package

oneduo/laravel-mail-scheduler

Schedule outgoing Laravel mail with a simple, database-backed approach: store messages with send times, dispatch them via Laravel’s scheduler and queue, and manage pending deliveries through an easy Eloquent API. Useful for reminders, digests, and delayed notifications.

View on GitHub
Deep Wiki
Context7

Getting Started

This package adds database-backed scheduled delivery of mailables on top of Laravel's built-in later()/delay(): a mailable is persisted as a row with a send time, and a console command flushes due rows. Use it when you need visibility and cancellation of future emails (drip sequences, reminders, "send in 3 days unless they upgrade") rather than a fire-and-forget queue delay.

Minimum path:

  1. composer require oneduo/laravel-mail-scheduler — v1.0.0 adds the Laravel 11 constraint. If you're on Laravel 12, check composer.json constraints before assuming it installs.
  2. Publish and run the migration for the scheduled-mail table: php artisan vendor:publish --tag="mail-scheduler-migrations" then php artisan migrate. Read the migration first — it tells you the exact table/column names you'll query in tests and reports.
  3. Add the package trait to the mailable you want to defer (use Schedulable; in the Mailable class), then schedule an instance instead of calling Mail::send().
  4. Register the package's console command with Laravel's scheduler — in Laravel 11 that's routes/console.php (Schedule::command('...')->everyMinute()->withoutOverlapping()), not Kernel.php. This step is the one people forget; without it nothing ever sends.
  5. Confirm the cron entry for schedule:run exists and a queue worker is running (scheduled mail is normally pushed onto the queue, not sent inline).

First use case: in a Registered listener, schedule the day-3 onboarding email rather than building a ShouldQueue job + delay. You get a row you can inspect, re-schedule, or cancel from an admin panel.

Before wiring anything into production, read the trait source (src/Traits/Schedulable.php) and the command class — the public surface is small (schedule() / scheduleTo() plus the command), and the README is the authoritative place for exact signatures, which shift between minor versions.

Implementation Patterns

Schedule from domain events, not controllers. Keep the mailable "dumb" (it just uses the trait) and do the scheduling in a listener or an action class. This keeps the scheduling decision testable and lets you cancel from the same place later.

Treat the scheduled row as a first-class record. The package exposes a model (swappable via config). Resolve it through its repository/model rather than raw DB queries so a later config change (custom model, custom table) doesn't break your admin screens. Typical operations: list pending for a user, cancel one, re-schedule one to a new at.

Use it as the persisted alternative to delay(). Decision rule:

  • Mail::to($u)->later(...) / $mailable->delay(...) — short, non-business-critical delays where losing the email on a queue flush is acceptable.
  • This package — long horizons (days/weeks), anything a user might ask to cancel, anything support needs to look up.
  • Laravel's Mailable::afterCommit() and ShouldQueue still apply; combine, don't replace.

Recipient overrides. Use the scheduleTo()-style entry point when the recipient differs from the mailable's constructor data (e.g. CC an account owner later). Otherwise the trait inherits the standard to()/cc() set on the mailable and serialises them into the row.

Custom model / multi-tenant setups. Publish the config and point the model at your own subclass, then apply a global scope (tenant, team) — the scheduler command runs in the console with no authenticated user, so any tenancy scoping must be explicit on the model or the command will happily send across tenants.

Testing. Mail::fake() alone does not prove scheduling works. Assert two things: (a) after triggering the listener, a scheduled-mail row exists with the expected at and recipient; (b) after $this->travelTo(...) past the send time and running the package command via $this->artisan('...'), Mail::assertQueued(...) holds. Freeze time rather than sleeping; the command is minute-granular, so tests must move the clock.

Observability. Since scheduled mail is business state, wrap creation in a small service that fires your own domain event (MailScheduled, MailScheduledCancelled) so analytics and audit logs don't depend on the package's internals.

Gotchas and Tips

  • Laravel 11 is the ceiling declared by this release. v1.0.0 shipped Laravel 11 support only. On Laravel 12, expect constraint conflicts — verify with composer why-not oneduo/laravel-mail-scheduler before forcing the install; do not --ignore-platform-reqs in production.
  • Double sends on multi-server. The command is a normal artisan command; without ->withoutOverlapping() (and, on multiple hosts, ->onOneServer() for a cache-shared scheduler) two hosts can pick the same due row. Add both, and confirm whether the package locks rows internally before relying on it.
  • The scheduler cron is a single point of failure. schedule:run not running, or the queue worker being down, silently means "nothing sent" — the rows just accumulate. Alert on the age of the oldest unsent row, not on error logs.
  • Timezone drift. Scheduled times are almost certainly stored in UTC; travelTo, Carbon::setTestNow and any comparison you write must be consistent. Assert on the stored value, not a formatted string.
  • Queued mailable serialisation bites. The standard Laravel rules apply: if the mailable holds an Eloquent model and the recipient is soft-deleted or the model changes before the send time, the job fails at send time, not at schedule time. Prefer IDs + ShouldBeUnique-style guards over stale snapshots, or re-fetch in prepareMailableForDelivery().
  • Rows are consumed. Depending on version, sent rows are deleted rather than marked — plan your own audit trail (listener on the model, or a separate sent_mails table) before someone asks "was this email actually sent?".
  • Mail::fake() gives false confidence. The command fetches models directly; a fake will pass while the command throws. Always add the command-level integration test described above.
  • Cancellation is your responsibility. Nothing in the package knows a user upgraded, unsubscribed, or was deleted. Add a SoftDeletes/deleted guard in the mailable or a model observer that cancels pending rows — otherwise scheduled drip emails go to churned accounts.
  • Extension point is the model + config. Swap the model, add scopes, add casts for scheduled_at, but avoid overriding the command — reimplementing the flush loop forfeits future fixes in a package that just declared 1.0 stability.
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.
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib