From bfa5bab15dd3ea00172cbc1a22efe30ea32317f3 Mon Sep 17 00:00:00 2001 From: Manos Pitsidianakis Date: Wed, 4 Dec 2019 19:42:31 +0200 Subject: [PATCH] JMAP WIP #4 --- melib/Cargo.toml | 2 +- melib/src/backends/jmap.rs | 9 + melib/src/backends/jmap/connection.rs | 2 + melib/src/backends/jmap/objects.rs | 3 + melib/src/backends/jmap/objects/email.rs | 312 ++++++++++++++++----- melib/src/backends/jmap/objects/mailbox.rs | 66 +++++ melib/src/backends/jmap/protocol.rs | 227 ++++----------- melib/src/backends/jmap/rfc8620.rs | 83 +++++- melib/src/email.rs | 3 +- melib/src/email/address.rs | 31 ++ melib/src/email/parser.rs | 30 -- 11 files changed, 483 insertions(+), 285 deletions(-) create mode 100644 melib/src/backends/jmap/objects/mailbox.rs diff --git a/melib/Cargo.toml b/melib/Cargo.toml index 93e19e2bb..dfa01d918 100644 --- a/melib/Cargo.toml +++ b/melib/Cargo.toml @@ -27,7 +27,7 @@ uuid = { version = "0.7.4", features = ["serde", "v4"] } text_processing = { path = "../text_processing", version = "*", optional= true } libc = {version = "0.2.59", features = ["extra_traits",]} reqwest = { version ="0.10.0-alpha.2", optional=true, features = ["json", "blocking" ]} -serde_json = { version = "1.0", optional = true } +serde_json = { version = "1.0", optional = true, features = ["raw_value",] } [features] default = ["unicode_algorithms", "imap_backend", "maildir_backend", "mbox_backend", "jmap_backend", "vcard"] diff --git a/melib/src/backends/jmap.rs b/melib/src/backends/jmap.rs index 8d25e9827..890e09b77 100644 --- a/melib/src/backends/jmap.rs +++ b/melib/src/backends/jmap.rs @@ -45,6 +45,15 @@ macro_rules! _impl { } } +#[macro_export] +macro_rules! _impl_get_mut { + ($method:ident, $field:ident : $t:ty) => { + pub fn $method(&mut self) -> &mut $t { + &mut self.$field + } + } + } + pub mod connection; use connection::*; diff --git a/melib/src/backends/jmap/connection.rs b/melib/src/backends/jmap/connection.rs index 036abd828..8a40bb8dd 100644 --- a/melib/src/backends/jmap/connection.rs +++ b/melib/src/backends/jmap/connection.rs @@ -27,6 +27,7 @@ pub struct JmapConnection { pub client: Arc>, pub online_status: Arc>, pub server_conf: JmapServerConf, + pub account_id: Arc>, } impl JmapConnection { @@ -59,6 +60,7 @@ impl JmapConnection { client: Arc::new(Mutex::new(client)), online_status, server_conf, + account_id: Arc::new(Mutex::new(String::new())), }) } } diff --git a/melib/src/backends/jmap/objects.rs b/melib/src/backends/jmap/objects.rs index cc29d5b51..b4ec2ea6c 100644 --- a/melib/src/backends/jmap/objects.rs +++ b/melib/src/backends/jmap/objects.rs @@ -23,3 +23,6 @@ use super::*; mod email; pub use email::*; + +mod mailbox; +pub use mailbox::*; diff --git a/melib/src/backends/jmap/objects/email.rs b/melib/src/backends/jmap/objects/email.rs index d10d40e46..c97d98418 100644 --- a/melib/src/backends/jmap/objects/email.rs +++ b/melib/src/backends/jmap/objects/email.rs @@ -22,7 +22,11 @@ use super::*; use crate::backends::jmap::protocol::*; use crate::backends::jmap::rfc8620::bool_false; +use serde::de::{Deserialize, Deserializer}; +use serde_json::Value; +use std::collections::hash_map::DefaultHasher; use std::collections::HashMap; +use std::hash::Hasher; // 4.1.1. // Metadata @@ -122,17 +126,215 @@ use std::collections::HashMap; // "internal date" in IMAP [RFC3501]./ #[derive(Deserialize, Serialize, Debug)] +#[serde(rename_all = "camelCase")] pub struct EmailObject { - pub id: Id, - pub blob_id: Id, - pub thread_id: Id, - pub mailbox_ids: HashMap, - pub keywords: HashMap, - pub size: u64, - pub received_at: String, + #[serde(default)] + id: Id, + #[serde(default)] + mailbox_ids: HashMap, + #[serde(default)] + size: u64, + #[serde(default)] + received_at: String, + #[serde(default)] + to: Vec, + #[serde(default)] + bcc: Vec, + #[serde(default)] + reply_to: Option, + #[serde(default)] + cc: Vec, + #[serde(default)] + from: Vec, + #[serde(default)] + in_reply_to_email_id: Id, + #[serde(default)] + keywords: Value, + #[serde(default)] + attached_emails: Option, + #[serde(default)] + attachments: Vec, + #[serde(default)] + blob_id: String, + #[serde(default)] + has_attachment: bool, + #[serde(default)] + #[serde(deserialize_with = "deserialize_header")] + headers: HashMap, + #[serde(default)] + html_body: Vec, + #[serde(default)] + preview: Option, + #[serde(default)] + sent_at: String, + #[serde(default)] + subject: String, + #[serde(default)] + text_body: Vec, + #[serde(default)] + thread_id: Id, } -impl Object for EmailObject {} +#[derive(Deserialize, Serialize, Debug, Default)] +#[serde(rename_all = "camelCase")] +struct Header { + name: String, + value: String, +} + +fn deserialize_header<'de, D>( + deserializer: D, +) -> std::result::Result, D::Error> +where + D: Deserializer<'de>, +{ + let v = >::deserialize(deserializer)?; + Ok(v.into_iter().map(|t| (t.name, t.value)).collect()) +} + +#[derive(Deserialize, Serialize, Debug, Default)] +#[serde(rename_all = "camelCase")] +struct EmailAddress { + email: String, + name: Option, +} + +impl Into for EmailAddress { + fn into(self) -> crate::email::Address { + let Self { email, mut name } = self; + crate::make_address!((name.take().unwrap_or_default()), email) + } +} + +impl std::fmt::Display for EmailAddress { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + if self.name.is_some() { + write!(f, "{} <{}>", self.name.as_ref().unwrap(), &self.email) + } else { + write!(f, "{}", &self.email) + } + } +} + +impl std::convert::From for crate::Envelope { + fn from(mut t: EmailObject) -> crate::Envelope { + let mut env = crate::Envelope::new(0); + env.set_date(std::mem::replace(&mut t.sent_at, String::new()).as_bytes()); + if let Some(d) = crate::email::parser::date(env.date_as_str().as_bytes()) { + env.set_datetime(d); + } + + if let Some(v) = t.headers.get("Message-ID").or(t.headers.get("Message-Id")) { + env.set_message_id(v.as_bytes()); + } + if let Some(v) = t.headers.get("In-Reply-To") { + env.set_in_reply_to(v.as_bytes()); + env.push_references(v.as_bytes()); + } + if let Some(v) = t.headers.get("References") { + let parse_result = crate::email::parser::references(v.as_bytes()); + if parse_result.is_done() { + for v in parse_result.to_full_result().unwrap() { + env.push_references(v); + } + } + env.set_references(v.as_bytes()); + } + if let Some(v) = t.headers.get("Date") { + env.set_date(v.as_bytes()); + if let Some(d) = crate::email::parser::date(v.as_bytes()) { + env.set_datetime(d); + } + } + env.set_has_attachments(t.has_attachment); + env.set_subject(std::mem::replace(&mut t.subject, String::new()).into_bytes()); + + env.set_from( + std::mem::replace(&mut t.from, Vec::new()) + .into_iter() + .map(|addr| addr.into()) + .collect::>(), + ); + env.set_to( + std::mem::replace(&mut t.to, Vec::new()) + .into_iter() + .map(|addr| addr.into()) + .collect::>(), + ); + + env.set_cc( + std::mem::replace(&mut t.cc, Vec::new()) + .into_iter() + .map(|addr| addr.into()) + .collect::>(), + ); + + env.set_bcc( + std::mem::replace(&mut t.bcc, Vec::new()) + .into_iter() + .map(|addr| addr.into()) + .collect::>(), + ); + + if env.references.is_some() { + if let Some(pos) = env + .references + .as_ref() + .map(|r| &r.refs) + .unwrap() + .iter() + .position(|r| r == env.message_id()) + { + env.references.as_mut().unwrap().refs.remove(pos); + } + } + + let mut h = DefaultHasher::new(); + h.write(t.id.as_bytes()); + env.set_hash(h.finish()); + env + } +} + +#[derive(Deserialize, Serialize, Debug)] +#[serde(rename_all = "camelCase")] +struct HtmlBody { + blob_id: Id, + cid: Option, + disposition: String, + headers: Value, + language: Option>, + location: Option, + name: Option, + part_id: Option, + size: u64, + #[serde(alias = "type")] + content_type: String, + #[serde(default)] + sub_parts: Vec, +} + +#[derive(Deserialize, Serialize, Debug)] +#[serde(rename_all = "camelCase")] +struct TextBody { + blob_id: Id, + cid: Option, + disposition: String, + headers: Value, + language: Option>, + location: Option, + name: Option, + part_id: Option, + size: u64, + #[serde(alias = "type")] + content_type: String, + #[serde(default)] + sub_parts: Vec, +} + +impl Object for EmailObject { + const NAME: &'static str = "Email"; +} #[derive(Deserialize, Serialize, Debug)] #[serde(rename_all = "camelCase")] @@ -166,9 +368,9 @@ impl Method for EmailQueryCall { #[derive(Deserialize, Serialize, Debug)] #[serde(rename_all = "camelCase")] -pub struct EmailGetCall { +pub struct EmailGet { #[serde(flatten)] - pub get_call: GetCall, + pub get_call: Get, #[serde(skip_serializing_if = "Vec::is_empty")] pub body_properties: Vec, #[serde(default = "bool_false")] @@ -181,13 +383,13 @@ pub struct EmailGetCall { pub max_body_value_bytes: u64, } -impl Method for EmailGetCall { +impl Method for EmailGet { const NAME: &'static str = "Email/get"; } -impl EmailGetCall { - pub fn new(get_call: GetCall) -> Self { - EmailGetCall { +impl EmailGet { + pub fn new(get_call: Get) -> Self { + EmailGet { get_call, body_properties: Vec::new(), fetch_text_body_values: false, @@ -197,7 +399,7 @@ impl EmailGetCall { } } - _impl!(get_call: GetCall); + _impl!(get_call: Get); _impl!(body_properties: Vec); _impl!(fetch_text_body_values: bool); _impl!(fetch_html_body_values: bool); @@ -249,7 +451,7 @@ pub struct EmailFilterCondition { #[serde(skip_serializing_if = "String::is_empty")] pub body: String, #[serde(skip_serializing_if = "Vec::is_empty")] - pub header: Vec, + pub header: Vec, } impl EmailFilterCondition { @@ -272,68 +474,24 @@ impl EmailFilterCondition { _impl!(bcc: String); _impl!(subject: String); _impl!(body: String); - _impl!(header: Vec); + _impl!(header: Vec); } impl FilterTrait for EmailFilterCondition {} -// The following convenience properties are also specified for the Email -// object: -// -// o messageId: "String[]|null" (immutable) -// -// The value is identical to the value of "header:Message- -// ID:asMessageIds". For messages conforming to RFC 5322, this will -// be an array with a single entry. -// -// o inReplyTo: "String[]|null" (immutable) -// -// The value is identical to the value of "header:In-Reply- -// To:asMessageIds". -// -// o references: "String[]|null" (immutable) -// -// The value is identical to the value of -// "header:References:asMessageIds". -// -// o sender: "EmailAddress[]|null" (immutable) -// -// The value is identical to the value of -// "header:Sender:asAddresses". -// -// o from: "EmailAddress[]|null" (immutable) -// -// The value is identical to the value of "header:From:asAddresses". -// -// o to: "EmailAddress[]|null" (immutable) -// -// The value is identical to the value of "header:To:asAddresses". -// -// o cc: "EmailAddress[]|null" (immutable) -// -// The value is identical to the value of "header:Cc:asAddresses". -// -// o bcc: "EmailAddress[]|null" (immutable) -// -// The value is identical to the value of "header:Bcc:asAddresses". -// -// o replyTo: "EmailAddress[]|null" (immutable) -// -// The value is identical to the value of "header:Reply- -// To:asAddresses". -// -// o subject: "String|null" (immutable) -// -// The value is identical to the value of "header:Subject:asText". -// -// -// -// Jenkins & Newman Standards Track [Page 34] -// -// RFC 8621 JMAP Mail August 2019 -// -// -// o sentAt: "Date|null" (immutable; default on creation: current -// server time) -// -// The value is identical to the value of "header:Date:asDate". +#[derive(Deserialize, Serialize, Debug)] +#[serde(rename_all = "camelCase")] +pub enum MessageProperty { + ThreadId, + MailboxId, + IsUnread, + IsFlagged, + IsAnswered, + IsDraft, + HasAttachment, + From, + To, + Subject, + Date, + Preview, +} diff --git a/melib/src/backends/jmap/objects/mailbox.rs b/melib/src/backends/jmap/objects/mailbox.rs new file mode 100644 index 000000000..f3d828cfe --- /dev/null +++ b/melib/src/backends/jmap/objects/mailbox.rs @@ -0,0 +1,66 @@ +/* + * 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 . + */ + +use super::*; + +#[derive(Deserialize, Serialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct MailboxObject { + pub id: String, + pub is_subscribed: bool, + pub my_rights: JmapRights, + pub name: String, + pub parent_id: Option, + pub role: Option, + pub sort_order: u64, + pub total_emails: u64, + pub total_threads: u64, + pub unread_emails: u64, + pub unread_threads: u64, +} + +impl Object for MailboxObject { + const NAME: &'static str = "Mailbox"; +} + +#[derive(Deserialize, Serialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct JmapRights { + pub may_add_items: bool, + pub may_create_child: bool, + pub may_delete: bool, + pub may_read_items: bool, + pub may_remove_items: bool, + pub may_rename: bool, + pub may_set_keywords: bool, + pub may_set_seen: bool, + pub may_submit: bool, +} +#[derive(Deserialize, Serialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct MailboxGet { + #[serde(flatten)] + pub get_call: Get, +} + +impl Method for MailboxGet { + const NAME: &'static str = "Mailbox/query"; +} diff --git a/melib/src/backends/jmap/protocol.rs b/melib/src/backends/jmap/protocol.rs index 26a150d0f..d205c5359 100644 --- a/melib/src/backends/jmap/protocol.rs +++ b/melib/src/backends/jmap/protocol.rs @@ -23,6 +23,7 @@ use super::folder::JmapFolder; use super::*; use serde::{de::DeserializeOwned, Serialize}; use serde_json::{json, Value}; +use std::convert::TryFrom; pub type Id = String; pub type UtcDate = String; @@ -38,26 +39,14 @@ macro_rules! get_request_no { }}; } +pub trait Response { + const NAME: &'static str; +} + pub trait Method: Serialize { const NAME: &'static str; } -#[derive(Deserialize, Serialize, Debug)] -#[serde(rename_all = "camelCase")] -pub enum MessageProperty { - ThreadId, - MailboxId, - IsUnread, - IsFlagged, - IsAnswered, - IsDraft, - HasAttachment, - From, - To, - Subject, - Date, - Preview, -} macro_rules! get_path_hash { ($path:expr) => {{ use std::collections::hash_map::DefaultHasher; @@ -78,7 +67,7 @@ pub struct Request { * Trait object because its serialize() will be generic. */ method_calls: Vec, - #[serde(skip_serializing)] + #[serde(skip)] request_no: Arc>, } @@ -129,19 +118,41 @@ pub fn get_mailboxes(conn: &JmapConnection) -> Result::try_from(v.method_responses.remove(0))?; + let GetResponse:: { + list, account_id, .. + } = m; + *conn.account_id.lock().unwrap() = account_id; + Ok(list + .into_iter() + .map(|r| { + let MailboxObject { + id, + is_subscribed, + my_rights, + name, + parent_id, + role, + sort_order, + total_emails, + total_threads, + unread_emails, + unread_threads, + } = r; + let hash = get_path_hash!(&name); + ( + hash, + JmapFolder { + name: name.clone(), + hash, + path: name, + v: Vec::new(), id, is_subscribed, my_rights, - name, parent_id, role, sort_order, @@ -149,132 +160,19 @@ pub fn get_mailboxes(conn: &JmapConnection) -> Result, +pub struct JsonResponse<'a> { + #[serde(borrow)] + method_responses: Vec>, } -#[derive(Deserialize, Debug)] -#[serde(rename_all = "camelCase")] -pub struct MethodResponse(String, Response, String); - -#[derive(Deserialize, Debug)] -#[serde(rename_all = "camelCase")] -#[serde(untagged)] -pub enum Response { - #[serde(rename_all = "camelCase")] - MailboxGet { - account_id: String, - list: Vec, - not_found: Vec, - state: String, - }, - #[serde(rename_all = "camelCase")] - EmailQuery { - account_id: String, - can_calculate_changes: bool, - collapse_threads: bool, - filter: Value, - ids: Vec, - position: u64, - query_state: String, - sort: Option, - total: usize, - }, - Empty {}, -} - -#[derive(Deserialize, Debug)] -#[serde(rename_all = "camelCase")] -pub struct MailboxResponse { - id: String, - is_subscribed: bool, - my_rights: JmapRights, - name: String, - parent_id: Option, - role: Option, - sort_order: u64, - total_emails: u64, - total_threads: u64, - unread_emails: u64, - unread_threads: u64, -} -#[derive(Deserialize, Debug, Clone)] -#[serde(rename_all = "camelCase")] -pub struct JmapRights { - may_add_items: bool, - may_create_child: bool, - may_delete: bool, - may_read_items: bool, - may_remove_items: bool, - may_rename: bool, - may_set_keywords: bool, - may_set_seen: bool, - may_submit: bool, -} - -// [ -// [ "getMessageList", { -// filter: { -// inMailboxes: [ "mailbox1" ] -// }, -// sort: [ "date desc", "id desc" ] -// collapseThreads: true, -// position: 0, -// limit: 10, -// fetchThreads: true, -// fetchMessages: true, -// fetchMessageProperties: [ -// "threadId", -// "mailboxId", -// "isUnread", -// "isFlagged", -// "isAnswered", -// "isDraft", -// "hasAttachment", -// "from", -// "to", -// "subject", -// "date", -// "preview" -// ], -// fetchSearchSnippets: false -// }, "call1"] -// ] pub fn get_message_list(conn: &JmapConnection, folder: &JmapFolder) -> Result> { let seq = get_request_no!(conn.request_no); let email_call: EmailQueryCall = EmailQueryCall { @@ -372,7 +270,6 @@ pub fn get_message_list(conn: &JmapConnection, folder: &JmapFolder) -> Result Result::try_from(v.method_responses.remove(0))?; + let QueryResponse:: { ids, .. } = m; + Ok(ids) } pub fn get_message(conn: &JmapConnection, ids: &[String]) -> Result> { let seq = get_request_no!(conn.request_no); - let email_call: EmailGetCall = - EmailGetCall::new(GetCall::new().ids(Some(ids.iter().cloned().collect::>()))); + let email_call: EmailGet = EmailGet::new( + Get::new() + .ids(Some(ids.iter().cloned().collect::>())) + .account_id(conn.account_id.lock().unwrap().clone()), + ); let mut req = Request::new(conn.request_no.clone()); req.add_call(email_call); @@ -407,14 +305,13 @@ pub fn get_message(conn: &JmapConnection, ids: &[String]) -> Result(&res_text)?)?.as_bytes(), - ) - .unwrap(); - Ok(vec![]) + let mut v: MethodResponse = serde_json::from_str(&res_text).unwrap(); + let e = GetResponse::::try_from(v.method_responses.remove(0))?; + let GetResponse:: { list, .. } = e; + Ok(list + .into_iter() + .map(std::convert::Into::into) + .collect::>()) } /* diff --git a/melib/src/backends/jmap/rfc8620.rs b/melib/src/backends/jmap/rfc8620.rs index f8cb0ce4c..14f6d0ccd 100644 --- a/melib/src/backends/jmap/rfc8620.rs +++ b/melib/src/backends/jmap/rfc8620.rs @@ -22,16 +22,18 @@ use super::Id; use core::marker::PhantomData; use serde::{de::DeserializeOwned, Serialize}; -use serde_json::Value; +use serde_json::{value::RawValue, Value}; mod filters; pub use filters::*; mod comparator; pub use comparator::*; -use super::protocol::Method; +use super::protocol::{Method, Response}; use std::collections::HashMap; -pub trait Object {} +pub trait Object { + const NAME: &'static str; +} // 5.1. /get // @@ -102,7 +104,7 @@ pub struct Account { #[derive(Deserialize, Serialize, Debug)] #[serde(rename_all = "camelCase")] -pub struct GetCall +pub struct Get where OBJ: std::fmt::Debug + Serialize, { @@ -112,10 +114,11 @@ where pub ids: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub properties: Option>, + #[serde(skip)] _ph: PhantomData<*const OBJ>, } -impl GetCall +impl Get where OBJ: std::fmt::Debug + Serialize, { @@ -173,14 +176,41 @@ where // the maximum number the server is willing to process in a single // method call. -#[derive(Serialize, Debug)] +#[derive(Serialize, Deserialize, Debug)] #[serde(rename_all = "camelCase")] -pub struct GetResponse { +pub struct MethodResponse<'a> { + #[serde(borrow)] + pub method_responses: Vec<&'a RawValue>, + #[serde(default)] + pub created_ids: HashMap, + #[serde(default)] + pub session_state: String, +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct GetResponse { #[serde(skip_serializing_if = "String::is_empty")] - account_id: String, - state: String, - list: Vec, - not_found: Vec, + pub account_id: String, + pub state: String, + pub list: Vec, + pub not_found: Vec, +} + +impl std::convert::TryFrom<&RawValue> for GetResponse { + type Error = crate::error::MeliError; + fn try_from(t: &RawValue) -> Result, crate::error::MeliError> { + let res: (String, GetResponse, String) = serde_json::from_str(t.get())?; + assert_eq!(&res.0, &format!("{}/get", OBJ::NAME)); + Ok(res.1) + } +} + +impl GetResponse { + _impl_get_mut!(account_id_mut, account_id: String); + _impl_get_mut!(state_mut, state: String); + _impl_get_mut!(list_mut, list: Vec); + _impl_get_mut!(not_found_mut, not_found: Vec); } #[derive(Deserialize, Debug)] @@ -379,6 +409,7 @@ where limit: Option, #[serde(default = "bool_false")] calculate_total: bool, + #[serde(skip)] _ph: PhantomData<*const OBJ>, } @@ -495,3 +526,33 @@ pub fn bool_true() -> bool { // server cannot process it. If the filter was the result of a user's // search input, the client SHOULD suggest that the user simplify their // search. + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct QueryResponse { + #[serde(skip_serializing_if = "String::is_empty", default)] + pub account_id: String, + pub query_state: String, + pub can_calculate_changes: bool, + pub position: u64, + pub ids: Vec, + #[serde(default)] + pub total: u64, + #[serde(default)] + pub limit: u64, + #[serde(skip)] + _ph: PhantomData<*const OBJ>, +} + +impl std::convert::TryFrom<&RawValue> for QueryResponse { + type Error = crate::error::MeliError; + fn try_from(t: &RawValue) -> Result, crate::error::MeliError> { + let res: (String, QueryResponse, String) = serde_json::from_str(t.get())?; + assert_eq!(&res.0, &format!("{}/query", OBJ::NAME)); + Ok(res.1) + } +} + +impl QueryResponse { + _impl_get_mut!(ids_mut, ids: Vec); +} diff --git a/melib/src/email.rs b/melib/src/email.rs index 6fa261bd6..f54477c35 100644 --- a/melib/src/email.rs +++ b/melib/src/email.rs @@ -125,7 +125,7 @@ pub struct Envelope { subject: Option>, message_id: MessageID, in_reply_to: Option, - references: Option, + pub references: Option, other_headers: FnvHashMap, timestamp: UnixTimestamp, @@ -542,6 +542,7 @@ impl Envelope { None => Vec::new(), } } + pub fn other_headers(&self) -> &FnvHashMap { &self.other_headers } diff --git a/melib/src/email/address.rs b/melib/src/email/address.rs index bdf9a96f5..e70ac6606 100644 --- a/melib/src/email/address.rs +++ b/melib/src/email/address.rs @@ -256,3 +256,34 @@ impl fmt::Debug for References { write!(f, "{:#?}", self.refs) } } + +#[macro_export] +macro_rules! make_address { + ($d:expr, $a:expr) => { + Address::Mailbox(if $d.is_empty() { + MailboxAddress { + raw: format!("{}", $a).into_bytes(), + display_name: StrBuilder { + offset: 0, + length: 0, + }, + address_spec: StrBuilder { + offset: 0, + length: $a.len(), + }, + } + } else { + MailboxAddress { + raw: format!("{} <{}>", $d, $a).into_bytes(), + display_name: StrBuilder { + offset: 0, + length: $d.len(), + }, + address_spec: StrBuilder { + offset: $d.len() + 2, + length: $a.len(), + }, + } + }) + }; +} diff --git a/melib/src/email/parser.rs b/melib/src/email/parser.rs index c830fa85d..278e25a5a 100644 --- a/melib/src/email/parser.rs +++ b/melib/src/email/parser.rs @@ -1083,36 +1083,6 @@ mod tests { ); } - macro_rules! make_address { - ($d:literal, $a:literal) => { - Address::Mailbox(if $d.is_empty() { - MailboxAddress { - raw: format!("<{}>", $a).into_bytes(), - display_name: StrBuilder { - offset: 0, - length: 0, - }, - address_spec: StrBuilder { - offset: 1, - length: $a.len(), - }, - } - } else { - MailboxAddress { - raw: format!("{} <{}>", $d, $a).into_bytes(), - display_name: StrBuilder { - offset: 0, - length: $d.len(), - }, - address_spec: StrBuilder { - offset: $d.len() + 2, - length: $a.len(), - }, - } - }) - }; - } - #[test] fn test_address_list() { let s = b"Obit Oppidum ,