Send Corrections Back

This page shows when and how your integration should report user corrections to SpamFoo Client, and what SpamFoo Client does with them. Corrections are how SpamFoo Client learns each user's preferences.

Know When to Send a Correction

Send a correction when a person changes SpamFoo Client's result for a message:

  • Marks a message as spam, or moves it to the junk folder, after it was delivered as not spam.
  • Marks a message as not spam, or moves it out of the junk folder.
  • Moves a message to a different classification, such as from Promotions to Primary.

Do not send corrections for moves made by your own server rules or filters. Those are not user preferences, and they would teach SpamFoo Client the wrong thing. Send one correction per user action.

Before You Start

  • Proxy secret: correction requests always need the X-SpamFoo-Proxy-Auth header, even from localhost. See Authenticate Your Requests.
  • The message: send the raw message as the request body, the same way you send it to /classify-stream. The user's stored copy is fine.
  • The same user ID: send the same X-User-ID value you used when the message was classified. Otherwise the correction applies to a different user.
  • The original verdict (optional): if you stored isSpam and classification from the classification result, send them back as the original verdict.
  • Bulk corrections (optional): when you send many old corrections at once, such as after an import, add X-SpamFoo-Priority: bulk. They then wait in the background queue and can't slow down live mail. See Report Spam or Not Spam.
  • Retries (optional): if your integration can send the same correction twice, add X-SpamFoo-Event-Id with an ID that stays the same across retries. SpamFoo Client then applies it only once. See Send a Correction More Than Once.

Report Spam or Not Spam

POST/feedback-stream
Header Required Value
X-SpamFoo-Proxy-Auth Yes The proxy secret.
X-User-ID Yes The mailbox that made the correction, as a full email address.
X-Is-Spam Yes The correct answer: true if the user marked the message as spam, false if the user marked it as not spam.
X-Original-Is-Spam No The isSpam value SpamFoo Client returned when the message arrived. If you leave it out, SpamFoo Client classifies the message again when the correction arrives and treats that as the original verdict.
X-Sender-IP No IP address of the server that delivered the message to your mail server.
X-Sender-Email No Envelope sender (SMTP MAIL FROM).
X-Send-Email-Text No true only if the user agreed to share message text. See Share Message Text Only With Consent.
X-Subject-Strip-Prefixes No A tag your mail server adds to subjects, such as [SPAM]. SpamFoo Client removes it from the subject before it learns from the message. Send the header once per tag.
curl -X POST http://localhost:16253/feedback-stream \
    -H "Content-Type: message/rfc822" \
    -H "X-SpamFoo-Proxy-Auth: $SPAMFOO_PROXY_SECRET" \
    -H "X-User-ID: [email protected]" \
    -H "X-Is-Spam: true" \
    -H "X-Original-Is-Spam: false" \
    -H "X-Sender-IP: 203.0.113.50" \
    -H "X-Sender-Email: [email protected]" \
    --data-binary @message.eml
using System.Net.Http.Headers;

using var http = new HttpClient { BaseAddress = new Uri("http://localhost:16253") };
var secret = Environment.GetEnvironmentVariable("SPAMFOO_PROXY_SECRET")
    ?? throw new InvalidOperationException("Set SPAMFOO_PROXY_SECRET.");

await using var eml = File.OpenRead("message.eml");
var content = new StreamContent(eml);
content.Headers.ContentType = new MediaTypeHeaderValue("message/rfc822");

using var request = new HttpRequestMessage(HttpMethod.Post, "/feedback-stream") { Content = content };
request.Headers.Add("X-SpamFoo-Proxy-Auth", secret);
request.Headers.Add("X-User-ID", "[email protected]");
request.Headers.Add("X-Is-Spam", "true");
request.Headers.Add("X-Original-Is-Spam", "false");
request.Headers.Add("X-Sender-IP", "203.0.113.50");
request.Headers.Add("X-Sender-Email", "[email protected]");

using var response = await http.SendAsync(request);
Console.WriteLine($"{(int)response.StatusCode} {await response.Content.ReadAsStringAsync()}");
import os
import requests

headers = {
    "Content-Type": "message/rfc822",
    "X-SpamFoo-Proxy-Auth": os.environ["SPAMFOO_PROXY_SECRET"],
    "X-User-ID": "[email protected]",
    "X-Is-Spam": "true",
    "X-Original-Is-Spam": "false",
    "X-Sender-IP": "203.0.113.50",
    "X-Sender-Email": "[email protected]",
}
with open("message.eml", "rb") as eml:
    response = requests.post("http://localhost:16253/feedback-stream",
                             data=eml, headers=headers, timeout=60)
print(response.status_code, response.text)

A successful correction returns 200:

{
    "status": "personalization_only",
    "userId": "[email protected]"
}

See Report Spam or Not Spam for the full reference.

Report a Classification Fix

POST/tabs/feedback-stream

Use this endpoint when a user moves a message that is not spam to a different Email Classification. The message must have a From address.

Header Required Value
X-SpamFoo-Proxy-Auth Yes The proxy secret.
X-User-ID Yes The mailbox that made the correction, as a full email address.
X-Classification Yes The correct classification: primary, transactions, updates, or promotions. Not case-sensitive.
X-Original-Classification No The classification SpamFoo Client returned when the message arrived. If you leave it out, SpamFoo Client uses its own prediction for the message at correction time.
X-Send-Email-Text No true only if the user agreed to share message text.
X-Subject-Strip-Prefixes No A subject tag your mail server adds. Send the header once per tag.
curl -X POST http://localhost:16253/tabs/feedback-stream \
    -H "Content-Type: message/rfc822" \
    -H "X-SpamFoo-Proxy-Auth: $SPAMFOO_PROXY_SECRET" \
    -H "X-User-ID: [email protected]" \
    -H "X-Classification: primary" \
    -H "X-Original-Classification: promotions" \
    --data-binary @message.eml
using System.Net.Http.Headers;

using var http = new HttpClient { BaseAddress = new Uri("http://localhost:16253") };
var secret = Environment.GetEnvironmentVariable("SPAMFOO_PROXY_SECRET")
    ?? throw new InvalidOperationException("Set SPAMFOO_PROXY_SECRET.");

await using var eml = File.OpenRead("message.eml");
var content = new StreamContent(eml);
content.Headers.ContentType = new MediaTypeHeaderValue("message/rfc822");

using var request = new HttpRequestMessage(HttpMethod.Post, "/tabs/feedback-stream") { Content = content };
request.Headers.Add("X-SpamFoo-Proxy-Auth", secret);
request.Headers.Add("X-User-ID", "[email protected]");
request.Headers.Add("X-Classification", "primary");
request.Headers.Add("X-Original-Classification", "promotions");

using var response = await http.SendAsync(request);
Console.WriteLine($"{(int)response.StatusCode} {await response.Content.ReadAsStringAsync()}");
import os
import requests

headers = {
    "Content-Type": "message/rfc822",
    "X-SpamFoo-Proxy-Auth": os.environ["SPAMFOO_PROXY_SECRET"],
    "X-User-ID": "[email protected]",
    "X-Classification": "primary",
    "X-Original-Classification": "promotions",
}
with open("message.eml", "rb") as eml:
    response = requests.post("http://localhost:16253/tabs/feedback-stream",
                             data=eml, headers=headers, timeout=60)
print(response.status_code, response.text)

A successful correction returns 200:

{
    "status": "success",
    "userId": "[email protected]",
    "classification": "primary"
}

See Report a Classification Fix for the full reference.

By default, a correction that SpamFoo Client sends on to SpamFoo carries only numbers derived from the message, not its text. The X-Send-Email-Text header changes that for one correction.

  • Send X-Send-Email-Text: true only when the user has agreed to share message content for training.
  • With it, the correction also carries the message text with sensitive patterns removed, plus the headers and link addresses used for phishing training.
  • Without it, or with any other value, no message text is shared.

Know What Happens After a Correction

  • The user's memory updates right away. SpamFoo Client stores the corrected message in that user's memory of past corrections. Later messages that closely match it, especially from the same sender, can follow the user's correction instead of the global models.
  • Earlier results are cleared. Cached results are dropped, so the next classification reflects the correction.
  • Wrong memories are fixed. If the user's own memory caused the mistake, SpamFoo Client removes or weakens the memory entries that caused it.
  • Global model errors are reported. SpamFoo Client sends the correction to SpamFoo to help improve the global models only when the global models made the mistake. It skips this when the user's own personalization made the decision, or when the correction matches what the models already said. The user ID is hashed before it leaves the server.
  • Sender reputation is updated. A spam or not spam correction also counts toward the sending IP address and, when the message passed DMARC or aligned SPF, the sending domain.

For a plain-language version of this process, see How Corrections Are Used.

Understand How Corrections Personalize Results

Personalization comes entirely from corrections. Your integration does not need to create rules for each user.

  • Per user: each X-User-ID has its own memory. One user's corrections do not change another user's personalization.
  • Visible in results: when a user's memory decides a message, /classify-stream returns bypassReason set to knn_memory or knn_memory_cross_sender (the match came from a different sender). When memory decides the Email Classification, the value is tabs_knn (in bypassReason from /classify-stream, or in personalizationSource from /tabs/classify-stream).
  • Admin-wide rules are separate: allow and block rules set in the admin console apply to many users at once. See Manage Spam Rules and Senders.

To learn more, see How Personalization Works.

Remove a User's Personal Data

To delete everything SpamFoo Client has learned for one user, for example when a mailbox is deleted, call both endpoints. The first clears spam personalization and the second clears classification personalization.

curl -X DELETE http://localhost:16253/personalization/data \
    -H "X-SpamFoo-Proxy-Auth: $SPAMFOO_PROXY_SECRET" \
    -H "Content-Type: application/json" \
    -d '{"userId": "[email protected]"}'

curl -X DELETE http://localhost:16253/personalization/tabs/data \
    -H "X-SpamFoo-Proxy-Auth: $SPAMFOO_PROXY_SECRET" \
    -H "Content-Type: application/json" \
    -d '{"userId": "[email protected]"}'

See Manage Personal Data for details.

Handle Errors

Status Meaning
400 A required header is missing or has a bad value, the body is empty, the message could not be parsed, or (for classification fixes) the message has no From address. The error field says which.
401 The proxy secret is missing or wrong.
413 The message is larger than the configured size limit.
429 Standalone mode only. Too many requests from this address sent a wrong secret, so it is blocked for a while.
503 Only for corrections sent with X-SpamFoo-Priority: bulk: SpamFoo Client is busy. Wait the seconds in the Retry-After header and send it again.

A failed correction does not affect mail delivery. Log it, and retry later if the failure was temporary.