# Incoming message size gate (mailcow)

This integration is **not active merely by deploying the web app**. The
Rspamd plugin and mailcow limits must be installed and tested separately.

## Production observation (2026-09-29)

The production mailcow host had this repository's `official_mail_size.lua`
installed (matching SHA-256), an `official_mail_size` configuration block, and
`rspamadm configtest` reported `syntax OK`. Postfix's global
`message_size_limit` was 100 MiB. The public policy endpoint returned HTTP 401
without its private bearer token, consistent with a configured policy token.
An initial log inspection showed `milter-reject: END-OF-MESSAGE` for roughly
27 MiB and 42 MiB messages, but did not establish those recipients' plans.
The follow-up live SMTP test used independently verified active Free and
Growth mailboxes and connected to port 25 on the mailcow host itself, because
the testing PC's public IP was rejected by Spamhaus before `DATA`:

| Recipient plan | Total test message | SMTP result | Dovecot INBOX |
| --- | ---: | --- | --- |
| Free | 24 MiB | accepted | present |
| Free | 25 MiB − 4–5 KiB | accepted | present |
| Free | 25 MiB + 3–4 KiB | `554 5.7.1` at end of `DATA` | absent |
| Free | 26 MiB | `554 5.7.1 Recipient message size limit exceeded` at end of `DATA` | absent |
| Growth | 26 MiB | accepted | present |
| Growth | 39 MiB | accepted | present |
| Growth | 40 MiB − 4–5 KiB | accepted | present |
| Growth | 40 MiB + 3–4 KiB | `554 5.7.1` at end of `DATA` | absent |
| Growth | 41 MiB | `554 5.7.1 Recipient message size limit exceeded` at end of `DATA` | absent |
| Free + Growth in one transaction | 26 MiB | same `554 5.7.1` at end of `DATA` | absent in both |

These are real SMTP and mailbox observations, not just policy-unit tests. They
bracket both plan limits to within about 5 KiB, but do **not** establish the
single-byte boundary, alias-domain routing, policy timeouts, or every possible
sender route. The accepted synthetic test messages were expunged by their exact
Message-ID after INBOX verification; rejected messages were never delivered.
Recheck the live configuration after any mailcow or policy deployment. Do not
commit or log the private policy token.

- Free: 25 MiB total SMTP message size.
- Growth/Business: 40 MiB total SMTP message size.
- MIME/base64 overhead counts. A 25 MiB file will not necessarily fit in a
  25 MiB message; the compose-side ordinary attachment allowance and inbound
  SMTP message allowance are different measurements.
- Messages to multiple local recipients use the strictest plan. External
  recipients are ignored by the web policy endpoint.
- Authenticated outbound submissions are skipped by the Rspamd rule. Test
  this distinction against the live mailcow milter configuration before
  activation; the global Postfix cap still applies to those submissions.

## Production setup

1. Deploy the web policy endpoint, then provide a random 32+ character
   `MAIL_INBOUND_POLICY_TOKEN` through the production secret manager. Never
   place the token in the repository or in a screenshot.
2. Confirm the Mailcow API key can read `/api/v1/get/alias/all` and
   `/api/v1/get/alias-domain/all`. The endpoint follows active exact aliases,
   catch-all routes, and alias domains to their final local mailboxes and
   applies the strictest recipient plan. An unresolved local route or an
   unavailable Mailcow API produces a temporary SMTP failure, not an assumed
   Free tier or an unguarded acceptance. Before activation, compare this
   resolution against the live Mailcow routing inventory, including any
   custom recipient maps or forwarding rules that are not represented by
   these two API responses.
3. In mailcow, put `official_mail_size.lua` in
   `data/conf/rspamd/plugins.d/`. Add this block to
   `data/conf/rspamd/rspamd.conf.local`, with a private token and an HTTPS URL
   reachable from the Rspamd container:

   ```ucl
   official_mail_size {
     url = "https://mail.example.com/api/mailbox/inbound-size-policy";
     token = "<private-32+-character-token>";
   }
   ```

4. Do **not** set Postfix's global `message_size_limit` to 40 MiB: a valid
   40 MiB outbound attachment grows in transit due to MIME/base64 encoding.
   Keep the global limit at least 64 MiB (for example
   `message_size_limit = 67108864` in `data/conf/postfix/extra.cf`) and align
   Rspamd `max_message` so the plugin sees messages up to that limit. Align
   applicable ClamAV scan limits too. The plugin applies the 25/40 MiB
   **inbound recipient** rule; Postfix's global size cap is a separate
   transport safety bound. See
   [mailcow's message-size instructions](https://docs.mailcow.email/manual-guides/Postfix/u_e-postfix-attachment_size/).
5. Validate configuration inside the mailcow containers, restart affected
   services, and inspect logs. Send real SMTP test messages at 25 MiB,
   25 MiB + 1 byte, 40 MiB, and 40 MiB + 1 byte as Free, paid, and mixed-plan
   recipients plus paid-only, mixed-plan, catch-all and alias-domain
   destinations. Confirm rejection is an SMTP response, not delivery followed
   by deletion. Test Mailcow API and policy API timeouts produce a temporary
   SMTP failure.

This integration does not copy third-party large-attachment links into File
Drawer. Those links are controlled by the sender and remain in the original
message; only actual received MIME attachments are indexed in File Drawer.
