melib/jmap: implement Backend::submit(), server-side submission
Tests / Test on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (pull_request) Successful in 14m12s Details
Tests / Test on ${{ matrix.build }} (linux-amd64, ubuntu-latest, stable, x86_64-unknown-linux-gnu) (push) Successful in 8m3s Details

Well this was more complex that it should have been. And not very
optimized because we're not using pipelining in the submit() path:

1. first upload email bytes as a Blob object. This requires a standalone
   API post call at a specific url so it cannot be changed with followup
   calls to reference the blob's id.
2. Create an EmailObject in the drafts folder.
3. Create an EmailSubmission object referencing the email id of prevous
   call. Unfortunately I cannot get the Result Reference to work in
   stalwart jmap, so for now this is too a separate transaction.

Caveat emptor: Errors might not be returned to the user.

Closes #277.

https://git.meli.delivery/meli/meli/issues/277

https://git.meli.delivery/meli/meli/pulls/279

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
pull/279/head
Manos Pitsidianakis 2023-08-28 14:43:23 +03:00
parent 38bc1369cc
commit 59513b2670
Signed by: Manos Pitsidianakis
GPG Key ID: 7729C7707F7E09D0
9 changed files with 1041 additions and 21 deletions

View File

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

View File

@ -19,7 +19,7 @@
* along with meli. If not, see <http://www.gnu.org/licenses/>.
*/
use std::sync::MutexGuard;
use std::{convert::TryFrom, sync::MutexGuard};
use isahc::config::Configurable;
@ -204,6 +204,89 @@ impl JmapConnection {
*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(())
}
@ -211,6 +294,16 @@ impl JmapConnection {
self.session.lock().unwrap().primary_accounts[JMAP_MAIL_CAPABILITY].clone()
}
pub fn mail_identity_id(&self) -> Option<Id<IdentityObject>> {
self.session
.lock()
.unwrap()
.identities
.keys()
.next()
.cloned()
}
pub fn session_guard(&'_ self) -> MutexGuard<'_, Session> {
self.session.lock().unwrap()
}

View File

@ -31,7 +31,7 @@ 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::{
@ -189,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>>>>,
@ -926,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 {
@ -945,6 +1111,8 @@ impl JmapType {
let store = Arc::new(Store {
account_name: Arc::new(s.name.clone()),
account_hash,
main_identity: s.make_display_name(),
extra_identities: s.extra_identities.clone(),
account_id: Arc::new(Mutex::new(Id::empty())),
online_status,
event_consumer,

View File

@ -32,3 +32,6 @@ pub use thread::*;
mod identity;
pub use identity::*;
mod submission;
pub use submission::*;

View File

@ -25,7 +25,7 @@ use super::*;
///
/// An *Identity* object stores information about an email address or domain the
/// user may send from.
#[derive(Deserialize, Serialize, Debug, Clone)]
#[derive(Deserialize, Serialize, Debug, Clone, Default)]
#[serde(rename_all = "camelCase")]
pub struct IdentityObject {
/// id: `Id` (immutable; server-set)
@ -87,13 +87,15 @@ impl Object for IdentityObject {
const NAME: &'static str = "Identity";
}
#[derive(Serialize, Clone, Copy, Debug)]
#[serde(rename_all = "camelCase")]
pub struct IdentityGet;
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.
@ -105,18 +107,88 @@ impl Method<IdentityObject> for IdentityGet {
/// o "forbiddenFrom": The user is not allowed to send from the address
/// given as the "email" property of the Identity.
/// ```
#[derive(Serialize, Clone, Copy, Debug)]
#[serde(rename_all = "camelCase")]
pub struct IdentitySet;
#[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";
}
#[derive(Serialize, Clone, Copy, Debug)]
#[serde(rename_all = "camelCase")]
pub struct IdentityChanges;
#[cfg(test)]
mod tests {
use std::sync::{Arc, Mutex};
impl Method<IdentityObject> for IdentityChanges {
const NAME: &'static str = "Identity/changes";
use serde_json::json;
use crate::jmap::*;
#[test]
fn test_jmap_identity_methods() {
let account_id = "blahblah";
let prev_seq = 33;
let main_identity = "user@example.com";
let mut req = Request::new(Arc::new(Mutex::new(prev_seq)));
let identity_set = IdentitySet(
Set::<IdentityObject>::new()
.account_id(account_id.into())
.create(Some({
let id: Id<IdentityObject> = main_identity.into();
let address = crate::email::Address::try_from(main_identity)
.unwrap_or_else(|_| crate::email::Address::new(None, main_identity.into()));
indexmap! {
id.clone().into() => IdentityObject {
id,
name: address.get_display_name().unwrap_or_default(),
email: address.get_email(),
..IdentityObject::default()
}
}
})),
);
req.add_call(&identity_set);
let identity_get = IdentityGet::new().account_id(account_id.into());
req.add_call(&identity_get);
assert_eq!(
json! {&req},
json! {{
"methodCalls" : [
[
"Identity/set",
{
"accountId" : account_id,
"create" : {
"user@example.com" : {
"bcc" : null,
"email" : main_identity,
"htmlSignature" : "",
"name" : "",
"replyTo" : null,
"textSignature" : ""
}
},
"destroy" : null,
"ifInState" : null,
"update" : null
},
"m33"
],
[
"Identity/get",
{
"accountId": account_id
},
"m34"
]
],
"using" : [
"urn:ietf:params:jmap:core",
"urn:ietf:params:jmap:mail"
]
}},
);
}
}

View File

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

View File

@ -682,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
@ -802,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> {

View File

@ -62,3 +62,154 @@ impl<T: Clone + PartialEq + Eq + Hash> From<T> for Argument<T> {
Self::Value(v)
}
}
#[cfg(test)]
mod tests {
use std::sync::{Arc, Mutex};
use serde_json::json;
use crate::jmap::*;
#[test]
fn test_jmap_argument_serde() {
let account_id = "blahblah";
let blob_id: Id<BlobObject> = Id::new_uuid_v4();
let draft_mailbox_id: Id<MailboxObject> = Id::new_uuid_v4();
let sent_mailbox_id: Id<MailboxObject> = Id::new_uuid_v4();
let prev_seq = 33;
let mut req = Request::new(Arc::new(Mutex::new(prev_seq)));
let creation_id: Id<EmailObject> = "1".into();
let import_call: EmailImport =
EmailImport::new()
.account_id(account_id.into())
.emails(indexmap! {
creation_id =>
EmailImportObject::new()
.blob_id(blob_id.clone())
.keywords(indexmap! {
"$draft".to_string() => true,
})
.mailbox_ids(indexmap! {
draft_mailbox_id.clone() => true,
}),
});
let prev_seq = req.add_call(&import_call);
let subm_set_call: EmailSubmissionSet = EmailSubmissionSet::new(
Set::<EmailSubmissionObject>::new()
.account_id(account_id.into())
.create(Some(indexmap! {
Argument::from(Id::from("k1490")) => EmailSubmissionObject::new(
/* account_id: */ account_id.into(),
/* identity_id: */ account_id.into(),
/* email_id: */ Argument::reference::<EmailImport, EmailObject, EmailObject>(prev_seq, ResultField::<EmailImport, EmailObject>::new("/id")),
/* envelope: */ None,
/* undo_status: */ None
)
})),
)
.on_success_update_email(Some(
indexmap! {
"#k1490".into() => json!({
format!("mailboxIds/{draft_mailbox_id}"): null,
format!("mailboxIds/{sent_mailbox_id}"): true,
"keywords/$draft": null
})
}
));
_ = req.add_call(&subm_set_call);
assert_eq!(
json! {&subm_set_call},
json! {{
"accountId": account_id,
"create": {
"k1490": {
"#emailId": {
"name": "Email/import",
"path":"/id",
"resultOf":"m33"
},
"envelope": null,
"identityId": account_id,
"undoStatus": "final"
}
},
"destroy": null,
"ifInState": null,
"onSuccessDestroyEmail": null,
"onSuccessUpdateEmail": {
"#k1490": {
"keywords/$draft": null,
format!("mailboxIds/{draft_mailbox_id}"): null,
format!("mailboxIds/{sent_mailbox_id}"): true
}
},
"update": null,
}},
);
assert_eq!(
json! {&req},
json! {{
"methodCalls": [
[
"Email/import",
{
"accountId": account_id,
"emails": {
"1": {
"blobId": blob_id.to_string(),
"keywords": {
"$draft": true
},
"mailboxIds": {
draft_mailbox_id.to_string(): true
},
"receivedAt": null
}
}
},
"m33"
],
[
"EmailSubmission/set",
{
"accountId": account_id,
"create": {
"k1490": {
"#emailId": {
"name": "Email/import",
"path": "/id",
"resultOf": "m33"
},
"envelope": null,
"identityId": account_id,
"undoStatus": "final"
}
},
"destroy": null,
"ifInState": null,
"onSuccessDestroyEmail": null,
"onSuccessUpdateEmail": {
"#k1490": {
"keywords/$draft": null,
format!("mailboxIds/{draft_mailbox_id}"): null,
format!("mailboxIds/{sent_mailbox_id}"): true
}
},
"update": null
},
"m34"
]
],
"using": [
"urn:ietf:params:jmap:core",
"urn:ietf:params:jmap:mail"
]
}},
);
}
}

View File

@ -24,7 +24,10 @@ use std::sync::Arc;
use indexmap::IndexMap;
use serde_json::Value;
use crate::jmap::rfc8620::{Account, Id, Object, State};
use crate::jmap::{
rfc8620::{Account, Id, Object, State},
IdentityObject,
};
#[derive(Deserialize, Serialize, Debug, Clone, Default)]
#[serde(rename_all = "camelCase")]
@ -32,6 +35,8 @@ 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>,