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.
Resumen
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
-
Inbound HTTP webhook —
POST /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. -
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— default993UNSUBSCRIBE_IMAP_USER— mailbox loginUNSUBSCRIBE_IMAP_PASSWORD— mailbox password or app passwordUNSUBSCRIBE_IMAP_FOLDER— defaultINBOXUNSUBSCRIBE_IMAP_SSL—1(default) for IMAPS, set to0only if you really need plaintextUNSUBSCRIBE_IMAP_INTERVAL_MINUTES— poll interval in minutes, default10
Example: SendGrid Inbound Parse
- 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. - 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 theX-Inbound-Secretheader if your provider supports custom headers). - Leave "POST the raw, full MIME message" off. SendGrid will then send a multipart/form-data payload with
subject,text,fromfields, which the endpoint understands out of the box. - Point the unsubscribe alias on your mail provider (e.g.
support@visibility-tool.comor a dedicatedunsubscribe@…alias) at the parse host. - Send a test mail with subject
unsubscribe-onboardingand 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-logand filter by categorysystem: successful opt-outs are logged asemail_unsubscribedwithsource: email_mailto. Bad tokens or unknown accounts are logged asemail_unsubscribe_failed. - Replies with a malformed or missing token are accepted, logged, and silently ignored — nothing is bounced back to the sender.
Bueno saber
- 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-SecretorAuthorization: Bearer …). Query-string secrets work as a fallback but can leak into proxy and access logs.