/api/qmail/net/messages/upload

GET POST

Upload-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.

Two-Step Send (Upload then Tell)

This endpoint is the upload-only half of the new compose flow:

  1. POST /upload — stages the email; returns a file_guid + task_id.
  2. 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.

Recipient Hints Are Optional

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 /tell can resolve them from the receipt without the caller re-supplying them. Per Q25(a), /tell rejects request-side recipient overrides.
  • Reserve the inbox-fee locker; it stays RESERVED until /tell confirms it. A pure upload-only call (no recipients) recycles the locker funding to AVAILABLE.
Receipt Required

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.

GUIDs Are Immutable (v1)

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)

success boolean
true when the worker thread was spawned successfully.
command string
Always qmail-upload.
task_id string
Identifier for the background upload task. Poll /api/system/tasks?id=<task_id> for status.
url string
Full polling URL for the task (canonical task-poll URL emitted by json_add_task_poll_fields).
file_guid string
The 32-character hex GUID of the upload. Use this with /tell or /receipts.
duration integer
Storage duration (weeks) the upload was filed for.
message string
Human-readable hint that the upload has started and how to poll.

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

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

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

Body Sources Are Mutually Exclusive

Pass exactly one of body, plain_text_qmail_path, or email_file. Supplying zero or more than one returns HTTP 400.

Inbox-Fee Locker Behavior (Q18(a))

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.

Per-Request Debug

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.

Limits
  • Plain-text body file: 1 MB max (server returns 400 on overflow).
  • Up to 50 recipients per to/cc/bcc field.
  • Storage duration: 1–520 weeks (clamped to range).