QMail Ping — Group 6, Code 72
Long-poll inbox wait. The client opens a TCP request to the recipient’s beacon RAIDA. If tells are already waiting, the server responds immediately. If none are ready, the server parks the connection until a tell arrives or the long-poll watcher times out.
Quick reference
| Command Group | 6 (QMail) |
| Command Code | 72 (0x48) |
| Server function | cmd_qmail_ping in cmd_qmail.c |
| Request packet | 32-byte plaintext RAIDA header + encrypted body fields + plaintext 3E 3E terminator |
| Minimum request body | 50 bytes total: encrypted challenge(16) + qmail identity/auth(32), then plaintext terminator(2) |
| Current rest_core body | 54 bytes total: it reuses the ping/peek builder and includes an encrypted 4-byte timestamp that cmd_qmail_ping ignores |
| Success response | 32-byte plaintext RAIDA response header + encrypted tell-list payload + plaintext 3E 3E terminator |
| No-mail response | No immediate response. The TCP connection is parked in STATE_PING_ACTIVE; timeout returns a status-only header. |
| Encryption | Required — AES-128-CTR, type 1, keyed by the caller’s selected coin AN for this RAIDA |
Purpose
ping is QMail’s long-poll receive primitive. It scans the recipient mailbox directory for .tell files. If one or more tells exist, the server returns them using the same tell-list payload format as peek. If none exist, it creates the inbox directory if needed, registers an inotify watcher, and leaves the TCP connection open.
The server path is selected from the mailbox identity in the request body: QMAIL_MAILBOX_ROOT/{denom_hex}/{serial_number}/inbox/. The request must be sent to the recipient’s beacon RAIDA; other RAIDAs may not have that mailbox coin loaded.
Wire packet
The RAIDA packet header is the 32 plaintext bytes at the front of the TCP packet. The 16-byte challenge/CRC is encrypted with the rest of the request body. raidax groups that challenge with the QMail identity block and calls those first 48 decrypted body bytes qmail_preamble_t.
QMail Ping request packet, bytes on TCP
+-------------------------------+-----------------------------------------+----------------------+
| RAIDA request header | encrypted request body | plaintext terminator |
| 32 bytes | 48 bytes minimum, 52 from rest_core | 2 bytes |
+-------------------------------+-----------------------------------------+----------------------+
| command group=6, command=72 | challenge/CRC(16) | 3E 3E |
| encryption type=1 | QMail identity/auth block(32) | |
| body_size=50 or 54 | optional ignored timestamp(4) | |
| nonce(8) | | |
+-------------------------------+-----------------------------------------+----------------------+
The current rest_core ping path reuses the same body builder as peek, so it sends body_size = 54 (0x0036). The raidax ping handler only requires 48 + 2 body bytes and ignores any bytes after the QMail identity/auth block.
Request header (32 bytes, plaintext)
| Offset | Size | Field | Ping value / meaning |
|---|---|---|---|
| 0 | 1 | Router version | 0x01 |
| 1 | 1 | Split ID | 0x00 for the standard single-frame request |
| 2 | 1 | RAIDA ID | Target beacon RAIDA index, 0–24 |
| 3 | 1 | Shard ID | Current rest_core sends shard 0x03 |
| 4 | 1 | Command Group | 0x06 (QMail) |
| 5 | 1 | Command Code | 0x48 (72, ping) |
| 6–7 | 2 | Coin ID | 00 06 CloudCoin network ID |
| 8 | 1 | Bitfield | 0x01 |
| 9–15 | 7 | Reserved | Zero-filled by rest_core |
| 16 | 1 | Encryption type | 0x01 AES-128-CTR with coin AN |
| 17 | 1 | Encryption key denomination | Denomination of the coin used for wire encryption |
| 18–21 | 4 | Encryption key SN | Serial number of the coin used for wire encryption, big-endian |
| 22–23 | 2 | Body size | 00 36 from current rest_core; 00 32 is the minimum accepted by raidax |
| 24–31 | 8 | Nonce | AES-CTR nonce used to encrypt/decrypt the request body |
Request body (50 bytes minimum, 54 from current rest_core)
Decrypted request body layout
+-------------------+--------------------------+---------------------+----------------------+
| challenge/CRC | QMail identity/auth | optional timestamp | terminator |
| 16 bytes | 32 bytes | 4 bytes | 2 bytes |
| encrypted | encrypted | encrypted if sent | plaintext |
+-------------------+--------------------------+---------------------+----------------------+
| body[0..15] | body[16..47] | body[48..51] | body[52..53] |
| required | required | ignored by ping | required |
+-------------------+--------------------------+---------------------+----------------------+
| Body offset | Size | Encrypted? | Field | Description |
|---|---|---|---|---|
| 0–11 | 12 | Yes | Challenge random | Random bytes generated by the client. |
| 12–15 | 4 | Yes | Challenge CRC32 | Big-endian CRC32 of body bytes 0–11. The server verifies this after decrypting. |
| 16–23 | 8 | Yes | Session ID | All zeros for current QMail AES-128 requests. |
| 24–25 | 2 | Yes | Coin Type | 00 06. |
| 26 | 1 | Yes | Mailbox denomination | Recipient mailbox coin denomination. |
| 27–30 | 4 | Yes | Mailbox serial number | Recipient mailbox serial number, big-endian. |
| 31 | 1 | Yes | Reserved | Was Device ID. Current server ignores it; clients should write zero. |
| 32–47 | 16 | Yes | Authenticity Number | The mailbox coin AN for this RAIDA. After decryption, process_inbox_scan() compares it to the stored AN for the body’s denomination/SN. |
| 48–51 | 4 | Yes if sent | Ignored timestamp | Current rest_core sends this because ping and peek share a builder. cmd_qmail_ping ignores it and calls process_inbox_scan(..., 0, ...). |
| last 2 | 2 | No | Terminator | Fixed 3E 3E. Included in body_size but not encrypted. |
raidax names body offsets 0–47 qmail_preamble_t. In the terminology above, the challenge is still body data because it is encrypted with the rest of the body fields.
Response packet
There are two normal ping outcomes: a tell-list success response when mail is available, or a status-only timeout response when the parked long-poll expires without mail.
QMail Ping success response, bytes on TCP
+-------------------------------+-----------------------------------------+----------------------+
| RAIDA response header | encrypted tell-list payload | plaintext terminator |
| 32 bytes | payload_len bytes | 2 bytes |
+-------------------------------+-----------------------------------------+----------------------+
| status=250, group=6 | array header(8) | 3E 3E |
| body_size=payload_len+2 | record 1 | |
| challenge signature | record 2 | |
| | ... | |
+-------------------------------+-----------------------------------------+----------------------+
QMail Ping timeout response, current raidax stale-watcher path
+-------------------------------+
| RAIDA response header |
| 32 bytes |
+-------------------------------+
| status=17, body_size=0 |
| no encrypted payload |
| no terminator |
+-------------------------------+
Current raidax sets timeout status to ERROR_UDP_FRAME_TIMEOUT (17, 0x11) when a parked ping watcher goes stale. Some client-side comments and aliases refer to RAIDA_STATUS_TIMEOUT (245, 0xF5); the current server code path shown above is the authoritative behavior.
Response header (32 bytes, plaintext)
| Offset | Size | Field | Description |
|---|---|---|---|
| 0 | 1 | RAIDA ID | Responding RAIDA index. |
| 1 | 1 | Shard ID | Legacy response path writes zero. |
| 2 | 1 | Status | 0xFA on success, 0x11 on current long-poll timeout, or another error code. |
| 3 | 1 | Command Group | 0x06. |
| 4–5 | 2 | Frame count | Legacy response writes 00 01. |
| 6–7 | 2 | Echo bytes | Echoes legacy request bytes 30–31. |
| 8 | 1 | Reserved | Zero. |
| 9–11 | 3 | Body size | Big-endian length of encrypted payload plus terminator. Zero for timeout. |
| 12–15 | 4 | Execution time | Server execution time in microseconds, big-endian. |
| 16–31 | 16 | Challenge signature | Legacy response signature used by the client to verify the response belongs to the request. |
Tell-list payload on success (encrypted)
Decrypted tell-list payload
+----------------------+--------------------------------------+--------------------------------+
| Array header | Tell record 1 | Tell record 2 ... |
| 8 bytes | 64 + (M * 32) + manifest_len bytes | variable |
+----------------------+--------------------------------------+--------------------------------+
| Payload offset | Size | Field | Description |
|---|---|---|---|
| 0 | 1 | tell_count | Number of tell records following this array header. For a ping success response, this should be one or more. |
| 1–2 | 2 | total_tells | Currently zero-filled by the server. |
| 3–7 | 5 | reserved | Zero-filled. |
| 8... | Variable | tell records | tell_count records, each parsed using the record structure below. |
Tell record structure
Each returned record is the full contents of one .tell file. The server does not add a per-record length field. For manifest v1 records, the client reads three fields from the 64-byte file header to compute the record size:
stripe_count(byte 29, called M): number of server-location entries.file_count(byte 52, called N) andfile_entry_size(byte 53, always16): together describe the file manifest. The big-endianmanifest_lenat bytes 54–55 must equalN × file_entry_size.
manifest v1 record_size = 64 + (M * 32) + manifest_len
legacy v0 record_size = 64 + (M * 32), then optionally skip an 18-byte footer
+-------------------------+-----------------------------+--------------------------------+
| QMail file header | server location entries | file manifest entries |
| 64 bytes | M entries * 32 bytes | N entries * file_entry_size |
+-------------------------+-----------------------------+--------------------------------+
Current manifest v1 records do not include a trailing footer; the manifest entries are the last bytes of each record. Pre-manifest legacy records have manifest_version=0, no manifest entries, and may have an 18-byte recipient footer on disk (tag 0x50, length 16, recipient locker). A receiver that sees that footer after a legacy record should skip it before parsing the next record.
QMail file header (64 bytes)
| Record offset | Size | Field | Description |
|---|---|---|---|
| 0–15 | 16 | email_id | Message/file GUID. Used by download to fetch stored stripes. |
| 16–17 | 2 | sender_coin_id | 00 06. |
| 18 | 1 | sender_denomination | Sender mailbox denomination. |
| 19–22 | 4 | sender_serial_number | Sender mailbox serial number, big-endian. |
| 23 | 1 | sender_device_id | Device tag copied from the tell blob. Logged, not enforced. |
| 24–27 | 4 | timestamp | Sender timestamp, big-endian Unix seconds. |
| 28 | 1 | tell_type | Current QMail email tells use 0. |
| 29 | 1 | stripe_count (M) | Number of 32-byte server entries following this header. The tell sender validated this as 1–32. |
| 30–45 | 16 | locker_code | Locker/download key copied into the notification. The receiver passes this to download. |
| 46–49 | 4 | total_file_size | Big-endian original size of the body file only (file_type 0x01). Attachment original sizes live in the manifest entries. |
| 50 | 1 | version | 0x02. |
| 51 | 1 | manifest_version | 1 for current manifest records. 0 marks a legacy pre-manifest record. |
| 52 | 1 | file_count (N) | For manifest v1, number of files described by the manifest, including the body. Attachment count is N - 1. Legacy v0 records write 0. |
| 53 | 1 | file_entry_size | For manifest v1, always 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 N × file_entry_size. Legacy v0 records write 0. |
| 56 | 1 | manifest_flags | For manifest v1, bit 0 (footer_removed) is set and bit 1 (crc32_present) is set when the per-entry CRC32 field is populated. Bits 2–7 are reserved. |
| 57–63 | 7 | reserved | Pass-through tail of the manifest header. Must be zero for manifest v1. |
Server location entry (32 bytes each)
There are M entries immediately after the file header. This structure tells the recipient which RAIDA servers hold the stripes and how to connect to them for download.
| Entry offset | Size | Field | Description |
|---|---|---|---|
| 0 | 1 | stripe_index | Stripe number within the file’s stripe set. |
| 1 | 1 | stripe_type | Current rest_core writes 0 for data and 1 for parity. |
| 2 | 1 | server_id | RAIDA index holding this stripe. This is stored in the first byte of the legacy stripe_id field. |
| 3–9 | 7 | stripe_id reserved | Currently zero-filled by rest_core. |
| 10–19 | 10 | IPv6 prefix | Zero-filled for IPv4-mapped addresses. |
| 20–21 | 2 | IPv4 marker | FF FF for IPv4-mapped address. |
| 22–25 | 4 | IPv4 address | Storage RAIDA IPv4 octets. |
| 26–27 | 2 | Port | Storage RAIDA TCP port, big-endian. |
| 28–31 | 4 | reserved | Zero-filled by rest_core. |
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 sizing the reassembly buffer so padding bytes are trimmed correctly.
| Entry offset | Size | Field | Description |
|---|---|---|---|
| 0 | 1 | file_type | 0x01 body, 0x0A attachment 1, 0x0B attachment 2, etc. First entry MUST be the body. |
| 1 | 1 | file_flags | Bit 0 = body; bit 1 = attachment; other bits reserved. |
| 2–3 | 2 | reserved | Pass-through; must be zero. |
| 4–11 | 8 | original_size | Big-endian uint64 size before striping/padding. The receiver uses this to size each file’s reassembly buffer. |
| 12–15 | 4 | crc32 | Big-endian CRC32 over the original file bytes. Optional: written only when the file header’s manifest_flags.crc32_present bit is set; zero otherwise. Receivers must only verify the CRC when the flag is set. |
Local REST test calls
These calls use the two-client local setup: client1 is the sender on port 8081 and client2 is the receiver on port 8082. Stop the receiver background beacon if you want this manual ping call to be the process that consumes the Tell.
# Optional: stop client2 background beacon before the sender sends mail
GET http://127.0.0.1:8082/api/qmail/local/beacon/control?action=stop
# Long-poll client2's beacon RAIDA; notifications include manifest_version, file_count, manifest_flags, and files[]
GET http://127.0.0.1:8082/api/qmail/net/beacon/ping?timeout=10
# Download the received email and all manifest-listed attachments
GET http://127.0.0.1:8082/api/qmail/net/messages/download?file_guid=<FILE_GUID>
# Inspect downloaded attachment rows
GET http://127.0.0.1:8082/api/qmail/db/attachments/list?email_id=<FILE_GUID>
Long-poll mechanics
- Validate the request body length and
3E 3Eterminator. - Decrypt the body and validate the 16-byte challenge/CRC.
- Read the mailbox identity from the decrypted body and verify the AN.
- Scan
QMAIL_MAILBOX_ROOT/{denom_hex}/{sn}/inbox/for.tellfiles. - If one or more tells are found, read them into the tell-list payload, queue them for deletion, and send a success response.
- If no tells are found, create the inbox directory if needed, register an inotify monitor, set
STATE_PING_ACTIVE, and leave the TCP connection open. - When a tell file is atomically renamed into the inbox, the watcher dispatches
qmail_resume_ping2(), which rescans and sends the success response. - If the watcher expires first, current raidax sends a status-only timeout header with status
17and body size0.
Status codes
| Decimal | Hex | Symbol | Meaning |
|---|---|---|---|
| 250 | 0xFA | STATUS_SUCCESS | Returned with the tell-list response when one or more tells were ready immediately or arrived during the long-poll. |
| 17 | 0x11 | ERROR_UDP_FRAME_TIMEOUT | Current raidax stale-watcher timeout path. Response is a 32-byte header only; no body and no terminator. |
| 8 | 0x08 | ERROR_COIN_NOT_FOUND | Mailbox (denom, SN) from the body is not loaded on this RAIDA. |
| 16 | 0x10 | ERROR_INVALID_PACKET_LENGTH | Request body is shorter than the required 48 + 2 bytes. |
| 33 | 0x21 | ERROR_INVALID_EOF | Protocol terminator was not 3E 3E. |
| 34 | 0x22 | ERROR_INVALID_ENCRYPTION | Server could not decrypt/validate the body before reaching cmd_qmail_ping. |
| 37 | 0x25 | ERROR_INVALID_CRC | The decrypted challenge CRC did not match. |
| 194 | 0xC2 | ERROR_FILESYSTEM | Inbox opendir() failed for a reason other than ENOENT, or the inotify watch could not be armed. |
| 200 | 0xC8 | ERROR_INVALID_AN | Body AN does not match the server’s stored AN for the body denomination/SN. |
| 254 | 0xFE | ERROR_MEMORY_ALLOC | Server response buffer allocation failed. |
Compatibility note: some rest_core client code comments refer to RAIDA_STATUS_TIMEOUT (245) for clean long-poll timeout. Current raidax source sends 17 on the stale watcher path.
Common mistakes
Expecting an immediate empty response
Unlike peek, ping does not normally return tell_count = 0. No mail means the connection is parked until mail arrives or the watcher times out.
Confusing the packet header with qmail_preamble_t
The 32-byte RAIDA header is plaintext. The server’s 48-byte qmail_preamble_t is decrypted body data: challenge/CRC plus the QMail identity/auth block.
Using the timestamp as a filter
The 4-byte timestamp field is meaningful for peek. Current rest_core sends it on ping too, but cmd_qmail_ping ignores it and scans with timestamp 0.
Computing record_size without reading the manifest header
Each record’s size depends on both stripe_count (byte 29) and manifest_len (bytes 54–55). A client that uses 64 + M*32 alone will land partway into the manifest of the first record and misparse every subsequent one. Always include manifest_len in the per-record advance.
Points of confusion
- ping vs peek. ping blocks until a tell arrives or the watcher times out. peek returns immediately and applies
since_timestamp. - Tells are deleted on read. A successful ping queues returned files for deletion after directory scanning. Multiple devices sharing one mailbox can race; only the first device receives a given tell.
- Records are variable length. There is no length prefix per record. Use
stripe_count(byte 29) andmanifest_len(bytes 54–55) from the file header to compute64 + M*32 + manifest_len. - Timeout currently uses status 17. If a client is only looking for status 245, it may misclassify the current raidax timeout response.