Skip to content

Commit

Permalink
[sdk] Range loading, demo
Browse files Browse the repository at this point in the history
  • Loading branch information
charlag committed Oct 10, 2024
1 parent b839aee commit 436fe35
Show file tree
Hide file tree
Showing 17 changed files with 1,727 additions and 138 deletions.
1,015 changes: 980 additions & 35 deletions tuta-sdk/rust/Cargo.lock

Large diffs are not rendered by default.

6 changes: 5 additions & 1 deletion tuta-sdk/rust/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,8 @@

resolver = "2"

members = ["sdk", "uniffi-bindgen"]
members = [
"sdk",
"uniffi-bingen",
"demo",
]
11 changes: 11 additions & 0 deletions tuta-sdk/rust/demo/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"
117 changes: 117 additions & 0 deletions tuta-sdk/rust/demo/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
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<RestResponse, RestClientError> {
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<RestResponse, Box<dyn Error>> {
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<dyn Error>> {
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), "244.0.0".to_owned());
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(())
}
1 change: 1 addition & 0 deletions tuta-sdk/rust/sdk/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ mockall_double = "0.3.1"
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"

[target.'cfg(target_os = "android")'.dependencies]
android_log = "0.1.3"
Expand Down
123 changes: 88 additions & 35 deletions tuta-sdk/rust/sdk/src/crypto_entity_client.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
#[mockall_double::double]
use crate::crypto::crypto_facade::CryptoFacade;
use crate::element_value::ParsedEntity;
use crate::entities::entity_facade::EntityFacade;
use crate::entities::Entity;
#[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;

Expand Down Expand Up @@ -39,54 +42,104 @@ impl CryptoEntityClient {
) -> Result<T, ApiCallError> {
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::<T>(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::<T>(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<T: Entity + Deserialize<'static>>(
&self,
type_model: &TypeModel,
mut parsed_entity: ParsedEntity,
) -> Result<T, ApiCallError> {
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::<T>(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<T: Entity + Deserialize<'static>>(
&self,
list_id: &GeneratedId,
start_id: &GeneratedId,
count: usize,
direction: ListLoadDirection,
) -> Result<Vec<T>, 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::<T>(parsed_entity)
let result_list: Vec<T> = parsed_entities
.into_iter()
.map(|e| self.instance_mapper.parse_entity::<T>(e))
.collect::<Result<Vec<T>, _>>()
.map_err(|error| ApiCallError::InternalSdkError {
error_message: format!(
"Failed to parse unencrypted entity into proper types: {}",
error
),
})?;
Ok(typed_entity)
Ok(result_list)
}
}
}
Expand Down
Loading

0 comments on commit 436fe35

Please sign in to comment.