melib/jmap: Use Url instead of String in deserializing
Cargo manifest lints / Lint Cargo manifests on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Successful in 13m55s
Details
Run Tests / Test on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Successful in 18m21s
Details
Run cargo lints / Lint on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Successful in 8m7s
Details
Cargo manifest lints / Lint Cargo manifests on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Successful in 13m55s
Details
Run Tests / Test on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Successful in 18m21s
Details
Run cargo lints / Lint on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Successful in 8m7s
Details
Catch invalid URLs at the parsing stage. Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>pull/353/head
parent
417b24cd84
commit
51e3f163d4
|
@ -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]]
|
||||
|
|
|
@ -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"]
|
||||
|
|
|
@ -19,6 +19,9 @@
|
|||
* 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;
|
||||
#[macro_use]
|
||||
mod protocol_parser;
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
* 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;
|
||||
|
||||
|
@ -28,8 +28,7 @@ use crate::error::NetworkErrorKind;
|
|||
|
||||
#[derive(Debug)]
|
||||
pub struct JmapConnection {
|
||||
pub session: Arc<Mutex<Session>>,
|
||||
pub request_no: Arc<Mutex<usize>>,
|
||||
pub request_no: Arc<FutureMutex<usize>>,
|
||||
pub client: Arc<HttpClient>,
|
||||
pub server_conf: JmapServerConf,
|
||||
pub store: Arc<Store>,
|
||||
|
@ -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();
|
||||
|
||||
*self.store.online_status.lock().await = (Instant::now(), Ok(()));
|
||||
*self.session.lock().unwrap() = session;
|
||||
*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;
|
||||
|
||||
/* 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::<IdentityObject>::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::<IdentityObject>::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<Account> {
|
||||
self.session.lock().unwrap().primary_accounts[JMAP_MAIL_CAPABILITY].clone()
|
||||
}
|
||||
|
||||
pub fn mail_identity_id(&self) -> Option<Id<IdentityObject>> {
|
||||
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<FutureMappedMutexGuard<'_, (Instant, Result<Session>), 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::<EmailObject>::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::<EmailChanges, EmailObject>::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<MailboxObject>;
|
||||
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::<EmailQueryChanges, EmailObject>::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<MailboxObject> = 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::<EmailQueryChanges, EmailObject>::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::<SmallVec<[MailboxHash; 8]>>();
|
||||
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<isahc::Response<isahc::AsyncBody>> {
|
||||
pub async fn get_async(&self, url: &Url) -> Result<isahc::Response<isahc::AsyncBody>> {
|
||||
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<T: Into<Vec<u8>> + Send + Sync>(
|
||||
&self,
|
||||
api_url: Option<&str>,
|
||||
api_url: Option<&Url>,
|
||||
request: T,
|
||||
) -> Result<isahc::Response<isahc::AsyncBody>> {
|
||||
let request: Vec<u8> = 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?)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,6 +19,9 @@
|
|||
* 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::{
|
||||
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<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)]
|
||||
pub struct Store {
|
||||
pub account_name: Arc<String>,
|
||||
pub account_hash: AccountHash,
|
||||
pub main_identity: String,
|
||||
pub extra_identities: Vec<String>,
|
||||
pub account_id: Arc<Mutex<Id<Account>>>,
|
||||
pub byte_cache: Arc<Mutex<HashMap<EnvelopeHash, EnvelopeCache>>>,
|
||||
pub id_store: Arc<Mutex<HashMap<EnvelopeHash, Id<EmailObject>>>>,
|
||||
pub reverse_id_store: Arc<Mutex<HashMap<Id<EmailObject>, EnvelopeHash>>>,
|
||||
pub blob_id_store: Arc<Mutex<HashMap<EnvelopeHash, Id<BlobObject>>>>,
|
||||
pub byte_cache: Arc<FutureMutex<HashMap<EnvelopeHash, EnvelopeCache>>>,
|
||||
pub id_store: Arc<FutureMutex<HashMap<EnvelopeHash, Id<EmailObject>>>>,
|
||||
pub reverse_id_store: Arc<FutureMutex<HashMap<Id<EmailObject>, EnvelopeHash>>>,
|
||||
pub blob_id_store: Arc<FutureMutex<HashMap<EnvelopeHash, Id<BlobObject>>>>,
|
||||
pub collection: Collection,
|
||||
pub mailboxes: Arc<RwLock<HashMap<MailboxHash, JmapMailbox>>>,
|
||||
pub mailboxes_index: Arc<RwLock<HashMap<MailboxHash, HashSet<EnvelopeHash>>>>,
|
||||
pub mailbox_state: Arc<Mutex<State<MailboxObject>>>,
|
||||
pub online_status: Arc<FutureMutex<(Instant, Result<()>)>>,
|
||||
pub mailbox_state: Arc<FutureMutex<State<MailboxObject>>>,
|
||||
pub online_status: OnlineStatus,
|
||||
pub is_subscribed: Arc<IsSubscribedFn>,
|
||||
pub core_capabilities: Arc<Mutex<IndexMap<String, CapabilitiesObject>>>,
|
||||
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<TagHash> = 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<EmailObject>,
|
||||
) -> 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<EmailObject> = "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::<EmailObject>::try_from(v.method_responses.remove(0))?;
|
||||
let QueryResponse::<EmailObject> { 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::<MailboxObject>::new()
|
||||
.account_id(conn.mail_account_id())
|
||||
.account_id(mail_account_id)
|
||||
.create(Some({
|
||||
let id: Id<MailboxObject> = 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::<EmailObject>::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::<EmailObject>::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::<EmailObject>::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::<EmailObject>::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<EmailObject> = "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::<EmailSubmissionObject>::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<dyn Fn(&str) -> bool + Send + Sync>,
|
||||
event_consumer: BackendEventConsumer,
|
||||
) -> Result<Box<dyn MailBackend>> {
|
||||
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)?;
|
||||
|
|
|
@ -35,3 +35,6 @@ pub use identity::*;
|
|||
|
||||
mod 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)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct EmailSet {
|
||||
|
|
|
@ -114,81 +114,3 @@ pub struct IdentitySet(pub Set<IdentityObject>);
|
|||
impl Method<IdentityObject> 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::<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.
|
||||
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 {
|
||||
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 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())
|
||||
}))
|
||||
}
|
||||
|
|
|
@ -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<OBJ: Object> {
|
||||
pub trait Response<OBJ: Object>: Send + Sync {
|
||||
const NAME: &'static str;
|
||||
}
|
||||
|
||||
pub trait Method<OBJ: Object>: Serialize {
|
||||
pub trait Method<OBJ: Object>: Serialize + Send + Sync {
|
||||
const NAME: &'static str;
|
||||
}
|
||||
|
||||
|
@ -58,11 +49,21 @@ pub struct Request {
|
|||
method_calls: Vec<Value>,
|
||||
|
||||
#[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 {
|
||||
pub fn new(request_no: Arc<Mutex<usize>>) -> Self {
|
||||
pub fn new(request_no: Arc<FutureMutex<usize>>) -> Self {
|
||||
Self {
|
||||
using: USING,
|
||||
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);
|
||||
self.method_calls
|
||||
.push(serde_json::to_value((M::NAME, call, &format!("m{}", seq))).unwrap());
|
||||
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(
|
||||
|
@ -83,13 +92,14 @@ pub async fn get_mailboxes(
|
|||
request: Option<Request>,
|
||||
) -> Result<HashMap<MailboxHash, JmapMailbox>> {
|
||||
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::<MailboxObject>::new().account_id(conn.mail_account_id()));
|
||||
req.add_call(&mailbox_get);
|
||||
MailboxGet::new(Get::<MailboxObject>::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::<MailboxObject>::try_from(v.method_responses.remove(0))?;
|
||||
let GetResponse::<MailboxObject> {
|
||||
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<MailboxHash, JmapMailbox> = list
|
||||
.into_iter()
|
||||
.map(|r| {
|
||||
|
@ -169,9 +178,10 @@ pub async fn get_message_list(
|
|||
conn: &mut JmapConnection,
|
||||
mailbox: &JmapMailbox,
|
||||
) -> Result<Vec<Id<EmailObject>>> {
|
||||
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::<EmailObject>::try_from(v.method_responses.remove(0))?;
|
||||
let QueryResponse::<EmailObject> { 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());
|
||||
|
|
|
@ -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<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
|
||||
S: Serializer,
|
||||
{
|
||||
|
@ -413,7 +422,7 @@ pub struct GetResponse<OBJ: Object> {
|
|||
impl<OBJ: Object + DeserializeOwned> std::convert::TryFrom<&RawValue> for GetResponse<OBJ> {
|
||||
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())?;
|
||||
assert_eq!(&res.0, &format!("{}/get", OBJ::NAME));
|
||||
Ok(res.1)
|
||||
|
@ -527,7 +536,7 @@ pub struct QueryResponse<OBJ: Object> {
|
|||
impl<OBJ: Object + DeserializeOwned> std::convert::TryFrom<&RawValue> for QueryResponse<OBJ> {
|
||||
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())?;
|
||||
assert_eq!(&res.0, &format!("{}/query", OBJ::NAME));
|
||||
Ok(res.1)
|
||||
|
@ -657,7 +666,7 @@ pub struct ChangesResponse<OBJ: Object> {
|
|||
impl<OBJ: Object + DeserializeOwned> std::convert::TryFrom<&RawValue> for ChangesResponse<OBJ> {
|
||||
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())?;
|
||||
assert_eq!(&res.0, &format!("{}/changes", OBJ::NAME));
|
||||
Ok(res.1)
|
||||
|
@ -803,7 +812,7 @@ where
|
|||
}
|
||||
|
||||
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
|
||||
S: Serializer,
|
||||
{
|
||||
|
@ -898,7 +907,7 @@ pub struct SetResponse<OBJ: Object> {
|
|||
impl<OBJ: Object + DeserializeOwned> std::convert::TryFrom<&RawValue> for SetResponse<OBJ> {
|
||||
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())?;
|
||||
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<Account>,
|
||||
blob_id: &Id<BlobObject>,
|
||||
name: Option<String>,
|
||||
) -> String {
|
||||
) -> Result<Url> {
|
||||
// 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
|
||||
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: &str, account_id: &Id<Account>) -> String {
|
||||
pub fn upload_request_format(upload_url: &Url, account_id: &Id<Account>) -> Result<Url> {
|
||||
//"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<Account>) -> 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)]
|
||||
|
|
|
@ -62,154 +62,3 @@ impl<T: Clone + PartialEq + Eq + Hash> From<T> for Argument<T> {
|
|||
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::*;
|
||||
|
||||
pub trait FilterTrait<T>: Default {}
|
||||
pub trait FilterTrait<T>: Default + Send + Sync {}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[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 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<String, CapabilitiesObject>,
|
||||
|
@ -38,11 +39,11 @@ pub struct Session {
|
|||
#[serde(skip)]
|
||||
pub identities: IndexMap<Id<IdentityObject>, IdentityObject>,
|
||||
pub username: String,
|
||||
pub api_url: Arc<String>,
|
||||
pub download_url: Arc<String>,
|
||||
pub api_url: Arc<Url>,
|
||||
pub download_url: Arc<Url>,
|
||||
|
||||
pub upload_url: Arc<String>,
|
||||
pub event_source_url: Arc<String>,
|
||||
pub upload_url: Arc<Url>,
|
||||
pub event_source_url: Arc<Url>,
|
||||
pub state: State<Session>,
|
||||
#[serde(flatten)]
|
||||
pub extra_properties: IndexMap<String, Value>,
|
||||
|
@ -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<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)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CapabilitiesObject {
|
||||
|
|
Loading…
Reference in New Issue