/api/qmail/net/messages/upload_and_tell
GET POSTConvenience compose endpoint. Runs qmail_upload_files() and qmail_send_tells() back-to-back inside a single background task; one task_id covers both phases. Replaces the legacy /api/qmail/net/messages/send (deleted in Phase 3.7).
Description
The /api/qmail/net/messages/upload_and_tell endpoint composes the two-step send into a single async call. It accepts the same inputs as /upload plus a required recipient list (to, cc, or bcc). The background worker first stages the CBDF on the QMail RAIDA, then fires Tell2 to each recipient's beacon RAIDA. A single task_id tracks both phases.
Use /upload_and_tell when you have a finalized message ready to deliver immediately. Use the two-step flow (/upload then /tell) when you need to:
- Stage a draft, inspect the receipt, and only fire Tell after explicit confirmation.
- Retry a Tell after fixing some upstream issue without re-staging the upload.
- Fire multiple delayed Tells from the same upload — though note Q25(b): re-Tell on a started receipt returns
409, so this only works once per upload.
The legacy /api/qmail/net/messages/send endpoint was deleted in commit fd56a24 (Phase 3.7). Existing code that called /send should switch to /upload_and_tell — same input shape, no synchronous result. The most visible behavioral difference is that the response is now async-only: the caller gets a task_id immediately and must poll /api/system/tasks for completion.
Internally this endpoint uses the same split-commit primitives as the two-step flow:
- Storage lockers are committed at upload time.
- Inbox-fee locker is committed only after Tell succeeds; on failure it stays
RESERVEDfor the (deferred) Phase 8 reclamation sweep.
Net effect: a failed Tell phase does not double-spend the inbox fee — no Tell, no fee sunk.
Parameters
Send parameters as form data (POST) or query string values (GET). All inputs from /upload are accepted; recipients are required.
| Parameter | Type | Required | Description |
|---|---|---|---|
body |
string | One of three | Inline plain-text email body. Mutually exclusive with plain_text_qmail_path and email_file. |
plain_text_qmail_path |
string (path) | One of three | Server-side path to a plain-text body file (max 1 MB). |
email_file |
string (path) | One of three | Server-side path to a pre-built .qmail CBDF file. Bypasses CBDF encoding entirely. |
attachment_file_path |
string (path), repeatable | No | Server-side path to one attachment. Repeat the parameter for multiple attachments. Legacy form: attachments=A,B,C. |
to |
string, repeatable | Yes (one of to/cc/bcc) | Recipient address. Repeat the parameter or pass a comma/semicolon-separated string. At least one recipient (to, cc, or bcc) is required. |
cc |
string, repeatable | Yes (one of to/cc/bcc) | CC recipient. Same shape as to. |
bcc |
string, repeatable | Yes (one of to/cc/bcc) | BCC recipient. Same shape as to. NOT stamped into the CBDF visible-metadata block. |
subject |
string | No | Email subject line. Stamped into the CBDF and stored on the receipt. |
file_guid |
string (32 hex chars) | No | Optional caller-supplied GUID. Returns HTTP 409 if a receipt already exists for that GUID. |
duration |
integer (weeks) | No | Storage duration. Default 4; clamped to [1, 520]. |
wallet_path / wallet |
string | No | Wallet that funds storage and inbox-fee lockers. Defaults to Default. |
debug |
boolean | No | Set debug=true to enable per-request LOG_DEBUG verbosity for this one call. |
If none of to, cc, or bcc is supplied, the call fails with HTTP 400 and the message "At least one recipient required (to/cc/bcc)". For an upload with no Tell, use /upload instead.
Response
The handler returns HTTP 200 OK immediately after spawning the worker. Poll /api/system/tasks?id=<task_id> for status; both phases progress under the same task_id. The receipt at /api/qmail/receipts?guid=<file_guid> reflects upload state (immediately) and Tell state (after Tell phase completes).
Success Response Properties (immediate)
true when the worker thread was spawned successfully.qmail-upload-and-tell.Success Response Example (immediate)
{
"success": true,
"command": "qmail-upload-and-tell",
"task_id": "task_3f2a1c40",
"url": "/api/system/tasks?id=task_3f2a1c40",
"file_guid": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6",
"duration": 4,
"message": "Upload+Tell started — poll /api/system/tasks for status, or GET /api/qmail/receipts?guid=... for receipt detail"
}
Task Poll Example (both phases complete — all accepted)
{
"status": "success",
"task_id": "task_3f2a1c40",
"progress": 100,
"message": "Upload + Tell complete: all beacons accepted",
"data": {
"file_guid": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6",
"upload_successes": 25,
"upload_failures": 0,
"beacons": 1,
"tell_successes": 1,
"tell_failures": 0,
"all_accepted": true
}
}
Task Poll Example (Tell phase failed mid-flight)
{
"status": "failed",
"task_id": "task_3f2a1c40",
"progress": 75,
"message": "Tell phase failed: RESULT_NETWORK_ERROR — Beacon RAIDA unreachable"
}
Error Response — Missing Recipients (HTTP 400)
{
"error": true,
"message": "At least one recipient required (to/cc/bcc)",
"code": 400
}
Interactive API Tester
Test this Endpoint
Examples
cURL — Body Mode
curl "http://localhost:8081/api/qmail/net/messages/upload_and_tell?body=QMail%20upload%2Btell%20doc%20test%20from%20Client1.&subject=Upload%2BTell%20Doc%20Test&to=%40chariot.harbor.byte&duration=4"
cURL — File Mode with Attachments
curl "http://localhost:8081/api/qmail/net/messages/upload_and_tell?email_file=E%3A%5CClient1%5CClient_Data%5CWallets%5CMail%5COutgoing%5Ctest1.qmail&attachment_file_path=E%3A%5CClient1%5CClient_Data%5CWallets%5CMail%5COutgoing%5Cattachment1.pdf&attachment_file_path=E%3A%5CClient1%5CClient_Data%5CWallets%5CMail%5COutgoing%5Cattachment2.pdf&subject=Upload%2BTell%20File%20Doc%20Test&to=%40chariot.harbor.byte&duration=4"
JavaScript (fetch)
const compose = await fetch('http://localhost:8081/api/qmail/net/messages/upload_and_tell?body=QMail%20upload%2Btell%20doc%20test%20from%20Client1.&subject=Upload%2BTell%20Doc%20Test&to=%40chariot.harbor.byte&duration=4').then(r => r.json());
if (!compose.success) {
throw new Error(`Upload+Tell kickoff failed: ${compose.message}`);
}
console.log(`task=${compose.task_id} guid=${compose.file_guid}`);
// Poll until both phases finish.
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(compose.task_id);
console.log('Final state:', finalState);
// Inspect the receipt for per-recipient detail.
const receipt = await fetch(`/api/qmail/receipts?guid=${compose.file_guid}`).then(r => r.json());
console.log('Recipients:', receipt.tell?.recipients);
Python
import time
import requests
BASE = 'http://localhost:8081/api'
compose = requests.get(f'{BASE}/qmail/net/messages/upload_and_tell', params={
'body': 'QMail upload+tell doc test from Client1.',
'subject': 'Upload+Tell Doc Test',
'to': '@chariot.harbor.byte',
'duration': 4,
}).json()
assert compose['success'], compose
task_id = compose['task_id']
file_guid = compose['file_guid']
while True:
t = requests.get(f'{BASE}/system/tasks', params={'id': task_id}).json()
if t['status'] != 'running':
break
time.sleep(1)
print('Final state:', t)
receipt = requests.get(f'{BASE}/qmail/receipts', params={'guid': file_guid}).json()
print('Recipients:', receipt.get('tell', {}).get('recipients'))
Important Notes
The same task_id is updated through both upload and Tell phases. Watch the progress field move from upload to Tell, and the message field for phase markers.
If the upload phase succeeds but Tell fails, the receipt is still written to <Mail>/Receipts/<file_guid>.json. The retry worker (qmail_tell_retry.c) will keep attempting failed beacons in the background; check /api/qmail/db/tells/list_pending for queue inspection.
- Plain-text body file: 1 MB max.
- Up to 50 recipients per
to/cc/bccfield. - Storage duration: 1–520 weeks.