<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Email List Subscribe / Unsubscribe — Arc Plan</title>
<style>
body { font-family: system-ui, sans-serif; max-width: 760px;
margin: 1.5em auto; padding: 0 1em; line-height: 1.5;
color: #1a1a1a; }
h1 { font-size: 1.4em; }
h2 { font-size: 1.1em; margin-top: 1.4em; }
ol { margin: 0.3em 0; }
li { margin: 0.25em 0; }
.check { color: #1a7f37; font-weight: bold; }
.next { color: #b35900; font-weight: bold; }
.q { color: #8250df; }
code { background: #f2f2f2; padding: 0 0.25em; border-radius: 3px; }
em { color: #555; }
</style>
</head>
<body>
<h1>Email List Subscribe / Unsubscribe</h1>
<p style="margin:.8rem 2rem;padding:.7rem 1rem;background:#eef2f7">
<strong>Arc Post-Mortem Summary.</strong>
Built the email-list subscribe and
unsubscribe flow. It added a per-group mail subscription with its checkbox
(Phase A), List-Unsubscribe headers on group and bulk mail plus a web
unsubscribe path (Phase B), registration and whole-domain unsubscribe (Phase
C), and the bot mailbox with MailSite configured (Phase D). Existing behavior
was verified first and the design decisions were resolved with Chris.</p>
<p><em>Arc started 2026-06-17. This is the List-Unsubscribe piece of
v10 ToDo item 12 (Message-ID / MIME-Version / Content-Type /
Content-Transfer-Encoding and DKIM signing already landed; the
List-Unsubscribe headers plus the web + mailto unsubscribe machinery
are what remain). Goal: let a
user turn off bulk mail from a group, keep unsubscribed users out of
the recipient lists, and attach RFC 8058 List-Unsubscribe headers
(a one-click URL and a mailto address) to bulk and registration mail,
backed by a web endpoint and — when MailSite is configured
— a <code>bot@<domain></code> mailbox that processes mailto
unsubscribes.</em></p>
<h2>What exists today (verified)</h2>
<ol>
<li>Membership lives in <code>USER_GROUP (USER_ID, GROUP_ID, STATUS,
JOIN_DATE)</code>, primary key <code>(GROUP_ID, USER_ID)</code>
(<code>ProfileModel</code> fresh schema). No mail-subscription
column yet.</li>
<li><code>GroupModel::getThreadFollowers($thread_id, $owner_id,
$exclude_id)</code> returns USER_ID / USER_NAME / EMAIL for everyone
who posted in a thread plus the owner; it does not currently know
the thread's group. <code>GroupModel::getGroupUsers($group_id,
$filter, $sorts, ...)</code> returns every member.</li>
<li>Bulk mail is queued as serialized four-element tuples
<code>[subject, body, to, from]</code> in per-batch
<code>.txt</code> files; <code>BulkEmailJob</code> reads them and
calls <code>$mail_server->sendImmediate(...)</code>. The enqueue
points with group context are in <code>SocialComponent</code> near
line 1516 (thread followers) and line 1911 (group users).</li>
<li><code>MimeMessage::build($from, $to, $subject, $body,
$attachments, $extra_headers)</code> already accepts an
<code>$extra_headers</code> array — the natural seam for the
List-Unsubscribe headers.</li>
<li><span class="check">✓</span> DKIM signing is already wired:
<code>SmtpClient::sendImmediate</code> wraps the built message in
<code>self::dkimSign(...)</code> (which calls
<code>DkimKey::sign</code> when a selector is configured, pass-through
otherwise), so the List-Unsubscribe headers added via
<code>$extra_headers</code> end up inside the signed message.</li>
</ol>
<h2>Phase A — per-group mail subscription + the checkbox</h2>
<ol>
<li><span class="check">✓</span> Added a <code>RECEIVE_MAIL</code>
column to <code>USER_GROUP</code> (default 1 = subscribed) in the
fresh schema, plus the portable v108 <code>ADD ... DEFAULT 1</code>
migration that switches it on for every existing membership.</li>
<li><span class="check">✓</span> <code>GroupModel</code>:
<code>getGroupUsers</code> gained a <code>$subscribed_only</code> flag
(filters <code>RECEIVE_MAIL <> 0</code>);
<code>getThreadFollowers</code> gained a <code>$mail_only</code> flag
that, via a correlated <code>NOT EXISTS</code> on
thread→group→<code>USER_GROUP</code>, drops members with
<code>RECEIVE_MAIL = 0</code>. The two mail callers in
<code>SocialComponent</code> (post→thread-followers,
owner-post→group-users) now pass the flag; the contact-requests
caller of <code>getThreadFollowers</code> stays unfiltered.</li>
<li><span class="check">✓</span> A "Receive Group Mail"
checkbox on the group settings page, shown in <em>both</em> the
editable (<code>editgroup</code>) and the read-only
(<code>infogroup</code>) views as a row in the settings table (tied
by its HTML <code>form</code> attribute to a separate hidden form, so
ticking it saves only this choice and the main Save leaves it alone;
a new <code>changemailpref</code> case checks the
viewer is a member, via <code>checkUserGroup</code>, then writes
their <code>RECEIVE_MAIL</code> and returns to the page it came
from). Driven by new <code>GroupModel</code>
<code>getMailSubscription</code> / <code>setMailSubscription</code>.
So a read-only viewer can still unsubscribe themselves.</li>
<li><span class="check">✓</span> Thread level: a
<code>THREAD_UNSUBSCRIBE</code> table (v109) recording per-user
opt-outs; <code>GroupModel</code> <code>isThreadUnsubscribed</code> /
<code>setThreadUnsubscribe</code>; the second arm of the
<code>getThreadFollowers</code> exclusion (a <code>NOT EXISTS</code>
on that table); and a Follow / Unfollow button on the thread view,
wired to a <code>togglethreadmail</code> feed action that sets the
opt-out (idempotent, so a refresh does not re-toggle) and returns to
the thread.</li>
<li><span class="check">✓</span> Regression fix (DB-error
sweep across recent arcs): positional <code>INSERT</code>s broke
when a column was added to a table they target. (1)
<code>GroupModel::addUserGroup</code>'s
<code>INSERT INTO USER_GROUP VALUES (?,?,?,?)</code> now names its
columns so <code>RECEIVE_MAIL</code> defaults — this is what
made joining / creating a group fail. (2) Createdb.php fresh-install
seeds: the eight USER_GROUP seeds now supply <code>RECEIVE_MAIL</code>
(= 1) and the four SOCIAL_GROUPS seeds now supply
<code>RENDER_ENGINE</code> (the OPTIONS arc added it to the schema at
v106 but never to Createdb.php). (3) Swept every table our v100+
migrations added a column to: MAIL_ALIAS (v102, DOMAIN),
SOCIAL_GROUPS (v106), USER_GROUP (v108) — MAIL_ALIAS has no
positional inserts, so the other two were the only casualties.
Verified the fixed seeds insert cleanly at the current schema and the
old value-counts still fail. Going-forward rule: when a table is
altered, scan every insert into it.</li>
<li><span class="check">✓</span> Reworked thread level from
opt-out to opt-in (Chris's call): by default you do <em>not</em>
follow a thread; posting to or starting a thread follows it, and the
state icon toggles it. The <code>THREAD_UNSUBSCRIBE</code> table (which
recorded "stop mailing me") is replaced by <code>THREAD_FOLLOW</code>
(which records "mail me about this thread"), via a v110 migration that
drops the old table and creates the new one. <code>GroupModel</code>
gains <code>isThreadSubscribed</code> /
<code>setThreadSubscribe</code> and a <code>getThreadSubscribers</code>
that lists a thread's followers with their addresses; the post-notify
path mails that list, and <code>getThreadFollowers</code> reverts to
its plain "thread participants" form for the contact-requests caller.
The toggle action and the (temporary) Follow / Unfollow button now use
a <code>follow</code> flag. <em class="q">Open choices: (a) icon will
be a bell (following) / bell-with-slash (not following) unless Chris
prefers another; (b) thread-follow currently mails replies independent
of the group's RECEIVE_MAIL switch — following a thread mails you
even with group mail off (alternative would be to treat RECEIVE_MAIL as
a master mute).</em></li>
<li><span class="check">✓</span> UI: replaced the big button
with a small toggle icon — a small bell (following) or
bell-with-slash (not following) that shows the current state and
toggles on click. The two bell icons are added to the standard
<code>iconlink</code> helper. The breadcrumb bell uses
<code>renderButton</code> (a boxed icon link); the feed-row bell uses
<code>renderFormButton</code> with an onclick, exactly like the
AI-summary button beside it, so they match in size and styling. Also
tidied that helper method: fixed the <code>additional_attibutes</code>
parameter typo (now <code>additional_attributes</code>) and switched
the feed's AI-summary <code>echo</code>s to <code>e()</code>. The bell
sits in the breadcrumb menu bar right after the thread name on the
thread view, and on each thread row in
the feed just after the AI-summary control. The feed needs to know the
follow state of every thread it lists, so the controller looks them up
in one query (<code>getFollowedThreads</code>) and the toggle action
now takes a <code>follow_thread</code> id so a feed row can follow the
thread it names and return to the feed.</li>
</ol>
<h2>Phase B — List-Unsubscribe on group / bulk mail + the web
endpoint</h2>
<p><em>Foundation (done): the make / parse primitive that items 3 and 4
both share — <code>UnsubscribeToken::make($user_id, $group_id)</code>
/ <code>parse($token)</code> in
<code>src/library/mail/UnsubscribeToken.php</code>: the two ids plus an
<code>AUTH_KEY</code>-keyed HMAC, joined by dots, URL safe. Item 3 builds
the link with <code>make</code>; item 4 validates it with
<code>parse</code>. Covered by <code>UnsubscribeTokenTest</code>
(round-trip, tampered ids, forged signature, malformed input). This is
preparation, not a numbered deliverable.</em></p>
<ol>
<li><span class="check">✓</span> Decided the unsubscribe mailto
address:
<code>MailSiteFactory::unsubscribeMailtoAddress($root_email)</code>
returns <code>bot@<first MAIL_DOMAIN></code> when this Yioop runs
its own mail, otherwise the root account address passed in. Kept it
pure (reads config, takes the root e-mail as a parameter, touches no
model) so as not to widen the factory's existing model-use exception.
(Also fixed two pre-existing >80 lines in GroupfeedElement noticed
while here.)</li>
<li><span class="check">✓</span> Carried the unsubscribe header
through to the send path. Decided to precompute the List-Unsubscribe
header at enqueue and carry it (rather than stuff raw group +
recipient into the mail tuple): added an <code>$extra_headers</code>
argument to <code>SmtpClient::send()</code> and <code>sendQueue()</code>
(forwarded to <code>sendImmediate</code>, which already supported it),
queued records now serialize a five-element tuple, and
<code>BulkEmailJob</code>'s two readers accept four- or five-element
tuples so queued files written before the upgrade still send (with no
extra headers). Also added the missing <code>is_array</code> guard in
<code>doTasks</code> so a corrupt queued record no longer trips
<code>count(false)</code>.</li>
<li><span class="check">✓</span> Attach, via
<code>MimeMessage::build</code>'s <code>$extra_headers</code>:
<code>List-Unsubscribe: <mailto:...>, <https://...></code>
and <code>List-Unsubscribe-Post: List-Unsubscribe=One-Click</code>
(RFC 8058). Built by a pure
<code>UnsubscribeToken::listUnsubscribeHeaders($user_id, $group_id,
$base_url, $mailto_address)</code> — the web link is the item 4
endpoint with a <code>make</code> token, the mailto is item 1's
<code>unsubscribeMailtoAddress</code> carrying the same token in its
subject, plus the one-click marker. Wired into the group-users bulk
path in <code>SocialComponent</code> (the new-thread-by-owner
notification): each recipient's message now carries headers that turn
off that user + group's mail on the site it went out from. Covered by
a new <code>listUnsubscribeHeadersTestCase</code>; full suite green.
<span class="q">Open (Chris to decide):</span> the other group mail
path, thread-follow notifications, is deliberately left untouched
— a <em>group</em> unsubscribe there would be wrong (it would
turn group mail off while the follow mail, which is independent of
<code>RECEIVE_MAIL</code>, kept arriving). That path wants its own
thread-level unsubscribe (drop <code>THREAD_FOLLOW</code> for that
thread), which is a separate token + endpoint action. The owner
join-request notice (a single-recipient operational mail) is left as
plain mail.</li>
<li><span class="check">✓</span> A web unsubscribe endpoint (a route, like the robots / mta-sts
routes): the signed token identifies user + group; a one-click POST
performs the unsubscribe (a plain GET shows a small confirm page so
a link preview fetch does not silently unsubscribe).
<span class="check">✓</span> Decided (Chris): the endpoint
lives in <code>ApiController</code>, whose role is being widened from
the LLM activities (summarize / transcribe / translate) to general
endpoints. As prep, <code>ApiController</code> was brought to style
conformance this patch (wrapped four long lines, the one
<code>//</code> comment turned into <code>/* */</code>,
<code>$result_obj</code> renamed off the banned <code>_obj</code>
suffix to <code>$decoded</code>, generic <code>$model</code> renamed
to <code>$llm_model</code>).
<span class="check">✓</span> Done (this session): added the
<code>unsubscribe</code> activity to <code>ApiController</code>. It
cleans the <code>token</code>, runs it through
<code>UnsubscribeToken::parse</code>, and on a bad token shows a short
"invalid or expired" page. For a good token it looks up the group
name; a GET shows a confirmation page whose button POSTs the token
back (so link scanners following the GET cannot unsubscribe anyone),
and a POST calls <code>GroupModel::setMailSubscription(..., 0)</code>
to turn that group's mail off for that user. The page is a small
self-contained localized HTML document written straight to the
response and ended with <code>webExit()</code>, the same emit-and-stop
idiom the API JSON path uses, so it renders identically on the atto
server and Apache. Five <code>api_unsubscribe_*</code> locale strings
added. Covered by a new <code>ApiUnsubscribeTest</code> (the repo's
first controller test): it builds a throwaway membership row in an
isolated SQLite database, injects it into the controller, and checks
that an invalid link shows no form and changes nothing, a GET shows
the confirm form carrying the token without unsubscribing, and a POST
actually flips <code>RECEIVE_MAIL</code> to off. Full unit suite
40907/40907 green.
<span class="check">✓</span> Refactored (Chris's review): the
activity no longer prints HTML or branch on
<code>$_SERVER['REQUEST_METHOD']</code>. It now returns view data and
lets a new <code>UnsubscribeView</code> (web layout) draw the page
directly — no separate element, since nothing here is swapped
out — and it decides
confirm-vs-unsubscribe from a request parameter
(<code>List-Unsubscribe=One-Click</code>, the RFC 8058 marker the
mail client or the confirm button sends) read through
<code>$_REQUEST</code>, keeping the endpoint agnostic to GET vs POST.
Test reworked to assert the returned state and the flipped
subscription; full suite 40913/40913 green.
<span class="check">✓</span> Extended (this patch) for
whole-site unsubscribe: the same endpoint now also accepts the
whole-site (email-address) token. After the member-and-group parse
fails it tries <code>UnsubscribeToken::parseEmail</code>; a valid
one-click then calls <code>MailSuppressionModel::suppress</code> to
turn off all of this site's mail to that address, and
<code>UnsubscribeView</code> gained an all-mail confirm / done
message (two new <code>api_unsubscribe_all_*</code> locale strings).
Routing is by token shape, so the group path is untouched.
<code>ApiUnsubscribeTest</code> gains whole-site confirm and
one-click-suppress cases.</li>
</ol>
<h2>Phase C — registration / whole-domain unsubscribe</h2>
<ol>
<li><span class="check">✓</span> Done (this patch): a suppressed
email address is burned — it can no longer be used to register.
At registration <code>dataIntegrityCheck</code> consults the
suppression list and, when the address is on it, refuses the account
and shows the user a message that the address can no longer be used;
no account is created and no activation mail is sent. The check is
gated to registration only (password recovery stays transactional, so
an existing user can still recover). Covered by
<code>RegisterSuppressionTest</code>.</li>
<li><span class="check">✓</span> Done: a suppression store keyed
by email address, and the send path skips any address on it.
Store — a <code>MAIL_SUPPRESSION</code> table (one row per
address, unique index on EMAIL) added to the fresh schema and via
<code>upgradeDatabaseVersion111</code> (DATABASE_VERSION 110 →
111), plus a <code>MailSuppressionModel</code> with
<code>isSuppressed</code> / <code>suppress</code> /
<code>unsuppress</code> (case-insensitive, mirrors
<code>MailSenderAllowModel</code>), covered by
<code>MailSuppressionModelTest</code>.
Skip — the check lives in <code>BulkEmailJob</code> (the queue
consumer). Before sending each queued message it holds the recipient
back only when the message is list mail — it carries a
<code>List-Unsubscribe</code> header — and the address is on the
suppression list. Transactional mail such as a password reset carries
no <code>List-Unsubscribe</code> and is always sent, so a suppressed
address can still recover their account. Covered by
<code>BulkEmailJobTest</code>.</li>
<li><span class="check">✓</span> Done (this patch): the
activation email offers the whole-site unsubscribe.
<code>RegisterController::sendActivationMail</code> now adds the
whole-site List-Unsubscribe headers (one-click via the bot mailbox
and web links, from <code>listUnsubscribeHeadersForEmail</code>) and
a plain visible "stop all mail from this site" line at the foot of
the message pointing at the web endpoint
(<code>UnsubscribeToken::unsubscribeUrl</code>, shared with the
header builder so the two links never drift). Only the activation
mail carries it — password recovery stays plain. Safe because
the registration gate already refuses a suppressed address, so this
mail never goes to a suppressed recipient. The group and whole-site
header builders now share one private helper; covered by
<code>UnsubscribeTokenTest</code>.</li>
</ol>
<h2>Phase D — the <code>bot</code> mailbox (MailSite configured)</h2>
<ol>
<li><span class="check">✓</span> Done (this patch): the username
<code>bot</code> is reserved — it is added to the registration
forbidden-name list, so no account may be created as <code>bot</code>
and the <code>bot@<domain></code> mailbox identity stays owned by
the system. Covered by <code>RegisterSuppressionTest</code>.</li>
<li><span class="check">✓</span> Done: <code>BulkEmailJob</code>
reads incoming <code>bot@<domain></code> mail and applies the
same unsubscribe a click would. The forgery guard
(<code>UnsubscribeToken::makeEmail</code> / <code>parseEmail</code>,
earlier patch) signs the email address itself with the site secret
under a distinct all-mail prefix; the bot trusts only this
signature, never the spoofable <code>From</code>, so an altered
signature, a signature reused under a different address, or a plain
unsigned address are all rejected (covered by
<code>UnsubscribeTokenTest</code>). The reader
(<code>processBotMailbox</code>, this patch) runs at the top of the
name-server mail tick: when a local store has a <code>bot</code>
INBOX it walks each message, unfolds the Subject (the mailto links
produce "unsubscribe <token>"), and on a verified token
suppresses the address (whole-site token) or turns off the group
(member-and-group token) through <code>applyUnsubscribeToken</code>,
then flags and expunges every examined message so stray mail to the
bot address is cleared too. The store is reached through a new
shared <code>MailSiteFactory::storage()</code> and the mailbox name
lives in one <code>UNSUBSCRIBE_MAILBOX</code> constant. Covered by
<code>BulkEmailJobTest</code> (group token, whole-site token, forged
token ignored, mailbox process-and-clear, stray-mail clear).</li>
</ol>
<h2>Other work this phase</h2>
<ol>
<li class="q">Found-bug fix folded in this patch:
<code>MailRecordCache</code> did <code>new LRUCache(...)</code>
without importing it, so it resolved to a non-existent
<code>...\library\mail\LRUCache</code> and threw "class not
found" the moment <code>codetool unit</code> reached
<code>DmarcCheckTest</code> (which warms the cache). Added the
missing <code>use</code>; the full unit suite now runs to
completion. (The two Index test failures that then surfaced are
sandbox-only — PHP 8.3 here vs 8.5 on the dev box — and
pass on Chris's box, so left alone.)</li>
<li class="check"><span class="check">✓</span> Found-bug fix #2
(real portability bug, not sandbox): when the PHP
<code>memory_limit</code> is unlimited (<code>-1</code>, the common
CLI default), <code>metricToInt("-1")</code> is <code>-1</code>, so
every "<code>usage > limit * fill_factor</code>" memory check goes
permanently true (and the matching "<" loop guard permanently
false). That made the indexer seal a new partition almost every
document — so the on-disk archive's layout depended on
<code>memory_limit</code>, which it must not (archives are portable).
It also one-shotted FetchUrl's curl pump loop and made the fetcher
think memory was always low. Fixed with one helper,
<code>memoryCeiling($fill_factor)</code> in Utility.php (returns
<code>PHP_INT_MAX</code> when the limit is unlimited), applied at
every decision site: PartitionDocumentBundle, FetchUrl (x3), Fetcher,
and the legacy IndexArchiveBundle. The log-only site in
IndexDocumentBundle keeps printing the raw limit. With the fix the
two Index tests pass here under <code>-1</code> too; full suite
40893/40894 (the one miss, VersionManager restoreVersion, fails on
plain HEAD too and touches none of this code — a separate
sandbox-only flake).</li>
<li class="check"><span class="check">✓</span> Found-bug fix #3
(real defect, surfaced by IconProcessorTest leaving a stray
"<code>..txt</code>" file in the repo root when OCR is enabled):
<code>ImageProcessor::saveTempFile</code> built its temp name with
<code>... . " . $file_extension"</code> — the intended
"<code>.</code>" separator was swallowed into a string literal, so
names came out as "<code><hash> . ico</code>" with embedded
spaces (VideoProcessor's twin was already correct). That spaced path
was then handed unquoted to <code>tesseract</code> in
<code>ComputerVision::recognizeText</code>, so the shell split it and
tesseract saw output base "<code>.</code>", writing
"<code>.</code>"+"<code>.txt</code>" = "<code>..txt</code>" into the
working folder. Fixed the separator (now
"<code>.</code>" . <code>$file_extension</code>) and hardened
the exec with <code>escapeshellarg</code> on all three path/lang
arguments so no future path can spray files into cwd. Added
<code>tempFileNameTestCase</code> (runs without tesseract) asserting
the temp name has no spaces and ends with the given extension; it
fails on the old code, passes on the new. Only reproduces where
TESSERACT is defined, which is why the PHP 8.3 sandbox never saw
it.</li>
<li class="check"><span class="check">✓</span> Found-bug fix #4
(real flaky defect in VersionManager, ~50% fail rate here, passes
more often on the dev box purely by timing luck):
<code>restoreVersionTestCase</code> failed about half the time because
<code>getActiveVersion</code> could skip the exact version asked for.
A version's folder is named by its timestamp turned into a string at
PHP's default float precision, which rounds the last digit; the
timestamp searched on is kept at full precision from the serialized
record. So restoring a version to its own exact time could compare as
just past that version and fall back to an older one (here the 1970
"initial" version, timestamp 1), leaving the expected file missing.
Fixed by rounding the incoming search value through the same string
form the folder names use, in both <code>getActiveVersion</code> and
(same latent boundary bug) <code>getVersionsInRange</code>. Proven:
a 60x repeat loop went from ~half failing to 60/60, the test is now a
steady [5/5], and the full unit suite is 40897/40897 all green.</li>
<li class="q">Code audit (this patch, no behaviour change): inlined
two single-use private helpers in <code>UnsubscribeToken</code>
(<code>encode</code> into <code>makeEmail</code>,
<code>decode</code> into <code>parseEmail</code>); merged the two
near-identical signing methods (<code>sign</code> and
<code>signDomain</code>, same body differing only by prefix) into
one <code>signPayload($prefix, $payload)</code> with four callers;
and removed the duplicated "one-click → act → done, else
confirm" block in <code>ApiController::unsubscribe</code> by setting
the state once after a single scope branch. Suite unchanged at
40946/40946.</li>
</ol>
<h2>Out of this arc</h2>
<ol>
<li>DKIM signing (RFC 6376) — verified already done (see
above), so the only item-12 mail piece left after this arc is
documenting SPF / DMARC (DNS-side) in the install guide.</li>
</ol>
<h2>Design decisions (resolved with Chris)</h2>
<ol>
<li>Thread + group level both honored. Group level is the
<code>RECEIVE_MAIL</code> column. Thread level is supported at least
at the header level, with an "unfollow" control on the thread page
(Phase A item 4).</li>
<li>Whole-domain suppression is a standalone table keyed by email
address.</li>
<li>Unsubscribe tokens are signed with <code>crawlAuthHash</code> so
the URL and the mailto suffix cannot be forged.</li>
<li><code>bot@<domain></code> uses the domain the mail was going
out on, resolved at enqueue time.</li>
<li><span class="check">✓</span> Decided in Phase B: the header
is precomputed at enqueue and carried as <code>$extra_headers</code>
through the send / queue / sweep path (see Phase B item 2).</li>
</ol>
</body>
</html>