QMail Object Transfer v1
Stable normative protocol for configurable, resumable uploads and downloads of QMail objects without a fixed file-size or page-size ceiling.
Stable v1 contract
This wire contract is frozen for implementation as of June 11, 2026. Commands 70 through 75 remain valid for compatibility. Commands 76 through 84 use the layouts and status assignments below. Future additions must use new protocol versions, extension fields, or flags without changing v1 field meanings.
1. Goals and non-goals
- Support objects larger than 4 GiB by using unsigned 64-bit object sizes and byte offsets.
- Allow the client to request an upload chunk size while allowing the server to reduce it to a locally configured limit.
- Allow download range size to differ from upload chunk size.
- Support resume, out-of-order ranges, idempotent retries, and integrity verification.
- Support atomic replacement and owner-authorized deletion while keeping a stable object ID.
- Keep storage implementation independent of the wire protocol. RAM, tmpfs, disk, and distributed storage are server policy.
- Allow future DRD records to advertise server limits, while the live server remains authoritative.
- Do not encode a permanent maximum object size in the protocol.
The protocol does not promise that every QMail server accepts every object. A client selects suitable servers from DRD capabilities and must tolerate individual storage servers rejecting or losing a copy.
2. Terminology
| Term | Definition |
|---|---|
| Logical file | The sender's complete attachment, body, or private metadata before QMail striping. |
| Stored object | The exact byte sequence stored on one QMail server. This is normally one encoded stripe of a logical file. |
| Object ID | A 16-byte identifier stable across upload, Tell publication, object-info lookup, and download. |
| Transfer ID | A client-generated, nonzero, cryptographically random 16-byte idempotency key for one stored-object upload attempt. It is bound to the authenticated owner and is not a session or authorization token. |
| Generation | A client-selected, server-validated unsigned 64-bit logical revision of one object ID. The same file revision uses the same target generation on every selected QMail server. |
| Chunk | One upload request carrying a byte range of a stored object. |
| Range | A zero-based byte offset and length. Download ranges are independent of upload chunk boundaries. |
| Owner | The authenticated coin identity that created the object. Ownership controls replacement, deletion, and future ACL administration. |
Size meaning
total_size in commands 76 through 82 is the number of bytes stored on this server, not the reconstructed size of the sender's original attachment. The Tell manifest carries the logical file size separately.
Transfer state is not a session
A resumable upload necessarily has server-side state: accepted parameters, payment and capacity reservation, received ranges, hashes, and expiry. That durable state is keyed by the authenticated owner plus the client-generated Transfer ID and may be resumed over any later TCP connection. The Object ID remains permanent; the Transfer ID isolates one attempt so delayed packets from an aborted attempt cannot modify a retry.
3. Command assignments
Group 6 commands 70 through 75 remain available during migration. The new transfer protocol uses:
| Code | Name | Purpose |
|---|---|---|
| 76 | object_begin | Validate policy, reserve capacity, and create a transfer. |
| 77 | object_put_range | Upload one byte range. |
| 78 | object_transfer_status | Resume an upload by listing received or missing ranges. |
| 79 | object_commit | Verify completeness and publish the stored object. |
| 80 | object_abort | Release an incomplete transfer. |
| 81 | object_info | Return committed object metadata and download recommendations. |
| 82 | object_get_range | Download an arbitrary byte range. |
| 83 | object_capabilities | Return the live server limits that override stale DRD data. |
| 84 | object_delete | Owner-authorized atomic deletion of a committed object. |
All v1 object-transfer commands require TCP and AES-128 QMail authentication. A server receiving one over UDP returns ERROR_TCP_REQUIRED before allocating a request body.
4. Extended framing v1
The existing response header has a 24-bit body length. New transfer commands define symmetric 32-bit framing so range sizes can evolve without another command redesign.
Request header use
| Header bytes | Size | Meaning |
|---|---|---|
| 0 | 1 | Reserved routing byte. Must be 00. |
| 8 | 1 | Reserved for extended framing. Must be 00. |
| 9 | 1 | Extended framing version. Must be 1. |
| 10-13 | 4 | Unsigned big-endian complete request body length. |
| 14-15 | 2 | Reserved; must be zero. |
| 22-23 | 2 | Must be FFFF, indicating that bytes 10-13 are authoritative. |
Response header use
| Header bytes | Size | Meaning |
|---|---|---|
| 8 | 1 | Extended framing version. Must be 1. |
| 9-12 | 4 | Unsigned big-endian complete response body length. |
| 13-15 | 3 | Reserved; must be zero in v1. |
For commands 76 through 84, response bytes 12 through 15 no longer carry legacy execution time; bytes 9 through 12 are the complete 32-bit body length and bytes 13 through 15 are zero. A successful request body length is 16-byte challenge + 32-byte identity preamble + command payload + 2-byte clear terminator. A successful response body length is encrypted response payload + 2-byte clear terminator. Error responses have body length zero. Errors produced after successful request decryption carry the normal challenge hash. ERROR_TCP_REQUIRED is produced before body decryption and therefore carries sixteen zero challenge bytes. The configured upload and download range maxima must be smaller than the framing limit after fixed command overhead. Servers must reject an oversized length before allocation or socket reads.
5. Common command prefix
Every command payload after the normal 48-byte QMail preamble begins with this 16-byte prefix:
| Offset | Size | Field | Rules |
|---|---|---|---|
| 0 | 2 | protocol_version | Unsigned big-endian. Must be 1. |
| 2 | 2 | command_header_length | Bytes from this prefix through the final command-specific header byte. Range data starts at this offset. |
| 4 | 4 | flags | Must be zero in v1. Nonzero values return ERROR_UNSUPPORTED_PROTOCOL. |
| 8 | 8 | request_id | Client-selected value echoed in successful response bodies. It correlates one request and response and may change on retry; it is not the persistent Transfer ID. |
command_header_length must equal the exact v1 size listed for the command. Future protocol versions may define larger headers; a v1 implementation does not accept them.
6. Command formats
76 object_begin
Fixed request header length: 144 bytes, including the common prefix.
common_prefix 16 transfer_id 16 object_id 16 locker_code 16 file_type 1 requested_retention_seconds 8 hash_algorithm 1 (1 = SHA-256) operation 1 (0 = create, 1 = replace) storage_class 2 reserved 7 preferred_chunk 4 total_size 8 expected_generation 8 (0 for create; required for replace) target_generation 8 object_hash 32
The client generates transfer_id with a cryptographically secure random generator. It uses one ID per stored-object attempt and may use the same ID on different QMail servers for the corresponding distributed upload. The server keys transfer records by authenticated owner plus Transfer ID; knowledge of an ID never grants access.
storage_class=0 selects the server default. preferred_chunk=0 selects the server preference. Otherwise the server returns an accepted size no larger than the requested size or its configured maximum. requested_retention_seconds is an unsigned 64-bit duration; zero requests the server default. For a nonzero request, the server either accepts the exact duration or returns ERROR_RETENTION_UNAVAILABLE; it does not silently shorten paid retention. locker_code carries the existing 16-byte storage-payment marker. total_size must be at least 1. The server reserves exactly total_size physical content bytes in the selected class before consuming payment. Metadata overhead is server-owned and is not included in the reservation.
Create fails if a live object already exists, requires expected_generation=0, and accepts any nonzero target_generation. Replace requires owner authentication, an exact expected_generation, and target_generation > expected_generation. The server commits exactly the requested target rather than incrementing a local counter. This lets a client publish one logical revision across servers whose prior local generations differ after partial failures. A stale expected generation or non-monotonic target returns ERROR_GENERATION_CONFLICT.
object_begin is idempotent by owner and Transfer ID. The server durably records the begin attempt before initiating payment. An exact retry, ignoring only the common request_id, never reserves capacity or initiates payment twice. While payment is pending it returns the existing ERROR_PAYMENT_PROCESSING; after acceptance it returns the original negotiated values while echoing the retry's request ID. Reusing the same Transfer ID with any different command-specific field returns ERROR_TRANSFER_CONFLICT. An aborted or expired attempt requires a new Transfer ID.
Successful response header length: 80 bytes.
common_prefix 16 transfer_id 16 accepted_chunk 4 max_parallel 2 storage_class 2 hash_algorithm 1 operation 1 expires_at 8 (Unix seconds) base_generation 8 target_generation 8 accepted_retention_seconds 8 reserved 6
accepted_retention_seconds is the exact duration attached to the committed object. When the request was zero, this reports the server-selected default; zero in the response means no scheduled expiry. For a nonzero accepted duration, object expiry is committed_at + accepted_retention_seconds using checked arithmetic.
Phase I payment scope
Payment is once per server for the tuple (authenticated owner, object_id, locker_code). It is shared by every file_type under that Object ID and is independent of Transfer ID. Each stored object still receives its own capacity reservation and transfer state. Replacements using the same tuple reuse the existing payment marker; changing the locker code creates a different payment key. Deletion does not refund payment.
The payment record has durable pending, paid, and failed states. The first begin writes pending before dispatching locker consumption. Other begins for the same payment key return ERROR_PAYMENT_PROCESSING while it is pending, reuse it when paid, and return ERROR_PAYMENT_REQUIRED when failed. Under legacy_locker_marker, successful asynchronous dispatch is the accepted payment event, matching commands 70 and 75. An ambiguous dispatch state after a crash must be reconciled and must not be automatically charged again.
77 object_put_range
Fixed request header length: 80 bytes. Data follows immediately.
common_prefix 16 transfer_id 16 offset 8 data_length 4 hash_algorithm 1 reserved 3 range_hash 32 data N
Ranges may arrive concurrently and out of order. A byte-identical retry succeeds idempotently. An overlapping range containing different bytes returns ERROR_RANGE_CONFLICT.
Successful response header length: 64 bytes.
common_prefix 16 transfer_id 16 offset 8 data_length 4 range_flags 4 (bit 0 = duplicate retry) received_unique 8 reserved 8
78 object_transfer_status
Fixed request header length: 48 bytes.
common_prefix 16 transfer_id 16 cursor 8 (0 = first response) range_mode 1 (0 = missing, 1 = received) reserved 1 max_ranges 2 (1..256) reserved 4
Successful response fixed header length: 72 bytes, followed by range_count 16-byte entries.
common_prefix 16 transfer_id 16 transfer_state 1 (0 receiving, 1 ready, 2 committed, 3 aborted, 4 expired) range_mode 1 response_flags 2 target_generation 8 total_size 8 received_unique 8 next_cursor 8 (0 = no more entries) range_count 2 reserved 2 range entries N * 16 range entry: offset 8 length 8
response_flags bit 0 means more entries remain and must agree with next_cursor != 0; all other bits are zero in v1. The cursor is opaque despite its integer encoding. Clients must return it unchanged. The server returns at most 256 entries and never constructs an unbounded response.
79 object_commit
Fixed request header length: 80 bytes.
common_prefix 16 transfer_id 16 total_size 8 hash_algorithm 1 reserved 7 object_hash 32
Successful response header length: 96 bytes.
common_prefix 16 object_id 16 file_type 1 object_state 1 (1 = committed) storage_class 2 generation 8 total_size 8 hash_algorithm 1 reserved 3 object_hash 32 committed_at 8 (Unix seconds)
The server verifies complete coverage, total size, and the object hash before publishing.
A commit publishes the target generation accepted by command 76 and atomically changes the current-generation pointer only after all verification succeeds. Readers must see either the complete old generation or the complete new generation, never a mixture. The response returns the committed target generation.
Commit is idempotent by owner and Transfer ID. If the commit response is lost, an exact retry of total_size, hash algorithm, and object hash returns the original successful commit metadata. A retry with different values returns ERROR_TRANSFER_CONFLICT.
80 object_abort
Fixed request header length: 32 bytes.
common_prefix 16 transfer_id 16
Successful response header length: 48 bytes.
common_prefix 16 transfer_id 16 transfer_state 1 (3 = aborted) reserved 15
The server retains an aborted-transfer tombstone through the configured terminal-record interval and at least until the original transfer expiry, making repeated aborts idempotent. An unknown transfer returns ERROR_TRANSFER_NOT_FOUND; a committed transfer returns ERROR_OBJECT_STATE.
81 object_info
Fixed request header length: 48 bytes.
common_prefix 16 object_id 16 file_type 1 reserved 7 generation 8 (0 = latest committed)
Successful response header length: 112 bytes.
common_prefix 16 object_id 16 file_type 1 object_state 1 (1 = committed) storage_class 2 hash_algorithm 1 acl_version 1 (1 in Phase I) object_flags 2 (bit 0 = volatile storage) generation 8 total_size 8 recommended_length 4 reserved 4 committed_at 8 expires_at 8 (0 = no scheduled expiry) object_hash 32
The response is owner-neutral. A current tombstone returns ERROR_FILE_NOT_EXIST. Replaced generations remain readable only during the configured generation grace period.
82 object_get_range
Fixed request header length: 64 bytes.
common_prefix 16 object_id 16 file_type 1 request_flags 1 reserved 6 generation 8 (0 = latest committed generation) offset 8 requested_length 4 reserved 4
The server may return fewer bytes than requested. Successful response header length is 104 bytes, followed by data:
common_prefix 16 object_id 16 file_type 1 response_flags 1 hash_algorithm 1 reserved 1 generation 8 offset 8 data_length 4 recommended_length 4 total_size 8 object_hash 32 reserved 4 data N
request_flags must be zero in v1. Response flag bit 0 means the returned range reaches the end of the object; bit 1 means the selected storage class is volatile. Upload chunk boundaries do not constrain downloads. The client may request ranges using the current server's recommendation. A client must pin the generation returned by object-info for all ranges in one reconstruction, preventing an edit from mixing generations. Once a delete tombstone becomes current, the server rejects new range requests for every prior generation.
83 object_capabilities
Request header length: 16 bytes, consisting only of the common prefix. Successful response fixed header length: 80 bytes, followed by class_count 64-byte storage-class entries.
common_prefix 16 capability_schema 2 (1) protocol_min 2 (1) protocol_max 2 (1) transport_flags 2 (bit 0 = TCP) server_flags 4 (see below) preferred_chunk 4 max_chunk 4 max_download_range 4 max_active_transfers 4 max_parallel_transfer 2 class_count 2 (0..64) max_object_global 8 generated_at 8 expires_at 8 payment_mode 2 (1 = legacy_locker_marker) reserved 6 class entries N * 64 server_flags: bit 0 = object transfer enabled bit 1 = replacement supported bit 2 = deletion supported bit 3 = legacy locker-marker payment bit 4 = Phase I allow-all reads storage-class entry: class_id 2 media_type 1 (1 RAM, 2 NVMe, 3 SSD, 4 HDD, 255 other) class_flags 1 (bit 0 = volatile) max_object 8 capacity_bytes 8 available_bytes 8 max_retention_seconds 8 (0 = server policy) price_schedule_id 4 reserved 24
This live response is authoritative when it differs from cached DRD data. max_object_global and each class max_object must be at least 1; the lower applicable value wins. capacity_bytes=0 or available_bytes=0 means the value is not disclosed, not that the class has zero capacity. available_bytes is advisory and may change immediately; successful reservation by command 76 is authoritative. expires_at=0 means the live response has no advertised cache lifetime; otherwise clients must refresh it after that Unix time.
84 object_delete
Fixed request header length: 56 bytes.
common_prefix 16 object_id 16 file_type 1 reserved 7 expected_generation 8 target_generation 8
Successful response header length: 64 bytes.
common_prefix 16 object_id 16 file_type 1 object_state 1 (2 = tombstone) reserved 6 tombstone_generation 8 deleted_at 8 reserved 8
Only the owner may delete. expected_generation must match the current live generation and target_generation must be greater. A successful delete atomically publishes a tombstone at the requested target generation, makes the object unavailable to new object-info and get-range requests, and returns that generation. Existing range responses already in progress may finish against their pinned generation.
Deletion is idempotent when the same owner repeats it with the same expected and target generations. It does not imply a payment refund. Physical bytes may be reclaimed after active readers release the old generation and the configured deletion grace period expires.
7. Tell integration
QMail announces object-stored mail with the standard manifest v1 tell (manifest_version=1, 16-byte entries) — see the cmd 71 Tell spec. There is no Object Transfer–specific manifest entry on the wire. Instead, the object key is fully derivable from the tell itself:
object_id = the email GUID (the email_id field — first 16 bytes of the
tell routing header, absolute bytes 48-63; mirrored at
file-header offset 0-15)
file_type = manifest entry file_type (0x00 meta, 0x01 body, 0x0A+ attachments)
generation = 1 (senders always commit message objects at generation 1)
The sender uploads every message file (private meta, body, attachments) under that key via commands 76–79 before sending the tell. The receiver fetches each file via object-info (cmd 81) and get-range (cmd 82) requests against the servers listed in the tell’s server-location entries, reconstructs the logical file from its stripes, and verifies it against the manifest entry’s sender-authored CRC32 (manifest_flags.crc32_present is always set by object-storing senders) plus the per-stripe SHA-256 hashes returned by cmd 81. A receiver that cannot find the object (older striped-storage senders) falls back to the legacy paged download path (cmd 74; the legacy paged-storage family is cmds 70/74/75). Stripe geometry and the parity algorithm come from the tell’s server-location entries and the file-header parity_algo byte.
Committing message objects at a fixed generation 1 keeps delivered mail immutable: a replacement upload would require a higher target generation, which the receiver never requests for mail objects.
8. Required server validation
- Reject commands 76 through 84 over UDP before allocating the declared body.
- Parse every wire integer as unsigned big-endian.
- Reject all additions and multiplications that overflow their destination type.
- Require
offset <= total_sizeandlength <= total_size - offset. - Check configured object and range limits before allocation, socket reads, reservation, or payment consumption.
- Reject a nonzero requested retention greater than the selected storage class maximum. Do not silently clamp it.
- Require every successful negotiated chunk size, requested download length, and uploaded
data_lengthto be at least 1. - Require upload
data_lengthto equal the trailing data bytes and download responsedata_lengthto equal the returned data bytes. - Never allocate
total_sizemerely because the client declared it. - Reject an all-zero Transfer ID and bind every transfer lookup to the authenticated identity that created it.
- Persist the owner, Transfer ID, immutable begin fields, reservation state, and payment-key reference before initiating an irreversible payment operation.
- Key Phase I payment by authenticated owner, Object ID, and locker code. File type and Transfer ID are not part of the payment key.
- Persist payment state before asynchronous dispatch. Never automatically redispatch an ambiguous payment after a restart.
- Make exact begin retries replay-safe and at-most-once for reservation and payment. Reject reuse with different fields as
ERROR_TRANSFER_CONFLICT. - Store the authenticated creator as owner metadata. Replacement, deletion, and ACL administration are always owner-only.
- Require each upload offset to be a multiple of the accepted chunk size; require full accepted-chunk length except for the range ending exactly at
total_size. - Store received-range metadata in a sparse structure rather than one bit per byte.
- Verify the range hash before accepting a range and the complete object hash before commit.
- Keep incomplete transfers invisible to download commands.
- Publish replacements and deletions through an atomic generation pointer; never mutate a committed generation in place.
- Require a monotonic client-selected target generation and exact expected-generation compare-and-swap for replacement and deletion.
- Expire abandoned transfers and release capacity and payment reservations.
- Apply independent per-identity and server-wide limits for active transfers, reserved bytes, memory, bandwidth, and request rate.
9. Object-transfer status codes
Values 218 through 235 are assigned to Object Transfer v1. Value 217 is intentionally not used because it is already published as RAIDA_CONNECTION.
Reused protocol statuses
| Code | Symbol | Meaning |
|---|---|---|
| 167 | ERROR_PAYMENT_PROCESSING | Payment is still pending; retry the idempotent begin request. |
| 169 | ERROR_PAYMENT_REQUIRED | The required storage payment was not supplied or accepted. |
| 202 | ERROR_FILE_NOT_EXIST | The requested committed object does not exist. |
| 250 | SUCCESS | The command completed successfully. |
Object Transfer v1 assignments
| Code | Symbol | Meaning |
|---|---|---|
| 218 | ERROR_TCP_REQUIRED | Retry using TCP. |
| 219 | ERROR_UNSUPPORTED_PROTOCOL | Unsupported protocol, framing version, flag, or hash algorithm. |
| 220 | ERROR_OBJECT_TOO_LARGE | Stored object exceeds the server's current configured limit. |
| 221 | ERROR_RANGE_TOO_LARGE | Upload or download range exceeds the current limit. |
| 222 | ERROR_TRANSFER_NOT_FOUND | Transfer ID is unknown. |
| 223 | ERROR_TRANSFER_EXPIRED | Transfer existed but expired. |
| 224 | ERROR_RANGE_CONFLICT | Overlapping bytes differ from data already accepted. |
| 225 | ERROR_TRANSFER_INCOMPLETE | Commit attempted with missing ranges. |
| 226 | ERROR_HASH_MISMATCH | Range or complete-object integrity check failed. |
| 227 | ERROR_QUOTA_EXCEEDED | Identity or server quota would be exceeded. |
| 228 | ERROR_OBJECT_NOT_COMMITTED | Object exists only as an incomplete transfer. |
| 229 | ERROR_INVALID_RANGE | Offset, length, alignment, or arithmetic is invalid. |
| 230 | ERROR_STORAGE_FULL | Server cannot reserve the requested bytes. |
| 231 | ERROR_OBJECT_STATE | Operation is invalid for the object's current state. |
| 232 | ERROR_NOT_OBJECT_OWNER | Authenticated caller may read under the ACL but may not replace, delete, or administer the object. |
| 233 | ERROR_GENERATION_CONFLICT | Expected generation is stale or does not match the current object generation. |
| 234 | ERROR_TRANSFER_CONFLICT | The owner reused a Transfer ID with different immutable begin fields. |
| 235 | ERROR_RETENTION_UNAVAILABLE | The selected class cannot provide the exact requested retention duration. |
10. Server configuration
Limits are local policy and must not be compiled into the wire protocol. Initial configuration keys:
qmail_object_transfer_enabled = false qmail_max_object_bytes = 26843545600 qmail_preferred_chunk_bytes = 1048576 qmail_max_chunk_bytes = 8388608 qmail_max_download_range_bytes = 8388608 qmail_cache_max_mb = 16384 qmail_max_active_transfers = 256 qmail_max_active_transfers_per_identity = 8 qmail_max_reserved_bytes_per_identity = 107374182400 qmail_transfer_ttl_seconds = 86400 qmail_transfer_tombstone_ttl_seconds = 604800 qmail_default_storage_class = 1 qmail_delete_grace_seconds = 300 qmail_payment_mode = "legacy_locker_marker" [[qmail_storage_class]] id = 1 name = "ram" backend = "ram" volatile = true capacity_bytes = 17179869184 max_object_bytes = 26843545600 price_schedule = "ram-v1" [[qmail_storage_class]] id = 2 name = "nvme" backend = "filesystem" path = "/opt/raidax/QMail_Data/nvme" volatile = false price_schedule = "nvme-v1"
The values above are deployment examples, not protocol defaults. Terminal transfer records should be retained for the configured tombstone interval so delayed begin, commit, abort, and range retries remain idempotent and cannot start a second payment attempt. Configuration loading must use checked 64-bit parsing, reject impossible combinations, and expose effective values through command 83 and monitoring.
A server implements only its local storage promise. It does not calculate or coordinate the client's horizontal, vertical, or diagonal parity. Storage class is a local service tier, not a claim about global QMail durability.
Accepted storage must have meaningful semantics
Cross-server parity tolerates server failures, but it does not make arbitrary silent eviction acceptable. After accepting and committing an object, a server should retain it until expiry, owner deletion, or an actual failure of the selected storage class. When capacity is unavailable, reject object_begin with ERROR_STORAGE_FULL instead of evicting paid, unexpired objects. A RAM class may explicitly be volatile across process or host failure, and that fact must be advertised.
11. Future DRD capability record
The server should generate one canonical capability structure that can be returned live by command 83 and later signed and published through the Distributed Resource Directory:
- RAIDA/server identity and capability schema version
- supported QMail object-transfer protocol versions
- supported transports
- maximum stored-object size
- preferred and maximum upload chunk sizes
- maximum download range size
- storage classes, volatility flags, media/backend properties, and retention options
- fee schedule identifier
- generated-at and expires-at timestamps
- signature algorithm and server signature
DRD data is advisory discovery information. The server may lower limits at any time and returns the specific policy error. The client must not continue allocating or retrying blindly when a live limit differs from DRD.
12. Phase I policy
- Edit and delete: object IDs remain stable. Replacement creates a complete new generation and atomically promotes it. Owner-authorized delete creates a tombstone generation.
- Storage classes: RAM, NVMe, SSD, HDD, and future classes are local server offerings. The client chooses servers, classes, and global parity. The server advertises and enforces only its local contract.
- Phase I ACL: reads are allow-all. Replace, delete, and future ACL administration are never allow-all and require the authenticated owner.
- Hash scope: per-server object hashes cover exact stored bytes; the Tell manifest separately hashes the reconstructed logical file.
- Generation coordination: the client assigns one target generation to a logical file revision. Each server validates its own current value with
expected_generationand commits the shared target value. - Transfer identity: the client generates the Transfer ID. The server uses it only as an owner-bound idempotency and attempt-isolation key, never as a session credential.
- Retention: command 76 uses unsigned 64-bit seconds. Zero selects the server default; nonzero requests are accepted exactly or rejected.
- Payment scope: Phase I charges once per server for owner + Object ID + locker code and shares that payment across file types and replacement generations.
Phase I uses legacy_locker_marker payment mode: begin starts the existing asynchronous locker-consumption path and records the existing reuse marker. The owner-bound Transfer ID makes each begin attempt idempotent, while the separate owner + Object ID + locker-code payment key prevents charging again for another file type or replacement generation. Duplicate ranges do not charge again. Replacement and deletion do not refund or automatically settle a byte-accurate difference. This limitation is accepted for v1. Command 83 and future DRD records advertise the payment-mode and fee-schedule identifiers so a later payment mode can be added without changing transfer packet layouts.
Command 76 reserves exactly total_size stored-content bytes in the selected local class for the life of the transfer. Commands 70 through 75 remain supported compatibility commands; v1 does not assign a retirement date or require clients to stop using them.
Phase I ACL rules
READand owner-neutralOBJECT_INFO: allow-all for a caller who knows the object identifier and can make a valid encrypted QMail request.REPLACE,DELETE, and futureSET_ACL: authenticated owner only.- Allow-all does not mean anonymous plaintext access; transport authentication and encryption still apply.
- The server stores owner identity and ACL version now so Phase III can add account groups, resource groups, and explicit permissions without changing object ownership.
13. Machine-readable contract and test vectors
The vector file contains complete request and successful response packets for commands 76 through 84, plus error-response vectors for TCP-required (218), Transfer-ID reuse conflict (234), and retention-unavailable (235). It records plaintext, ciphertext, framing lengths, AES key, nonce, challenge, and expected packet bytes. The deterministic generator and artifact validator are stored under protocol/spec/tools/.
14. Server implementation checklist
- Implement the frozen schema, status assignments, and binary test vectors without changing field meanings.
- Add checked big-endian uint64 helpers and overflow-safe range validation.
- Add configurable limits and command 83 capability output.
- Add extended request and response framing for commands 76 through 84.
- Add early TCP-only rejection and independent command rate limits.
- Replace the QMail linked-list cache with hash lookup plus constant-time LRU tracking.
- Add a storage backend interface and sparse transfer/range metadata.
- Implement capacity reservation, transfer TTL, abort, and cleanup.
- Implement owner-bound client Transfer IDs and durable at-most-once begin/payment records.
- Implement shared owner/Object ID/locker-code payment records with restart-safe pending, paid, and failed states.
- Implement begin, put-range, status, and commit with idempotency and hashes.
- Implement generation-aware object-info and get-range without depending on upload chunk boundaries.
- Implement owner-only atomic replacement, tombstone deletion, and generation garbage collection.
- Keep cmd 71 Tell validation at manifest v1 (16-byte entries); object-stored mail uses the standard v1 manifest with the object key derived from the email GUID (section 7).
- Add monitoring for active transfers, reserved bytes, cache use, evictions, range conflicts, hash failures, and throughput.
- Run interoperability, fuzz, restart, expiry, quota, sparse-file, and multi-gigabyte tests.
- Enable behind
qmail_object_transfer_enabled, deploy to selected servers, then advertise through DRD.
Client implementation handoff
The protocol is stable for client implementation. Packet serializers must follow the machine-readable schema and frozen test vectors. The client must use 64-bit offsets, negotiate upload chunk size, query live capabilities after DRD discovery, support resume and commit, and choose download range size independently.