Search by

beziworld / laravel-mailcow

kamcw

Laravel client for the mailcow API: domains, mailboxes, aliases, quotas, DKIM, app passwords, mail queue and logs.

Package info

github.com/Beziworld/laravel-mailcow

Homepage

Issues

pkg:composer/beziworld/laravel-mailcow

Statistics

Installs: 1

Dependents: 0

Suggesters: 0

Stars: 0

v1.0.0 2026-11-10 20:31 UTC

This package is auto-updated.

Last update: 2026-11-10 20:33:10 UTC


README

tests Latest Version License

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.

Polska wersja dokumentacji

Features

  • X-API-Key authentication, read-only and read-write keys, several servers as named connections
  • Typed results (Domain, Mailbox, Alias, DkimKey, AppPassword, LogEntry, QueueItem) with the full object in raw and 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 an ApiException with the translation key (domain_exists, mailbox_quota_exceeded, ...)
  • The {} answer mailcow sends for empty lists and missing objects becomes [] or null
  • 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() and post() for any endpoint, php artisan mailcow:check to 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 and payload() 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/mailq and delete/mailq send {"action": ...}; mailcow reads items and attr.action instead, 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.

Need an integration? Get in touch through beziworld.eu.

Other packages by the author

License

MIT. See LICENSE.md. This package is not affiliated with the mailcow project or its maintainers.