QMail Object Begin — Group 6, Code 76
Opens a resumable upload for one stored object: validates policy, reserves capacity, initiates payment, and creates a Transfer ID-keyed transfer record. The client then uploads byte ranges with object_put_range and finishes with object_commit.
Quick reference
| Command Group | 6 |
| Command Code | 76 |
| Server function | cmd_qmail_object_begin in qmail_object_transfer.c |
| Wire structures | qmail_object_begin_req_t, qmail_object_begin_resp_t |
| Body layout | Preamble (48) + Common prefix (16) + Begin payload (128) = 192 bytes request header |
| Transport | TCP only — UDP returns ERROR_TCP_REQUIRED before the body is read |
| Encryption | Required — AES-128 |
| Idempotent | Yes — keyed by (authenticated owner, transfer_id) |
Purpose
Before uploading any bytes, the client declares what it wants to store: total size, hash algorithm, storage class, retention, and whether this is a new object (operation=0, create) or a replacement of an existing one (operation=1, replace). The server reserves exactly total_size physical content bytes in the selected class, attempts payment, and returns an accepted upload chunk size the client should use for subsequent object_put_range calls.
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 unsigned 64-bit; 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, the same field used by cmd 70 upload.
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, letting 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.
Request body
Preamble (48 bytes, offsets 0–47)
Standard QMail preamble. See QMail Overview — Universal preamble.
Common prefix (16 bytes, offsets 48–63)
Every Object Transfer v1 command payload begins with this prefix. See the Object Transfer overview for the shared framing rules.
| Offset | Size | Field | Description |
|---|---|---|---|
| 48–49 | 2 | protocol_version | Unsigned big-endian. Must be 1. |
| 50–51 | 2 | command_header_length | Bytes from this prefix through the final command-specific header byte. Must equal 128 for this command (144 including the prefix). |
| 52–55 | 4 | flags | Must be zero in v1. Nonzero returns ERROR_UNSUPPORTED_PROTOCOL. |
| 56–63 | 8 | request_id | Client-selected value echoed in successful response bodies. Correlates one request/response pair; not the persistent Transfer ID. |
Begin payload (128 bytes, offsets 64–191)
| Offset | Size | Field | Description |
|---|---|---|---|
| 64–79 | 16 | transfer_id | Client-generated, cryptographically random, nonzero. One ID per stored-object attempt; the same ID may be used on different QMail servers for the corresponding distributed upload. All-zero is rejected. |
| 80–95 | 16 | object_id | 16-byte identifier stable across begin, put_range, commit, info, and get_range. For QMail mail objects this is the email GUID. |
| 96–111 | 16 | locker_code | ASCII locker key, null-padded to 16 bytes. Storage-payment marker, same semantics as cmd 70/75. |
| 112 | 1 | file_type | Same encoding as cmd 70/74: 0x00 meta, 0x01 body, 0x0A+ attachments. |
| 113–120 | 8 | requested_retention_seconds | Unsigned big-endian. Zero requests the server default. |
| 121 | 1 | hash_algorithm | 1 = SHA-256. No other value is defined in v1. |
| 122 | 1 | operation | 0 = create, 1 = replace. |
| 123–124 | 2 | storage_class | Unsigned big-endian. 0 selects the server default. |
| 125–131 | 7 | reserved | Must be zero. |
| 132–135 | 4 | preferred_chunk | Unsigned big-endian. 0 selects the server preference. |
| 136–143 | 8 | total_size | Unsigned big-endian. Must be at least 1. Bytes stored on this server, not the reconstructed logical file size. |
| 144–151 | 8 | expected_generation | Must be 0 for create. Required exact match of the current live generation for replace. |
| 152–159 | 8 | target_generation | Any nonzero value for create. Must be strictly greater than expected_generation for replace. |
| 160–191 | 32 | object_hash | SHA-256 of the complete stored object, verified at commit time. |
| last 2 | 2 | Terminator | Fixed clear-text terminator. |
Total request body length: 16 (challenge) + 32 (identity preamble) + 16 (common prefix) + 128 (begin payload) + 2 (terminator) = 194 bytes.
Response body
Successful response header length: 80 bytes (16-byte common prefix + 64-byte begin response).
| Field | Size | Description |
|---|---|---|
| common_prefix | 16 | Echoes request_id; see the common prefix table above. |
| transfer_id | 16 | Echo of the request's transfer_id. |
| accepted_chunk | 4 | Upload chunk size the client should use for object_put_range. No larger than the requested preferred_chunk or the server's configured maximum. |
| max_parallel | 2 | Maximum number of concurrent put_range calls the server will accept for this transfer. |
| storage_class | 2 | The class actually selected (never 0 in the response). |
| hash_algorithm | 1 | Echo of the request's hash_algorithm. |
| operation | 1 | Echo of the request's operation. |
| expires_at | 8 | Unix seconds. When this transfer attempt itself expires if not committed. |
| base_generation | 8 | The current live generation at the time of begin (0 for create). |
| target_generation | 8 | Echo of the request's target_generation. |
| accepted_retention_seconds | 8 | The exact duration attached to the committed object. When the request was zero, reports the server-selected default; zero here means no scheduled expiry. |
| reserved | 6 | Zero. |
Object expiry, when accepted_retention_seconds is nonzero, is committed_at + accepted_retention_seconds using checked arithmetic.
Status codes
| Code | Symbol | Meaning |
|---|---|---|
| 250 | SUCCESS | Transfer created; see response body. |
| 167 | ERROR_PAYMENT_PROCESSING | Payment for this owner/object_id/locker_code tuple is still pending from a prior begin. Retry the idempotent begin request. |
| 169 | ERROR_PAYMENT_REQUIRED | The required storage payment was not supplied or accepted; a prior payment attempt for this key failed. |
| 218 | ERROR_TCP_REQUIRED | Retry using TCP. Produced before body decryption, so the response carries sixteen zero challenge bytes. |
| 219 | ERROR_UNSUPPORTED_PROTOCOL | Unsupported protocol_version, framing version, nonzero flags, or hash_algorithm. |
| 220 | ERROR_OBJECT_TOO_LARGE | total_size exceeds the server's current configured limit. |
| 227 | ERROR_QUOTA_EXCEEDED | Per-identity or server-wide active-transfer quota would be exceeded. |
| 230 | ERROR_STORAGE_FULL | Server cannot reserve the requested bytes in the selected class. |
| 231 | ERROR_OBJECT_STATE | Create requested but a live object already exists, or another state conflict. |
| 233 | ERROR_GENERATION_CONFLICT | Stale expected_generation or non-monotonic target_generation. |
| 234 | ERROR_TRANSFER_CONFLICT | The owner reused this transfer_id with different immutable begin fields. |
| 235 | ERROR_RETENTION_UNAVAILABLE | The selected storage class cannot provide the exact requested nonzero retention duration. |
| 223 | ERROR_TRANSFER_EXPIRED | A prior attempt with this transfer_id existed but has expired; a new transfer_id is required. |
Idempotency
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, which is allowed to change — never reserves capacity or initiates payment twice. While payment is pending it returns 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.
Common mistakes
Reusing a Transfer ID across different objects
A Transfer ID is scoped to one upload attempt of one stored object. Reusing it for a second, different upload (different object_id, size, or hash) returns ERROR_TRANSFER_CONFLICT rather than starting a new transfer. Generate a fresh random Transfer ID per stored-object attempt.
Expecting a silent retention clamp
Unlike some storage systems, a nonzero requested_retention_seconds is either honored exactly or rejected with ERROR_RETENTION_UNAVAILABLE. The server never silently shortens paid retention. Check the response's accepted_retention_seconds, or handle the rejection, rather than assuming the request was granted.
Treating total_size as the logical file size
total_size is the size of this server's stored stripe, not the sender's original attachment. The reconstructed logical file size lives in the Tell manifest entry, not in any Object Transfer field.