diff --git a/melib/src/backends/jmap.rs b/melib/src/backends/jmap.rs index c3cf9b35d..333177b52 100644 --- a/melib/src/backends/jmap.rs +++ b/melib/src/backends/jmap.rs @@ -283,11 +283,80 @@ impl MailBackend for JmapType { fn save( &self, - _bytes: Vec, - _mailbox_hash: MailboxHash, + bytes: Vec, + mailbox_hash: MailboxHash, _flags: Option, ) -> ResultFuture<()> { - Err(MeliError::new("Unimplemented.")) + let mailboxes = self.mailboxes.clone(); + let connection = self.connection.clone(); + Ok(Box::pin(async move { + let mut conn = connection.lock().await; + conn.connect().await?; + /* + * 1. upload binary blob, get blobId + * 2. Email/import + */ + let mut res = conn + .client + .post_async( + &upload_request_format(&conn.session, conn.mail_account_id()), + bytes, + ) + .await?; + + let mailbox_id: String = { + let mailboxes_lck = mailboxes.read().unwrap(); + if let Some(mailbox) = mailboxes_lck.get(&mailbox_hash) { + mailbox.id.clone() + } else { + return Err(MeliError::new(format!( + "Mailbox with hash {} not found", + mailbox_hash + ))); + } + }; + let res_text = res.text_async().await?; + + let upload_response: UploadResponse = serde_json::from_str(&res_text)?; + let mut req = Request::new(conn.request_no.clone()); + let creation_id = "1".to_string(); + 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() + .account_id(conn.mail_account_id().to_string()) + .emails(email_imports); + + req.add_call(&import_call); + let mut res = conn + .client + .post_async(&conn.session.api_url, serde_json::to_string(&req)?) + .await?; + let res_text = res.text_async().await?; + + let mut v: MethodResponse = serde_json::from_str(&res_text)?; + let m = ImportResponse::try_from(v.method_responses.remove(0)).or_else(|err| { + let ierr: Result = + serde_json::from_str(&res_text).map_err(|err| err.into()); + if let Ok(err) = ierr { + Err(MeliError::new(format!("Could not save message: {:?}", err))) + } else { + Err(err.into()) + } + })?; + + if let Some(err) = m.not_created.get(&creation_id) { + return Err(MeliError::new(format!("Could not save message: {:?}", err))); + } + Ok(()) + })) } fn tags(&self) -> Option>>> { diff --git a/melib/src/backends/jmap/objects/email.rs b/melib/src/backends/jmap/objects/email.rs index cf3cb1f51..bb9f8fb07 100644 --- a/melib/src/backends/jmap/objects/email.rs +++ b/melib/src/backends/jmap/objects/email.rs @@ -29,6 +29,9 @@ use std::collections::hash_map::DefaultHasher; use std::collections::HashMap; use std::hash::Hasher; +mod import; +pub use import::*; + // 4.1.1. // Metadata // These properties represent metadata about the message in the mail diff --git a/melib/src/backends/jmap/objects/email/import.rs b/melib/src/backends/jmap/objects/email/import.rs new file mode 100644 index 000000000..5fd0895a3 --- /dev/null +++ b/melib/src/backends/jmap/objects/email/import.rs @@ -0,0 +1,200 @@ +/* + * meli - + * + * Copyright 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 super::*; +use serde_json::value::RawValue; + +/// #`import` +/// +/// Objects of type `Foo` are imported via a call to `Foo/import`. +/// +/// It takes the following arguments: +/// +/// - `account_id`: "Id" +/// +/// The id of the account to use. +/// +#[derive(Deserialize, Serialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct ImportCall { + ///accountId: "Id" + ///The id of the account to use. + pub account_id: String, + ///ifInState: "String|null" + ///This is a state string as returned by the "Email/get" method. If + ///supplied, the string must match the current state of the account + ///referenced by the accountId; otherwise, the method will be aborted + ///and a "stateMismatch" error returned. If null, any changes will + ///be applied to the current state. + #[serde(skip_serializing_if = "Option::is_none")] + pub if_in_state: Option, + ///o emails: "Id[EmailImport]" + ///A map of creation id (client specified) to EmailImport objects. + pub emails: HashMap, +} + +#[derive(Deserialize, Serialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct EmailImport { + ///o blobId: "Id" + ///The id of the blob containing the raw message [RFC5322]. + pub blob_id: String, + ///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, + ///o keywords: "String[Boolean]" (default: {}) + ///The keywords to apply to the Email. + pub keywords: HashMap, + + ///o receivedAt: "UTCDate" (default: time of most recent Received + ///header, or time of import on server if none) + ///The "receivedAt" date to set on the Email. + pub received_at: Option, +} + +impl ImportCall { + pub fn new() -> Self { + Self { + account_id: String::new(), + if_in_state: None, + emails: HashMap::default(), + } + } + + _impl!( + /// - accountId: "Id" + /// + /// The id of the account to use. + /// + account_id: String + ); + _impl!(if_in_state: Option); + _impl!(emails: HashMap); +} + +impl Method for ImportCall { + const NAME: &'static str = "Email/import"; +} + +impl EmailImport { + pub fn new() -> Self { + Self { + blob_id: String::new(), + mailbox_ids: HashMap::default(), + keywords: HashMap::default(), + received_at: None, + } + } + + _impl!(blob_id: String); + _impl!(mailbox_ids: HashMap); + _impl!(keywords: HashMap); + _impl!(received_at: Option); +} + +#[derive(Deserialize, Serialize, Debug)] +#[serde(rename_all = "camelCase")] +#[serde(tag = "type")] +pub enum ImportError { + ///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 + /// import an Email considered to be a duplicate with an "alreadyExists" + /// SetError. + AlreadyExists { + description: Option, + /// An "existingId" property of type "Id" MUST be included on + ///the SetError object with the id of the existing Email. If duplicates + ///are allowed, the newly created Email object MUST have a separate id + ///and independent mutable properties to the existing object. + existing_id: Id, + }, + ///If the "blobId", "mailboxIds", or "keywords" properties are invalid + ///(e.g., missing, wrong type, id not found), the server MUST reject the + ///import with an "invalidProperties" SetError. + InvalidProperties { + description: Option, + properties: Vec, + }, + ///If the Email cannot be imported because it would take the account + ///over quota, the import should be rejected with an "overQuota" + ///SetError. + OverQuota { description: Option }, + ///If the blob referenced is not a valid message [RFC5322], the server + ///MAY modify the message to fix errors (such as removing NUL octets or + ///fixing invalid headers). If it does this, the "blobId" on the + ///response MUST represent the new representation and therefore be + ///different to the "blobId" on the EmailImport object. Alternatively, + ///the server MAY reject the import with an "invalidEmail" SetError. + InvalidEmail { description: Option }, + ///An "ifInState" argument was supplied, and it does not match the current state. + StateMismatch, +} + +#[derive(Deserialize, Serialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct ImportResponse { + ///o accountId: "Id" + ///The id of the account used for this call. + pub account_id: Id, + + ///o oldState: "String|null" + ///The state string that would have been returned by "Email/get" on + ///this account before making the requested changes, or null if the + ///server doesn't know what the previous state string was. + pub old_state: Option, + + ///o newState: "String" + ///The state string that will now be returned by "Email/get" on this + ///account. + pub new_state: Option, + + ///o created: "Id[Email]|null" + ///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, + + ///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, +} + +impl std::convert::TryFrom<&RawValue> for ImportResponse { + type Error = crate::error::MeliError; + fn try_from(t: &RawValue) -> Result { + let res: (String, ImportResponse, String) = serde_json::from_str(t.get())?; + assert_eq!(&res.0, &ImportCall::NAME); + Ok(res.1) + } +} + +#[derive(Deserialize, Serialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct ImportEmailResult { + pub id: Id, + pub blob_id: Id, + pub thread_id: Id, + pub size: usize, +} diff --git a/melib/src/backends/jmap/rfc8620.rs b/melib/src/backends/jmap/rfc8620.rs index d75185be4..2df2b8b23 100644 --- a/melib/src/backends/jmap/rfc8620.rs +++ b/melib/src/backends/jmap/rfc8620.rs @@ -815,3 +815,30 @@ pub fn upload_request_format(session: &JmapSession, account_id: &Id) -> String { } ret } + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct UploadResponse { + ///o accountId: "Id" + /// + /// The id of the account used for the call. + pub account_id: String, + ///o blobId: "Id" + /// + ///The id representing the binary data uploaded. The data for this id is immutable. + ///The id *only* refers to the binary data, not any metadata. + pub blob_id: String, + ///o type: "String" + /// + ///The media type of the file (as specified in [RFC6838], + ///Section 4.2) as set in the Content-Type header of the upload HTTP + ///request. + + #[serde(rename = "type")] + pub _type: String, + + ///o size: "UnsignedInt" + /// + /// The size of the file in octets. + pub size: usize, +}