QMail Upload — Group 6, Code 70
Stores one file fragment (stripe) for one email under a 16-byte GUID. The sender calls upload in parallel against all storage RAIDAs before invoking tell to notify the recipient’s beacon.
Quick reference
| Command Group | 6 (CHAT/QMail) |
| Command Code | 70 |
| Server function | cmd_qmail_upload2 in cmd_qmail.c |
| Wire structure | qmail_upload2_req_t in qmail_structs.h |
| Body layout | Preamble (48) + Payload (38) + Data (N) + Terminator (2) |
| Response body | None (status only) |
| Encryption | Required — AES-128 keyed by sender’s coin AN |
| Transport | TCP recommended (data length up to ~10 MB per stripe) |
| Server-side limit | QMAIL_MAX_STRIPE_SIZE = 10 MiB |
Purpose
The sender splits each email file into stripes using Reed-Solomon-style erasure coding (typically 7 data + 1 parity = 8 stripes). One upload request is sent to each storage RAIDA in parallel, each carrying one stripe. The recipient later downloads the stripes from those RAIDAs and reassembles the file.
An email is normally a group of multiple files: the body (file_type = 1), optional metadata (file_type = 0), and zero to several attachments (file_type = 10..14). Each file is uploaded separately under the same GUID; the storage server distinguishes them by the file_type byte, which selects the on-disk file extension.
Request body
All offsets are relative to the start of the decrypted body (after the 32-byte routing header that the executor framework wraps every request in).
Preamble (48 bytes, offsets 0–47)
The standard QMail preamble. See QMail Services — Universal preamble for the byte-level breakdown.
Command payload (38 bytes, offsets 48–85)
| Offset | Size | Field | Description |
|---|---|---|---|
| 48–63 | 16 | File GUID | 16-byte identifier for the email this stripe belongs to. Same GUID on every stripe of every file in the email. |
| 64–79 | 16 | Locker Code | ASCII storage-payment locker key, null-padded to 16 bytes (e.g. "X7KQ-M3PL-9RVB" + zeros). The server consumes this locker as payment for storage. May not be all zeros. |
| 80 | 1 | File Type | Selects which file inside the email this stripe belongs to. See File Types table below. |
| 81 | 1 | Storage Duration | Storage retention code. Server logs but does not currently enforce. |
| 82–85 | 4 | Data Length | Length of the stripe data that follows, in bytes (big-endian). |
| 86… | N | Stripe Data | The actual stripe bytes. Length must equal the Data Length field. |
| last 2 | 2 | Terminator | Fixed 3E 3E. Server rejects with ERROR_INVALID_PACKET_LENGTH if missing. |
Total body length: 48 + 38 + Data Length + 2. The server validates this exactly — one byte off and the request is rejected with ERROR_INVALID_PACKET_LENGTH.
File types
The file_type byte at offset 80 picks the storage filename suffix. The server’s mapping (cmd_qmail_upload2 in cmd_qmail.c):
| Code | Suffix | Purpose |
|---|---|---|
| 0 | .meta | Email metadata (subjects, attachment IDs — future use). |
| 1 | .qmail | Email body and styling. The primary file in every email. |
| 2–9 | .blob | Reserved (web page, instant message, key material, etc.). |
| 10 | .0.bin | First attachment. |
| 11 | .1.bin | Second attachment. |
| 12–255 | .{N−10}.bin | Subsequent attachments. |
Response
No body. The status byte in the response header indicates the outcome.
Status codes
| Decimal | Hex | Symbol | Meaning |
|---|---|---|---|
| 250 | 0xFA | STATUS_SUCCESS | Stripe stored under QMAIL_PUBLIC_UPLOAD_ROOT/G1/G2/GUID/00000000GUID.suffix; .acl sidecar written; locker payment consumed asynchronously. |
| 8 | 0x08 | ERROR_COIN_NOT_FOUND | Sender’s (denom, SN) is not loaded on this RAIDA. |
| 16 | 0x10 | ERROR_INVALID_PACKET_LENGTH | Body too short (< 88 bytes), missing terminator, or 48 + 38 + data_length + 2 doesn’t equal the actual body size. |
| 40 | 0x28 | ERROR_INVALID_SN_OR_DENOMINATION | Denomination outside −8…+6. |
| 169 | 0xA9 | ERROR_PAYMENT_REQUIRED | Locker key was empty or the asynchronous coin download to the storage RAIDA failed to start. |
| 194 | 0xC2 | ERROR_FILESYSTEM | Server could not create the storage directory or write the file (disk full, permission denied, path-traversal check failed). |
| 200 | 0xC8 | ERROR_INVALID_AN | Preamble AN does not match the server’s stored AN for the (denom, SN). Most common cause: client sent a zero-AN preamble. |
Server-side storage layout
The storage path is sharded by the first two bytes of the GUID:
Root: /opt/raidax/QMail_Data/public_uploads/
Path: ROOT/{G1}/{G2}/{GUID_hex}/00000000{GUID_hex}.{suffix}
G1 = hex(file_guid[0]) e.g. "a3"
G2 = hex(file_guid[1]) e.g. "f7"
GUID_hex = hex(file_guid) 32 lowercase hex chars
suffix = chosen by file_type (.qmail, .meta, .{N}.bin, .blob)
Beside each stored file the server writes a .acl sidecar:
{file_path}.acl (22 bytes, Phase I "allow all" format)
byte 0 version = 0x01
byte 1 flags = 0x01 (ACL_ALLOW_ALL)
bytes 2-17 owner_guid = the email GUID (16 bytes)
byte 18 owner_denom = sender denomination
bytes 19-22 owner_sn = sender SN (4 bytes)
The ACL sidecar exists today as a placeholder; download currently logs its absence but does not enforce. Future phases will read the sidecar to gate downloads by (recipient SN, recipient AN).
Example request body (hex)
Preamble (48 bytes, offsets 0-47):
A1 B2 C3 D4 E5 F6 07 08 09 0A 0B 0C XX XX XX XX <- challenge: 12 random + 4 CRC32 BE
00 00 00 00 00 00 00 00 <- session_id (zeros)
00 06 <- coin_type
01 <- denomination = 1
00 00 0B 19 <- serial_number = 2841 BE
00 <- reserved (was Device ID)
[16 bytes of authenticity AN] <- AN
Payload (38 bytes, offsets 48-85):
[16 bytes File GUID]
58 37 4B 51 2D 4D 33 50 4C 2D 39 52 56 42 00 00 <- locker_code "X7KQ-M3PL-9RVB" + 2 nulls
01 <- file_type = 1 (.qmail)
00 <- storage_duration
00 00 00 50 <- data_length = 80 BE
Stripe data (80 bytes):
[80 bytes of stripe content]
Terminator:
3E 3E
Total body size: 48 + 38 + 80 + 2 = 168 bytes.
Common mistakes
Locker code padding
The locker key is an ASCII string up to 16 bytes long. If your key is shorter than 16 bytes, null-pad the remainder — do not space-pad or repeat the key. The server trims trailing nulls before passing the key to the locker download.
Empty locker key
An all-zero locker code field is rejected with ERROR_PAYMENT_REQUIRED. The server has no concept of "free" upload — every stripe is paid storage.
Body length must be exact
The server computes expected = 48 + 38 + data_length + 2 and compares to the actual decrypted body size. Off-by-one (e.g. forgetting the terminator) returns ERROR_INVALID_PACKET_LENGTH.
Per-RAIDA AN
Each RAIDA holds a different AN for the same (denom, SN). The preamble’s AN field must contain this RAIDA’s 16-byte AN, not a master key or the AN for some other RAIDA. Sending the wrong AN returns ERROR_INVALID_AN.
Points of confusion
- Preamble size. The current value is
QMAIL_PREAMBLE_SIZE = 48. Older specs say 49 — that was a typo from when byte 31 was Device ID and counted as a separate field. Byte range 0–47 is 48 bytes; the server enforces 48. - Reserved byte 31. Documented as “Reserved (was Device ID)”. The server reads it but does nothing with it. Available for protocol extension; clients should write zero today.
- File type 2 vs 10. File type 2 is reserved for non-email blob storage (web pages, IM, etc.) and gets the
.blobsuffix. Attachments start at file type 10. Don’t use 2 for attachments. - Locker payment is asynchronous.
consume_locker_paymentkicks off a background coin download from the locker and returns success immediately. If the background download later fails, the storage is already committed — the failure is logged but does not roll back the upload. Future phases may add a synchronous verification mode. - Storage duration is currently advisory. The byte is logged but the server does not yet enforce retention. Don’t rely on the server to delete expired files.