jmap: implement server_submission #279
|
@ -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"]
|
||||
|
|
|
@ -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)> {
|
||||
|
|
|
@ -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?)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -29,3 +29,9 @@ pub use mailbox::*;
|
|||
|
||||
mod thread;
|
||||
pub use thread::*;
|
||||
|
||||
mod identity;
|
||||
pub use identity::*;
|
||||
|
||||
mod submission;
|
||||
pub use submission::*;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>,
|
||||
|
|
|
@ -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"
|
||||
]
|
||||
}},
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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";
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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(),
|
||||
|
|
|
@ -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 };
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
]
|
||||
}},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>,
|
||||
}
|
|
@ -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;
|
||||
|
|
Loading…
Reference in New Issue