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 Group6
Command Code74
Server functioncmd_qmail_download2 in cmd_qmail.c
Wire structureqmail_download2_req_t (request), qmail_download_resp_t (response header)
Body layoutPreamble (48) + Payload (37) + Terminator (2)
Response body8-byte header + up to 256 KB of stripe data
EncryptionRequired — AES-128 keyed by recipient’s coin AN
Page sizeFixed at 256 KB by the server (page_size = 256 * 1024)
File-size capQMAIL_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)

OffsetSizeFieldDescription
48–6316file_guid16-byte email GUID. Same value the sender used in upload and tell.
64–7916locker_codeASCII 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).
801file_typePicks 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.
811bytes_per_pageReserved. Server hard-codes 256 KB pages and ignores this byte. Available for protocol extension.
82–843page_number3-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 22TerminatorFixed 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:

OffsetSizeFieldDescription
01file_typeEcho of the request’s file_type.
11versionAlways 0x02.
21bytes_per_pageAlways 0. Reserved by the server.
31page_numberEcho of the request’s page_number (low byte only — truncated from the 3-byte request field).
4–74data_lengthBig-endian byte count of the stripe data that follows. Up to 262144 (256 KB).
8…NdataRaw 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

DecimalHexSymbolMeaning
2500xFASTATUS_SUCCESSPage returned in the response body.
160x10ERROR_INVALID_PACKET_LENGTHBody shorter than 87 bytes or missing terminator.
340x22ERROR_INVALID_ENCRYPTIONWire-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).
1940xC2ERROR_FILESYSTEMServer failed the path-traversal check or could not open the file for reading.
1980xC6ERROR_INVALID_PARAMETERFile on disk exceeds QMAIL_MAX_STRIPE_SIZE (10 MiB), or page_number * 256KB >= total_size (page is past the end of the file).
2020xCAERROR_FILE_NOT_EXISTNo 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.
2540xFEERROR_MEMORY_ALLOCServer 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

  1. 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.
  2. 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.
  3. The 1-byte bytes_per_page field 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.
  4. 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.
  5. Total file size is read at the time of the call. The server fseeks to end and reads ftell() on every page. Truncation, deletion, or pruning between page calls returns ERROR_INVALID_PARAMETER if the read offset moves past end.