NewWe open-sourced 50+ Laravel packages
Custom AI apps, agents and automation — Roundly ConsultingRoundly
All packages

A mail goes to the user’s digest when the type is Priority::Low and digestible() and the user’s frequency is not off (the default is notifications.digest.default, off). Mandatory digestible types are digested too — they still arrive. The inbox item is created at once either way:

use RoundlyConsulting\Notifications\Enums\DigestFrequency;

Notifications::preferences($user)->digest(DigestFrequency::Daily);            // at notifications.digest.time
Notifications::preferences($user)->digest(DigestFrequency::Weekly, '07:30');  // on notifications.digest.weekly_day
Notifications::preferences($user)->digest(DigestFrequency::Off);

Notifications::digests()->sendDue();          // int — what the sweep runs
Notifications::digests()->preview($user);     // ?MailMessage — the digest of the user's parked items, never sent

Slot and period

Computed when the mail is parked, in the user’s timezone. Daily → today at the time, or tomorrow when that has passed (DST keeps the local slot), period Y-m-d. Weekly → the next configured weekday at the time (today if it is that day and not past), period GGGG-[W]WW. A slot inside quiet hours moves to their end; the period stays.

Runs

The sweep groups due parked deliveries by recipient, frequency and period, creates one DigestRun per group (unique per recipient, frequency and period), claims it by compare-and-set and dispatches the send — a second sweep or a retried job sends nothing. Every entry is re-checked (access, mute, subject, type) and failures are dropped. Nothing left → the run is empty and no mail goes out; otherwise one mail grouped by context, then category, at most digest.max_items lines (“And :count more items in your inbox”), and DigestSent fires.

  • Late items — an item parked for a period whose run already went out rolls into the user’s next slot.
  • Frequency change — pending items move to the new slot; switching off sends them as one final digest at the next sweep.
  • Send failure — the run is retried by the next sweeps until notifications.mail.max_attempts, then fails with its entries.
  • Crash — a run still sending after digest.lease (PT15M) fails together with its entries — never re-grouped forever, never silently lost.
  • A recipient gone → empty; a recipient without an address → failed(no_address). The address resolver applies exactly as for typed mail.
  • Notifications other packages send (interop) are never digested.

Show your open-source love

This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.

More ways to support, including crypto

By donating, you agree to our donation terms.

Want this built into your product?

We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.