From 8d50e83a3312d9d6037b340c55c22c9a128c9b28 Mon Sep 17 00:00:00 2001 From: Manos Pitsidianakis Date: Tue, 25 Aug 2020 12:25:26 +0300 Subject: [PATCH] melib/email: add case-insensitive Header struct - HeaderName is either 32 or less inlined bytes or heap-allocated vec for more than that. - Equality and hashing is case-insensitive - A HeaderMap is a hashmap from HeaderName to Strings that can be indexed with &str, case insensitive. Insertion order is also preserved --- melib/src/email.rs | 81 +++++----- melib/src/email/compose.rs | 103 ++++++------ melib/src/email/headers.rs | 288 +++++++++++++++++++++++++++++++++ src/components/mail/compose.rs | 68 +++----- src/components/mail/view.rs | 4 +- src/components/utilities.rs | 4 +- tests/generated.mail | 2 +- tests/generating_email.rs | 20 +-- 8 files changed, 410 insertions(+), 160 deletions(-) create mode 100644 melib/src/email/headers.rs diff --git a/melib/src/email.rs b/melib/src/email.rs index 092603c1..9f26575a 100644 --- a/melib/src/email.rs +++ b/melib/src/email.rs @@ -22,7 +22,7 @@ /*! * Email parsing, handling, sending etc. */ -use std::collections::HashMap; +use std::convert::TryInto; mod compose; pub use self::compose::*; @@ -37,7 +37,9 @@ mod address; pub mod parser; use crate::parser::BytesExt; pub use address::*; +mod headers; pub mod signatures; +pub use headers::*; use crate::backends::BackendOp; use crate::datetime::UnixTimestamp; @@ -139,7 +141,7 @@ pub struct Envelope { message_id: MessageID, in_reply_to: Option, pub references: Option, - other_headers: HashMap, + other_headers: HeaderMap, timestamp: UnixTimestamp, thread: ThreadNodeHash, @@ -153,15 +155,16 @@ pub struct Envelope { impl fmt::Debug for Envelope { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "Envelope {{\n\tsubject: {}\n\tdate: {},\n\tfrom:{:#?},\n\tto {:#?},\n\tmessage_id: {},\n\tin_reply_to: {:?}\n\treferences: {:#?},\n\thash: {}\n}}", - self.subject(), - self.date, - self.from, - self.to, - self.message_id_display(), - self.in_reply_to_display(), - self.references, - self.hash) + f.debug_struct("Envelope") + .field("Subject", &self.subject()) + .field("Date", &self.date) + .field("From", &self.from) + .field("To", &self.to) + .field("Message-ID", &self.message_id_display()) + .field("In-Reply-To", &self.in_reply_to_display()) + .field("References", &self.references) + .field("Hash", &self.hash) + .finish() } } @@ -183,17 +186,7 @@ impl Envelope { message_id: MessageID::default(), in_reply_to: None, references: None, - other_headers: [ - ("From".to_string(), String::new()), - ("To".to_string(), String::new()), - ("Subject".to_string(), String::new()), - ("Date".to_string(), String::new()), - ("Cc".to_string(), String::new()), - ("Bcc".to_string(), String::new()), - ] - .iter() - .cloned() - .collect(), + other_headers: Default::default(), timestamp: 0, @@ -253,49 +246,40 @@ impl Envelope { let mut in_reply_to = None; for (name, value) in headers { - self.other_headers.insert( - String::from_utf8(name.to_vec()) - .unwrap_or_else(|err| String::from_utf8_lossy(&err.into_bytes()).into()), - parser::encodings::phrase(value, false) - .map(|(_, value)| { - String::from_utf8(value) - .unwrap_or_else(|err| String::from_utf8_lossy(&err.into_bytes()).into()) - }) - .unwrap_or_else(|_| String::from_utf8_lossy(value).into()), - ); - if name.eq_ignore_ascii_case(b"to") { + let name: HeaderName = name.try_into()?; + if name == "to" { let parse_result = parser::address::rfc2822address_list(value); if parse_result.is_ok() { let value = parse_result.unwrap().1; self.set_to(value); }; - } else if name.eq_ignore_ascii_case(b"cc") { + } else if name == "cc" { let parse_result = parser::address::rfc2822address_list(value); if parse_result.is_ok() { let value = parse_result.unwrap().1; self.set_cc(value); }; - } else if name.eq_ignore_ascii_case(b"bcc") { + } else if name == "bcc" { let parse_result = parser::address::rfc2822address_list(value); if parse_result.is_ok() { let value = parse_result.unwrap().1; self.set_bcc(value.to_vec()); }; - } else if name.eq_ignore_ascii_case(b"from") { + } else if name == "from" { let parse_result = parser::address::rfc2822address_list(value); if parse_result.is_ok() { let value = parse_result.unwrap().1; self.set_from(value); } - } else if name.eq_ignore_ascii_case(b"subject") { + } else if name == "subject" { let parse_result = parser::encodings::phrase(value.trim(), false); if parse_result.is_ok() { let value = parse_result.unwrap().1; self.set_subject(value); }; - } else if name.eq_ignore_ascii_case(b"message-id") { + } else if name == "message-id" { self.set_message_id(value); - } else if name.eq_ignore_ascii_case(b"references") { + } else if name == "references" { { let parse_result = parser::address::references(value); if parse_result.is_ok() { @@ -305,10 +289,10 @@ impl Envelope { } } self.set_references(value); - } else if name.eq_ignore_ascii_case(b"in-reply-to") { + } else if name == "in-reply-to" { self.set_in_reply_to(value); in_reply_to = Some(value); - } else if name.eq_ignore_ascii_case(b"date") { + } else if name == "date" { let parse_result = parser::encodings::phrase(value, false); if parse_result.is_ok() { let value = parse_result.unwrap().1; @@ -316,7 +300,7 @@ impl Envelope { } else { self.set_date(value); } - } else if name.eq_ignore_ascii_case(b"content-type") { + } else if name == "content-type" { match parser::attachments::content_type(value) { Ok((_, (ct, cst, ref params))) if ct.eq_ignore_ascii_case(b"multipart") @@ -341,6 +325,15 @@ impl Envelope { _ => {} } } + self.other_headers.insert( + name, + parser::encodings::phrase(value, false) + .map(|(_, value)| { + String::from_utf8(value) + .unwrap_or_else(|err| String::from_utf8_lossy(&err.into_bytes()).into()) + }) + .unwrap_or_else(|_| String::from_utf8_lossy(value).into()), + ); } /* * https://tools.ietf.org/html/rfc5322#section-3.6.4 @@ -644,11 +637,11 @@ impl Envelope { } } - pub fn other_headers(&self) -> &HashMap { + pub fn other_headers(&self) -> &HeaderMap { &self.other_headers } - pub fn other_headers_mut(&mut self) -> &mut HashMap { + pub fn other_headers_mut(&mut self) -> &mut HeaderMap { &mut self.other_headers } diff --git a/melib/src/email/compose.rs b/melib/src/email/compose.rs index 9984499e..0d0ca99e 100644 --- a/melib/src/email/compose.rs +++ b/melib/src/email/compose.rs @@ -20,11 +20,9 @@ */ use super::*; -use crate::backends::BackendOp; use crate::email::attachments::AttachmentBuilder; use crate::shellexpand::ShellExpandTrait; use data_encoding::BASE64_MIME; -use indexmap::IndexMap; use std::ffi::OsStr; use std::io::Read; use std::path::{Path, PathBuf}; @@ -39,7 +37,7 @@ use super::parser; #[derive(Debug, PartialEq, Eq, Clone)] pub struct Draft { - pub headers: IndexMap, + pub headers: HeaderMap, pub body: String, pub attachments: Vec, @@ -47,17 +45,17 @@ pub struct Draft { impl Default for Draft { fn default() -> Self { - let mut headers = IndexMap::with_capacity_and_hasher(8, Default::default()); + let mut headers = HeaderMap::default(); headers.insert( - "Date".into(), + HeaderName::new_unchecked("Date"), crate::datetime::timestamp_to_string(crate::datetime::now(), None), ); - headers.insert("From".into(), "".into()); - headers.insert("To".into(), "".into()); - headers.insert("Cc".into(), "".into()); - headers.insert("Bcc".into(), "".into()); + headers.insert(HeaderName::new_unchecked("From"), "".into()); + headers.insert(HeaderName::new_unchecked("To"), "".into()); + headers.insert(HeaderName::new_unchecked("Cc"), "".into()); + headers.insert(HeaderName::new_unchecked("Bcc"), "".into()); + headers.insert(HeaderName::new_unchecked("Subject"), "".into()); - headers.insert("Subject".into(), "".into()); Draft { headers, body: String::new(), @@ -78,20 +76,9 @@ impl str::FromStr for Draft { let mut ret = Draft::default(); for (k, v) in headers { - ret.headers.insert( - String::from_utf8(k.to_vec())?, - String::from_utf8(v.to_vec())?, - ); + ret.headers + .insert(k.try_into()?, String::from_utf8(v.to_vec())?); } - if ret.headers.contains_key("From") && !ret.headers.contains_key("Message-ID") { - if let Ok((_, addr)) = super::parser::address::mailbox(ret.headers["From"].as_bytes()) { - if let Some(fqdn) = addr.get_fqdn() { - ret.headers - .insert("Message-ID".into(), random::gen_message_id(&fqdn)); - } - } - } - let body = Envelope::new(0).body_bytes(s.as_bytes()); ret.body = String::from_utf8(decode(&body, None))?; @@ -101,30 +88,28 @@ impl str::FromStr for Draft { } impl Draft { - pub fn edit(envelope: &Envelope, mut op: Box) -> Result { + pub fn edit(envelope: &Envelope, bytes: &[u8]) -> Result { let mut ret = Draft::default(); //TODO: Inform user if error - { - let bytes = futures::executor::block_on(op.as_bytes()?)?; - for (k, v) in envelope.headers(&bytes).unwrap_or_else(|_| Vec::new()) { - ret.headers.insert(k.into(), v.into()); - } + for (k, v) in envelope.headers(&bytes).unwrap_or_else(|_| Vec::new()) { + ret.headers.insert(k.try_into()?, v.into()); } - ret.body = envelope.body(op)?.text(); + ret.body = envelope.body_bytes(bytes).text(); Ok(ret) } pub fn set_header(&mut self, header: &str, value: String) -> &mut Self { - self.headers.insert(header.to_string(), value); + self.headers + .insert(HeaderName::new_unchecked(header), value); self } pub fn new_reply(envelope: &Envelope, bytes: &[u8], reply_to_all: bool) -> Self { let mut ret = Draft::default(); ret.headers_mut().insert( - "References".into(), + HeaderName::new_unchecked("References"), format!( "{} {}", envelope @@ -140,35 +125,47 @@ impl Draft { envelope.message_id_display() ), ); - ret.headers_mut() - .insert("In-Reply-To".into(), envelope.message_id_display().into()); + ret.headers_mut().insert( + HeaderName::new_unchecked("In-Reply-To"), + envelope.message_id_display().into(), + ); // "Mail-Followup-To/(To+Cc+(Mail-Reply-To/Reply-To/From)) for follow-up, // Mail-Reply-To/Reply-To/From for reply-to-author." // source: https://cr.yp.to/proto/replyto.html if reply_to_all { if let Some(reply_to) = envelope.other_headers().get("Mail-Followup-To") { - ret.headers_mut().insert("To".into(), reply_to.to_string()); + ret.headers_mut() + .insert(HeaderName::new_unchecked("To"), reply_to.to_string()); } else { if let Some(reply_to) = envelope.other_headers().get("Reply-To") { - ret.headers_mut().insert("To".into(), reply_to.to_string()); - } else { ret.headers_mut() - .insert("To".into(), envelope.field_from_to_string()); + .insert(HeaderName::new_unchecked("To"), reply_to.to_string()); + } else { + ret.headers_mut().insert( + HeaderName::new_unchecked("To"), + envelope.field_from_to_string(), + ); } // FIXME: add To/Cc } } else { if let Some(reply_to) = envelope.other_headers().get("Mail-Reply-To") { - ret.headers_mut().insert("To".into(), reply_to.to_string()); - } else if let Some(reply_to) = envelope.other_headers().get("Reply-To") { - ret.headers_mut().insert("To".into(), reply_to.to_string()); - } else { ret.headers_mut() - .insert("To".into(), envelope.field_from_to_string()); + .insert(HeaderName::new_unchecked("To"), reply_to.to_string()); + } else if let Some(reply_to) = envelope.other_headers().get("Reply-To") { + ret.headers_mut() + .insert(HeaderName::new_unchecked("To"), reply_to.to_string()); + } else { + ret.headers_mut().insert( + HeaderName::new_unchecked("To"), + envelope.field_from_to_string(), + ); } } - ret.headers_mut() - .insert("Cc".into(), envelope.field_cc_to_string()); + ret.headers_mut().insert( + HeaderName::new_unchecked("Cc"), + envelope.field_cc_to_string(), + ); let body = envelope.body_bytes(bytes); ret.body = { let reply_body_bytes = decode_rec(&body, None); @@ -177,7 +174,7 @@ impl Draft { let mut ret = format!( "On {} {} wrote:\n", envelope.date_as_str(), - ret.headers()["To"] + &ret.headers()["To"] ); for l in lines { ret.push('>'); @@ -191,11 +188,11 @@ impl Draft { ret } - pub fn headers_mut(&mut self) -> &mut IndexMap { + pub fn headers_mut(&mut self) -> &mut HeaderMap { &mut self.headers } - pub fn headers(&self) -> &IndexMap { + pub fn headers(&self) -> &HeaderMap { &self.headers } @@ -219,7 +216,7 @@ impl Draft { pub fn to_string(&self) -> Result { let mut ret = String::new(); - for (k, v) in &self.headers { + for (k, v) in self.headers.deref() { ret.extend(format!("{}: {}\n", k, v).chars()); } @@ -236,12 +233,14 @@ impl Draft { if let Ok((_, addr)) = super::parser::address::mailbox(self.headers["From"].as_bytes()) { if let Some(fqdn) = addr.get_fqdn() { - self.headers - .insert("Message-ID".into(), random::gen_message_id(&fqdn)); + self.headers.insert( + HeaderName::new_unchecked("Message-ID"), + random::gen_message_id(&fqdn), + ); } } } - for (k, v) in &self.headers { + for (k, v) in self.headers.deref() { if v.is_ascii() { ret.extend(format!("{}: {}\n", k, v).chars()); } else { diff --git a/melib/src/email/headers.rs b/melib/src/email/headers.rs new file mode 100644 index 00000000..4dbe9899 --- /dev/null +++ b/melib/src/email/headers.rs @@ -0,0 +1,288 @@ +/* + * meli - headers + * + * Copyright 2020 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 crate::error::MeliError; +use indexmap::IndexMap; +use smallvec::SmallVec; +use std::borrow::Borrow; +use std::cmp::{Eq, PartialEq}; +use std::convert::TryFrom; +use std::fmt; +use std::hash::{Hash, Hasher}; +use std::ops::{Deref, DerefMut}; + +#[derive(Clone, Copy, Serialize, Deserialize)] +pub struct HeaderNameType(S); + +///Case insensitive wrapper for a header name. As of `RFC5322` it's guaranteened to be ASCII. +pub type HeaderName = HeaderNameType>; + +impl HeaderName { + pub fn new_unchecked(from: &str) -> Self { + HeaderNameType(from.as_bytes().into()) + } +} + +impl> fmt::Display for HeaderNameType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.normalize()) + } +} + +impl> fmt::Debug for HeaderNameType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.as_str()) + } +} + +impl> PartialEq<[u8]> for HeaderNameType { + fn eq(&self, other: &[u8]) -> bool { + self.0.as_ref().eq_ignore_ascii_case(other) + } +} + +impl> PartialEq<&str> for HeaderNameType { + fn eq(&self, other: &&str) -> bool { + self.0.as_ref().eq_ignore_ascii_case(other.as_bytes()) + } +} + +impl, S2: AsRef<[u8]>> PartialEq> for HeaderNameType { + fn eq(&self, other: &HeaderNameType) -> bool { + self.0.as_ref().eq_ignore_ascii_case(other.0.as_ref()) + } +} + +impl> Eq for HeaderNameType {} + +impl> Hash for HeaderNameType { + fn hash(&self, state: &mut H) { + for b in self.0.as_ref().iter() { + b.to_ascii_lowercase().hash(state); + } + } +} + +impl TryFrom<&[u8]> for HeaderName { + type Error = MeliError; + + fn try_from(value: &[u8]) -> Result { + if value.is_ascii() { + Ok(HeaderNameType(value.into())) + } else { + Err(MeliError::new(format!( + "Header value is not ascii: {:?}", + value + ))) + } + } +} + +impl TryFrom<&str> for HeaderName { + type Error = MeliError; + + fn try_from(value: &str) -> Result { + if value.is_ascii() { + Ok(HeaderNameType(value.as_bytes().into())) + } else { + Err(MeliError::new(format!( + "Header value is not ascii: {:?}", + value + ))) + } + } +} + +trait HeaderKey { + fn to_key(&self) -> &[u8]; +} + +impl Hash for dyn HeaderKey + '_ { + fn hash(&self, state: &mut H) { + for b in self.to_key().iter() { + b.to_ascii_lowercase().hash(state); + } + } +} + +impl PartialEq for dyn HeaderKey + '_ { + fn eq(&self, other: &Self) -> bool { + self.to_key().eq_ignore_ascii_case(other.to_key()) + } +} + +impl Eq for dyn HeaderKey + '_ {} + +impl> HeaderKey for HeaderNameType { + fn to_key(&self) -> &[u8] { + self.0.as_ref() + } +} + +//Implement Borrow for all the lookup types as returning our trait object: + +impl<'a> Borrow for HeaderName { + fn borrow(&self) -> &(dyn HeaderKey + 'a) { + self + } +} + +impl> HeaderNameType { + pub fn as_str(&self) -> &str { + //HeadersType are ascii so valid utf8 + unsafe { std::str::from_utf8_unchecked(self.0.as_ref()) } + } + + pub fn normalize(&self) -> &str { + if self == &b"subject"[..] { + "Subject" + } else if self == &b"from"[..] { + "From" + } else if self == &b"to"[..] { + "To" + } else if self == &b"cc"[..] { + "Cc" + } else if self == &b"bcc"[..] { + "Bcc" + } else if self == &b"reply-to"[..] { + "Reply-To" + } else if self == &b"in-reply-to"[..] { + "In-Reply-To" + } else if self == &b"references"[..] { + "References" + } else if self == &b"sender"[..] { + "Sender" + } else if self == &b"mail-reply-to"[..] { + "Mail-Reply-To" + } else if self == &b"mail-followup-to"[..] { + "Mail-Followup-To" + } else if self == &b"mime-version"[..] { + "MIME-Version" + } else if self == &b"content-disposition"[..] { + "Content-Disposition" + } else if self == &b"content-transfer-encoding"[..] { + "Content-Transfer-Encoding" + } else if self == &b"content-type"[..] { + "Content-Type" + } else if self == &b"content-id"[..] { + "Content-ID" + } else if self == &b"content-description"[..] { + "Content-Description" + } else if self == &b"authentication-results"[..] { + "Authentication-Results" + } else if self == &b"dkim-signature"[..] { + "DKIM-Signature" + } else if self == &b"delivered-to"[..] { + "Delivered-To" + } else if self == &b"message-id"[..] { + "Message-ID" + } else if self == &b"comments"[..] { + "Comments" + } else if self == &b"keywords"[..] { + "Keywords" + } else if self == &b"resent-from"[..] { + "Resent-From" + } else if self == &b"resent-sender"[..] { + "Resent-Sender" + } else if self == &b"resent-to"[..] { + "Resent-To" + } else if self == &b"resent-cc"[..] { + "Resent-Cc" + } else if self == &b"resent-bcc"[..] { + "Resent-Bcc" + } else if self == &b"resent-date"[..] { + "Resent-Date" + } else if self == &b"resent-message-id"[..] { + "Resent-Message-ID" + } else if self == &b"resent-reply-to"[..] { + "Resent-Reply-To" + } else if self == &b"return-path"[..] { + "Return-Path" + } else if self == &b"received"[..] { + "Received" + } else { + self.as_str() + } + } +} + +#[derive(Debug, Default, PartialEq, Eq, Clone, Serialize, Deserialize)] +pub struct HeaderMap(indexmap::IndexMap); + +impl std::ops::Index<&[u8]> for HeaderMap { + type Output = str; + fn index(&self, k: &[u8]) -> &Self::Output { + (self.0)[HeaderNameType(k).borrow() as &dyn HeaderKey].as_str() + } +} + +impl std::ops::Index<&str> for HeaderMap { + type Output = str; + fn index(&self, k: &str) -> &Self::Output { + (self.0)[HeaderNameType(k).borrow() as &dyn HeaderKey].as_str() + } +} + +impl HeaderMap { + pub fn get_mut(&mut self, key: &str) -> Option<&mut String> { + (self.0).get_mut(HeaderNameType(key).borrow() as &dyn HeaderKey) + } + + pub fn get(&self, key: &str) -> Option<&String> { + (self.0).get(HeaderNameType(key).borrow() as &dyn HeaderKey) + } + + pub fn contains_key(&self, key: &str) -> bool { + (self.0).contains_key(HeaderNameType(key).borrow() as &dyn HeaderKey) + } + + pub fn remove(&mut self, key: &str) -> Option { + (self.0).remove(HeaderNameType(key).borrow() as &dyn HeaderKey) + } +} + +impl Deref for HeaderMap { + type Target = IndexMap; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for HeaderMap { + fn deref_mut(&mut self) -> &mut IndexMap { + &mut self.0 + } +} + +#[test] +fn test_headers_case_sensitivity() { + use std::convert::TryInto; + let mut headers = HeaderMap::default(); + headers.insert("from".try_into().unwrap(), "Myself ".into()); + assert_eq!(&headers["From"], "Myself "); + assert_eq!(&headers["From"], &headers["from"]); + assert_eq!(&headers["fROm"], &headers["from"]); + headers.get_mut("from").unwrap().pop(); + assert_eq!(&headers["From"], "Myself fmt::Result { // TODO display subject/info if self.reply_context.is_some() { - write!(f, "reply: {:8}", self.draft.headers()["Subject"]) + write!(f, "reply: {:8}", &self.draft.headers()["Subject"]) } else { write!(f, "composing") } @@ -158,21 +158,11 @@ impl Composer { if v.is_empty() { continue; } - if let Some(k) = ret - .draft - .headers() - .keys() - .find(|k| k.eq_ignore_ascii_case(h)) - { - let _k = k.clone(); - ret.draft.headers_mut().insert(_k, v.into()); - } else { - ret.draft.set_header(h, v.into()); - } + ret.draft.set_header(h, v.into()); } if *mailbox_acc_settings!(context[account_hash].composing.insert_user_agent) { ret.draft.set_header( - "User-Agent".into(), + "User-Agent", format!("meli {}", option_env!("CARGO_PKG_VERSION").unwrap_or("0.0")), ); } @@ -181,14 +171,18 @@ impl Composer { ret } - pub fn edit(new_account_hash: AccountHash, h: EnvelopeHash, context: &Context) -> Result { + pub fn edit( + account_hash: AccountHash, + env_hash: EnvelopeHash, + bytes: &[u8], + context: &Context, + ) -> Result { let mut ret = Composer::default(); - let op = context.accounts[&new_account_hash].operation(h)?; - let envelope: EnvelopeRef = context.accounts[&new_account_hash].collection.get_env(h); + let envelope: EnvelopeRef = context.accounts[&account_hash].collection.get_env(env_hash); - ret.draft = Draft::edit(&envelope, op)?; + ret.draft = Draft::edit(&envelope, bytes)?; - ret.account_hash = new_account_hash; + ret.account_hash = account_hash; Ok(ret) } @@ -202,16 +196,16 @@ impl Composer { let account = &context.accounts[&coordinates.0]; let envelope = account.collection.get_env(coordinates.2); let subject = envelope.subject(); - ret.draft.headers_mut().insert( - "Subject".into(), + ret.draft.set_header( + "Subject", if !subject.starts_with("Re: ") { format!("Re: {}", subject) } else { subject.into() }, ); - ret.draft.headers_mut().insert( - "References".into(), + ret.draft.set_header( + "References", format!( "{} {}", envelope @@ -228,8 +222,7 @@ impl Composer { ), ); ret.draft - .headers_mut() - .insert("In-Reply-To".into(), envelope.message_id_display().into()); + .set_header("In-Reply-To", envelope.message_id_display().into()); // "Mail-Followup-To/(To+Cc+(Mail-Reply-To/Reply-To/From)) for follow-up, // Mail-Reply-To/Reply-To/From for reply-to-author." @@ -274,7 +267,7 @@ impl Composer { { to.remove(&ours); } - ret.draft.headers_mut().insert("To".into(), { + ret.draft.set_header("To", { let mut ret: String = to.into_iter() .fold(String::new(), |mut s: String, n: Address| { @@ -286,22 +279,14 @@ impl Composer { ret.pop(); ret }); - ret.draft - .headers_mut() - .insert("Cc".into(), envelope.field_cc_to_string()); + ret.draft.set_header("Cc", envelope.field_cc_to_string()); } else { if let Some(reply_to) = envelope.other_headers().get("Mail-Reply-To") { - ret.draft - .headers_mut() - .insert("To".into(), reply_to.to_string()); + ret.draft.set_header("To", reply_to.to_string()); } else if let Some(reply_to) = envelope.other_headers().get("Reply-To") { - ret.draft - .headers_mut() - .insert("To".into(), reply_to.to_string()); + ret.draft.set_header("To", reply_to.to_string()); } else { - ret.draft - .headers_mut() - .insert("To".into(), envelope.field_from_to_string()); + ret.draft.set_header("To", envelope.field_from_to_string()); } } let body = envelope.body_bytes(bytes); @@ -399,7 +384,7 @@ impl Composer { let header_values = self.form.values_mut(); let draft_header_map = self.draft.headers_mut(); for (k, v) in draft_header_map.iter_mut() { - if let Some(ref vn) = header_values.get(k) { + if let Some(ref vn) = header_values.get(k.as_str()) { *v = vn.as_str().to_string(); } } @@ -537,8 +522,8 @@ impl Component for Composer { } if !self.draft.headers().contains_key("From") || self.draft.headers()["From"].is_empty() { - self.draft.headers_mut().insert( - "From".into(), + self.draft.set_header( + "From", crate::components::mail::get_display_name(context, self.account_hash), ); } @@ -798,8 +783,7 @@ impl Component for Composer { ) if selector.id() == *id => { if let Some(to_val) = result.downcast_mut::() { self.draft - .headers_mut() - .insert("To".to_string(), std::mem::replace(to_val, String::new())); + .set_header("To", std::mem::replace(to_val, String::new())); self.update_form(); } self.mode = ViewMode::Edit; diff --git a/src/components/mail/view.rs b/src/components/mail/view.rs index ca61fa6d..f39301a3 100644 --- a/src/components/mail/view.rs +++ b/src/components/mail/view.rs @@ -1563,8 +1563,8 @@ impl Component for MailView { list_management::ListAction::Email(email) => { if let Ok(mailto) = Mailto::try_from(*email) { let mut draft: Draft = mailto.into(); - draft.headers_mut().insert( - "From".into(), + draft.set_header( + "From", crate::components::mail::get_display_name( context, self.coordinates.0, diff --git a/src/components/utilities.rs b/src/components/utilities.rs index f7f03e99..be1b8a52 100644 --- a/src/components/utilities.rs +++ b/src/components/utilities.rs @@ -1675,7 +1675,8 @@ impl Component for Tabbed { self.help_curr_views = children_maps; return true; } - UIEvent::Action(Tab(Edit(account_hash, msg))) => { + UIEvent::Action(Tab(Edit(_, _))) => { + /* FIXME let composer = match Composer::edit(*account_hash, *msg, context) { Ok(c) => c, Err(e) => { @@ -1704,6 +1705,7 @@ impl Component for Tabbed { let mut children_maps = self.children[self.cursor_pos].get_shortcuts(context); children_maps.extend(self.get_shortcuts(context)); self.help_curr_views = children_maps; + */ return true; } UIEvent::Action(Tab(New(ref mut e))) if e.is_some() => { diff --git a/tests/generated.mail b/tests/generated.mail index 267c23a0..20f23ff0 100644 --- a/tests/generated.mail +++ b/tests/generated.mail @@ -1,8 +1,8 @@ +Subject: From: To: Cc: Bcc: -Subject: MIME-Version: 1.0 Content-Type: multipart/mixed; charset="utf-8"; boundary="bzz_bzz__bzz__" diff --git a/tests/generating_email.rs b/tests/generating_email.rs index c0acd498..04b94a9d 100644 --- a/tests/generating_email.rs +++ b/tests/generating_email.rs @@ -15,24 +15,8 @@ fn build_draft() { _ => {} } } - if new_draft.headers().contains_key("User-Agent") { - new_draft.headers_mut().remove("User-Agent"); - let pos = new_draft - .header_order - .iter() - .position(|k| k == "User-Agent") - .unwrap(); - new_draft.header_order.remove(pos); - } - { - new_draft.headers_mut().remove("Date"); - let pos = new_draft - .header_order - .iter() - .position(|k| k == "Date") - .unwrap(); - new_draft.header_order.remove(pos); - } + new_draft.headers_mut().remove("User-Agent"); + new_draft.headers_mut().remove("Date"); new_draft.attachments_mut().push(attachment); new_draft.set_body("hello world.".to_string());