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 Group6
Command Code76
Server functioncmd_qmail_object_begin in qmail_object_transfer.c
Wire structuresqmail_object_begin_req_t, qmail_object_begin_resp_t
Body layoutPreamble (48) + Common prefix (16) + Begin payload (128) = 192 bytes request header
TransportTCP only — UDP returns ERROR_TCP_REQUIRED before the body is read
EncryptionRequired — AES-128
IdempotentYes — 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.

OffsetSizeFieldDescription
48–492protocol_versionUnsigned big-endian. Must be 1.
50–512command_header_lengthBytes from this prefix through the final command-specific header byte. Must equal 128 for this command (144 including the prefix).
52–554flagsMust be zero in v1. Nonzero returns ERROR_UNSUPPORTED_PROTOCOL.
56–638request_idClient-selected value echoed in successful response bodies. Correlates one request/response pair; not the persistent Transfer ID.

Begin payload (128 bytes, offsets 64–191)

transfer_id (16 bytes) 64 79 object_id (16 bytes) 80 95 locker_code (16 bytes) 96 111 FT 112 requested_retention_seconds (8, BE) 113 120 HA 121 OP 122 storage_class (2) 123 124 reserved (7) 125 131 preferred_chunk (4, BE) 132 135 total_size (8, BE) 136 143 expected_generation (8, BE) 144 151 target_generation (8, BE) 152 159 object_hash (32 bytes, SHA-256) 160 191
OffsetSizeFieldDescription
64–7916transfer_idClient-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–9516object_id16-byte identifier stable across begin, put_range, commit, info, and get_range. For QMail mail objects this is the email GUID.
96–11116locker_codeASCII locker key, null-padded to 16 bytes. Storage-payment marker, same semantics as cmd 70/75.
1121file_typeSame encoding as cmd 70/74: 0x00 meta, 0x01 body, 0x0A+ attachments.
113–1208requested_retention_secondsUnsigned big-endian. Zero requests the server default.
1211hash_algorithm1 = SHA-256. No other value is defined in v1.
1221operation0 = create, 1 = replace.
123–1242storage_classUnsigned big-endian. 0 selects the server default.
125–1317reservedMust be zero.
132–1354preferred_chunkUnsigned big-endian. 0 selects the server preference.
136–1438total_sizeUnsigned big-endian. Must be at least 1. Bytes stored on this server, not the reconstructed logical file size.
144–1518expected_generationMust be 0 for create. Required exact match of the current live generation for replace.
152–1598target_generationAny nonzero value for create. Must be strictly greater than expected_generation for replace.
160–19132object_hashSHA-256 of the complete stored object, verified at commit time.
last 22TerminatorFixed 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).

FieldSizeDescription
common_prefix16Echoes request_id; see the common prefix table above.
transfer_id16Echo of the request's transfer_id.
accepted_chunk4Upload chunk size the client should use for object_put_range. No larger than the requested preferred_chunk or the server's configured maximum.
max_parallel2Maximum number of concurrent put_range calls the server will accept for this transfer.
storage_class2The class actually selected (never 0 in the response).
hash_algorithm1Echo of the request's hash_algorithm.
operation1Echo of the request's operation.
expires_at8Unix seconds. When this transfer attempt itself expires if not committed.
base_generation8The current live generation at the time of begin (0 for create).
target_generation8Echo of the request's target_generation.
accepted_retention_seconds8The exact duration attached to the committed object. When the request was zero, reports the server-selected default; zero here means no scheduled expiry.
reserved6Zero.

Object expiry, when accepted_retention_seconds is nonzero, is committed_at + accepted_retention_seconds using checked arithmetic.

Status codes

CodeSymbolMeaning
250SUCCESSTransfer created; see response body.
167ERROR_PAYMENT_PROCESSINGPayment for this owner/object_id/locker_code tuple is still pending from a prior begin. Retry the idempotent begin request.
169ERROR_PAYMENT_REQUIREDThe required storage payment was not supplied or accepted; a prior payment attempt for this key failed.
218ERROR_TCP_REQUIREDRetry using TCP. Produced before body decryption, so the response carries sixteen zero challenge bytes.
219ERROR_UNSUPPORTED_PROTOCOLUnsupported protocol_version, framing version, nonzero flags, or hash_algorithm.
220ERROR_OBJECT_TOO_LARGEtotal_size exceeds the server's current configured limit.
227ERROR_QUOTA_EXCEEDEDPer-identity or server-wide active-transfer quota would be exceeded.
230ERROR_STORAGE_FULLServer cannot reserve the requested bytes in the selected class.
231ERROR_OBJECT_STATECreate requested but a live object already exists, or another state conflict.
233ERROR_GENERATION_CONFLICTStale expected_generation or non-monotonic target_generation.
234ERROR_TRANSFER_CONFLICTThe owner reused this transfer_id with different immutable begin fields.
235ERROR_RETENTION_UNAVAILABLEThe selected storage class cannot provide the exact requested nonzero retention duration.
223ERROR_TRANSFER_EXPIREDA 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.