knyk/mailbox-bundle
knyk/mailbox-bundle is a Symfony bundle for PHP applications that packages mailbox-related configuration and services, helping you integrate mailbox handling with less boilerplate. It keeps mailbox setup organized and makes email workflow code easier to maintain.
Read this first: it's a Symfony bundle, not a Laravel package. knyk/mailbox-bundle is a thin DI wrapper that registers PhpImap\Mailbox as a container service so you can inject it instead of new Mailbox(...) in every controller. There is no service provider, no facade, no config file Laravel will read, and no publishable assets. The entire package is realistically a Configuration.php, a services.xml/PHP config, and a *Bundle class.
So a Laravel developer has two honest paths:
composer require php-imap/php-imap and bind the mailbox yourself. You lose nothing — the bundle's value is the DI wiring, which in Laravel is about 15 lines of AppServiceProvider.DependencyInjection/Configuration.php and the services file, copy the connection options (host, port, username, password, attachments dir, connection flags, server encoding) into config/mailboxes.php. That's the reusable part.Minimal Laravel binding:
// composer require php-imap/php-imap
// config/mailboxes.php
return [
'support' => [
'host' => env('IMAP_HOST'),
'port' => (int) env('IMAP_PORT', 993),
'flags' => env('IMAP_FLAGS', '/imap/ssl'),
'username' => env('IMAP_USERNAME'),
'password' => env('IMAP_PASSWORD'),
'attachments' => storage_path('app/imap/support'),
'encoding' => 'UTF-8',
],
];
$this->app->singleton('mailbox.support', fn () => new \PhpImap\Mailbox(
sprintf('{%s:%d%s}INBOX', $c['host'], $c['port'], $c['flags']),
$c['username'], $c['password'], $c['attachments'], $c['encoding']
));
First use case: a queued job that polls a support inbox for unseen mail and creates a Ticket. That's the whole job this package exists to support — anything beyond "connect, read, mark seen, download attachments" you'll be writing yourself.
Where to look first: the bundle's README (likely one example), then DependencyInjection/Configuration.php. If the README shows YAML under knyk_mailbox:, translate those keys one-to-one into your Laravel config array — that's the full extent of "integrating" it.
Wrap it, don't inject it raw. PhpImap\Mailbox is a concrete class with a stateful IMAP connection. Define your own contract so tests don't need a live server:
interface Inbox
{
public function unseen(): array; // array of MessageId
public function message(int $id): Message;
public function attachments(int $id): array;
public function markSeen(int $id): void;
}
Back it with a PhpImapInbox adapter and a FakeInbox for tests. If you do port the Symfony bundle, this adapter is the port — the bundle's service definition becomes the constructor call inside it.
Per-mailbox factory, not a singleton per inbox. A singleton is fine for one support address; the moment you have support@, billing@, noreply@, you want a manager keyed by name. The bundle's approach (named service per configured mailbox) translates to a small MailboxManager with driver(string $name): Inbox.
One job = one connection. Never keep a Mailbox instance alive across queue workers or across requests in Octane. IMAP is a stateful socket; idle connections time out and PHP's imap extension reconnects badly. Pattern:
public function handle(MailboxManager $mailboxes)
{
$inbox = $mailboxes->driver('support');
foreach ($inbox->unseen() as $id) {
ProcessInboundMessage::dispatch($id, 'support');
}
$inbox->disconnect(); // explicit, before the worker sleeps
}
Idempotency before anything else. Fetch getMailIds()/searchMailbox('UNSEEN'), then check your own inbound_messages table on the RFC Message-ID header before inserting. IMAP \Seen flags are not a reliable dedupe key — a crash between "read" and "persist" re-delivers, and a crash between "persist" and "mark seen" re-delivers too. Store the UID and the Message-ID.
Attachments: copy, then forget. PhpImap\Mailbox writes attachment files into the directory you pass it. Treat that directory as scratch space: move each file into Storage::disk('tickets') inside the job, then let the next poll overwrite. Never point the attachments directory at public/ or at a path that isn't pruned.
Chunk the mailbox. getMails() on a 20k-message INBOX will exhaust memory and hit max_execution_time. Prefer getMailIds() + getMail($id) in small batches, or searchMailbox('SINCE "1-Jan-2025" UNSEEN') to bound the work.
Integration tips. Bind the manager as a singleton, resolve it from the job's handle() (not the constructor) so Octane workers don't hold stale sockets. Log the mailbox name, UID, and Message-ID on every failure — but never the password or the full {host:port/...} string.
ext-imap is the hard dependency, and it's fading. php-imap/php-imap needs PHP's IMAP extension. That extension was unbundled from core in PHP 8.4 and now lives on PECL — so a fresh php:8.4 Docker image won't have it and composer install will fail the platform check. Either pin PHP ≤ 8.3, install via pecl install imap in your image, or switch to a pure-PHP client (webklex/php-imap). This single constraint outweighs everything else in this assessment for a new project.
The package itself is a liability signal. 0 stars, 0 dependents, 0 forks in practice means: no one has audited it, no one will fix it, and there's no CVE feed watching it. It's MIT and small enough to vendor — copy the two config files into app/ and drop the dependency. You'll get the same functionality with a codebase you control.
OAuth is the real wall, not the bundle. Gmail and Microsoft 365 both require OAuth2 (or app passwords, which Google is phasing out) for IMAP. PhpImap\Mailbox takes a username/password and has no OAuth flow. The bundle can't help you here — you'll need XOAUTH2 support, which php-imap/php-imap doesn't provide cleanly. Verify this before promising a client that inbound mail will work against their Google Workspace tenant.
Connection strings are stringly-typed and easy to get wrong. {imap.example.com:993/imap/ssl}INBOX — port 993 with /ssl (implicit TLS) vs port 143 with /tls (STARTTLS) are not interchangeable. Wrong combo gives an opaque ConnectionException with no hint. Add /novalidate-cert only for self-signed dev servers, never in production. The Symfony bundle's config keys exist mostly to build this string for you; if you hand-roll, build it in one helper and log it on failure.
Encoding bugs are silent. Set the server encoding to UTF-8 unless the provider says otherwise; otherwise you get mojibake in subject lines that survives into your database and is only noticed by a user. Decode MIME-encoded headers (=?UTF-8?B?...?=) before persisting — neither the bundle nor PhpImap normalizes this for you.
Queue worker timeouts and IMAP timeouts fight each other. default_socket_timeout and imap_timeout() defaults can exceed your job's $timeout, causing a MaxAttemptsExceededException on a job that would have succeeded. Set an explicit IMAP timeout (10–30s) and a job timeout comfortably above it, and give the job $tries = 1 with a scheduled re-poll instead of retries — retrying a half-consumed mailbox is how you get duplicates.
deleteMail() doesn't delete. It flags \Deleted; you need expunge() to actually remove. Same class of mistake as markAsSeen() needing a subsequent expunge() on some servers to be visible to other clients.
Debugging: wrap every call in try/catch for PhpImap\Exceptions\ConnectionException and PhpImap\Exceptions\InvalidParameterException, log imap_last_error(), and add a mailbox:test {name} artisan command early. Being able to run one command that connects, prints the message count, and disconnects will save you hours versus debugging from inside a queued job.
Extension points: there are none. The bundle offers no events, no decorators, no plugin hooks. Your extension point is replacing the service definition — which, in Laravel, means you were never really using the bundle anyway. Treat knyk/mailbox-bundle as documentation of how to wire PhpImap\Mailbox into a DI container, and build the Laravel equivalent yourself.
How can I help you explore Laravel packages today?