Browse Source

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 1 year ago
parent
commit
8d50e83a33
Signed by: epilys GPG Key ID: 73627C2F690DF710
  1. 81
      melib/src/email.rs
  2. 101
      melib/src/email/compose.rs
  3. 288
      melib/src/email/headers.rs
  4. 68
      src/components/mail/compose.rs
  5. 4
      src/components/mail/view.rs
  6. 4
      src/components/utilities.rs
  7. 2
      tests/generated.mail
  8. 20
      tests/generating_email.rs

81
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<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
}

101
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<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())?,
);
}
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));
}
}
ret.headers
.insert(k.try_into()?, String::from_utf8(v.to_vec())?);
}
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());
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(),
);
}
}
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 {

288
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 <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");
}

68
src/components/mail/compose.rs

@ -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;

4
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,

4
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() => {

2
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__"

20
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());

Loading…
Cancel
Save