Scoped mailbox access for mailcow: read, send mail and get webhooks in your automation

A practical walkthrough of the Mailbox API for mailcow: create scoped keys to read, send and organize mail per mailbox, verify signed webhooks, and route new-mail alerts to Telegram.

Why you want this

mailcow ships with an admin API. It is great for creating domains, mailboxes and aliases, but it says nothing about the mail inside a mailbox. If you want a script, a CRM or an automation workflow to read a shared inbox, send a reply as that address, or file a message into a folder, the admin API leaves you on your own.

The Mailbox API for mailcow fills that gap. It gives your own programs a controlled REST interface to the mail itself: list folders and messages, search, read a full message with attachments, send as the mailbox, and mark, move or delete. On top of that, each monitored mailbox can push every new message to an https webhook as signed JSON, and to a Telegram chat. Everything runs inside your mailcow network, and you decide which mailboxes and which rights each key gets.

This tutorial walks through the setup the way I did it on my own host: keys first, then reading and sending, then webhooks into an automation tool such as n8n, and finally Telegram alerts.

Prerequisites

  • mailcow-dockerized on Linux with docker compose and systemd, and the MediaLab Works mailcow Toolkit 0.5.0 or newer installed (this product is one of its modules, with its own license).
  • Root access on the host to run the Toolkit installer, and outbound HTTPS from the server to medialab.works for the license check.
  • An https address for the API, used in signed attachment links, and, for webhooks, an https endpoint of your own.
  • Optional: a Telegram bot token from @BotFather and the chat id of each mailbox you want to notify.
  • The module is open source (GPL-3.0), so you receive the complete source code.

Step 1 — Install the module and activate the license

The module ships in the Toolkit package. On the mailcow host, as root, run the installer as usual:

sudo ./install.sh --mailcow-dir /opt/mailcow-dockerized --name <instance>

It copies data/web/mlw-mailbox-api/, adds the menu item, the host agent job and the once-a-minute timer, and reloads php-fpm. Then open System → Mailbox API in the mailcow admin menu; you will see a license banner and the settings below it.

To activate, open System → Updates (Toolkit) and find the Mailbox API for mailcow card: paste the key and activate it, or start the 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 keeps working for 7 days after the last good check. Without a valid license the API answers 503 not_licensed and the timer sends nothing, but mailboxes and messages are untouched.

Step 2 — Turn on the API and set the basics

In System → Mailbox API:

  • API on turns the API on or off. The license is checked anyway.
  • Public API address is 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) set your limits.
  • Dovecot master user is optional and only needed if you set your own in mailcow.conf; by default the module reads the master credentials mailcow creates at start.
  • Telegram bot token is optional for now; we come back to it in Step 6.

Step 3 — Create scoped keys

In the API keys card, give the key a name, tick the rights it needs — read, send, organize — choose the mailboxes it may use, and press Create key. The key (mbx_…) is shown once, when it is created, so store it right away. The list shows the prefix, rights, mailboxes and last use; Revoke disables it immediately. Only the SHA-256 of a key is stored in the database, and every call is logged.

This is the part I like most for automation: a workflow that only files incoming mail into folders gets a key with organize on one mailbox, and nothing else. A notification script gets read only. You never hand out a key that can reach every mailbox on the server.

Send the key on every call:

Authorization: Bearer mbx_…

Step 4 — Read and organize mail

All routes are called on https://<mail host>/mlw-mailbox-api/?r=<route> — mailcow's nginx has no PATH_INFO, so the route lives 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.

Start by checking what a key can do:

curl -H "Authorization: Bearer $KEY" \
  "https://mail.example.com/mlw-mailbox-api/?r=/v1/me"

GET /v1/me returns the key name, rights, mailboxes and limits; GET /v1/mailboxes lists the allowed mailboxes with their folder count and INBOX UIDNEXT. To see the folders and a page of the newest messages:

curl -H "Authorization: Bearer $KEY" \
  "https://mail.example.com/mlw-mailbox-api/?r=/v1/folders&mailbox=office@example.com"

curl -H "Authorization: Bearer $KEY" \
  "https://mail.example.com/mlw-mailbox-api/?r=/v1/messages&mailbox=office@example.com&folder=INBOX&limit=20&offset=0&unseen=1"

The message list supports q= for search and unseen=1 for unread only, and comes back newest first with uid, subject, from, to, date, size and seen. Reading one message gives the text, HTML, headers and attachments (name, type, size, signed url):

curl -H "Authorization: Bearer $KEY" \
  "https://mail.example.com/mlw-mailbox-api/?r=/v1/messages/1234&mailbox=office@example.com&folder=INBOX"

Attachments come as signed links that expire after 1 hour and are bound to the API key that can read the mailbox, so you can download them without passing the key around. Regenerating a link means reading the message again through the API.

With the organize right you can mark read or unread, move and delete:

curl -X POST -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \
  -d '{"read":true}' \
  "https://mail.example.com/mlw-mailbox-api/?r=/v1/messages/1234/read&mailbox=office@example.com&folder=INBOX"

curl -X POST -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \
  -d '{"folder":"Archive"}' \
  "https://mail.example.com/mlw-mailbox-api/?r=/v1/messages/1234/move&mailbox=office@example.com&folder=INBOX"

A DELETE on /v1/messages/{uid} expunges the message. 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.

Step 5 — Send as the mailbox

Sending needs the send right. The body takes to, subject and text, and also supports cc, bcc, html and reply_to. A copy is put in Sent, which matters if someone later opens that mailbox and wonders what was answered:

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"

Nothing is sent to medialab.works; the message goes out through your own mailcow.

Step 6 — Webhooks for new mail, verified

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 using UIDNEXT per folder. Note the design: the first run only sets the starting point, so no old mail is replayed — new mail from then on is delivered.

Each new message produces one JSON POST with event, mailbox, folder, uid, received_at and a message object containing message_id, date, from, to, cc, subject, size, text, html, headers and attachments (name, type, size, sha256 and a signed url).

Before you use the body, verify the signature header:

X-MLW-Signature: t=1792300000,v1=<hex HMAC-SHA256(secret, "t.body")>
X-MLW-Event: message.received

Each monitor has its own secret, generated on first use; the page shows it when the monitor is created and with the Secret button. Compare the HMAC with your secret and reject timestamps older than five minutes. In n8n, for example, put that check in a small Code or Function node right after the Webhook node, before anything else touches the payload.

Failed deliveries are retried after 30 s, 1 min, 2 min … up to five attempts. Deliveries are queued in the database and sent by the once-a-minute timer, and the Recent deliveries card shows status and answer — your first stop when a workflow seems silent.

Step 7 — Telegram notifications per mailbox

Paste the bot token under Settings (it is stored encrypted, only the last four characters are shown; one bot serves all mailboxes). Then put the chat id — a user, a group or a channel — on each monitored mailbox. The bot must be a member and, for channels, an administrator. Telegram next to a mailbox sends a test message. The notification shows the sender, the subject and the first 400 characters of the text.

This is a nice fit for a shared inbox: one chat per monitored mailbox, and you see new mail on your phone without opening a mail client.

Tips and troubleshooting

  • 503 not_licensed: activate the license under System → Updates (Toolkit); without internet the module works for 7 days after the last check.
  • 502 server_error with "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; create a new key with the right scope in the admin page.
  • No webhook arrives: check that the monitor is active, the address is https and answers 2xx, and look at Recent deliveries; the timer runs every minute (systemctl status mlcow-mailapi-<name>.timer).
  • No old mail after adding a monitor: by design, the first run only records the current position.
  • Keep the Public API address set to your real https address; otherwise signed attachment links point to the wrong place.
  • The API does not create, delete or change mailboxes; it only works inside the mailboxes you allow. It reads mail through dovecot's master login inside the mailcow network, the same mechanism mailcow uses for imapsync.

Conclusion

The admin API manages the server; this module manages the mail, one scoped key at a time. With a read/send/organize key per mailbox, signed webhooks into n8n and a Telegram chat for the inboxes you care about, you get automation around your own mailcow without giving anything away. Start with one mailbox and a read-only key, confirm the webhook signature check works, then widen the scope. The user guide has the full route table, and the changelog tracks updates; if something does not line up, the knowledge base and FAQ are good next stops, or contact us.

This post was written with AI assistance from our product documentation or the sources listed and checked automatically before publication.

Read next

Running Emby, Jellyfin or Nextcloud?

Our plugins and apps add Live TV, subtitles, sign-up and security to your own server, with a free trial.