melib/jmap: Use Url instead of String in deserializing

Catch invalid URLs at the parsing stage.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
pull/353/head
Manos Pitsidianakis 3 months ago
parent 417b24cd84
commit 51e3f163d4
No known key found for this signature in database
GPG Key ID: 7729C7707F7E09D0

2
Cargo.lock generated

@ -1350,6 +1350,7 @@ dependencies = [
"socket2 0.5.5", "socket2 0.5.5",
"stderrlog", "stderrlog",
"unicode-segmentation", "unicode-segmentation",
"url",
"uuid", "uuid",
"xdg", "xdg",
] ]
@ -2457,6 +2458,7 @@ dependencies = [
"form_urlencoded", "form_urlencoded",
"idna", "idna",
"percent-encoding", "percent-encoding",
"serde",
] ]
[[package]] [[package]]

@ -50,8 +50,8 @@ serde_path_to_error = { version = "0.1" }
smallvec = { version = "^1.5.0", features = ["serde"] } smallvec = { version = "^1.5.0", features = ["serde"] }
smol = "1.0.0" smol = "1.0.0"
socket2 = { version = "0.5", features = [] } socket2 = { version = "0.5", features = [] }
unicode-segmentation = { version = "1.2.1", default-features = false, optional = true } unicode-segmentation = { version = "1.2.1", default-features = false, optional = true }
url = { version = "2.4", optional = true }
uuid = { version = "^1", features = ["serde", "v4", "v5"] } uuid = { version = "^1", features = ["serde", "v4", "v5"] }
xdg = "2.1.0" xdg = "2.1.0"
@ -64,7 +64,7 @@ http = ["isahc"]
http-static = ["isahc", "isahc/static-curl"] http-static = ["isahc", "isahc/static-curl"]
imap = ["imap-codec", "tls"] imap = ["imap-codec", "tls"]
imap-trace = ["imap"] imap-trace = ["imap"]
jmap = ["http"] jmap = ["http", "url/serde"]
jmap-trace = ["jmap"] jmap-trace = ["jmap"]
nntp = ["tls"] nntp = ["tls"]
nntp-trace = ["nntp"] nntp-trace = ["nntp"]

@ -19,6 +19,9 @@
* along with meli. If not, see <http://www.gnu.org/licenses/>. * along with meli. If not, see <http://www.gnu.org/licenses/>.
*/ */
// In case we forget to wait some future.
#![deny(unused_must_use)]
use smallvec::SmallVec; use smallvec::SmallVec;
#[macro_use] #[macro_use]
mod protocol_parser; mod protocol_parser;

@ -19,7 +19,7 @@
* along with meli. If not, see <http://www.gnu.org/licenses/>. * along with meli. If not, see <http://www.gnu.org/licenses/>.
*/ */
use std::{convert::TryFrom, sync::MutexGuard}; use std::convert::TryFrom;
use isahc::config::Configurable; use isahc::config::Configurable;
@ -28,8 +28,7 @@ use crate::error::NetworkErrorKind;
#[derive(Debug)] #[derive(Debug)]
pub struct JmapConnection { pub struct JmapConnection {
pub session: Arc<Mutex<Session>>, pub request_no: Arc<FutureMutex<usize>>,
pub request_no: Arc<Mutex<usize>>,
pub client: Arc<HttpClient>, pub client: Arc<HttpClient>,
pub server_conf: JmapServerConf, pub server_conf: JmapServerConf,
pub store: Arc<Store>, pub store: Arc<Store>,
@ -69,8 +68,7 @@ impl JmapConnection {
let client = client.build()?; let client = client.build()?;
let server_conf = server_conf.clone(); let server_conf = server_conf.clone();
Ok(Self { Ok(Self {
session: Arc::new(Mutex::new(Default::default())), request_no: Arc::new(FutureMutex::new(0)),
request_no: Arc::new(Mutex::new(0)),
client: Arc::new(client), client: Arc::new(client),
server_conf, server_conf,
store, store,
@ -79,28 +77,37 @@ impl JmapConnection {
} }
pub async fn connect(&mut self) -> Result<()> { 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(()); return Ok(());
} }
fn to_well_known(uri: &str) -> String { fn to_well_known(uri: &Url) -> Url {
let uri = uri.trim_start_matches('/'); let mut uri = uri.clone();
format!("{uri}/.well-known/jmap") uri.set_path(".well-known/jmap");
uri
} }
let mut jmap_session_resource_url = to_well_known(&self.server_conf.server_url); 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: { 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:// // attempt recovery by trying https://
self.server_conf.server_url = format!( self.server_conf.server_url.set_scheme("https").expect(
"https{}", "set_scheme to https must succeed here because we checked earlier that \
self.server_conf.server_url.trim_start_matches("http") current scheme is http",
); );
jmap_session_resource_url = to_well_known(&self.server_conf.server_url); 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!( log::error!(
"Account {} server URL should start with `https`. Please correct your \ "Account {} server URL should start with `https`. Please correct your \
configuration value. Its current value is `{}`.", configuration value. Its current value is `{}`.",
@ -119,11 +126,12 @@ impl JmapConnection {
&self.server_conf.server_url, &err &self.server_conf.server_url, &err
)) ))
.set_source(Some(Arc::new(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); return Err(err);
} }
Ok(s) => s, Ok(s) => s,
}; };
let req_instant = Instant::now();
if !req.status().is_success() { if !req.status().is_success() {
let kind: crate::error::NetworkErrorKind = req.status().into(); let kind: crate::error::NetworkErrorKind = req.status().into();
@ -133,7 +141,11 @@ impl JmapConnection {
&self.server_conf.server_url, res_text &self.server_conf.server_url, res_text
)) ))
.set_kind(kind.into()); .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); return Err(err);
} }
@ -147,7 +159,11 @@ impl JmapConnection {
&self.server_conf.server_url, &err &self.server_conf.server_url, &err
)) ))
.set_source(Some(Arc::new(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); return Err(err);
} }
Ok(s) => s, Ok(s) => s,
@ -163,7 +179,11 @@ impl JmapConnection {
&self.server_conf.server_url, &res_text &self.server_conf.server_url, &res_text
)) ))
.set_source(Some(Arc::new(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); return Err(err);
} }
Ok(s) => s, Ok(s) => s,
@ -181,7 +201,11 @@ impl JmapConnection {
.join(", "), .join(", "),
core_capability = JMAP_CORE_CAPABILITY 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); return Err(err);
} }
if !session.capabilities.contains_key(JMAP_MAIL_CAPABILITY) { if !session.capabilities.contains_key(JMAP_MAIL_CAPABILITY) {
@ -197,24 +221,37 @@ impl JmapConnection {
.join(", "), .join(", "),
mail_capability = JMAP_MAIL_CAPABILITY 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); return Err(err);
} }
*self.store.core_capabilities.lock().unwrap() = session.capabilities.clone(); *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. */ /* Fetch account identities. */
let mut id_list = { let mut id_list = {
let mut req = Request::new(self.request_no.clone()); let mut req = Request::new(self.request_no.clone());
let identity_get = IdentityGet::new().account_id(self.mail_account_id()); let identity_get = IdentityGet::new().account_id(mail_account_id.clone());
req.add_call(&identity_get); req.add_call(&identity_get).await;
let mut res_text = self.post_async(None, serde_json::to_string(&req)?).await?; let mut res_text = self.post_async(None, serde_json::to_string(&req)?).await?;
let res_text = res_text.text().await?; let res_text = res_text.text().await?;
let mut v: MethodResponse = match deserialize_from_str(&res_text) { let mut v: MethodResponse = match deserialize_from_str(&res_text) {
Err(err) => { 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); return Err(err);
} }
Ok(s) => s, Ok(s) => s,
@ -227,7 +264,7 @@ impl JmapConnection {
let mut req = Request::new(self.request_no.clone()); let mut req = Request::new(self.request_no.clone());
let identity_set = IdentitySet( let identity_set = IdentitySet(
Set::<IdentityObject>::new() Set::<IdentityObject>::new()
.account_id(self.mail_account_id()) .account_id(mail_account_id.clone())
.create(Some({ .create(Some({
let address = let address =
crate::email::Address::try_from(self.store.main_identity.as_str()) 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 mut res_text = self.post_async(None, serde_json::to_string(&req)?).await?;
let res_text = res_text.text().await?; let res_text = res_text.text().await?;
let _: MethodResponse = match deserialize_from_str(&res_text) { let _: MethodResponse = match deserialize_from_str(&res_text) {
Err(err) => { 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); return Err(err);
} }
Ok(s) => s, Ok(s) => s,
}; };
let mut req = Request::new(self.request_no.clone()); let mut req = Request::new(self.request_no.clone());
let identity_get = IdentityGet::new().account_id(self.mail_account_id()); let identity_get = IdentityGet::new().account_id(mail_account_id.clone());
req.add_call(&identity_get); req.add_call(&identity_get).await;
let mut res_text = self.post_async(None, serde_json::to_string(&req)?).await?; let mut res_text = self.post_async(None, serde_json::to_string(&req)?).await?;
let res_text = res_text.text().await?; let res_text = res_text.text().await?;
let mut v: MethodResponse = match deserialize_from_str(&res_text) { let mut v: MethodResponse = match deserialize_from_str(&res_text) {
Err(err) => { 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); return Err(err);
} }
Ok(s) => s, Ok(s) => s,
@ -284,28 +329,17 @@ impl JmapConnection {
GetResponse::<IdentityObject>::try_from(v.method_responses.remove(0))?; GetResponse::<IdentityObject>::try_from(v.method_responses.remove(0))?;
id_list = list; id_list = list;
} }
self.session.lock().unwrap().identities = self.session_guard().await?.identities =
id_list.into_iter().map(|id| (id.id.clone(), id)).collect(); id_list.into_iter().map(|id| (id.id.clone(), id)).collect();
Ok(()) Ok(())
} }
pub fn mail_account_id(&self) -> Id<Account> { #[inline]
self.session.lock().unwrap().primary_accounts[JMAP_MAIL_CAPABILITY].clone() pub async fn session_guard(
} &'_ self,
) -> Result<FutureMappedMutexGuard<'_, (Instant, Result<Session>), Session>> {
pub fn mail_identity_id(&self) -> Option<Id<IdentityObject>> { self.store.online_status.session_guard().await
self.session
.lock()
.unwrap()
.identities
.keys()
.next()
.cloned()
}
pub fn session_guard(&'_ self) -> MutexGuard<'_, Session> {
self.session.lock().unwrap()
} }
pub fn add_refresh_event(&self, event: RefreshEvent) { pub fn add_refresh_event(&self, event: RefreshEvent) {
@ -325,15 +359,16 @@ impl JmapConnection {
} else { } else {
return Ok(()); return Ok(());
}; };
let mail_account_id = self.session_guard().await?.mail_account_id();
loop { loop {
let email_changes_call: EmailChanges = EmailChanges::new( let email_changes_call: EmailChanges = EmailChanges::new(
Changes::<EmailObject>::new() Changes::<EmailObject>::new()
.account_id(self.mail_account_id().clone()) .account_id(mail_account_id.clone())
.since_state(current_state.clone()), .since_state(current_state.clone()),
); );
let mut req = Request::new(self.request_no.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( let email_get_call: EmailGet = EmailGet::new(
Get::new() Get::new()
.ids(Some(Argument::reference::< .ids(Some(Argument::reference::<
@ -344,43 +379,46 @@ impl JmapConnection {
prev_seq, prev_seq,
ResultField::<EmailChanges, EmailObject>::new("/created"), ResultField::<EmailChanges, EmailObject>::new("/created"),
))) )))
.account_id(self.mail_account_id().clone()), .account_id(mail_account_id.clone()),
); );
req.add_call(&email_get_call); req.add_call(&email_get_call).await;
let mailbox_id: Id<MailboxObject>; let mailbox = self
if let Some(mailbox) = self.store.mailboxes.read().unwrap().get(&mailbox_hash) { .store
if let Some(email_query_state) = mailbox.email_query_state.lock().unwrap().clone() { .mailboxes
mailbox_id = mailbox.id.clone(); .read()
let email_query_changes_call = EmailQueryChanges::new( .unwrap()
QueryChanges::new(self.mail_account_id().clone(), email_query_state) .get(&mailbox_hash)
.filter(Some(Filter::Condition( .map(|m| {
EmailFilterCondition::new() let email_query_state = m.email_query_state.lock().unwrap().clone();
.in_mailbox(Some(mailbox_id.clone())) let mailbox_id: Id<MailboxObject> = m.id.clone();
.into(), (email_query_state, mailbox_id)
))), });
); if let Some((Some(email_query_state), mailbox_id)) = mailbox {
let seq_no = req.add_call(&email_query_changes_call); let email_query_changes_call = EmailQueryChanges::new(
let email_get_call: EmailGet = EmailGet::new( QueryChanges::new(mail_account_id.clone(), email_query_state).filter(Some(
Get::new() Filter::Condition(
.ids(Some(Argument::reference::< EmailFilterCondition::new()
EmailQueryChanges, .in_mailbox(Some(mailbox_id.clone()))
EmailObject, .into(),
EmailObject, ),
>( )),
seq_no, );
ResultField::<EmailQueryChanges, EmailObject>::new("/removed"), let seq_no = req.add_call(&email_query_changes_call).await;
))) let email_get_call: EmailGet = EmailGet::new(
.account_id(self.mail_account_id().clone()) Get::new()
.properties(Some(vec![ .ids(Some(Argument::reference::<
"keywords".to_string(), EmailQueryChanges,
"mailboxIds".to_string(), EmailObject,
])), EmailObject,
); >(
req.add_call(&email_get_call); seq_no,
} else { ResultField::<EmailQueryChanges, EmailObject>::new("/removed"),
return Ok(()); )))
} .account_id(mail_account_id.clone())
.properties(Some(vec!["keywords".to_string(), "mailboxIds".to_string()])),
);
req.add_call(&email_get_call).await;
} else { } else {
return Ok(()); return Ok(());
} }
@ -395,7 +433,7 @@ impl JmapConnection {
} }
let mut v: MethodResponse = match deserialize_from_str(&res_text) { let mut v: MethodResponse = match deserialize_from_str(&res_text) {
Err(err) => { 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); return Err(err);
} }
Ok(s) => s, Ok(s) => s,
@ -425,11 +463,8 @@ impl JmapConnection {
.collect::<SmallVec<[MailboxHash; 8]>>(); .collect::<SmallVec<[MailboxHash; 8]>>();
mailbox_hashes.push(v); mailbox_hashes.push(v);
} }
for (env, mailbox_hashes) in list for (obj, mailbox_hashes) in list.into_iter().zip(mailbox_hashes) {
.into_iter() let env = self.store.add_envelope(obj).await;
.map(|obj| self.store.add_envelope(obj))
.zip(mailbox_hashes)
{
for mailbox_hash in mailbox_hashes.iter().skip(1).cloned() { for mailbox_hash in mailbox_hashes.iter().skip(1).cloned() {
let mut mailboxes_lck = self.store.mailboxes.write().unwrap(); let mut mailboxes_lck = self.store.mailboxes.write().unwrap();
mailboxes_lck.entry(mailbox_hash).and_modify(|mbox| { 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); let response = v.method_responses.remove(0);
match EmailQueryChangesResponse::try_from(response) { match EmailQueryChangesResponse::try_from(response) {
Ok(EmailQueryChangesResponse { Ok(EmailQueryChangesResponse {
@ -581,7 +616,7 @@ impl JmapConnection {
let _: MethodResponse = match deserialize_from_str(&res_text) { let _: MethodResponse = match deserialize_from_str(&res_text) {
Err(err) => { Err(err) => {
log::error!("{}", &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); return Err(err);
} }
Ok(s) => s, Ok(s) => s,
@ -589,19 +624,19 @@ impl JmapConnection {
Ok(res_text) Ok(res_text)
} }
pub async fn get_async(&self, url: &str) -> Result<isahc::Response<isahc::AsyncBody>> { pub async fn get_async(&self, url: &Url) -> Result<isahc::Response<isahc::AsyncBody>> {
if cfg!(feature = "jmap-trace") { 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); log::trace!("get_async(): url `{}` response {:?}", url, res);
Ok(res?) Ok(res?)
} else { } else {
Ok(self.client.get_async(url).await?) Ok(self.client.get_async(url.as_str()).await?)
} }
} }
pub async fn post_async<T: Into<Vec<u8>> + Send + Sync>( pub async fn post_async<T: Into<Vec<u8>> + Send + Sync>(
&self, &self,
api_url: Option<&str>, api_url: Option<&Url>,
request: T, request: T,
) -> Result<isahc::Response<isahc::AsyncBody>> { ) -> Result<isahc::Response<isahc::AsyncBody>> {
let request: Vec<u8> = request.into(); let request: Vec<u8> = request.into();
@ -612,9 +647,9 @@ impl JmapConnection {
); );
} }
if let Some(api_url) = api_url { 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 { } 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?) Ok(self.client.post_async(api_url.as_str(), request).await?)
} }
} }

@ -19,6 +19,9 @@
* along with meli. If not, see <http://www.gnu.org/licenses/>. * along with meli. If not, see <http://www.gnu.org/licenses/>.
*/ */
// In case we forget to wait some future.
#![deny(unused_must_use)]
use std::{ use std::{
collections::{BTreeSet, HashMap, HashSet}, collections::{BTreeSet, HashMap, HashSet},
convert::TryFrom, convert::TryFrom,
@ -28,11 +31,18 @@ use std::{
time::{Duration, Instant}, 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 indexmap::{IndexMap, IndexSet};
use isahc::{config::RedirectPolicy, AsyncReadResponseExt, HttpClient}; use isahc::{config::RedirectPolicy, AsyncReadResponseExt, HttpClient};
use serde_json::{json, Value}; use serde_json::{json, Value};
use smallvec::SmallVec; use smallvec::SmallVec;
use url::Url;
use crate::{ use crate::{
backends::*, backends::*,
@ -117,7 +127,7 @@ pub struct EnvelopeCache {
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct JmapServerConf { pub struct JmapServerConf {
pub server_url: String, pub server_url: Url,
pub server_username: String, pub server_username: String,
pub server_password: String, pub server_password: String,
pub use_token: bool, 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:ident[$var:literal], $default:expr) => {
$s.extra $s.extra
.get($var) .get($var)
@ -169,8 +192,8 @@ impl JmapServerConf {
))); )));
} }
Ok(Self { Ok(Self {
server_url: get_conf_val!(s["server_url"])?.to_string(), server_url: get_conf_val!(s["server_url"], Url)?,
server_username: get_conf_val!(s["server_username"])?.to_string(), server_username: get_conf_val!(s["server_username"], String)?,
server_password: s.server_password()?, server_password: s.server_password()?,
use_token, use_token,
danger_accept_invalid_certs: get_conf_val!(s["danger_accept_invalid_certs"], false)?, 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<FutureMutex<(Instant, Result<Session>)>>);
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<Instant>) {
self.0.lock().await.0 = value.unwrap_or_else(Instant::now);
}
/// Set inner value.
pub async fn set(&self, t: Option<Instant>, value: Result<Session>) -> Result<Session> {
std::mem::replace(
&mut (*self.0.lock().await),
(t.unwrap_or_else(Instant::now), value),
)
.1
}
pub async fn session_guard(
&'_ self,
) -> Result<FutureMappedMutexGuard<'_, (Instant, Result<Session>), 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)] #[derive(Debug)]
pub struct Store { pub struct Store {
pub account_name: Arc<String>, pub account_name: Arc<String>,
pub account_hash: AccountHash, pub account_hash: AccountHash,
pub main_identity: String, pub main_identity: String,
pub extra_identities: Vec<String>, pub extra_identities: Vec<String>,
pub account_id: Arc<Mutex<Id<Account>>>, pub byte_cache: Arc<FutureMutex<HashMap<EnvelopeHash, EnvelopeCache>>>,
pub byte_cache: Arc<Mutex<HashMap<EnvelopeHash, EnvelopeCache>>>, pub id_store: Arc<FutureMutex<HashMap<EnvelopeHash, Id<EmailObject>>>>,
pub id_store: Arc<Mutex<HashMap<EnvelopeHash, Id<EmailObject>>>>, pub reverse_id_store: Arc<FutureMutex<HashMap<Id<EmailObject>, EnvelopeHash>>>,
pub reverse_id_store: Arc<Mutex<HashMap<Id<EmailObject>, EnvelopeHash>>>, pub blob_id_store: Arc<FutureMutex<HashMap<EnvelopeHash, Id<BlobObject>>>>,
pub blob_id_store: Arc<Mutex<HashMap<EnvelopeHash, Id<BlobObject>>>>,
pub collection: Collection, pub collection: Collection,
pub mailboxes: Arc<RwLock<HashMap<MailboxHash, JmapMailbox>>>, pub mailboxes: Arc<RwLock<HashMap<MailboxHash, JmapMailbox>>>,
pub mailboxes_index: Arc<RwLock<HashMap<MailboxHash, HashSet<EnvelopeHash>>>>, pub mailboxes_index: Arc<RwLock<HashMap<MailboxHash, HashSet<EnvelopeHash>>>>,
pub mailbox_state: Arc<Mutex<State<MailboxObject>>>, pub mailbox_state: Arc<FutureMutex<State<MailboxObject>>>,
pub online_status: Arc<FutureMutex<(Instant, Result<()>)>>, pub online_status: OnlineStatus,
pub is_subscribed: Arc<IsSubscribedFn>, pub is_subscribed: Arc<IsSubscribedFn>,
pub core_capabilities: Arc<Mutex<IndexMap<String, CapabilitiesObject>>>, pub core_capabilities: Arc<Mutex<IndexMap<String, CapabilitiesObject>>>,
pub event_consumer: BackendEventConsumer, pub event_consumer: BackendEventConsumer,
} }
impl Store { 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 flags = Flag::default();
let mut labels: IndexSet<TagHash> = IndexSet::new(); let mut labels: IndexSet<TagHash> = IndexSet::new();
let mut tag_lck = self.collection.tag_index.write().unwrap(); let id;
for t in obj.keywords().keys() { let mailbox_ids;
match t.as_str() { let blob_id;
"$draft" => { {
flags |= Flag::DRAFT; let mut tag_lck = self.collection.tag_index.write().unwrap();
} for t in obj.keywords().keys() {
"$seen" => { match t.as_str() {
flags |= Flag::SEEN; "$draft" => {
} flags |= Flag::DRAFT;
"$flagged" => { }
flags |= Flag::FLAGGED; "$seen" => {
} flags |= Flag::SEEN;
"$answered" => { }
flags |= Flag::REPLIED; "$flagged" => {
} flags |= Flag::FLAGGED;
"$junk" | "$notjunk" => { /* ignore */ } }
_ => { "$answered" => {
let tag_hash = TagHash::from_bytes(t.as_bytes()); flags |= Flag::REPLIED;
tag_lck.entry(tag_hash).or_insert_with(|| t.to_string()); }
labels.insert(tag_hash); "$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(); id = obj.id.clone();
let mailbox_ids = obj.mailbox_ids.clone(); mailbox_ids = obj.mailbox_ids.clone();
let blob_id = obj.blob_id.clone(); blob_id = obj.blob_id.clone();
drop(tag_lck); }
let mut ret: Envelope = obj.into(); let mut ret: Envelope = obj.into();
ret.set_flags(flags); ret.set_flags(flags);
ret.tags_mut().extend(labels); ret.tags_mut().extend(labels);
let mut id_store_lck = self.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().unwrap(); let mut reverse_id_store_lck = self.reverse_id_store.lock().await;
let mut blob_id_store_lck = self.blob_id_store.lock().unwrap(); let mut blob_id_store_lck = self.blob_id_store.lock().await;
let mailboxes_lck = self.mailboxes.read().unwrap(); let mailboxes_lck = self.mailboxes.read().unwrap();
let mut mailboxes_index_lck = self.mailboxes_index.write().unwrap(); let mut mailboxes_index_lck = self.mailboxes_index.write().unwrap();
for (mailbox_id, _) in mailbox_ids { for (mailbox_id, _) in mailbox_ids {
@ -262,14 +333,14 @@ impl Store {
ret ret
} }
pub fn remove_envelope( pub async fn remove_envelope(
&self, &self,
obj_id: Id<EmailObject>, obj_id: Id<EmailObject>,
) -> Option<(EnvelopeHash, SmallVec<[MailboxHash; 8]>)> { ) -> Option<(EnvelopeHash, SmallVec<[MailboxHash; 8]>)> {
let env_hash = self.reverse_id_store.lock().unwrap().remove(&obj_id)?; let env_hash = self.reverse_id_store.lock().await.remove(&obj_id)?;
self.id_store.lock().unwrap().remove(&env_hash); self.id_store.lock().await.remove(&env_hash);
self.blob_id_store.lock().unwrap().remove(&env_hash); self.blob_id_store.lock().await.remove(&env_hash);
self.byte_cache.lock().unwrap().remove(&env_hash); self.byte_cache.lock().await.remove(&env_hash);
let mut mailbox_hashes = SmallVec::new(); let mut mailbox_hashes = SmallVec::new();
{ {
let mut mailboxes_lck = self.mailboxes_index.write().unwrap(); let mut mailboxes_lck = self.mailboxes_index.write().unwrap();
@ -318,14 +389,9 @@ impl MailBackend for JmapType {
let connection = self.connection.clone(); let connection = self.connection.clone();
let timeout_dur = self.server_conf.timeout; let timeout_dur = self.server_conf.timeout;
Ok(Box::pin(async move { Ok(Box::pin(async move {
match timeout(timeout_dur, connection.lock()).await { let _conn = timeout(timeout_dur, connection.lock()).await?;
Ok(_conn) => match timeout(timeout_dur, online.lock()).await { let _session = timeout(timeout_dur, online.session_guard()).await??;
Err(err) => Err(err), Ok(())
Ok(lck) if lck.1.is_err() => lck.1.clone(),
_ => Ok(()),
},
Err(err) => Err(err),
}
})) }))
} }
@ -440,13 +506,13 @@ impl MailBackend for JmapType {
* 1. upload binary blob, get blobId * 1. upload binary blob, get blobId
* 2. Email/import * 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 let mut res = conn
.post_async( .post_async(
Some(&upload_request_format( Some(&upload_request_format(&upload_url, &mail_account_id)?),
upload_url.as_str(),
&conn.mail_account_id(),
)),
bytes, bytes,
) )
.await?; .await?;
@ -466,7 +532,7 @@ impl MailBackend for JmapType {
let upload_response: UploadResponse = match deserialize_from_str(&res_text) { let upload_response: UploadResponse = match deserialize_from_str(&res_text) {
Err(err) => { 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); return Err(err);
} }
Ok(s) => s, Ok(s) => s,
@ -474,23 +540,24 @@ impl MailBackend for JmapType {
let mut req = Request::new(conn.request_no.clone()); let mut req = Request::new(conn.request_no.clone());
let creation_id: Id<EmailObject> = "1".to_string().into(); let creation_id: Id<EmailObject> = "1".to_string().into();
let import_call: EmailImport = EmailImport::new() let import_call: EmailImport =
.account_id(conn.mail_account_id()) EmailImport::new()
.emails(indexmap! { .account_id(mail_account_id)
creation_id.clone() => EmailImportObject::new() .emails(indexmap! {
.blob_id(upload_response.blob_id) creation_id.clone() => EmailImportObject::new()
.mailbox_ids(indexmap! { .blob_id(upload_response.blob_id)
mailbox_id => true .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 mut res = conn.post_async(None, serde_json::to_string(&req)?).await?;
let res_text = res.text().await?; let res_text = res.text().await?;
let mut v: MethodResponse = match deserialize_from_str(&res_text) { let mut v: MethodResponse = match deserialize_from_str(&res_text) {
Err(err) => { 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); return Err(err);
} }
Ok(s) => s, Ok(s) => s,
@ -549,28 +616,29 @@ impl MailBackend for JmapType {
Ok(Box::pin(async move { Ok(Box::pin(async move {
let mut conn = connection.lock().await; let mut conn = connection.lock().await;
conn.connect().await?; conn.connect().await?;
let mail_account_id = conn.session_guard().await?.mail_account_id();
let email_call: EmailQuery = EmailQuery::new( let email_call: EmailQuery = EmailQuery::new(
Query::new() Query::new()
.account_id(conn.mail_account_id()) .account_id(mail_account_id)
.filter(Some(filter)) .filter(Some(filter))
.position(0), .position(0),
) )
.collapse_threads(false); .collapse_threads(false);
let mut req = Request::new(conn.request_no.clone()); 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 mut res = conn.post_async(None, serde_json::to_string(&req)?).await?;
let res_text = res.text().await?; let res_text = res.text().await?;
let mut v: MethodResponse = match deserialize_from_str(&res_text) { let mut v: MethodResponse = match deserialize_from_str(&res_text) {
Err(err) => { 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); return Err(err);
} }
Ok(s) => s, Ok(s) => s,
}; };
*store.online_status.lock().await = (std::time::Instant::now(), Ok(())); store.online_status.update_timestamp(None).await;
let m = QueryResponse::<EmailObject>::try_from(v.method_responses.remove(0))?; let m = QueryResponse::<EmailObject>::try_from(v.method_responses.remove(0))?;
let QueryResponse::<EmailObject> { ids, .. } = m; let QueryResponse::<EmailObject> { ids, .. } = m;
let ret = ids.into_iter().map(|id| id.into_hash()).collect(); let ret = ids.into_iter().map(|id| id.into_hash()).collect();
@ -596,9 +664,10 @@ impl MailBackend for JmapType {
let connection = self.connection.clone(); let connection = self.connection.clone();
Ok(Box::pin(async move { Ok(Box::pin(async move {
let mut conn = connection.lock().await; let mut conn = connection.lock().await;
let mail_account_id = conn.session_guard().await?.mail_account_id();
let mailbox_set_call: MailboxSet = MailboxSet::new( let mailbox_set_call: MailboxSet = MailboxSet::new(
Set::<MailboxObject>::new() Set::<MailboxObject>::new()
.account_id(conn.mail_account_id()) .account_id(mail_account_id)
.create(Some({ .create(Some({
let id: Id<MailboxObject> = path.as_str().into(); let id: Id<MailboxObject> = path.as_str().into();
indexmap! { indexmap! {
@ -612,7 +681,7 @@ impl MailBackend for JmapType {
); );
let mut req = Request::new(conn.request_no.clone()); 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?; let new_mailboxes = protocol::get_mailboxes(&mut conn, Some(req)).await?;
*store.mailboxes.write().unwrap() = new_mailboxes; *store.mailboxes.write().unwrap() = new_mailboxes;
@ -707,7 +776,7 @@ impl MailBackend for JmapType {
} }
{ {
for env_hash in env_hashes.iter() { 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()); // ids.push(id.clone());
// id_map.insert(id.clone(), env_hash); // id_map.insert(id.clone(), env_hash);
update_map.insert( update_map.insert(
@ -718,15 +787,16 @@ impl MailBackend for JmapType {
} }
} }
let conn = connection.lock().await; let conn = connection.lock().await;
let mail_account_id = conn.session_guard().await?.mail_account_id();
let email_set_call: EmailSet = EmailSet::new( let email_set_call: EmailSet = EmailSet::new(
Set::<EmailObject>::new() Set::<EmailObject>::new()
.account_id(conn.mail_account_id()) .account_id(mail_account_id)
.update(Some(update_map)), .update(Some(update_map)),
); );
let mut req = Request::new(conn.request_no.clone()); 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?; 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) { let mut v: MethodResponse = match deserialize_from_str(&res_text) {
Err(err) => { 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); return Err(err);
} }
Ok(s) => s, Ok(s) => s,
}; };
*store.online_status.lock().await = (std::time::Instant::now(), Ok(())); store.online_status.update_timestamp(None).await;
let m = SetResponse::<EmailObject>::try_from(v.method_responses.remove(0))?; let m = SetResponse::<EmailObject>::try_from(v.method_responses.remove(0))?;
if let Some(ids) = m.not_updated { if let Some(ids) = m.not_updated {
if !ids.is_empty() { if !ids.is_empty() {
@ -815,7 +885,7 @@ impl MailBackend for JmapType {
} }
{ {
for hash in env_hashes.iter() { 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()); ids.push(id.clone());
id_map.insert(id.clone(), hash); id_map.insert(id.clone(), hash);
update_map.insert( update_map.insert(
@ -826,23 +896,24 @@ impl MailBackend for JmapType {
} }
} }
let conn = connection.lock().await; let conn = connection.lock().await;
let mail_account_id = conn.session_guard().await?.mail_account_id();
let email_set_call: EmailSet = EmailSet::new( let email_set_call: EmailSet = EmailSet::new(
Set::<EmailObject>::new() Set::<EmailObject>::new()
.account_id(conn.mail_account_id()) .account_id(mail_account_id.clone())
.update(Some(update_map)), .update(Some(update_map)),
); );
let mut req = Request::new(conn.request_no.clone()); 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( let email_call: EmailGet = EmailGet::new(
Get::new() Get::new()
.ids(Some(Argument::Value(ids))) .ids(Some(Argument::Value(ids)))
.account_id(conn.mail_account_id()) .account_id(mail_account_id)
.properties(Some(vec!["keywords".to_string()])), .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?; 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); //debug!("res_text = {}", &res_text);
let mut v: MethodResponse = match deserialize_from_str(&res_text) { let mut v: MethodResponse = match deserialize_from_str(&res_text) {
Err(err) => { 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); return Err(err);
} }
Ok(s) => s, Ok(s) => s,
}; };
*store.online_status.lock().await = (std::time::Instant::now(), Ok(())); store.online_status.update_timestamp(None).await;
let m = SetResponse::<EmailObject>::try_from(v.method_responses.remove(0))?; let m = SetResponse::<EmailObject>::try_from(v.method_responses.remove(0))?;
if let Some(ids) = m.not_updated { if let Some(ids) = m.not_updated {
return Err(Error::new( return Err(Error::new(
@ -992,19 +1063,18 @@ impl MailBackend for JmapType {
}) })
}; };
let conn = connection.lock().await; let conn = connection.lock().await;
let mail_account_id = conn.session_guard().await?.mail_account_id();
// [ref:TODO] smarter identity detection based on From: ? // [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( return Err(Error::new(
"You need to setup an Identity in the JMAP server.", "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 let mut res = conn
.post_async( .post_async(
Some(&upload_request_format( Some(&upload_request_format(&upload_url, &mail_account_id)?),
upload_url.as_str(),
&conn.mail_account_id(),
)),
bytes, bytes,
) )
.await?; .await?;
@ -1012,7 +1082,7 @@ impl MailBackend for JmapType {
let upload_response: UploadResponse = match deserialize_from_str(&res_text) { let upload_response: UploadResponse = match deserialize_from_str(&res_text) {
Err(err) => { 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); return Err(err);
} }
Ok(s) => s, Ok(s) => s,
@ -1021,7 +1091,7 @@ impl MailBackend for JmapType {
let mut req = Request::new(conn.request_no.clone()); let mut req = Request::new(conn.request_no.clone());
let creation_id: Id<EmailObject> = "newid".into(); let creation_id: Id<EmailObject> = "newid".into();
let import_call: EmailImport = EmailImport::new() let import_call: EmailImport = EmailImport::new()
.account_id(conn.mail_account_id()) .account_id(mail_account_id.clone())
.emails(indexmap! { .emails(indexmap! {
creation_id => EmailImportObject::new() creation_id => EmailImportObject::new()
.blob_id(upload_response.blob_id) .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 mut res = conn.post_async(None, serde_json::to_string(&req)?).await?;
let res_text = res.text().await?; let res_text = res.text().await?;
let v: MethodResponse = match deserialize_from_str(&res_text) { let v: MethodResponse = match deserialize_from_str(&res_text) {
Err(err) => { 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); return Err(err);
} }
Ok(s) => s, Ok(s) => s,
@ -1061,10 +1131,10 @@ impl MailBackend for JmapType {
let mut req = Request::new(conn.request_no.clone()); let mut req = Request::new(conn.request_no.clone());
let subm_set_call: EmailSubmissionSet = EmailSubmissionSet::new( let subm_set_call: EmailSubmissionSet = EmailSubmissionSet::new(
Set::<EmailSubmissionObject>::new() Set::<EmailSubmissionObject>::new()
.account_id(conn.mail_account_id()) .account_id(mail_account_id.clone())
.create(Some(indexmap! { .create(Some(indexmap! {
Argument::from(Id::from("k1490")) => EmailSubmissionObject::new( Argument::from(Id::from("k1490")) => EmailSubmissionObject::new(
/* account_id: */ conn.mail_account_id(), /* account_id: */ mail_account_id,
/* identity_id: */ identity_id, /* identity_id: */ identity_id,
/* email_id: */ email_id, /* email_id: */ email_id,
/* envelope: */ None, /* envelope: */ None,
@ -1080,15 +1150,15 @@ impl MailBackend for JmapType {
}) })
})); }));
req.add_call(&subm_set_call); req.add_call(&subm_set_call).await;
let mut res = conn.post_async(None, serde_json::to_string(&req)?).await?;
let mut res = conn.post_async(None, serde_json::to_string(&req)?).await?;
let res_text = res.text().await?; let res_text = res.text().await?;
// [ref:TODO] parse/return any error. // [ref:TODO] parse/return any error.
let _: MethodResponse = match deserialize_from_str(&res_text) { let _: MethodResponse = match deserialize_from_str(&res_text) {
Err(err) => { 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); return Err(err);
} }
Ok(s) => s, Ok(s) => s,
@ -1106,10 +1176,10 @@ impl JmapType {
is_subscribed: Box<dyn Fn(&str) -> bool + Send + Sync>, is_subscribed: Box<dyn Fn(&str) -> bool + Send + Sync>,
event_consumer: BackendEventConsumer, event_consumer: BackendEventConsumer,
) -> Result<Box<dyn MailBackend>> { ) -> Result<Box<dyn MailBackend>> {
let online_status = Arc::new(FutureMutex::new(( let online_status = OnlineStatus(Arc::new(FutureMutex::new((
std::time::Instant::now(), std::time::Instant::now(),
Err(Error::new("Account is uninitialised.")), Err(Error::new("Account is uninitialised.")),
))); ))));
let server_conf = JmapServerConf::new(s)?; let server_conf = JmapServerConf::new(s)?;
let account_hash = AccountHash::from_bytes(s.name.as_bytes()); let account_hash = AccountHash::from_bytes(s.name.as_bytes());
@ -1118,7 +1188,6 @@ impl JmapType {
account_hash, account_hash,
main_identity: s.make_display_name(), main_identity: s.make_display_name(),
extra_identities: s.extra_identities.clone(), extra_identities: s.extra_identities.clone(),
account_id: Arc::new(Mutex::new(Id::empty())),
online_status, online_status,
event_consumer, event_consumer,
is_subscribed: Arc::new(IsSubscribedFn(is_subscribed)), 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:ident[$var:literal], $default:expr) => {
$s.extra $s.extra
.remove($var) .remove($var)
@ -1171,7 +1253,7 @@ impl JmapType {
.unwrap_or_else(|| Ok($default)) .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["server_username"])?;
get_conf_val!(s["use_token"], false)?; get_conf_val!(s["use_token"], false)?;

@ -35,3 +35,6 @@ pub use identity::*;
mod submission; mod submission;
pub use submission::*; pub use submission::*;
#[cfg(test)]
mod tests;

@ -747,49 +747,6 @@ impl From<crate::search::Query> for Filter<EmailFilterCondition, EmailObject> {
} }
} }
#[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<EmailFilterCondition, EmailObject> = 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)] #[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct EmailSet { pub struct EmailSet {

@ -114,81 +114,3 @@ pub struct IdentitySet(pub Set<IdentityObject>);
impl Method<IdentityObject> for IdentitySet { impl Method<IdentityObject> for IdentitySet {
const NAME: &'static str = "Identity/set"; 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::<IdentityObject>::new()
.account_id(account_id.into())
.create(Some({
let id: Id<IdentityObject> = 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"
]
}},
);
}
}

@ -385,115 +385,3 @@ pub struct Address {
/// applied. /// applied.
pub parameters: Option<Value>, pub parameters: Option<Value>,
} }
#[cfg(test)]
mod tests {
use serde_json::json;
use super::*;
#[test]
fn test_jmap_undo_status() {
let account_id: Id<Account> = "blahblah".into();
let ident_id: Id<IdentityObject> = "sdusssssss".into();
let email_id: Id<EmailObject> = 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<Account> = "blahblah".into();
let ident_id: Id<IdentityObject> = "sdusssssss".into();
let email_id: Id<EmailObject> = 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::<EmailImport, EmailObject, EmailObject>(
42,
ResultField::<EmailImport, EmailObject>::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",
})
);
}
}

@ -0,0 +1,241 @@
//
// melib - jmap module.
//
// Copyright 2024 Emmanouil Pitsidianakis <manos@pitsidianak.is>
//
// 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 <http://www.gnu.org/licenses/>.
//
// 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<EmailFilterCondition, EmailObject> = 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<Account> = "blahblah".into();
let ident_id: Id<IdentityObject> = "sdusssssss".into();
let email_id: Id<EmailObject> = 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<Account> = "blahblah".into();
let ident_id: Id<IdentityObject> = "sdusssssss".into();
let email_id: Id<EmailObject> = 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::<EmailImport, EmailObject, EmailObject>(
42,
ResultField::<EmailImport, EmailObject>::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::<IdentityObject>::new()
.account_id(account_id.into())
.create(Some({
let id: Id<IdentityObject> = 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"
]
}},
);
}

@ -47,38 +47,35 @@ impl JmapOp {
impl BackendOp for JmapOp { impl BackendOp for JmapOp {
fn as_bytes(&mut self) -> ResultFuture<Vec<u8>> { fn as_bytes(&mut self) -> ResultFuture<Vec<u8>> {
{
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 store = self.store.clone();
let hash = self.hash; let hash = self.hash;
let connection = self.connection.clone(); let connection = self.connection.clone();
Ok(Box::pin(async move { 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; let mut conn = connection.lock().await;
conn.connect().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 let mut res = conn
.get_async(&download_request_format( .get_async(&download_request_format(
download_url.as_str(), &download_url,
&conn.mail_account_id(), &mail_account_id,
&blob_id, &blob_id,
None, None,
)) )?)
.await?; .await?;
let res_text = res.text().await?; let res_text = res.text().await?;
store store.byte_cache.lock().await.entry(hash).or_default().bytes = Some(res_text.clone());
.byte_cache
.lock()
.unwrap()
.entry(hash)
.or_default()
.bytes = Some(res_text.clone());
Ok(res_text.into_bytes()) Ok(res_text.into_bytes())
})) }))
} }

@ -30,20 +30,11 @@ pub type UtcDate = String;
use super::rfc8620::{Object, State}; use super::rfc8620::{Object, State};
macro_rules! get_request_no { pub trait Response<OBJ: Object>: Send + Sync {
($lock:expr) => {{
let mut lck = $lock.lock().unwrap();
let ret = *lck;
*lck += 1;
ret
}};
}
pub trait Response<OBJ: Object> {
const NAME: &'static str; const NAME: &'static str;
} }
pub trait Method<OBJ: Object>: Serialize { pub trait Method<OBJ: Object>: Serialize + Send + Sync {
const NAME: &'static str; const NAME: &'static str;
} }
@ -58,11 +49,21 @@ pub struct Request {
method_calls: Vec<Value>, method_calls: Vec<Value>,
#[serde(skip)] #[serde(skip)]
request_no: Arc<Mutex<usize>>, request_no: Arc<FutureMutex<usize>>,
}
macro_rules! get_request_no {
($lock:expr) => {{
let mut lck = $lock.lock().await;
let ret = *lck;
*lck += 1;
drop(lck);
ret
}};
} }
impl Request { impl Request {
pub fn new(request_no: Arc<Mutex<usize>>) -> Self { pub fn new(request_no: Arc<FutureMutex<usize>>) -> Self {
Self { Self {
using: USING, using: USING,
method_calls: Vec::new(), method_calls: Vec::new(),
@ -70,12 +71,20 @@ impl Request {
} }
} }
pub fn add_call<M: Method<O>, O: Object>(&mut self, call: &M) -> usize { pub async fn add_call<M: Method<O>, O: Object>(&mut self, call: &M) -> usize {
let seq = get_request_no!(self.request_no); let seq = get_request_no!(self.request_no);
self.method_calls self.method_calls
.push(serde_json::to_value((M::NAME, call, &format!("m{}", seq))).unwrap()); .push(serde_json::to_value((M::NAME, call, &format!("m{}", seq))).unwrap());
seq seq
} }
pub fn request_no(&self) -> Arc<FutureMutex<usize>> {
self.request_no.clone()
}
pub async fn request_no_value(&self) -> usize {
get_request_no!(self.request_no)
}
} }
pub async fn get_mailboxes( pub async fn get_mailboxes(
@ -83,13 +92,14 @@ pub async fn get_mailboxes(
request: Option<Request>, request: Option<Request>,
) -> Result<HashMap<MailboxHash, JmapMailbox>> { ) -> Result<HashMap<MailboxHash, JmapMailbox>> {
let mut req = request.unwrap_or_else(|| Request::new(conn.request_no.clone())); 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 = let mailbox_get: MailboxGet =
MailboxGet::new(Get::<MailboxObject>::new().account_id(conn.mail_account_id())); MailboxGet::new(Get::<MailboxObject>::new().account_id(mail_account_id));
req.add_call(&mailbox_get); req.add_call(&mailbox_get).await;
let res_text = conn.send_request(serde_json::to_string(&req)?).await?; let res_text = conn.send_request(serde_json::to_string(&req)?).await?;
let mut v: MethodResponse = deserialize_from_str(&res_text)?; 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::<MailboxObject>::try_from(v.method_responses.remove(0))?; let m = GetResponse::<MailboxObject>::try_from(v.method_responses.remove(0))?;
let GetResponse::<MailboxObject> { let GetResponse::<MailboxObject> {
list, account_id, .. list, account_id, ..
@ -99,14 +109,13 @@ pub async fn get_mailboxes(
// `isSubscribed` is false on a mailbox, it should be regarded as // `isSubscribed` is false on a mailbox, it should be regarded as
// subscribed. // subscribed.
let is_personal: bool = { let is_personal: bool = {
let session = conn.session_guard(); let session = conn.session_guard().await?;
session session
.accounts .accounts
.get(&account_id) .get(&account_id)
.map(|acc| acc.is_personal) .map(|acc| acc.is_personal)
.unwrap_or(false) .unwrap_or(false)
}; };
*conn.store.account_id.lock().unwrap() = account_id;
let mut ret: HashMap<MailboxHash, JmapMailbox> = list let mut ret: HashMap<MailboxHash, JmapMailbox> = list
.into_iter() .into_iter()
.map(|r| { .map(|r| {
@ -169,9 +178,10 @@ pub async fn get_message_list(
conn: &mut JmapConnection, conn: &mut JmapConnection,
mailbox: &JmapMailbox, mailbox: &JmapMailbox,
) -> Result<Vec<Id<EmailObject>>> { ) -> Result<Vec<Id<EmailObject>>> {
let mail_account_id = conn.session_guard().await?.mail_account_id();
let email_call: EmailQuery = EmailQuery::new( let email_call: EmailQuery = EmailQuery::new(
Query::new() Query::new()
.account_id(conn.mail_account_id()) .account_id(mail_account_id)
.filter(Some(Filter::Condition( .filter(Some(Filter::Condition(
EmailFilterCondition::new() EmailFilterCondition::new()
.in_mailbox(Some(mailbox.id.clone())) .in_mailbox(Some(mailbox.id.clone()))
@ -182,19 +192,19 @@ pub async fn get_message_list(
.collapse_threads(false); .collapse_threads(false);
let mut req = Request::new(conn.request_no.clone()); 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 mut res = conn.post_async(None, serde_json::to_string(&req)?).await?;
let res_text = res.text().await?; let res_text = res.text().await?;
let mut v: MethodResponse = match deserialize_from_str(&res_text) { let mut v: MethodResponse = match deserialize_from_str(&res_text) {
Err(err) => { 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); return Err(err);
} }
Ok(s) => s, 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::<EmailObject>::try_from(v.method_responses.remove(0))?; let m = QueryResponse::<EmailObject>::try_from(v.method_responses.remove(0))?;
let QueryResponse::<EmailObject> { ids, .. } = m; let QueryResponse::<EmailObject> { ids, .. } = m;
conn.last_method_response = Some(res_text); conn.last_method_response = Some(res_text);
@ -285,10 +295,11 @@ impl EmailFetchState {
mut position, mut position,
batch_size, 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 mailbox_id = store.mailboxes.read().unwrap()[&mailbox_hash].id.clone();
let email_query_call: EmailQuery = EmailQuery::new( let email_query_call: EmailQuery = EmailQuery::new(
Query::new() Query::new()
.account_id(conn.mail_account_id().clone()) .account_id(mail_account_id.clone())
.filter(Some(Filter::Condition( .filter(Some(Filter::Condition(
EmailFilterCondition::new() EmailFilterCondition::new()
.in_mailbox(Some(mailbox_id)) .in_mailbox(Some(mailbox_id))
@ -300,7 +311,7 @@ impl EmailFetchState {
.collapse_threads(false); .collapse_threads(false);
let mut req = Request::new(conn.request_no.clone()); 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( let email_call: EmailGet = EmailGet::new(
Get::new() Get::new()
@ -311,15 +322,14 @@ impl EmailFetchState {
>( >(
prev_seq, EmailQuery::RESULT_FIELD_IDS 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 res_text = conn.send_request(serde_json::to_string(&req)?).await?;
let mut v: MethodResponse = match deserialize_from_str(&res_text) { let mut v: MethodResponse = match deserialize_from_str(&res_text) {
Err(err) => { Err(err) => {
*conn.store.online_status.lock().await = _ = conn.store.online_status.set(None, Err(err.clone())).await;
(Instant::now(), Err(err.clone()));
return Err(err); return Err(err);
} }
Ok(v) => v, Ok(v) => v,
@ -338,7 +348,7 @@ impl EmailFetchState {
let mut unread = BTreeSet::default(); let mut unread = BTreeSet::default();
let mut ret = Vec::with_capacity(list.len()); let mut ret = Vec::with_capacity(list.len());
for obj in list { for obj in list {
let env = store.add_envelope(obj); let env = store.add_envelope(obj).await;
total.insert(env.hash()); total.insert(env.hash());
if !env.is_seen() { if !env.is_seen() {
unread.insert(env.hash()); unread.insert(env.hash());

@ -22,6 +22,7 @@
use std::{ use std::{
hash::{Hash, Hasher}, hash::{Hash, Hasher},
marker::PhantomData, marker::PhantomData,
sync::Arc,
}; };
use indexmap::IndexMap; use indexmap::IndexMap;
@ -30,8 +31,13 @@ use serde::{
ser::{Serialize, SerializeStruct, Serializer}, ser::{Serialize, SerializeStruct, Serializer},
}; };
use serde_json::{value::RawValue, Value}; 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; mod filters;
pub use filters::*; pub use filters::*;
@ -39,14 +45,17 @@ mod comparator;
pub use comparator::*; pub use comparator::*;
mod argument; mod argument;
pub use argument::*; pub use argument::*;
#[cfg(test)]
mod tests;
pub type PatchObject = Value; pub type PatchObject = Value;
impl Object for PatchObject { impl Object for PatchObject {
const NAME: &'static str = "PatchObject"; const NAME: &'static str = "PatchObject";
} }
use super::{deserialize_from_str, protocol::Method}; pub trait Object: Send + Sync {
pub trait Object {
const NAME: &'static str; const NAME: &'static str;
} }
@ -334,7 +343,7 @@ where
} }
impl<OBJ: Object + Serialize + std::fmt::Debug> Serialize for Get<OBJ> { impl<OBJ: Object + Serialize + std::fmt::Debug> Serialize for Get<OBJ> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where where
S: Serializer, S: Serializer,
{ {
@ -413,7 +422,7 @@ pub struct GetResponse<OBJ: Object> {
impl<OBJ: Object + DeserializeOwned> std::convert::TryFrom<&RawValue> for GetResponse<OBJ> { impl<OBJ: Object + DeserializeOwned> std::convert::TryFrom<&RawValue> for GetResponse<OBJ> {
type Error = crate::error::Error; type Error = crate::error::Error;
fn try_from(t: &RawValue) -> Result<Self, crate::error::Error> { fn try_from(t: &RawValue) -> Result<Self> {
let res: (String, Self, String) = deserialize_from_str(t.get())?; let res: (String, Self, String) = deserialize_from_str(t.get())?;
assert_eq!(&res.0, &format!("{}/get", OBJ::NAME)); assert_eq!(&res.0, &format!("{}/get", OBJ::NAME));
Ok(res.1) Ok(res.1)
@ -527,7 +536,7 @@ pub struct QueryResponse<OBJ: Object> {
impl<OBJ: Object + DeserializeOwned> std::convert::TryFrom<&RawValue> for QueryResponse<OBJ> { impl<OBJ: Object + DeserializeOwned> std::convert::TryFrom<&RawValue> for QueryResponse<OBJ> {
type Error = crate::error::Error; type Error = crate::error::Error;
fn try_from(t: &RawValue) -> std::result::Result<Self, Self::Error> { fn try_from(t: &RawValue) -> Result<Self> {
let res: (String, Self, String) = deserialize_from_str(t.get())?; let res: (String, Self, String) = deserialize_from_str(t.get())?;
assert_eq!(&res.0, &format!("{}/query", OBJ::NAME)); assert_eq!(&res.0, &format!("{}/query", OBJ::NAME));
Ok(res.1) Ok(res.1)
@ -657,7 +666,7 @@ pub struct ChangesResponse<OBJ: Object> {
impl<OBJ: Object + DeserializeOwned> std::convert::TryFrom<&RawValue> for ChangesResponse<OBJ> { impl<OBJ: Object + DeserializeOwned> std::convert::TryFrom<&RawValue> for ChangesResponse<OBJ> {
type Error = crate::error::Error; type Error = crate::error::Error;
fn try_from(t: &RawValue) -> std::result::Result<Self, Self::Error> { fn try_from(t: &RawValue) -> Result<Self> {
let res: (String, Self, String) = deserialize_from_str(t.get())?; let res: (String, Self, String) = deserialize_from_str(t.get())?;
assert_eq!(&res.0, &format!("{}/changes", OBJ::NAME)); assert_eq!(&res.0, &format!("{}/changes", OBJ::NAME));
Ok(res.1) Ok(res.1)
@ -803,7 +812,7 @@ where
} }
impl<OBJ: Object + Serialize + std::fmt::Debug> Serialize for Set<OBJ> { impl<OBJ: Object + Serialize + std::fmt::Debug> Serialize for Set<OBJ> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where where
S: Serializer, S: Serializer,
{ {
@ -898,7 +907,7 @@ pub struct SetResponse<OBJ: Object> {
impl<OBJ: Object + DeserializeOwned> std::convert::TryFrom<&RawValue> for SetResponse<OBJ> { impl<OBJ: Object + DeserializeOwned> std::convert::TryFrom<&RawValue> for SetResponse<OBJ> {
type Error = crate::error::Error; type Error = crate::error::Error;
fn try_from(t: &RawValue) -> Result<Self, crate::error::Error> { fn try_from(t: &RawValue) -> Result<Self> {
let res: (String, Self, String) = deserialize_from_str(t.get())?; let res: (String, Self, String) = deserialize_from_str(t.get())?;
assert_eq!(&res.0, &format!("{}/set", OBJ::NAME)); assert_eq!(&res.0, &format!("{}/set", OBJ::NAME));
Ok(res.1) Ok(res.1)
@ -991,59 +1000,84 @@ impl std::fmt::Display for SetError {
} }
pub fn download_request_format( pub fn download_request_format(
download_url: &str, download_url: &Url,
account_id: &Id<Account>, account_id: &Id<Account>,
blob_id: &Id<BlobObject>, blob_id: &Id<BlobObject>,
name: Option<String>, name: Option<String>,
) -> String { ) -> Result<Url> {
// https://jmap.fastmail.com/download/{accountId}/{blobId}/{name} // https://jmap.fastmail.com/download/{accountId}/{blobId}/{name}
let mut ret = String::with_capacity( let mut ret = String::with_capacity(
download_url.len() download_url.as_str().len()
+ blob_id.len() + blob_id.len()
+ name.as_ref().map(|n| n.len()).unwrap_or(0) + name.as_ref().map(|n| n.len()).unwrap_or(0)
+ account_id.len(), + account_id.len(),
); );
let mut prev_pos = 0; let mut prev_pos = 0;
while let Some(pos) = download_url.as_bytes()[prev_pos..].find(b"{") { while let Some(pos) = download_url.as_str().as_bytes()[prev_pos..].find(b"{") {
ret.push_str(&download_url[prev_pos..prev_pos + pos]); ret.push_str(&download_url.as_str()[prev_pos..prev_pos + 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()); ret.push_str(account_id.as_str());
prev_pos += "{accountId}".len(); 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()); ret.push_str(blob_id.as_str());
prev_pos += "{blobId}".len(); 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("")); ret.push_str(name.as_deref().unwrap_or(""));
prev_pos += "{name}".len(); 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"); ret.push_str("application/octet-stream");
prev_pos += "{name}".len(); prev_pos += "{name}".len();
} else { } else {
// [ref:FIXME]: return protocol error here
log::error!( log::error!(
"BUG: unknown parameter in download url: {}", "BUG: unknown parameter in download_url: {}",
&download_url[prev_pos..] &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() { if prev_pos != download_url.as_str().len() {
ret.push_str(&download_url[prev_pos..]); ret.push_str(&download_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!(
pub fn upload_request_format(upload_url: &str, account_id: &Id<Account>) -> String { "`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<Account>) -> Result<Url> {
//"uploadUrl": "https://jmap.fastmail.com/upload/{accountId}/", //"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; let mut prev_pos = 0;
while let Some(pos) = upload_url.as_bytes()[prev_pos..].find(b"{") { while let Some(pos) = upload_url.as_str().as_bytes()[prev_pos..].find(b"{") {
ret.push_str(&upload_url[prev_pos..prev_pos + pos]); ret.push_str(&upload_url.as_str()[prev_pos..prev_pos + 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()); ret.push_str(account_id.as_str());
prev_pos += "{accountId}".len(); prev_pos += "{accountId}".len();
break; break;
@ -1052,10 +1086,22 @@ pub fn upload_request_format(upload_url: &str, account_id: &Id<Account>) -> Stri
prev_pos += 1; prev_pos += 1;
} }
} }
if prev_pos != upload_url.len() { if prev_pos != upload_url.as_str().len() {
ret.push_str(&upload_url[prev_pos..]); 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)] #[derive(Clone, Debug, Deserialize, Serialize)]

@ -62,154 +62,3 @@ impl<T: Clone + PartialEq + Eq + Hash> From<T> for Argument<T> {
Self::Value(v) 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<BlobObject> = Id::new_uuid_v4();
let draft_mailbox_id: Id<MailboxObject> = Id::new_uuid_v4();
let sent_mailbox_id: Id<MailboxObject> = Id::new_uuid_v4();
let prev_seq = 33;
let mut req = Request::new(Arc::new(Mutex::new(prev_seq)));
let creation_id: Id<EmailObject> = "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::<EmailSubmissionObject>::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::<EmailImport, EmailObject, EmailObject>(prev_seq, ResultField::<EmailImport, EmailObject>::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"
]
}},
);
}
}

@ -21,7 +21,8 @@
use super::*; use super::*;
pub trait FilterTrait<T>: Default {} pub trait FilterTrait<T>: Default + Send + Sync {}
#[derive(Debug, Serialize)] #[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[serde(untagged)] #[serde(untagged)]

@ -0,0 +1,169 @@
//
// melib - jmap module.
//
// Copyright 2024 Emmanouil Pitsidianakis <manos@pitsidianak.is>
//
// 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 <http://www.gnu.org/licenses/>.
//
// 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<BlobObject> = Id::new_uuid_v4();
let draft_mailbox_id: Id<MailboxObject> = Id::new_uuid_v4();
let sent_mailbox_id: Id<MailboxObject> = Id::new_uuid_v4();
let prev_seq = 33;
let mut req = Request::new(Arc::new(FutureMutex::new(prev_seq)));
let creation_id: Id<EmailObject> = "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::<EmailSubmissionObject>::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::<EmailImport, EmailObject, EmailObject>(prev_seq, ResultField::<EmailImport, EmailObject>::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"
]
}},
);
}

@ -23,13 +23,14 @@ use std::sync::Arc;
use indexmap::IndexMap; use indexmap::IndexMap;
use serde_json::Value; use serde_json::Value;
use url::Url;
use crate::jmap::{ use crate::jmap::{
rfc8620::{Account, Id, Object, State}, 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")] #[serde(rename_all = "camelCase")]
pub struct Session { pub struct Session {
pub capabilities: IndexMap<String, CapabilitiesObject>, pub capabilities: IndexMap<String, CapabilitiesObject>,
@ -38,11 +39,11 @@ pub struct Session {
#[serde(skip)] #[serde(skip)]
pub identities: IndexMap<Id<IdentityObject>, IdentityObject>, pub identities: IndexMap<Id<IdentityObject>, IdentityObject>,
pub username: String, pub username: String,
pub api_url: Arc<String>, pub api_url: Arc<Url>,
pub download_url: Arc<String>, pub download_url: Arc<Url>,
pub upload_url: Arc<String>, pub upload_url: Arc<Url>,
pub event_source_url: Arc<String>, pub event_source_url: Arc<Url>,
pub state: State<Session>, pub state: State<Session>,
#[serde(flatten)] #[serde(flatten)]
pub extra_properties: IndexMap<String, Value>, pub extra_properties: IndexMap<String, Value>,
@ -52,6 +53,19 @@ impl Object for Session {
const NAME: &'static str = stringify!(Session); const NAME: &'static str = stringify!(Session);
} }
impl Session {
/// Return the first identity.
pub fn mail_identity_id(&self) -> Option<Id<IdentityObject>> {
self.identities.keys().next().cloned()
}
/// Return the account ID corresponding to the [`JMAP_MAIL_CAPABILITY`]
/// capability.
pub fn mail_account_id(&self) -> Id<Account> {
self.primary_accounts[JMAP_MAIL_CAPABILITY].clone()
}
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)] #[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct CapabilitiesObject { pub struct CapabilitiesObject {

Loading…
Cancel
Save