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 Group6 (CHAT/QMail)
Command Code70
Server functioncmd_qmail_upload2 in cmd_qmail.c
Wire structureqmail_upload2_req_t in qmail_structs.h
Body layoutPreamble (48) + Payload (38) + Data (N) + Terminator (2)
Response bodyNone (status only)
EncryptionRequired — AES-128 keyed by sender’s coin AN
TransportTCP recommended (data length up to ~10 MB per stripe)
Server-side limitQMAIL_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)

OffsetSizeFieldDescription
48–6316File GUID16-byte identifier for the email this stripe belongs to. Same GUID on every stripe of every file in the email.
64–7916Locker CodeASCII 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.
801File TypeSelects which file inside the email this stripe belongs to. See File Types table below.
811Storage DurationStorage retention code. Server logs but does not currently enforce.
82–854Data LengthLength of the stripe data that follows, in bytes (big-endian).
86…NStripe DataThe actual stripe bytes. Length must equal the Data Length field.
last 22TerminatorFixed 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):

CodeSuffixPurpose
0.metaEmail metadata (subjects, attachment IDs — future use).
1.qmailEmail body and styling. The primary file in every email.
2–9.blobReserved (web page, instant message, key material, etc.).
10.0.binFirst attachment.
11.1.binSecond attachment.
12–255.{N−10}.binSubsequent attachments.

Response

No body. The status byte in the response header indicates the outcome.

Status codes

DecimalHexSymbolMeaning
2500xFASTATUS_SUCCESSStripe stored under QMAIL_PUBLIC_UPLOAD_ROOT/G1/G2/GUID/00000000GUID.suffix; .acl sidecar written; locker payment consumed asynchronously.
80x08ERROR_COIN_NOT_FOUNDSender’s (denom, SN) is not loaded on this RAIDA.
160x10ERROR_INVALID_PACKET_LENGTHBody too short (< 88 bytes), missing terminator, or 48 + 38 + data_length + 2 doesn’t equal the actual body size.
400x28ERROR_INVALID_SN_OR_DENOMINATIONDenomination outside −8…+6.
1690xA9ERROR_PAYMENT_REQUIREDLocker key was empty or the asynchronous coin download to the storage RAIDA failed to start.
1940xC2ERROR_FILESYSTEMServer could not create the storage directory or write the file (disk full, permission denied, path-traversal check failed).
2000xC8ERROR_INVALID_ANPreamble 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

  1. 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.
  2. 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.
  3. File type 2 vs 10. File type 2 is reserved for non-email blob storage (web pages, IM, etc.) and gets the .blob suffix. Attachments start at file type 10. Don’t use 2 for attachments.
  4. Locker payment is asynchronous. consume_locker_payment kicks 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.
  5. 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.