QMail Download — Group 6, Code 74
Fetches one 256 KB page of one stored file from a storage RAIDA. The recipient calls download in parallel against each storage RAIDA listed in the tell’s notification blob, then reassembles the file locally.
Quick reference
| Command Group | 6 |
| Command Code | 74 |
| Server function | cmd_qmail_download2 in cmd_qmail.c |
| Wire structure | qmail_download2_req_t (request), qmail_download_resp_t (response header) |
| Body layout | Preamble (48) + Payload (37) + Terminator (2) |
| Response body | 8-byte header + up to 256 KB of stripe data |
| Encryption | Required — AES-128 keyed by recipient’s coin AN |
| Page size | Fixed at 256 KB by the server (page_size = 256 * 1024) |
| File-size cap | QMAIL_MAX_STRIPE_SIZE = 10 MiB |
Purpose
After receiving a tell notification, the recipient knows the email’s GUID, the locker code that was written into the file, and the list of storage RAIDAs holding stripes. The recipient issues one download per stripe (in parallel) to the listed RAIDAs, providing the GUID, the locker code (for authenticated download), the file_type (which file inside the email to fetch), and a page number (for files larger than 256 KB).
Phase I of the QMail rollout uses ACL sidecars in “allow all” mode — the server checks for the .acl file’s presence but does not enforce per-recipient permissions. Future phases will restrict download to authorized recipients.
Request body
Preamble (48 bytes, offsets 0–47)
Standard QMail preamble. See QMail Services — Universal preamble.
Command payload (37 bytes, offsets 48–84)
| Offset | Size | Field | Description |
|---|---|---|---|
| 48–63 | 16 | file_guid | 16-byte email GUID. Same value the sender used in upload and tell. |
| 64–79 | 16 | locker_code | ASCII locker key, null-padded to 16 bytes. Provided for symmetry with upload; the server reads it but does not currently enforce a payment for download (Phase I). |
| 80 | 1 | file_type | Picks which file inside the email to fetch. Same encoding as upload: 0=meta, 1=qmail/body, 10..14=attachment 1..5, etc. Server selects the on-disk file by suffix. |
| 81 | 1 | bytes_per_page | Reserved. Server hard-codes 256 KB pages and ignores this byte. Available for protocol extension. |
| 82–84 | 3 | page_number | 3-byte big-endian page index. Page 0 is the first 256 KB. Files larger than 256 KB require multiple download calls with incrementing page numbers. |
| last 2 | 2 | Terminator | Fixed 3E 3E. |
Total body length: exactly 48 + 37 + 2 = 87 bytes.
Response body
On success, the response carries an 8-byte header followed by up to 256 KB of stripe data. Layout matches qmail_download_resp_t:
| Offset | Size | Field | Description |
|---|---|---|---|
| 0 | 1 | file_type | Echo of the request’s file_type. |
| 1 | 1 | version | Always 0x02. |
| 2 | 1 | bytes_per_page | Always 0. Reserved by the server. |
| 3 | 1 | page_number | Echo of the request’s page_number (low byte only — truncated from the 3-byte request field). |
| 4–7 | 4 | data_length | Big-endian byte count of the stripe data that follows. Up to 262144 (256 KB). |
| 8… | N | data | Raw stripe bytes. The recipient assembles N stripes into a complete file. |
The last page of a file may be shorter than 256 KB — data_length reports the actual byte count.
Status codes
| Decimal | Hex | Symbol | Meaning |
|---|---|---|---|
| 250 | 0xFA | STATUS_SUCCESS | Page returned in the response body. |
| 16 | 0x10 | ERROR_INVALID_PACKET_LENGTH | Body shorter than 87 bytes or missing terminator. |
| 34 | 0x22 | ERROR_INVALID_ENCRYPTION | Wire-level decryption failed before the command body was reached. Most often caused by the client building the request with a zero-AN preamble (e.g. an aliasing bug in the encryption-key fill path). |
| 194 | 0xC2 | ERROR_FILESYSTEM | Server failed the path-traversal check or could not open the file for reading. |
| 198 | 0xC6 | ERROR_INVALID_PARAMETER | File on disk exceeds QMAIL_MAX_STRIPE_SIZE (10 MiB), or page_number * 256KB >= total_size (page is past the end of the file). |
| 202 | 0xCA | ERROR_FILE_NOT_EXIST | No file matching 00000000{GUID}.{suffix} exists at the GUID-sharded path. Either the GUID is wrong, the file_type is wrong (and the corresponding suffix wasn’t uploaded), or the upload was never delivered to this RAIDA. |
| 254 | 0xFE | ERROR_MEMORY_ALLOC | Server allocation of the 8 + page_size output buffer failed. |
Note: download does not currently authenticate the AN against a stored coin record. The preamble carries an AN field that the executor uses for wire encryption, but the handler doesn’t look up a page or compare the AN. Authorization is gated by ACL sidecar — today “allow all”.
Pagination
For files larger than 256 KB the recipient calls download repeatedly, incrementing page_number until the response’s data_length is less than 256 KB (the last page) or the call returns ERROR_INVALID_PARAMETER (page past end).
page_number = 0
while True:
resp = download(guid, locker, file_type, page_number)
if resp.status == ERROR_INVALID_PARAMETER:
break // past end of file
if resp.status != STATUS_SUCCESS:
raise
yield resp.data // 0..256KB chunk
if resp.data_length < 256*1024:
break // last page
page_number += 1
Most stripes are smaller than 256 KB and need only one call. Pagination is mainly for long-form attachments (file_type 10+).
Common mistakes
Wrong AN in preamble
Although download doesn’t currently look up the coin to compare ANs, the preamble’s AN is still used to derive the wire-encryption key. If the client puts a zero-AN in the preamble (a common bug when an encryption-state struct is unsafely aliased), the server cannot decrypt the body, never reaches the handler, and returns ERROR_INVALID_ENCRYPTION with user:0/0 in the access log.
Asking for the wrong file_type
An email’s body is at file_type=1; attachments are at file_type=10..14. If the sender uploaded only the body and the recipient requests file_type=10, the server returns ERROR_FILE_NOT_EXIST — not because the email is missing, but because no attachment file was ever stored under that suffix.
Treating “0 of 8 stripes” as a network problem
If every parallel download returns ERROR_INVALID_ENCRYPTION, the recipient’s reassembly code reports “Insufficient stripes: 0/8 downloaded”. That looks like a network failure but is actually a wire-encryption failure on every call. Inspect the server log for status 34 entries before suspecting the network.
Using the locker_code field as a payment
The locker_code field exists for symmetry with upload, where it represents storage payment. download doesn’t currently consume it — the server reads the field but ignores it. Don’t expect the field to gate access; that role is intended for the ACL sidecar in future phases.
Points of confusion
- Preamble AN is not authenticated against a coin. Unlike upload, tell, ping, and peek, download’s handler does not call
get_page_by_sn_lock+ AN comparison. The preamble’s AN is consumed by the wire-encryption layer and that’s the end of it. So download currently has no per-coin authorization — only path-correctness and ACL-sidecar presence. - Response page_number is one byte, request page_number is three. The request supports up to 16,777,216 pages (~4 TB of paginated file space, far beyond the 10 MiB cap). The response truncates to the low byte. For files within the 10 MiB cap (max 40 pages of 256 KB), the truncation is harmless because the high bytes of the request page number are always zero. If the cap is ever raised, the response field becomes ambiguous.
- The 1-byte
bytes_per_pagefield is reserved. The server uses a fixed 256 KB page size and ignores this byte. Available for protocol extension — e.g., a future client could request smaller pages for low-bandwidth links. - ACL is presence-only. The server checks
access(acl_path, F_OK). If the sidecar is missing the access is logged as “allowing (legacy file)” and the download proceeds. If the sidecar is present its contents are not read in Phase I. - Total file size is read at the time of the call. The server
fseeks to end and readsftell()on every page. Truncation, deletion, or pruning between page calls returnsERROR_INVALID_PARAMETERif the read offset moves past end.