QMail Ping — Group 6, Code 72
Long-poll the recipient’s beacon RAIDA for new tell notifications. The connection parks until mail arrives or the socket-level timeout fires.
Quick reference
| Command Group | 6 |
| Command Code | 72 |
| Server function | cmd_qmail_ping2 in cmd_qmail.c |
| Body layout | Preamble (48) + optional ignored timestamp (4) + Terminator (2) |
| Response | Tell-list array (header + N notification records) on mail arrival; status-only with STATUS_TIMEOUT on socket timeout |
| Encryption | Required — AES-128 keyed by recipient’s coin AN |
| Side effect on success | All matching .tell2 files in the recipient’s inbox are read into the response and then remove()d |
Purpose
The recipient’s mailbox client opens a TCP connection to its beacon RAIDA and sends a ping. If the inbox already has tells, the server returns them immediately. Otherwise the server arms an inotify watch on the inbox directory and parks the connection in STATE_PING_ACTIVE. When a tell later arrives (writer’s rename triggers IN_MOVED_TO), the watcher wakes the parked connection, the server scans the inbox into a response buffer, and ships the bytes back.
Once a tell is read into a response, the server deletes the .tell2 file. Tells are one-shot — if multiple devices share the same mailbox, only the first ping receives each tell.
Request body
ping has no required command payload. The minimum body is the 48-byte preamble plus the 2-byte terminator. Current rest_core sends the shared ping/peek body with an additional 4-byte timestamp after the preamble; raidax accepts and ignores those 4 bytes for ping.
Preamble (48 bytes) <- recipient identity + AN
Optional timestamp (4) accepted but ignored by ping
Terminator (2 bytes) 3E 3E
Total body size: 50 bytes minimum. Current rest_core sends 54 bytes because it includes the same 4-byte timestamp field used by peek. The preamble’s (denomination, serial_number) identify the inbox to scan, and the AN authenticates the request.
Response body
On success the response body is a tell-list:
Header (8 bytes):
byte 0 tell_count (number of records that follow)
bytes 1-2 total_tells (currently zero-filled)
bytes 3-7 reserved (zero-filled) <- 5 reserved bytes
Record 1: file header (64) + M×32 server-location entries + 18-byte footer
Record 2: ...
...
Record N
Each record is the verbatim contents of one .tell2 file from the recipient’s inbox — the pass-through blob written by tell plus the 18-byte per-recipient footer (tag=0x50, length=16, receiver_locker[16]).
Per-record content
The first 64 bytes of each record are the qmail_v2_file_header_t — same layout the sender wrote in tell. The next M×32 bytes are server-location entries, where M is stripe_count from the file header. The 18 trailing bytes are the locker footer.
The recipient parses each record to extract the email GUID, sender SN+denom, file size, and the list of (server_id, stripe_index, IP, port) tuples needed to issue parallel download requests.
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 after long-poll wake). |
| 245 | 0xF5 | STATUS_TIMEOUT | Connection timed out at the socket level after parking with no tells. Body is empty. |
| 8 | 0x08 | ERROR_COIN_NOT_FOUND | Recipient’s (denom, SN) is not loaded on this RAIDA. The client targeted the wrong beacon. |
| 16 | 0x10 | ERROR_INVALID_PACKET_LENGTH | Body too short or missing terminator. |
| 194 | 0xC2 | ERROR_FILESYSTEM | Server failed to opendir() the inbox or arm the inotify watch. |
| 200 | 0xC8 | ERROR_INVALID_AN | Preamble AN doesn’t match the server’s stored AN. Authentication failed. |
| 254 | 0xFE | ERROR_MEMORY_ALLOC | Response buffer allocation failed. |
One additional case to be aware of: when the per-coin rate-limiter trips (check_coin_rate), the connection is silently dropped (STATE_SILENT_DROP) without sending a status. The client sees the TCP connection close.
Long-poll mechanics
The server-side flow:
- Validate preamble + terminator.
- Look up sender’s page; verify AN.
- Apply per-coin rate limit. If exceeded: silent drop.
- Resolve inbox path:
QMAIL_MAILBOX_ROOT/{denom_hex}/{sn}/inbox/. - Scan the directory for
.tell2files. Skip dotfiles (atomic-write tempfiles). - If one or more files found: read them into the response buffer, queue them for
remove(), returnSTATUS_SUCCESSwith the tell-list body. - If empty (or directory missing):
mkdir_p()the inbox if needed, register an inotify monitor (IN_CLOSE_WRITE | IN_MOVED_TO), set state toSTATE_PING_ACTIVE, and return without sending a response. The connection stays open. - When inotify fires (a tell renamed a file in), the network layer calls
qmail_resume_ping, which re-runs the inbox scan and sends the response. - If the socket-level timeout fires before a tell arrives, the network layer sends
STATUS_TIMEOUTwith no body.
The maximum response buffer is MAX_TELL_RESPONSE_SIZE = 1 MiB. If the inbox contains more tells than fit, the iteration stops at the cap; remaining tells stay on disk and are picked up on the next ping.
Maximum tells deleted per scan: MAX_TELL_DELETE = 256 (any beyond this are returned in the response but not deleted; they will be re-returned on the next ping — effectively a soft cap on inbox burst size).
Common mistakes
Pinging the wrong RAIDA
Each mailbox has exactly one beacon RAIDA, agreed at registration time. Pinging any other RAIDA returns ERROR_COIN_NOT_FOUND because that RAIDA does not hold the inbox. The recipient’s client must know its own beacon (read it from /api/qmail/local/identity/whoami in rest_core).
Treating ping as fire-and-forget
A successful ping response removes the tells from the inbox. If your client crashes between receiving the response and persisting the tells locally, the tells are lost. Persist before acknowledging.
Long-poll sockets and NAT
A parked connection holds the TCP socket open indefinitely waiting for inotify. NAT/firewall idle timeouts can drop the connection without notifying either side. The client should re-ping after a reasonable interval (60–120 seconds) rather than relying on the server to wake.
Points of confusion
- ping vs peek. ping blocks until a tell arrives or the socket times out. peek returns immediately with whatever is currently in the inbox newer than a caller-supplied timestamp.
- The 5 reserved bytes in the response header. The 8-byte response header has
tell_count,total_tells[2], then 5 reserved bytes.total_tellsis currently always zero; the 5 reserved bytes are also zero. Available for protocol extension. - Tells are deleted on read, not on ack. The server removes the file the moment it includes the bytes in the response — before the client confirms receipt. There is no two-phase delivery.
- Atomic-write tempfiles are filtered by dotfile prefix. The directory scanner skips any entry beginning with
., which catches both./..and the.tmp.PID.TIME.SEQ.GUID.tell2files written by tell during atomic publish. - Per-coin rate limiting is silent. A flooding client gets no status — the server just closes the connection without writing a response. Look for
RATE_DROPin the server log if the client appears to be losing connections inexplicably.