Push an Address Map

An address map tells SpamFoo Client which mailboxes your mail server has and which addresses deliver to each one: users, their other addresses, alias domains, and catch-all addresses. SpamFoo Client then files mail sent to an alias under the user who owns it, so each user's corrections and rules apply to all of their addresses. You can also give it a CardDAV address book for each domain to use for phishing protection.

PUT/integration/address-map
GET/integration/address-map

When to Use It

  • Your users receive mail at more than one address, through aliases, alias domains, or +tag addresses, and you want one set of corrections and rules per person.
  • A domain has a catch-all address that delivers to one mailbox.
  • You want SpamFoo Client to protect the people in a domain's address book against impersonation.

Push the whole address map each time it changes, or on a schedule. Each push replaces the one before it.

How Addresses Are Matched to Users

On every request that sends the proxy secret, SpamFoo Client looks up the X-User-ID header in the address map and replaces it with the owning user's id. It tries these in order and uses the first match:

  1. The exact address, as a user's id or one of their addresses.
  2. The same address with any +tag removed.
  3. The same local part at the primary domain, when the address is at one of that domain's aliases.
  4. The domain's catchAll user. This step is used only when the last push had "complete": true, and never for a list address.

An address with no match is left as it is. Only the X-User-ID header is matched. A userId in a JSON body or query string is not.

Request

Send the proxy secret in X-SpamFoo-Proxy-Auth, even from loopback. Send the body as JSON with Content-Type: application/json. The body can be up to 32 MiB.

{
    "source": "mymailserver",
    "complete": true,
    "domains": [
        { "name": "example.com", "aliases": ["example.net"], "catchAll": "[email protected]" }
    ],
    "users": [
        { "id": "[email protected]", "addresses": ["[email protected]"], "displayName": "Pat Lee" },
        { "id": "[email protected]" }
    ],
    "lists": [
        { "address": "[email protected]", "targets": ["[email protected]"] }
    ],
    "sources": [
        {
            "kind": "carddav",
            "domain": "example.com",
            "url": "https://mail.example.com/carddav/gal/",
            "username": "gal-reader",
            "password": "..."
        }
    ]
}
Field Type Description
source string A label for what sent the address map, up to 256 characters. It's returned in the summary.
complete boolean true when the push lists every address on the mail server. Catch-all matching is used only then. Defaults to false.
domains array Each domain has a name, optional aliases (other domain names that deliver to it), and an optional catchAll: the id or an address of a user in the same push.
users array Each user has an id, the address you send as X-User-ID, and optional addresses: the user's other addresses. Users can also have displayName, kind, active, role (user, domainAdmin, or systemAdmin), and adminDomains. SpamFoo Client stores and counts these, but they don't change results.
lists array Mailing lists, each with an address and its targets. A list address can't also be a user's address. Lists are stored and counted, but they don't change results.
sources array CardDAV address books, one per domain. See Address Book Sources.

Rules for Entries

  • Addresses and domains are compared without regard to letter case. International domain names can be sent in either form.
  • An address can belong to only one user, and an alias domain to only one domain. An alias can't also be listed as a domain.
  • An entry that breaks a rule is skipped and listed in rejected in the response. The rest of the push is still saved. A bad optional field on a user falls back to its default, and the user is kept.
  • The whole push is refused with 400 if the JSON doesn't match the format, such as "active": "yes" instead of true, or if it has more than 50,000 domains, 200,000 users, 50,000 lists, 1,000 sources, or 500,000 entries in total.
  • Each domain or user can have up to 1,000 aliases, addresses, or admin domains, and each list up to 10,000 targets. Entries past the limit are skipped and reported.

Address Book Sources

A source gives SpamFoo Client a CardDAV address book for one domain. SpamFoo Client reads it about once an hour and adds its contacts to the domain's protected identities, so mail that pretends to come from those people is caught. See Protect Against Phishing.

Field Description
kind Always carddav.
domain The domain the address book belongs to.
url The address book's address, up to 2048 characters. It must use https://, or http:// to the same machine.
username, password The account SpamFoo Client signs in with. The password is required the first time you send a domain. After that you can leave it out to keep the saved one. Passwords are stored encrypted.
tlsPinSha256 Optional, for a server with a self-signed certificate. The SHA-256 hash of the server's public key (its SubjectPublicKeyInfo), as 64 hex digits or base64. When it's set, SpamFoo Client accepts the server only if the key matches, and skips the usual certificate checks. Needs an https:// address.

When a later push leaves out a domain's source, SpamFoo Client stops reading that address book.

Response

PUT saves the address map and returns a summary. GET returns the same summary for the address map now in use, with version 0 before the first push.

{
    "version": 3,
    "updatedAt": "2026-10-13T14:00:00+00:00",
    "source": "mymailserver",
    "complete": true,
    "counts": {
        "domains": 1,
        "users": 2,
        "activeUsers": 2,
        "addresses": 1,
        "lists": 1,
        "sources": 1,
        "rejected": 1
    },
    "rejected": [
        { "value": "[email protected]", "reason": "..." }
    ]
}
  • rejected lists up to 1,000 skipped entries, each with the value and the reason. counts.rejected is the total.
  • Rejected sources never echo the address book's URL or password.
  • The address map is saved as address-map.json in the data directory, so it survives a restart.

Errors

Status Body Cause
400 {"error": "..."} The body is missing, isn't valid JSON for this endpoint, or is over a count limit. Nothing was changed.
401 Empty The proxy secret was missing or wrong.
413 {"error": "request_body_too_large", "maxBytes": 33554432} The body is over 32 MiB.
415 {"error": "Content-Type must be application/json"} The request wasn't sent as JSON.
429 {"error": "...", "code": "auth_blocked"} Too many wrong secrets came from this address. See Repeated Wrong Secrets.
500 {"error": "the address map could not be saved"} Saving failed. The previous address map stays in use.

Example

curl -s -X PUT http://localhost:16253/integration/address-map \
    -H "X-SpamFoo-Proxy-Auth: $SPAMFOO_PROXY_SECRET" \
    -H "Content-Type: application/json" \
    --data-binary @address-map.json
using System.Net.Http.Json;
using System.Text.Json;

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") };
http.DefaultRequestHeaders.Add("X-SpamFoo-Proxy-Auth", secret);

var addressMap = new
{
    source = "mymailserver",
    complete = true,
    domains = new[] { new { name = "example.com", aliases = new[] { "example.net" } } },
    users = new[] { new { id = "[email protected]", addresses = new[] { "[email protected]" } } }
};

using var response = await http.PutAsJsonAsync("/integration/address-map", addressMap);
response.EnsureSuccessStatusCode();
var summary = await response.Content.ReadFromJsonAsync<JsonElement>();
Console.WriteLine($"rejected={summary.GetProperty("counts").GetProperty("rejected")}");
import os
import requests

address_map = {
    "source": "mymailserver",
    "complete": True,
    "domains": [{"name": "example.com", "aliases": ["example.net"]}],
    "users": [{"id": "[email protected]", "addresses": ["[email protected]"]}],
}

response = requests.put(
    "http://localhost:16253/integration/address-map",
    headers={"X-SpamFoo-Proxy-Auth": os.environ["SPAMFOO_PROXY_SECRET"]},
    json=address_map,
    timeout=60,
)
response.raise_for_status()
print("rejected:", response.json()["counts"]["rejected"])