Report Spam or Not Spam

Tell SpamFoo Client that a user marked a message as spam, or rescued it from junk as not spam. SpamFoo Client learns from the correction for that user and may report it to SpamFoo to improve the global models.

POST/feedback-stream

When to Use It

  • A user moves a message into the junk folder, or clicks a "Mark as spam" control.
  • A user moves a message out of the junk folder, or clicks a "Not spam" control.
  • To report that a message belongs in a different Email Classification, use Report a Classification Fix instead.

What SpamFoo Client does with a correction:

  • Learns for the user: it stores an embedding of the message (a list of numbers that represents its content, not the text) in the user's correction memory. Later messages from the same sender, or very similar messages, are judged with that correction in mind. See How Personalization Works.
  • Keeps a local copy: it saves a compressed copy of the corrected message on the SpamFoo Client server, so the correction can be rebuilt when the embedding model changes.
  • Reports model mistakes: when the correction disagrees with what the global models decided, SpamFoo Client queues an anonymized correction for SpamFoo. The user ID is hashed first. It skips the report when the user's own past corrections decided the message, or when the global models already agreed. See How Corrections Are Used.
  • Feeds reputation: when SpamFoo Client reports to SpamFoo, the correction also counts toward the reputation of the IP address in X-Sender-IP and, when the message passed DMARC or aligned SPF, the sender's domain.

Send the message the user corrected, as it is stored in their mailbox. This endpoint has no license check.

Request

The body is the raw message (raw .eml content), sent unchanged. See Send the Message in the Request Body.

Header Required Type Description
Content-Type Recommended string message/rfc822
X-SpamFoo-Proxy-Auth Yes string The proxy secret, required even from loopback. See Authenticate Requests.
X-SpamFoo-Priority No bulk Send bulk for corrections that aren't from a user acting right now, such as a batch import. They run on the background queue with their own limit, so they can't slow down live mail. Without it, the correction is handled right away.
X-User-ID Yes string The user who made the correction. Use the same value you send as X-User-ID when classifying.
X-Is-Spam Yes true or false true when the user marked the message as spam. false when the user marked it as not spam.
X-Original-Is-Spam No true or false The isSpam value SpamFoo returned when the message arrived. If you omit it, SpamFoo Client scores the message again at correction time and uses that verdict instead, which can differ from the original one.
X-Sender-IP No IPv4 or IPv6 IP address of the server that delivered the message. Used for authentication checks and IP reputation.
X-Sender-Email No email address Envelope sender (SMTP MAIL FROM). Used for authentication checks.
X-Send-Email-Text No true or false Send true only when the user agreed to share message content. The correction report to SpamFoo then includes the message text, with email addresses, phone numbers, card numbers, and similar details replaced by placeholders, plus phishing details built from the message's text, headers, and links. Default false.
X-Subject-Strip-Prefixes No string, repeatable A literal tag your mail server adds to subjects, such as [SPAM]. SpamFoo Client removes it before it analyzes or reports the subject. Send the header once per tag. A single JSON array of strings, such as ["[SPAM]", "[EXTERNAL]"], is also accepted.
X-SpamFoo-Dont-Report No true or false Send true when the admin has turned off reporting to SpamFoo. The correction still trains the user's local memory, but nothing about it is sent to SpamFoo. Default false. Any other value is rejected with 400 Bad Request.
X-SpamFoo-Event-Id No string, up to 128 characters Your own ID for this correction, so a retry isn't applied twice. See Send a Correction More Than Once.

Response

A successful request returns 200:

{
    "status": "personalization_only",
    "userId": "[email protected]"
}
Field Type Description
status string Always personalization_only on success. It does not tell you whether a report was queued for SpamFoo.
userId string The X-User-ID you sent.

Send a Correction More Than Once

If your integration can send the same correction twice, such as a retry after a lost answer, add X-SpamFoo-Event-Id with an ID that stays the same across retries.

  • The ID is 1 to 128 printable ASCII characters with no spaces. An ID that doesn't fit is ignored, and the correction is applied normally.
  • A repeat of an ID that was already applied for the same user on this endpoint returns 200 with {"status": "duplicate", "duplicate": true, "userId": "[email protected]"} and changes nothing.
  • An ID counts as used only after its correction succeeds, so you can retry a failed attempt with the same ID.
  • SpamFoo Client remembers IDs in memory for 24 hours. A restart forgets them.
  • Without the header, every correction is applied, because a user can correct the same message more than once.

Errors

Status Body Cause
400 {"error": "X-User-ID header is required"} X-User-ID was missing or blank.
400 {"error": "X-Is-Spam header is required (true/false)"} X-Is-Spam was missing or not true or false.
400 {"error": "No email content provided"} The body was empty.
400 {"error": "Email could not be parsed"} The body is not a readable message.
401 Empty The proxy secret was missing or wrong.
413 {"error": "request_body_too_large", "maxBytes": ...} The message is over the body size limit.
429 {"error": "...", "code": "auth_blocked"} Too many wrong secrets came from this address. See Repeated Wrong Secrets.
503 {"error": "server_busy", "code": "server_busy", ...} or {"error": "Classification queue timeout", "code": "queue_timeout", ...} Only with X-SpamFoo-Priority: bulk. SpamFoo Client is busy. Wait the seconds in Retry-After and send it again.

Example

This example reports that a user rescued a message from junk, and that SpamFoo had originally marked it as spam.

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: false" \
    -H "X-Original-Is-Spam: true" \
    -H "X-Sender-IP: 203.0.113.50" \
    -H "X-Sender-Email: [email protected]" \
    --data-binary @message.eml
using System.Net.Http.Headers;

var secret = Environment.GetEnvironmentVariable("SPAMFOO_PROXY_SECRET")
    ?? throw new InvalidOperationException("Set SPAMFOO_PROXY_SECRET.");

using var http = new HttpClient { BaseAddress = new Uri("http://localhost:16253") };

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", "false");
request.Headers.Add("X-Original-Is-Spam", "true");
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": "false",
    "X-Original-Is-Spam": "true",
    "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)