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 carries a pass-through record: 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 beacon verifies the locker holds at least the recipient’s configured fee before accepting the delivery.
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 | Reserved for future beacon-side payments. ASCII, null-padded. Currently parsed but unused; the only locker check that runs is per-recipient (see address entries). |
| 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 email (body plus attachments). The beacon sanity-checks this against the on-disk body stripe it stored during upload: body_est = body_stripe_bytes × (stripe_count − 1). If the client value is more than 5% below that lower bound, the beacon overwrites the field with the estimate in the stored .tell (it never rejects the tell on size alone). The check is skipped when parity_algo is unknown or stripe_count == 1. The 5% slack absorbs bit-padding rounding noise from the striping math and is itself a tunable knob — once the manifest gives every file’s original_size, the check can become an exact equality. |
| 50 | 1 | version | Set to 0x02. |
| 51 | 1 | manifest_version | 1 for current manifest records. 0 marks a legacy pre-manifest record and is accepted only for compatibility; new senders must write 1. |
| 52 | 1 | file_count | For manifest v1, number of stored files including the body. Attachment count is file_count - 1. Must be ≥ 1. 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. 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 list of files that belong to this email. The body is listed first with file_type=0x01; attachments follow using sequential file types 0x0A, 0x0B, and so on. The receiver uses original_size when reassembling stripes so padding bytes are trimmed correctly. The first manifest entry MUST be the body (file_type=0x01); the server rejects with ERROR_INVALID_PARAMETER otherwise.
| Entry offset | Size | Field | Description |
|---|---|---|---|
| 0 | 1 | file_type | 0x01 body, 0x0A attachment 1, 0x0B attachment 2, etc. |
| 1 | 1 | file_flags | Bit 0 = body; bit 1 = attachment; 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. |
| 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, or first manifest entry is not the body. |
| 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 verification is permissive on lookup failure. If the beacon’s call to rest_core for the recipient’s inbox fee fails, fee is treated as 0 and the locker check passes. Recipients who haven’t configured a fee in rest_core are effectively free to mail.
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.