beziworld / laravel-mailcow
Laravel client for the mailcow API: domains, mailboxes, aliases, quotas, DKIM, app passwords, mail queue and logs.
Requires
- php: ^8.3
- ext-json: *
- guzzlehttp/guzzle: ^7.8
- illuminate/console: ^12.0|^13.0
- illuminate/contracts: ^12.0|^13.0
- illuminate/http: ^12.0|^13.0
- illuminate/support: ^12.0|^13.0
Requires (Dev)
- larastan/larastan: ^3.0
- laravel/pint: ^1.18
- orchestra/testbench: ^10.0|^11.0
- pestphp/pest: ^3.7|^4.0
- pestphp/pest-plugin-laravel: ^3.0|^4.0
- phpstan/phpstan-strict-rules: ^2.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-11-10 20:33:10 UTC
README
Laravel client for the mailcow: dockerized API: domains and alias domains, mailboxes, aliases and catch-all addresses, quotas, DKIM keys, app passwords, rate limits, the Postfix queue, the quarantine, container logs and status, for one or many mail servers.
Features
X-API-Keyauthentication, read-only and read-write keys, several servers as named connections- Typed results (
Domain,Mailbox,Alias,DkimKey,AppPassword,LogEntry,QueueItem) with the full object inrawand dates in the time zone of your server - Reliable error detection: mailcow answers failed changes with HTTP 200 and
"type": "danger"; every failed item becomes anApiExceptionwith the translation key (domain_exists,mailbox_quota_exceeded, ...) - The
{}answer mailcow sends for empty lists and missing objects becomes[]ornull - Quotas in MB on input, bytes on output, plus
quotas()->nearlyFull(90)for alerts - Mail queue actions as implemented by mailcow (
flush,deliver,hold,release,delete,clear) - GET requests are repeated once after a connection error or HTTP 5xx; changes are sent exactly once
get()andpost()for any endpoint,php artisan mailcow:checkto test a connection- PHP 8.3+, Laravel 12 and 13, PHPStan level 8
Why this package
There is no Laravel package for mailcow on Packagist (checked in November 2026). The framework-agnostic clients
(exbil/mailcow-php-api, e-monsite/mailcow-php-client, vexura/mailcow-api) return the raw answers and leave
the HTTP 200 failures of mailcow to you. This package is built from the official OpenAPI spec of
mailcow-dockerized and its json_api.php, follows the source code where the spec examples are inconsistent (mail
queue actions), adds Laravel configuration, typed data, Http::fake() support for your own tests and an Artisan
check.
Installation
composer require beziworld/laravel-mailcow
php artisan vendor:publish --tag=mailcow-config # optional
MAILCOW_URL=https://mail.example.com MAILCOW_API_KEY=ABCDEF-123456-ABCDEF-123456-ABCDEF MAILCOW_TIMEZONE=Europe/Warsaw # TZ of mailcow.conf, used for created/modified dates MAILCOW_VERIFY_SSL=true
php artisan mailcow:check
Create the key in the mailcow UI (Configuration > Access > Edit administrator details > API), choose read-only or read-write access and allow only the IP address of your application.
Usage examples
Provision mail for a new customer
use Beziworld\Mailcow\Exceptions\ApiException; use Beziworld\Mailcow\Facades\Mailcow; try { Mailcow::domains()->create('shop.example.com', mailboxes: 25, quotaMb: 51200, defaultMailboxQuotaMb: 2048); Mailcow::dkim()->generate('shop.example.com', selector: 'dkim', keySize: 2048); Mailcow::mailboxes()->create('office@shop.example.com', $password, quotaMb: 4096, name: 'Office'); Mailcow::aliases()->create('sales@shop.example.com', 'office@shop.example.com'); } catch (ApiException $e) { // $e->errorKey(): "domain_exists", "mailbox_quota_exceeded", ...; $e->messages() lists every failed item report($e); } $dkim = Mailcow::dkim()->get('shop.example.com'); // Publish $dkim->recordName() ("dkim._domainkey.shop.example.com") as TXT with $dkim->txt
Domains and alias domains
foreach (Mailcow::domains()->all() as $domain) { // $domain->name, $domain->mailboxes, $domain->maxMailboxes, $domain->usedBytes, $domain->usagePercent() } Mailcow::domains()->update('shop.example.com', ['mailboxes' => 50, 'description' => 'Shop']); Mailcow::domains()->activate('shop.example.com', false); Mailcow::domains()->setRateLimit('shop.example.com', 500, 'h'); Mailcow::domains()->createAliasDomain('shop.example.net', 'shop.example.com'); Mailcow::domains()->delete('old.example.com');
Mailboxes
$mailboxes = Mailcow::mailboxes()->all('shop.example.com'); // list<Mailbox> $mailbox = Mailcow::mailboxes()->get('office@shop.example.com'); Mailcow::mailboxes()->changePassword('office@shop.example.com', $newPassword, forceUpdate: true); Mailcow::mailboxes()->update('office@shop.example.com', ['name' => 'Front office', 'sogo_access' => '1']); Mailcow::mailboxes()->setSenderAcl('office@shop.example.com', ['default', 'sales@shop.example.com']); Mailcow::mailboxes()->activate('intern@shop.example.com', false); Mailcow::mailboxes()->delete('intern@shop.example.com');
Quotas
Mailcow::quotas()->setMailbox('office@shop.example.com', 8192); // MB Mailcow::quotas()->setDomain('shop.example.com', 102400, maxMailboxQuotaMb: 20480); foreach (Mailcow::quotas()->nearlyFull(90) as $mailbox) { Notification::route('mail', $mailbox->username)->notify(new MailboxAlmostFull($mailbox->percentInUse)); } Mailcow::quotas()->storage(); // ['total' => '41G', 'used' => '11G', 'used_percent' => '28%', ...]
Aliases and catch-all
Mailcow::aliases()->create('team@shop.example.com', ['anna@shop.example.com', 'piotr@shop.example.com']); Mailcow::aliases()->createCatchAll('shop.example.com', 'office@shop.example.com'); Mailcow::aliases()->createBlackhole('noreply@shop.example.com'); Mailcow::aliases()->createTimeLimited('office@shop.example.com', validityHours: 72, description: 'Trade fair'); $alias = Mailcow::aliases()->find('team@shop.example.com'); Mailcow::aliases()->update($alias->id, ['goto' => 'anna@shop.example.com']); Mailcow::aliases()->delete($alias->id);
App passwords
Mailcow::appPasswords()->create('office@shop.example.com', 'Phone', $appPassword, ['imap_access', 'smtp_access']); foreach (Mailcow::appPasswords()->of('office@shop.example.com') as $app) { // $app->id, $app->name, $app->active } Mailcow::appPasswords()->delete($id);
Queue, quarantine, logs and status
$deferred = Mailcow::queue()->all(); // list<QueueItem> Mailcow::queue()->flush(); // retry every deferred message now Mailcow::queue()->hold('B98C6260CA1'); Mailcow::queue()->release('B98C6260CA1'); Mailcow::queue()->delete('B98C6260CA1'); Mailcow::quarantine()->all(); Mailcow::quarantine()->release(33); foreach (Mailcow::logs()->postfix(200) as $entry) { // $entry->time, $entry->program, $entry->priority, $entry->message } Mailcow::logs()->get('netfilter', 50); Mailcow::status()->version(); // "2026-11" Mailcow::status()->stoppedContainers(); // ["clamd-mailcow"]
Several servers
// config/mailcow.php 'connections' => [ 'default' => [...], 'eu' => ['url' => env('MAILCOW_EU_URL'), 'api_key' => env('MAILCOW_EU_KEY')], ], Mailcow::connection('eu')->mailboxes()->all(); Mailcow::using(['url' => $server->url, 'api_key' => decrypt($server->api_key)])->status()->version();
Anything else
Mailcow::get('get/bcc/all'); Mailcow::post('add/bcc', ['local_dest' => 'shop.example.com', 'bcc_dest' => 'archive@example.net', 'type' => 'sender']);
Exceptions
All exceptions extend MailcowException:
| Exception | When |
|---|---|
AuthenticationException |
HTTP 401 (unknown key or address not allowed) or 403 (read-only key used for a change, isReadOnlyKey()). Never retried. Extends ApiException. |
ApiException |
An item failed (type danger or error, also with HTTP 200), HTTP errors and connection errors: path(), errorKey(), messages(), httpStatus(), payload() |
RateLimitException |
HTTP 429, retryAfter() in seconds |
InvalidConfiguration |
Unknown connection, missing URL / key or an invalid time zone |
Notes
- Multi-item calls (
delete('a@x', 'b@x')) are processed item by item by mailcow; when one item fails the exception lists all failures andpayload()holds the answers of the successful ones. - The alias domain endpoints are implemented by mailcow but missing from the published OpenAPI spec.
- The OpenAPI examples of
edit/mailqanddelete/mailqsend{"action": ...}; mailcow readsitemsandattr.actioninstead, which is what this package sends.
Development
composer install
composer ci # pint --test, phpstan (level 8), pest
Author
Created and maintained by Kamil Ćwiertnia, beziworld.eu.
- E-mail: biuro@beziworld.eu
- Issues and feature requests: GitHub issues
- Security vulnerabilities: see SECURITY.md
Need an integration? Get in touch through beziworld.eu.
Other packages by the author
- beziworld/laravel-ispconfig: ISPConfig 3 remote API
- beziworld/laravel-hestiacp: Hestia Control Panel
- beziworld/laravel-directadmin: DirectAdmin
License
MIT. See LICENSE.md. This package is not affiliated with the mailcow project or its maintainers.