diff --git a/Cargo.lock b/Cargo.lock index 7bb993ae..f34c6f19 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1350,6 +1350,7 @@ dependencies = [ "socket2 0.5.5", "stderrlog", "unicode-segmentation", + "url", "uuid", "xdg", ] @@ -2457,6 +2458,7 @@ dependencies = [ "form_urlencoded", "idna", "percent-encoding", + "serde", ] [[package]] diff --git a/melib/Cargo.toml b/melib/Cargo.toml index 842f8d9b..ffb9cd22 100644 --- a/melib/Cargo.toml +++ b/melib/Cargo.toml @@ -50,8 +50,8 @@ serde_path_to_error = { version = "0.1" } smallvec = { version = "^1.5.0", features = ["serde"] } smol = "1.0.0" socket2 = { version = "0.5", features = [] } - unicode-segmentation = { version = "1.2.1", default-features = false, optional = true } +url = { version = "2.4", optional = true } uuid = { version = "^1", features = ["serde", "v4", "v5"] } xdg = "2.1.0" @@ -64,7 +64,7 @@ http = ["isahc"] http-static = ["isahc", "isahc/static-curl"] imap = ["imap-codec", "tls"] imap-trace = ["imap"] -jmap = ["http"] +jmap = ["http", "url/serde"] jmap-trace = ["jmap"] nntp = ["tls"] nntp-trace = ["nntp"] diff --git a/melib/src/imap/mod.rs b/melib/src/imap/mod.rs index 90829406..323e3d18 100644 --- a/melib/src/imap/mod.rs +++ b/melib/src/imap/mod.rs @@ -19,6 +19,9 @@ * along with meli. If not, see . */ +// In case we forget to wait some future. +#![deny(unused_must_use)] + use smallvec::SmallVec; #[macro_use] mod protocol_parser; diff --git a/melib/src/jmap/connection.rs b/melib/src/jmap/connection.rs index ed998358..c45ad0e4 100644 --- a/melib/src/jmap/connection.rs +++ b/melib/src/jmap/connection.rs @@ -19,7 +19,7 @@ * along with meli. If not, see . */ -use std::{convert::TryFrom, sync::MutexGuard}; +use std::convert::TryFrom; use isahc::config::Configurable; @@ -28,8 +28,7 @@ use crate::error::NetworkErrorKind; #[derive(Debug)] pub struct JmapConnection { - pub session: Arc>, - pub request_no: Arc>, + pub request_no: Arc>, pub client: Arc, pub server_conf: JmapServerConf, pub store: Arc, @@ -69,8 +68,7 @@ impl JmapConnection { let client = client.build()?; let server_conf = server_conf.clone(); Ok(Self { - session: Arc::new(Mutex::new(Default::default())), - request_no: Arc::new(Mutex::new(0)), + request_no: Arc::new(FutureMutex::new(0)), client: Arc::new(client), server_conf, store, @@ -79,28 +77,37 @@ impl JmapConnection { } pub async fn connect(&mut self) -> Result<()> { - if self.store.online_status.lock().await.1.is_ok() { + if self.store.online_status.is_ok().await { return Ok(()); } - fn to_well_known(uri: &str) -> String { - let uri = uri.trim_start_matches('/'); - format!("{uri}/.well-known/jmap") + fn to_well_known(uri: &Url) -> Url { + let mut uri = uri.clone(); + uri.set_path(".well-known/jmap"); + uri } let mut jmap_session_resource_url = to_well_known(&self.server_conf.server_url); - let mut req = match self.client.get_async(&jmap_session_resource_url).await { + let mut req = match self + .client + .get_async(jmap_session_resource_url.as_str()) + .await + { Err(err) => 'block: { - if matches!(NetworkErrorKind::from(err.kind()), NetworkErrorKind::ProtocolViolation if self.server_conf.server_url.starts_with("http://")) + if matches!(NetworkErrorKind::from(err.kind()), NetworkErrorKind::ProtocolViolation if self.server_conf.server_url.scheme() == "http") { // attempt recovery by trying https:// - self.server_conf.server_url = format!( - "https{}", - self.server_conf.server_url.trim_start_matches("http") + self.server_conf.server_url.set_scheme("https").expect( + "set_scheme to https must succeed here because we checked earlier that \ + current scheme is http", ); jmap_session_resource_url = to_well_known(&self.server_conf.server_url); - if let Ok(s) = self.client.get_async(&jmap_session_resource_url).await { + if let Ok(s) = self + .client + .get_async(jmap_session_resource_url.as_str()) + .await + { log::error!( "Account {} server URL should start with `https`. Please correct your \ configuration value. Its current value is `{}`.", @@ -119,11 +126,12 @@ impl JmapConnection { &self.server_conf.server_url, &err )) .set_source(Some(Arc::new(err))); - *self.store.online_status.lock().await = (Instant::now(), Err(err.clone())); + _ = self.store.online_status.set(None, Err(err.clone())).await; return Err(err); } Ok(s) => s, }; + let req_instant = Instant::now(); if !req.status().is_success() { let kind: crate::error::NetworkErrorKind = req.status().into(); @@ -133,7 +141,11 @@ impl JmapConnection { &self.server_conf.server_url, res_text )) .set_kind(kind.into()); - *self.store.online_status.lock().await = (Instant::now(), Err(err.clone())); + _ = self + .store + .online_status + .set(Some(req_instant), Err(err.clone())) + .await; return Err(err); } @@ -147,7 +159,11 @@ impl JmapConnection { &self.server_conf.server_url, &err )) .set_source(Some(Arc::new(err))); - *self.store.online_status.lock().await = (Instant::now(), Err(err.clone())); + _ = self + .store + .online_status + .set(Some(req_instant), Err(err.clone())) + .await; return Err(err); } Ok(s) => s, @@ -163,7 +179,11 @@ impl JmapConnection { &self.server_conf.server_url, &res_text )) .set_source(Some(Arc::new(err))); - *self.store.online_status.lock().await = (Instant::now(), Err(err.clone())); + _ = self + .store + .online_status + .set(Some(req_instant), Err(err.clone())) + .await; return Err(err); } Ok(s) => s, @@ -181,7 +201,11 @@ impl JmapConnection { .join(", "), core_capability = JMAP_CORE_CAPABILITY )); - *self.store.online_status.lock().await = (Instant::now(), Err(err.clone())); + _ = self + .store + .online_status + .set(Some(req_instant), Err(err.clone())) + .await; return Err(err); } if !session.capabilities.contains_key(JMAP_MAIL_CAPABILITY) { @@ -197,24 +221,37 @@ impl JmapConnection { .join(", "), mail_capability = JMAP_MAIL_CAPABILITY )); - *self.store.online_status.lock().await = (Instant::now(), Err(err.clone())); + _ = self + .store + .online_status + .set(Some(req_instant), Err(err.clone())) + .await; return Err(err); } + *self.store.core_capabilities.lock().unwrap() = session.capabilities.clone(); + let mail_account_id = session.mail_account_id(); + _ = self + .store + .online_status + .set(Some(req_instant), Ok(session)) + .await; - *self.store.online_status.lock().await = (Instant::now(), Ok(())); - *self.session.lock().unwrap() = session; /* Fetch account identities. */ let mut id_list = { let mut req = Request::new(self.request_no.clone()); - let identity_get = IdentityGet::new().account_id(self.mail_account_id()); - req.add_call(&identity_get); + let identity_get = IdentityGet::new().account_id(mail_account_id.clone()); + req.add_call(&identity_get).await; let mut res_text = self.post_async(None, serde_json::to_string(&req)?).await?; let res_text = res_text.text().await?; let mut v: MethodResponse = match deserialize_from_str(&res_text) { Err(err) => { - *self.store.online_status.lock().await = (Instant::now(), Err(err.clone())); + _ = self + .store + .online_status + .set(Some(req_instant), Err(err.clone())) + .await; return Err(err); } Ok(s) => s, @@ -227,7 +264,7 @@ impl JmapConnection { let mut req = Request::new(self.request_no.clone()); let identity_set = IdentitySet( Set::::new() - .account_id(self.mail_account_id()) + .account_id(mail_account_id.clone()) .create(Some({ let address = crate::email::Address::try_from(self.store.main_identity.as_str()) @@ -258,24 +295,32 @@ impl JmapConnection { } })), ); - req.add_call(&identity_set); + req.add_call(&identity_set).await; let mut res_text = self.post_async(None, serde_json::to_string(&req)?).await?; let res_text = res_text.text().await?; let _: MethodResponse = match deserialize_from_str(&res_text) { Err(err) => { - *self.store.online_status.lock().await = (Instant::now(), Err(err.clone())); + _ = self + .store + .online_status + .set(Some(req_instant), Err(err.clone())) + .await; return Err(err); } Ok(s) => s, }; let mut req = Request::new(self.request_no.clone()); - let identity_get = IdentityGet::new().account_id(self.mail_account_id()); - req.add_call(&identity_get); + let identity_get = IdentityGet::new().account_id(mail_account_id.clone()); + req.add_call(&identity_get).await; let mut res_text = self.post_async(None, serde_json::to_string(&req)?).await?; let res_text = res_text.text().await?; let mut v: MethodResponse = match deserialize_from_str(&res_text) { Err(err) => { - *self.store.online_status.lock().await = (Instant::now(), Err(err.clone())); + _ = self + .store + .online_status + .set(Some(req_instant), Err(err.clone())) + .await; return Err(err); } Ok(s) => s, @@ -284,28 +329,17 @@ impl JmapConnection { GetResponse::::try_from(v.method_responses.remove(0))?; id_list = list; } - self.session.lock().unwrap().identities = + self.session_guard().await?.identities = id_list.into_iter().map(|id| (id.id.clone(), id)).collect(); Ok(()) } - pub fn mail_account_id(&self) -> Id { - self.session.lock().unwrap().primary_accounts[JMAP_MAIL_CAPABILITY].clone() - } - - pub fn mail_identity_id(&self) -> Option> { - self.session - .lock() - .unwrap() - .identities - .keys() - .next() - .cloned() - } - - pub fn session_guard(&'_ self) -> MutexGuard<'_, Session> { - self.session.lock().unwrap() + #[inline] + pub async fn session_guard( + &'_ self, + ) -> Result), Session>> { + self.store.online_status.session_guard().await } pub fn add_refresh_event(&self, event: RefreshEvent) { @@ -325,15 +359,16 @@ impl JmapConnection { } else { return Ok(()); }; + let mail_account_id = self.session_guard().await?.mail_account_id(); loop { let email_changes_call: EmailChanges = EmailChanges::new( Changes::::new() - .account_id(self.mail_account_id().clone()) + .account_id(mail_account_id.clone()) .since_state(current_state.clone()), ); let mut req = Request::new(self.request_no.clone()); - let prev_seq = req.add_call(&email_changes_call); + let prev_seq = req.add_call(&email_changes_call).await; let email_get_call: EmailGet = EmailGet::new( Get::new() .ids(Some(Argument::reference::< @@ -344,43 +379,46 @@ impl JmapConnection { prev_seq, ResultField::::new("/created"), ))) - .account_id(self.mail_account_id().clone()), + .account_id(mail_account_id.clone()), ); - req.add_call(&email_get_call); - let mailbox_id: Id; - if let Some(mailbox) = self.store.mailboxes.read().unwrap().get(&mailbox_hash) { - if let Some(email_query_state) = mailbox.email_query_state.lock().unwrap().clone() { - mailbox_id = mailbox.id.clone(); - let email_query_changes_call = EmailQueryChanges::new( - QueryChanges::new(self.mail_account_id().clone(), email_query_state) - .filter(Some(Filter::Condition( - EmailFilterCondition::new() - .in_mailbox(Some(mailbox_id.clone())) - .into(), - ))), - ); - let seq_no = req.add_call(&email_query_changes_call); - let email_get_call: EmailGet = EmailGet::new( - Get::new() - .ids(Some(Argument::reference::< - EmailQueryChanges, - EmailObject, - EmailObject, - >( - seq_no, - ResultField::::new("/removed"), - ))) - .account_id(self.mail_account_id().clone()) - .properties(Some(vec![ - "keywords".to_string(), - "mailboxIds".to_string(), - ])), - ); - req.add_call(&email_get_call); - } else { - return Ok(()); - } + req.add_call(&email_get_call).await; + let mailbox = self + .store + .mailboxes + .read() + .unwrap() + .get(&mailbox_hash) + .map(|m| { + let email_query_state = m.email_query_state.lock().unwrap().clone(); + let mailbox_id: Id = m.id.clone(); + (email_query_state, mailbox_id) + }); + if let Some((Some(email_query_state), mailbox_id)) = mailbox { + let email_query_changes_call = EmailQueryChanges::new( + QueryChanges::new(mail_account_id.clone(), email_query_state).filter(Some( + Filter::Condition( + EmailFilterCondition::new() + .in_mailbox(Some(mailbox_id.clone())) + .into(), + ), + )), + ); + let seq_no = req.add_call(&email_query_changes_call).await; + let email_get_call: EmailGet = EmailGet::new( + Get::new() + .ids(Some(Argument::reference::< + EmailQueryChanges, + EmailObject, + EmailObject, + >( + seq_no, + ResultField::::new("/removed"), + ))) + .account_id(mail_account_id.clone()) + .properties(Some(vec!["keywords".to_string(), "mailboxIds".to_string()])), + ); + req.add_call(&email_get_call).await; } else { return Ok(()); } @@ -395,7 +433,7 @@ impl JmapConnection { } let mut v: MethodResponse = match deserialize_from_str(&res_text) { Err(err) => { - *self.store.online_status.lock().await = (Instant::now(), Err(err.clone())); + _ = self.store.online_status.set(None, Err(err.clone())).await; return Err(err); } Ok(s) => s, @@ -425,11 +463,8 @@ impl JmapConnection { .collect::>(); mailbox_hashes.push(v); } - for (env, mailbox_hashes) in list - .into_iter() - .map(|obj| self.store.add_envelope(obj)) - .zip(mailbox_hashes) - { + for (obj, mailbox_hashes) in list.into_iter().zip(mailbox_hashes) { + let env = self.store.add_envelope(obj).await; for mailbox_hash in mailbox_hashes.iter().skip(1).cloned() { let mut mailboxes_lck = self.store.mailboxes.write().unwrap(); mailboxes_lck.entry(mailbox_hash).and_modify(|mbox| { @@ -460,7 +495,7 @@ impl JmapConnection { } } } - let reverse_id_store_lck = self.store.reverse_id_store.lock().unwrap(); + let reverse_id_store_lck = self.store.reverse_id_store.lock().await; let response = v.method_responses.remove(0); match EmailQueryChangesResponse::try_from(response) { Ok(EmailQueryChangesResponse { @@ -581,7 +616,7 @@ impl JmapConnection { let _: MethodResponse = match deserialize_from_str(&res_text) { Err(err) => { log::error!("{}", &err); - *self.store.online_status.lock().await = (Instant::now(), Err(err.clone())); + _ = self.store.online_status.set(None, Err(err.clone())).await; return Err(err); } Ok(s) => s, @@ -589,19 +624,19 @@ impl JmapConnection { Ok(res_text) } - pub async fn get_async(&self, url: &str) -> Result> { + pub async fn get_async(&self, url: &Url) -> Result> { if cfg!(feature = "jmap-trace") { - let res = self.client.get_async(url).await; + let res = self.client.get_async(url.as_str()).await; log::trace!("get_async(): url `{}` response {:?}", url, res); Ok(res?) } else { - Ok(self.client.get_async(url).await?) + Ok(self.client.get_async(url.as_str()).await?) } } pub async fn post_async> + Send + Sync>( &self, - api_url: Option<&str>, + api_url: Option<&Url>, request: T, ) -> Result> { let request: Vec = request.into(); @@ -612,9 +647,9 @@ impl JmapConnection { ); } if let Some(api_url) = api_url { - Ok(self.client.post_async(api_url, request).await?) + Ok(self.client.post_async(api_url.as_str(), request).await?) } else { - let api_url = self.session.lock().unwrap().api_url.clone(); + let api_url = self.session_guard().await?.api_url.clone(); Ok(self.client.post_async(api_url.as_str(), request).await?) } } diff --git a/melib/src/jmap/mod.rs b/melib/src/jmap/mod.rs index fe403a54..e8987bf3 100644 --- a/melib/src/jmap/mod.rs +++ b/melib/src/jmap/mod.rs @@ -19,6 +19,9 @@ * along with meli. If not, see . */ +// In case we forget to wait some future. +#![deny(unused_must_use)] + use std::{ collections::{BTreeSet, HashMap, HashSet}, convert::TryFrom, @@ -28,11 +31,18 @@ use std::{ time::{Duration, Instant}, }; -use futures::{lock::Mutex as FutureMutex, Stream}; +use futures::{ + lock::{ + MappedMutexGuard as FutureMappedMutexGuard, Mutex as FutureMutex, + MutexGuard as FutureMutexGuard, + }, + Stream, +}; use indexmap::{IndexMap, IndexSet}; use isahc::{config::RedirectPolicy, AsyncReadResponseExt, HttpClient}; use serde_json::{json, Value}; use smallvec::SmallVec; +use url::Url; use crate::{ backends::*, @@ -117,7 +127,7 @@ pub struct EnvelopeCache { #[derive(Clone, Debug)] pub struct JmapServerConf { - pub server_url: String, + pub server_url: Url, pub server_username: String, pub server_password: String, pub use_token: bool, @@ -135,6 +145,19 @@ macro_rules! get_conf_val { )) }) }; + ($s:ident[$var:literal], $t:ty) => { + get_conf_val!($s[$var]).and_then(|v| { + <$t>::from_str(&v).map_err(|e| { + Error::new(format!( + "Configuration error ({}): Invalid value for field `{}`: {}\n{}", + $s.name.as_str(), + $var, + v, + e + )) + }) + }) + }; ($s:ident[$var:literal], $default:expr) => { $s.extra .get($var) @@ -169,8 +192,8 @@ impl JmapServerConf { ))); } Ok(Self { - server_url: get_conf_val!(s["server_url"])?.to_string(), - server_username: get_conf_val!(s["server_username"])?.to_string(), + server_url: get_conf_val!(s["server_url"], Url)?, + server_username: get_conf_val!(s["server_username"], String)?, server_password: s.server_password()?, use_token, danger_accept_invalid_certs: get_conf_val!(s["danger_accept_invalid_certs"], false)?, @@ -185,66 +208,114 @@ impl JmapServerConf { } } +#[derive(Debug, Clone)] +#[repr(transparent)] +pub struct OnlineStatus(pub Arc)>>); + +impl OnlineStatus { + /// Returns if session value is `Ok(_)`. + pub async fn is_ok(&self) -> bool { + self.0.lock().await.1.is_ok() + } + + /// Get timestamp of last update. + pub async fn timestamp(&self) -> Instant { + self.0.lock().await.0 + } + + /// Get timestamp of last update. + pub async fn update_timestamp(&self, value: Option) { + self.0.lock().await.0 = value.unwrap_or_else(Instant::now); + } + + /// Set inner value. + pub async fn set(&self, t: Option, value: Result) -> Result { + std::mem::replace( + &mut (*self.0.lock().await), + (t.unwrap_or_else(Instant::now), value), + ) + .1 + } + + pub async fn session_guard( + &'_ self, + ) -> Result), Session>> { + let guard = self.0.lock().await; + if let Err(ref err) = guard.1 { + return Err(err.clone()); + } + Ok(FutureMutexGuard::map(guard, |status| { + // SAFETY: we checked if it's an Err() in the previous line, but we cannot do it + // in here since it's a closure. So unwrap unchecked for API + // convenience. + unsafe { status.1.as_mut().unwrap_unchecked() } + })) + } +} + #[derive(Debug)] pub struct Store { pub account_name: Arc, pub account_hash: AccountHash, pub main_identity: String, pub extra_identities: Vec, - pub account_id: Arc>>, - pub byte_cache: Arc>>, - pub id_store: Arc>>>, - pub reverse_id_store: Arc, EnvelopeHash>>>, - pub blob_id_store: Arc>>>, + pub byte_cache: Arc>>, + pub id_store: Arc>>>, + pub reverse_id_store: Arc, EnvelopeHash>>>, + pub blob_id_store: Arc>>>, pub collection: Collection, pub mailboxes: Arc>>, pub mailboxes_index: Arc>>>, - pub mailbox_state: Arc>>, - pub online_status: Arc)>>, + pub mailbox_state: Arc>>, + pub online_status: OnlineStatus, pub is_subscribed: Arc, pub core_capabilities: Arc>>, pub event_consumer: BackendEventConsumer, } impl Store { - pub fn add_envelope(&self, obj: EmailObject) -> Envelope { + pub async fn add_envelope(&self, obj: EmailObject) -> Envelope { let mut flags = Flag::default(); let mut labels: IndexSet = IndexSet::new(); - let mut tag_lck = self.collection.tag_index.write().unwrap(); - for t in obj.keywords().keys() { - match t.as_str() { - "$draft" => { - flags |= Flag::DRAFT; - } - "$seen" => { - flags |= Flag::SEEN; - } - "$flagged" => { - flags |= Flag::FLAGGED; - } - "$answered" => { - flags |= Flag::REPLIED; - } - "$junk" | "$notjunk" => { /* ignore */ } - _ => { - let tag_hash = TagHash::from_bytes(t.as_bytes()); - tag_lck.entry(tag_hash).or_insert_with(|| t.to_string()); - labels.insert(tag_hash); + let id; + let mailbox_ids; + let blob_id; + { + let mut tag_lck = self.collection.tag_index.write().unwrap(); + for t in obj.keywords().keys() { + match t.as_str() { + "$draft" => { + flags |= Flag::DRAFT; + } + "$seen" => { + flags |= Flag::SEEN; + } + "$flagged" => { + flags |= Flag::FLAGGED; + } + "$answered" => { + flags |= Flag::REPLIED; + } + "$junk" | "$notjunk" => { /* ignore */ } + _ => { + let tag_hash = TagHash::from_bytes(t.as_bytes()); + tag_lck.entry(tag_hash).or_insert_with(|| t.to_string()); + labels.insert(tag_hash); + } } } - } - let id = obj.id.clone(); - let mailbox_ids = obj.mailbox_ids.clone(); - let blob_id = obj.blob_id.clone(); - drop(tag_lck); + id = obj.id.clone(); + mailbox_ids = obj.mailbox_ids.clone(); + blob_id = obj.blob_id.clone(); + } let mut ret: Envelope = obj.into(); ret.set_flags(flags); ret.tags_mut().extend(labels); - let mut id_store_lck = self.id_store.lock().unwrap(); - let mut reverse_id_store_lck = self.reverse_id_store.lock().unwrap(); - let mut blob_id_store_lck = self.blob_id_store.lock().unwrap(); + let mut id_store_lck = self.id_store.lock().await; + let mut reverse_id_store_lck = self.reverse_id_store.lock().await; + let mut blob_id_store_lck = self.blob_id_store.lock().await; let mailboxes_lck = self.mailboxes.read().unwrap(); let mut mailboxes_index_lck = self.mailboxes_index.write().unwrap(); for (mailbox_id, _) in mailbox_ids { @@ -262,14 +333,14 @@ impl Store { ret } - pub fn remove_envelope( + pub async fn remove_envelope( &self, obj_id: Id, ) -> Option<(EnvelopeHash, SmallVec<[MailboxHash; 8]>)> { - let env_hash = self.reverse_id_store.lock().unwrap().remove(&obj_id)?; - self.id_store.lock().unwrap().remove(&env_hash); - self.blob_id_store.lock().unwrap().remove(&env_hash); - self.byte_cache.lock().unwrap().remove(&env_hash); + let env_hash = self.reverse_id_store.lock().await.remove(&obj_id)?; + self.id_store.lock().await.remove(&env_hash); + self.blob_id_store.lock().await.remove(&env_hash); + self.byte_cache.lock().await.remove(&env_hash); let mut mailbox_hashes = SmallVec::new(); { let mut mailboxes_lck = self.mailboxes_index.write().unwrap(); @@ -318,14 +389,9 @@ impl MailBackend for JmapType { let connection = self.connection.clone(); let timeout_dur = self.server_conf.timeout; Ok(Box::pin(async move { - match timeout(timeout_dur, connection.lock()).await { - Ok(_conn) => match timeout(timeout_dur, online.lock()).await { - Err(err) => Err(err), - Ok(lck) if lck.1.is_err() => lck.1.clone(), - _ => Ok(()), - }, - Err(err) => Err(err), - } + let _conn = timeout(timeout_dur, connection.lock()).await?; + let _session = timeout(timeout_dur, online.session_guard()).await??; + Ok(()) })) } @@ -440,13 +506,13 @@ impl MailBackend for JmapType { * 1. upload binary blob, get blobId * 2. Email/import */ - let upload_url = { conn.session.lock().unwrap().upload_url.clone() }; + let (upload_url, mail_account_id) = { + let g = conn.session_guard().await?; + (g.upload_url.clone(), g.mail_account_id()) + }; let mut res = conn .post_async( - Some(&upload_request_format( - upload_url.as_str(), - &conn.mail_account_id(), - )), + Some(&upload_request_format(&upload_url, &mail_account_id)?), bytes, ) .await?; @@ -466,7 +532,7 @@ impl MailBackend for JmapType { let upload_response: UploadResponse = match deserialize_from_str(&res_text) { Err(err) => { - *conn.store.online_status.lock().await = (Instant::now(), Err(err.clone())); + _ = conn.store.online_status.set(None, Err(err.clone())).await; return Err(err); } Ok(s) => s, @@ -474,23 +540,24 @@ impl MailBackend for JmapType { let mut req = Request::new(conn.request_no.clone()); let creation_id: Id = "1".to_string().into(); - let import_call: EmailImport = EmailImport::new() - .account_id(conn.mail_account_id()) - .emails(indexmap! { - creation_id.clone() => EmailImportObject::new() - .blob_id(upload_response.blob_id) - .mailbox_ids(indexmap! { - mailbox_id => true - }) - }); + let import_call: EmailImport = + EmailImport::new() + .account_id(mail_account_id) + .emails(indexmap! { + creation_id.clone() => EmailImportObject::new() + .blob_id(upload_response.blob_id) + .mailbox_ids(indexmap! { + mailbox_id => true + }) + }); - req.add_call(&import_call); + req.add_call(&import_call).await; let mut res = conn.post_async(None, serde_json::to_string(&req)?).await?; let res_text = res.text().await?; let mut v: MethodResponse = match deserialize_from_str(&res_text) { Err(err) => { - *conn.store.online_status.lock().await = (Instant::now(), Err(err.clone())); + _ = conn.store.online_status.set(None, Err(err.clone())).await; return Err(err); } Ok(s) => s, @@ -549,28 +616,29 @@ impl MailBackend for JmapType { Ok(Box::pin(async move { let mut conn = connection.lock().await; conn.connect().await?; + let mail_account_id = conn.session_guard().await?.mail_account_id(); let email_call: EmailQuery = EmailQuery::new( Query::new() - .account_id(conn.mail_account_id()) + .account_id(mail_account_id) .filter(Some(filter)) .position(0), ) .collapse_threads(false); let mut req = Request::new(conn.request_no.clone()); - req.add_call(&email_call); + req.add_call(&email_call).await; let mut res = conn.post_async(None, serde_json::to_string(&req)?).await?; let res_text = res.text().await?; let mut v: MethodResponse = match deserialize_from_str(&res_text) { Err(err) => { - *conn.store.online_status.lock().await = (Instant::now(), Err(err.clone())); + _ = conn.store.online_status.set(None, Err(err.clone())).await; return Err(err); } Ok(s) => s, }; - *store.online_status.lock().await = (std::time::Instant::now(), Ok(())); + store.online_status.update_timestamp(None).await; let m = QueryResponse::::try_from(v.method_responses.remove(0))?; let QueryResponse:: { ids, .. } = m; let ret = ids.into_iter().map(|id| id.into_hash()).collect(); @@ -596,9 +664,10 @@ impl MailBackend for JmapType { let connection = self.connection.clone(); Ok(Box::pin(async move { let mut conn = connection.lock().await; + let mail_account_id = conn.session_guard().await?.mail_account_id(); let mailbox_set_call: MailboxSet = MailboxSet::new( Set::::new() - .account_id(conn.mail_account_id()) + .account_id(mail_account_id) .create(Some({ let id: Id = path.as_str().into(); indexmap! { @@ -612,7 +681,7 @@ impl MailBackend for JmapType { ); let mut req = Request::new(conn.request_no.clone()); - let _prev_seq = req.add_call(&mailbox_set_call); + let _prev_seq = req.add_call(&mailbox_set_call).await; let new_mailboxes = protocol::get_mailboxes(&mut conn, Some(req)).await?; *store.mailboxes.write().unwrap() = new_mailboxes; @@ -707,7 +776,7 @@ impl MailBackend for JmapType { } { for env_hash in env_hashes.iter() { - if let Some(id) = store.id_store.lock().unwrap().get(&env_hash) { + if let Some(id) = store.id_store.lock().await.get(&env_hash) { // ids.push(id.clone()); // id_map.insert(id.clone(), env_hash); update_map.insert( @@ -718,15 +787,16 @@ impl MailBackend for JmapType { } } let conn = connection.lock().await; + let mail_account_id = conn.session_guard().await?.mail_account_id(); let email_set_call: EmailSet = EmailSet::new( Set::::new() - .account_id(conn.mail_account_id()) + .account_id(mail_account_id) .update(Some(update_map)), ); let mut req = Request::new(conn.request_no.clone()); - let _prev_seq = req.add_call(&email_set_call); + let _prev_seq = req.add_call(&email_set_call).await; let mut res = conn.post_async(None, serde_json::to_string(&req)?).await?; @@ -734,12 +804,12 @@ impl MailBackend for JmapType { let mut v: MethodResponse = match deserialize_from_str(&res_text) { Err(err) => { - *conn.store.online_status.lock().await = (Instant::now(), Err(err.clone())); + _ = conn.store.online_status.set(None, Err(err.clone())).await; return Err(err); } Ok(s) => s, }; - *store.online_status.lock().await = (std::time::Instant::now(), Ok(())); + store.online_status.update_timestamp(None).await; let m = SetResponse::::try_from(v.method_responses.remove(0))?; if let Some(ids) = m.not_updated { if !ids.is_empty() { @@ -815,7 +885,7 @@ impl MailBackend for JmapType { } { for hash in env_hashes.iter() { - if let Some(id) = store.id_store.lock().unwrap().get(&hash) { + if let Some(id) = store.id_store.lock().await.get(&hash) { ids.push(id.clone()); id_map.insert(id.clone(), hash); update_map.insert( @@ -826,23 +896,24 @@ impl MailBackend for JmapType { } } let conn = connection.lock().await; + let mail_account_id = conn.session_guard().await?.mail_account_id(); let email_set_call: EmailSet = EmailSet::new( Set::::new() - .account_id(conn.mail_account_id()) + .account_id(mail_account_id.clone()) .update(Some(update_map)), ); let mut req = Request::new(conn.request_no.clone()); - req.add_call(&email_set_call); + req.add_call(&email_set_call).await; let email_call: EmailGet = EmailGet::new( Get::new() .ids(Some(Argument::Value(ids))) - .account_id(conn.mail_account_id()) + .account_id(mail_account_id) .properties(Some(vec!["keywords".to_string()])), ); - req.add_call(&email_call); + req.add_call(&email_call).await; let mut res = conn.post_async(None, serde_json::to_string(&req)?).await?; @@ -857,12 +928,12 @@ impl MailBackend for JmapType { //debug!("res_text = {}", &res_text); let mut v: MethodResponse = match deserialize_from_str(&res_text) { Err(err) => { - *conn.store.online_status.lock().await = (Instant::now(), Err(err.clone())); + _ = conn.store.online_status.set(None, Err(err.clone())).await; return Err(err); } Ok(s) => s, }; - *store.online_status.lock().await = (std::time::Instant::now(), Ok(())); + store.online_status.update_timestamp(None).await; let m = SetResponse::::try_from(v.method_responses.remove(0))?; if let Some(ids) = m.not_updated { return Err(Error::new( @@ -992,19 +1063,18 @@ impl MailBackend for JmapType { }) }; let conn = connection.lock().await; + let mail_account_id = conn.session_guard().await?.mail_account_id(); + // [ref:TODO] smarter identity detection based on From: ? - let Some(identity_id) = conn.mail_identity_id() else { + let Some(identity_id) = conn.session_guard().await?.mail_identity_id() else { return Err(Error::new( "You need to setup an Identity in the JMAP server.", )); }; - let upload_url = { conn.session.lock().unwrap().upload_url.clone() }; + let upload_url = { conn.session_guard().await?.upload_url.clone() }; let mut res = conn .post_async( - Some(&upload_request_format( - upload_url.as_str(), - &conn.mail_account_id(), - )), + Some(&upload_request_format(&upload_url, &mail_account_id)?), bytes, ) .await?; @@ -1012,7 +1082,7 @@ impl MailBackend for JmapType { let upload_response: UploadResponse = match deserialize_from_str(&res_text) { Err(err) => { - *conn.store.online_status.lock().await = (Instant::now(), Err(err.clone())); + _ = conn.store.online_status.set(None, Err(err.clone())).await; return Err(err); } Ok(s) => s, @@ -1021,7 +1091,7 @@ impl MailBackend for JmapType { let mut req = Request::new(conn.request_no.clone()); let creation_id: Id = "newid".into(); let import_call: EmailImport = EmailImport::new() - .account_id(conn.mail_account_id()) + .account_id(mail_account_id.clone()) .emails(indexmap! { creation_id => EmailImportObject::new() .blob_id(upload_response.blob_id) @@ -1034,13 +1104,13 @@ impl MailBackend for JmapType { }), }); - req.add_call(&import_call); + req.add_call(&import_call).await; let mut res = conn.post_async(None, serde_json::to_string(&req)?).await?; let res_text = res.text().await?; let v: MethodResponse = match deserialize_from_str(&res_text) { Err(err) => { - *conn.store.online_status.lock().await = (Instant::now(), Err(err.clone())); + _ = conn.store.online_status.set(None, Err(err.clone())).await; return Err(err); } Ok(s) => s, @@ -1061,10 +1131,10 @@ impl MailBackend for JmapType { let mut req = Request::new(conn.request_no.clone()); let subm_set_call: EmailSubmissionSet = EmailSubmissionSet::new( Set::::new() - .account_id(conn.mail_account_id()) + .account_id(mail_account_id.clone()) .create(Some(indexmap! { Argument::from(Id::from("k1490")) => EmailSubmissionObject::new( - /* account_id: */ conn.mail_account_id(), + /* account_id: */ mail_account_id, /* identity_id: */ identity_id, /* email_id: */ email_id, /* envelope: */ None, @@ -1080,15 +1150,15 @@ impl MailBackend for JmapType { }) })); - req.add_call(&subm_set_call); - let mut res = conn.post_async(None, serde_json::to_string(&req)?).await?; + req.add_call(&subm_set_call).await; + let mut res = conn.post_async(None, serde_json::to_string(&req)?).await?; let res_text = res.text().await?; // [ref:TODO] parse/return any error. let _: MethodResponse = match deserialize_from_str(&res_text) { Err(err) => { - *conn.store.online_status.lock().await = (Instant::now(), Err(err.clone())); + _ = conn.store.online_status.set(None, Err(err.clone())).await; return Err(err); } Ok(s) => s, @@ -1106,10 +1176,10 @@ impl JmapType { is_subscribed: Box bool + Send + Sync>, event_consumer: BackendEventConsumer, ) -> Result> { - let online_status = Arc::new(FutureMutex::new(( + let online_status = OnlineStatus(Arc::new(FutureMutex::new(( std::time::Instant::now(), Err(Error::new("Account is uninitialised.")), - ))); + )))); let server_conf = JmapServerConf::new(s)?; let account_hash = AccountHash::from_bytes(s.name.as_bytes()); @@ -1118,7 +1188,6 @@ impl JmapType { account_hash, main_identity: s.make_display_name(), extra_identities: s.extra_identities.clone(), - account_id: Arc::new(Mutex::new(Id::empty())), online_status, event_consumer, is_subscribed: Arc::new(IsSubscribedFn(is_subscribed)), @@ -1154,6 +1223,19 @@ impl JmapType { )) }) }; + ($s:ident[$var:literal], $t:ty) => { + get_conf_val!($s[$var]).and_then(|v| { + <$t>::from_str(&v).map_err(|e| { + Error::new(format!( + "Configuration error ({}): Invalid value for field `{}`: {}\n{}", + $s.name.as_str(), + $var, + v, + e + )) + }) + }) + }; ($s:ident[$var:literal], $default:expr) => { $s.extra .remove($var) @@ -1171,7 +1253,7 @@ impl JmapType { .unwrap_or_else(|| Ok($default)) }; } - get_conf_val!(s["server_url"])?; + get_conf_val!(s["server_url"], Url)?; get_conf_val!(s["server_username"])?; get_conf_val!(s["use_token"], false)?; diff --git a/melib/src/jmap/objects.rs b/melib/src/jmap/objects.rs index 3e9635f8..4278153f 100644 --- a/melib/src/jmap/objects.rs +++ b/melib/src/jmap/objects.rs @@ -35,3 +35,6 @@ pub use identity::*; mod submission; pub use submission::*; + +#[cfg(test)] +mod tests; diff --git a/melib/src/jmap/objects/email.rs b/melib/src/jmap/objects/email.rs index bfbf72b9..cb6341c7 100644 --- a/melib/src/jmap/objects/email.rs +++ b/melib/src/jmap/objects/email.rs @@ -747,49 +747,6 @@ impl From for Filter { } } -#[test] -fn test_jmap_query() { - use std::sync::{Arc, Mutex}; - let q: crate::search::Query = crate::search::Query::try_from( - "subject:wah or (from:Manos and (subject:foo or subject:bar))", - ) - .unwrap(); - let f: Filter = Filter::from(q); - assert_eq!( - r#"{"operator":"OR","conditions":[{"subject":"wah"},{"operator":"AND","conditions":[{"from":"Manos"},{"operator":"OR","conditions":[{"subject":"foo"},{"subject":"bar"}]}]}]}"#, - serde_json::to_string(&f).unwrap().as_str() - ); - let filter = { - let mailbox_id = "mailbox_id".to_string(); - - let mut r = Filter::Condition( - EmailFilterCondition::new() - .in_mailbox(Some(mailbox_id.into())) - .into(), - ); - r &= f; - r - }; - - let email_call: EmailQuery = EmailQuery::new( - Query::new() - .account_id("account_id".to_string().into()) - .filter(Some(filter)) - .position(0), - ) - .collapse_threads(false); - - let request_no = Arc::new(Mutex::new(0)); - let mut req = Request::new(request_no.clone()); - req.add_call(&email_call); - - assert_eq!( - r#"{"using":["urn:ietf:params:jmap:core","urn:ietf:params:jmap:mail"],"methodCalls":[["Email/query",{"accountId":"account_id","calculateTotal":false,"collapseThreads":false,"filter":{"conditions":[{"inMailbox":"mailbox_id"},{"conditions":[{"subject":"wah"},{"conditions":[{"from":"Manos"},{"conditions":[{"subject":"foo"},{"subject":"bar"}],"operator":"OR"}],"operator":"AND"}],"operator":"OR"}],"operator":"AND"},"position":0,"sort":null},"m0"]]}"#, - serde_json::to_string(&req).unwrap().as_str() - ); - assert_eq!(*request_no.lock().unwrap(), 1); -} - #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct EmailSet { diff --git a/melib/src/jmap/objects/identity.rs b/melib/src/jmap/objects/identity.rs index cf9bc720..60ca953d 100644 --- a/melib/src/jmap/objects/identity.rs +++ b/melib/src/jmap/objects/identity.rs @@ -114,81 +114,3 @@ pub struct IdentitySet(pub Set); impl Method for IdentitySet { const NAME: &'static str = "Identity/set"; } - -#[cfg(test)] -mod tests { - use std::sync::{Arc, Mutex}; - - use serde_json::json; - - use crate::jmap::*; - - #[test] - fn test_jmap_identity_methods() { - let account_id = "blahblah"; - let prev_seq = 33; - let main_identity = "user@example.com"; - let mut req = Request::new(Arc::new(Mutex::new(prev_seq))); - - let identity_set = IdentitySet( - Set::::new() - .account_id(account_id.into()) - .create(Some({ - let id: Id = main_identity.into(); - let address = crate::email::Address::try_from(main_identity) - .unwrap_or_else(|_| crate::email::Address::new(None, main_identity.into())); - indexmap! { - id.clone().into() => IdentityObject { - id, - name: address.get_display_name().unwrap_or_default(), - email: address.get_email(), - ..IdentityObject::default() - } - } - })), - ); - req.add_call(&identity_set); - - let identity_get = IdentityGet::new().account_id(account_id.into()); - req.add_call(&identity_get); - - assert_eq!( - json! {&req}, - json! {{ - "methodCalls" : [ - [ - "Identity/set", - { - "accountId" : account_id, - "create" : { - "user@example.com" : { - "bcc" : null, - "email" : main_identity, - "htmlSignature" : "", - "name" : "", - "replyTo" : null, - "textSignature" : "" - } - }, - "destroy" : null, - "ifInState" : null, - "update" : null - }, - "m33" - ], - [ - "Identity/get", - { - "accountId": account_id - }, - "m34" - ] - ], - "using" : [ - "urn:ietf:params:jmap:core", - "urn:ietf:params:jmap:mail" - ] - }}, - ); - } -} diff --git a/melib/src/jmap/objects/submission.rs b/melib/src/jmap/objects/submission.rs index b6fb28c5..50a4c03c 100644 --- a/melib/src/jmap/objects/submission.rs +++ b/melib/src/jmap/objects/submission.rs @@ -385,115 +385,3 @@ pub struct Address { /// applied. pub parameters: Option, } - -#[cfg(test)] -mod tests { - use serde_json::json; - - use super::*; - - #[test] - fn test_jmap_undo_status() { - let account_id: Id = "blahblah".into(); - let ident_id: Id = "sdusssssss".into(); - let email_id: Id = Id::from("683f9246-56d4-4d7d-bd0c-3d4de6db7cbf"); - let mut obj = EmailSubmissionObject::new( - account_id, - ident_id.clone(), - email_id.clone(), - None, - /* undo_status */ None, - ); - - assert_eq!( - json!(&obj), - json!({ - "emailId": email_id, - "envelope": null, - "identityId": &ident_id, - "undoStatus": "final", - }) - ); - - obj.undo_status = UndoStatus::Pending; - assert_eq!( - json!(&obj), - json!({ - "emailId": email_id, - "envelope": null, - "identityId": &ident_id, - "undoStatus": "pending", - }) - ); - obj.undo_status = UndoStatus::Final; - assert_eq!( - json!(&obj), - json!({ - "emailId": email_id, - "envelope": null, - "identityId": &ident_id, - "undoStatus": "final", - }) - ); - obj.undo_status = UndoStatus::Canceled; - assert_eq!( - json!(&obj), - json!({ - "emailId": email_id, - "envelope": null, - "identityId": &ident_id, - "undoStatus": "canceled", - }) - ); - } - - #[test] - fn test_jmap_email_submission_object() { - let account_id: Id = "blahblah".into(); - let ident_id: Id = "sdusssssss".into(); - let email_id: Id = Id::from("683f9246-56d4-4d7d-bd0c-3d4de6db7cbf"); - let obj = EmailSubmissionObject::new( - account_id.clone(), - ident_id.clone(), - email_id.clone(), - None, - /* undo_status */ None, - ); - - assert_eq!( - json!(&obj), - json!({ - "emailId": email_id, - "envelope": null, - "identityId": &ident_id, - "undoStatus": "final", - }) - ); - - let obj = EmailSubmissionObject::new( - account_id, - ident_id.clone(), - /* email_id: */ - Argument::reference::( - 42, - ResultField::::new("/id"), - ), - None, - Some(UndoStatus::Final), - ); - - assert_eq!( - json!(&obj), - json!({ - "#emailId": { - "name": "Email/import", - "path": "/id", - "resultOf": "m42", - }, - "envelope": null, - "identityId": &ident_id, - "undoStatus": "final", - }) - ); - } -} diff --git a/melib/src/jmap/objects/tests.rs b/melib/src/jmap/objects/tests.rs new file mode 100644 index 00000000..c22840fc --- /dev/null +++ b/melib/src/jmap/objects/tests.rs @@ -0,0 +1,241 @@ +// +// melib - jmap module. +// +// Copyright 2024 Emmanouil Pitsidianakis +// +// This file is part of meli. +// +// meli is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// meli is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with meli. If not, see . +// +// SPDX-License-Identifier: EUPL-1.2 OR GPL-3.0-or-later + +use serde_json::json; + +use super::*; + +#[test] +fn test_jmap_query() { + let q: crate::search::Query = crate::search::Query::try_from( + "subject:wah or (from:Manos and (subject:foo or subject:bar))", + ) + .unwrap(); + let f: Filter = Filter::from(q); + assert_eq!( + r#"{"operator":"OR","conditions":[{"subject":"wah"},{"operator":"AND","conditions":[{"from":"Manos"},{"operator":"OR","conditions":[{"subject":"foo"},{"subject":"bar"}]}]}]}"#, + serde_json::to_string(&f).unwrap().as_str() + ); + let filter = { + let mailbox_id = "mailbox_id".to_string(); + + let mut r = Filter::Condition( + EmailFilterCondition::new() + .in_mailbox(Some(mailbox_id.into())) + .into(), + ); + r &= f; + r + }; + + let email_call: EmailQuery = EmailQuery::new( + Query::new() + .account_id("account_id".to_string().into()) + .filter(Some(filter)) + .position(0), + ) + .collapse_threads(false); + + let request_no = Arc::new(FutureMutex::new(0)); + let mut req = Request::new(request_no.clone()); + futures::executor::block_on(req.add_call(&email_call)); + + assert_eq!( + r#"{"using":["urn:ietf:params:jmap:core","urn:ietf:params:jmap:mail"],"methodCalls":[["Email/query",{"accountId":"account_id","calculateTotal":false,"collapseThreads":false,"filter":{"conditions":[{"inMailbox":"mailbox_id"},{"conditions":[{"subject":"wah"},{"conditions":[{"from":"Manos"},{"conditions":[{"subject":"foo"},{"subject":"bar"}],"operator":"OR"}],"operator":"AND"}],"operator":"OR"}],"operator":"AND"},"position":0,"sort":null},"m0"]]}"#, + serde_json::to_string(&req).unwrap().as_str() + ); + assert_eq!(*futures::executor::block_on(request_no.lock()), 1); +} + +#[test] +fn test_jmap_undo_status() { + let account_id: Id = "blahblah".into(); + let ident_id: Id = "sdusssssss".into(); + let email_id: Id = Id::from("683f9246-56d4-4d7d-bd0c-3d4de6db7cbf"); + let mut obj = EmailSubmissionObject::new( + account_id, + ident_id.clone(), + email_id.clone(), + None, + /* undo_status */ None, + ); + + assert_eq!( + json!(&obj), + json!({ + "emailId": email_id, + "envelope": null, + "identityId": &ident_id, + "undoStatus": "final", + }) + ); + + obj.undo_status = UndoStatus::Pending; + assert_eq!( + json!(&obj), + json!({ + "emailId": email_id, + "envelope": null, + "identityId": &ident_id, + "undoStatus": "pending", + }) + ); + obj.undo_status = UndoStatus::Final; + assert_eq!( + json!(&obj), + json!({ + "emailId": email_id, + "envelope": null, + "identityId": &ident_id, + "undoStatus": "final", + }) + ); + obj.undo_status = UndoStatus::Canceled; + assert_eq!( + json!(&obj), + json!({ + "emailId": email_id, + "envelope": null, + "identityId": &ident_id, + "undoStatus": "canceled", + }) + ); +} + +#[test] +fn test_jmap_email_submission_object() { + let account_id: Id = "blahblah".into(); + let ident_id: Id = "sdusssssss".into(); + let email_id: Id = Id::from("683f9246-56d4-4d7d-bd0c-3d4de6db7cbf"); + let obj = EmailSubmissionObject::new( + account_id.clone(), + ident_id.clone(), + email_id.clone(), + None, + /* undo_status */ None, + ); + + assert_eq!( + json!(&obj), + json!({ + "emailId": email_id, + "envelope": null, + "identityId": &ident_id, + "undoStatus": "final", + }) + ); + + let obj = EmailSubmissionObject::new( + account_id, + ident_id.clone(), + /* email_id: */ + Argument::reference::( + 42, + ResultField::::new("/id"), + ), + None, + Some(UndoStatus::Final), + ); + + assert_eq!( + json!(&obj), + json!({ + "#emailId": { + "name": "Email/import", + "path": "/id", + "resultOf": "m42", + }, + "envelope": null, + "identityId": &ident_id, + "undoStatus": "final", + }) + ); +} + +#[test] +fn test_jmap_identity_methods() { + let account_id = "blahblah"; + let prev_seq = 33; + let main_identity = "user@example.com"; + let mut req = Request::new(Arc::new(FutureMutex::new(prev_seq))); + + let identity_set = IdentitySet( + Set::::new() + .account_id(account_id.into()) + .create(Some({ + let id: Id = main_identity.into(); + let address = crate::email::Address::try_from(main_identity) + .unwrap_or_else(|_| crate::email::Address::new(None, main_identity.into())); + indexmap! { + id.clone().into() => IdentityObject { + id, + name: address.get_display_name().unwrap_or_default(), + email: address.get_email(), + ..IdentityObject::default() + } + } + })), + ); + futures::executor::block_on(req.add_call(&identity_set)); + + let identity_get = IdentityGet::new().account_id(account_id.into()); + futures::executor::block_on(req.add_call(&identity_get)); + + assert_eq!( + json! {&req}, + json! {{ + "methodCalls" : [ + [ + "Identity/set", + { + "accountId" : account_id, + "create" : { + "user@example.com" : { + "bcc" : null, + "email" : main_identity, + "htmlSignature" : "", + "name" : "", + "replyTo" : null, + "textSignature" : "" + } + }, + "destroy" : null, + "ifInState" : null, + "update" : null + }, + "m33" + ], + [ + "Identity/get", + { + "accountId": account_id + }, + "m34" + ] + ], + "using" : [ + "urn:ietf:params:jmap:core", + "urn:ietf:params:jmap:mail" + ] + }}, + ); +} diff --git a/melib/src/jmap/operations.rs b/melib/src/jmap/operations.rs index e9076013..6faf12fc 100644 --- a/melib/src/jmap/operations.rs +++ b/melib/src/jmap/operations.rs @@ -47,38 +47,35 @@ impl JmapOp { impl BackendOp for JmapOp { fn as_bytes(&mut self) -> ResultFuture> { - { - let byte_lck = self.store.byte_cache.lock().unwrap(); - if let Some(Some(ret)) = byte_lck.get(&self.hash).map(|c| c.bytes.clone()) { - return Ok(Box::pin(async move { Ok(ret.into_bytes()) })); - } - } let store = self.store.clone(); let hash = self.hash; let connection = self.connection.clone(); Ok(Box::pin(async move { - let blob_id = store.blob_id_store.lock().unwrap()[&hash].clone(); + { + let byte_lck = store.byte_cache.lock().await; + if let Some(Some(ret)) = byte_lck.get(&hash).map(|c| c.bytes.clone()) { + return Ok(ret.into_bytes()); + } + } + let blob_id = store.blob_id_store.lock().await[&hash].clone(); let mut conn = connection.lock().await; conn.connect().await?; - let download_url = conn.session.lock().unwrap().download_url.clone(); + let (download_url, mail_account_id) = { + let g = store.online_status.session_guard().await?; + (g.download_url.clone(), g.mail_account_id()) + }; let mut res = conn .get_async(&download_request_format( - download_url.as_str(), - &conn.mail_account_id(), + &download_url, + &mail_account_id, &blob_id, None, - )) + )?) .await?; let res_text = res.text().await?; - store - .byte_cache - .lock() - .unwrap() - .entry(hash) - .or_default() - .bytes = Some(res_text.clone()); + store.byte_cache.lock().await.entry(hash).or_default().bytes = Some(res_text.clone()); Ok(res_text.into_bytes()) })) } diff --git a/melib/src/jmap/protocol.rs b/melib/src/jmap/protocol.rs index 2ea1ee10..6d7aa303 100644 --- a/melib/src/jmap/protocol.rs +++ b/melib/src/jmap/protocol.rs @@ -30,20 +30,11 @@ pub type UtcDate = String; use super::rfc8620::{Object, State}; -macro_rules! get_request_no { - ($lock:expr) => {{ - let mut lck = $lock.lock().unwrap(); - let ret = *lck; - *lck += 1; - ret - }}; -} - -pub trait Response { +pub trait Response: Send + Sync { const NAME: &'static str; } -pub trait Method: Serialize { +pub trait Method: Serialize + Send + Sync { const NAME: &'static str; } @@ -58,11 +49,21 @@ pub struct Request { method_calls: Vec, #[serde(skip)] - request_no: Arc>, + request_no: Arc>, +} + +macro_rules! get_request_no { + ($lock:expr) => {{ + let mut lck = $lock.lock().await; + let ret = *lck; + *lck += 1; + drop(lck); + ret + }}; } impl Request { - pub fn new(request_no: Arc>) -> Self { + pub fn new(request_no: Arc>) -> Self { Self { using: USING, method_calls: Vec::new(), @@ -70,12 +71,20 @@ impl Request { } } - pub fn add_call, O: Object>(&mut self, call: &M) -> usize { + pub async fn add_call, O: Object>(&mut self, call: &M) -> usize { let seq = get_request_no!(self.request_no); self.method_calls .push(serde_json::to_value((M::NAME, call, &format!("m{}", seq))).unwrap()); seq } + + pub fn request_no(&self) -> Arc> { + self.request_no.clone() + } + + pub async fn request_no_value(&self) -> usize { + get_request_no!(self.request_no) + } } pub async fn get_mailboxes( @@ -83,13 +92,14 @@ pub async fn get_mailboxes( request: Option, ) -> Result> { let mut req = request.unwrap_or_else(|| Request::new(conn.request_no.clone())); + let mail_account_id = conn.session_guard().await?.mail_account_id(); let mailbox_get: MailboxGet = - MailboxGet::new(Get::::new().account_id(conn.mail_account_id())); - req.add_call(&mailbox_get); + MailboxGet::new(Get::::new().account_id(mail_account_id)); + req.add_call(&mailbox_get).await; let res_text = conn.send_request(serde_json::to_string(&req)?).await?; let mut v: MethodResponse = deserialize_from_str(&res_text)?; - *conn.store.online_status.lock().await = (std::time::Instant::now(), Ok(())); + conn.store.online_status.update_timestamp(None).await; let m = GetResponse::::try_from(v.method_responses.remove(0))?; let GetResponse:: { list, account_id, .. @@ -99,14 +109,13 @@ pub async fn get_mailboxes( // `isSubscribed` is false on a mailbox, it should be regarded as // subscribed. let is_personal: bool = { - let session = conn.session_guard(); + let session = conn.session_guard().await?; session .accounts .get(&account_id) .map(|acc| acc.is_personal) .unwrap_or(false) }; - *conn.store.account_id.lock().unwrap() = account_id; let mut ret: HashMap = list .into_iter() .map(|r| { @@ -169,9 +178,10 @@ pub async fn get_message_list( conn: &mut JmapConnection, mailbox: &JmapMailbox, ) -> Result>> { + let mail_account_id = conn.session_guard().await?.mail_account_id(); let email_call: EmailQuery = EmailQuery::new( Query::new() - .account_id(conn.mail_account_id()) + .account_id(mail_account_id) .filter(Some(Filter::Condition( EmailFilterCondition::new() .in_mailbox(Some(mailbox.id.clone())) @@ -182,19 +192,19 @@ pub async fn get_message_list( .collapse_threads(false); let mut req = Request::new(conn.request_no.clone()); - req.add_call(&email_call); + req.add_call(&email_call).await; let mut res = conn.post_async(None, serde_json::to_string(&req)?).await?; let res_text = res.text().await?; let mut v: MethodResponse = match deserialize_from_str(&res_text) { Err(err) => { - *conn.store.online_status.lock().await = (Instant::now(), Err(err.clone())); + _ = conn.store.online_status.set(None, Err(err.clone())).await; return Err(err); } Ok(s) => s, }; - *conn.store.online_status.lock().await = (std::time::Instant::now(), Ok(())); + conn.store.online_status.update_timestamp(None).await; let m = QueryResponse::::try_from(v.method_responses.remove(0))?; let QueryResponse:: { ids, .. } = m; conn.last_method_response = Some(res_text); @@ -285,10 +295,11 @@ impl EmailFetchState { mut position, batch_size, } => { + let mail_account_id = conn.session_guard().await?.mail_account_id(); let mailbox_id = store.mailboxes.read().unwrap()[&mailbox_hash].id.clone(); let email_query_call: EmailQuery = EmailQuery::new( Query::new() - .account_id(conn.mail_account_id().clone()) + .account_id(mail_account_id.clone()) .filter(Some(Filter::Condition( EmailFilterCondition::new() .in_mailbox(Some(mailbox_id)) @@ -300,7 +311,7 @@ impl EmailFetchState { .collapse_threads(false); let mut req = Request::new(conn.request_no.clone()); - let prev_seq = req.add_call(&email_query_call); + let prev_seq = req.add_call(&email_query_call).await; let email_call: EmailGet = EmailGet::new( Get::new() @@ -311,15 +322,14 @@ impl EmailFetchState { >( prev_seq, EmailQuery::RESULT_FIELD_IDS ))) - .account_id(conn.mail_account_id().clone()), + .account_id(mail_account_id), ); - let _prev_seq = req.add_call(&email_call); + let _prev_seq = req.add_call(&email_call).await; let res_text = conn.send_request(serde_json::to_string(&req)?).await?; let mut v: MethodResponse = match deserialize_from_str(&res_text) { Err(err) => { - *conn.store.online_status.lock().await = - (Instant::now(), Err(err.clone())); + _ = conn.store.online_status.set(None, Err(err.clone())).await; return Err(err); } Ok(v) => v, @@ -338,7 +348,7 @@ impl EmailFetchState { let mut unread = BTreeSet::default(); let mut ret = Vec::with_capacity(list.len()); for obj in list { - let env = store.add_envelope(obj); + let env = store.add_envelope(obj).await; total.insert(env.hash()); if !env.is_seen() { unread.insert(env.hash()); diff --git a/melib/src/jmap/rfc8620.rs b/melib/src/jmap/rfc8620.rs index 7e6ad99b..c3d963a0 100644 --- a/melib/src/jmap/rfc8620.rs +++ b/melib/src/jmap/rfc8620.rs @@ -22,6 +22,7 @@ use std::{ hash::{Hash, Hasher}, marker::PhantomData, + sync::Arc, }; use indexmap::IndexMap; @@ -30,8 +31,13 @@ use serde::{ ser::{Serialize, SerializeStruct, Serializer}, }; use serde_json::{value::RawValue, Value}; +use url::Url; -use crate::{email::parser::BytesExt, jmap::session::Session}; +use crate::{ + email::parser::BytesExt, + error::{Error, ErrorKind, Result}, + jmap::{deserialize_from_str, protocol::Method, session::Session}, +}; mod filters; pub use filters::*; @@ -39,14 +45,17 @@ mod comparator; pub use comparator::*; mod argument; pub use argument::*; + +#[cfg(test)] +mod tests; + pub type PatchObject = Value; impl Object for PatchObject { const NAME: &'static str = "PatchObject"; } -use super::{deserialize_from_str, protocol::Method}; -pub trait Object { +pub trait Object: Send + Sync { const NAME: &'static str; } @@ -334,7 +343,7 @@ where } impl Serialize for Get { - fn serialize(&self, serializer: S) -> Result + fn serialize(&self, serializer: S) -> std::result::Result where S: Serializer, { @@ -413,7 +422,7 @@ pub struct GetResponse { impl std::convert::TryFrom<&RawValue> for GetResponse { type Error = crate::error::Error; - fn try_from(t: &RawValue) -> Result { + fn try_from(t: &RawValue) -> Result { let res: (String, Self, String) = deserialize_from_str(t.get())?; assert_eq!(&res.0, &format!("{}/get", OBJ::NAME)); Ok(res.1) @@ -527,7 +536,7 @@ pub struct QueryResponse { impl std::convert::TryFrom<&RawValue> for QueryResponse { type Error = crate::error::Error; - fn try_from(t: &RawValue) -> std::result::Result { + fn try_from(t: &RawValue) -> Result { let res: (String, Self, String) = deserialize_from_str(t.get())?; assert_eq!(&res.0, &format!("{}/query", OBJ::NAME)); Ok(res.1) @@ -657,7 +666,7 @@ pub struct ChangesResponse { impl std::convert::TryFrom<&RawValue> for ChangesResponse { type Error = crate::error::Error; - fn try_from(t: &RawValue) -> std::result::Result { + fn try_from(t: &RawValue) -> Result { let res: (String, Self, String) = deserialize_from_str(t.get())?; assert_eq!(&res.0, &format!("{}/changes", OBJ::NAME)); Ok(res.1) @@ -803,7 +812,7 @@ where } impl Serialize for Set { - fn serialize(&self, serializer: S) -> Result + fn serialize(&self, serializer: S) -> std::result::Result where S: Serializer, { @@ -898,7 +907,7 @@ pub struct SetResponse { impl std::convert::TryFrom<&RawValue> for SetResponse { type Error = crate::error::Error; - fn try_from(t: &RawValue) -> Result { + fn try_from(t: &RawValue) -> Result { let res: (String, Self, String) = deserialize_from_str(t.get())?; assert_eq!(&res.0, &format!("{}/set", OBJ::NAME)); Ok(res.1) @@ -991,59 +1000,84 @@ impl std::fmt::Display for SetError { } pub fn download_request_format( - download_url: &str, + download_url: &Url, account_id: &Id, blob_id: &Id, name: Option, -) -> String { +) -> Result { // https://jmap.fastmail.com/download/{accountId}/{blobId}/{name} let mut ret = String::with_capacity( - download_url.len() + download_url.as_str().len() + blob_id.len() + name.as_ref().map(|n| n.len()).unwrap_or(0) + account_id.len(), ); let mut prev_pos = 0; - while let Some(pos) = download_url.as_bytes()[prev_pos..].find(b"{") { - ret.push_str(&download_url[prev_pos..prev_pos + pos]); + while let Some(pos) = download_url.as_str().as_bytes()[prev_pos..].find(b"{") { + ret.push_str(&download_url.as_str()[prev_pos..prev_pos + pos]); prev_pos += pos; - if download_url[prev_pos..].starts_with("{accountId}") { + if download_url.as_str()[prev_pos..].starts_with("{accountId}") { ret.push_str(account_id.as_str()); prev_pos += "{accountId}".len(); - } else if download_url[prev_pos..].starts_with("{blobId}") { + } else if download_url.as_str()[prev_pos..].starts_with("{blobId}") { ret.push_str(blob_id.as_str()); prev_pos += "{blobId}".len(); - } else if download_url[prev_pos..].starts_with("{name}") { + } else if download_url.as_str()[prev_pos..].starts_with("{name}") { ret.push_str(name.as_deref().unwrap_or("")); prev_pos += "{name}".len(); - } else if download_url[prev_pos..].starts_with("{type}") { + } else if download_url.as_str()[prev_pos..].starts_with("{type}") { ret.push_str("application/octet-stream"); prev_pos += "{name}".len(); } else { - // [ref:FIXME]: return protocol error here log::error!( - "BUG: unknown parameter in download url: {}", - &download_url[prev_pos..] + "BUG: unknown parameter in download_url: {}", + &download_url.as_str()[prev_pos..] ); - break; + return Err(Error::new( + "Could not instantiate URL from JMAP server's URL template value", + ) + .set_details(format!( + "`download_url` template returned by server in session object could not be \ + instantiated with `accountId`:\ndownload_url: {}\naccountId: {}\nblobId: \ + {}\nUnknown parameter found {}\n\nIf you believe these values are correct and \ + should have been accepted, please report it as a bug! Otherwise inform the \ + server administrator for this protocol violation.", + download_url, + account_id, + blob_id, + &download_url.as_str()[prev_pos..] + )) + .set_kind(ErrorKind::ProtocolError)); } } - if prev_pos != download_url.len() { - ret.push_str(&download_url[prev_pos..]); + if prev_pos != download_url.as_str().len() { + ret.push_str(&download_url.as_str()[prev_pos..]); } - ret -} - -pub fn upload_request_format(upload_url: &str, account_id: &Id) -> String { + Url::parse(&ret).map_err(|err| { + Error::new("Could not instantiate URL from JMAP server's URL template value") + .set_details(format!( + "`download_url` template returned by server in session object could not be \ + instantiated with `accountId`:\ndownload_url: {}\naccountId: {}\nblobId: \ + {}\nresult: {ret}\n\nIf you believe these values are correct and should have \ + been accepted, please report it as a bug! Otherwise inform the server \ + administrator for this protocol violation.", + download_url, account_id, blob_id + )) + .set_kind(ErrorKind::ProtocolError) + .set_source(Some(Arc::new(err))) + }) +} + +pub fn upload_request_format(upload_url: &Url, account_id: &Id) -> Result { //"uploadUrl": "https://jmap.fastmail.com/upload/{accountId}/", - let mut ret = String::with_capacity(upload_url.len() + account_id.len()); + let mut ret = String::with_capacity(upload_url.as_str().len() + account_id.len()); let mut prev_pos = 0; - while let Some(pos) = upload_url.as_bytes()[prev_pos..].find(b"{") { - ret.push_str(&upload_url[prev_pos..prev_pos + pos]); + while let Some(pos) = upload_url.as_str().as_bytes()[prev_pos..].find(b"{") { + ret.push_str(&upload_url.as_str()[prev_pos..prev_pos + pos]); prev_pos += pos; - if upload_url[prev_pos..].starts_with("{accountId}") { + if upload_url.as_str()[prev_pos..].starts_with("{accountId}") { ret.push_str(account_id.as_str()); prev_pos += "{accountId}".len(); break; @@ -1052,10 +1086,22 @@ pub fn upload_request_format(upload_url: &str, account_id: &Id) -> Stri prev_pos += 1; } } - if prev_pos != upload_url.len() { - ret.push_str(&upload_url[prev_pos..]); + if prev_pos != upload_url.as_str().len() { + ret.push_str(&upload_url.as_str()[prev_pos..]); } - ret + Url::parse(&ret).map_err(|err| { + Error::new("Could not instantiate URL from JMAP server's URL template value") + .set_details(format!( + "`upload_url` template returned by server in session object could not be \ + instantiated with `accountId`:\nupload_url: {}\naccountId: {}\nresult: \ + {ret}\n\nIf you believe these values are correct and should have been accepted, \ + please report it as a bug! Otherwise inform the server administrator for this \ + protocol violation.", + upload_url, account_id + )) + .set_kind(ErrorKind::ProtocolError) + .set_source(Some(Arc::new(err))) + }) } #[derive(Clone, Debug, Deserialize, Serialize)] diff --git a/melib/src/jmap/rfc8620/argument.rs b/melib/src/jmap/rfc8620/argument.rs index 75f84ef3..1f6d7bd8 100644 --- a/melib/src/jmap/rfc8620/argument.rs +++ b/melib/src/jmap/rfc8620/argument.rs @@ -62,154 +62,3 @@ impl From for Argument { Self::Value(v) } } - -#[cfg(test)] -mod tests { - use std::sync::{Arc, Mutex}; - - use serde_json::json; - - use crate::jmap::*; - - #[test] - fn test_jmap_argument_serde() { - let account_id = "blahblah"; - let blob_id: Id = Id::new_uuid_v4(); - let draft_mailbox_id: Id = Id::new_uuid_v4(); - let sent_mailbox_id: Id = Id::new_uuid_v4(); - let prev_seq = 33; - - let mut req = Request::new(Arc::new(Mutex::new(prev_seq))); - let creation_id: Id = "1".into(); - let import_call: EmailImport = - EmailImport::new() - .account_id(account_id.into()) - .emails(indexmap! { - creation_id => - EmailImportObject::new() - .blob_id(blob_id.clone()) - .keywords(indexmap! { - "$draft".to_string() => true, - }) - .mailbox_ids(indexmap! { - draft_mailbox_id.clone() => true, - }), - }); - - let prev_seq = req.add_call(&import_call); - - let subm_set_call: EmailSubmissionSet = EmailSubmissionSet::new( - Set::::new() - .account_id(account_id.into()) - .create(Some(indexmap! { - Argument::from(Id::from("k1490")) => EmailSubmissionObject::new( - /* account_id: */ account_id.into(), - /* identity_id: */ account_id.into(), - /* email_id: */ Argument::reference::(prev_seq, ResultField::::new("/id")), - /* envelope: */ None, - /* undo_status: */ None - ) - })), - ) - .on_success_update_email(Some( - indexmap! { - "#k1490".into() => json!({ - format!("mailboxIds/{draft_mailbox_id}"): null, - format!("mailboxIds/{sent_mailbox_id}"): true, - "keywords/$draft": null - }) - } - )); - _ = req.add_call(&subm_set_call); - - assert_eq!( - json! {&subm_set_call}, - json! {{ - "accountId": account_id, - "create": { - "k1490": { - "#emailId": { - "name": "Email/import", - "path":"/id", - "resultOf":"m33" - }, - "envelope": null, - "identityId": account_id, - "undoStatus": "final" - } - }, - "destroy": null, - "ifInState": null, - "onSuccessDestroyEmail": null, - "onSuccessUpdateEmail": { - "#k1490": { - "keywords/$draft": null, - format!("mailboxIds/{draft_mailbox_id}"): null, - format!("mailboxIds/{sent_mailbox_id}"): true - } - }, - "update": null, - }}, - ); - assert_eq!( - json! {&req}, - json! {{ - "methodCalls": [ - [ - "Email/import", - { - "accountId": account_id, - "emails": { - "1": { - "blobId": blob_id.to_string(), - "keywords": { - "$draft": true - }, - "mailboxIds": { - draft_mailbox_id.to_string(): true - }, - "receivedAt": null - } - } - }, - "m33" - ], - [ - "EmailSubmission/set", - { - "accountId": account_id, - "create": { - "k1490": { - "#emailId": { - "name": "Email/import", - "path": "/id", - "resultOf": "m33" - }, - "envelope": null, - "identityId": account_id, - "undoStatus": "final" - } - }, - "destroy": null, - "ifInState": null, - "onSuccessDestroyEmail": null, - "onSuccessUpdateEmail": { - "#k1490": { - "keywords/$draft": null, - format!("mailboxIds/{draft_mailbox_id}"): null, - format!("mailboxIds/{sent_mailbox_id}"): true - } - }, - "update": null - }, - "m34" - ] - ], - "using": [ - "urn:ietf:params:jmap:core", - "urn:ietf:params:jmap:mail" - ] - }}, - ); - } -} diff --git a/melib/src/jmap/rfc8620/filters.rs b/melib/src/jmap/rfc8620/filters.rs index e656e94d..2c3027de 100644 --- a/melib/src/jmap/rfc8620/filters.rs +++ b/melib/src/jmap/rfc8620/filters.rs @@ -21,7 +21,8 @@ use super::*; -pub trait FilterTrait: Default {} +pub trait FilterTrait: Default + Send + Sync {} + #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] #[serde(untagged)] diff --git a/melib/src/jmap/rfc8620/tests.rs b/melib/src/jmap/rfc8620/tests.rs new file mode 100644 index 00000000..79790078 --- /dev/null +++ b/melib/src/jmap/rfc8620/tests.rs @@ -0,0 +1,169 @@ +// +// melib - jmap module. +// +// Copyright 2024 Emmanouil Pitsidianakis +// +// This file is part of meli. +// +// meli is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// meli is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with meli. If not, see . +// +// SPDX-License-Identifier: EUPL-1.2 OR GPL-3.0-or-later + +use std::sync::Arc; + +use serde_json::json; + +use crate::jmap::*; + +#[test] +fn test_jmap_argument_serde() { + let account_id = "blahblah"; + let blob_id: Id = Id::new_uuid_v4(); + let draft_mailbox_id: Id = Id::new_uuid_v4(); + let sent_mailbox_id: Id = Id::new_uuid_v4(); + let prev_seq = 33; + + let mut req = Request::new(Arc::new(FutureMutex::new(prev_seq))); + let creation_id: Id = "1".into(); + let import_call: EmailImport = + EmailImport::new() + .account_id(account_id.into()) + .emails(indexmap! { + creation_id => + EmailImportObject::new() + .blob_id(blob_id.clone()) + .keywords(indexmap! { + "$draft".to_string() => true, + }) + .mailbox_ids(indexmap! { + draft_mailbox_id.clone() => true, + }), + }); + + let prev_seq = futures::executor::block_on(req.add_call(&import_call)); + + let subm_set_call: EmailSubmissionSet = EmailSubmissionSet::new( + Set::::new() + .account_id(account_id.into()) + .create(Some(indexmap! { + Argument::from(Id::from("k1490")) => EmailSubmissionObject::new( + /* account_id: */ account_id.into(), + /* identity_id: */ account_id.into(), + /* email_id: */ Argument::reference::(prev_seq, ResultField::::new("/id")), + /* envelope: */ None, + /* undo_status: */ None + ) + })), + ) + .on_success_update_email(Some( + indexmap! { + "#k1490".into() => json!({ + format!("mailboxIds/{draft_mailbox_id}"): null, + format!("mailboxIds/{sent_mailbox_id}"): true, + "keywords/$draft": null + }) + } + )); + _ = futures::executor::block_on(req.add_call(&subm_set_call)); + + assert_eq!( + json! {&subm_set_call}, + json! {{ + "accountId": account_id, + "create": { + "k1490": { + "#emailId": { + "name": "Email/import", + "path":"/id", + "resultOf":"m33" + }, + "envelope": null, + "identityId": account_id, + "undoStatus": "final" + } + }, + "destroy": null, + "ifInState": null, + "onSuccessDestroyEmail": null, + "onSuccessUpdateEmail": { + "#k1490": { + "keywords/$draft": null, + format!("mailboxIds/{draft_mailbox_id}"): null, + format!("mailboxIds/{sent_mailbox_id}"): true + } + }, + "update": null, + }}, + ); + assert_eq!( + json! {&req}, + json! {{ + "methodCalls": [ + [ + "Email/import", + { + "accountId": account_id, + "emails": { + "1": { + "blobId": blob_id.to_string(), + "keywords": { + "$draft": true + }, + "mailboxIds": { + draft_mailbox_id.to_string(): true + }, + "receivedAt": null + } + } + }, + "m33" + ], + [ + "EmailSubmission/set", + { + "accountId": account_id, + "create": { + "k1490": { + "#emailId": { + "name": "Email/import", + "path": "/id", + "resultOf": "m33" + }, + "envelope": null, + "identityId": account_id, + "undoStatus": "final" + } + }, + "destroy": null, + "ifInState": null, + "onSuccessDestroyEmail": null, + "onSuccessUpdateEmail": { + "#k1490": { + "keywords/$draft": null, + format!("mailboxIds/{draft_mailbox_id}"): null, + format!("mailboxIds/{sent_mailbox_id}"): true + } + }, + "update": null + }, + "m34" + ] + ], + "using": [ + "urn:ietf:params:jmap:core", + "urn:ietf:params:jmap:mail" + ] + }}, + ); +} diff --git a/melib/src/jmap/session.rs b/melib/src/jmap/session.rs index 400aace5..ecf8bd76 100644 --- a/melib/src/jmap/session.rs +++ b/melib/src/jmap/session.rs @@ -23,13 +23,14 @@ use std::sync::Arc; use indexmap::IndexMap; use serde_json::Value; +use url::Url; use crate::jmap::{ rfc8620::{Account, Id, Object, State}, - IdentityObject, + IdentityObject, JMAP_MAIL_CAPABILITY, }; -#[derive(Clone, Debug, Default, Deserialize, Serialize)] +#[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct Session { pub capabilities: IndexMap, @@ -38,11 +39,11 @@ pub struct Session { #[serde(skip)] pub identities: IndexMap, IdentityObject>, pub username: String, - pub api_url: Arc, - pub download_url: Arc, + pub api_url: Arc, + pub download_url: Arc, - pub upload_url: Arc, - pub event_source_url: Arc, + pub upload_url: Arc, + pub event_source_url: Arc, pub state: State, #[serde(flatten)] pub extra_properties: IndexMap, @@ -52,6 +53,19 @@ impl Object for Session { const NAME: &'static str = stringify!(Session); } +impl Session { + /// Return the first identity. + pub fn mail_identity_id(&self) -> Option> { + self.identities.keys().next().cloned() + } + + /// Return the account ID corresponding to the [`JMAP_MAIL_CAPABILITY`] + /// capability. + pub fn mail_account_id(&self) -> Id { + self.primary_accounts[JMAP_MAIL_CAPABILITY].clone() + } +} + #[derive(Clone, Debug, Default, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct CapabilitiesObject {