/api/qmail/net/messages/upload
GET POSTUpload-only async endpoint. Stages the CBDF onto the QMail RAIDA and writes the receipt; does not fire Tell. Returns a task_id and file_guid immediately so the caller can poll /api/system/tasks and later call /api/qmail/net/messages/tell to deliver. To do upload + tell in one shot, use /api/qmail/net/messages/upload_and_tell.
Description
The /api/qmail/net/messages/upload endpoint stages a CBDF email file (optionally with attachments) onto the 25 QMail RAIDA servers using RAID-5 striped delivery, then writes a receipt JSON file to the local Mail wallet. It never sends Tell.
This endpoint is the upload-only half of the new compose flow:
- POST /upload — stages the email; returns a
file_guid+task_id. - POST /tell with that
file_guid— fires the Tell to each recipient's beacon RAIDA.
Splitting these two phases lets a caller stage a draft, verify the upload via the receipt, and only fire Tell once it has decided to actually deliver. To do both in one call, use /upload_and_tell.
Recipients (to, cc, bcc) on this endpoint are optional. If supplied they are used to:
- Stamp the CBDF metadata (sender sees the same recipient list a Tell would carry).
- Seed the receipt's
tell.recipients[], so a later/tellcan resolve them from the receipt without the caller re-supplying them. Per Q25(a),/tellrejects request-side recipient overrides. - Reserve the inbox-fee locker; it stays
RESERVEDuntil/tellconfirms it. A pure upload-only call (no recipients) recycles the locker funding toAVAILABLE.
This endpoint requires that a Mail wallet is configured locally. If qmail_receipt_get_mail_wallet_path() can't resolve one, /upload returns HTTP 500 with {"error":true,"message":"No Mail wallet configured (required for receipts)"}. Receipts live at <Mail-wallet>/Receipts/<file_guid>.json and can be fetched via /api/qmail/receipts.
Per Q15, GUIDs are immutable in v1. If the caller supplies file_guid and a receipt with that GUID already exists, /upload returns HTTP 409 Conflict. Omit file_guid to let the server generate a fresh one.
Parameters
Send parameters as form data (POST) or query string values (GET). The HTTP server merges form-encoded POST bodies into the same params table, so any caller may use either method.
| Parameter | Type | Required | Description |
|---|---|---|---|
body |
string | One of three | Inline plain-text email body. The server CBDF-encodes it using the resolved sender identity. Mutually exclusive with plain_text_qmail_path and email_file. |
plain_text_qmail_path |
string (path) | One of three | Server-side filesystem path to a plain-text body file (max 1 MB). The server reads it and CBDF-encodes the contents. |
email_file |
string (path) | One of three | Server-side filesystem path to a pre-built .qmail CBDF file. Bypasses CBDF encoding entirely. |
attachment_file_path |
string (path), repeatable | No | Server-side filesystem path to one attachment. Repeat the parameter for multiple attachments. Legacy form: a single attachments=A,B,C comma/semicolon-joined string. |
to |
string, repeatable | No | Optional recipient address. Used for CBDF metadata stamping and to seed the receipt's tell.recipients[] for a later /tell. Repeat the parameter or pass a comma/semicolon-separated string. |
cc |
string, repeatable | No | Optional CC recipient. Same shape as to. |
bcc |
string, repeatable | No | Optional BCC recipient. Same shape as to. Note that BCC addresses are 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 for the upload. Returns HTTP 409 if a receipt already exists for that GUID. Omit to let the server generate one. |
duration |
integer (weeks) | No | Storage duration for the upload, in weeks. Default 4; clamped to the range [1, 520]. |
wallet_path / wallet |
string | No | Wallet that funds the upload (storage locker payments). Defaults to Default if omitted, per resolve_wallet_path(). |
debug |
boolean | No | Set debug=true to enable per-request LOG_DEBUG verbosity for this one call (server-thread-local; does not affect concurrent requests). Useful for diagnosing a single failing upload via main.log. |
Response
The handler returns HTTP 200 OK immediately after spawning the upload worker. The actual upload progress is tracked through the task system; poll /api/system/tasks?id=<task_id> for status. The receipt at /api/qmail/receipts?guid=<file_guid> is updated through the lifecycle.
Success Response Properties (immediate)
true when the worker thread was spawned successfully.qmail-upload./api/system/tasks?id=<task_id> for status.json_add_task_poll_fields).Success Response Example (immediate)
{
"success": true,
"command": "qmail-upload",
"task_id": "task_5a91c802",
"url": "/api/system/tasks?id=task_5a91c802",
"file_guid": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6",
"duration": 4,
"message": "Upload started (no Tell) — poll /api/system/tasks for status, or GET /api/qmail/receipts?guid=... for receipt detail"
}
Task Poll Example (after upload completes)
{
"status": "success",
"task_id": "task_5a91c802",
"progress": 100,
"message": "Upload completed; Tell pending",
"data": {
"file_guid": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6",
"upload_successes": 25,
"upload_failures": 0,
"used_sync_fallback": false,
"low_balance_warning": false
}
}
Error Response — GUID Conflict (HTTP 409)
{
"error": true,
"message": "A receipt already exists for that file_guid",
"code": 409
}
Error Response — Missing Body Source (HTTP 400)
{
"error": true,
"message": "Missing body source: supply one of body, plain_text_qmail_path, or email_file",
"code": 400
}
Interactive API Tester
Test this Endpoint
Examples
cURL — Body Mode (no recipients)
curl "http://localhost:8081/api/qmail/net/messages/upload?body=QMail%20upload-only%20doc%20test%20from%20Client1%20%28no%20Tell%20will%20fire%29.&subject=Upload-only%20Doc%20Test&to=%40chariot.harbor.byte&duration=4"
cURL — File Mode with Attachments
curl "http://localhost:8081/api/qmail/net/messages/upload?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&to=%40chariot.harbor.byte&subject=Upload%20File%20Doc%20Test&duration=4"
JavaScript (fetch)
// Step 1 — stage the upload (returns task_id + file_guid)
const upload = await fetch('http://localhost:8081/api/qmail/net/messages/upload?body=QMail%20upload-only%20doc%20test%20from%20Client1%20%28no%20Tell%20will%20fire%29.&subject=Upload-only%20Doc%20Test&to=%40chariot.harbor.byte&duration=4').then(r => r.json());
if (!upload.success) {
throw new Error(`Upload kickoff failed: ${upload.message}`);
}
console.log(`Upload task=${upload.task_id} guid=${upload.file_guid}`);
// Step 2 — 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(upload.task_id);
console.log('Upload result:', finalState);
// Step 3 (optional) — fire Tell to deliver
// const fireTellUrl = `/api/qmail/net/messages/tell?file_guid=${upload.file_guid}`;
// const tell = await fetch(fireTellUrl).then(r => r.json());
Python
import time
import requests
BASE = 'http://localhost:8081/api'
# Step 1 — stage the upload (returns task_id + file_guid)
upload = requests.get(f'{BASE}/qmail/net/messages/upload', params={
'body': 'QMail upload-only doc test from Client1.',
'subject': 'Upload Doc Test',
'to': '@chariot.harbor.byte',
'duration': 4,
}).json()
assert upload['success'], upload
task_id = upload['task_id']
file_guid = upload['file_guid']
print(f'task={task_id} guid={file_guid}')
# Step 2 — poll the task until status leaves "running"
while True:
t = requests.get(f'{BASE}/system/tasks', params={'id': task_id}).json()
if t['status'] != 'running':
break
time.sleep(1)
print('Upload result:', t)
# Step 3 (optional) — fire Tell to deliver
# tell = requests.get(f'{BASE}/qmail/net/messages/tell', params={'file_guid': file_guid}).json()
Important Notes
Pass exactly one of body, plain_text_qmail_path, or email_file. Supplying zero or more than one returns HTTP 400.
If any recipient hint is present (to, cc, or bcc), the upload reserves an inbox-fee locker and leaves it RESERVED for the deferred Tell. The locker is committed when /tell succeeds, or recycled if the receipt update fails. A pure upload-only call (no recipients) recycles the funding immediately.
Append &debug=true to bump this single request's log level to LOG_DEBUG in main.log. The override is per-thread; it does not affect concurrent requests. Useful for diagnosing a single failing upload without globally raising verbosity.
- Plain-text body file: 1 MB max (server returns
400on overflow). - Up to 50 recipients per
to/cc/bccfield. - Storage duration: 1–520 weeks (clamped to range).