QMail Tell — Group 6, Code 71
Notifies a recipient’s beacon RAIDA that mail is waiting. The sender uploads stripes to many RAIDAs but sends one tell per recipient to that recipient’s pre-agreed beacon RAIDA.
Quick reference
| Command Group | 6 |
| Command Code | 71 |
| Server function | cmd_qmail_tell in cmd_qmail.c |
| Wire structures | qmail_tell_req_t, qmail_address_entry_t, qmail_file_header_t |
| Body layout | Preamble (48) + Routing (48) + Addresses (N×32) + Tell record (64 + M×32 + manifest_len) + Terminator (2) |
| Encryption | Required — AES-128 keyed by sender’s coin AN |
| Side effect | One .tell file written per recipient under QMAIL_MAILBOX_ROOT/{denom}/{sn}/inbox/ (atomic publish via temp+rename) |
Purpose
After uploading the stripes, the sender calls tell on each recipient’s beacon RAIDA. The tell is a public delivery envelope: a 64-byte file header, M 32-byte server-location entries, and a file manifest. The server-location entries tell the recipient which RAIDAs hold the stripes; the manifest tells the recipient which stored files exist and the original byte size of each file. The beacon writes the record to the recipient’s inbox directory; when the recipient’s ping or peek arrives, the beacon reads back the record and returns it.
The sender pays a per-recipient inbox fee by attaching a funded locker key to each address entry. The same funded inbox-fee locker is also copied into the routing header's beacon_payment_locker field so the beacon can validate the fee before accepting the delivery and pass the locker code on to the recipient.
Privacy boundary
The Tell envelope is visible to the beacon operator. It MUST NOT contain private display data such as subject, body preview, attachment names, labels, or human-readable filenames. Those fields belong in the private CBDF meta object (file_type=0) that the recipient downloads from the storage RAIDAs after receiving the Tell.
Request body
Preamble (48)
Routing header (48) qmail_tell_req_t
R × Address entry (32 each) qmail_address_entry_t
File header (64) qmail_file_header_t (start of blob)
M × Server location (32 each) pass-through storage server map
F × File manifest entry (16 each) pass-through file list (manifest v1)
Terminator (2) 3E 3E
Server validates: body_size == 48 + 48 + R×32 + 64 + M×32 + manifest_len + 2. Here R is the recipient count, M is the storage-server count, and F is the file count in the manifest.
Routing header (48 bytes, offsets 48–95)
Layout matches qmail_tell_req_t:
| Offset | Size | Field | Description |
|---|---|---|---|
| 48–63 | 16 | email_id | The 16-byte GUID for this email. Same value used as File GUID in the upload calls and used by the recipient when they later call download. |
| 64–67 | 4 | total_file_size | Big-endian total size in bytes. The beacon does not validate this field on the routing header itself — see the matching field on the file header (offset 46–49) for the enforced check. |
| 68–71 | 4 | reserved_1 | Available for future use. Set to zero. |
| 72–75 | 4 | client_timestamp | Big-endian Unix timestamp (seconds) at the moment of build. Server rejects with ERROR_INVALID_PARAMETER if it differs from server time by more than 60 seconds. The 60-second tolerance is a tunable security knob — lower values shorten the replay window once volunteer-operator NTP discipline allows it. |
| 76 | 1 | tell_type | Must be 0 today. Server rejects other values. |
| 77 | 1 | address_count (N) | Number of recipients in this tell. Must be > 0. |
| 78 | 1 | server_count (M) | Number of 32-byte server-location entries that follow the file header. Must equal stripe_count in the file header. |
| 79–94 | 16 | beacon_payment_locker | Funded inbox-fee locker for the recipients represented by this Tell. ASCII, null-padded. The beacon may peek this locker to verify that it holds at least the recipient's configured inbox fee before accepting delivery, and the value is passed through to the recipient. |
| 95 | 1 | reserved_2 | Available for future use. Set to zero. |
Address entries (N × 32 bytes)
Layout per recipient matches qmail_address_entry_t. The server iterates the N entries and writes one .tell file to each recipient’s inbox. Offsets below are relative to the start of each entry.
| Offset | Size | Field | Description |
|---|---|---|---|
| 0 | 1 | type | Recipient kind: 0=To, 1=CC, 2=BCC, 3=Mass. |
| 1–2 | 2 | coin_id | Network ID. Fixed 00 06. |
| 3 | 1 | denomination | Recipient’s mailbox denomination (signed; valid range −8…+6). |
| 4–7 | 4 | serial_number | Recipient’s mailbox SN (big-endian). |
| 8–23 | 16 | locker_payment_key | Funded locker covering this recipient’s inbox fee. ASCII, null-padded. Must be non-empty (all-zero fails delivery for this recipient). |
| 24–31 | 8 | reserved | Available for future use, per-recipient. Set to zero. |
Notification blob (64 + M × 32 + manifest_len bytes)
The blob is the pass-through payload — the beacon stores it verbatim and the recipient reads it back through ping/peek. For current manifest v1 records, the first 64 bytes are the file header (qmail_file_header_t), followed by M 32-byte server-location entries and then F 16-byte file manifest entries. There is no trailing recipient footer in manifest v1 records; the manifest entries are the last bytes of the blob.
Two of the manifest-header bytes carry sender-controlled metadata that the beacon also acts on:
edit_sequence(file-header offset 57) is a monotonic byte set by the sender. The beacon stores the blob verbatim, but on duplicate-GUID delivery it first reads byte 57 of any existing.tellin the recipient’s inbox and rejects the new tell whenincoming < existing. Senders revising a previously-sent email bumpedit_sequence; replayed older tells are dropped.0= original,1+= edits. LEGACY manifest-version-0 senders are treated as0by definition.parity_algo(file-header offset 58) names the fault-tolerance algorithm the sender used to stripe the email.0= RAID-5 XOR (1 parity stripe). Reserved for future Reed-Solomon / GF(2^8) IDs. The beacon uses this to compute its body-size estimate fortotal_file_sizevalidation; unknown algorithms skip the size check.
File header (64 bytes)
The two bytes ES (edit_sequence, offset 57) and PA (parity_algo, offset 58) are the sender-controlled extension fields introduced by PR 2. ES drives the beacon's "newer edit wins" guard on duplicate-GUID delivery; PA selects the fault-tolerance algorithm the sender used and gates the beacon's total_file_size sanity check. See the offset 57 and offset 58 rows in the table below for the semantics. Field-key for the diagram: DN=denomination, DV=sender_device_id, TT=tell_type, SC=stripe_count, VR=version, MV=manifest_version, FC=file_count, FES=file_entry_size, MFL=manifest_flags, tfs=total_file_size.
| Offset | Size | Field | Description |
|---|---|---|---|
| 0–15 | 16 | email_id | Same GUID as the routing header. Server validates they match. |
| 16–17 | 2 | sender_coin_id | Network ID. Must be 00 06. |
| 18 | 1 | sender_denomination | Must equal preamble denomination. |
| 19–22 | 4 | sender_serial_number | Big-endian. Must equal preamble SN. |
| 23 | 1 | sender_device_id | Per-sender device tag. Logged, not enforced. |
| 24–27 | 4 | client_timestamp | Big-endian. Independent of the routing-header timestamp; both are checked. |
| 28 | 1 | tell_type | Must be 0. |
| 29 | 1 | stripe_count | Number of server-location entries that follow. Must be 1…32 and equal the routing-header server_count. |
| 30–45 | 16 | locker_code | Standardized 16-byte locker code passed through to the recipient. |
| 46–49 | 4 | total_file_size | Big-endian original size of the primary body object (file_type=1) for legacy convenience. Current recipients should use the manifest entries as the authoritative size list for file_type=0, file_type=1, and attachments. The beacon may sanity-check server-owned fields, but it cannot verify cross-RAIDA storage geometry because it generally does not hold the stored object bytes. |
| 50 | 1 | version | Set to 0x02. |
| 51 | 1 | manifest_version | 1 for ALL current manifest records, including mail stored through Object Transfer v1. 0 marks a legacy pre-manifest record and is accepted only for compatibility; new senders must write 1. No other value is valid — object-stored mail needs no richer entry because the object key is derived from the tell itself (object_id = the email GUID in the routing header, generation 1, file_type from the manifest entry). |
| 52 | 1 | file_count | For manifest v1, number of stored files including private meta, body, and attachments. Attachment count is file_count - 2 when both required entries are present. Must be ≥ 2 for new QMail beta records. Legacy v0 records write 0. |
| 53 | 1 | file_entry_size | For manifest v1, must be 16. Legacy v0 records write 0. |
| 54–55 | 2 | manifest_len | For manifest v1, big-endian byte length of the manifest trailing section. Must equal file_count * file_entry_size. Legacy v0 records write 0. |
| 56 | 1 | manifest_flags | For manifest v1, bit 0 (footer_removed) is set because the 18-byte recipient footer is not part of the current wire format. Bit 1 (crc32_present) is set when the sender filled the per-entry CRC32 field; current object-storing senders always set it — the CRC32 is the receiver’s end-to-end integrity check. Bits 2–7 are reserved (must be 0). |
| 57 | 1 | edit_sequence | Sender-controlled monotonic revision byte. 0 = original, 1+ = edits. The beacon reads byte 57 of any existing inbox .tell on duplicate-GUID delivery and rejects when incoming < existing. LEGACY manifest-v0 records are treated as 0. |
| 58 | 1 | parity_algo | Fault-tolerance algorithm ID. 0 = RAID-5 XOR (1 parity stripe). Reserved for future Reed-Solomon / GF(2^8) algorithms. The beacon uses this to compute the body-size estimate for total_file_size validation. |
| 59–63 | 5 | reserved | Must be zero for manifest v1. Pass-through tail of the manifest header. |
Server location entries (32 bytes each)
The 32-byte format is partially structured (the recipient’s ping parser reads the first three bytes), partially opaque. The rest_core sender writes:
| Offset | Size | Field | Description |
|---|---|---|---|
| 0 | 1 | stripe_index | Position 0…M−1 within the file’s stripe set. |
| 1 | 1 | stripe_type | Currently encoded as 0=data, 1=parity by the sender. This is not attachment identity; attachment identity belongs in the manifest entry’s file_type. |
| 2 | 1 | server_id | RAIDA index (0…24) holding this stripe. |
| 3–19 | 17 | opaque | Currently zero-filled by the rest_core sender. Available bytes inside the server-location entry. |
| 20–25 | 6 | IPv4-mapped address | Bytes 20–21 are FF FF; bytes 22–25 hold the IPv4 octets of the storage RAIDA. |
| 26–27 | 2 | port | Big-endian TCP port of the storage RAIDA. |
| 28–31 | 4 | trailing | Currently zero-filled. |
File manifest entries (N × 16 bytes)
The manifest is the authoritative public object list for this email. New QMail beta senders list the private CBDF meta object first with file_type=0x00, the body/content object second with file_type=0x01, and attachments afterward using sequential file types 0x0A, 0x0B, and so on. The receiver downloads file_type=0 first, parses private subject/filename/display metadata locally, and then downloads the body and attachments described by that private meta. The server validates manifest structure but does not read or interpret private CBDF keys.
Object-stored mail uses this same manifest. Senders that store files through Object Transfer v1 (commands 76–84) upload every file under object_id = the email GUID at generation 1, so the receiver derives the full object key from the tell itself: object_id from the routing header, file_type and original_size from the manifest entry. The receiver fetches via object-info/get-range (cmd 81/82), falls back to the legacy striped pages when the object does not exist, and verifies the reconstructed file against the entry’s sender-authored CRC32. No richer manifest entry is needed or permitted on the wire.
| Entry offset | Size | Field | Description |
|---|---|---|---|
| 0 | 1 | file_type | 0x00 private CBDF meta, 0x01 body/content, 0x0A attachment 1, 0x0B attachment 2, etc. |
| 1 | 1 | file_flags | Bit 0 = body/content; bit 1 = attachment; bit 2 = private meta; other bits reserved. |
| 2–3 | 2 | reserved | Must be zero. |
| 4–11 | 8 | original_size | Big-endian uint64 size before striping/padding. |
| 12–15 | 4 | crc32 | Big-endian CRC32 over the original file bytes. Optional: written when manifest_flags.crc32_present (bit 1) is set; zero otherwise. Receivers must only verify the CRC when the flag is set. |
Local REST test calls
These calls are for a two-client local test setup where client1 is the sender on port 8081 and client2 is the receiver on port 8082.
# Confirm both local identities
GET http://127.0.0.1:8081/api/qmail/local/identity/whoami
GET http://127.0.0.1:8082/api/qmail/local/identity/whoami
# Optional: stop receiver background beacon so manual peek/ping can consume the Tell deterministically
GET http://127.0.0.1:8082/api/qmail/local/beacon/control?action=stop
# Sender uploads body + one attachment and sends Tell to the receiver
GET http://127.0.0.1:8081/api/qmail/net/messages/upload_and_tell?to=<CLIENT2_QMAIL_ADDRESS>&subject=ManifestTest&body=Hello%20from%20client1&attachment_file_path=C%3A%5Ctmp%5Catt1.txt
# Poll the async task returned by upload_and_tell
GET http://127.0.0.1:8081/api/system/tasks?task_id=<TASK_ID>
# Inspect sender receipt after the task returns file_guid
GET http://127.0.0.1:8081/api/qmail/receipts?guid=<FILE_GUID>
# Receiver consumes the Tell and sees manifest_version/file_count/files[] in JSON
GET http://127.0.0.1:8082/api/qmail/net/beacon/peek?since=0
# Receiver downloads the body and all manifest-listed attachments
GET http://127.0.0.1:8082/api/qmail/net/messages/download?file_guid=<FILE_GUID>
# Confirm downloaded attachment rows and file_type values
GET http://127.0.0.1:8082/api/qmail/db/attachments/list?email_id=<FILE_GUID>
GET http://127.0.0.1:8082/api/qmail/db/attachments/get?email_id=<FILE_GUID>&attachment_id=<ATTACHMENT_ID>&info=1
Response
No body. Status only.
Status codes
| Decimal | Hex | Symbol | Meaning |
|---|---|---|---|
| 250 | 0xFA | STATUS_SUCCESS | At least one recipient inbox received the .tell file. |
| 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, missing terminator, address_count or server_count = 0, or actual size doesn’t match the calculated size. |
| 18 | 0x12 | ERROR_WRONG_RAIDA | Zero deliveries succeeded — every recipient was rejected (bad denom, fee unpaid, filesystem error, empty locker key). |
| 40 | 0x28 | ERROR_INVALID_SN_OR_DENOMINATION | Sender denomination outside −8…+6. |
| 167 | 0xA7 | ERROR_PAYMENT_PROCESSING | Temporary inbox-fee lookup or locker-validation dependency failure. Sender must keep the Tell queued and retry; this is not a permanent recipient failure. |
| 198 | 0xC6 | ERROR_INVALID_PARAMETER | Validation failed on the blob: bad sender_coin_id, denomination/SN mismatch with preamble, timestamp out of range, tell_type != 0, stripe_count out of 1..32, stripe_count != server_count, unsupported manifest version, invalid manifest length, nonzero manifest reserved bytes, missing required private-meta/body manifest entries, or malformed file flags. |
| 200 | 0xC8 | ERROR_INVALID_AN | Preamble AN does not match server’s stored AN. |
Server-side delivery
For each accepted recipient the server writes:
Path: /opt/raidax/QMail_Data/mailboxes/{denom_hex}/{sn}/inbox/00000000{GUID_hex}.tell
Body: pass-through Tell record
64-byte file header
M×32 server entries
N×16 file manifest entries
Migration note: pre-manifest .tell files on disk include an 18-byte recipient
footer (tag=0x50, length=16, recipient_locker). Implementations may strip that
footer when migrating older files. Current manifest v1 records have no footer.
Writes use atomic publish: open a hidden tempfile (.tmp.PID.TIME.SEQ.GUID.tell) with O_CREAT|O_EXCL, write, fsync, and rename() into the final .tell path. The recipient’s long-poll inotify watch fires on IN_MOVED_TO only after the rename, guaranteeing the watcher never sees a partial file. Any .tmp.* orphan from a crashed sender is ignored by the directory scanner because the dotfile prefix.
Before each per-recipient write, the beacon checks for an existing .tell at the target path. If one exists and its edit_sequence (byte 57) is greater than the incoming blob’s, the recipient is skipped (the older revision is not allowed to clobber the newer one). Equal-or-greater edit_sequence values overwrite, matching the documented idempotent re-send / edit semantics.
After delivery, when the recipient’s ping or peek reads the file into the response, the server remove()s the file. Tells are one-shot; once read they are gone.
Common mistakes
Skew between client clock and server clock
The 60-second timestamp tolerance is checked twice (routing header + file header). If the sending machine’s clock is wrong by more than 60 seconds, every tell returns ERROR_INVALID_PARAMETER. NTP-sync the sender.
stripe_count vs server_count
The file header’s stripe_count and the routing header’s server_count describe the same M number from different angles. They must be equal — the server compares them and rejects on mismatch. stripe_count is also bounded by 1–32; sending 0 or 33+ is rejected.
Sender identity lives in two places
The preamble has (denom, SN), and the file header has (denom, SN). Both are validated and must match. If you change the preamble for any reason (per-RAIDA AN swapping during parallel send), make sure the blob still uses the original sender identity, not whatever you just wrote into the preamble.
One tell per beacon, not per recipient’s storage
tell is sent to the recipient’s beacon RAIDA only — not to the storage RAIDAs that hold the stripes. Sending tell to a storage RAIDA wastes a packet; it’ll be accepted but no recipient will ever poll that RAIDA for the notification.
Points of confusion
- The server doesn’t verify file existence. tell just writes the notification blob to the recipient’s inbox; it does not contact the storage RAIDAs to confirm the stripes are actually there. If you call tell before upload finishes, the recipient will be told about a file they cannot download.
- Inbox-fee lookup failure is retryable, not free. A definite no-fee result means an authoritative zero-fee response (for example
success:truewithinbox_fee:0) or an explicitly documented server-side mapping such as contact-not-found to fee=0. Do not treatsuccess:false, an error envelope, a missingsuccessfield, bad JSON, or no response as free. If the beacon cannot determine the recipient’s inbox fee because rest_core or another payment dependency failed, the beacon returnsERROR_PAYMENT_PROCESSING(167) so the client keeps the Tell queued for retry. stripe_typeis not attachment identity. It remains0=dataor1=parity. Attachment identity belongs in the file manifest’sfile_typefield.- Atomic-publish is recent. Older raidax builds wrote the
.tellfile in place. The current build uses temp+rename so the inotify watcher never sees a partial file. Senders see no behavior change; recipients see a smaller race window. - Multiple reserved regions. Three structures in this command have reserved bytes:
reserved_1[4]+reserved_2[1]in the routing header (beacon-local),reserved[8]per recipient address entry (beacon-local), andreserved[5]at offsets 59–63 of the file header (pass-through). Per-filereserved[2]in each manifest entry is also pass-through but only 2 bytes wide. Bytes 57 (edit_sequence) and 58 (parity_algo) of the file header used to be reserved; they now carry defined meanings.