jmap: implement server_submission #279

Merged
Manos Pitsidianakis merged 13 commits from jmap-submission into master 2023-08-28 17:39:33 +03:00
16 changed files with 1614 additions and 258 deletions

View File

@ -71,6 +71,7 @@ http-static = ["isahc", "isahc/static-curl"]
imap = ["imap-codec", "tls"]
imap-trace = ["imap"]
jmap = ["http"]
jmap-trace = ["jmap"]
nntp = ["tls"]
nntp-trace = ["nntp"]
maildir = ["notify"]

View File

@ -27,6 +27,7 @@ use serde::{Deserialize, Deserializer, Serialize, Serializer};
use crate::{
backends::SpecialUsageMailbox,
email::Address,
error::{Error, Result},
};
pub use crate::{SortField, SortOrder};
@ -55,11 +56,7 @@ impl AccountSettings {
/// Create the account's display name from fields
/// [`AccountSettings::identity`] and [`AccountSettings::display_name`].
pub fn make_display_name(&self) -> String {
if let Some(d) = self.display_name.as_ref() {
format!("{} <{}>", d, self.identity)
} else {
self.identity.to_string()
}
Address::new(self.display_name.clone(), self.identity.clone()).to_string()
}
pub fn order(&self) -> Option<(SortField, SortOrder)> {

View File

@ -19,7 +19,7 @@
* along with meli. If not, see <http://www.gnu.org/licenses/>.
*/
use std::sync::MutexGuard;
use std::{convert::TryFrom, sync::MutexGuard};
use isahc::config::Configurable;
@ -28,11 +28,12 @@ use crate::error::NetworkErrorKind;
#[derive(Debug)]
pub struct JmapConnection {
pub session: Arc<Mutex<JmapSession>>,
pub session: Arc<Mutex<Session>>,
pub request_no: Arc<Mutex<usize>>,
pub client: Arc<HttpClient>,
pub server_conf: JmapServerConf,
pub store: Arc<Store>,
pub last_method_response: Option<String>,
}
impl JmapConnection {
@ -73,6 +74,7 @@ impl JmapConnection {
client: Arc::new(client),
server_conf,
store,
last_method_response: None,
})
}
@ -151,7 +153,7 @@ impl JmapConnection {
Ok(s) => s,
};
let session: JmapSession = match deserialize_from_str(&res_text) {
let session: Session = match deserialize_from_str(&res_text) {
Err(err) => {
let err = Error::new(format!(
"Could not connect to JMAP server endpoint for {}. Is your server url setting \
@ -182,8 +184,6 @@ impl JmapConnection {
*self.store.online_status.lock().await = (Instant::now(), Err(err.clone()));
return Err(err);
}
*self.store.core_capabilities.lock().unwrap() =
session.capabilities[JMAP_CORE_CAPABILITY].clone();
if !session.capabilities.contains_key(JMAP_MAIL_CAPABILITY) {
let err = Error::new(format!(
"Server {} does not support JMAP Mail capability ({mail_capability}). Returned \
@ -200,9 +200,93 @@ impl JmapConnection {
*self.store.online_status.lock().await = (Instant::now(), Err(err.clone()));
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;
/* 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 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()));
return Err(err);
}
Ok(s) => s,
};
let GetResponse::<IdentityObject> { list, .. } =
GetResponse::<IdentityObject>::try_from(v.method_responses.remove(0))?;
list
};
if id_list.is_empty() {
let mut req = Request::new(self.request_no.clone());
let identity_set = IdentitySet(
Set::<IdentityObject>::new()
.account_id(self.mail_account_id())
.create(Some({
let address =
crate::email::Address::try_from(self.store.main_identity.as_str())
.unwrap_or_else(|_| {
crate::email::Address::new(
None,
self.store.main_identity.clone(),
)
});
let id: Id<IdentityObject> = Id::new_uuid_v4();
log::trace!(
"identity id = {}, {:#?}",
id,
IdentityObject {
id: id.clone(),
name: address.get_display_name().unwrap_or_default(),
email: address.get_email(),
..IdentityObject::default()
}
);
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 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()));
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 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()));
return Err(err);
}
Ok(s) => s,
};
let GetResponse::<IdentityObject> { list, .. } =
GetResponse::<IdentityObject>::try_from(v.method_responses.remove(0))?;
id_list = list;
}
self.session.lock().unwrap().identities =
id_list.into_iter().map(|id| (id.id.clone(), id)).collect();
Ok(())
}
@ -210,7 +294,17 @@ impl JmapConnection {
self.session.lock().unwrap().primary_accounts[JMAP_MAIL_CAPABILITY].clone()
}
pub fn session_guard(&'_ self) -> MutexGuard<'_, JmapSession> {
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()
}
@ -242,7 +336,11 @@ impl JmapConnection {
let prev_seq = req.add_call(&email_changes_call);
let email_get_call: EmailGet = EmailGet::new(
Get::new()
.ids(Some(JmapArgument::reference(
.ids(Some(Argument::reference::<
EmailChanges,
EmailObject,
EmailObject,
>(
prev_seq,
ResultField::<EmailChanges, EmailObject>::new("/created"),
)))
@ -250,20 +348,26 @@ impl JmapConnection {
);
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()))
.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(JmapArgument::reference(
.ids(Some(Argument::reference::<
EmailQueryChanges,
EmailObject,
EmailObject,
>(
seq_no,
ResultField::<EmailQueryChanges, EmailObject>::new("/removed"),
)))
@ -280,14 +384,15 @@ impl JmapConnection {
} else {
return Ok(());
}
let api_url = self.session.lock().unwrap().api_url.clone();
let mut res = self
.client
.post_async(api_url.as_str(), serde_json::to_string(&req)?)
.await?;
let mut res = self.post_async(None, serde_json::to_string(&req)?).await?;
let res_text = res.text().await?;
debug!(&res_text);
if cfg!(feature = "jmap-trace") {
log::trace!(
"email_changes(): for mailbox {mailbox_hash} response {:?}",
res_text
);
}
let mut v: MethodResponse = match deserialize_from_str(&res_text) {
Err(err) => {
*self.store.online_status.lock().await = (Instant::now(), Err(err.clone()));
@ -410,8 +515,16 @@ impl JmapConnection {
}
Ok(_) => {}
Err(err) => {
debug!(mailbox_hash);
debug!(err);
log::error!(
"Could not deserialize EmailQueryChangesResponse from server response:
- mailbox_hash: {mailbox_hash}
- error: {err}
- debug details:
Json request was: {:?}
Json reply was: {}",
serde_json::to_string(&req),
res_text
);
}
}
let GetResponse::<EmailObject> { list, .. } =
@ -456,11 +569,15 @@ impl JmapConnection {
}
pub async fn send_request(&self, request: String) -> Result<String> {
let api_url = self.session.lock().unwrap().api_url.clone();
let mut res = self.client.post_async(api_url.as_str(), request).await?;
if cfg!(feature = "jmap-trace") {
log::trace!("send_request(): request {:?}", request);
}
let mut res = self.post_async(None, request).await?;
let res_text = res.text().await?;
debug!(&res_text);
if cfg!(feature = "jmap-trace") {
log::trace!("send_request(): response {:?}", res_text);
}
let _: MethodResponse = match deserialize_from_str(&res_text) {
Err(err) => {
log::error!("{}", &err);
@ -471,4 +588,34 @@ impl JmapConnection {
};
Ok(res_text)
}
pub async fn get_async(&self, url: &str) -> Result<isahc::Response<isahc::AsyncBody>> {
if cfg!(feature = "jmap-trace") {
let res = self.client.get_async(url).await;
log::trace!("get_async(): url `{}` response {:?}", url, res);
Ok(res?)
} else {
Ok(self.client.get_async(url).await?)
}
}
pub async fn post_async<T: Into<Vec<u8>> + Send + Sync>(
&self,
api_url: Option<&str>,
request: T,
) -> Result<isahc::Response<isahc::AsyncBody>> {
let request: Vec<u8> = request.into();
if cfg!(feature = "jmap-trace") {
log::trace!(
"post_async(): request {:?}",
String::from_utf8_lossy(&request)
);
}
if let Some(api_url) = api_url {
Ok(self.client.post_async(api_url, request).await?)
} else {
let api_url = self.session.lock().unwrap().api_url.clone();
Ok(self.client.post_async(api_url.as_str(), request).await?)
}
}
}

View File

@ -29,8 +29,9 @@ use std::{
};
use futures::{lock::Mutex as FutureMutex, Stream};
use indexmap::IndexMap;
use isahc::{config::RedirectPolicy, AsyncReadResponseExt, HttpClient};
use serde_json::Value;
use serde_json::{json, Value};
use smallvec::SmallVec;
use crate::{
@ -67,6 +68,7 @@ macro_rules! _impl {
pub const JMAP_CORE_CAPABILITY: &str = "urn:ietf:params:jmap:core";
pub const JMAP_MAIL_CAPABILITY: &str = "urn:ietf:params:jmap:mail";
pub const JMAP_SUBMISSION_CAPABILITY: &str = "urn:ietf:params:jmap:submission";
pub mod operations;
use operations::*;
@ -77,6 +79,9 @@ use connection::*;
pub mod protocol;
use protocol::*;
pub mod session;
use session::*;
pub mod rfc8620;
use rfc8620::*;
@ -184,6 +189,8 @@ impl JmapServerConf {
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>>>>,
@ -195,7 +202,7 @@ pub struct Store {
pub mailbox_state: Arc<Mutex<State<MailboxObject>>>,
pub online_status: Arc<FutureMutex<(Instant, Result<()>)>>,
pub is_subscribed: Arc<IsSubscribedFn>,
pub core_capabilities: Arc<Mutex<rfc8620::CapabilitiesObject>>,
pub core_capabilities: Arc<Mutex<IndexMap<String, CapabilitiesObject>>>,
pub event_consumer: BackendEventConsumer,
}
@ -291,9 +298,18 @@ impl MailBackend for JmapType {
supports_search: true,
extensions: None,
supports_tags: true,
supports_submission: false,
supports_submission: true,
};
CAPABILITIES
let supports_submission: bool = self
.store
.core_capabilities
.lock()
.map(|c| c.contains_key(JMAP_SUBMISSION_CAPABILITY))
.unwrap_or(false);
MailBackendCapabilities {
supports_submission: CAPABILITIES.supports_submission || supports_submission,
..CAPABILITIES
}
}
fn is_online(&self) -> ResultFuture<()> {
@ -321,11 +337,11 @@ impl MailBackend for JmapType {
Ok(Box::pin(async_stream::try_stream! {
let mut conn = connection.lock().await;
conn.connect().await?;
let batch_size: u64 = conn.store.core_capabilities.lock().unwrap().max_objects_in_get;
let batch_size: u64 = conn.store.core_capabilities.lock().unwrap()[JMAP_CORE_CAPABILITY].max_objects_in_get;
let mut fetch_state = protocol::EmailFetchState::Start { batch_size };
loop {
let res = fetch_state.fetch(
&conn,
&mut conn,
&store,
mailbox_hash,
).await?;
@ -383,7 +399,7 @@ impl MailBackend for JmapType {
let mut conn = connection.lock().await;
conn.connect().await?;
if store.mailboxes.read().unwrap().is_empty() {
let new_mailboxes = debug!(protocol::get_mailboxes(&conn).await)?;
let new_mailboxes = debug!(protocol::get_mailboxes(&mut conn, None).await)?;
*store.mailboxes.write().unwrap() = new_mailboxes;
}
@ -423,14 +439,13 @@ impl MailBackend for JmapType {
* 1. upload binary blob, get blobId
* 2. Email/import
*/
let (api_url, upload_url) = {
let lck = conn.session.lock().unwrap();
(lck.api_url.clone(), lck.upload_url.clone())
};
let upload_url = { conn.session.lock().unwrap().upload_url.clone() };
let mut res = conn
.client
.post_async(
&upload_request_format(upload_url.as_str(), &conn.mail_account_id()),
Some(&upload_request_format(
upload_url.as_str(),
&conn.mail_account_id(),
)),
bytes,
)
.await?;
@ -457,25 +472,19 @@ impl MailBackend for JmapType {
};
let mut req = Request::new(conn.request_no.clone());
let creation_id: Id<EmailObject> = "1".to_string().into();
let mut email_imports = HashMap::default();
let mut mailbox_ids = HashMap::default();
mailbox_ids.insert(mailbox_id, true);
email_imports.insert(
creation_id.clone(),
EmailImport::new()
.blob_id(upload_response.blob_id)
.mailbox_ids(mailbox_ids),
);
let import_call: ImportCall = ImportCall::new()
let import_call: EmailImport = EmailImport::new()
.account_id(conn.mail_account_id())
.emails(email_imports);
.emails(indexmap! {
creation_id.clone() => EmailImportObject::new()
.blob_id(upload_response.blob_id)
.mailbox_ids(indexmap! {
mailbox_id => true
})
});
req.add_call(&import_call);
let mut res = conn
.client
.post_async(api_url.as_str(), 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 mut v: MethodResponse = match deserialize_from_str(&res_text) {
@ -485,8 +494,8 @@ impl MailBackend for JmapType {
}
Ok(s) => s,
};
let m = ImportResponse::try_from(v.method_responses.remove(0)).map_err(|err| {
let ierr: Result<ImportError> = deserialize_from_str(&res_text);
let m = EmailImportResponse::try_from(v.method_responses.remove(0)).map_err(|err| {
let ierr: Result<EmailImportError> = deserialize_from_str(&res_text);
if let Ok(err) = ierr {
Error::new(format!("Could not save message: {:?}", err))
} else {
@ -494,7 +503,7 @@ impl MailBackend for JmapType {
}
})?;
if let Some(err) = m.not_created.get(&creation_id) {
if let Some(err) = m.not_created.and_then(|m| m.get(&creation_id).cloned()) {
return Err(Error::new(format!("Could not save message: {:?}", err)));
}
Ok(())
@ -549,12 +558,8 @@ impl MailBackend for JmapType {
let mut req = Request::new(conn.request_no.clone());
req.add_call(&email_call);
let api_url = conn.session.lock().unwrap().api_url.clone();
let mut res = conn
.client
.post_async(api_url.as_str(), 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 mut v: MethodResponse = match deserialize_from_str(&res_text) {
@ -584,11 +589,48 @@ impl MailBackend for JmapType {
fn create_mailbox(
&mut self,
_path: String,
path: String,
) -> ResultFuture<(MailboxHash, HashMap<MailboxHash, Mailbox>)> {
Err(Error::new(
"Creating mailbox is currently unimplemented for the JMAP backend.",
))
let store = self.store.clone();
let connection = self.connection.clone();
Ok(Box::pin(async move {
let mut conn = connection.lock().await;
let mailbox_set_call: MailboxSet = MailboxSet::new(
Set::<MailboxObject>::new()
.account_id(conn.mail_account_id())
.create(Some({
let id: Id<MailboxObject> = path.as_str().into();
indexmap! {
id.clone().into() => MailboxObject {
id,
name: path.clone(),
..MailboxObject::default()
}
}
})),
);
let mut req = Request::new(conn.request_no.clone());
let _prev_seq = req.add_call(&mailbox_set_call);
let new_mailboxes = protocol::get_mailboxes(&mut conn, Some(req)).await?;
*store.mailboxes.write().unwrap() = new_mailboxes;
let new_mailboxes: HashMap<MailboxHash, Mailbox> = store
.mailboxes
.read()
.unwrap()
.iter()
.filter(|(_, f)| f.is_subscribed)
.map(|(&h, f)| (h, BackendMailbox::clone(f) as Mailbox))
.collect();
let id = new_mailboxes
.values()
.find(|m| m.path() == path)
.map(|m| m.hash())
.unwrap();
Ok((id, new_mailboxes))
}))
}
fn delete_mailbox(
@ -650,8 +692,8 @@ impl MailBackend for JmapType {
mailboxes_lck[&destination_mailbox_hash].id.clone(),
)
};
let mut update_map: HashMap<Id<EmailObject>, Value> = HashMap::default();
let mut update_keywords: HashMap<String, Value> = HashMap::default();
let mut update_map: IndexMap<Argument<Id<EmailObject>>, Value> = IndexMap::default();
let mut update_keywords: IndexMap<String, Value> = IndexMap::default();
update_keywords.insert(
format!("mailboxIds/{}", &destination_mailbox_id),
serde_json::json!(true),
@ -667,12 +709,14 @@ impl MailBackend for JmapType {
if let Some(id) = store.id_store.lock().unwrap().get(&env_hash) {
// ids.push(id.clone());
// id_map.insert(id.clone(), env_hash);
update_map.insert(id.clone(), serde_json::json!(update_keywords.clone()));
update_map.insert(
Argument::from(id.clone()),
serde_json::json!(update_keywords.clone()),
);
}
}
}
let conn = connection.lock().await;
let api_url = conn.session.lock().unwrap().api_url.clone();
let email_set_call: EmailSet = EmailSet::new(
Set::<EmailObject>::new()
@ -683,10 +727,7 @@ impl MailBackend for JmapType {
let mut req = Request::new(conn.request_no.clone());
let _prev_seq = req.add_call(&email_set_call);
let mut res = conn
.client
.post_async(api_url.as_str(), 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?;
@ -723,10 +764,10 @@ impl MailBackend for JmapType {
let store = self.store.clone();
let connection = self.connection.clone();
Ok(Box::pin(async move {
let mut update_map: HashMap<Id<EmailObject>, Value> = HashMap::default();
let mut update_map: IndexMap<Argument<Id<EmailObject>>, Value> = IndexMap::default();
let mut ids: Vec<Id<EmailObject>> = Vec::with_capacity(env_hashes.rest.len() + 1);
let mut id_map: HashMap<Id<EmailObject>, EnvelopeHash> = HashMap::default();
let mut update_keywords: HashMap<String, Value> = HashMap::default();
let mut id_map: IndexMap<Id<EmailObject>, EnvelopeHash> = IndexMap::default();
let mut update_keywords: IndexMap<String, Value> = IndexMap::default();
for (flag, value) in flags.iter() {
match flag {
Ok(f) => {
@ -767,7 +808,10 @@ impl MailBackend for JmapType {
if let Some(id) = store.id_store.lock().unwrap().get(&hash) {
ids.push(id.clone());
id_map.insert(id.clone(), hash);
update_map.insert(id.clone(), serde_json::json!(update_keywords.clone()));
update_map.insert(
Argument::from(id.clone()),
serde_json::json!(update_keywords.clone()),
);
}
}
}
@ -783,18 +827,14 @@ impl MailBackend for JmapType {
req.add_call(&email_set_call);
let email_call: EmailGet = EmailGet::new(
Get::new()
.ids(Some(JmapArgument::Value(ids)))
.ids(Some(Argument::Value(ids)))
.account_id(conn.mail_account_id())
.properties(Some(vec!["keywords".to_string()])),
);
req.add_call(&email_call);
let api_url = conn.session.lock().unwrap().api_url.clone();
//debug!(serde_json::to_string(&req)?);
let mut res = conn
.client
.post_async(api_url.as_str(), 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?;
/*
@ -888,6 +928,170 @@ impl MailBackend for JmapType {
"Deleting messages is currently unimplemented for the JMAP backend.",
))
}
// [ref:TODO] add support for BLOB extension
fn submit(
&self,
bytes: Vec<u8>,
mailbox_hash: Option<MailboxHash>,
_flags: Option<Flag>,
) -> ResultFuture<()> {
let store = self.store.clone();
let connection = self.connection.clone();
Ok(Box::pin(async move {
// Steps:
//
// 1. upload blob/save to Draft as EmailObject
// 2. get id and make an EmailSubmissionObject
// 3. This call then sends the Email immediately, and if successful, removes the
// "$draft" flag and moves it from the Drafts folder to the Sent folder.
let (draft_mailbox_id, sent_mailbox_id) = {
let mailboxes_lck = store.mailboxes.read().unwrap();
let find_fn = |usage: SpecialUsageMailbox| -> Result<Id<MailboxObject>> {
if let Some(sent_folder) =
mailboxes_lck.values().find(|m| m.special_usage() == usage)
{
Ok(sent_folder.id.clone())
} else if let Some(sent_folder) = mailboxes_lck
.values()
.find(|m| m.special_usage() == SpecialUsageMailbox::Inbox)
{
Ok(sent_folder.id.clone())
} else {
Ok(mailboxes_lck
.values()
.next()
.ok_or_else(|| {
Error::new(format!(
"Account `{}` has no mailboxes.",
store.account_name
))
})?
.id
.clone())
}
};
(find_fn(SpecialUsageMailbox::Drafts)?, {
if let Some(h) = mailbox_hash {
if let Some(m) = mailboxes_lck.get(&h) {
m.id.clone()
} else {
return Err(Error::new(format!(
"Could not find mailbox with hash {h}",
)));
}
} else {
find_fn(SpecialUsageMailbox::Sent)?
}
})
};
let conn = connection.lock().await;
// [ref:TODO] smarter identity detection based on From: ?
let Some(identity_id) = conn.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 mut res = conn
.post_async(
Some(&upload_request_format(
upload_url.as_str(),
&conn.mail_account_id(),
)),
bytes,
)
.await?;
let res_text = res.text().await?;
let upload_response: UploadResponse = match deserialize_from_str(&res_text) {
Err(err) => {
*conn.store.online_status.lock().await = (Instant::now(), Err(err.clone()));
return Err(err);
}
Ok(s) => s,
};
{
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())
.emails(indexmap! {
creation_id => EmailImportObject::new()
.blob_id(upload_response.blob_id)
.keywords(indexmap! {
"$draft".to_string() => true,
"$seen".to_string() => true,
})
.mailbox_ids(indexmap! {
draft_mailbox_id.clone() => true,
}),
});
req.add_call(&import_call);
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()));
return Err(err);
}
Ok(s) => s,
};
// [ref:TODO] handle this better?
let res: Value = serde_json::from_str(v.method_responses[0].get())?;
let _new_state = res[1]["newState"].as_str().unwrap().to_string();
let _old_state = res[1]["oldState"].as_str().unwrap().to_string();
let email_id = Id::from(
res[1]["created"]["newid"]["id"]
.as_str()
.unwrap()
.to_string(),
);
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())
.create(Some(indexmap! {
Argument::from(Id::from("k1490")) => EmailSubmissionObject::new(
/* account_id: */ conn.mail_account_id(),
/* identity_id: */ identity_id,
/* email_id: */ email_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);
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()));
return Err(err);
}
Ok(s) => s,
};
}
Ok(())
}))
}
}
impl JmapType {
@ -907,12 +1111,14 @@ impl JmapType {
let store = Arc::new(Store {
account_name: Arc::new(s.name.clone()),
account_hash,
account_id: Arc::new(Mutex::new(Id::new())),
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)),
collection: Collection::default(),
core_capabilities: Arc::new(Mutex::new(rfc8620::CapabilitiesObject::default())),
core_capabilities: Arc::new(Mutex::new(IndexMap::default())),
byte_cache: Default::default(),
id_store: Default::default(),
reverse_id_store: Default::default(),

View File

@ -29,3 +29,9 @@ pub use mailbox::*;
mod thread;
pub use thread::*;
mod identity;
pub use identity::*;
mod submission;
pub use submission::*;

View File

@ -19,8 +19,9 @@
* along with meli. If not, see <http://www.gnu.org/licenses/>.
*/
use std::{collections::HashMap, marker::PhantomData};
use std::marker::PhantomData;
use indexmap::IndexMap;
use serde::de::{Deserialize, Deserializer};
use serde_json::{value::RawValue, Value};
@ -144,7 +145,7 @@ pub struct EmailObject {
#[serde(default)]
pub blob_id: Id<BlobObject>,
#[serde(default)]
pub mailbox_ids: HashMap<Id<MailboxObject>, bool>,
pub mailbox_ids: IndexMap<Id<MailboxObject>, bool>,
#[serde(default)]
pub size: u64,
#[serde(default)]
@ -168,7 +169,7 @@ pub struct EmailObject {
#[serde(default)]
pub references: Option<Vec<String>>,
#[serde(default)]
pub keywords: HashMap<String, bool>,
pub keywords: IndexMap<String, bool>,
#[serde(default)]
pub attached_emails: Option<Id<BlobObject>>,
#[serde(default)]
@ -177,7 +178,7 @@ pub struct EmailObject {
pub has_attachment: bool,
#[serde(default)]
#[serde(deserialize_with = "deserialize_header")]
pub headers: HashMap<String, String>,
pub headers: IndexMap<String, String>,
#[serde(default)]
pub html_body: Vec<HtmlBody>,
#[serde(default)]
@ -191,7 +192,7 @@ pub struct EmailObject {
#[serde(default)]
pub thread_id: Id<ThreadObject>,
#[serde(flatten)]
pub extra: HashMap<String, Value>,
pub extra: IndexMap<String, Value>,
}
/// Deserializer that uses `Default::default()` in place of a present but `null`
@ -207,10 +208,10 @@ where
}
impl EmailObject {
_impl!(get keywords, keywords: HashMap<String, bool>);
_impl!(get keywords, keywords: IndexMap<String, bool>);
}
#[derive(Deserialize, Serialize, Debug, Default)]
#[derive(Deserialize, Serialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Header {
pub name: String,
@ -219,7 +220,7 @@ pub struct Header {
fn deserialize_header<'de, D>(
deserializer: D,
) -> std::result::Result<HashMap<String, String>, D::Error>
) -> std::result::Result<IndexMap<String, String>, D::Error>
where
D: Deserializer<'de>,
{
@ -227,7 +228,7 @@ where
Ok(v.into_iter().map(|t| (t.name, t.value)).collect())
}
#[derive(Deserialize, Serialize, Debug, Default)]
#[derive(Deserialize, Serialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct EmailAddress {
pub email: String,

View File

@ -19,6 +19,7 @@
* along with meli. If not, see <http://www.gnu.org/licenses/>.
*/
use indexmap::IndexMap;
use serde_json::value::RawValue;
use super::*;
@ -34,7 +35,7 @@ use super::*;
/// The id of the account to use.
#[derive(Deserialize, Serialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct ImportCall {
pub struct EmailImport {
/// accountId: `Id`
/// The id of the account to use.
pub account_id: Id<Account>,
@ -48,22 +49,22 @@ pub struct ImportCall {
pub if_in_state: Option<State<EmailObject>>,
/// o emails: `Id[EmailImport]`
/// A map of creation id (client specified) to EmailImport objects.
pub emails: HashMap<Id<EmailObject>, EmailImport>,
pub emails: IndexMap<Id<EmailObject>, EmailImportObject>,
}
#[derive(Deserialize, Serialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct EmailImport {
pub struct EmailImportObject {
/// o blobId: `Id`
/// The id of the blob containing the raw message `RFC5322`.
pub blob_id: Id<BlobObject>,
/// o mailboxIds: `Id[Boolean]`
/// The ids of the Mailboxes to assign this Email to. At least one
/// Mailbox MUST be given.
pub mailbox_ids: HashMap<Id<MailboxObject>, bool>,
pub mailbox_ids: IndexMap<Id<MailboxObject>, bool>,
/// o keywords: `String[Boolean]` (default: {})
/// The keywords to apply to the Email.
pub keywords: HashMap<String, bool>,
pub keywords: IndexMap<String, bool>,
/// o receivedAt: `UTCDate` (default: time of most recent Received
/// header, or time of import on server if none)
@ -71,12 +72,12 @@ pub struct EmailImport {
pub received_at: Option<String>,
}
impl ImportCall {
impl EmailImport {
pub fn new() -> Self {
Self {
account_id: Id::new(),
account_id: Id::empty(),
if_in_state: None,
emails: HashMap::default(),
emails: IndexMap::default(),
}
}
@ -87,33 +88,7 @@ impl ImportCall {
account_id: Id<Account>
);
_impl!(if_in_state: Option<State<EmailObject>>);
_impl!(emails: HashMap<Id<EmailObject>, EmailImport>);
}
impl Default for ImportCall {
fn default() -> Self {
Self::new()
}
}
impl Method<EmailObject> for ImportCall {
const NAME: &'static str = "Email/import";
}
impl EmailImport {
pub fn new() -> Self {
Self {
blob_id: Id::new(),
mailbox_ids: HashMap::default(),
keywords: HashMap::default(),
received_at: None,
}
}
_impl!(blob_id: Id<BlobObject>);
_impl!(mailbox_ids: HashMap<Id<MailboxObject>, bool>);
_impl!(keywords: HashMap<String, bool>);
_impl!(received_at: Option<String>);
_impl!(emails: IndexMap<Id<EmailObject>, EmailImportObject>);
}
impl Default for EmailImport {
@ -122,10 +97,36 @@ impl Default for EmailImport {
}
}
#[derive(Deserialize, Serialize, Debug)]
impl Method<EmailObject> for EmailImport {
const NAME: &'static str = "Email/import";
}
impl EmailImportObject {
pub fn new() -> Self {
Self {
blob_id: Id::empty(),
mailbox_ids: IndexMap::default(),
keywords: IndexMap::default(),
received_at: None,
}
}
_impl!(blob_id: Id<BlobObject>);
_impl!(mailbox_ids: IndexMap<Id<MailboxObject>, bool>);
_impl!(keywords: IndexMap<String, bool>);
_impl!(received_at: Option<String>);
}
impl Default for EmailImportObject {
fn default() -> Self {
Self::new()
}
}
#[derive(Deserialize, Serialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
#[serde(tag = "type")]
pub enum ImportError {
pub enum EmailImportError {
/// The server MAY forbid two Email objects with the same exact content
/// `RFC5322`, or even just with the same Message-ID `RFC5322`, to
/// coexist within an account. In this case, it MUST reject attempts to
@ -165,7 +166,7 @@ pub enum ImportError {
#[derive(Deserialize, Serialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct ImportResponse {
pub struct EmailImportResponse {
/// o accountId: `Id`
/// The id of the account used for this call.
pub account_id: Id<Account>,
@ -185,28 +186,28 @@ pub struct ImportResponse {
/// A map of the creation id to an object containing the `id`,
/// `blobId`, `threadId`, and `size` properties for each successfully
/// imported Email, or null if none.
pub created: HashMap<Id<EmailObject>, ImportEmailResult>,
pub created: Option<IndexMap<Id<EmailObject>, EmailImportResult>>,
/// o notCreated: `Id[SetError]|null`
/// A map of the creation id to a SetError object for each Email that
/// failed to be created, or null if all successful. The possible
/// errors are defined above.
pub not_created: HashMap<Id<EmailObject>, ImportError>,
pub not_created: Option<IndexMap<Id<EmailObject>, EmailImportError>>,
}
impl std::convert::TryFrom<&RawValue> for ImportResponse {
impl std::convert::TryFrom<&RawValue> for EmailImportResponse {
type Error = crate::error::Error;
fn try_from(t: &RawValue) -> Result<Self> {
let res: (String, Self, String) = deserialize_from_str(t.get())?;
assert_eq!(&res.0, &ImportCall::NAME);
assert_eq!(&res.0, &EmailImport::NAME);
Ok(res.1)
}
}
#[derive(Deserialize, Serialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct ImportEmailResult {
pub struct EmailImportResult {
pub id: Id<EmailObject>,
pub blob_id: Id<BlobObject>,
pub thread_id: Id<ThreadObject>,

View File

@ -0,0 +1,194 @@
/*
* meli - jmap module.
*
* Copyright 2023 Manos Pitsidianakis
*
* This file is part of meli.
*
* meli is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* meli is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with meli. If not, see <http://www.gnu.org/licenses/>.
*/
use super::*;
/// # Identity
///
/// An *Identity* object stores information about an email address or domain the
/// user may send from.
#[derive(Deserialize, Serialize, Debug, Clone, Default)]
#[serde(rename_all = "camelCase")]
pub struct IdentityObject {
/// id: `Id` (immutable; server-set)
/// The id of the `Identity`.
pub id: Id<IdentityObject>,
/// name: `String` (default: "")
/// The "From" name the client SHOULD use when creating a new Email
/// from this Identity.
#[serde(default)]
pub name: String,
/// email: `String` (immutable)
/// The "From" email address the client MUST use when creating a new
/// Email from this Identity. If the "mailbox" part of the address
/// (the section before the "@") is the single character "*" (e.g.,
/// "*@example.com"), the client may use any valid address ending in
/// that domain (e.g., "[email protected]").
pub email: String,
/// replyTo: `EmailAddress[]|null` (default: null)
/// The Reply-To value the client SHOULD set when creating a new Email
/// from this Identity.
#[serde(default)]
pub reply_to: Option<Vec<EmailAddress>>,
/// bcc: `EmailAddress[]|null` (default: null)
/// The Bcc value the client SHOULD set when creating a new Email from
/// this Identity.
#[serde(default)]
pub bcc: Option<Vec<EmailAddress>>,
/// textSignature: `String` (default: "")
/// A signature the client SHOULD insert into new plaintext messages
// that will be sent from this Identity. Clients MAY ignore this
// and/or combine this with a client-specific signature preference.
#[serde(default)]
pub text_signature: String,
/// htmlSignature: `String` (default: "")
/// A signature the client SHOULD insert into new HTML messages that
/// will be sent from this Identity. This text MUST be an HTML
/// snippet to be inserted into the "<body></body>" section of the
/// HTML. Clients MAY ignore this and/or combine this with a client-
/// specific signature preference.
#[serde(default)]
pub html_signature: String,
/// mayDelete: `Boolean` (server-set)
/// Is the user allowed to delete this Identity? Servers may wish to
/// set this to false for the user's username or other default
/// address. Attempts to destroy an Identity with "mayDelete: false"
/// will be rejected with a standard "forbidden" SetError.
#[serde(skip_serializing)]
pub may_delete: bool,
}
impl Object for IdentityObject {
const NAME: &'static str = "Identity";
}
pub type IdentityGet = Get<IdentityObject>;
impl Method<IdentityObject> for IdentityGet {
const NAME: &'static str = "Identity/get";
}
pub type IdentityChanges = Changes<IdentityObject>;
impl Method<IdentityObject> for IdentityChanges {
const NAME: &'static str = "Identity/changes";
}
// [ref:TODO]: implement `forbiddenFrom` error for Identity/set.
/// `IdentitySet` method.
///
/// ```text
/// This is a standard "/set" method as described in [RFC8620],
/// Section 5.3. The following extra SetError types are defined:
/// For "create":
/// o "forbiddenFrom": The user is not allowed to send from the address
/// given as the "email" property of the Identity.
/// ```
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase", transparent)]
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"
]
}},
);
}
}

View File

@ -27,7 +27,7 @@ impl Id<MailboxObject> {
}
}
#[derive(Deserialize, Serialize, Debug)]
#[derive(Deserialize, Serialize, Debug, Default)]
#[serde(rename_all = "camelCase")]
pub struct MailboxObject {
pub id: Id<MailboxObject>,
@ -60,6 +60,23 @@ pub struct JmapRights {
pub may_set_seen: bool,
pub may_submit: bool,
}
impl Default for JmapRights {
fn default() -> Self {
Self {
may_add_items: true,
may_create_child: true,
may_delete: true,
may_read_items: true,
may_remove_items: true,
may_rename: true,
may_set_keywords: true,
may_set_seen: true,
may_submit: true,
}
}
}
#[derive(Deserialize, Serialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct MailboxGet {
@ -75,3 +92,47 @@ impl MailboxGet {
impl Method<MailboxObject> for MailboxGet {
const NAME: &'static str = "Mailbox/get";
}
/// 2.5. Mailbox/set
///
/// This is a standard `/set` method as described in `[RFC8620]`,
/// Section 5.3 but with the following additional request argument:
///
///
/// The following extra SetError types are defined:
///
/// For `destroy`:
///
/// - `mailboxHasChild`: The Mailbox still has at least one child Mailbox. The
/// client MUST remove these before it can delete the parent Mailbox.
///
/// - `mailboxHasEmail`: The Mailbox has at least one Email assigned to it, and
/// the `onDestroyRemoveEmails` argument was false.
#[derive(Deserialize, Serialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct MailboxSet {
#[serde(flatten)]
pub set_call: Set<MailboxObject>,
/// onDestroyRemoveEmails: `Boolean` (default: false)
///
/// If false, any attempt to destroy a Mailbox that still has Emails
/// in it will be rejected with a `mailboxHasEmail` SetError. If
/// true, any Emails that were in the Mailbox will be removed from it,
/// and if in no other Mailboxes, they will be destroyed when the
/// Mailbox is destroyed.
#[serde(default)]
pub on_destroy_remove_emails: bool,
}
impl MailboxSet {
pub fn new(set_call: Set<MailboxObject>) -> Self {
Self {
set_call,
on_destroy_remove_emails: false,
}
}
}
impl Method<MailboxObject> for MailboxSet {
const NAME: &'static str = "Mailbox/set";
}

View File

@ -0,0 +1,499 @@
/*
* meli
*
* Copyright 2023 Manos Pitsidianakis
*
* This file is part of meli.
*
* meli is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* meli is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with meli. If not, see <http://www.gnu.org/licenses/>.
*/
use indexmap::IndexMap;
use serde::ser::{Serialize, SerializeStruct, Serializer};
use super::*;
/// `UndoStatus`
///
/// This represents whether the submission may be canceled. This is
/// server set on create and MUST be one of the following values:
/// * `pending`: It may be possible to cancel this submission.
/// * `final`: The message has been relayed to at least one recipient
/// in a manner that cannot be recalled. It is no longer possible
/// to cancel this submission.
/// * `canceled`: The submission was canceled and will not be
/// delivered to any recipient.
/// On systems that do not support unsending, the value of this
/// property will always be `final`. On systems that do support
/// canceling submission, it will start as `pending` and MAY
/// transition to `final` when the server knows it definitely cannot
/// recall the message, but it MAY just remain `pending`. If in
/// pending state, a client can attempt to cancel the submission by
/// setting this property to `canceled`; if the update succeeds, the
/// submission was successfully canceled, and the message has not been
/// delivered to any of the original recipients.
#[derive(Deserialize, Serialize, Default, Clone, Copy, Debug)]
#[serde(rename_all = "camelCase")]
pub enum UndoStatus {
/// It may be possible to cancel this submission.
Pending,
/// The message has been relayed to at least one recipient in a manner that
/// cannot be recalled. It is no longer possible to cancel this
/// submission.
#[default]
Final,
/// The submission was canceled and will not be delivered to any recipient.
Canceled,
}
/// This represents the delivery status for each of the submission's
/// recipients, if known. This property MAY not be supported by all
/// servers, in which case it will remain null. Servers that support
/// it SHOULD update the EmailSubmission object each time the status
/// of any of the recipients changes, even if some recipients are
/// still being retried.
#[derive(Deserialize, Serialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct DeliveryStatusObject {
/// The SMTP reply string returned for this recipient when the
/// server last tried to relay the message, or in a later Delivery
/// Status Notification (DSN, as defined in `[RFC3464]`) response for
/// the message. This SHOULD be the response to the RCPT TO stage,
/// unless this was accepted and the message as a whole was
/// rejected at the end of the DATA stage, in which case the DATA
/// stage reply SHOULD be used instead.
///
/// Multi-line SMTP responses should be concatenated to a single
/// string as follows:
///
/// + The hyphen following the SMTP code on all but the last line
/// is replaced with a space.
///
/// + Any prefix in common with the first line is stripped from
/// lines after the first.
///
/// + CRLF is replaced by a space.
///
/// For example:
///
/// 550-5.7.1 Our system has detected that this message is
/// 550 5.7.1 likely spam.
///
/// would become:
///
/// 550 5.7.1 Our system has detected that this message is likely spam.
///
/// For messages relayed via an alternative to SMTP, the server MAY
/// generate a synthetic string representing the status instead.
/// If it does this, the string MUST be of the following form:
///
/// + A 3-digit SMTP reply code, as defined in `[RFC5321]`,
/// Section 4.2.3.
///
/// + Then a single space character.
///
/// + Then an SMTP Enhanced Mail System Status Code as defined in
/// `[RFC3463]`, with a registry defined in `[RFC5248]`.
///
/// + Then a single space character.
///
/// + Then an implementation-specific information string with a
/// human-readable explanation of the response.
pub smtp_reply: String,
/// Represents whether the message has been successfully delivered
/// to the recipient.
pub delivered: String,
/// Represents whether the message has been displayed to the recipient.
pub displayed: Displayed,
}
/// Represents whether the message has been displayed to the recipient.
/// If a Message Disposition Notification (MDN) is received for
/// this recipient with Disposition-Type (as per `[RFC8098]`,
/// Section 3.2.6.2) equal to `displayed`, this property SHOULD be
/// set to `yes`.
#[derive(Deserialize, Serialize, Default, Clone, Copy, Debug)]
#[serde(rename_all = "camelCase")]
pub enum Displayed {
/// The recipient's system claims the message content has been displayed to
/// the recipient. Note that there is no guarantee that the recipient
/// has noticed, read, or understood the content.
Yes,
/// The display status is unknown. This is the initial value.
#[default]
Unknown,
}
/// Represents whether the message has been successfully delivered
/// to the recipient.
///
/// Note that successful relaying to an external SMTP server SHOULD
/// NOT be taken as an indication that the message has successfully
/// reached the final mail store. In this case though, the server
/// may receive a DSN response, if requested.
///
/// If a DSN is received for the recipient with Action equal to
/// `delivered`, as per `[RFC3464]`, Section 2.3.3, then the
/// `delivered` property SHOULD be set to `yes`; if the Action
/// equals `failed`, the property SHOULD be set to `no`. Receipt
/// of any other DSN SHOULD NOT affect this property.
///
/// The server MAY also set this property based on other feedback
/// channels.
#[derive(Deserialize, Serialize, Default, Clone, Copy, Debug)]
#[serde(rename_all = "camelCase")]
pub enum Delivered {
/// The message is in a local mail queue and the status will change once it
/// exits the local mail queues. The `smtpReply` property may still
/// change.
#[default]
Queued,
/// The message was successfully delivered to the mail store of the
/// recipient. The `smtpReply` property is final.
Yes,
/// Delivery to the recipient permanently failed. The `smtpReply` property
/// is final.
No,
/// The final delivery status is unknown, (e.g., it was relayed to an
/// external machine and no further information is available). The
/// `smtpReply` property may still change if a DSN arrives.
Unknown,
}
/// # Email Submission
///
/// An *EmailSubmission* object represents the submission of an Email for
/// delivery to one or more recipients. It has the following properties:
#[derive(Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct EmailSubmissionObject {
/// accountId: `Id`
/// The id of the account to use.
#[serde(skip_serializing)]
pub account_id: Id<Account>,
/// identityId: `Id` (immutable)
/// The id of the Identity to associate with this submission.
pub identity_id: Id<IdentityObject>,
/// The id of the Email to send. The Email being sent does not have
/// to be a draft, for example, when "redirecting" an existing Email
/// to a different address.
pub email_id: Argument<Id<EmailObject>>,
/// The Thread id of the Email to send. This is set by the server to
/// the `threadId` property of the Email referenced by the `emailId`.
#[serde(skip_serializing)]
pub thread_id: Id<ThreadObject>,
/// Information for use when sending via SMTP.
pub envelope: Option<EnvelopeObject>,
/// sendAt: `UTCDate` (immutable; server-set)
/// The date the submission was/will be released for delivery. If the
/// client successfully used `FUTURERELEASE` `[RFC4865]` with the
/// submission, this MUST be the time when the server will release the
/// message; otherwise, it MUST be the time the `EmailSubmission` was
/// created.
#[serde(skip_serializing)]
pub send_at: String,
/// This represents whether the submission may be canceled.
pub undo_status: UndoStatus,
/// deliveryStatus: `String[DeliveryStatus]|null` (server-set)
///
/// This represents the delivery status for each of the submission's
/// recipients, if known. This property MAY not be supported by all
/// servers, in which case it will remain null. Servers that support
/// it SHOULD update the `EmailSubmission` object each time the status
/// of any of the recipients changes, even if some recipients are
/// still being retried.
#[serde(skip_serializing)]
pub delivery_status: Option<IndexMap<String, DeliveryStatusObject>>,
/// dsnBlobIds: `Id[]` (server-set)
/// A list of blob ids for DSNs `[RFC3464]` received for this
/// submission, in order of receipt, oldest first. The blob is the
/// whole MIME message (with a top-level content-type of "multipart/
/// report"), as received.
#[serde(skip_serializing)]
pub dsn_blob_ids: Vec<Id<BlobObject>>,
/// mdnBlobIds: `Id[]` (server-set)
/// A list of blob ids for MDNs `[RFC8098]` received for this
/// submission, in order of receipt, oldest first. The blob is the
/// whole MIME message (with a top-level content-type of "multipart/
/// report"), as received.
#[serde(skip_serializing)]
pub mdn_blob_ids: Vec<Id<BlobObject>>,
}
impl Object for EmailSubmissionObject {
const NAME: &'static str = "EmailSubmission";
}
impl EmailSubmissionObject {
/// Create a new `EmailSubmissionObject`, with all the server-set fields
/// initialized as empty.
pub fn new(
account_id: Id<Account>,
identity_id: Id<IdentityObject>,
email_id: impl Into<Argument<Id<EmailObject>>>,
envelope: Option<EnvelopeObject>,
undo_status: Option<UndoStatus>,
) -> Self {
Self {
account_id,
identity_id,
email_id: email_id.into(),
thread_id: "".into(),
envelope,
send_at: String::new(),
undo_status: undo_status.unwrap_or_default(),
delivery_status: None,
dsn_blob_ids: vec![],
mdn_blob_ids: vec![],
}
}
}
impl Serialize for EmailSubmissionObject {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut state = serializer.serialize_struct(stringify! {EmailSubmissionObject}, 4)?;
state.serialize_field("identityId", &self.identity_id)?;
state.serialize_field(
if matches!(self.email_id, Argument::Value(_)) {
"emailId"
} else {
"#emailId"
},
&self.email_id,
)?;
state.serialize_field("envelope", &self.envelope)?;
state.serialize_field("undoStatus", &self.undo_status)?;
state.end()
}
}
#[derive(Serialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct EmailSubmissionSet {
#[serde(flatten)]
pub set_call: Set<EmailSubmissionObject>,
/// onSuccessUpdateEmail: `Id[PatchObject]|null`
/// A map of [`EmailSubmission`] id to an object containing properties to
/// update on the [`Email`](EmailObject) object referenced by the
/// [`EmailSubmission`] if the create/update/destroy succeeds. (For
/// references to EmailSubmissions created in the same
/// `/set` invocation, this is equivalent to a creation-reference, so the id
/// will be the creation id prefixed with a `#`.)
#[serde(default)]
pub on_success_update_email: Option<IndexMap<Id<EmailSubmissionObject>, PatchObject>>,
/// onSuccessDestroyEmail: `Id[]|null`
/// A list of EmailSubmission ids for which the Email with the
/// corresponding `emailId` should be destroyed if the create/update/
/// destroy succeeds. (For references to EmailSubmission creations,
/// this is equivalent to a creation-reference, so the id will be the
/// creation id prefixed with a `#`.)
#[serde(default)]
pub on_success_destroy_email: Option<Vec<Id<EmailSubmissionObject>>>,
}
impl Method<EmailSubmissionObject> for EmailSubmissionSet {
const NAME: &'static str = "EmailSubmission/set";
}
impl EmailSubmissionSet {
pub fn new(set_call: Set<EmailSubmissionObject>) -> Self {
Self {
set_call,
on_success_update_email: None,
on_success_destroy_email: None,
}
}
pub fn on_success_update_email(
self,
on_success_update_email: Option<IndexMap<Id<EmailSubmissionObject>, PatchObject>>,
) -> Self {
Self {
on_success_update_email,
..self
}
}
pub fn on_success_destroy_email(
self,
on_success_destroy_email: Option<Vec<Id<EmailSubmissionObject>>>,
) -> Self {
Self {
on_success_destroy_email,
..self
}
}
}
/// Information for use when sending via SMTP.
#[derive(Deserialize, Serialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct EnvelopeObject {
/// The email address to use as the return address in the SMTP
/// submission, plus any parameters to pass with the MAIL FROM
/// address. The JMAP server MAY allow the address to be the empty
/// string.
/// When a JMAP server performs an SMTP message submission, it MAY
/// use the same id string for the ENVID parameter `[RFC3461]` and
/// the EmailSubmission object id. Servers that do this MAY
/// replace a client-provided value for ENVID with a server-
/// provided value.
pub mail_from: Address,
/// The email addresses to send the message to, and any RCPT TO
/// parameters to pass with the recipient.
pub rcpt_to: Vec<Address>,
}
impl Object for EnvelopeObject {
const NAME: &'static str = "Envelope";
}
#[derive(Deserialize, Serialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct Address {
/// The email address being represented by the object. This is a
/// "Mailbox" as used in the Reverse-path or Forward-path of the
/// MAIL FROM or RCPT TO command in `[RFC5321]`.
pub email: String,
/// Any parameters to send with the email address (either mail-
/// parameter or rcpt-parameter as appropriate, as specified in
/// `[RFC5321]`). If supplied, each key in the object is a parameter
/// name, and the value is either the parameter value (type
/// `String`) or null if the parameter does not take a value. For
/// both name and value, any xtext or unitext encodings are removed
/// (see `[RFC3461]` and `[RFC6533]`) and JSON string encoding is
/// 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",
})
);
}
}

View File

@ -62,7 +62,6 @@ impl BackendOp for JmapOp {
conn.connect().await?;
let download_url = conn.session.lock().unwrap().download_url.clone();
let mut res = conn
.client
.get_async(&download_request_format(
download_url.as_str(),
&conn.mail_account_id(),

View File

@ -22,7 +22,7 @@
use std::convert::{TryFrom, TryInto};
use serde::Serialize;
use serde_json::{json, Value};
use serde_json::Value;
use super::{mailbox::JmapMailbox, *};
@ -78,17 +78,15 @@ impl Request {
}
}
pub async fn get_mailboxes(conn: &JmapConnection) -> Result<HashMap<MailboxHash, JmapMailbox>> {
let seq = get_request_no!(conn.request_no);
let res_text = conn
.send_request(serde_json::to_string(&json!({
"using": [JMAP_CORE_CAPABILITY, JMAP_MAIL_CAPABILITY],
"methodCalls": [["Mailbox/get", {
"accountId": conn.mail_account_id()
},
format!("#m{}",seq).as_str()]],
}))?)
.await?;
pub async fn get_mailboxes(
conn: &mut JmapConnection,
request: Option<Request>,
) -> Result<HashMap<MailboxHash, JmapMailbox>> {
let mut req = request.unwrap_or_else(|| Request::new(conn.request_no.clone()));
let mailbox_get: MailboxGet =
MailboxGet::new(Get::<MailboxObject>::new().account_id(conn.mail_account_id()));
req.add_call(&mailbox_get);
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(()));
@ -96,6 +94,7 @@ pub async fn get_mailboxes(conn: &JmapConnection) -> Result<HashMap<MailboxHash,
let GetResponse::<MailboxObject> {
list, account_id, ..
} = m;
conn.last_method_response = Some(res_text);
// Is account set as `personal`? (`isPersonal` property). Then, even if
// `isSubscribed` is false on a mailbox, it should be regarded as
// subscribed.
@ -167,7 +166,7 @@ pub async fn get_mailboxes(conn: &JmapConnection) -> Result<HashMap<MailboxHash,
}
pub async fn get_message_list(
conn: &JmapConnection,
conn: &mut JmapConnection,
mailbox: &JmapMailbox,
) -> Result<Vec<Id<EmailObject>>> {
let email_call: EmailQuery = EmailQuery::new(
@ -185,11 +184,7 @@ pub async fn get_message_list(
let mut req = Request::new(conn.request_no.clone());
req.add_call(&email_call);
let api_url = conn.session.lock().unwrap().api_url.clone();
let mut res = conn
.client
.post_async(api_url.as_str(), 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 mut v: MethodResponse = match deserialize_from_str(&res_text) {
@ -202,6 +197,7 @@ pub async fn get_message_list(
*conn.store.online_status.lock().await = (std::time::Instant::now(), Ok(()));
let m = QueryResponse::<EmailObject>::try_from(v.method_responses.remove(0))?;
let QueryResponse::<EmailObject> { ids, .. } = m;
conn.last_method_response = Some(res_text);
Ok(ids)
}
@ -209,15 +205,14 @@ pub async fn get_message_list(
pub async fn get_message(conn: &JmapConnection, ids: &[String]) -> Result<Vec<Envelope>> {
let email_call: EmailGet = EmailGet::new(
Get::new()
.ids(Some(JmapArgument::value(ids.to_vec())))
.ids(Some(Argument::value(ids.to_vec())))
.account_id(conn.mail_account_id().to_string()),
);
let mut req = Request::new(conn.request_no.clone());
req.add_call(&email_call);
let mut res = conn
.client
.post_async(&conn.session.api_url, serde_json::to_string(&req)?)
.post_async(None, serde_json::to_string(&req)?)
.await?;
let res_text = res.text().await?;
@ -273,7 +268,7 @@ impl EmailFetchState {
pub async fn fetch(
&mut self,
conn: &JmapConnection,
conn: &mut JmapConnection,
store: &Store,
mailbox_hash: MailboxHash,
) -> Result<Vec<Envelope>> {
@ -309,9 +304,12 @@ impl EmailFetchState {
let email_call: EmailGet = EmailGet::new(
Get::new()
.ids(Some(JmapArgument::reference(
prev_seq,
EmailQuery::RESULT_FIELD_IDS,
.ids(Some(Argument::reference::<
EmailQuery,
EmailObject,
EmailObject,
>(
prev_seq, EmailQuery::RESULT_FIELD_IDS
)))
.account_id(conn.mail_account_id().clone()),
);
@ -330,6 +328,7 @@ impl EmailFetchState {
let e =
GetResponse::<EmailObject>::try_from(v.method_responses.pop().unwrap())?;
let GetResponse::<EmailObject> { list, state, .. } = e;
conn.last_method_response = Some(res_text);
if self.must_update_state(conn, mailbox_hash, state).await? {
*self = Self::Start { batch_size };

View File

@ -22,25 +22,28 @@
use std::{
hash::{Hash, Hasher},
marker::PhantomData,
sync::Arc,
};
use indexmap::IndexMap;
use serde::{
de::DeserializeOwned,
ser::{Serialize, SerializeStruct, Serializer},
};
use serde_json::{value::RawValue, Value};
use crate::email::parser::BytesExt;
use crate::{email::parser::BytesExt, jmap::session::Session};
mod filters;
pub use filters::*;
mod comparator;
pub use comparator::*;
mod argument;
use std::collections::HashMap;
pub use argument::*;
pub type PatchObject = Value;
impl Object for PatchObject {
const NAME: &'static str = "PatchObject";
}
use super::{deserialize_from_str, protocol::Method};
pub trait Object {
@ -95,7 +98,7 @@ impl<OBJ> Hash for Id<OBJ> {
impl<OBJ> Default for Id<OBJ> {
fn default() -> Self {
Self::new()
Self::empty()
}
}
@ -108,6 +111,15 @@ impl<OBJ> From<String> for Id<OBJ> {
}
}
impl<OBJ> From<&str> for Id<OBJ> {
fn from(inner: &str) -> Self {
Self {
inner: inner.to_string(),
_ph: PhantomData,
}
}
}
impl<OBJ> std::fmt::Display for Id<OBJ> {
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
std::fmt::Display::fmt(&self.inner, fmt)
@ -115,13 +127,20 @@ impl<OBJ> std::fmt::Display for Id<OBJ> {
}
impl<OBJ> Id<OBJ> {
pub fn new() -> Self {
pub fn empty() -> Self {
Self {
inner: String::new(),
_ph: PhantomData,
}
}
pub fn new_uuid_v4() -> Self {
Self {
inner: uuid::Uuid::new_v4().hyphenated().to_string(),
_ph: PhantomData,
}
}
pub fn as_str(&self) -> &str {
self.inner.as_str()
}
@ -209,61 +228,19 @@ impl<OBJ> State<OBJ> {
}
}
#[derive(Deserialize, Serialize, Debug, Clone, Default)]
#[serde(rename_all = "camelCase")]
pub struct JmapSession {
pub capabilities: HashMap<String, CapabilitiesObject>,
pub accounts: HashMap<Id<Account>, Account>,
pub primary_accounts: HashMap<String, Id<Account>>,
pub username: String,
pub api_url: Arc<String>,
pub download_url: Arc<String>,
pub upload_url: Arc<String>,
pub event_source_url: Arc<String>,
pub state: State<JmapSession>,
#[serde(flatten)]
pub extra_properties: HashMap<String, Value>,
}
impl Object for JmapSession {
const NAME: &'static str = "Session";
}
#[derive(Deserialize, Serialize, Clone, Default, Debug)]
#[serde(rename_all = "camelCase")]
pub struct CapabilitiesObject {
#[serde(default)]
pub max_size_upload: u64,
#[serde(default)]
pub max_concurrent_upload: u64,
#[serde(default)]
pub max_size_request: u64,
#[serde(default)]
pub max_concurrent_requests: u64,
#[serde(default)]
pub max_calls_in_request: u64,
#[serde(default)]
pub max_objects_in_get: u64,
#[serde(default)]
pub max_objects_in_set: u64,
#[serde(default)]
pub collation_algorithms: Vec<String>,
}
#[derive(Deserialize, Serialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Account {
pub name: String,
pub is_personal: bool,
pub is_read_only: bool,
pub account_capabilities: HashMap<String, Value>,
pub account_capabilities: IndexMap<String, Value>,
#[serde(flatten)]
pub extra_properties: HashMap<String, Value>,
pub extra_properties: IndexMap<String, Value>,
}
impl Object for Account {
const NAME: &'static str = "Account";
const NAME: &'static str = stringify!(Account);
}
#[derive(Copy, Clone, Debug)]
@ -273,6 +250,14 @@ impl Object for BlobObject {
const NAME: &'static str = "Blob";
}
#[derive(Clone, Copy, Deserialize, Serialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct BlobGet;
impl Method<BlobObject> for BlobGet {
const NAME: &'static str = "Blob/get";
}
/// #`get`
///
/// Objects of type `Foo` are fetched via a call to `Foo/get`.
@ -291,7 +276,7 @@ where
pub account_id: Id<Account>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(flatten)]
pub ids: Option<JmapArgument<Vec<Id<OBJ>>>>,
pub ids: Option<Argument<Vec<Id<OBJ>>>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub properties: Option<Vec<String>>,
#[serde(skip)]
@ -304,7 +289,7 @@ where
{
pub fn new() -> Self {
Self {
account_id: Id::new(),
account_id: Id::empty(),
ids: None,
properties: None,
_ph: PhantomData,
@ -317,13 +302,13 @@ where
account_id: Id<Account>
);
_impl!(
/// - ids: `Option<JmapArgument<Vec<String>>>`
/// - ids: `Option<Argument<Vec<String>>>`
///
/// The ids of the Foo objects to return. If `None`, then *all*
/// records of the data type are returned, if this is
/// supported for that data type and the number of records
/// does not exceed the `max_objects_in_get` limit.
ids: Option<JmapArgument<Vec<Id<OBJ>>>>
ids: Option<Argument<Vec<Id<OBJ>>>>
);
_impl!(
/// - properties: `Option<Vec<String>>`
@ -370,11 +355,12 @@ impl<OBJ: Object + Serialize + std::fmt::Debug> Serialize for Get<OBJ> {
}
match self.ids.as_ref() {
None => {}
Some(JmapArgument::Value(ref v)) => state.serialize_field("ids", v)?,
Some(JmapArgument::ResultReference {
Some(Argument::Value(ref v)) => state.serialize_field("ids", v)?,
Some(Argument::ResultReference {
ref result_of,
ref name,
ref path,
..
}) => {
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
@ -409,9 +395,9 @@ pub struct MethodResponse<'a> {
#[serde(borrow)]
pub method_responses: Vec<&'a RawValue>,
#[serde(default)]
pub created_ids: HashMap<Id<String>, Id<String>>,
pub created_ids: IndexMap<Id<String>, Id<String>>,
#[serde(default)]
pub session_state: State<JmapSession>,
pub session_state: State<Session>,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
@ -479,7 +465,7 @@ where
{
pub fn new() -> Self {
Self {
account_id: Id::new(),
account_id: Id::empty(),
filter: None,
sort: None,
position: 0,
@ -558,7 +544,7 @@ pub struct ResultField<M: Method<OBJ>, OBJ: Object> {
}
impl<M: Method<OBJ>, OBJ: Object> ResultField<M, OBJ> {
pub fn new(field: &'static str) -> Self {
pub const fn new(field: &'static str) -> Self {
Self {
field,
_ph: PhantomData,
@ -566,22 +552,11 @@ impl<M: Method<OBJ>, OBJ: Object> ResultField<M, OBJ> {
}
}
// error[E0723]: trait bounds other than `Sized` on const fn parameters are
// unstable --> melib/src/backends/jmap/rfc8620.rs:626:6
// |
// 626 | impl<M: Method<OBJ>, OBJ: Object> ResultField<M, OBJ> {
// | ^
// |
// = note: for more information, see issue https://github.com/rust-lang/rust/issues/57563
// = help: add `#![feature(const_fn)]` to the crate attributes to enable
// impl<M: Method<OBJ>, OBJ: Object> ResultField<M, OBJ> {
// pub const fn new(field: &'static str) -> Self {
// Self {
// field,
// _ph: PhantomData,
// }
// }
// }
impl<M: Method<OBJ>, OBJ: Object> From<&'static str> for ResultField<M, OBJ> {
fn from(field: &'static str) -> Self {
Self::new(field)
}
}
/// #`changes`
///
@ -624,7 +599,7 @@ where
{
pub fn new() -> Self {
Self {
account_id: Id::new(),
account_id: Id::empty(),
since_state: State::new(),
max_changes: None,
_ph: PhantomData,
@ -707,7 +682,7 @@ impl<OBJ: Object> ChangesResponse<OBJ> {
/// and dependencies that may exist if doing multiple operations at once
/// (for example, to ensure there is always a minimum number of a certain
/// record type).
#[derive(Deserialize, Serialize, Clone, Debug)]
#[derive(Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Set<OBJ>
where
@ -736,7 +711,7 @@ where
///
/// The client MUST omit any properties that may only be set by the
/// server (for example, the `id` property on most object types).
pub create: Option<HashMap<Id<OBJ>, OBJ>>,
pub create: Option<IndexMap<Argument<Id<OBJ>>, OBJ>>,
/// o update: `Id[PatchObject]|null`
///
/// A map of an id to a Patch object to apply to the current Foo
@ -780,12 +755,12 @@ where
/// is also a valid PatchObject. The client may choose to optimise
/// network usage by just sending the diff or may send the whole
/// object; the server processes it the same either way.
pub update: Option<HashMap<Id<OBJ>, Value>>,
pub update: Option<IndexMap<Argument<Id<OBJ>>, PatchObject>>,
/// o destroy: `Id[]|null`
///
/// A list of ids for Foo objects to permanently delete, or null if no
/// objects are to be destroyed.
pub destroy: Option<Vec<Id<OBJ>>>,
pub destroy: Option<Vec<Argument<Id<OBJ>>>>,
}
impl<OBJ> Set<OBJ>
@ -794,7 +769,7 @@ where
{
pub fn new() -> Self {
Self {
account_id: Id::new(),
account_id: Id::empty(),
if_in_state: None,
create: None,
update: None,
@ -813,7 +788,9 @@ where
/// state.
if_in_state: Option<State<OBJ>>
);
_impl!(update: Option<HashMap<Id<OBJ>, Value>>);
_impl!(update: Option<IndexMap<Argument<Id<OBJ>>, PatchObject>>);
_impl!(create: Option<IndexMap<Argument<Id<OBJ>>, OBJ>>);
_impl!(destroy: Option<Vec<Argument<Id<OBJ>>>>);
}
impl<OBJ> Default for Set<OBJ>
@ -825,6 +802,38 @@ where
}
}
impl<OBJ: Object + Serialize + std::fmt::Debug> Serialize for Set<OBJ> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let fields_no = 5;
let mut state = serializer.serialize_struct("Set", fields_no)?;
state.serialize_field("accountId", &self.account_id)?;
state.serialize_field("ifInState", &self.if_in_state)?;
state.serialize_field("update", &self.update)?;
state.serialize_field("destroy", &self.destroy)?;
if let Some(ref m) = self.create {
let map = m
.into_iter()
.map(|(k, v)| {
let mut v = serde_json::json!(v);
if let Some(ref mut obj) = v.as_object_mut() {
obj.remove("id");
}
(k, v)
})
.collect::<IndexMap<_, Value>>();
state.serialize_field("create", &map)?;
} else {
state.serialize_field("create", &self.create)?;
}
state.end()
}
}
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct SetResponse<OBJ: Object> {
@ -837,7 +846,7 @@ pub struct SetResponse<OBJ: Object> {
/// The state string that would have been returned by `Foo/get` before
/// making the requested changes, or null if the server doesn't know
/// what the previous state string was.
pub old_state: State<OBJ>,
pub old_state: Option<State<OBJ>>,
/// o newState: `String`
///
/// The state string that will now be returned by `Foo/get`.
@ -851,7 +860,7 @@ pub struct SetResponse<OBJ: Object> {
/// and thus set to a default by the server.
///
/// This argument is null if no Foo objects were successfully created.
pub created: Option<HashMap<Id<OBJ>, OBJ>>,
pub created: Option<IndexMap<Id<OBJ>, OBJ>>,
/// o updated: `Id[Foo|null]|null`
///
/// The keys in this map are the ids of all Foos that were
@ -863,7 +872,7 @@ pub struct SetResponse<OBJ: Object> {
/// any changes to server-set or computed properties.
///
/// This argument is null if no Foo objects were successfully updated.
pub updated: Option<HashMap<Id<OBJ>, Option<OBJ>>>,
pub updated: Option<IndexMap<Id<OBJ>, Option<OBJ>>>,
/// o destroyed: `Id[]|null`
///
/// A list of Foo ids for records that were successfully destroyed, or

View File

@ -19,15 +19,18 @@
* along with meli. If not, see <http://www.gnu.org/licenses/>.
*/
use std::hash::Hash;
use crate::jmap::{
protocol::Method,
rfc8620::{Object, ResultField},
};
#[derive(Deserialize, Serialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub enum JmapArgument<T: Clone> {
#[derive(Deserialize, Serialize, Clone, PartialEq, Eq, Hash, Debug)]
#[serde(rename_all = "camelCase", untagged)]
pub enum Argument<T: Clone + PartialEq + Eq + Hash> {
Value(T),
#[serde(rename_all = "camelCase")]
ResultReference {
result_of: String,
name: String,
@ -35,14 +38,15 @@ pub enum JmapArgument<T: Clone> {
},
}
impl<T: Clone> JmapArgument<T> {
impl<T: Clone + PartialEq + Eq + Hash> Argument<T> {
pub fn value(v: T) -> Self {
Self::Value(v)
}
pub fn reference<M, OBJ>(result_of: usize, path: ResultField<M, OBJ>) -> Self
pub fn reference<M, OBJ, MethodOBJ>(result_of: usize, path: ResultField<M, MethodOBJ>) -> Self
where
M: Method<OBJ>,
M: Method<MethodOBJ>,
MethodOBJ: Object,
OBJ: Object,
{
Self::ResultReference {
@ -52,3 +56,160 @@ impl<T: Clone> JmapArgument<T> {
}
}
}
impl<T: Clone + PartialEq + Eq + Hash> From<T> for Argument<T> {
fn from(v: T) -> Self {
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"
]
}},
);
}
}

View File

@ -0,0 +1,74 @@
/*
* meli - jmap module.
*
* Copyright 2019 Manos Pitsidianakis
*
* This file is part of meli.
*
* meli is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* meli is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with meli. If not, see <http://www.gnu.org/licenses/>.
*/
use std::sync::Arc;
use indexmap::IndexMap;
use serde_json::Value;
use crate::jmap::{
rfc8620::{Account, Id, Object, State},
IdentityObject,
};
#[derive(Deserialize, Serialize, Debug, Clone, Default)]
#[serde(rename_all = "camelCase")]
pub struct Session {
pub capabilities: IndexMap<String, CapabilitiesObject>,
pub accounts: IndexMap<Id<Account>, Account>,
pub primary_accounts: IndexMap<String, Id<Account>>,
#[serde(skip)]
pub identities: IndexMap<Id<IdentityObject>, IdentityObject>,
pub username: String,
pub api_url: Arc<String>,
pub download_url: Arc<String>,
pub upload_url: Arc<String>,
pub event_source_url: Arc<String>,
pub state: State<Session>,
#[serde(flatten)]
pub extra_properties: IndexMap<String, Value>,
}
impl Object for Session {
const NAME: &'static str = stringify!(Session);
}
#[derive(Deserialize, Serialize, Clone, Default, Debug)]
#[serde(rename_all = "camelCase")]
pub struct CapabilitiesObject {
#[serde(default)]
pub max_size_upload: u64,
#[serde(default)]
pub max_concurrent_upload: u64,
#[serde(default)]
pub max_size_request: u64,
#[serde(default)]
pub max_concurrent_requests: u64,
#[serde(default)]
pub max_calls_in_request: u64,
#[serde(default)]
pub max_objects_in_get: u64,
#[serde(default)]
pub max_objects_in_set: u64,
#[serde(default)]
pub collation_algorithms: Vec<String>,
}

View File

@ -185,6 +185,7 @@ pub extern crate nom;
#[macro_use]
extern crate bitflags;
pub extern crate futures;
#[macro_use]
pub extern crate indexmap;
pub extern crate smallvec;
pub extern crate smol;