summaryrefslogtreecommitdiffstats
path: root/melib/src
diff options
context:
space:
mode:
authorManos Pitsidianakis <el13635@mail.ntua.gr>2020-09-19 17:45:58 +0300
committerManos Pitsidianakis <el13635@mail.ntua.gr>2020-09-19 20:44:39 +0300
commit3210ee5c67177d46ab2876f8a4cd15c67525457d (patch)
tree8ed899f97cffb34c4382311b262fb16b6c300ddc /melib/src
parentcfc380b47dbfb5c9bf015864461b437ea9e27db2 (diff)
melib/jmap: impl save() message
Closes #60
Diffstat (limited to 'melib/src')
-rw-r--r--melib/src/backends/jmap.rs75
-rw-r--r--melib/src/backends/jmap/objects/email.rs3
-rw-r--r--melib/src/backends/jmap/objects/email/import.rs200
-rw-r--r--melib/src/backends/jmap/rfc8620.rs27
4 files changed, 302 insertions, 3 deletions
diff --git a/melib/src/backends/jmap.rs b/melib/src/backends/jmap.rs
index c3cf9b35..333177b5 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<u8>,
- _mailbox_hash: MailboxHash,
+ bytes: Vec<u8>,
+ mailbox_hash: MailboxHash,
_flags: Option<Flag>,
) -> 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<ImportError> =
+ 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<Arc<RwLock<BTreeMap<u64, String>>>> {
diff --git a/melib/src/backends/jmap/objects/email.rs b/melib/src/backends/jmap/objects/email.rs
index cf3cb1f5..bb9f8fb0 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 00000000..5fd0895a
--- /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 <http://www.gnu.org/licenses/>.
+ */
+
+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<String>,
+ ///o emails: "Id[EmailImport]"
+ ///A map of creation id (client specified) to EmailImport objects.
+ pub emails: HashMap<Id, EmailImport>,
+}
+
+#[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<Id, bool>,
+ ///o keywords: "String[Boolean]" (default: {})
+ ///The keywords to apply to the Email.
+ pub keywords: HashMap<String, bool>,
+
+ ///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<String>,
+}
+
+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<String>);
+ _impl!(emails: HashMap<Id, EmailImport>);
+}
+
+impl Method<EmailObject> 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<Id, bool>);
+ _impl!(keywords: HashMap<String, bool>);
+ _impl!(received_at: Option<String>);
+}
+
+#[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<String>,
+ /// 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<String>,
+ properties: Vec<String>,
+ },
+ ///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<String> },
+ ///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<String> },
+ ///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<String>,
+
+ ///o newState: "String"
+ ///The state string that will now be returned by "Email/get" on this
+ ///account.
+ pub new_state: Option<String>,
+
+ ///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<Id, ImportEmailResult>,
+
+ ///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, ImportError>,
+}
+
+impl std::convert::TryFrom<&RawValue> for ImportResponse {
+ type Error = crate::error::MeliError;
+ fn try_from(t: &RawValue) -> Result<ImportResponse> {
+ 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 d75185be..2df2b8b2 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,
+}