New SDK branch stacked on `sdk-blob-element-reading` (it reuses the `decrypt_with_owner_key` helper introduced there). Adds `MailFacade::load_mail_details_draft` plus the supporting `CryptoEntityClient::load_encrypted` accessor. Cherry-picked into `tutabridge-integration` so the bridge can finally render a body for draft mails instead of logging "No details for mail …" on every prefetch sweep. Held from upstream submission; the rebase notes in SDK_PRS.md spell out the dependency on the blob branch so the integration can be rebuilt deterministically.
10 KiB
SDK changes tracking
TutaBridge depends on a few changes to the Tuta Rust SDK (tuta-sdk/rust/sdk,
vendored as the tuta-repo submodule). To stay able to switch back to Tuta's
upstream at any time, every SDK change is kept as its own single-commit
branch off upstream/master, each independently reviewable / mergeable by
Tuta. They are combined only in the tutabridge-integration branch, which is
what the submodule actually checks out.
- Fork (our branches live here):
spartanz51/tutanota - Upstream:
tutao/tutanota - Integration branch (sum of the changes below):
tutabridge-integration
Rule: never accumulate unrelated SDK changes on one branch. One concern = one
branch = one commit, rebasable on upstream/master.
Branches
| Branch | Summary | Upstream PR | Fork PR | Submitted to upstream | Merged | Live-tested |
|---|---|---|---|---|---|---|
sdk-load-multiple |
EntityClient/CryptoEntityClient.load_multiple (batch entity loading) |
tutao#10854 | — | yes (open) | no | yes (sync 500 mails) |
sdk-blob-element-reading |
BlobFacade.load_blob_element + MailFacade.load_mail_details_blob (read MailDetailsBlob) |
tutao#10870 | — | yes (open) | no | yes (body decrypt over IMAP) |
sdk-2fa-session |
Interactive 2FA: initiate_session, authenticate_with_second_factor_totp, is_second_factor_pending, cancel_create_session |
tutao#10871 | — | yes (open) | no | yes (full TOTP login) |
sdk-folder-system |
Rebuild FolderSystem tree (system/custom/nested), add MailSetKind Label/Imported/Scheduled + accessors |
— | spartanz51#4 | no (held) | no | yes (custom folders listed + read over IMAP) |
sdk-move-mails |
MailFacade.move_mails (move to an arbitrary folder via MoveMailService) |
— | spartanz51#5 | no (held) | no | yes (IMAP MOVE between folders) |
sdk-event-bus |
WebSocket EventBusClient (/event?…) — realtime entity updates with catch-up via groupsToLastEventBatchIds, plus observable WsState |
— | — | no (held) | no | yes (live-tested over Phase 2/3 bridge integration, 26 unit tests) |
sdk-mail-set-entry-id |
mail_set_entry_id::{construct, deconstruct} — encode/decode MailSetEntry._id (4-byte truncated timestamp + 9-byte raw Mail id, base64url-no-pad) |
— | — | no (held) | no | yes (consumed by the bridge realtime delta optimisation; 8 unit tests, TS test vector asserted) |
sdk-inline-decrypt |
CryptoEntityClient::decrypt_inline_and_parse<T> — decrypt an entity payload arriving inline via the event bus, with no REST round-trip; EntityClient::parse_raw helper |
— | — | no (held) | no | yes (consumed by the bridge realtime path to skip load_mail on every Mail UPDATE / new mail; 3 integration tests against the live decryption fixture) |
sdk-mail-draft-details |
MailFacade::load_mail_details_draft — load + decrypt the MailDetailsDraft of a draft Mail (session key from the parent Mail); CryptoEntityClient::load_encrypted accessor for the no-auto-decrypt fetch step |
— | — | no (held) | no | yes (consumed by the bridge prefetch loop so drafts get a proper body instead of the "No details for mail" spam; 3 contract unit tests) |
Notes per branch
sdk-load-multiple
Additive utility, mirrors TS EntityClient.loadMultiple. Maintainer (charlag)
asked why it's submitted (not user-facing) and about LLM use; answered honestly.
sdk-blob-element-reading
Additive. Mirrors TS blob reading + doBlobRequestWithRetry/tryServers.
Returns MailDetails from load_mail_details_blob (matches TS).
sdk-2fa-session
Refactors create_session to delegate to initiate_session; reuses the
existing parse_session_id; no clientIdentifier change. Additive otherwise.
sdk-folder-system
Held — not submitted upstream. It modifies the existing FolderSystem
struct, which upstream marks as WIP (// this structure should probably change rather soon), so they likely want to design it themselves. Faithful port of
FolderSystem.ts. Submit only if the other PRs get traction and a maintainer
signals appetite — align the API with them first. Needed locally regardless for
custom/nested folder support in the bridge. Live-tested in the bridge: custom
folders are listed and read over IMAP (the custom-folders bridge change keys
everything by folder id).
sdk-event-bus
Held — not submitted upstream. Port of
src/common/api/worker/EventBusClient.ts: WebSocket client for /event?…,
reconnect/backoff matching the TS close-code semantics, catch-up of missed
batches via the groupsToLastEventBatchIds query param. Entity-update batches
are decoded from the server's untyped wire format into a small typed
EntityUpdateBatch (the other message kinds stay as raw serde_json::Value
since the bridge does not need them yet — consumers can apply the SDK's type
machinery if they want a typed view). Exposes a WsState
(Stopped/Connecting/Connected/Reconnecting) via state() -> watch::Receiver<_> so UIs can render the connection lifecycle live. New
dependency: tokio-tungstenite configured to reuse the existing rustls
stack; gated behind the existing net feature. 26 unit tests cover URL
building, wire parsing, reconnect logic and state-broadcast behaviour.
Live-tested as part of the bridge realtime sync (Phase 2/3).
sdk-mail-set-entry-id
Held — not submitted upstream. New module mail_set_entry_id exposing
construct(receive_date, mail_id) -> CustomId and the inverse
deconstruct(custom_id) -> Result<(DateTime, GeneratedId), _>. Mirrors the
TypeScript helpers in src/platform-kit/meta/EntityUtils.ts — a
MailSetEntry._id.element_id is a 13-byte buffer (4 bytes of timestamp
shifted right by 10 bits, then 9 raw bytes of the referenced Mail.element_id)
encoded as base64url-no-pad. Knowing the encoding lets a realtime consumer
extract the mail id directly from a MailSetEntry CREATE/DELETE event
without an extra REST round-trip. 8 unit tests assert the TS test vector
verbatim plus round-trip and four error shapes. Live-tested in the bridge
realtime delta path (Phase 2 of the realtime work).
sdk-mail-draft-details
Held — not submitted upstream. MailFacade::load_mail_details_draft(&mail)
mirrors the TS MailFacade.loadMailDetailsDraft() companion of
loadMailDetailsBlob. A draft mail's body lives in a MailDetailsDraft
list element (not a blob archive entry like MailDetailsBlob), so it's
fetched through the standard list-element REST path; the session key is
always resolved from the parent Mail (_ownerEncSessionKey +
_ownerGroup + _ownerKeyVersion), never from the MailDetailsDraft
itself — the draft's own _ownerEncSessionKey is ZeroOrOne on the
wire and the TS client never relies on it being set, so we match that
contract. CryptoEntityClient::load_encrypted is a small public helper
that exposes the raw EntityClient::load step without the auto-decrypt
that load_untyped performs — the list-element counterpart of
BlobFacade::load_blob_element for the same use case (caller has the
session key from elsewhere and will decrypt the result with
decrypt_with_owner_key). Built off sdk-blob-element-reading
because it depends on decrypt_with_owner_key introduced there; if the
blob branch merges upstream first this branch becomes a clean
one-commit branch off upstream/master. 3 unit tests pin down the
early-return contract (rejects a received mail, refuses a missing
draft id, refuses a missing _ownerEncSessionKey). Live-tested in the
bridge prefetch path so drafts get a body instead of the "No details
for mail" log spam.
sdk-inline-decrypt
Held — not submitted upstream. CryptoEntityClient::decrypt_inline_and_parse<T>
takes the still-encrypted JSON delivered inside a WebSocket
EntityUpdate.instance and walks the existing decryption pipeline
locally (JsonSerializer::parse → CryptoFacade::resolve_session_key
→ EntityFacade::decrypt_and_map → InstanceMapper::parse_entity),
producing the same typed entity as a full REST load would, with no
network call. Returns Ok(None) when the session key cannot be
resolved — a transient state (post-reply attachment key propagation in
the TS) that should fall through to a REST load rather than surface
as an error. EntityClient::parse_raw is exposed as a thin public
wrapper around the previously-private JsonSerializer so the new
method can feed an arbitrary deserialised JSON object into the same
parsing pass load uses internally. The MockEntityClient mock
declaration is extended for the new accessor. Three integration tests
reuse the captured download_mail_test/mail.json fixture as a
simulated event-bus payload — the happy-path test asserts the inline
decryption yields the same Mail (subject, recipientCount) that the
REST-backed download_mail_test extracts. Live-tested in the bridge
realtime path (Phase 3 of the realtime work).
Rebasing on a newer upstream
cd tuta-repo
git fetch upstream
# rebase each SDK branch on the new master (resolve only if upstream touched
# the same files — so far it hasn't)
git rebase upstream/master sdk-load-multiple
git rebase upstream/master sdk-blob-element-reading
git rebase upstream/master sdk-2fa-session
git rebase upstream/master sdk-folder-system
git rebase upstream/master sdk-move-mails
git rebase upstream/master sdk-event-bus
git rebase upstream/master sdk-mail-set-entry-id
git rebase upstream/master sdk-inline-decrypt
# `sdk-mail-draft-details` is stacked on `sdk-blob-element-reading` (it
# depends on `decrypt_with_owner_key`). Rebase it onto the rebased blob
# branch so it stays linear:
git rebase sdk-blob-element-reading sdk-mail-draft-details
# rebuild the integration branch from the rebased branches
git checkout -B tutabridge-integration upstream/master
git cherry-pick sdk-load-multiple sdk-blob-element-reading sdk-2fa-session sdk-folder-system sdk-move-mails sdk-event-bus sdk-mail-set-entry-id sdk-inline-decrypt sdk-mail-draft-details
When an upstream PR merges, drop that branch from the cherry-pick list — the
integration branch shrinks until (ideally) it equals upstream/master.