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 {