mailnix / Suppressions and bounce handling

    Core concepts

    Suppressions and bounce handling

    A suppression list is the set of addresses mailnix will refuse to mail. Every send runs the recipient set through this list and drops matches before the message reaches your provider, so you never re-mail a dead address and slowly tank your sender reputation.

    This article covers what the list is, how it fills up, and how to manage it.

    The model in one minute

    • The list is org-wide. It is one list shared by every project in your organization. A bounce caught in your marketing project protects your transactional project too.
    • Every source writes to the same list. Self-hosted MTAs (Stalwart, Postfix, Dovecot), cloud providers (AWS SES), ISP feedback loops, a CSV import, and manual adds all land in one place. You filter and audit it from /suppressions.
    • Hard vs. soft is decided for you. A hard failure (DSN 5.x.x, double-bounce, "user unknown") suppresses the address immediately. A soft failure (DSN 4.x.x, "mailbox full", greylisting) increments a per-address counter and suppresses only once it crosses the threshold, so a recipient on holiday is not suppressed on the first deferral. A complaint (someone hit "mark as spam") suppresses immediately.
    • Auto-suppression is opt-in. Ingesting bounce events always records them (metrics, trace events, counters). Whether a hard bounce automatically adds the address to the suppression list is a setting per organization. Contact support if you do not see the toggle in your account yet.
    • Filtering happens at send time. On every send mailnix consults the suppression list and drops matches before the message leaves. The trace records the drop with an auto_suppressed event so you can audit what was filtered.

    Where the entries come from

    Each entry on the list is tagged with the source that wrote it. You can filter by source on /suppressions or via GET /v1/suppressions?source=….

    Source tagWhere it comes from
    bounceA hard bounce arrived from any provider (SES SNS, Stalwart webhook, Postfix log, Dovecot mailbox, custom log shipper).
    complaintSomeone marked the message as spam at their mailbox provider.
    manualYou added the address from the dashboard or the API.
    importedA bulk CSV or JSON import (typically from a previous ESP).

    Feeding the list from your MTA

    mailnix can ingest bounces from any of the common patterns. Pick the one that matches what you already run.

    Self-hosted Stalwart

    Stalwart's webhook tracer POSTs delivery failures to mailnix; mailnix turns each one into a suppression entry.

    1. Mint a bounce source. Open /settings/bounce-sources, click Add bounce source, pick Stalwart, and Mint HMAC secret. You get back a webhook URL, a one-time HMAC secret, and a ready-to-paste Stalwart TOML block. REST equivalent: POST /v1/ingest-sources with {"provider":"stalwart"}. MCP equivalent: bounce_source_create(provider="stalwart").
    2. Paste the TOML into Stalwart and reload. It points a [tracer.webhook] at POST /v1/ingest/stalwart, signs each batch with the HMAC secret, and forwards only the delivery.dsn* and delivery.doubleBounce event types.
    3. Confirm auto-suppression is enabled for your org (ask support if the toggle is not visible).

    Verify: trigger a bounce to a known-dead address, then search /suppressions for it. It should appear with source=bounce.

    Postfix

    Postfix does not emit webhooks, so you bridge it one of two ways.

    Option A, log shipper (recommended). Tail /var/log/maillog with Vector or Filebeat, keep the status=bounced, expired, and deferred lines, and POST them as JSON Lines to POST /v1/ingest/logs?source=postfix. mailnix maps 5.x.x to hard, 4.x.x to soft, expired to hard, and deferred to soft. A sample Vector config is on the bounce-sources detail page after you mint a Postfix source.

    Option B, IMAP DSN mailbox (no log access needed). Point Postfix's bounce_notice_recipient at a dedicated mailbox, then give mailnix read-only IMAP credentials for it. mailnix polls the mailbox, parses each RFC 3464 DSN, suppresses the failed recipient, and marks the message \Seen. Set it up by minting a postfix bounce source with an imap_config:

    curl -sS -X POST https://api.mailnix.ch/v1/ingest-sources \
      -H "Authorization: Bearer $MAILNIX_API_TOKEN" \
      -H "Content-Type: application/json" \
      -d '{
            "provider": "postfix",
            "label": "Postfix bounce mailbox",
            "imap_config": {
              "host": "imap.example.com",
              "port": 993,
              "username": "bounces@example.com",
              "password": "***",
              "mailbox": "INBOX",
              "poll_interval_seconds": 30
            }
          }'

    Or tick Poll an IMAP DSN mailbox when adding the source in the dashboard.

    Dovecot mailbox

    Dovecot is the storage side: another MTA deposits bounce DSNs into a mailbox and Dovecot holds them. Two ways to drain it.

    Option A, mailnix polls the mailbox (recommended). Mint a dovecot bounce source with an imap_config (same shape as the Postfix IMAP option above). mailnix SEARCHes UNSEEN, parses each RFC 3464 DSN, suppresses the recipient, and marks the message \Seen. No script to maintain.

    Option B, doveadm CLI shipper (keeps IMAP creds off mailnix). A cron or systemd timer runs doveadm fetch against the bounce mailbox, pipes the output through a small parser, and POSTs JSON Lines to POST /v1/ingest/logs?source=dovecot. Mint a dovecot source from the dashboard to get the shipper script template.

    ISP feedback loops (mark-as-spam complaints)

    When a recipient hits "mark as spam" at their mailbox provider (Yahoo, Microsoft, Comcast, others), the provider emails you an ARF (RFC 5965) complaint report. Forward those to mailnix and the complainer is suppressed immediately as a complaint, not a counter-gated soft bounce.

    1. Enroll in each provider's feedback loop (FBL). Yahoo/AOL, Microsoft SNDS/JMRP, and Comcast all run their own programs. (Gmail does not have a per-message FBL; it reports aggregate rates via Postmaster Tools, so there is no ARF to forward.)
    2. Pipe the FBL mailbox at mailnix. Point a .forward, a sieve pipe, or a small cron job at POST /v1/ingest/arf with the raw RFC822 report as the body:
    cat "$ARF_MESSAGE_FILE" | curl -sS -X POST \
      https://api.mailnix.ch/v1/ingest/arf \
      -H "Authorization: Bearer $MAILNIX_API_TOKEN" \
      -H "Content-Type: message/rfc822" \
      --data-binary @-

    If a trusted forwarder (not the ISP directly) posts the reports, mint an arf bounce source and require HMAC-signed requests for an additional integrity check.

    AWS SES

    If you already use SES as a destination, you receive its bounce and complaint notifications over the SNS webhook. Those events flow through the same bounce-ingest pipeline as Stalwart and Postfix: a hard SES bounce or a complaint lands in the one org-wide suppression list, tagged source=bounce or source=complaint.

    Connect SES from /destinations and confirm the SNS subscription. No extra ingest endpoint to wire; the existing SNS webhook feeds suppression directly.

    Managing the list

    Import addresses from a previous ESP

    When you migrate off SendGrid, Mailchimp, or any other provider, bring their known-bad addresses with you so your first mailnix campaign does not re-discover bounces your old provider already paid for.

    • Dashboard: /suppressions, click Import, upload the CSV. Up to 50,000 rows per import.
    • REST: POST /v1/suppressions/bulk-import with a JSON or text/csv body, same 50k cap. The response reports added vs. skipped counts.
    • MCP / agent: email_suppression_bulk_import(rows=[...]) for batches up to 1,000 per call. For larger files use the REST endpoint or the dashboard upload.

    Each row needs an email; reason and project_id are optional. Imported entries are tagged source=imported, so you can always filter them back out (GET /v1/suppressions?source=imported) to audit or roll back the import. A tagged reason (for example "from SendGrid 2026-05") narrows it further.

    Un-suppress a recipient

    A mailbox that used to bounce sometimes comes back: the employee returned, the domain fixed its DNS, or you suppressed someone by mistake. Suppression is reversible and audited.

    1. Find the entry on /suppressions. REST equivalent: GET /v1/suppressions?q=alice@example.com. MCP equivalent: email_suppression_list(q="alice@example.com"). Check why it was suppressed (source, reason, and when) before removing it.
    2. Remove it. Click Remove on the row. REST equivalent: DELETE /v1/suppressions/{id}. MCP equivalent: email_suppression_remove(id). The next send to that address goes through.

    If the underlying problem is not actually fixed, the next bounce will just re-suppress the address (immediately, for a hard bounce), so removing an entry is safe to try.

    Add a single address manually

    You sometimes want to pre-emptively suppress an address: a competitor's abuse-report email, an internal alias that should never receive marketing, a known role address that bounces silently at the recipient's MTA.

    • Dashboard: /suppressions, click Add.
    • REST: POST /v1/suppressions with {"email":"...", "reason":"..."}.
    • MCP / agent: email_suppression_add(email, reason?).

    Manual entries are tagged source=manual.

    End-to-end verification

    A quick smoke test that proves "bounce in, suppressed, next send skips it", for any of the ingest sources above:

    1. Send a test message to an address you know will hard-bounce (for example bounce-test@your-own-domain with no such mailbox).
    2. Watch the bounce arrive. The bounce-source detail page shows an incrementing event counter.
    3. Confirm suppression. The bounced address shows up on /suppressions with source=bounce or complaint, and the original send's trace (look it up by Message-ID) gets an auto_suppressed event appended.
    4. Confirm the skip. Send to the same address again. The trace shows it filtered out before dispatch.

    If step 3 never happens but step 2 does, auto-suppression is off for your org. Contact support to turn it on.

    What's next

    • Traces for the data model that records every drop and every delivery event.
    • Destinations for connecting the providers whose bounces feed this list.
    • SMTP relay for keeping your existing SMTP app while gaining suppression-aware sending.