/api/qmail/net/messages/tell

GET POST

Tell-only async endpoint. Loads the receipt for a given file_guid and fires Tell2 to each unique beacon RAIDA covering its recipient list. The recipients come from the receipt — request-side overrides are rejected with 409 per Q25(a).

Replace the Placeholder GUID

The link above uses 00000000000000000000000000000000 as a placeholder. Replace it with a real file_guid returned by an earlier /upload call before clicking — otherwise the response is HTTP 404 Receipt not found.

Description

The /api/qmail/net/messages/tell endpoint is the second half of the two-step send flow. It takes a file_guid from a prior /upload call, rehydrates the upload artifacts from the receipt at <Mail>/Receipts/<file_guid>.json, rehydrates the sender's per-RAIDA encryption ANs from the Mail wallet, and fires a Tell2 to each unique beacon RAIDA in the recipient list.

Sender Identity Is Rehydrated, Not Stored

Per Q19(a), receipts never contain bearer secrets — neither sender_ans nor enc_key.ans are ever serialized. /tell re-reads the per-RAIDA ANs from the Mail wallet at run time using the receipt's sender_serial_number. If the sender identity coin can no longer be located (wallet moved, file deleted), the task fails with "Sender identity coin not found in Mail wallet".

When /tell Will Refuse (Q25(b), updated)

The handler claims the receipt synchronously before spawning the worker, so duplicate /tell calls and re-tells against finished receipts are deterministic. The rejection rules are:

  • tell.status == "in_progress" — another /tell worker holds the claim. Returns HTTP 409 with "Tell already in progress; poll /api/qmail/receipts for status".
  • tell.status == "success" — every recipient has been delivered. Returns HTTP 409 with "Tell already complete for this email".
  • Every recipient already "sent" (e.g. a re-tell on a partial that the retry worker has since drained). Returns HTTP 409 with "No recipients need Tell; all receipt rows are already sent".
  • upload.status != "success" — upload is still in flight or failed; /tell won't proceed. Returns HTTP 409 with the current upload status in the message.

Retrying is allowed from tell.status == "failed" or tell.status == "partial". The handler claims the receipt, marks every recipient that is not already "sent" as "sending", and the worker only targets those rows. Recipients that are already "sent" are never re-told.

For background retries the retry worker handles transient beacon failures automatically. See /api/qmail/db/tells/retry.

No Recipient Overrides (Q25(a))

Supplying to, cc, or bcc on a /tell request is rejected with HTTP 409 Conflict. Recipients are sourced exclusively from the receipt's tell.recipients[], which is seeded by /upload when the caller passes recipient hints. If the receipt has no recipients, the call fails with HTTP 400; re-upload with hints, or use /upload_and_tell.

Parameters

Send parameters as form data (POST) or query string values (GET). Form-encoded POST bodies are merged into the params table, so either method works.

Parameter Type Required Description
file_guid string (32 hex chars) Yes The GUID returned by a prior /upload or /upload_and_tell call. Returns HTTP 404 if no receipt exists for that GUID.
wallet_path / wallet string No Wallet used to fund any pool work performed during Tell (the inbox-fee locker confirmation step). Defaults to Default.
debug boolean No Set debug=true to enable per-request LOG_DEBUG verbosity for this one call. Useful for following the rehydrate→Tell flow in main.log.
Rejected Parameters

Per Q25(a), the following parameters are rejected with HTTP 409 if any are present: to, cc, bcc. Recipients come from the receipt only.

Response

The handler returns HTTP 200 OK immediately after spawning the Tell worker. Poll /api/system/tasks?id=<task_id> for status, or fetch the receipt at /api/qmail/receipts?guid=<file_guid> to see the per-recipient status (each row's status, attempts, last_error, timestamps).

Success Response Properties (immediate)

success boolean
true when the Tell worker thread was spawned.
command string
Always qmail-tell.
task_id string
Identifier for the background Tell task.
url string
Full polling URL for the task.
file_guid string
Echo of the GUID being told.
recipient_count integer
Number of recipients the worker will actually iterate. On a fresh not_started receipt this equals the total recipient count; on a retry from failed or partial it is the count of rows that are not already "sent".
receipt_recipient_count integer
Total recipient rows on the receipt, including ones that were already "sent" in a previous Tell. Compare with recipient_count to see how many rows are being skipped on a retry.
request_id string
16-char hex correlation id (64 bits of entropy). Also present on every log line emitted by this request as rid=<id>; grep "rid=<id>" main.log retrieves the full transaction.
message string
Human-readable hint that the Tell has started and how to poll.

Success Response Example (immediate)

{
  "success": true,
  "command": "qmail-tell",
  "request_id": "5a91c802deadbeef",
  "task_id": "task_8c7d5e10",
  "url": "/api/system/tasks?id=task_8c7d5e10",
  "file_guid": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6",
  "recipient_count": 1,
  "receipt_recipient_count": 1,
  "message": "Tell started — poll /api/system/tasks for status, or GET /api/qmail/receipts?guid=... for receipt detail"
}

Task Poll Example (after Tell completes — all accepted)

{
  "status": "success",
  "task_id": "task_8c7d5e10",
  "progress": 100,
  "message": "Tell complete: all beacons accepted",
  "data": {
    "file_guid": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6",
    "beacons": 1,
    "tell_successes": 1,
    "tell_failures": 0,
    "all_accepted": true
  }
}

Task Poll Example (Tell completed — partial, retry queued)

{
  "status": "success",
  "task_id": "task_8c7d5e10",
  "progress": 100,
  "message": "Tell complete: some beacons failed (queued for retry)",
  "data": {
    "file_guid": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6",
    "beacons": 3,
    "tell_successes": 2,
    "tell_failures": 1,
    "all_accepted": false
  }
}

Error Response — Receipt Not Found (HTTP 404)

{
  "error": true,
  "message": "Receipt not found for that file_guid",
  "code": 404
}

Error Response — Tell Already In Progress (HTTP 409)

{
  "error": true,
  "message": "Tell already in progress; poll /api/qmail/receipts for status",
  "code": 409
}

Error Response — Tell Already Complete (HTTP 409)

{
  "error": true,
  "message": "Tell already complete for this email",
  "code": 409
}

Error Response — All Recipients Already Sent (HTTP 409)

Returned on a re-tell against a partial receipt where the retry worker has since drained the queue.

{
  "error": true,
  "message": "No recipients need Tell; all receipt rows are already sent",
  "code": 409
}

Error Response — Recipient Override Rejected (HTTP 409)

{
  "error": true,
  "message": "Recipient overrides are not allowed; recipients come from the receipt",
  "code": 409
}

Interactive API Tester

Test this Endpoint

Examples

cURL — Two-Step Compose (upload + tell)

# Step 1 — stage the upload (records the receipt with recipient hints)
GUID=$(curl -s "http://localhost:8081/api/qmail/net/messages/upload?body=Hi&to=%40chariot.harbor.byte" | jq -r .file_guid)

# Step 2 — fire Tell using the GUID
curl "http://localhost:8081/api/qmail/net/messages/tell?file_guid=$GUID"

JavaScript (fetch)

// Fire Tell for a previously uploaded GUID
const fileGuid = 'a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6';  // from /upload
const tellUrl = `http://localhost:8081/api/qmail/net/messages/tell?file_guid=${fileGuid}`;
const tell = await fetch(tellUrl).then(r => r.json());

if (!tell.success) {
    throw new Error(`Tell kickoff failed: ${tell.message} (HTTP ${tell.code ?? '?'})`);
}
console.log(`Tell task=${tell.task_id} (${tell.recipient_count} recipient${tell.recipient_count === 1 ? '' : 's'})`);

// Poll the task until status leaves "running"
async function pollTask(taskId) {
    while (true) {
        const r = await fetch(`/api/system/tasks?id=${encodeURIComponent(taskId)}`);
        const t = await r.json();
        if (t.status !== 'running') return t;
        await new Promise(r => setTimeout(r, 1000));
    }
}
const finalState = await pollTask(tell.task_id);
console.log('Tell result:', finalState);

Python

import time
import requests

BASE = 'http://localhost:8081/api'
file_guid = 'a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6'  # from /upload

tell = requests.get(f'{BASE}/qmail/net/messages/tell',
                    params={'file_guid': file_guid}).json()
if not tell.get('success'):
    raise SystemExit(f"Tell kickoff failed: {tell.get('message')} (HTTP {tell.get('code')})")

task_id = tell['task_id']
print(f'task={task_id} recipients={tell["recipient_count"]}')

while True:
    t = requests.get(f'{BASE}/system/tasks', params={'id': task_id}).json()
    if t['status'] != 'running':
        break
    time.sleep(1)
print('Tell result:', t)

Important Notes

Inbox-Fee Locker (Q18(a))

If /upload reserved an inbox-fee locker (i.e., recipient hints were supplied), /tell confirms it on success. If /tell fails before any beacon is accepted, the locker stays RESERVED for the (deferred) Phase 8 reclamation sweep.

Per-Recipient Retries

Beacons that fail on the first attempt are queued for retry by qmail_tell_retry.c. Each retry updates the corresponding recipient row in the receipt (status, attempts, last_error, timestamps), and consults /api/qmail/db/tells/list_pending for queue inspection.

Encryption Is Mandatory (BUG-11)

Per Phase 1 BUG-11, Tell2 cannot be sent unencrypted. If no encryption key is available at run time, the task fails with "No encryption key available for tell". The retry worker handles the same condition by deferring the row back to pending rather than sending a plaintext Tell.

Per-Request Debug

Append &debug=true to enable LOG_DEBUG output for this single request. Particularly useful for verifying the rehydrate path (sender ANs, encryption key selection, beacon RAIDA dispatch).