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 Group6
Command Code72
Server functioncmd_qmail_ping2 in cmd_qmail.c
Body layoutPreamble (48) + optional ignored timestamp (4) + Terminator (2)
ResponseTell-list array (header + N notification records) on mail arrival; status-only with STATUS_TIMEOUT on socket timeout
EncryptionRequired — AES-128 keyed by recipient’s coin AN
Side effect on successAll 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

DecimalHexSymbolMeaning
2500xFASTATUS_SUCCESSReturned with the tell-list response when one or more tells were ready (immediately or after long-poll wake).
2450xF5STATUS_TIMEOUTConnection timed out at the socket level after parking with no tells. Body is empty.
80x08ERROR_COIN_NOT_FOUNDRecipient’s (denom, SN) is not loaded on this RAIDA. The client targeted the wrong beacon.
160x10ERROR_INVALID_PACKET_LENGTHBody too short or missing terminator.
1940xC2ERROR_FILESYSTEMServer failed to opendir() the inbox or arm the inotify watch.
2000xC8ERROR_INVALID_ANPreamble AN doesn’t match the server’s stored AN. Authentication failed.
2540xFEERROR_MEMORY_ALLOCResponse 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:

  1. Validate preamble + terminator.
  2. Look up sender’s page; verify AN.
  3. Apply per-coin rate limit. If exceeded: silent drop.
  4. Resolve inbox path: QMAIL_MAILBOX_ROOT/{denom_hex}/{sn}/inbox/.
  5. Scan the directory for .tell2 files. Skip dotfiles (atomic-write tempfiles).
  6. If one or more files found: read them into the response buffer, queue them for remove(), return STATUS_SUCCESS with the tell-list body.
  7. If empty (or directory missing): mkdir_p() the inbox if needed, register an inotify monitor (IN_CLOSE_WRITE | IN_MOVED_TO), set state to STATE_PING_ACTIVE, and return without sending a response. The connection stays open.
  8. 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.
  9. If the socket-level timeout fires before a tell arrives, the network layer sends STATUS_TIMEOUT with 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

  1. 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.
  2. The 5 reserved bytes in the response header. The 8-byte response header has tell_count, total_tells[2], then 5 reserved bytes. total_tells is currently always zero; the 5 reserved bytes are also zero. Available for protocol extension.
  3. 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.
  4. 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.tell2 files written by tell during atomic publish.
  5. 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_DROP in the server log if the client appears to be losing connections inexplicably.