diff --git a/melib/src/conf.rs b/melib/src/conf.rs
index 7cf0a5e5..18c27ed7 100644
--- a/melib/src/conf.rs
+++ b/melib/src/conf.rs
@@ -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)> {
diff --git a/melib/src/jmap/connection.rs b/melib/src/jmap/connection.rs
index 57e6a501..c2b2c62a 100644
--- a/melib/src/jmap/connection.rs
+++ b/melib/src/jmap/connection.rs
@@ -19,7 +19,7 @@
* along with meli. If not, see .
*/
-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:: { list, .. } =
+ GetResponse::::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::::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 = 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:: { list, .. } =
+ GetResponse::::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> {
+ self.session
+ .lock()
+ .unwrap()
+ .identities
+ .keys()
+ .next()
+ .cloned()
+ }
+
pub fn session_guard(&'_ self) -> MutexGuard<'_, Session> {
self.session.lock().unwrap()
}
diff --git a/melib/src/jmap/mod.rs b/melib/src/jmap/mod.rs
index bcb06bfe..fe43af1a 100644
--- a/melib/src/jmap/mod.rs
+++ b/melib/src/jmap/mod.rs
@@ -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,
pub account_hash: AccountHash,
+ pub main_identity: String,
+ pub extra_identities: Vec,
pub account_id: Arc>>,
pub byte_cache: Arc>>,
pub id_store: Arc>>>,
@@ -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,
+ mailbox_hash: Option,
+ _flags: Option,
+ ) -> 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> {
+ 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 = "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::::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,
diff --git a/melib/src/jmap/objects.rs b/melib/src/jmap/objects.rs
index 8f61de5f..3e9635f8 100644
--- a/melib/src/jmap/objects.rs
+++ b/melib/src/jmap/objects.rs
@@ -32,3 +32,6 @@ pub use thread::*;
mod identity;
pub use identity::*;
+
+mod submission;
+pub use submission::*;
diff --git a/melib/src/jmap/objects/identity.rs b/melib/src/jmap/objects/identity.rs
index e4709565..b8cf5478 100644
--- a/melib/src/jmap/objects/identity.rs
+++ b/melib/src/jmap/objects/identity.rs
@@ -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;
impl Method for IdentityGet {
const NAME: &'static str = "Identity/get";
}
+pub type IdentityChanges = Changes;
+impl Method for IdentityChanges {
+ const NAME: &'static str = "Identity/changes";
+}
// [ref:TODO]: implement `forbiddenFrom` error for Identity/set.
/// `IdentitySet` method.
@@ -105,18 +107,88 @@ impl Method 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);
impl Method for IdentitySet {
const NAME: &'static str = "Identity/set";
}
-#[derive(Serialize, Clone, Copy, Debug)]
-#[serde(rename_all = "camelCase")]
-pub struct IdentityChanges;
-
-impl Method for IdentityChanges {
- const NAME: &'static str = "Identity/changes";
+#[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::::new()
+ .account_id(account_id.into())
+ .create(Some({
+ let id: Id = 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"
+ ]
+ }},
+ );
+ }
}
diff --git a/melib/src/jmap/objects/submission.rs b/melib/src/jmap/objects/submission.rs
new file mode 100644
index 00000000..2ba39606
--- /dev/null
+++ b/melib/src/jmap/objects/submission.rs
@@ -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 .
+ */
+
+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,
+ /// identityId: `Id` (immutable)
+ /// The id of the Identity to associate with this submission.
+ pub identity_id: Id,
+ /// 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>,
+ /// 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,
+ /// Information for use when sending via SMTP.
+ pub envelope: Option,
+ /// 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>,
+ /// 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>,
+ /// 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>,
+}
+
+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,
+ identity_id: Id,
+ email_id: impl Into>>,
+ envelope: Option,
+ undo_status: Option,
+ ) -> 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(&self, serializer: S) -> std::result::Result
+ 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,
+ /// 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, 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>>,
+}
+
+impl Method for EmailSubmissionSet {
+ const NAME: &'static str = "EmailSubmission/set";
+}
+
+impl EmailSubmissionSet {
+ pub fn new(set_call: Set) -> 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, PatchObject>>,
+ ) -> Self {
+ Self {
+ on_success_update_email,
+ ..self
+ }
+ }
+
+ pub fn on_success_destroy_email(
+ self,
+ on_success_destroy_email: Option>>,
+ ) -> 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,
+}
+
+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,
+}
+
+#[cfg(test)]
+mod tests {
+ use serde_json::json;
+
+ use super::*;
+
+ #[test]
+ fn test_jmap_undo_status() {
+ let account_id: Id = "blahblah".into();
+ let ident_id: Id = "sdusssssss".into();
+ let email_id: Id = 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 = "blahblah".into();
+ let ident_id: Id = "sdusssssss".into();
+ let email_id: Id = 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::(
+ 42,
+ ResultField::::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",
+ })
+ );
+ }
+}
diff --git a/melib/src/jmap/rfc8620.rs b/melib/src/jmap/rfc8620.rs
index bd700d05..a43d457b 100644
--- a/melib/src/jmap/rfc8620.rs
+++ b/melib/src/jmap/rfc8620.rs
@@ -682,7 +682,7 @@ impl ChangesResponse {
/// 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
where
@@ -802,6 +802,38 @@ where
}
}
+impl Serialize for Set {
+ fn serialize(&self, serializer: S) -> Result
+ 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::>();
+ 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 {
diff --git a/melib/src/jmap/rfc8620/argument.rs b/melib/src/jmap/rfc8620/argument.rs
index b87cce35..159822ea 100644
--- a/melib/src/jmap/rfc8620/argument.rs
+++ b/melib/src/jmap/rfc8620/argument.rs
@@ -62,3 +62,154 @@ impl From for Argument {
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 = Id::new_uuid_v4();
+ let draft_mailbox_id: Id = Id::new_uuid_v4();
+ let sent_mailbox_id: Id = Id::new_uuid_v4();
+ let prev_seq = 33;
+
+ let mut req = Request::new(Arc::new(Mutex::new(prev_seq)));
+ let creation_id: Id = "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::::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::(prev_seq, ResultField::::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"
+ ]
+ }},
+ );
+ }
+}
diff --git a/melib/src/jmap/session.rs b/melib/src/jmap/session.rs
index 38556c01..05031566 100644
--- a/melib/src/jmap/session.rs
+++ b/melib/src/jmap/session.rs
@@ -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,
pub accounts: IndexMap, Account>,
pub primary_accounts: IndexMap>,
+ #[serde(skip)]
+ pub identities: IndexMap, IdentityObject>,
pub username: String,
pub api_url: Arc,
pub download_url: Arc,