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.
/integration/address-map/integration/address-mapWhen to Use It
- Your users receive mail at more than one address, through aliases, alias domains, or
+tagaddresses, 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:
- The exact address, as a user's
idor one of theiraddresses. - The same address with any
+tagremoved. - The same local part at the primary domain, when the address is at one of that domain's
aliases. - The domain's
catchAlluser. 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
rejectedin 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
400if the JSON doesn't match the format, such as"active": "yes"instead oftrue, 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": "..." }
]
}
rejectedlists up to 1,000 skipped entries, each with the value and the reason.counts.rejectedis the total.- Rejected sources never echo the address book's URL or password.
- The address map is saved as
address-map.jsonin 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"])