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
master
Manos Pitsidianakis 2020-08-25 12:25:26 +03:00
parent 0f3bf858a3
commit 8d50e83a33
Signed by: Manos Pitsidianakis
GPG Key ID: 73627C2F690DF710
8 changed files with 410 additions and 160 deletions

View File

@ -22,7 +22,7 @@
/*! /*!
* Email parsing, handling, sending etc. * Email parsing, handling, sending etc.
*/ */
use std::collections::HashMap; use std::convert::TryInto;
mod compose; mod compose;
pub use self::compose::*; pub use self::compose::*;
@ -37,7 +37,9 @@ mod address;
pub mod parser; pub mod parser;
use crate::parser::BytesExt; use crate::parser::BytesExt;
pub use address::*; pub use address::*;
mod headers;
pub mod signatures; pub mod signatures;
pub use headers::*;
use crate::backends::BackendOp; use crate::backends::BackendOp;
use crate::datetime::UnixTimestamp; use crate::datetime::UnixTimestamp;
@ -139,7 +141,7 @@ pub struct Envelope {
message_id: MessageID, message_id: MessageID,
in_reply_to: Option<MessageID>, in_reply_to: Option<MessageID>,
pub references: Option<References>, pub references: Option<References>,
other_headers: HashMap<String, String>, other_headers: HeaderMap,
timestamp: UnixTimestamp, timestamp: UnixTimestamp,
thread: ThreadNodeHash, thread: ThreadNodeHash,
@ -153,15 +155,16 @@ pub struct Envelope {
impl fmt::Debug for Envelope { impl fmt::Debug for Envelope {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 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}}", f.debug_struct("Envelope")
self.subject(), .field("Subject", &self.subject())
self.date, .field("Date", &self.date)
self.from, .field("From", &self.from)
self.to, .field("To", &self.to)
self.message_id_display(), .field("Message-ID", &self.message_id_display())
self.in_reply_to_display(), .field("In-Reply-To", &self.in_reply_to_display())
self.references, .field("References", &self.references)
self.hash) .field("Hash", &self.hash)
.finish()
} }
} }
@ -183,17 +186,7 @@ impl Envelope {
message_id: MessageID::default(), message_id: MessageID::default(),
in_reply_to: None, in_reply_to: None,
references: None, references: None,
other_headers: [ other_headers: Default::default(),
("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(),
timestamp: 0, timestamp: 0,
@ -253,49 +246,40 @@ impl Envelope {
let mut in_reply_to = None; let mut in_reply_to = None;
for (name, value) in headers { for (name, value) in headers {
self.other_headers.insert( let name: HeaderName = name.try_into()?;
String::from_utf8(name.to_vec()) if name == "to" {
.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 parse_result = parser::address::rfc2822address_list(value); let parse_result = parser::address::rfc2822address_list(value);
if parse_result.is_ok() { if parse_result.is_ok() {
let value = parse_result.unwrap().1; let value = parse_result.unwrap().1;
self.set_to(value); 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); let parse_result = parser::address::rfc2822address_list(value);
if parse_result.is_ok() { if parse_result.is_ok() {
let value = parse_result.unwrap().1; let value = parse_result.unwrap().1;
self.set_cc(value); 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); let parse_result = parser::address::rfc2822address_list(value);
if parse_result.is_ok() { if parse_result.is_ok() {
let value = parse_result.unwrap().1; let value = parse_result.unwrap().1;
self.set_bcc(value.to_vec()); 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); let parse_result = parser::address::rfc2822address_list(value);
if parse_result.is_ok() { if parse_result.is_ok() {
let value = parse_result.unwrap().1; let value = parse_result.unwrap().1;
self.set_from(value); 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); let parse_result = parser::encodings::phrase(value.trim(), false);
if parse_result.is_ok() { if parse_result.is_ok() {
let value = parse_result.unwrap().1; let value = parse_result.unwrap().1;
self.set_subject(value); self.set_subject(value);
}; };
} else if name.eq_ignore_ascii_case(b"message-id") { } else if name == "message-id" {
self.set_message_id(value); 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); let parse_result = parser::address::references(value);
if parse_result.is_ok() { if parse_result.is_ok() {
@ -305,10 +289,10 @@ impl Envelope {
} }
} }
self.set_references(value); 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); self.set_in_reply_to(value);
in_reply_to = Some(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); let parse_result = parser::encodings::phrase(value, false);
if parse_result.is_ok() { if parse_result.is_ok() {
let value = parse_result.unwrap().1; let value = parse_result.unwrap().1;
@ -316,7 +300,7 @@ impl Envelope {
} else { } else {
self.set_date(value); 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) { match parser::attachments::content_type(value) {
Ok((_, (ct, cst, ref params))) Ok((_, (ct, cst, ref params)))
if ct.eq_ignore_ascii_case(b"multipart") 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 * https://tools.ietf.org/html/rfc5322#section-3.6.4
@ -644,11 +637,11 @@ impl Envelope {
} }
} }
pub fn other_headers(&self) -> &HashMap<String, String> { pub fn other_headers(&self) -> &HeaderMap {
&self.other_headers &self.other_headers
} }
pub fn other_headers_mut(&mut self) -> &mut HashMap<String, String> { pub fn other_headers_mut(&mut self) -> &mut HeaderMap {
&mut self.other_headers &mut self.other_headers
} }

View File

@ -20,11 +20,9 @@
*/ */
use super::*; use super::*;
use crate::backends::BackendOp;
use crate::email::attachments::AttachmentBuilder; use crate::email::attachments::AttachmentBuilder;
use crate::shellexpand::ShellExpandTrait; use crate::shellexpand::ShellExpandTrait;
use data_encoding::BASE64_MIME; use data_encoding::BASE64_MIME;
use indexmap::IndexMap;
use std::ffi::OsStr; use std::ffi::OsStr;
use std::io::Read; use std::io::Read;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
@ -39,7 +37,7 @@ use super::parser;
#[derive(Debug, PartialEq, Eq, Clone)] #[derive(Debug, PartialEq, Eq, Clone)]
pub struct Draft { pub struct Draft {
pub headers: IndexMap<String, String>, pub headers: HeaderMap,
pub body: String, pub body: String,
pub attachments: Vec<AttachmentBuilder>, pub attachments: Vec<AttachmentBuilder>,
@ -47,17 +45,17 @@ pub struct Draft {
impl Default for Draft { impl Default for Draft {
fn default() -> Self { fn default() -> Self {
let mut headers = IndexMap::with_capacity_and_hasher(8, Default::default()); let mut headers = HeaderMap::default();
headers.insert( headers.insert(
"Date".into(), HeaderName::new_unchecked("Date"),
crate::datetime::timestamp_to_string(crate::datetime::now(), None), crate::datetime::timestamp_to_string(crate::datetime::now(), None),
); );
headers.insert("From".into(), "".into()); headers.insert(HeaderName::new_unchecked("From"), "".into());
headers.insert("To".into(), "".into()); headers.insert(HeaderName::new_unchecked("To"), "".into());
headers.insert("Cc".into(), "".into()); headers.insert(HeaderName::new_unchecked("Cc"), "".into());
headers.insert("Bcc".into(), "".into()); headers.insert(HeaderName::new_unchecked("Bcc"), "".into());
headers.insert(HeaderName::new_unchecked("Subject"), "".into());
headers.insert("Subject".into(), "".into());
Draft { Draft {
headers, headers,
body: String::new(), body: String::new(),
@ -78,20 +76,9 @@ impl str::FromStr for Draft {
let mut ret = Draft::default(); let mut ret = Draft::default();
for (k, v) in headers { for (k, v) in headers {
ret.headers.insert( ret.headers
String::from_utf8(k.to_vec())?, .insert(k.try_into()?, String::from_utf8(v.to_vec())?);
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()); let body = Envelope::new(0).body_bytes(s.as_bytes());
ret.body = String::from_utf8(decode(&body, None))?; ret.body = String::from_utf8(decode(&body, None))?;
@ -101,30 +88,28 @@ impl str::FromStr for Draft {
} }
impl Draft { impl Draft {
pub fn edit(envelope: &Envelope, mut op: Box<dyn BackendOp>) -> Result<Self> { pub fn edit(envelope: &Envelope, bytes: &[u8]) -> Result<Self> {
let mut ret = Draft::default(); let mut ret = Draft::default();
//TODO: Inform user if error //TODO: Inform user if error
{ for (k, v) in envelope.headers(&bytes).unwrap_or_else(|_| Vec::new()) {
let bytes = futures::executor::block_on(op.as_bytes()?)?; ret.headers.insert(k.try_into()?, v.into());
for (k, v) in envelope.headers(&bytes).unwrap_or_else(|_| Vec::new()) {
ret.headers.insert(k.into(), v.into());
}
} }
ret.body = envelope.body(op)?.text(); ret.body = envelope.body_bytes(bytes).text();
Ok(ret) Ok(ret)
} }
pub fn set_header(&mut self, header: &str, value: String) -> &mut Self { 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 self
} }
pub fn new_reply(envelope: &Envelope, bytes: &[u8], reply_to_all: bool) -> Self { pub fn new_reply(envelope: &Envelope, bytes: &[u8], reply_to_all: bool) -> Self {
let mut ret = Draft::default(); let mut ret = Draft::default();
ret.headers_mut().insert( ret.headers_mut().insert(
"References".into(), HeaderName::new_unchecked("References"),
format!( format!(
"{} {}", "{} {}",
envelope envelope
@ -140,35 +125,47 @@ impl Draft {
envelope.message_id_display() envelope.message_id_display()
), ),
); );
ret.headers_mut() ret.headers_mut().insert(
.insert("In-Reply-To".into(), envelope.message_id_display().into()); 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-Followup-To/(To+Cc+(Mail-Reply-To/Reply-To/From)) for follow-up,
// Mail-Reply-To/Reply-To/From for reply-to-author." // Mail-Reply-To/Reply-To/From for reply-to-author."
// source: https://cr.yp.to/proto/replyto.html // source: https://cr.yp.to/proto/replyto.html
if reply_to_all { if reply_to_all {
if let Some(reply_to) = envelope.other_headers().get("Mail-Followup-To") { 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 { } else {
if let Some(reply_to) = envelope.other_headers().get("Reply-To") { 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() 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 // FIXME: add To/Cc
} }
} else { } else {
if let Some(reply_to) = envelope.other_headers().get("Mail-Reply-To") { 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() 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() ret.headers_mut().insert(
.insert("Cc".into(), envelope.field_cc_to_string()); HeaderName::new_unchecked("Cc"),
envelope.field_cc_to_string(),
);
let body = envelope.body_bytes(bytes); let body = envelope.body_bytes(bytes);
ret.body = { ret.body = {
let reply_body_bytes = decode_rec(&body, None); let reply_body_bytes = decode_rec(&body, None);
@ -177,7 +174,7 @@ impl Draft {
let mut ret = format!( let mut ret = format!(
"On {} {} wrote:\n", "On {} {} wrote:\n",
envelope.date_as_str(), envelope.date_as_str(),
ret.headers()["To"] &ret.headers()["To"]
); );
for l in lines { for l in lines {
ret.push('>'); ret.push('>');
@ -191,11 +188,11 @@ impl Draft {
ret ret
} }
pub fn headers_mut(&mut self) -> &mut IndexMap<String, String> { pub fn headers_mut(&mut self) -> &mut HeaderMap {
&mut self.headers &mut self.headers
} }
pub fn headers(&self) -> &IndexMap<String, String> { pub fn headers(&self) -> &HeaderMap {
&self.headers &self.headers
} }
@ -219,7 +216,7 @@ impl Draft {
pub fn to_string(&self) -> Result<String> { pub fn to_string(&self) -> Result<String> {
let mut ret = String::new(); 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()); 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 Ok((_, addr)) = super::parser::address::mailbox(self.headers["From"].as_bytes())
{ {
if let Some(fqdn) = addr.get_fqdn() { if let Some(fqdn) = addr.get_fqdn() {
self.headers self.headers.insert(
.insert("Message-ID".into(), random::gen_message_id(&fqdn)); 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() { if v.is_ascii() {
ret.extend(format!("{}: {}\n", k, v).chars()); ret.extend(format!("{}: {}\n", k, v).chars());
} else { } else {

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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>(S);
///Case insensitive wrapper for a header name. As of `RFC5322` it's guaranteened to be ASCII.
pub type HeaderName = HeaderNameType<SmallVec<[u8; 32]>>;
impl HeaderName {
pub fn new_unchecked(from: &str) -> Self {
HeaderNameType(from.as_bytes().into())
}
}
impl<S: AsRef<[u8]>> fmt::Display for HeaderNameType<S> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.normalize())
}
}
impl<S: AsRef<[u8]>> fmt::Debug for HeaderNameType<S> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.as_str())
}
}
impl<S: AsRef<[u8]>> PartialEq<[u8]> for HeaderNameType<S> {
fn eq(&self, other: &[u8]) -> bool {
self.0.as_ref().eq_ignore_ascii_case(other)
}
}
impl<S: AsRef<[u8]>> PartialEq<&str> for HeaderNameType<S> {
fn eq(&self, other: &&str) -> bool {
self.0.as_ref().eq_ignore_ascii_case(other.as_bytes())
}
}
impl<S1: AsRef<[u8]>, S2: AsRef<[u8]>> PartialEq<HeaderNameType<S2>> for HeaderNameType<S1> {
fn eq(&self, other: &HeaderNameType<S2>) -> bool {
self.0.as_ref().eq_ignore_ascii_case(other.0.as_ref())
}
}
impl<S: AsRef<[u8]>> Eq for HeaderNameType<S> {}
impl<S: AsRef<[u8]>> Hash for HeaderNameType<S> {
fn hash<H: Hasher>(&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<Self, Self::Error> {
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<Self, Self::Error> {
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<H: Hasher>(&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<S: AsRef<[u8]>> HeaderKey for HeaderNameType<S> {
fn to_key(&self) -> &[u8] {
self.0.as_ref()
}
}
//Implement Borrow for all the lookup types as returning our trait object:
impl<'a> Borrow<dyn HeaderKey + 'a> for HeaderName {
fn borrow(&self) -> &(dyn HeaderKey + 'a) {
self
}
}
impl<S: AsRef<[u8]>> HeaderNameType<S> {
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<HeaderName, String>);
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<String> {
(self.0).remove(HeaderNameType(key).borrow() as &dyn HeaderKey)
}
}
impl Deref for HeaderMap {
type Target = IndexMap<HeaderName, String>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for HeaderMap {
fn deref_mut(&mut self) -> &mut IndexMap<HeaderName, String> {
&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 <a@b.c>".into());
assert_eq!(&headers["From"], "Myself <a@b.c>");
assert_eq!(&headers["From"], &headers["from"]);
assert_eq!(&headers["fROm"], &headers["from"]);
headers.get_mut("from").unwrap().pop();
assert_eq!(&headers["From"], "Myself <a@b.c");
headers.insert("frOM".try_into().unwrap(), "nada".into());
assert_eq!(&headers["fROm"], "nada");
}

View File

@ -137,7 +137,7 @@ impl fmt::Display for Composer {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
// TODO display subject/info // TODO display subject/info
if self.reply_context.is_some() { if self.reply_context.is_some() {
write!(f, "reply: {:8}", self.draft.headers()["Subject"]) write!(f, "reply: {:8}", &self.draft.headers()["Subject"])
} else { } else {
write!(f, "composing") write!(f, "composing")
} }
@ -158,21 +158,11 @@ impl Composer {
if v.is_empty() { if v.is_empty() {
continue; continue;
} }
if let Some(k) = ret ret.draft.set_header(h, v.into());
.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());
}
} }
if *mailbox_acc_settings!(context[account_hash].composing.insert_user_agent) { if *mailbox_acc_settings!(context[account_hash].composing.insert_user_agent) {
ret.draft.set_header( ret.draft.set_header(
"User-Agent".into(), "User-Agent",
format!("meli {}", option_env!("CARGO_PKG_VERSION").unwrap_or("0.0")), format!("meli {}", option_env!("CARGO_PKG_VERSION").unwrap_or("0.0")),
); );
} }
@ -181,14 +171,18 @@ impl Composer {
ret ret
} }
pub fn edit(new_account_hash: AccountHash, h: EnvelopeHash, context: &Context) -> Result<Self> { pub fn edit(
account_hash: AccountHash,
env_hash: EnvelopeHash,
bytes: &[u8],
context: &Context,
) -> Result<Self> {
let mut ret = Composer::default(); let mut ret = Composer::default();
let op = context.accounts[&new_account_hash].operation(h)?; let envelope: EnvelopeRef = context.accounts[&account_hash].collection.get_env(env_hash);
let envelope: EnvelopeRef = context.accounts[&new_account_hash].collection.get_env(h);
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) Ok(ret)
} }
@ -202,16 +196,16 @@ impl Composer {
let account = &context.accounts[&coordinates.0]; let account = &context.accounts[&coordinates.0];
let envelope = account.collection.get_env(coordinates.2); let envelope = account.collection.get_env(coordinates.2);
let subject = envelope.subject(); let subject = envelope.subject();
ret.draft.headers_mut().insert( ret.draft.set_header(
"Subject".into(), "Subject",
if !subject.starts_with("Re: ") { if !subject.starts_with("Re: ") {
format!("Re: {}", subject) format!("Re: {}", subject)
} else { } else {
subject.into() subject.into()
}, },
); );
ret.draft.headers_mut().insert( ret.draft.set_header(
"References".into(), "References",
format!( format!(
"{} {}", "{} {}",
envelope envelope
@ -228,8 +222,7 @@ impl Composer {
), ),
); );
ret.draft ret.draft
.headers_mut() .set_header("In-Reply-To", envelope.message_id_display().into());
.insert("In-Reply-To".into(), envelope.message_id_display().into());
// "Mail-Followup-To/(To+Cc+(Mail-Reply-To/Reply-To/From)) for follow-up, // "Mail-Followup-To/(To+Cc+(Mail-Reply-To/Reply-To/From)) for follow-up,
// Mail-Reply-To/Reply-To/From for reply-to-author." // Mail-Reply-To/Reply-To/From for reply-to-author."
@ -274,7 +267,7 @@ impl Composer {
{ {
to.remove(&ours); to.remove(&ours);
} }
ret.draft.headers_mut().insert("To".into(), { ret.draft.set_header("To", {
let mut ret: String = let mut ret: String =
to.into_iter() to.into_iter()
.fold(String::new(), |mut s: String, n: Address| { .fold(String::new(), |mut s: String, n: Address| {
@ -286,22 +279,14 @@ impl Composer {
ret.pop(); ret.pop();
ret ret
}); });
ret.draft ret.draft.set_header("Cc", envelope.field_cc_to_string());
.headers_mut()
.insert("Cc".into(), envelope.field_cc_to_string());
} else { } else {
if let Some(reply_to) = envelope.other_headers().get("Mail-Reply-To") { if let Some(reply_to) = envelope.other_headers().get("Mail-Reply-To") {
ret.draft ret.draft.set_header("To", reply_to.to_string());
.headers_mut()
.insert("To".into(), reply_to.to_string());
} else if let Some(reply_to) = envelope.other_headers().get("Reply-To") { } else if let Some(reply_to) = envelope.other_headers().get("Reply-To") {
ret.draft ret.draft.set_header("To", reply_to.to_string());
.headers_mut()
.insert("To".into(), reply_to.to_string());
} else { } else {
ret.draft ret.draft.set_header("To", envelope.field_from_to_string());
.headers_mut()
.insert("To".into(), envelope.field_from_to_string());
} }
} }
let body = envelope.body_bytes(bytes); let body = envelope.body_bytes(bytes);
@ -399,7 +384,7 @@ impl Composer {
let header_values = self.form.values_mut(); let header_values = self.form.values_mut();
let draft_header_map = self.draft.headers_mut(); let draft_header_map = self.draft.headers_mut();
for (k, v) in draft_header_map.iter_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(); *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() if !self.draft.headers().contains_key("From") || self.draft.headers()["From"].is_empty()
{ {
self.draft.headers_mut().insert( self.draft.set_header(
"From".into(), "From",
crate::components::mail::get_display_name(context, self.account_hash), crate::components::mail::get_display_name(context, self.account_hash),
); );
} }
@ -798,8 +783,7 @@ impl Component for Composer {
) if selector.id() == *id => { ) if selector.id() == *id => {
if let Some(to_val) = result.downcast_mut::<String>() { if let Some(to_val) = result.downcast_mut::<String>() {
self.draft self.draft
.headers_mut() .set_header("To", std::mem::replace(to_val, String::new()));
.insert("To".to_string(), std::mem::replace(to_val, String::new()));
self.update_form(); self.update_form();
} }
self.mode = ViewMode::Edit; self.mode = ViewMode::Edit;

View File

@ -1563,8 +1563,8 @@ impl Component for MailView {
list_management::ListAction::Email(email) => { list_management::ListAction::Email(email) => {
if let Ok(mailto) = Mailto::try_from(*email) { if let Ok(mailto) = Mailto::try_from(*email) {
let mut draft: Draft = mailto.into(); let mut draft: Draft = mailto.into();
draft.headers_mut().insert( draft.set_header(
"From".into(), "From",
crate::components::mail::get_display_name( crate::components::mail::get_display_name(
context, context,
self.coordinates.0, self.coordinates.0,

View File

@ -1675,7 +1675,8 @@ impl Component for Tabbed {
self.help_curr_views = children_maps; self.help_curr_views = children_maps;
return true; return true;
} }
UIEvent::Action(Tab(Edit(account_hash, msg))) => { UIEvent::Action(Tab(Edit(_, _))) => {
/* FIXME
let composer = match Composer::edit(*account_hash, *msg, context) { let composer = match Composer::edit(*account_hash, *msg, context) {
Ok(c) => c, Ok(c) => c,
Err(e) => { Err(e) => {
@ -1704,6 +1705,7 @@ impl Component for Tabbed {
let mut children_maps = self.children[self.cursor_pos].get_shortcuts(context); let mut children_maps = self.children[self.cursor_pos].get_shortcuts(context);
children_maps.extend(self.get_shortcuts(context)); children_maps.extend(self.get_shortcuts(context));
self.help_curr_views = children_maps; self.help_curr_views = children_maps;
*/
return true; return true;
} }
UIEvent::Action(Tab(New(ref mut e))) if e.is_some() => { UIEvent::Action(Tab(New(ref mut e))) if e.is_some() => {

View File

@ -1,8 +1,8 @@
Subject:
From: From:
To: To:
Cc: Cc:
Bcc: Bcc:
Subject:
MIME-Version: 1.0 MIME-Version: 1.0
Content-Type: multipart/mixed; charset="utf-8"; boundary="bzz_bzz__bzz__" Content-Type: multipart/mixed; charset="utf-8"; boundary="bzz_bzz__bzz__"

View File

@ -15,24 +15,8 @@ fn build_draft() {
_ => {} _ => {}
} }
} }
if new_draft.headers().contains_key("User-Agent") { new_draft.headers_mut().remove("User-Agent");
new_draft.headers_mut().remove("User-Agent"); new_draft.headers_mut().remove("Date");
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.attachments_mut().push(attachment); new_draft.attachments_mut().push(attachment);
new_draft.set_body("hello world.".to_string()); new_draft.set_body("hello world.".to_string());