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

Swiftmailerdbbundle Laravel Package

whiteoctober/swiftmailerdbbundle

A Symfony bundle that stores SwiftMailer's spooled emails in a database table instead of memory or files. Queued messages survive restarts and can be sent by cron or workers across multiple servers, delivering reliably without losing mail when a process crashes.

View on GitHub
Deep Wiki
Context7

Getting Started

Reality check first. This is a Symfony bundle (WhiteOctoberSwiftMailerDBBundle), not a Laravel package. It targets Symfony 2.x/3.x and SwiftMailer, which reached end-of-life in November 2021, and its last release was August 2018. On any modern Laravel app you almost certainly want Mail::queue() with the database queue driver instead — it gives you the same "persist messages, flush later from a worker" semantics with zero extra dependencies. Reach for this package only if you are maintaining a legacy Symfony app, or you are reading its source as a reference design for a spool you intend to port into Eloquent.

Where to look first (the repo is small, read it in this order):

  1. Spool/DoctrineSpool.php — the entire idea lives here; it implements Swift_Spool. queueMessage() serializes a Swift_Message into a row, sendQueuedMessages() selects unsent rows and hands them back to the transport.
  2. Entity/Email.php — the Doctrine entity backing the spool table (message blob, timestamps, sent flag).
  3. DependencyInjection/Configuration.php + Resources/config/services.xml — the config keys (table_name, keep_sent_messages, entity_manager) and the actual service id you must reference. Confirm the id from services.xml rather than trusting blog posts; it has changed across versions.

Minimal Symfony wiring:

# config.yml
white_october_swift_mailer_db:
    table_name: email_spool
    keep_sent_messages: false
    entity_manager: default

swiftmailer:
    spool:
        type: service
        id: white_october_swift_mailer_db.spool   # verify against services.xml

Create the table with doctrine:schema:update --force in dev, but write a real migration for production. Then flush from cron:

* * * * * php bin/console swiftmailer:spool:send --env=prod --message-limit=200

First use case: making transactional mail survive a deploy, a Redis bounce, or a transport outage. Instead of a file spool (gone on container rebuild) or memory (gone on request end), the message is a durable DB row, and a disposable cron worker drains it.

Implementation Patterns

Cron-driven drain, not a daemon. The canonical pattern is swiftmailer:spool:send on a one-minute cron, bounded by --message-limit and --time-limit so a backlog can't produce a runaway process. This is the package's whole value proposition: no long-lived worker to supervise.

Audit trail via keep_sent_messages: true. With the flag off, rows are deleted after successful send and you have no record of what went out. Turn it on if mail volume is a compliance concern, then add a pruning command that deletes rows where sent_at < now() - 30 days — otherwise the table grows unbounded and the unsent-row query starts doing sequential scans.

Environment split. Keep spool: { type: memory } in dev and test so you never need a worker locally, and use the DB spool only in prod. In tests, assert on the spool table before flushing:

$count = $em->getRepository(Email::class)->count(['sentAt' => null]);
$this->assertSame(1, $count);

Porting the idea to Laravel. If you genuinely need spool semantics (retry on transport failure, single flush point), don't fight the Doctrine dependency — reimplement the pattern:

// app/Models/MailSpool.php
protected $table = 'mail_spool';
protected $casts = ['payload' => 'array', 'sent_at' => 'datetime'];
// columns: id, payload (json), attempts, available_at, sent_at, last_error

// app/Console/Commands/FlushMailSpool.php
MailSpool::whereNull('sent_at')->where('available_at', '<=', now())
    ->orderBy('id')->limit(200)->get()
    ->each(function (MailSpool $row) {
        try {
            Mail::raw($row->payload['body'], fn ($m) => $m->to($row->payload['to'])
                ->subject($row->payload['subject']));
            $row->update(['sent_at' => now()]);
        } catch (Throwable $e) {
            $row->increment('attempts');
            $row->update(['last_error' => $e->getMessage(),
                          'available_at' => now()->addMinutes(5 * $row->attempts)]);
        }
    });

Storing JSON instead of a serialized PHP object avoids the Swift_Message unserialize hazards and keeps the payload inspectable.

Per-connection routing. The entity_manager config lets you push the spool table onto a separate Doctrine connection — useful when mail writes shouldn't compete with primary OLTP traffic or when you want the spool on a different replica.

Failure handling. The bundle has no retry counter. If a message fails at the transport layer it remains "unsent" and is retried every single cron run, forever. Add your own attempts/last_error columns (or port the pattern with them, as above) so a permanently failing recipient doesn't occupy a worker slot indefinitely.

Gotchas and Tips

  • It will not composer require into a Laravel project. Doctrine ORM, symfony/framework-bundle, and Symfony DI are hard requirements. Budget for a rewrite, not an integration.
  • Double-send risk under concurrency. sendQueuedMessages() selects unsent rows; if two workers overlap there is no row-level lock by default and the same message goes out twice. Run exactly one cron instance, or wrap the SELECT in FOR UPDATE SKIP LOCKED (Postgres/MySQL 8) in your port.
  • Serialized Swift_Message blobs include attachments. A 5 MB PDF times 1,000 rows is a 5 GB table and a max_allowed_packet error waiting to happen. Strip attachments or store them by reference before queuing.
  • keep_sent_messages: false deletes successes only. Failures accumulate silently — monitor COUNT(*) WHERE sent_at IS NULL with an alert threshold, or you'll discover the backlog during an incident.
  • Schema drift. The bundle does not ship a migration. doctrine:schema:update in prod is how people corrupt their DB; hand-write the CREATE TABLE and commit it.
  • Service id churn. The spool service id differs between the 1.x and later tags. If swiftmailer.spool: send throws "service not found," grep Resources/config/services.xml for the <service id= entry.
  • Cache/container staleness after config change. Changing table_name requires a cache:clear; the value is compile-time, not runtime.
  • Debugging tip: the fastest triage is SELECT id, created_at, sent_at FROM email_spool WHERE sent_at IS NULL ORDER BY id DESC LIMIT 10. If rows are piling up, the problem is your cron, not your transport — check the command's exit code and log output before debugging SMTP.
  • Extension point: the entity mapping can be overridden by re-declaring the Email entity in your own bundle's Entity/ directory, as long as the table name matches the configured table_name. This is how people add attempts, priority, or tenant_id columns without forking.
  • Migration path off the package: Symfony Mailer (5.4+/6+) has no DB spool either — the replacement is symfony/messenger with an async transport plus MailerInterface::send() dispatched through a message bus. If you're on SwiftMailer and planning an upgrade, skip this bundle and go straight to Messenger rather than adopting a dead abstraction.
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.
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin