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 preservedmemfd
parent
0f3bf858a3
commit
8d50e83a33
|
@ -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<MessageID>,
|
||||
pub references: Option<References>,
|
||||
other_headers: HashMap<String, String>,
|
||||
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<String, String> {
|
||||
pub fn other_headers(&self) -> &HeaderMap {
|
||||
&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
|
||||
}
|
||||
|
||||
|
|
|
@ -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<String, String>,
|
||||
pub headers: HeaderMap,
|
||||
pub body: String,
|
||||
|
||||
pub attachments: Vec<AttachmentBuilder>,
|
||||
|
@ -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<dyn BackendOp>) -> Result<Self> {
|
||||
pub fn edit(envelope: &Envelope, bytes: &[u8]) -> Result<Self> {
|
||||
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<String, String> {
|
||||
pub fn headers_mut(&mut self) -> &mut HeaderMap {
|
||||
&mut self.headers
|
||||
}
|
||||
|
||||
pub fn headers(&self) -> &IndexMap<String, String> {
|
||||
pub fn headers(&self) -> &HeaderMap {
|
||||
&self.headers
|
||||
}
|
||||
|
||||
|
@ -219,7 +216,7 @@ impl Draft {
|
|||
pub fn to_string(&self) -> Result<String> {
|
||||
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 {
|
||||
|
|
|
@ -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");
|
||||
}
|
|
@ -137,7 +137,7 @@ impl fmt::Display for Composer {
|
|||
fn fmt(&self, f: &mut fmt::Formatter) -> 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<Self> {
|
||||
pub fn edit(
|
||||
account_hash: AccountHash,
|
||||
env_hash: EnvelopeHash,
|
||||
bytes: &[u8],
|
||||
context: &Context,
|
||||
) -> Result<Self> {
|
||||
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::<String>() {
|
||||
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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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() => {
|
||||
|
|
|
@ -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__"
|
||||
|
||||
|
|
|
@ -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());
|
||||
|
|
Loading…
Reference in New Issue