Skip to main content

Processing Feedback Loops and Spam Complaints

Subscribe to SES, SendGrid, and Postmark complaint notifications, parse ARF reports, suppress complainers instantly, and enroll in Yahoo CFL and Microsoft JMRP feedback loops.

When a recipient clicks "Report spam," that signal can reach you โ€” but only if you have wired up the feedback loops that carry it, and only if you act on it the instant it arrives. Ignored complaints are the fastest route to the spam folder for every other recipient on your domain. This guide covers parsing the complaint, suppressing the address immediately, and enrolling in the provider feedback loops that deliver these reports.

Root cause: the spam button generates an ARF complaint you must consume

When a user marks your message as spam, the mailbox provider records it. For senders enrolled in a feedback loop (FBL), the provider then sends back an ARF complaint (Abuse Reporting Format, RFC 5965) โ€” a multipart/report message with report-type="feedback-report" that identifies the offending message. If you send through an ESP, that ESP typically receives the FBL on your behalf and re-exposes it as a complaint event (SES Complaint, SendGrid spamreport, Postmark SpamComplaint).

From spam button to complaint report A recipient marking a message as spam causes the mailbox provider to send a structured report to the sender's feedback loop address, which must trigger suppression. One Click, One Report, One Suppression recipient presses "report spam" mailbox provider records it against you complaint report structured, machine-readable suppress at once never ask why first The recipient address is redacted by most providers โ€” you get enough to suppress, not enough to investigate. A complaint is a harder signal than a bounce: the mailbox exists and its owner does not want your mail.
Complaints are unambiguous, so the only correct response is immediate and permanent suppression.

The danger is purely statistical. Mailbox providers compute a complaint rate = complaints รท delivered, and weight it heavily. Gmail's 2024 bulk-sender requirements set a hard expectation: keep the user-reported spam rate below 0.3%, and ideally under 0.1% as reported in Postmaster Tools. Each complaint you fail to suppress means that recipient keeps getting mail and keeps complaining, and the rate climbs. The fix is mechanical: capture the complaint, stop mailing that person, log it for analysis. The suppression store that "stop mailing" relies on is built in bounce and complaint handling.

Complaint feedback loop A recipient marks mail as spam; the mailbox provider emits an ARF report through a feedback loop to your ESP, which forwards a complaint event that suppresses the address. Complaint Feedback Loop Recipient clicks "spam" Mailbox Provider Gmail / Yahoo / MS ESP / FBL ARF report Suppress + log for analysis Keep complaint rate under 0.1% โ€” Gmail's 2024 threshold is 0.3% maximum. Suppression must be immediate โ€” never on the next batch.
The path a spam click takes back to your system: act on it the instant the complaint event lands, not on the next send cycle.

Exact fix: subscribe, suppress immediately, log, and parse ARF

Subscribe to the complaint notification for whichever provider you send through, suppress the address the moment the event arrives, and persist the raw report for trend analysis. Complaints, unlike soft bounces, never retry โ€” there is no backoff that makes a spam report better.

import json

def handle_complaint(provider: str, payload: dict, store, log) -> None:
    """Normalize each provider's complaint shape, then suppress + log."""
    addresses = []

    if provider == "ses":
        # SES: Complaint notification arrives via SNS (same topic as bounces).
        msg = json.loads(payload["Message"]) if "Message" in payload else payload
        if msg.get("notificationType") != "Complaint":
            return
        c = msg["complaint"]
        addresses = [r["emailAddress"] for r in c["complainedRecipients"]]
        # complaintFeedbackType is the ARF feedback-type: "abuse", "fraud", etc.
        detail = c.get("complaintFeedbackType", "abuse")

    elif provider == "sendgrid":
        # SendGrid Event Webhook delivers an ARRAY; filter event == "spamreport".
        for ev in payload if isinstance(payload, list) else [payload]:
            if ev.get("event") == "spamreport":
                addresses.append(ev["email"])
        detail = "spamreport"

    elif provider == "postmark":
        # Postmark sends Type == "SpamComplaint" on the bounce webhook.
        if payload.get("Type") == "SpamComplaint":
            addresses.append(payload["Email"])
        detail = "SpamComplaint"

    for addr in addresses:
        # IMMEDIATE suppression โ€” stop ALL non-essential mail to this address now.
        store.suppress(addr, reason="complaint", source=provider, detail=detail)
        # Log the raw event so you can analyze WHICH campaigns drive complaints.
        log.write(json.dumps({"provider": provider, "email": addr,
                              "type": detail}) + "\n")

When you receive a raw ARF message directly (running your own FBL endpoint rather than going through an ESP), parse the message/feedback-report MIME part to extract the feedback type and the original recipient:

from email import message_from_bytes

def parse_arf(raw_mime: bytes) -> dict:
    msg = message_from_bytes(raw_mime)
    result = {}
    for part in msg.walk():
        # The feedback-report part carries Feedback-Type and Original-Rcpt-To.
        if part.get_content_type() == "message/feedback-report":
            report = message_from_bytes(part.get_payload(decode=True))
            result["feedback_type"] = report.get("Feedback-Type")      # "abuse"
            result["original_rcpt"] = report.get("Original-Rcpt-To")   # the complainer
            result["reported_domain"] = report.get("Reported-Domain")
    return result

"Stop all non-essential mail" is a deliberate scope: after a complaint you should suppress marketing and lifecycle mail outright, and send only genuinely required transactional messages (a password reset the user themselves just requested, a legal notice). Continuing to blast newsletters at someone who reported you is what turns one complaint into a pattern.

Variant: per-provider feedback-loop enrollment

ESPs receive most FBLs automatically, but if you operate your own IPs or want first-party data, enroll directly:

Feedback routes by provider Microsoft and Yahoo offer direct feedback loops requiring enrollment, Gmail reports only aggregate rates through Postmaster Tools, and most ESPs normalise what they receive into one event type. Not Every Provider Names the Recipient Per-message reports Microsoft, Yahoo Enrollment required, per IP or per domain. suppress the exact address Aggregate only Gmail A daily spam rate, with no identifiable recipients. watch the trend, not addresses Normalised by the ESP one complaint event type The ESP enrolls on your behalf and emits a webhook. the path most senders use
Building against the ESP's normalised event is what keeps one consumer working across providers with very different feedback models.
  • Yahoo Complaint Feedback Loop (CFL): enroll your sending domain/DKIM d= at Yahoo's sender portal; Yahoo then routes ARF reports to the address you register. Yahoo requires a valid DKIM signature to match complaints to a sender.
  • Microsoft JMRP (Junk Mail Reporting Program): enroll your sending IPs at the Smart Network Data Services portal; Outlook.com/Hotmail then forward ARF complaints for those IPs. Pair it with Microsoft SNDS (Smart Network Data Services), which reports spam-trap hits and complaint rates per IP even without individual ARF reports.
  • Gmail: there is no per-recipient FBL for normal senders; Gmail exposes the aggregate complaint rate through Postmaster Tools instead. The high-volume FBL is header-based and aggregated, so for Gmail you watch the dashboard rather than parse individual reports.

Whichever loops you enroll in, every report must funnel into the same handle_complaint path so suppression is uniform regardless of source.

Yahoo Complaint Feedback Loop (CFL) enrollment

Yahoo (which also covers AOL and Verizon mailboxes) runs a domain-keyed FBL. You enroll the DKIM d= domain your mail is signed with โ€” not your IP โ€” at Yahoo's sender feedback portal, and Yahoo routes every spam complaint for mail bearing that DKIM signature to the report address you register (typically fbl@yourdomain or an ESP-managed mailbox). Two preconditions trip people up:

  • Mail must carry a valid DKIM signature with the d= you enrolled, or Yahoo cannot attribute the complaint and simply drops it. DKIM alignment is therefore a prerequisite, configured per the SPF, DKIM, and DMARC reference.
  • Yahoo's CFL reports are redacted ARF โ€” the recipient's address is hashed/obscured in many fields, so you match the complaint back to a send via the Original-Rcpt-To / X- headers your ESP stamped, not by reading the address off the report body. Keep a mapping from message-id to recipient so a redacted report still resolves to an address you can suppress.

Microsoft JMRP and SNDS

Microsoft (Outlook.com, Hotmail, Live) gates its complaint data behind two free programs you enroll separately, and both are IP-keyed, so they matter mainly when you send from your own dedicated IPs rather than an ESP's shared pool:

  • JMRP (Junk Mail Reporting Program) is the actual feedback loop: enroll your sending IPs and a report address, and Outlook.com forwards an ARF complaint each time a user there reports your mail as junk. Wire these into the same handle_complaint path.
  • SNDS (Smart Network Data Services) is the aggregate dashboard: per-IP it shows complaint rate, spam-trap hits, filter result (inbox vs junk), and a red/yellow/green reputation band โ€” even for the complaints JMRP does not individually forward. Treat SNDS as the early-warning gauge and JMRP as the per-address suppression feed; you need both, because SNDS tells you that reputation is slipping while JMRP tells you which addresses to suppress.

Gmail Postmaster Tools

Gmail does not offer a per-recipient feedback loop for ordinary senders โ€” there is no ARF report per complaint to parse. Instead, verify your domain in Gmail Postmaster Tools and watch the aggregate dashboards: the Spam Rate chart (the user-reported rate Gmail holds you to, with the 0.1% target and 0.3% ceiling), the Domain/IP Reputation bands, and the Authentication pass rates for SPF, DKIM, and DMARC. Because there is no event to suppress on, your Gmail control loop is different: alert when the Postmaster spam-rate line approaches 0.1%, then use your own per-campaign complaint logs (from the providers that do report individually) to find and pause the message stream driving it. Gmail also participates in the standards-track Feedback-ID header โ€” stamp a structured Feedback-ID: campaign:stream:senderid on outbound mail so Postmaster Tools can break the spam rate down by your own dimensions.

Provider FBL type Keyed on What you get How you act
Yahoo / AOL CFL (per-complaint ARF, redacted) DKIM d= domain Individual complaints Suppress via message-id mapping
Microsoft Outlook.com JMRP (per-complaint ARF) Sending IP Individual complaints Suppress directly
Microsoft Outlook.com SNDS (aggregate) Sending IP Complaint rate, trap hits, reputation Alert / throttle
Gmail Postmaster Tools (aggregate) Domain (+ Feedback-ID) Spam rate, reputation, auth Alert + pause campaign

Transactional pipeline integration

The complaint endpoint should validate the provider signature, normalize, and enqueue โ€” exactly like the bounce path โ€” and dedupe on the provider event id so a retried webhook does not double-log. SES complaints share the SNS topic and handler described in handling hard bounces with Amazon SES and SNS, so a single subscriber covers both event types. Track complaint rate per campaign in your logs so you can find and kill the specific message stream driving complaints before the domain-wide rate crosses 0.1%.

Two operational details make the per-campaign attribution work in practice. First, stamp a structured Feedback-ID header (Feedback-ID: <campaign>:<stream>:<senderid>) on every outbound message โ€” Gmail and several other providers expose your spam rate broken down by it, and it lets you slice your own complaint logs by campaign without joining back to a separate send log. Second, keep a durable message-id โ†’ recipient mapping for at least your complaint window (30โ€“60 days), because redacted FBLs (Yahoo CFL especially) hand you a report you can only resolve to an address through that mapping. Without it, an enrolled FBL produces complaints you cannot suppress, which is worse than not enrolling, since the rate climbs while you watch.

Finally, distinguish the list-unsubscribe signal from a spam complaint. Mail that includes a one-click List-Unsubscribe/List-Unsubscribe-Post header (required by Gmail and Yahoo for bulk senders) lets a recipient unsubscribe instead of reporting spam; an unsubscribe should suppress the marketing/lifecycle stream for that address but is not the reputation-damaging event a spamreport is. Route unsubscribes to a reason="unsubscribe" suppression and reserve reason="complaint" for genuine spam reports so your complaint-rate metric is not polluted by people who simply opted out cleanly.

Validation & deployment checklist

Frequently Asked Questions

Why is the complaining recipient's address redacted?

Most providers redact it to protect the recipient from retaliation, which is the whole point of a feedback loop being safe to use. What you receive is enough to identify the message โ€” usually your own tracking headers survive โ€” so the practical workflow is to include a per-message identifier you can map back to the recipient yourself.

Should a complaint suppress the address permanently?

Yes for that stream, and it should not require a second occurrence. A complaint is an explicit statement that the recipient does not want the mail, and continuing to send is both a reputation risk and a compliance one. Where a genuinely transactional message must still reach them โ€” a security alert, a legal notice โ€” that is a separate decision made deliberately, not a default.

Does Gmail offer a feedback loop?

Not a per-message one. Gmail publishes an aggregate spam rate through Postmaster Tools instead, so complaints from Gmail recipients are visible as a trend rather than as individual events. That is why the reputation monitoring path exists alongside this one โ€” neither covers the other's providers.

What complaint rate is acceptable?

Below 0.1%, and the major providers treat 0.3% as the point at which a sender becomes a problem. Transactional streams should sit far below both; a transactional complaint rate approaching 0.1% usually indicates marketing content has been mixed into the stream, or that recipients do not recognise the sender.

Do we need to enrol separately with each provider?

If you send directly, yes โ€” each provider runs its own programme with its own enrolment. Most senders inherit their ESP's enrolment instead and receive the complaints as a normalised webhook event, which is simpler and is why building against the ESP's event type rather than the raw report format is the more portable choice.


โ† Back to Bounce and Complaint Handling