Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sdk range request #7489

Merged
merged 6 commits into from
Oct 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
504 changes: 504 additions & 0 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-bindgen",
"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"
122 changes: 122 additions & 0 deletions tuta-sdk/rust/demo/src/main.rs
Original file line number Diff line number Diff line change
@@ -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<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));
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 @@ -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 }
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 @@
#[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;

Expand Down Expand Up @@ -40,54 +43,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
12 changes: 5 additions & 7 deletions tuta-sdk/rust/sdk/src/entities/entity_facade.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading