Mailbox API for mailcow — installation and API guide
Installation
- The module ships in mailcow Toolkit 0.5.0 or newer (one package, one license per product). On the mailcow host, as root, run the Toolkit installer as usual:
sudo ./install.sh --mailcow-dir /opt/mailcow-dockerized --name <instance>. It copiesdata/web/mlw-mailbox-api/, adds the menu item, the host agent job and the once-a-minute timer, and reloads php-fpm. - Open System → Mailbox API in the mailcow admin menu. The page shows a license banner and, below it, the settings.
License
Open System → Updates (Toolkit) and find the Mailbox API for mailcow card: paste the key and activate it, or start the 30-day trial. On the shell the same is possible with php /web/mlw-toolkit/cli.php activate mailcow-mailbox-api <KEY> inside php-fpm-mailcow. The license is checked daily; without internet the module works for 7 days after the last good check.
Without a valid license the API answers 503 not_licensed, the timer sends nothing, and the page says the license is missing. Nothing is deleted.
Settings
In System → Mailbox API:
- API on: turns the API on or off (the license is checked anyway).
- Public API address: the address used in signed attachment links, for example
https://mail.example.com/mlw-mailbox-api/. Leave it empty to derive it from the page. - Messages per page (1–200) and largest attachment (1–100 MB).
- Telegram bot token: from @BotFather; stored encrypted (only the last four characters are shown). One bot serves all mailboxes.
- Dovecot master user (optional): only if you set your own in
mailcow.conf; by default the module reads the master credentials mailcow creates at start.
API keys
In the API keys card: give the key a name, tick the rights — read, send, organize — choose the mailboxes it may use, and press Create key. The key (mbx_…) is shown once; store it. The list shows the prefix, rights, mailboxes and last use; Revoke disables it immediately. Only the SHA-256 is kept in the database.
Send it on every call:
Authorization: Bearer mbx_…
API reference
All routes are called on https://<mail host>/mlw-mailbox-api/?r=<route> (mailcow's nginx has no PATH_INFO, so the route is in the query string). Answers are JSON. Every route except the signed attachment link needs the key, and the key must allow the mailbox and the right.
| Method and route | Right | What it does |
|---|---|---|
GET /v1/me |
any | Key name, rights, mailboxes, limits |
GET /v1/mailboxes |
any | The allowed mailboxes, their folder count and INBOX UIDNEXT |
GET /v1/folders&mailbox=… |
read | Folder names |
GET /v1/messages&mailbox=…&folder=INBOX&limit=20&offset=0&q=…&unseen=1 |
read | Message list (newest first): uid, subject, from, to, date, size, seen |
GET /v1/messages/{uid}&mailbox=…&folder=… |
read | Full message: text, HTML, headers, attachments (name, type, size, signed url) |
POST /v1/messages/{uid}/read body {"read":true} |
organize | Mark read or unread |
POST /v1/messages/{uid}/move body {"folder":"Archive"} |
organize | Move to another folder |
DELETE /v1/messages/{uid}&mailbox=…&folder=… |
organize | Delete (expunges) |
POST /v1/send&mailbox=… body {"to":"a@b.c","subject":"…","text":"…"} |
send | Send as the mailbox (cc, bcc, html, reply_to supported); a copy goes to Sent |
GET /v1/attachments&…&t=… |
signed link | Download an attachment |
Examples:
curl -H "Authorization: Bearer $KEY" \
"https://mail.example.com/mlw-mailbox-api/?r=/v1/messages&mailbox=office@example.com&limit=5&unseen=1"
curl -X POST -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \
-d '{"to":"customer@example.org","subject":"Your invoice","text":"Attached."}' \
"https://mail.example.com/mlw-mailbox-api/?r=/v1/send&mailbox=office@example.com"
Errors are JSON with an HTTP status: 401 missing or wrong key, 403 the key has no right on that mailbox, 404 unknown route or message, 422 invalid input, 429/502 limits or a server error.
Webhooks
In the Monitored mailboxes card add a mailbox, the folder to watch (default INBOX), an https webhook address and, optionally, a Telegram chat id. The module checks every minute for new messages (UIDNEXT per folder; the first run only sets the starting point, no old mail is replayed).
Every new message produces one JSON POST:
{
"event": "message.received",
"mailbox": "office@example.com",
"folder": "INBOX",
"uid": 1234,
"received_at": "2026-10-18T06:12:00+00:00",
"message": {
"message_id": "…", "date": "…", "from": "Billing <billing@example.com>", "to": "…", "cc": "",
"subject": "Invoice 42", "size": 12345, "text": "…", "html": "…", "headers": {"…": "…"},
"attachments": [{"name": "invoice.pdf", "type": "application/pdf", "size": 23112,
"sha256": "…", "url": "https://mail.example.com/mlw-mailbox-api/?r=/v1/attachments&…&t=…"}]
}
}
Verify the signature header before using the body:
X-MLW-Signature: t=1792300000,v1=<hex HMAC-SHA256(secret, "t.body")>
X-MLW-Event: message.received
Compare with your secret (each monitor has its own, generated on first use: the page shows it when the monitor is created and with the Secret button) and reject timestamps older than five minutes. Failed deliveries are retried after 30 s, 1 min, 2 min … up to five attempts; the Recent deliveries card shows status and answer. Deliveries are queued in the database and sent by the once-a-minute timer.
Telegram
Paste the bot token under Settings, then put the chat id (a user, a group or a channel; the bot must be a member and, for channels, an administrator) on each monitored mailbox. Telegram next to a mailbox sends a test message. The notification shows the sender, the subject and the first 400 characters of the text.
Troubleshooting
503 not_licensed: activate the license (System → Updates (Toolkit)); without internet the module works for 7 days after the last check.502 server_errorwith "no dovecot master credentials": mailcow did not create/etc/sogo/sieve.creds; check that dovecot-mailcow and sogo-mailcow run, or set the master user and password by hand.403 not_allowed: the key does not cover that mailbox or lacks the right; edit the key (create a new one) in the admin page.- No webhook arrives: check the monitor is active, the address is https and answers 2xx, and look at Recent deliveries; the timer runs every minute (
systemctl status mlw-mailcow-mailapi-<name>.timer). - No old mail after adding a monitor: by design, the first run only records the current position; new mail from then on is delivered.