Inbound Unsubscribe Mail Handling

Turn the mailto-based "Unsubscribe" button in inbox clients (Apple Mail, Outlook, Gmail) into automated opt-outs by forwarding replies to the AI Visibility Tool.

TL;DR

Every product email carries a List-Unsubscribe header with a mailto: target (e.g. support@visibility-tool.com with subject "unsubscribe-onboarding"). To honour those replies you have to ingest that mailbox: either via an inbound-parse webhook (SendGrid / Mailgun / Postmark) pointed at /api/v1/inbound/unsubscribe, or via the optional IMAP poller. Both feed the same handler.

Two Ingestion Paths

  1. Inbound HTTP webhookPOST /api/v1/inbound/unsubscribe. Most managed inbound-parse providers (SendGrid Inbound Parse, Mailgun Routes, Postmark Inbound, Cloudflare Email Workers) can forward parsed messages as JSON or multipart/form-data. The endpoint is shared-secret authenticated.
  2. IMAP polling — an APScheduler job connects to a real mailbox you control, reads unread messages whose subject starts with unsubscribe-, processes them, and marks them seen. Only enabled when the IMAP env vars are set.

Required Environment Variables

Webhook (any inbound parser)
  • INBOUND_UNSUBSCRIBE_SECRET — shared secret the parser must present on every call. If unset, the endpoint returns 503 and inbound handling is disabled. Generate a long random string, e.g. openssl rand -hex 32.
IMAP poller (optional, only if you cannot use a webhook)
  • UNSUBSCRIBE_IMAP_HOST — IMAP server hostname. Setting this enables the poller.
  • UNSUBSCRIBE_IMAP_PORT — default 993
  • UNSUBSCRIBE_IMAP_USER — mailbox login
  • UNSUBSCRIBE_IMAP_PASSWORD — mailbox password or app password
  • UNSUBSCRIBE_IMAP_FOLDER — default INBOX
  • UNSUBSCRIBE_IMAP_SSL1 (default) for IMAPS, set to 0 only if you really need plaintext
  • UNSUBSCRIBE_IMAP_INTERVAL_MINUTES — poll interval in minutes, default 10

Example: SendGrid Inbound Parse

  1. In SendGrid, go to Settings → Inbound Parse and add a host (e.g. unsubscribe.visibility-tool.com) with the MX record that page tells you to create.
  2. Set the destination URL to:
    https://ai.visibility-tool.com/api/v1/inbound/unsubscribe?secret=YOUR_SECRET
    (or, preferred, leave the URL clean and configure SendGrid to send the secret in the X-Inbound-Secret header if your provider supports custom headers).
  3. Leave "POST the raw, full MIME message" off. SendGrid will then send a multipart/form-data payload with subject, text, from fields, which the endpoint understands out of the box.
  4. Point the unsubscribe alias on your mail provider (e.g. support@visibility-tool.com or a dedicated unsubscribe@… alias) at the parse host.
  5. Send a test mail with subject unsubscribe-onboarding and a valid token in the body. A successful call returns HTTP 200 with {"status": "done"}.

Mailgun Routes and Postmark Inbound work the same way — they post JSON or form-encoded payloads with subject / body-plain / sender (Mailgun) or Subject / TextBody / From (Postmark) and the endpoint handles all three shapes.

Example: IMAP Poller

When a managed inbound parser is not an option, point the app at a real mailbox. Set the env vars below, restart the app, and the scheduler will register a polling job automatically:

UNSUBSCRIBE_IMAP_HOST=imap.gmail.com
UNSUBSCRIBE_IMAP_PORT=993
UNSUBSCRIBE_IMAP_USER=unsubscribe@visibility-tool.com
UNSUBSCRIBE_IMAP_PASSWORD=********
UNSUBSCRIBE_IMAP_FOLDER=INBOX
UNSUBSCRIBE_IMAP_SSL=1
UNSUBSCRIBE_IMAP_INTERVAL_MINUTES=10

On startup you should see IMAP unsubscribe poller ENABLED (every 10 min) in the application logs. Each cycle reads up to 50 unread unsubscribe-* messages, opts the senders out, and marks the messages as Seen so they are not processed twice.

Verifying It Works

  • Send yourself a product email, then hit "Unsubscribe" in your inbox client. The reply should be opted out within seconds (webhook) or one polling cycle (IMAP).
  • Check /ai-visibility/admin/activity-log and filter by category system: successful opt-outs are logged as email_unsubscribed with source: email_mailto. Bad tokens or unknown accounts are logged as email_unsubscribe_failed.
  • Replies with a malformed or missing token are accepted, logged, and silently ignored — nothing is bounced back to the sender.
Bon à savoir
  • You only need one of the two paths. Pick the webhook if your transactional sender already offers inbound parsing — it is faster and has no mailbox to babysit.
  • Both paths share the same handler, so opt-outs always land in the right per-category preference (onboarding, billing reminders, weekly digest, product notifications) based on the signed token in the reply body.
  • Prefer sending the secret in a header (X-Inbound-Secret or Authorization: Bearer …). Query-string secrets work as a fallback but can leak into proxy and access logs.