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.
credit_state1How this upload was funded — see Zero-locker funding & welfare. 0 OK, 1 LOW, 2 EMPTY, 3 WELFARE. Always 0 while enforcement is off (the current fleet state).
remaining_kb3Big-endian, saturating at 0xFFFFFF. Remaining allowance in the active funding mode after this upload's charge, in whole KB (subscription credit, or welfare bytes left in the window). Zero while enforcement is off.
reserved2Zero.

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

Byte-offset note for parsers: credit_state is at response offset 74, remaining_kb at 75–77, trailing reserved at 78–79. Parsers that require all six trailing bytes (74–79) to be zero must relax that check to the final two bytes (78–79) before enforcement is turned on — see Zero-locker funding & welfare.

Zero-locker funding & welfare

An upload is normally paid for by attaching a funded locker_code (the 16-byte storage-payment marker). To let users store data without first buying and attaching a locker, object_begin also supports an all-zero locker_code (“zero-locker” upload). When the locker is all zeros the server decides how the stripe is funded from server-side balances, and reports the outcome in the response's credit_state and remaining_kb fields. A nonzero locker skips all of this and uses the legacy locker-payment path.

Deployment state — read this first

The funding logic below is compiled behind the server switch QMAIL_SUBSCRIPTION_ENFORCE, which is currently 0 (off) across the fleet. While off: a zero-locker upload is accepted for free under Phase 1, nothing is metered, and the server writes credit_state = 0 and remaining_kb = 0 (all trailing bytes zero). No 170/171 status is ever returned. This section documents the behaviour that becomes active once the switch is turned on, so core and GUI can implement it ahead of the flip.

Per-stripe, per-mailbox accounting

Each RAIDA sees only its own stripe of a striped message — never the total message size or the stripe count. All sizes and allowances below are therefore per server, per mailbox (identified by the authenticated denomination + serial number). A message split across N servers is funded independently on each.

Funding precedence

For a zero-locker upload the server tries these in order and stops at the first that succeeds:

#ModeRuleResult
1Free floorStripe total_size < 100 KB (102,400 bytes).Free. credit_state = OK (0). Untracked — no allowance is consumed.
2SubscriptionMailbox has prepaid credit that fully covers the stripe (loaded by a vendor via cmd 85 Subscribe).Credit is consumed. credit_state = OK (0), LOW (1), or EMPTY (2) depending on remaining balance; remaining_kb = credit left, in KB. Refunded if the transfer aborts or expires.
3WelfareNo/insufficient subscription, but the stripe fits the welfare free tier: 300 KB (307,200 bytes) per rolling 3-day window per mailbox.Granted. credit_state = WELFARE (3); remaining_kb = welfare bytes left in the window. Not refunded on abort/expiry. Over the cap → ERROR_WELFARE_EXHAUSTED (171).
4DenyStripe too large for welfare and no subscription.ERROR_NO_STORAGE_CREDIT (170). The client must attach a funded locker or top up a subscription.

credit_state values

ValueSymbolMeaning
0OKFunded with balance above the low-credit threshold, or by the free floor. Also the value when enforcement is off.
1LOWFunded by subscription; remaining credit is at or below the low threshold. Send succeeded — surface a top-up hint.
2EMPTYFunded by subscription; this charge took the balance to exactly zero. Send succeeded; the next zero-locker upload will fall through to welfare or be denied.
3WELFAREAllowed on the welfare free tier. remaining_kb is welfare bytes left in the current window.

remaining_kb is a 3-byte big-endian unsigned integer (KB), saturating at 0xFFFFFF (≈16 GB reported ceiling). The commit response (cmd 79) repurposes its own 3 formerly-reserved trailing bytes to report the mailbox's remaining subscription credit after commit, in the same KB encoding.

Client parsers must not hard-reject the trailing bytes

Before enforcement, bytes 74–79 of the begin response are all zero, so a parser that asserts “the last six bytes are reserved and must be zero” works today — but will reject every welfare and subscription upload the moment the switch is turned on, because credit_state (74) and remaining_kb (75–77) become nonzero. Clients must parse credit_state at offset 74 and remaining_kb at 75–77, and restrict any zero-check to the final two reserved bytes (78–79). Do this and handle status 170/171 before the server-side flip.

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.
170ERROR_NO_STORAGE_CREDITZero-locker upload whose stripe is too large for the welfare free tier, and the mailbox has no prepaid subscription credit. Only emitted when enforcement is on. Attach a funded locker, or top up the mailbox via cmd 85 Subscribe.
171ERROR_WELFARE_EXHAUSTEDZero-locker upload that would fit the welfare free tier, but this mailbox has used up its welfare allowance for the current rolling window. Only emitted when enforcement is on. Retry after the window resets, use a subscription, or attach a funded locker.
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.