diff --git a/tuta-sdk/rust/Cargo.lock b/tuta-sdk/rust/Cargo.lock index 9c794f0ecb15..8f0f6a279489 100644 --- a/tuta-sdk/rust/Cargo.lock +++ b/tuta-sdk/rust/Cargo.lock @@ -317,6 +317,12 @@ dependencies = [ "generic-array", ] +[[package]] +name = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + [[package]] name = "byteorder" version = "1.5.0" @@ -592,6 +598,17 @@ dependencies = [ "parking_lot_core", ] +[[package]] +name = "demo" +version = "0.1.0" +dependencies = [ + "async-trait", + "base64", + "reqwest", + "tokio", + "tuta-sdk", +] + [[package]] name = "der" version = "0.7.9" @@ -642,6 +659,15 @@ version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" +[[package]] +name = "encoding_rs" +version = "0.8.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59" +dependencies = [ + "cfg-if", +] + [[package]] name = "equivalent" version = "1.0.1" @@ -658,6 +684,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "fastrand" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" + [[package]] name = "fiat-crypto" version = "0.2.9" @@ -670,6 +702,30 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + [[package]] name = "fragile" version = "2.0.0" @@ -990,6 +1046,22 @@ dependencies = [ "tower-service", ] +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + [[package]] name = "hyper-util" version = "0.1.9" @@ -1009,6 +1081,16 @@ dependencies = [ "tracing", ] +[[package]] +name = "idna" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + [[package]] name = "indexmap" version = "2.6.0" @@ -1029,6 +1111,12 @@ dependencies = [ "generic-array", ] +[[package]] +name = "ipnet" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" + [[package]] name = "is_terminal_polyfill" version = "1.70.1" @@ -1079,6 +1167,15 @@ dependencies = [ "libc", ] +[[package]] +name = "js-sys" +version = "0.3.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a" +dependencies = [ + "wasm-bindgen", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -1260,6 +1357,23 @@ dependencies = [ "syn", ] +[[package]] +name = "native-tls" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466" +dependencies = [ + "libc", + "log 0.4.22", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + [[package]] name = "nom" version = "7.1.3" @@ -1333,6 +1447,27 @@ dependencies = [ "libm", ] +[[package]] +name = "num_enum" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e613fc340b2220f734a8595782c551f1250e969d87d3be1ae0579e8d4065179" +dependencies = [ + "num_enum_derive", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "num_threads" version = "0.1.7" @@ -1357,12 +1492,50 @@ version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" +[[package]] +name = "openssl" +version = "0.10.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9529f4786b70a3e8c61e11179af17ab6188ad8d0ded78c5529441ed39d4bd9c1" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "openssl-probe" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" +[[package]] +name = "openssl-sys" +version = "0.9.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f9e8deee91df40a943c71b917e5874b951d32a802526c85721ce3b776c929d6" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "oslog" version = "0.2.0" @@ -1374,6 +1547,16 @@ dependencies = [ "log 0.4.22", ] +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + [[package]] name = "parking_lot_core" version = "0.9.10" @@ -1413,6 +1596,12 @@ dependencies = [ "base64ct", ] +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + [[package]] name = "pin-project-lite" version = "0.2.14" @@ -1446,6 +1635,12 @@ dependencies = [ "spki", ] +[[package]] +name = "pkg-config" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" + [[package]] name = "plain" version = "0.2.3" @@ -1534,6 +1729,15 @@ dependencies = [ "syn", ] +[[package]] +name = "proc-macro-crate" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b" +dependencies = [ + "toml_edit", +] + [[package]] name = "proc-macro2" version = "1.0.87" @@ -1645,6 +1849,49 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +[[package]] +name = "reqwest" +version = "0.12.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8f4955649ef5c38cc7f9e8aa41761d48fb9677197daea9984dc54f56aad5e63" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "ipnet", + "js-sys", + "log 0.4.22", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls-pemfile", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "system-configuration", + "tokio", + "tokio-native-tls", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows-registry", +] + [[package]] name = "ring" version = "0.17.8" @@ -1934,6 +2181,18 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + [[package]] name = "sha2" version = "0.10.8" @@ -1951,6 +2210,15 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "signal-hook-registry" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +dependencies = [ + "libc", +] + [[package]] name = "signature" version = "2.2.0" @@ -2055,6 +2323,49 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "sync_wrapper" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" +dependencies = [ + "futures-core", +] + +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "tempfile" +version = "3.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04cbcdd0c794ebb0d4cf35e88edd2f7d2c4c3e9a5a6dab322839b321c6a87a64" +dependencies = [ + "cfg-if", + "fastrand", + "once_cell", + "rustix", + "windows-sys 0.59.0", +] + [[package]] name = "termtree" version = "0.4.1" @@ -2123,6 +2434,21 @@ dependencies = [ "time-core", ] +[[package]] +name = "tinyvec" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + [[package]] name = "tokio" version = "1.40.0" @@ -2133,7 +2459,9 @@ dependencies = [ "bytes", "libc", "mio", + "parking_lot", "pin-project-lite", + "signal-hook-registry", "socket2", "tokio-macros", "windows-sys 0.52.0", @@ -2150,6 +2478,16 @@ dependencies = [ "syn", ] +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + [[package]] name = "tokio-rustls" version = "0.26.0" @@ -2183,6 +2521,23 @@ dependencies = [ "serde", ] +[[package]] +name = "toml_datetime" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" + +[[package]] +name = "toml_edit" +version = "0.22.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "583c44c02ad26b0c3f3066fe629275e50627026c51ac2e595cca4c230ce1ce1d" +dependencies = [ + "indexmap", + "toml_datetime", + "winnow", +] + [[package]] name = "tower-service" version = "0.3.3" @@ -2237,6 +2592,7 @@ dependencies = [ "minicbor", "mockall", "mockall_double", + "num_enum", "oslog", "pqcrypto-kyber", "pqcrypto-traits", @@ -2277,12 +2633,27 @@ dependencies = [ "version_check", ] +[[package]] +name = "unicode-bidi" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" + [[package]] name = "unicode-ident" version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" +[[package]] +name = "unicode-normalization" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" +dependencies = [ + "tinyvec", +] + [[package]] name = "uniffi" version = "0.27.1" @@ -2418,12 +2789,29 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" +[[package]] +name = "url" +version = "2.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + [[package]] name = "utf8parse" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "version_check" version = "0.9.5" @@ -2455,6 +2843,83 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasm-bindgen" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5" +dependencies = [ + "cfg-if", + "once_cell", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b" +dependencies = [ + "bumpalo", + "log 0.4.22", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61e9300f63a621e96ed275155c108eb6f843b6a26d053f122ab69724559dc8ed" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484" + +[[package]] +name = "web-sys" +version = "0.3.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26fdeaafd9bd129f65e7c031593c24d62186301e0c72c8978fa1678be7d532c0" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "webpki-roots" version = "0.26.6" @@ -2515,6 +2980,36 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-registry" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0" +dependencies = [ + "windows-result", + "windows-strings", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-result" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-strings" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +dependencies = [ + "windows-result", + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.48.0" @@ -2663,6 +3158,15 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "winnow" +version = "0.6.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68a9bda4691f099d435ad181000724da8e5899daa10713c2d432552b9ccd3a6f" +dependencies = [ + "memchr", +] + [[package]] name = "zerocopy" version = "0.7.35" diff --git a/tuta-sdk/rust/Cargo.toml b/tuta-sdk/rust/Cargo.toml index e1d82cfcc742..7a53d63550cd 100644 --- a/tuta-sdk/rust/Cargo.toml +++ b/tuta-sdk/rust/Cargo.toml @@ -2,4 +2,8 @@ resolver = "2" -members = ["sdk", "uniffi-bindgen"] +members = [ + "sdk", + "uniffi-bindgen", + "demo", +] diff --git a/tuta-sdk/rust/demo/Cargo.toml b/tuta-sdk/rust/demo/Cargo.toml new file mode 100644 index 000000000000..5ea59e6b92dd --- /dev/null +++ b/tuta-sdk/rust/demo/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "demo" +version = "0.1.0" +edition = "2021" + +[dependencies] +tuta-sdk = { path = "../sdk" } +reqwest = "0.12.7" +tokio = { version = "1.40.0", features = ["full"] } +async-trait = "0.1.80" +base64 = "0.22.1" \ No newline at end of file diff --git a/tuta-sdk/rust/demo/src/main.rs b/tuta-sdk/rust/demo/src/main.rs new file mode 100644 index 000000000000..2df1c41e4cc1 --- /dev/null +++ b/tuta-sdk/rust/demo/src/main.rs @@ -0,0 +1,122 @@ +use std::collections::HashMap; +use std::error::Error; +use std::sync::Arc; + +use async_trait::async_trait; + +use tutasdk::folder_system::MailSetKind; +use tutasdk::generated_id::GeneratedId; +use tutasdk::login::{CredentialType, Credentials}; +use tutasdk::rest_client::{ + HttpMethod, RestClient, RestClientError, RestClientOptions, RestResponse, +}; +use tutasdk::Sdk; + +struct ReqwestHttpClient { + client: reqwest::Client, +} + +#[async_trait] +impl RestClient for ReqwestHttpClient { + async fn request_binary( + &self, + url: String, + method: HttpMethod, + options: RestClientOptions, + ) -> Result { + self.request_inner(url, method, options).await.map_err(|e| { + eprintln!("Network request failed! {:?}", e); + RestClientError::NetworkError + }) + } +} + +impl ReqwestHttpClient { + fn new() -> Self { + ReqwestHttpClient { + client: reqwest::Client::new(), + } + } + async fn request_inner( + &self, + url: String, + method: HttpMethod, + options: RestClientOptions, + ) -> Result> { + use reqwest::header::{HeaderMap, HeaderName}; + let mut req = self.client.request(http_method(method), url); + if let Some(body) = options.body { + req = req.body(body); + } + let mut headers: HeaderMap = HeaderMap::with_capacity(options.headers.len()); + for (key, value) in options.headers { + headers.insert(HeaderName::from_bytes(key.as_bytes())?, value.try_into()?); + } + let res = req.headers(headers).send().await?; + + let mut ret_headers = HashMap::with_capacity(res.headers().len()); + // for some reason collect() does not work + for (key, value) in res.headers() { + ret_headers.insert(key.to_string(), value.to_str()?.to_owned()); + } + Ok(RestResponse { + status: res.status().as_u16() as u32, + headers: ret_headers, + body: Some( + res.bytes() + .await + .expect("assuming response has a body") + .into(), + ), + }) + } +} + +fn http_method(http_method: HttpMethod) -> reqwest::Method { + use reqwest::Method; + match http_method { + HttpMethod::GET => Method::GET, + HttpMethod::POST => Method::POST, + HttpMethod::PUT => Method::PUT, + HttpMethod::DELETE => Method::DELETE, + } +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + use base64::prelude::*; + + // replace with real values + let host = "http://localhost:9000"; + let credentials = Credentials { + login: "bed-free@tutanota.de".to_owned(), + access_token: "access_token".to_owned(), + credential_type: CredentialType::Internal, + user_id: GeneratedId("user_id".to_owned()), + encrypted_passphrase_key: BASE64_STANDARD.decode("encrypted_passphrase_key").unwrap(), + }; + + let rest_client = ReqwestHttpClient::new(); + let sdk = Sdk::new(host.to_owned(), Arc::new(rest_client)); + let session = sdk.login(credentials).await?; + let mail_facade = session.mail_facade(); + + let mailbox = mail_facade.load_user_mailbox().await?; + + let folders = mail_facade.load_folders_for_mailbox(&mailbox).await?; + let inbox = folders + .system_folder_by_type(MailSetKind::Inbox) + .expect("inbox exists"); + let inbox_mails = mail_facade.load_mails_in_folder(inbox).await?; + + println!("Inbox:"); + for mail in inbox_mails { + let sender_arg = if mail.sender.name.is_empty() { + format!("<{}>", mail.sender.address) + } else { + format!("{} <{}>", mail.sender.name, mail.sender.address) + }; + println!("{0: <40}\t{1: <40}", sender_arg, mail.subject) + } + Ok(()) +} diff --git a/tuta-sdk/rust/sdk/Cargo.toml b/tuta-sdk/rust/sdk/Cargo.toml index 7e8dbc911567..7c9a48390cdc 100644 --- a/tuta-sdk/rust/sdk/Cargo.toml +++ b/tuta-sdk/rust/sdk/Cargo.toml @@ -30,6 +30,7 @@ futures = "0.3.30" log = "0.4.22" simple_logger = "5.0.0" uniffi = { git = "https://github.com/mozilla/uniffi-rs.git", rev = "13a1c559cb3708eeca40dcf95dc8b3ccccf3b88c" } +num_enum = "0.7.3" # only used for the native rest client hyper = { version = "1.4.1", features = ["client"], optional = true } diff --git a/tuta-sdk/rust/sdk/src/crypto_entity_client.rs b/tuta-sdk/rust/sdk/src/crypto_entity_client.rs index d9226b8af100..cbe507029552 100644 --- a/tuta-sdk/rust/sdk/src/crypto_entity_client.rs +++ b/tuta-sdk/rust/sdk/src/crypto_entity_client.rs @@ -1,12 +1,15 @@ #[cfg_attr(test, mockall_double::double)] use crate::crypto::crypto_facade::CryptoFacade; +use crate::element_value::ParsedEntity; use crate::entities::entity_facade::EntityFacade; use crate::entities::Entity; #[cfg_attr(test, mockall_double::double)] use crate::entity_client::EntityClient; use crate::entity_client::IdType; +use crate::generated_id::GeneratedId; use crate::instance_mapper::InstanceMapper; -use crate::ApiCallError; +use crate::metamodel::TypeModel; +use crate::{ApiCallError, ListLoadDirection}; use serde::Deserialize; use std::sync::Arc; @@ -40,54 +43,104 @@ impl CryptoEntityClient { ) -> Result { let type_ref = T::type_ref(); let type_model = self.entity_client.get_type_model(&type_ref)?; - let mut parsed_entity = self.entity_client.load(&type_ref, id).await?; + let parsed_entity = self.entity_client.load(&type_ref, id).await?; if type_model.marked_encrypted() { - let possible_session_key = self - .crypto_facade - .resolve_session_key(&mut parsed_entity, type_model) - .await + let typed_entity = self + .process_encrypted_entity(type_model, parsed_entity) + .await?; + Ok(typed_entity) + } else { + let typed_entity = self + .instance_mapper + .parse_entity::(parsed_entity) .map_err(|error| ApiCallError::InternalSdkError { error_message: format!( - "Failed to resolve session key for entity '{}' with ID: {}; {}", - type_model.name, id, error + "Failed to parse unencrypted entity into proper types: {}", + error ), })?; - match possible_session_key { - Some(session_key) => { - let decrypted_entity = self.entity_facade.decrypt_and_map( - type_model, - parsed_entity, - session_key, - )?; - let typed_entity = self - .instance_mapper - .parse_entity::(decrypted_entity) - .map_err(|e| ApiCallError::InternalSdkError { - error_message: format!( - "Failed to parse encrypted entity into proper types: {}", - e - ), - })?; - Ok(typed_entity) - }, - // `resolve_session_key()` only returns none if the entity is unencrypted, so - // no need to handle it - None => { - unreachable!() - }, + Ok(typed_entity) + } + } + + async fn process_encrypted_entity>( + &self, + type_model: &TypeModel, + mut parsed_entity: ParsedEntity, + ) -> Result { + let possible_session_key = self + .crypto_facade + .resolve_session_key(&mut parsed_entity, type_model) + .await + .map_err(|error| { + let id = parsed_entity.get("_id"); + ApiCallError::InternalSdkError { + error_message: format!( + "Failed to resolve session key for entity '{}' with ID: {:?}; {}", + type_model.name, id, error + ), + } + })?; + match possible_session_key { + Some(session_key) => { + let decrypted_entity = + self.entity_facade + .decrypt_and_map(type_model, parsed_entity, session_key)?; + let typed_entity = self + .instance_mapper + .parse_entity::(decrypted_entity) + .map_err(|e| ApiCallError::InternalSdkError { + error_message: format!( + "Failed to parse encrypted entity into proper types: {}", + e + ), + })?; + Ok(typed_entity) + }, + // `resolve_session_key()` only returns none if the entity is unencrypted, so + // no need to handle it + None => { + unreachable!() + }, + } + } + + #[allow(dead_code)] // will be used but rustc can't see it in some configurations right now + pub async fn load_range>( + &self, + list_id: &GeneratedId, + start_id: &GeneratedId, + count: usize, + direction: ListLoadDirection, + ) -> Result, ApiCallError> { + let type_ref = T::type_ref(); + let type_model = self.entity_client.get_type_model(&type_ref)?; + let parsed_entities = self + .entity_client + .load_range(&type_ref, list_id, start_id, count, direction) + .await?; + + if type_model.marked_encrypted() { + // StreamExt::collect requires result to be Default. Fall back to plain loop. + let mut result_list = Vec::with_capacity(parsed_entities.len()); + for entity in parsed_entities { + let typed_entity = self.process_encrypted_entity(type_model, entity).await?; + result_list.push(typed_entity); } + Ok(result_list) } else { - let typed_entity = self - .instance_mapper - .parse_entity::(parsed_entity) + let result_list: Vec = parsed_entities + .into_iter() + .map(|e| self.instance_mapper.parse_entity::(e)) + .collect::, _>>() .map_err(|error| ApiCallError::InternalSdkError { error_message: format!( "Failed to parse unencrypted entity into proper types: {}", error ), })?; - Ok(typed_entity) + Ok(result_list) } } } diff --git a/tuta-sdk/rust/sdk/src/entities/entity_facade.rs b/tuta-sdk/rust/sdk/src/entities/entity_facade.rs index 9124d6682b78..5a9546da6aa5 100644 --- a/tuta-sdk/rust/sdk/src/entities/entity_facade.rs +++ b/tuta-sdk/rust/sdk/src/entities/entity_facade.rs @@ -329,14 +329,12 @@ impl EntityFacadeImpl { None => type_model.app, }; - let Some(aggregate_type_model) = self - .type_model_provider - .get_type_model(dependency, association_model.ref_type) - else { - panic!("Undefined type_model {}", association_model.ref_type) - }; - if let AssociationType::Aggregation = association_model.association_type { + let aggregate_type_model = self + .type_model_provider + .get_type_model(dependency, association_model.ref_type) + .unwrap_or_else(|| panic!("Undefined type_model {}", association_model.ref_type)); + match (association_data, association_model.cardinality.borrow()) { (ElementValue::Null, Cardinality::ZeroOrOne) => Ok((ElementValue::Null, errors)), (ElementValue::Null, Cardinality::One) => Err(ApiCallError::InternalSdkError { diff --git a/tuta-sdk/rust/sdk/src/entity_client.rs b/tuta-sdk/rust/sdk/src/entity_client.rs index 642be21fbb2a..8336cadc80d3 100644 --- a/tuta-sdk/rust/sdk/src/entity_client.rs +++ b/tuta-sdk/rust/sdk/src/entity_client.rs @@ -5,7 +5,7 @@ use crate::element_value::{ElementValue, ParsedEntity}; use crate::generated_id::GeneratedId; use crate::json_element::RawEntity; use crate::json_serializer::JsonSerializer; -use crate::metamodel::TypeModel; +use crate::metamodel::{ElementType, TypeModel}; use crate::rest_client::{HttpMethod, RestClient, RestClientOptions}; use crate::rest_error::HttpError; use crate::type_model_provider::TypeModelProvider; @@ -47,40 +47,14 @@ impl EntityClient { type_ref: &TypeRef, id: &Id, ) -> Result { - let type_model = self.get_type_model(type_ref)?; let url = format!( "{}/rest/{}/{}/{}", self.base_url, type_ref.app, type_ref.type_, id ); - let model_version: u32 = type_model.version.parse().map_err(|_| { - let message = format!( - "Tried to parse invalid model_version {}", - type_model.version - ); - ApiCallError::InternalSdkError { - error_message: message, - } - })?; - let options = RestClientOptions { - body: None, - headers: self.auth_headers_provider.provide_headers(model_version), - }; - let response = self - .rest_client - .request_binary(url, HttpMethod::GET, options) - .await?; - let precondition = response.headers.get("precondition"); - match response.status { - 200..=299 => { - // Ok - }, - _ => { - return Err(ApiCallError::ServerResponseError { - source: HttpError::from_http_response(response.status, precondition)?, - }) - }, - } - let response_bytes = response.body.expect("no body"); + let response_bytes = self + .prepare_and_fire(type_ref, url) + .await? + .expect("no body"); let response_entity = serde_json::from_slice::(response_bytes.as_slice()).unwrap(); let parsed_entity = self.json_serializer.parse(type_ref, response_entity)?; @@ -115,13 +89,34 @@ impl EntityClient { #[allow(clippy::unused_async)] pub async fn load_range( &self, - _type_ref: &TypeRef, - _list_id: &GeneratedId, - _start_id: &GeneratedId, - _count: usize, - _list_load_direction: ListLoadDirection, + type_ref: &TypeRef, + list_id: &GeneratedId, + start_id: &GeneratedId, + count: usize, + direction: ListLoadDirection, ) -> Result, ApiCallError> { - todo!("entity client load_range") + let type_model = self.get_type_model(type_ref)?; + assert_eq!( + type_model.element_type, + ElementType::ListElement, + "cannot load range for non-list element" + ); + let reverse = direction == ListLoadDirection::DESC; + let url = format!( + "{}/rest/{}/{}/{}?start={start_id}&count={count}&reverse={reverse}", + self.base_url, type_ref.app, type_ref.type_, list_id + ); + let response_bytes = self + .prepare_and_fire(type_ref, url) + .await? + .expect("no body"); + let response_entities = serde_json::from_slice::>(response_bytes.as_slice()) + .expect("invalid response"); + let parsed_entities = response_entities + .into_iter() + .map(|e| self.json_serializer.parse(type_ref, e)) + .collect::, _>>()?; + Ok(parsed_entities) } /// Stores a newly created entity/instance as a single element on the backend @@ -188,6 +183,35 @@ impl EntityClient { ) -> Result<(), ApiCallError> { todo!("entity client erase_list_element") } + + async fn prepare_and_fire( + &self, + type_ref: &TypeRef, + url: String, + ) -> Result>, ApiCallError> { + let type_model = self.get_type_model(type_ref)?; + let model_version: u32 = type_model.version.parse().expect("invalid model_version"); + let options = RestClientOptions { + body: None, + headers: self.auth_headers_provider.provide_headers(model_version), + }; + let response = self + .rest_client + .request_binary(url, HttpMethod::GET, options) + .await?; + let precondition = response.headers.get("precondition"); + match response.status { + 200..=299 => { + // Ok + }, + _ => { + return Err(ApiCallError::ServerResponseError { + source: HttpError::from_http_response(response.status, precondition)?, + }) + }, + } + Ok(response.body) + } } #[cfg(test)] @@ -206,13 +230,13 @@ mockall::mock! { type_ref: &TypeRef, id: &Id, ) -> Result; - async fn load_all( + pub async fn load_all( &self, type_ref: &TypeRef, list_id: &IdTuple, start: Option, ) -> Result, ApiCallError>; - async fn load_range( + pub async fn load_range( &self, type_ref: &TypeRef, list_id: &GeneratedId, @@ -220,16 +244,177 @@ mockall::mock! { count: usize, list_load_direction: ListLoadDirection, ) -> Result, ApiCallError>; - async fn setup_element(&self, type_ref: &TypeRef, entity: RawEntity) -> Vec; - async fn setup_list_element( + pub async fn setup_element(&self, type_ref: &TypeRef, entity: RawEntity) -> Vec; + pub async fn setup_list_element( &self, type_ref: &TypeRef, list_id: &IdTuple, entity: RawEntity, ) -> Vec; - async fn update(&self, type_ref: &TypeRef, entity: ParsedEntity, model_version: u32) + pub async fn update(&self, type_ref: &TypeRef, entity: ParsedEntity, model_version: u32) -> Result<(), ApiCallError>; - async fn erase_element(&self, type_ref: &TypeRef, id: &GeneratedId) -> Result<(), ApiCallError>; - async fn erase_list_element(&self, type_ref: &TypeRef, id: IdTuple) -> Result<(), ApiCallError>; + pub async fn erase_element(&self, type_ref: &TypeRef, id: &GeneratedId) -> Result<(), ApiCallError>; + pub async fn erase_list_element(&self, type_ref: &TypeRef, id: IdTuple) -> Result<(), ApiCallError>; + } +} + +#[cfg(test)] +mod tests { + use std::collections::HashMap; + + use super::*; + use crate::entities::Entity; + use crate::metamodel::{Cardinality, ModelValue, ValueType}; + use crate::rest_client::{MockRestClient, RestResponse}; + use crate::{collection, str_map}; + use mockall::predicate::{always, eq}; + use serde::{Deserialize, Serialize}; + + #[derive(Clone, Serialize, Deserialize, Debug, PartialEq)] + struct TestListEntity { + _id: IdTuple, + field: String, + } + + impl Entity for TestListEntity { + fn type_ref() -> TypeRef { + TypeRef { + app: "test", + type_: "TestListEntity", + } + } + } + + #[tokio::test] + async fn test_load_range() { + let type_model_provider = mock_type_model_provider(); + + let list_id = GeneratedId("list_id".to_owned()); + let entity_map: HashMap = collection! { + "_id" => ElementValue::IdTupleId(IdTuple::new(list_id.clone(), GeneratedId("element_id".to_owned()))), + "field" => ElementValue::Bytes(vec![1, 2, 3]) + }; + let mut rest_client = MockRestClient::new(); + let url = "http://test.com/rest/test/TestListEntity/list_id?start=zzzzzzzzzzzz&count=100&reverse=true"; + let json_folder = JsonSerializer::new(type_model_provider.clone()) + .serialize(&TestListEntity::type_ref(), entity_map.clone()) + .unwrap(); + rest_client + .expect_request_binary() + .with(eq(url.to_owned()), eq(HttpMethod::GET), always()) + .return_once(move |_, _, _| { + Ok(RestResponse { + status: 200, + headers: HashMap::default(), + body: Some(serde_json::to_vec(&vec![json_folder]).unwrap()), + }) + }); + + let auth_headers_provider = HeadersProvider::new(Some("123".to_owned())); + let entity_client = EntityClient::new( + Arc::new(rest_client), + Arc::new(JsonSerializer::new(type_model_provider.clone())), + "http://test.com".to_owned(), + Arc::new(auth_headers_provider), + type_model_provider.clone(), + ); + + let result_entity = entity_client + .load_range( + &TestListEntity::type_ref(), + &list_id, + &GeneratedId::max_id(), + 100, + ListLoadDirection::DESC, + ) + .await + .expect("success"); + assert_eq!(result_entity, vec![entity_map]); + } + + #[tokio::test] + async fn test_load_range_asc() { + let type_model_provider = mock_type_model_provider(); + + let list_id = GeneratedId("list_id".to_owned()); + let entity_map: HashMap = collection! { + "_id" => ElementValue::IdTupleId(IdTuple::new(list_id.clone(), GeneratedId("element_id".to_owned()))), + "field" => ElementValue::Bytes(vec![1, 2, 3]) + }; + let mut rest_client = MockRestClient::new(); + let url = "http://test.com/rest/test/TestListEntity/list_id?start=------------&count=100&reverse=false"; + let json_folder = JsonSerializer::new(type_model_provider.clone()) + .serialize(&TestListEntity::type_ref(), entity_map.clone()) + .unwrap(); + rest_client + .expect_request_binary() + .with(eq(url.to_owned()), eq(HttpMethod::GET), always()) + .return_once(move |_, _, _| { + Ok(RestResponse { + status: 200, + headers: HashMap::default(), + body: Some(serde_json::to_vec(&vec![json_folder]).unwrap()), + }) + }); + + let auth_headers_provider = HeadersProvider::new(Some("123".to_owned())); + let entity_client = EntityClient::new( + Arc::new(rest_client), + Arc::new(JsonSerializer::new(type_model_provider.clone())), + "http://test.com".to_owned(), + Arc::new(auth_headers_provider), + type_model_provider.clone(), + ); + + let result_entity = entity_client + .load_range( + &TestListEntity::type_ref(), + &list_id, + &GeneratedId::min_id(), + 100, + ListLoadDirection::ASC, + ) + .await + .expect("success"); + assert_eq!(result_entity, vec![entity_map]); + } + + fn mock_type_model_provider() -> Arc { + let list_entity_type_model: TypeModel = TypeModel { + id: 1, + since: 1, + app: "test", + version: "1", + name: "TestListEntity", + element_type: ElementType::ListElement, + versioned: false, + encrypted: true, + root_id: "", + values: str_map! { + "_id" => ModelValue { + id: 1, + value_type: ValueType::GeneratedId, + cardinality: Cardinality::One, + is_final: true, + encrypted: false, + }, + "field" => + ModelValue { + id: 2, + value_type: ValueType::String, + cardinality: Cardinality::One, + is_final: false, + encrypted: true, + }, + }, + associations: HashMap::default(), + }; + + let type_model_provider = Arc::new(TypeModelProvider::new(str_map! { + "test" => str_map! { + "TestListEntity" => list_entity_type_model + } + })); + type_model_provider } } diff --git a/tuta-sdk/rust/sdk/src/folder_system.rs b/tuta-sdk/rust/sdk/src/folder_system.rs new file mode 100644 index 000000000000..4b18669e9dc4 --- /dev/null +++ b/tuta-sdk/rust/sdk/src/folder_system.rs @@ -0,0 +1,41 @@ +use crate::entities::tutanota::MailFolder; +use num_enum::TryFromPrimitive; + +pub struct FolderSystem { + // this structure should probably change rather soon + folders: Vec, +} + +#[derive(PartialEq, TryFromPrimitive)] +#[repr(u64)] +pub enum MailSetKind { + Custom = 0, + Inbox = 1, + Sent = 2, + Trash = 3, + Archive = 4, + Spam = 5, + Draft = 6, + All = 7, + Unknown = 9999, +} + +impl MailFolder { + fn mail_set_kind(&self) -> MailSetKind { + MailSetKind::try_from(self.folderType as u64).unwrap_or(MailSetKind::Unknown) + } +} + +impl FolderSystem { + #[must_use] + pub fn new(folders: Vec) -> Self { + Self { folders } + } + + #[must_use] + pub fn system_folder_by_type(&self, mail_set_kind: MailSetKind) -> Option<&MailFolder> { + self.folders + .iter() + .find(|f| f.mail_set_kind() == mail_set_kind) + } +} diff --git a/tuta-sdk/rust/sdk/src/generated_id.rs b/tuta-sdk/rust/sdk/src/generated_id.rs index e3ac7053c949..1c45aec52cc6 100644 --- a/tuta-sdk/rust/sdk/src/generated_id.rs +++ b/tuta-sdk/rust/sdk/src/generated_id.rs @@ -1,6 +1,7 @@ use crate::entity_client::IdType; use serde::de::{Error, Visitor}; use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use std::borrow::ToOwned; use std::fmt::{Debug, Display, Formatter}; pub const GENERATED_ID_STRUCT_NAME: &str = "GeneratedId"; @@ -25,6 +26,18 @@ impl GeneratedId { // not the actual alphabet we use in real generated IDs, but we aren't dealing with parsing generated IDs yet, so it's fine Self(generate_random_string::<9>()) } + + #[must_use] + pub fn min_id() -> Self { + // ideally should return a ref to a static id + GeneratedId("------------".to_owned()) + } + + #[must_use] + pub fn max_id() -> Self { + // ideally should return a ref to a static id + GeneratedId("zzzzzzzzzzzz".to_owned()) + } } impl From for String { diff --git a/tuta-sdk/rust/sdk/src/groups.rs b/tuta-sdk/rust/sdk/src/groups.rs new file mode 100644 index 000000000000..42eb64724777 --- /dev/null +++ b/tuta-sdk/rust/sdk/src/groups.rs @@ -0,0 +1,20 @@ +use num_enum::TryFromPrimitive; + +#[allow(dead_code)] +#[derive(PartialEq, TryFromPrimitive)] +#[repr(u64)] +pub enum GroupType { + User = 0, + Admin = 1, + MailingList = 2, + Customer = 3, + External = 4, + Mail = 5, + Contact = 6, + File = 7, + LocalAdmin = 8, + Calendar = 9, + Template = 10, + ContactList = 11, + Unknown = 9999, +} diff --git a/tuta-sdk/rust/sdk/src/key_loader_facade.rs b/tuta-sdk/rust/sdk/src/key_loader_facade.rs index 288519538ec7..8c3885f2e4fe 100644 --- a/tuta-sdk/rust/sdk/src/key_loader_facade.rs +++ b/tuta-sdk/rust/sdk/src/key_loader_facade.rs @@ -27,6 +27,8 @@ impl KeyLoaderFacade { } } + /// Load the symmetric group key for the groupId with the provided requestedVersion. + /// `currentGroupKey` needs to be set if the user is not a member of the group (e.g. an admin) pub async fn load_sym_group_key( &self, group_id: &GeneratedId, @@ -37,6 +39,7 @@ impl KeyLoaderFacade { Some(n) => { let group_key_version = n.version; if group_key_version < version { + // we might not have the membership for this group. so the caller needs to handle it by refreshing the cache return Err(KeyLoadError { reason: format!("Provided current group key is too old (${group_key_version}) to load the requested version ${version} for group ${group_id}") }); } n @@ -47,6 +50,7 @@ impl KeyLoaderFacade { if group_key.version == version { Ok(group_key.object) } else { + // TODO: refresh if group_key.version < version let group: Group = self.entity_client.load(&group_id.to_owned()).await?; let FormerGroupKey { symmetric_group_key, @@ -61,6 +65,7 @@ impl KeyLoaderFacade { async fn find_former_group_key( &self, group: &Group, + // TODO: why do we take it by ref if we are cloning it anyway current_group_key: &VersionedAesKey, target_key_version: i64, ) -> Result { @@ -163,10 +168,16 @@ impl KeyLoaderFacade { Ok(key) } + /// `group_id` MUST NOT be the user group id async fn load_and_decrypt_current_sym_group_key( &self, group_id: &GeneratedId, ) -> Result { + assert_ne!( + &self.user_facade.get_user_group_id(), + group_id, + "Must not add the user group to the regular group key cache" + ); let group_membership = self.user_facade.get_membership(group_id)?; let required_user_group_key = self .load_sym_user_group_key(group_membership.symKeyVersion) @@ -184,6 +195,7 @@ impl KeyLoaderFacade { &self, user_group_key_version: i64, ) -> Result { + // TODO: check for the version and refresh cache if needed self.load_sym_group_key( &self.user_facade.get_user_group_id(), user_group_key_version, diff --git a/tuta-sdk/rust/sdk/src/lib.rs b/tuta-sdk/rust/sdk/src/lib.rs index 43d3e4bf62a5..45b1e451f249 100644 --- a/tuta-sdk/rust/sdk/src/lib.rs +++ b/tuta-sdk/rust/sdk/src/lib.rs @@ -53,7 +53,9 @@ pub mod date; mod element_value; pub mod entities; mod entity_client; +pub mod folder_system; pub mod generated_id; +mod groups; mod instance_mapper; mod json_element; mod json_serializer; @@ -307,7 +309,7 @@ impl LoggedInSdk { /// Generates a new interface to operate on mail entities #[must_use] pub fn mail_facade(&self) -> MailFacade { - MailFacade::new(self.crypto_entity_client.clone()) + MailFacade::new(self.crypto_entity_client.clone(), self.user_facade.clone()) } } diff --git a/tuta-sdk/rust/sdk/src/login/login_facade.rs b/tuta-sdk/rust/sdk/src/login/login_facade.rs index 99a9ac730600..9088f2fa290b 100644 --- a/tuta-sdk/rust/sdk/src/login/login_facade.rs +++ b/tuta-sdk/rust/sdk/src/login/login_facade.rs @@ -24,8 +24,8 @@ use crate::{ApiCallError, IdTuple}; /// Error that may occur during login and session creation #[derive(Error, Debug, uniffi::Error, Clone, PartialEq)] pub enum LoginError { - #[error("InvalidSessionId: {error_message}")] - InvalidSessionId { error_message: String }, + #[error("InvalidAccessToken: {error_message}")] + InvalidAccessToken { error_message: String }, #[error("InvalidPassphrase: {error_message}")] InvalidPassphrase { error_message: String }, #[error("InvalidKey: {error_message}")] @@ -85,7 +85,7 @@ impl LoginFacade { credentials: &Credentials, ) -> Result { let session_id = parse_session_id(credentials.access_token.as_str()).map_err(|e| { - LoginError::InvalidSessionId { + LoginError::InvalidAccessToken { error_message: format!("Could not decode session id: {}", e), } })?; diff --git a/tuta-sdk/rust/sdk/src/mail_facade.rs b/tuta-sdk/rust/sdk/src/mail_facade.rs index 6a026d007db2..2f0e8a19cfe9 100644 --- a/tuta-sdk/rust/sdk/src/mail_facade.rs +++ b/tuta-sdk/rust/sdk/src/mail_facade.rs @@ -1,23 +1,87 @@ +use std::sync::Arc; + #[cfg_attr(test, mockall_double::double)] use crate::crypto_entity_client::CryptoEntityClient; -use crate::entities::tutanota::Mail; -use crate::{ApiCallError, IdTuple}; -use std::sync::Arc; +use crate::entities::tutanota::{Mail, MailBox, MailFolder, MailboxGroupRoot}; +use crate::folder_system::FolderSystem; +use crate::generated_id::GeneratedId; +use crate::groups::GroupType; +#[cfg_attr(test, mockall_double::double)] +use crate::user_facade::UserFacade; +use crate::{ApiCallError, IdTuple, ListLoadDirection}; /// Provides high level functions to manipulate mail entities via the REST API #[derive(uniffi::Object)] pub struct MailFacade { crypto_entity_client: Arc, + user_facade: Arc, } impl MailFacade { - pub fn new(crypto_entity_client: Arc) -> Self { + pub fn new( + crypto_entity_client: Arc, + user_facade: Arc, + ) -> Self { MailFacade { crypto_entity_client, + user_facade, } } } +impl MailFacade { + pub async fn load_user_mailbox(&self) -> Result { + let user = self.user_facade.get_user(); + let mail_group_ship = user + .memberships + .iter() + .find(|m| m.group_type() == GroupType::Mail) + .ok_or_else(|| ApiCallError::internal("User does not have mail group".to_owned()))?; + let group_root: MailboxGroupRoot = self + .crypto_entity_client + .load(&mail_group_ship.group) + .await?; + let mailbox: MailBox = self.crypto_entity_client.load(&group_root.mailbox).await?; + Ok(mailbox) + } + + pub async fn load_folders_for_mailbox( + &self, + mailbox: &MailBox, + ) -> Result { + let folders_list = &mailbox.folders.as_ref().unwrap().folders; + let folders: Vec = self + .crypto_entity_client + .load_range( + folders_list, + &GeneratedId::min_id(), + 100, + ListLoadDirection::ASC, + ) + .await?; + Ok(FolderSystem::new(folders)) + } + + pub async fn load_mails_in_folder( + &self, + folder: &MailFolder, + ) -> Result, ApiCallError> { + // TODO: real arguments + // TODO: this is a placeholder impl that doesn't work with mail sets + let mail_list_id = &folder.mails; + let mails = self + .crypto_entity_client + .load_range( + mail_list_id, + &GeneratedId::max_id(), + 20, + ListLoadDirection::DESC, + ) + .await?; + Ok(mails) + } +} + #[uniffi::export] impl MailFacade { /// Gets an email (an entity/instance of `Mail`) from the backend diff --git a/tuta-sdk/rust/sdk/src/metamodel.rs b/tuta-sdk/rust/sdk/src/metamodel.rs index 73eae4ac0c5d..b23abaa0ab9f 100644 --- a/tuta-sdk/rust/sdk/src/metamodel.rs +++ b/tuta-sdk/rust/sdk/src/metamodel.rs @@ -131,7 +131,10 @@ pub struct TypeModel { #[serde(rename = "type")] pub element_type: ElementType, pub versioned: bool, + #[cfg(not(test))] encrypted: bool, + #[cfg(test)] + pub encrypted: bool, pub root_id: &'static str, pub values: HashMap<&'static str, ModelValue>, pub associations: HashMap<&'static str, ModelAssociation>, diff --git a/tuta-sdk/rust/sdk/src/typed_entity_client.rs b/tuta-sdk/rust/sdk/src/typed_entity_client.rs index 50c90304441b..88e89d2f13ff 100644 --- a/tuta-sdk/rust/sdk/src/typed_entity_client.rs +++ b/tuta-sdk/rust/sdk/src/typed_entity_client.rs @@ -4,6 +4,7 @@ use crate::entity_client::EntityClient; use crate::entity_client::IdType; use crate::generated_id::GeneratedId; use crate::instance_mapper::InstanceMapper; +use crate::metamodel::{ElementType, TypeModel}; use crate::{ApiCallError, IdTuple, ListLoadDirection}; use serde::Deserialize; use std::sync::Arc; @@ -32,15 +33,7 @@ impl TypedEntityClient { id: &Id, ) -> Result { let type_model = self.entity_client.get_type_model(&T::type_ref())?; - if type_model.is_encrypted() { - return Err(ApiCallError::InternalSdkError { - error_message: format!( - "This client shall not handle encrypted fields! Entity: app: {}, name: {}", - &T::type_ref().app, - &T::type_ref().type_ - ), - }); - } + Self::check_if_encrypted(type_model)?; let parsed_entity = self.entity_client.load::(&T::type_ref(), id).await?; let typed_entity = self .instance_mapper @@ -64,16 +57,51 @@ impl TypedEntityClient { todo!("typed entity client load_all") } - #[allow(dead_code)] #[allow(clippy::unused_async)] pub async fn load_range>( &self, - _list_id: &GeneratedId, - _start_id: &GeneratedId, - _count: usize, - _list_load_direction: ListLoadDirection, + list_id: &GeneratedId, + start_id: &GeneratedId, + count: usize, + direction: ListLoadDirection, ) -> Result, ApiCallError> { - todo!("typed entity client load_range") + let type_model = self.entity_client.get_type_model(&T::type_ref())?; + Self::check_if_encrypted(type_model)?; + // TODO: enforce statically? + if type_model.element_type != ElementType::ListElement { + panic!( + "load_range for non-list type {}/{}", + type_model.app, type_model.name + ) + } + let entities = self + .entity_client + .load_range(&T::type_ref(), list_id, start_id, count, direction) + .await?; + let typed_entities = entities + .into_iter() + .map(|e| self.instance_mapper.parse_entity(e)) + .collect::, _>>() + .map_err(|e| { + let message = format!("Failed to parse entity: {e}"); + ApiCallError::InternalSdkError { + error_message: message, + } + })?; + Ok(typed_entities) + } + + // TODO: enforce statically? + fn check_if_encrypted(type_model: &TypeModel) -> Result<(), ApiCallError> { + if type_model.is_encrypted() { + return Err(ApiCallError::InternalSdkError { + error_message: format!( + "This client shall not handle encrypted fields! Entity: app: {}, name: {}", + type_model.app, type_model.name + ), + }); + } + Ok(()) } } diff --git a/tuta-sdk/rust/sdk/src/user_facade.rs b/tuta-sdk/rust/sdk/src/user_facade.rs index d6737caa38cf..625caf28a8d0 100644 --- a/tuta-sdk/rust/sdk/src/user_facade.rs +++ b/tuta-sdk/rust/sdk/src/user_facade.rs @@ -4,6 +4,7 @@ use crate::crypto::sha256; use crate::crypto::{Aes256Key, AES_256_KEY_SIZE}; use crate::entities::sys::{GroupMembership, User}; use crate::generated_id::GeneratedId; +use crate::groups::GroupType; #[cfg_attr(test, mockall_double::double)] use crate::key_cache::KeyCache; use crate::key_loader_facade::VersionedAesKey; @@ -142,3 +143,15 @@ impl UserFacade { }) } } + +impl GroupMembership { + #[must_use] + pub fn group_type(&self) -> GroupType { + match self.groupType { + None => GroupType::Unknown, + Some(group_type) => { + GroupType::try_from(group_type as u64).unwrap_or(GroupType::Unknown) + }, + } + } +} diff --git a/tuta-sdk/rust/sdk/src/util/test_utils.rs b/tuta-sdk/rust/sdk/src/util/test_utils.rs index 50f64a2af80f..1ad40fb07e52 100644 --- a/tuta-sdk/rust/sdk/src/util/test_utils.rs +++ b/tuta-sdk/rust/sdk/src/util/test_utils.rs @@ -137,6 +137,24 @@ pub fn create_test_entity_dict<'a, T: Entity + serde::Deserialize<'a>>() -> Pars entity } +/// Generate a test entity as a raw `ParsedEntity` dictionary type. +/// +/// The values will be set to these defaults: +/// * All ZeroOrOne values will be null +/// * All Any values will be empty +/// * All Encrypted One values will use bytes +/// * All Unencrypted One values will use default values, or random values if an ID type +/// +/// **NOTE:** The resulting dictionary is encrypted. +#[must_use] +pub fn create_encrypted_test_entity_dict<'a, T: Entity + serde::Deserialize<'a>>() -> ParsedEntity { + let provider = init_type_model_provider(); + let type_ref = T::type_ref(); + let entity = + create_encrypted_test_entity_dict_with_provider(&provider, type_ref.app, type_ref.type_); + entity +} + /// Convert a typed entity into a raw `ParsedEntity` dictionary type. /// /// # Panics @@ -240,6 +258,95 @@ fn create_test_entity_dict_with_provider( object } +fn create_encrypted_test_entity_dict_with_provider( + provider: &TypeModelProvider, + app: &str, + type_: &str, +) -> ParsedEntity { + let Some(model) = provider.get_type_model(app, type_) else { + panic!("Failed to create test entity {app}/{type_}: not in model") + }; + let mut object = ParsedEntity::new(); + + for (&name, value) in &model.values { + let element_value = match value.cardinality { + Cardinality::ZeroOrOne => ElementValue::Null, + Cardinality::Any => ElementValue::Array(Vec::new()), + Cardinality::One => { + if value.encrypted { + ElementValue::String(Default::default()) + } else { + match value.value_type { + ValueType::String => ElementValue::String(Default::default()), + ValueType::Number => ElementValue::Number(Default::default()), + ValueType::Bytes => ElementValue::Bytes(Default::default()), + ValueType::Date => ElementValue::Date(Default::default()), + ValueType::Boolean => ElementValue::Bool(Default::default()), + ValueType::GeneratedId => { + if name == "_id" && (model.element_type == ElementType::ListElement || model.element_type == ElementType::BlobElement) { + ElementValue::IdTupleId(IdTuple::new(GeneratedId::test_random(), GeneratedId::test_random())) + } else { + ElementValue::IdGeneratedId(GeneratedId::test_random()) + } + } + ValueType::CustomId => { + if name == "_id" && (model.element_type == ElementType::ListElement || model.element_type == ElementType::BlobElement) { + // TODO: adapt this when Custom Id tuples are supported + ElementValue::IdTupleId(IdTuple::new(GeneratedId::test_random(), GeneratedId::test_random())) + } else { + ElementValue::IdCustomId(CustomId::test_random()) + } + } + ValueType::CompressedString => todo!("Failed to create test entity {app}/{type_}: Compressed strings ({name}) are not yet supported!"), + } + } + }, + }; + + object.insert(name.to_owned(), element_value); + } + + for (&name, value) in &model.associations { + let association_value = + match value.cardinality { + Cardinality::ZeroOrOne => ElementValue::Null, + Cardinality::Any => ElementValue::Array(Vec::new()), + Cardinality::One => match value.association_type { + AssociationType::ElementAssociation => { + ElementValue::IdGeneratedId(GeneratedId::test_random()) + }, + AssociationType::ListAssociation => { + ElementValue::IdGeneratedId(GeneratedId::test_random()) + }, + AssociationType::ListElementAssociation => ElementValue::IdTupleId( + IdTuple::new(GeneratedId::test_random(), GeneratedId::test_random()), + ), + AssociationType::Aggregation => { + ElementValue::Dict(create_encrypted_test_entity_dict_with_provider( + provider, + value.dependency.unwrap_or(app), + value.ref_type, + )) + }, + AssociationType::BlobElementAssociation => ElementValue::IdTupleId( + IdTuple::new(GeneratedId::test_random(), GeneratedId::test_random()), + ), + }, + }; + object.insert(name.to_owned(), association_value); + } + + object +} + +#[macro_export] +macro_rules! str_map { + // map-like + ($($k:expr => $v:expr),* $(,)?) => {{ + core::convert::From::from([$(($k, $v),)*]) + }}; + } + #[macro_export] macro_rules! collection { // map-like