From 235fceaf2168af50c3804cecfbf69e64ff42598c Mon Sep 17 00:00:00 2001 From: Manos Pitsidianakis Date: Sun, 28 May 2023 17:40:50 +0300 Subject: [PATCH] melib: Add standard heeder constants in email::headers Like `http` crate does --- config_macros.rs | 2 + melib/src/email/compose.rs | 69 +-- melib/src/email/headers.rs | 253 +++------ melib/src/email/headers/names.rs | 800 +++++++++++++++++++++++++++ melib/src/email/mailto.rs | 8 +- melib/src/error.rs | 38 +- src/components/mail/compose.rs | 99 ++-- src/components/mail/compose/hooks.rs | 32 +- src/components/mail/view.rs | 2 +- src/conf/composing.rs | 4 +- src/conf/overrides.rs | 4 +- 11 files changed, 1008 insertions(+), 303 deletions(-) create mode 100644 melib/src/email/headers/names.rs diff --git a/config_macros.rs b/config_macros.rs index ce0ea750b..3744432a3 100644 --- a/config_macros.rs +++ b/config_macros.rs @@ -56,7 +56,9 @@ pub fn override_derive(filenames: &[(&str, &str)]) { #![allow(clippy::derivable_impls)] //! This module is automatically generated by config_macros.rs. + use super::*; +use melib::HeaderName; "## .to_string(); diff --git a/melib/src/email/compose.rs b/melib/src/email/compose.rs index 0a564a5ab..f8448c264 100644 --- a/melib/src/email/compose.rs +++ b/melib/src/email/compose.rs @@ -21,6 +21,7 @@ /*! Compose a `Draft`, with MIME and attachment support */ use std::{ + convert::TryFrom, ffi::OsStr, io::Read, path::{Path, PathBuf}, @@ -59,18 +60,18 @@ impl Default for Draft { fn default() -> Self { let mut headers = HeaderMap::default(); headers.insert( - HeaderName::new_unchecked("Date"), + HeaderName::DATE, crate::datetime::timestamp_to_string( crate::datetime::now(), Some(crate::datetime::RFC822_DATE), true, ), ); - 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(HeaderName::FROM, "".into()); + headers.insert(HeaderName::TO, "".into()); + headers.insert(HeaderName::CC, "".into()); + headers.insert(HeaderName::BCC, "".into()); + headers.insert(HeaderName::SUBJECT, "".into()); Draft { headers, @@ -118,12 +119,20 @@ impl Draft { Ok(ret) } - pub fn set_header(&mut self, header: &str, value: String) -> &mut Self { - self.headers - .insert(HeaderName::new_unchecked(header), value); + pub fn set_header(&mut self, header: HeaderName, value: String) -> &mut Self { + self.headers.insert(header, value); self } + pub fn try_set_header( + &mut self, + header: &str, + value: String, + ) -> std::result::Result<&mut Self, InvalidHeaderName> { + self.headers.insert(HeaderName::try_from(header)?, value); + Ok(self) + } + pub fn set_wrap_header_preamble(&mut self, value: Option<(String, String)>) -> &mut Self { self.wrap_header_preamble = value; self @@ -160,7 +169,7 @@ impl Draft { pub fn new_reply(envelope: &Envelope, bytes: &[u8], reply_to_all: bool) -> Self { let mut ret = Draft::default(); ret.headers_mut().insert( - HeaderName::new_unchecked("References"), + HeaderName::REFERENCES, format!( "{} {}", envelope @@ -177,7 +186,7 @@ impl Draft { ), ); ret.headers_mut().insert( - HeaderName::new_unchecked("In-Reply-To"), + HeaderName::IN_REPLY_TO, envelope.message_id_display().into(), ); // "Mail-Followup-To/(To+Cc+(Mail-Reply-To/Reply-To/From)) for follow-up, @@ -186,33 +195,27 @@ impl Draft { if reply_to_all { if let Some(reply_to) = envelope.other_headers().get("Mail-Followup-To") { ret.headers_mut() - .insert(HeaderName::new_unchecked("To"), reply_to.to_string()); + .insert(HeaderName::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()); + .insert(HeaderName::TO, reply_to.to_string()); } else { - ret.headers_mut().insert( - HeaderName::new_unchecked("To"), - envelope.field_from_to_string(), - ); + ret.headers_mut() + .insert(HeaderName::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(HeaderName::new_unchecked("To"), reply_to.to_string()); + .insert(HeaderName::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()); + .insert(HeaderName::TO, reply_to.to_string()); } else { - ret.headers_mut().insert( - HeaderName::new_unchecked("To"), - envelope.field_from_to_string(), - ); + ret.headers_mut() + .insert(HeaderName::TO, envelope.field_from_to_string()); } - ret.headers_mut().insert( - HeaderName::new_unchecked("Cc"), - envelope.field_cc_to_string(), - ); + ret.headers_mut() + .insert(HeaderName::CC, envelope.field_cc_to_string()); let body = envelope.body_bytes(bytes); ret.body = { let reply_body_bytes = body.decode_rec(Default::default()); @@ -221,7 +224,7 @@ impl Draft { let mut ret = format!( "On {} {} wrote:\n", envelope.date_as_str(), - &ret.headers()["To"] + &ret.headers()[HeaderName::TO] ); for l in lines { ret.push('>'); @@ -304,10 +307,8 @@ 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( - HeaderName::new_unchecked("Message-ID"), - random::gen_message_id(&fqdn), - ); + self.headers + .insert(HeaderName::MESSAGE_ID, random::gen_message_id(&fqdn)); } } } @@ -559,8 +560,8 @@ mod tests { default .set_wrap_header_preamble(Some(("".to_string()))) .set_body("αδφαφσαφασ".to_string()) - .set_header("Subject", "test_update()".into()) - .set_header("Date", "Sun, 16 Jun 2013 17:56:45 +0200".into()); + .set_header(HeaderName::SUBJECT, "test_update()".into()) + .set_header(HeaderName::DATE, "Sun, 16 Jun 2013 17:56:45 +0200".into()); let original = default.clone(); let s = default.to_edit_string(); diff --git a/melib/src/email/headers.rs b/melib/src/email/headers.rs index bfbd1893f..cbf26e02a 100644 --- a/melib/src/email/headers.rs +++ b/melib/src/email/headers.rs @@ -20,102 +20,18 @@ */ /*! Wrapper type `HeaderName` for case-insensitive comparisons */ + +pub mod names; use std::{ borrow::Borrow, cmp::{Eq, PartialEq}, - convert::TryFrom, - fmt, + convert::{TryFrom, TryInto}, hash::{Hash, Hasher}, ops::{Deref, DerefMut}, }; use indexmap::IndexMap; -use smallvec::SmallVec; - -use crate::error::Error; - -#[derive(Clone, Copy, Serialize, Deserialize)] -pub struct HeaderNameType(S); - -/// Case insensitive wrapper for a header name. As of `RFC5322` it's -/// guaranteed 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 = Error; - - fn try_from(value: &[u8]) -> Result { - if value.is_ascii() { - Ok(HeaderNameType(value.into())) - } else { - Err(Error::new(format!( - "Header value is not ascii: {:?}", - value - ))) - } - } -} - -impl TryFrom<&str> for HeaderName { - type Error = Error; - - fn try_from(value: &str) -> Result { - if value.is_ascii() { - Ok(HeaderNameType(value.as_bytes().into())) - } else { - Err(Error::new(format!( - "Header value is not ascii: {:?}", - value - ))) - } - } -} +pub use names::{HeaderName, InvalidHeaderName, Protocol, Standard, StandardHeader, Status}; trait HeaderKey { fn to_key(&self) -> &[u8]; @@ -137,9 +53,9 @@ impl PartialEq for dyn HeaderKey + '_ { impl Eq for dyn HeaderKey + '_ {} -impl> HeaderKey for HeaderNameType { +impl HeaderKey for HeaderName { fn to_key(&self) -> &[u8] { - self.0.as_ref() + self.as_bytes() } } @@ -151,119 +67,94 @@ impl<'a> Borrow for HeaderName { } } -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() - } - } -} - +/// Map of mail headers and values. +/// +/// Can be indexed by: +/// +/// - `usize` +/// - `&[u8]`, which panics if it's not a valid header value. +/// - `&str`, which also panics if it's not a valid header value. +/// - [HeaderName], which is guaranteed to be valid. +/// +/// # Panics +/// +/// Except for the above, indexing will also panic if index is out of range or +/// header key is not present in the map. #[derive(Debug, Default, PartialEq, Eq, Clone, Serialize, Deserialize)] pub struct HeaderMap(indexmap::IndexMap); +impl std::ops::Index for HeaderMap { + type Output = str; + fn index(&self, k: usize) -> &Self::Output { + (self.0)[k].as_str() + } +} + 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() + (self.0)[HeaderName::try_from(k) + .expect("Invalid bytes in header name.") + .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() + (self.0)[&HeaderName::try_from(k).expect("Invalid bytes in header name.")].as_str() + } +} + +impl std::ops::Index<&HeaderName> for HeaderMap { + type Output = str; + fn index(&self, k: &HeaderName) -> &Self::Output { + (self.0)[k].as_str() + } +} + +impl std::ops::Index for HeaderMap { + type Output = str; + fn index(&self, k: HeaderName) -> &Self::Output { + (self.0)[&k].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_mut + std::fmt::Debug>( + &mut self, + key: T, + ) -> Option<&mut String> + where + >::Error: std::fmt::Debug, + { + let k = key.try_into().expect("Invalid bytes in header name."); + (self.0).get_mut(&k) } - pub fn get(&self, key: &str) -> Option<&str> { - (self.0) - .get(HeaderNameType(key).borrow() as &dyn HeaderKey) - .map(|x| x.as_str()) + pub fn get + std::fmt::Debug>(&self, key: T) -> Option<&str> + where + >::Error: std::fmt::Debug, + { + let k = key.try_into().expect("Invalid bytes in header name."); + (self.0).get(&k).map(|x| x.as_str()) } - pub fn contains_key(&self, key: &str) -> bool { - (self.0).contains_key(HeaderNameType(key).borrow() as &dyn HeaderKey) + pub fn contains_key + std::fmt::Debug>(&self, key: T) -> bool + where + >::Error: std::fmt::Debug, + { + let k = key.try_into().expect("Invalid bytes in header name."); + (self.0).contains_key(&k) } - pub fn remove(&mut self, key: &str) -> Option { - (self.0).remove(HeaderNameType(key).borrow() as &dyn HeaderKey) + pub fn remove + std::fmt::Debug>(&mut self, key: T) -> Option + where + >::Error: std::fmt::Debug, + { + let k = key.try_into().expect("Invalid bytes in header name."); + (self.0).remove(&k) } } diff --git a/melib/src/email/headers/names.rs b/melib/src/email/headers/names.rs new file mode 100644 index 000000000..7895df057 --- /dev/null +++ b/melib/src/email/headers/names.rs @@ -0,0 +1,800 @@ +/* + * meli - melib crate. + * + * Copyright 2023 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 . + */ + +/*! E-mail header names. Also referred to as Fields in [RFC5322] */ +#![allow(non_upper_case_globals)] + +use std::{ + borrow::{Borrow, Cow}, + convert::TryFrom, + error::Error, + hash::{Hash, Hasher}, + str::FromStr, +}; + +use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; +use smallvec::SmallVec; + +use crate::email::parser::BytesExt; + +bitflags! { + #[derive(Default, Serialize, Deserialize)] + pub struct Protocol: u32 { + const None = 0b00000001; + const Mail = Self::None.bits() << 1; + const NNTP = Self::Mail.bits() << 1; + const MIME = Self::NNTP.bits() << 1; + } +} + +/// Case insensitive wrapper for a header name. As of `RFC5322` it's +/// guaranteed to be ASCII. +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub struct HeaderName { + inner: Repr, +} + +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +enum Repr { + Standard(StandardHeader), + Custom(T), +} + +// Used to hijack the Hash impl +#[derive(Debug, Clone, Eq, PartialEq)] +struct Custom(SmallVec<[u8; 32]>); + +/// A possible error when converting a `HeaderName` from another type. +pub struct InvalidHeaderName; + +impl Error for InvalidHeaderName {} + +impl std::fmt::Debug for InvalidHeaderName { + fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(fmt, "{}", "Invalid header name.") + } +} + +impl std::fmt::Display for InvalidHeaderName { + fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(fmt, "{}", stringify!(InvalidHeaderName)) + } +} + +macro_rules! standard_headers { + ( + $( + $(#[$docs:meta])* + ($konst:ident, $upcase:ident, $name:literal, $template:expr, $(Protocol::$var:tt)|+,$status:expr,$standards:expr); + )+ + ) => { + #[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)] + pub enum StandardHeader { + $( + $konst, + )+ + } + + $( + $(#[$docs])* + pub const $upcase: HeaderName = HeaderName { + inner: Repr::Standard(StandardHeader::$konst), + }; + )+ + + impl HeaderName { + $( + pub const $upcase: Self = $upcase; + )+ + } + + impl StandardHeader { + #[inline] + pub const fn as_str(&self) -> &'static str { + match *self { + $( + Self::$konst => $name, + )+ + } + } + + #[inline] + pub const fn protocol(&self) -> Protocol { + match *self { + $( + Self::$konst => Protocol::from_bits_truncate($(Protocol::$var.bits()|)* u32::MAX), + )+ + } + } + + #[inline] + pub const fn status(&self) -> Status { + match *self { + $( + Self::$konst => $status, + )+ + } + } + + #[inline] + pub const fn standards(&self) -> &[Standard] { + match *self { + $( + Self::$konst => $standards, + )+ + } + } + + pub fn from_bytes(name_bytes: &[u8]) -> Option { + match name_bytes { + $( + _ if name_bytes.eq_ignore_ascii_case($name.as_bytes()) => Some(Self::$konst), + )+ + _ => None, + } + } + } + + #[cfg(test)] + const TEST_HEADERS: &[(StandardHeader, &str)] = &[ + $( + (StandardHeader::$konst, $name), + )+ + ]; + + #[test] + fn test_parse_standard_headers() { + for &(std, name) in TEST_HEADERS { + // Test lower case + assert_eq!(HeaderName::from_bytes(name.as_bytes()).unwrap(), HeaderName::from(std)); + + // Test upper case + let upper = std::str::from_utf8(name.as_bytes()).expect("byte string constants are all utf-8").to_uppercase(); + assert_eq!(HeaderName::from_bytes(upper.as_bytes()).unwrap(), HeaderName::from(std)); + } + } + + #[test] + fn test_standard_headers_into_bytes() { + //for &(std, name) in TEST_HEADERS { + //let name = std::str::from_utf8(name.as_bytes()).unwrap(); + //let std = HeaderName::from(std); + // Test lower case + //let bytes: Bytes = + // HeaderName::from_bytes(name.as_bytes()).unwrap().inner.into(); + //assert_eq!(bytes, name); + //assert_eq!(HeaderName::from_bytes(name.as_bytes()).unwrap(), std); + + // Test upper case + // let upper = name.to_uppercase(); + //let bytes: Bytes = + // HeaderName::from_bytes(upper.as_bytes()).unwrap().inner.into(); + //assert_eq!(bytes, name.as_bytes()); + //assert_eq!(HeaderName::from_bytes(upper.as_bytes()).unwrap(), + // std); + //} + + } + } +} + +macro_rules! standards { + ( + $( + $(#[$docs:meta])* + ($konst:ident, $upcase:ident, $name:literal, $lowername:literal ); + )+ + ) => { + #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] + pub enum Standard { + $( + $konst, + )+ + } + + $( + $(#[$docs])* + pub const $upcase: Standard = Standard::$konst; + )+ + + impl Standard { + #[inline] + pub const fn as_str(&self) -> &'static str { + match *self { + $( + Self::$konst => $name, + )+ + } + } + + #[inline] + pub const fn url(&self) -> &str { + match *self { + $( + Self::$konst => concat!("https://www.rfc-editor.org/rfc/", $lowername, ".html"), + )+ + } + + } + + pub fn from_bytes(name_bytes: &[u8]) -> Option { + match name_bytes { + $( + _ if name_bytes.eq_ignore_ascii_case($name.as_bytes()) => Some(Self::$konst), + )+ + _ => None, + } + } + } + }; +} + +standards! { + (RFC0850, RFC0850, "RFC0850", "rfc0850"); + (RFC1808, RFC1808, "RFC1808", "rfc1808"); + (RFC1849, RFC1849, "RFC1849", "rfc1849"); + (RFC2068, RFC2068, "RFC2068", "rfc2068"); + (RFC2076, RFC2076, "RFC2076", "rfc2076"); + (RFC2110, RFC2110, "RFC2110", "rfc2110"); + (RFC2156, RFC2156, "RFC2156", "rfc2156"); + (RFC2557, RFC2557, "RFC2557", "rfc2557"); + (RFC2616, RFC2616, "RFC2616", "rfc2616"); + (RFC2980, RFC2980, "RFC2980", "rfc2980"); + (RFC3798, RFC3798, "RFC3798", "rfc3798"); + (RFC3834, RFC3834, "RFC3834", "rfc3834"); + (RFC3865, RFC3865, "RFC3865", "rfc3865"); + (RFC3977, RFC3977, "RFC3977", "rfc3977"); + (RFC4021, RFC4021, "RFC4021", "rfc4021"); + (RFC5064, RFC5064, "RFC5064", "rfc5064"); + (RFC5321, RFC5321, "RFC5321", "rfc5321"); + (RFC5322, RFC5322, "RFC5322", "rfc5322"); + (RFC5337, RFC5337, "RFC5337", "rfc5337"); + (RFC5504, RFC5504, "RFC5504", "rfc5504"); + (RFC5518, RFC5518, "RFC5518", "rfc5518"); + (RFC5536, RFC5536, "RFC5536", "rfc5536"); + (RFC5537, RFC5537, "RFC5537", "rfc5537"); + (RFC5703, RFC5703, "RFC5703", "rfc5703"); + (RFC6017, RFC6017, "RFC6017", "rfc6017"); + (RFC6068, RFC6068, "RFC6068", "rfc6068"); + (RFC6109, RFC6109, "RFC6109", "rfc6109"); + (RFC6376, RFC6376, "RFC6376", "rfc6376"); + (RFC6477, RFC6477, "RFC6477", "rfc6477"); + (RFC6758, RFC6758, "RFC6758", "rfc6758"); + (RFC6854, RFC6854, "RFC6854", "rfc6854"); + (RFC6857, RFC6857, "RFC6857", "rfc6857"); + (RFC7208, RFC7208, "RFC7208", "rfc7208"); + (RFC7259, RFC7259, "RFC7259", "rfc7259"); + (RFC7293, RFC7293, "RFC7293", "rfc7293"); + (RFC7444, RFC7444, "RFC7444", "rfc7444"); + (RFC7681, RFC7681, "RFC7681", "rfc7681"); + (RFC8058, RFC8058, "RFC8058", "rfc8058"); + (RFC8255, RFC8255, "RFC8255", "rfc8255"); + (RFC8315, RFC8315, "RFC8315", "rfc8315"); + (RFC8460, RFC8460, "RFC8460", "rfc8460"); + (RFC8601, RFC8601, "RFC8601", "rfc8601"); + (RFC8617, RFC8617, "RFC8617", "rfc8617"); + (RFC8689, RFC8689, "RFC8689", "rfc8689"); + (RFC9057, RFC9057, "RFC9057", "rfc9057"); + (RFC9228, RFC9228, "RFC9228", "rfc9228"); +} + +/// Status of field at the moment of writing. +#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)] +pub enum Status { + /// Deprecated, + Deprecated, + /// Experimental, + Experimental, + /// Informational, + Informational, + /// None, + None, + /// Obsoleted, + Obsoleted, + /// Reserved, + Reserved, + /// Standard, + Standard, +} + +// Generate constants for all standard e-mail field headers. +standard_headers! { +/* Unit Variant |Constant ident |Actual field value |Template value |Protocols |Status |Standards */ +/* -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- */ + (Subject, SUBJECT, "Subject", None, Protocol::Mail | Protocol::NNTP, Status::Standard, &[Standard::RFC5536, Standard::RFC5322]); + (ReplyTo, REPLY_TO, "Reply-To", None, Protocol::Mail | Protocol::NNTP, Status::Standard, &[Standard::RFC5536, Standard::RFC5322]); + (InReplyTo, IN_REPLY_TO, "In-Reply-To", None, Protocol::Mail, Status::Standard, &[Standard::RFC5322]); + (References, REFERENCES, "References", None, Protocol::Mail | Protocol::NNTP, Status::Standard, &[Standard::RFC5536, Standard::RFC5322]); + (MailReplyTo, MAIL_REPLY_TO, "Mail-Reply-To", None, Protocol::Mail, Status::None, &[]); + (MailFollowupTo, MAIL_FOLLOWUP_TO, "Mail-Followup-To", None, Protocol::Mail, Status::None, &[]); + (DeliveredTo, DELIVERED_TO, "Delivered-To", None, Protocol::Mail, Status::None, &[Standard::RFC9228]); + (Comments, COMMENTS, "Comments", None, Protocol::Mail, Status::None, &[]); + (Keywords, KEYWORDS, "Keywords", None, Protocol::Mail, Status::None, &[]); + (Received, RECEIVED, "Received", None, Protocol::Mail, Status::Standard, &[Standard::RFC5322, Standard::RFC5321]); + (ContentLanguage, CONTENT_LANGUAGE, "Content-Language", None, Protocol::MIME, Status::None, &[Standard::RFC4021]); + (ContentLength, CONTENT_LENGTH, "Content-Length", None, Protocol::Mail, Status::None, &[]); + (Forwarded, FORWARDED, "Forwarded", None, Protocol::Mail, Status::None, &[]); + (AcceptLanguage, ACCEPT_LANGUAGE, "Accept-Language", None, Protocol::Mail, Status::None, &[Standard::RFC4021]); + (AlsoControl, ALSO_CONTROL, "Also-Control", None, Protocol::NNTP, Status::Obsoleted, &[Standard::RFC1849, Standard::RFC5536]); + (AlternateRecipient, ALTERNATE_RECIPIENT, "Alternate-Recipient", None, Protocol::Mail, Status::None, &[Standard::RFC4021]); + (Approved, APPROVED, "Approved", None, Protocol::NNTP, Status::Standard, &[Standard::RFC5536]); + (ArcAuthenticationResults, ARC_AUTHENTICATION_RESULTS, "ARC-Authentication-Results", None, Protocol::Mail, Status::Experimental, &[Standard::RFC8617]); + (ArcMessageSignature, ARC_MESSAGE_SIGNATURE, "ARC-Message-Signature", None, Protocol::Mail, Status::Experimental, &[Standard::RFC8617]); + (ArcSeal, ARC_SEAL, "ARC-Seal", None, Protocol::Mail, Status::Experimental, &[Standard::RFC8617]); + (Archive, ARCHIVE, "Archive", None, Protocol::NNTP, Status::Standard, &[Standard::RFC5536]); + (ArchivedAt, ARCHIVED_AT, "Archived-At", None, Protocol::Mail | Protocol::NNTP, Status::Standard, &[Standard::RFC5064]); + (ArticleNames, ARTICLE_NAMES, "Article-Names", None, Protocol::NNTP, Status::Obsoleted, &[Standard::RFC1849, Standard::RFC5536]); + (ArticleUpdates, ARTICLE_UPDATES, "Article-Updates", None, Protocol::NNTP, Status::Obsoleted, &[Standard::RFC1849, Standard::RFC5536]); + (AuthenticationResults, AUTHENTICATION_RESULTS, "Authentication-Results", None, Protocol::Mail, Status::Standard, &[Standard::RFC8601]); + (AutoSubmitted, AUTO_SUBMITTED, "Auto-Submitted", None, Protocol::Mail, Status::Standard, &[Standard::RFC3834]); + (Autoforwarded, AUTOFORWARDED, "Autoforwarded", None, Protocol::Mail, Status::None, &[Standard::RFC4021]); + (Autosubmitted, AUTOSUBMITTED, "Autosubmitted", None, Protocol::Mail, Status::None, &[Standard::RFC4021]); + (Base, BASE, "Base", None, Protocol::MIME, Status::Obsoleted, &[Standard::RFC1808, Standard::RFC2068]); + (Bcc, BCC, "Bcc", None, Protocol::Mail, Status::Standard, &[Standard::RFC5322]); + (Body, BODY, "Body", None, Protocol::None, Status::Reserved, &[Standard::RFC6068]); + (CancelKey, CANCEL_KEY, "Cancel-Key", None, Protocol::NNTP, Status::Standard, &[Standard::RFC8315]); + (CancelLock, CANCEL_LOCK, "Cancel-Lock", None, Protocol::NNTP, Status::Standard, &[Standard::RFC8315]); + (Cc, CC, "Cc", None, Protocol::Mail, Status::Standard, &[Standard::RFC5322]); + (ContentAlternative, CONTENT_ALTERNATIVE, "Content-Alternative", None, Protocol::MIME, Status::None, &[Standard::RFC4021]); + (ContentBase, CONTENT_BASE, "Content-Base", None, Protocol::MIME, Status::Obsoleted, &[Standard::RFC2110, Standard::RFC2557]); + (ContentDescription, CONTENT_DESCRIPTION, "Content-Description", None, Protocol::MIME, Status::None, &[Standard::RFC4021]); + (ContentDisposition, CONTENT_DISPOSITION, "Content-Disposition", None, Protocol::MIME, Status::None, &[Standard::RFC4021]); + (ContentDuration, CONTENT_DURATION, "Content-Duration", None, Protocol::MIME, Status::None, &[Standard::RFC4021]); + (ContentFeatures, CONTENT_FEATURES, "Content-Features", None, Protocol::MIME, Status::None, &[Standard::RFC4021]); + (ContentId, CONTENT_ID, "Content-ID", None, Protocol::MIME, Status::None, &[Standard::RFC4021]); + (ContentIdentifier, CONTENT_IDENTIFIER, "Content-Identifier", None, Protocol::Mail, Status::None, &[Standard::RFC4021]); + (ContentLocation, CONTENT_LOCATION, "Content-Location", None, Protocol::MIME, Status::None, &[Standard::RFC4021]); + (ContentMd5, CONTENT_MD5, "Content-MD5", None, Protocol::MIME, Status::None, &[Standard::RFC4021]); + (ContentReturn, CONTENT_RETURN, "Content-Return", None, Protocol::Mail, Status::None, &[Standard::RFC4021]); + (ContentTransferEncoding, CONTENT_TRANSFER_ENCODING, "Content-Transfer-Encoding", None, Protocol::MIME, Status::None, &[Standard::RFC4021]); + (ContentTranslationType, CONTENT_TRANSLATION_TYPE, "Content-Translation-Type", None, Protocol::MIME, Status::Standard, &[Standard::RFC8255]); + (ContentType, CONTENT_TYPE, "Content-Type", None, Protocol::MIME, Status::None, &[Standard::RFC4021]); + (Control, CONTROL, "Control", None, Protocol::NNTP, Status::Standard, &[Standard::RFC5536]); + (Conversion, CONVERSION, "Conversion", None, Protocol::Mail, Status::None, &[Standard::RFC4021]); + (ConversionWithLoss, CONVERSION_WITH_LOSS, "Conversion-With-Loss", None, Protocol::Mail, Status::None, &[Standard::RFC4021]); + (DlExpansionHistory, DL_EXPANSION_HISTORY, "DL-Expansion-History", None, Protocol::Mail, Status::None, &[Standard::RFC4021]); + (Date, DATE, "Date", None, Protocol::Mail | Protocol::NNTP, Status::Standard, &[Standard::RFC5536, Standard::RFC5322]); + (DateReceived, DATE_RECEIVED, "Date-Received", None, Protocol::NNTP, Status::Obsoleted, &[Standard::RFC0850, Standard::RFC5536]); + (DeferredDelivery, DEFERRED_DELIVERY, "Deferred-Delivery", None, Protocol::Mail, Status::None, &[Standard::RFC4021]); + (DeliveryDate, DELIVERY_DATE, "Delivery-Date", None, Protocol::Mail, Status::None, &[Standard::RFC4021]); + (DiscardedX400IpmsExtensions, DISCARDED_X400_IPMS_EXTENSIONS, "Discarded-X400-IPMS-Extensions", None, Protocol::Mail, Status::None, &[Standard::RFC4021]); + (DiscardedX400MtsExtensions, DISCARDED_X400_MTS_EXTENSIONS, "Discarded-X400-MTS-Extensions", None, Protocol::Mail, Status::None, &[Standard::RFC4021]); + (DiscloseRecipients, DISCLOSE_RECIPIENTS, "Disclose-Recipients", None, Protocol::Mail, Status::None, &[Standard::RFC4021]); + (DispositionNotificationOptions, DISPOSITION_NOTIFICATION_OPTIONS, "Disposition-Notification-Options", None, Protocol::Mail, Status::None, &[Standard::RFC4021]); + (DispositionNotificationTo, DISPOSITION_NOTIFICATION_TO, "Disposition-Notification-To", None, Protocol::Mail, Status::None, &[Standard::RFC4021]); + (Distribution, DISTRIBUTION, "Distribution", None, Protocol::NNTP, Status::Standard, &[Standard::RFC5536]); + (DkimSignature, DKIM_SIGNATURE, "DKIM-Signature", None, Protocol::Mail, Status::Standard, &[Standard::RFC6376]); + (DowngradedBcc, DOWNGRADED_BCC, "Downgraded-Bcc", None, Protocol::Mail, Status::Obsoleted, &[Standard::RFC5504, Standard::RFC6857]); + (DowngradedCc, DOWNGRADED_CC, "Downgraded-Cc", None, Protocol::Mail, Status::Obsoleted, &[Standard::RFC5504, Standard::RFC6857]); + (DowngradedDispositionNotificationTo, DOWNGRADED_DISPOSITION_NOTIFICATION_TO, "Downgraded-Disposition-Notification-To", None, Protocol::Mail, Status::Obsoleted, &[Standard::RFC5504, Standard::RFC6857]); + (DowngradedFinalRecipient, DOWNGRADED_FINAL_RECIPIENT, "Downgraded-Final-Recipient", None, Protocol::Mail, Status::Standard, &[Standard::RFC6857]); + (DowngradedFrom, DOWNGRADED_FROM, "Downgraded-From", None, Protocol::Mail, Status::Obsoleted, &[Standard::RFC5504, Standard::RFC6857]); + (DowngradedInReplyTo, DOWNGRADED_IN_REPLY_TO, "Downgraded-In-Reply-To", None, Protocol::Mail, Status::Standard, &[Standard::RFC6857]); + (DowngradedMailFrom, DOWNGRADED_MAIL_FROM, "Downgraded-Mail-From", None, Protocol::Mail, Status::Obsoleted, &[Standard::RFC5504, Standard::RFC6857]); + (DowngradedMessageId, DOWNGRADED_MESSAGE_ID, "Downgraded-Message-Id", None, Protocol::Mail, Status::Standard, &[Standard::RFC6857]); + (DowngradedOriginalRecipient, DOWNGRADED_ORIGINAL_RECIPIENT, "Downgraded-Original-Recipient", None, Protocol::Mail, Status::Standard, &[Standard::RFC6857]); + (DowngradedRcptTo, DOWNGRADED_RCPT_TO, "Downgraded-Rcpt-To", None, Protocol::Mail, Status::Obsoleted, &[Standard::RFC5504, Standard::RFC6857]); + (DowngradedReferences, DOWNGRADED_REFERENCES, "Downgraded-References", None, Protocol::Mail, Status::Standard, &[Standard::RFC6857]); + (DowngradedReplyTo, DOWNGRADED_REPLY_TO, "Downgraded-Reply-To", None, Protocol::Mail, Status::Obsoleted, &[Standard::RFC5504, Standard::RFC6857]); + (DowngradedResentBcc, DOWNGRADED_RESENT_BCC, "Downgraded-Resent-Bcc", None, Protocol::Mail, Status::Obsoleted, &[Standard::RFC5504, Standard::RFC6857]); + (DowngradedResentCc, DOWNGRADED_RESENT_CC, "Downgraded-Resent-Cc", None, Protocol::Mail, Status::Obsoleted, &[Standard::RFC5504, Standard::RFC6857]); + (DowngradedResentFrom, DOWNGRADED_RESENT_FROM, "Downgraded-Resent-From", None, Protocol::Mail, Status::Obsoleted, &[Standard::RFC5504, Standard::RFC6857]); + (DowngradedResentReplyTo, DOWNGRADED_RESENT_REPLY_TO, "Downgraded-Resent-Reply-To", None, Protocol::Mail, Status::Obsoleted, &[Standard::RFC5504, Standard::RFC6857]); + (DowngradedResentSender, DOWNGRADED_RESENT_SENDER, "Downgraded-Resent-Sender", None, Protocol::Mail, Status::Obsoleted, &[Standard::RFC5504, Standard::RFC6857]); + (DowngradedResentTo, DOWNGRADED_RESENT_TO, "Downgraded-Resent-To", None, Protocol::Mail, Status::Obsoleted, &[Standard::RFC5504, Standard::RFC6857]); + (DowngradedReturnPath, DOWNGRADED_RETURN_PATH, "Downgraded-Return-Path", None, Protocol::Mail, Status::Obsoleted, &[Standard::RFC5504, Standard::RFC6857]); + (DowngradedSender, DOWNGRADED_SENDER, "Downgraded-Sender", None, Protocol::Mail, Status::Obsoleted, &[Standard::RFC5504, Standard::RFC6857]); + (DowngradedTo, DOWNGRADED_TO, "Downgraded-To", None, Protocol::Mail, Status::Obsoleted, &[Standard::RFC5504, Standard::RFC6857]); + (Encoding, ENCODING, "Encoding", None, Protocol::Mail, Status::None, &[Standard::RFC4021]); + (Encrypted, ENCRYPTED, "Encrypted", None, Protocol::Mail, Status::None, &[Standard::RFC4021]); + (Expires, EXPIRES, "Expires", None, Protocol::Mail | Protocol::NNTP, Status::None, &[Standard::RFC4021, Standard::RFC5536]); + (ExpiryDate, EXPIRY_DATE, "Expiry-Date", None, Protocol::Mail, Status::None, &[Standard::RFC4021]); + (FollowupTo, FOLLOWUP_TO, "Followup-To", None, Protocol::NNTP, Status::Standard, &[Standard::RFC5536]); + (From, FROM, "From", None, Protocol::Mail | Protocol::NNTP, Status::Standard, &[Standard::RFC5322, Standard::RFC6854]); + (GenerateDeliveryReport, GENERATE_DELIVERY_REPORT, "Generate-Delivery-Report", None, Protocol::Mail, Status::None, &[Standard::RFC4021]); + (Importance, IMPORTANCE, "Importance", None, Protocol::Mail, Status::None, &[Standard::RFC4021]); + (IncompleteCopy, INCOMPLETE_COPY, "Incomplete-Copy", None, Protocol::Mail, Status::None, &[Standard::RFC4021]); + (InjectionDate, INJECTION_DATE, "Injection-Date", None, Protocol::NNTP, Status::Standard, &[Standard::RFC5536]); + (InjectionInfo, INJECTION_INFO, "Injection-Info", None, Protocol::NNTP, Status::Standard, &[Standard::RFC5536]); + (Language, LANGUAGE, "Language", None, Protocol::Mail, Status::None, &[Standard::RFC4021]); + (LatestDeliveryTime, LATEST_DELIVERY_TIME, "Latest-Delivery-Time", None, Protocol::Mail, Status::None, &[Standard::RFC4021]); + (Lines, LINES, "Lines", None, Protocol::NNTP, Status::Deprecated, &[Standard::RFC5536, Standard::RFC3977]); + (ListArchive, LIST_ARCHIVE, "List-Archive", None, Protocol::Mail, Status::None, &[Standard::RFC4021]); + (ListHelp, LIST_HELP, "List-Help", None, Protocol::Mail, Status::None, &[Standard::RFC4021]); + (ListId, LIST_ID, "List-ID", None, Protocol::Mail, Status::None, &[Standard::RFC4021]); + (ListOwner, LIST_OWNER, "List-Owner", None, Protocol::Mail, Status::None, &[Standard::RFC4021]); + (ListPost, LIST_POST, "List-Post", None, Protocol::Mail, Status::None, &[Standard::RFC4021]); + (ListSubscribe, LIST_SUBSCRIBE, "List-Subscribe", None, Protocol::Mail, Status::None, &[Standard::RFC4021]); + (ListUnsubscribe, LIST_UNSUBSCRIBE, "List-Unsubscribe", Some("perm/list-unsubscribe"), Protocol::Mail, Status::None, &[Standard::RFC4021]); + (ListUnsubscribePost, LIST_UNSUBSCRIBE_POST, "List-Unsubscribe-Post", None, Protocol::Mail, Status::Standard, &[Standard::RFC8058]); + (MessageContext, MESSAGE_CONTEXT, "Message-Context", None, Protocol::Mail, Status::None, &[Standard::RFC4021]); + (MessageId, MESSAGE_ID, "Message-ID", None, Protocol::Mail | Protocol::NNTP, Status::Standard, &[Standard::RFC5322, Standard::RFC5536]); + (MessageType, MESSAGE_TYPE, "Message-Type", None, Protocol::Mail, Status::None, &[Standard::RFC4021]); + (MimeVersion, MIME_VERSION, "MIME-Version", None, Protocol::MIME, Status::None, &[Standard::RFC4021]); + (MtPriority, MT_PRIORITY, "MT-Priority", None, Protocol::Mail, Status::Standard, &[Standard::RFC6758]); + (Newsgroups, NEWSGROUPS, "Newsgroups", None, Protocol::NNTP, Status::Standard, &[Standard::RFC5536]); + (NntpPostingDate, NNTP_POSTING_DATE, "NNTP-Posting-Date", None, Protocol::NNTP, Status::Obsoleted, &[Standard::RFC5536]); + (NntpPostingHost, NNTP_POSTING_HOST, "NNTP-Posting-Host", None, Protocol::NNTP, Status::Obsoleted, &[Standard::RFC2980, Standard::RFC5536]); + (Obsoletes, OBSOLETES, "Obsoletes", None, Protocol::Mail, Status::None, &[Standard::RFC4021]); + (Organization, ORGANIZATION, "Organization", None, Protocol::Mail | Protocol::NNTP, Status::Informational, &[Standard::RFC7681, Standard::RFC5536]); + (OriginalEncodedInformationTypes, ORIGINAL_ENCODED_INFORMATION_TYPES, "Original-Encoded-Information-Types", None, Protocol::Mail, Status::None, &[Standard::RFC4021]); + (OriginalFrom, ORIGINAL_FROM, "Original-From", None, Protocol::Mail, Status::Standard, &[Standard::RFC5703]); + (OriginalMessageId, ORIGINAL_MESSAGE_ID, "Original-Message-ID", None, Protocol::Mail, Status::None, &[Standard::RFC4021]); + (OriginalRecipient, ORIGINAL_RECIPIENT, "Original-Recipient", Some("perm/original-recipient"),Protocol::Mail, Status::Standard, &[Standard::RFC3798, Standard::RFC5337]); + (OriginalSender, ORIGINAL_SENDER, "Original-Sender", None, Protocol::NNTP, Status::Standard, &[Standard::RFC5537]); + (OriginatorReturnAddress, ORIGINATOR_RETURN_ADDRESS, "Originator-Return-Address", None, Protocol::Mail, Status::None, &[Standard::RFC4021]); + (OriginalSubject, ORIGINAL_SUBJECT, "Original-Subject", None, Protocol::Mail, Status::Standard, &[Standard::RFC5703]); + (Path, PATH, "Path", None, Protocol::NNTP, Status::Standard, &[Standard::RFC5536]); + (PicsLabel, PICS_LABEL, "PICS-Label", None, Protocol::Mail, Status::None, &[Standard::RFC4021]); + (PostingVersion, POSTING_VERSION, "Posting-Version", None, Protocol::NNTP, Status::Obsoleted, &[Standard::RFC0850, Standard::RFC5536]); + (PreventNondeliveryReport, PREVENT_NONDELIVERY_REPORT, "Prevent-NonDelivery-Report", None, Protocol::Mail, Status::None, &[Standard::RFC4021]); + (Priority, PRIORITY, "Priority", None, Protocol::Mail, Status::None, &[Standard::RFC4021]); + (ReceivedSpf, RECEIVED_SPF, "Received-SPF", None, Protocol::Mail, Status::Standard, &[Standard::RFC7208]); + (RelayVersion, RELAY_VERSION, "Relay-Version", None, Protocol::NNTP, Status::Obsoleted, &[Standard::RFC0850, Standard::RFC5536]); + (ReplyBy, REPLY_BY, "Reply-By", None, Protocol::Mail, Status::None, &[Standard::RFC4021]); + (RequireRecipientValidSince, REQUIRE_RECIPIENT_VALID_SINCE, "Require-Recipient-Valid-Since", None, Protocol::Mail, Status::Standard, &[Standard::RFC7293]); + (ResentBcc, RESENT_BCC, "Resent-Bcc", None, Protocol::Mail, Status::Standard, &[Standard::RFC5322]); + (ResentCc, RESENT_CC, "Resent-Cc", None, Protocol::Mail, Status::Standard, &[Standard::RFC5322]); + (ResentDate, RESENT_DATE, "Resent-Date", None, Protocol::Mail, Status::Standard, &[Standard::RFC5322]); + (ResentFrom, RESENT_FROM, "Resent-From", None, Protocol::Mail, Status::Standard, &[Standard::RFC5322, Standard::RFC6854]); + (ResentMessageId, RESENT_MESSAGE_ID, "Resent-Message-ID", None, Protocol::Mail, Status::Standard, &[Standard::RFC5322]); + (ResentReplyTo, RESENT_REPLY_TO, "Resent-Reply-To", None, Protocol::Mail, Status::Obsoleted, &[Standard::RFC5322]); + (ResentSender, RESENT_SENDER, "Resent-Sender", None, Protocol::Mail, Status::Standard, &[Standard::RFC5322, Standard::RFC6854]); + (ResentTo, RESENT_TO, "Resent-To", None, Protocol::Mail, Status::Standard, &[Standard::RFC5322]); + (ReturnPath, RETURN_PATH, "Return-Path", None, Protocol::Mail, Status::Standard, &[Standard::RFC5322]); + (SeeAlso, SEE_ALSO, "See-Also", None, Protocol::NNTP, Status::Obsoleted, &[Standard::RFC1849, Standard::RFC5536]); + (Sender, SENDER, "Sender", None, Protocol::Mail | Protocol::NNTP, Status::Standard, &[Standard::RFC5322, Standard::RFC6854]); + (Sensitivity, SENSITIVITY, "Sensitivity", None, Protocol::Mail, Status::None, &[Standard::RFC4021]); + (Solicitation, SOLICITATION, "Solicitation", None, Protocol::Mail, Status::None, &[Standard::RFC3865]); + (Summary, SUMMARY, "Summary", None, Protocol::NNTP, Status::Standard, &[Standard::RFC5536]); + (Supersedes, SUPERSEDES, "Supersedes", None, Protocol::Mail | Protocol::NNTP, Status::None, &[Standard::RFC5536, Standard::RFC2156]); + (TlsReportDomain, TLS_REPORT_DOMAIN, "TLS-Report-Domain", None, Protocol::Mail, Status::Standard, &[Standard::RFC8460]); + (TlsReportSubmitter, TLS_REPORT_SUBMITTER, "TLS-Report-Submitter", None, Protocol::Mail, Status::Standard, &[Standard::RFC8460]); + (TlsRequired, TLS_REQUIRED, "TLS-Required", None, Protocol::Mail, Status::Standard, &[Standard::RFC8689]); + (To, TO, "To", None, Protocol::Mail, Status::Standard, &[Standard::RFC5322]); + (UserAgent, USER_AGENT, "User-Agent", None, Protocol::NNTP, Status::Standard, &[Standard::RFC5536, Standard::RFC2616]); + (VbrInfo, VBR_INFO, "VBR-Info", None, Protocol::Mail, Status::Standard, &[Standard::RFC5518]); + (X400ContentIdentifier, X400_CONTENT_IDENTIFIER, "X400-Content-Identifier", None, Protocol::Mail, Status::None, &[Standard::RFC4021]); + (X400ContentReturn, X400_CONTENT_RETURN, "X400-Content-Return", None, Protocol::Mail, Status::None, &[Standard::RFC4021]); + (X400ContentType, X400_CONTENT_TYPE, "X400-Content-Type", None, Protocol::Mail, Status::None, &[Standard::RFC4021]); + (X400MtsIdentifier, X400_MTS_IDENTIFIER, "X400-MTS-Identifier", None, Protocol::Mail, Status::None, &[Standard::RFC4021]); + (X400Originator, X400_ORIGINATOR, "X400-Originator", None, Protocol::Mail, Status::None, &[Standard::RFC4021]); + (X400Received, X400_RECEIVED, "X400-Received", None, Protocol::Mail, Status::None, &[Standard::RFC4021]); + (X400Recipients, X400_RECIPIENTS, "X400-Recipients", None, Protocol::Mail, Status::None, &[Standard::RFC4021]); + (X400Trace, X400_TRACE, "X400-Trace", None, Protocol::Mail, Status::None, &[Standard::RFC4021]); + (Xref, XREF, "Xref", None, Protocol::NNTP, Status::Standard, &[Standard::RFC5536]); + (ApparentlyTo, APPARENTLY_TO, "Apparently-To", Some("prov/apparently-to"), Protocol::Mail, Status::None, &[Standard::RFC2076]); + (Author, AUTHOR, "Author", None, Protocol::Mail, Status::None, &[Standard::RFC9057]); + (EdiintFeatures, EDIINT_FEATURES, "EDIINT-Features", None, Protocol::Mail, Status::None, &[Standard::RFC6017]); + (EesstVersion, EESST_VERSION, "Eesst-Version", None, Protocol::Mail, Status::None, &[Standard::RFC7681]); + (ErrorsTo, ERRORS_TO, "Errors-To", Some("prov/errors-to"), Protocol::Mail, Status::None, &[Standard::RFC2076]); + (JabberId, JABBER_ID, "Jabber-ID", Some("prov/jabber-id"), Protocol::Mail | Protocol::NNTP, Status::None, &[Standard::RFC7259]); + (SioLabel, SIO_LABEL, "SIO-Label", None, Protocol::Mail, Status::None, &[Standard::RFC7444]); + (SioLabelHistory, SIO_LABEL_HISTORY, "SIO-Label-History", None, Protocol::Mail, Status::None, &[Standard::RFC7444]); + (XArchivedAt, X_ARCHIVED_AT, "X-Archived-At", Some("prov/x-archived-at"), Protocol::Mail | Protocol::NNTP, Status::Deprecated, &[Standard::RFC5064]); + (XMittente, X_MITTENTE, "X-Mittente", None, Protocol::Mail, Status::None, &[Standard::RFC6109]); + (XRicevuta, X_RICEVUTA, "X-Ricevuta", None, Protocol::Mail, Status::None, &[Standard::RFC6109]); + (XRiferimentoMessageId, X_RIFERIMENTO_MESSAGE_ID, "X-Riferimento-Message-ID", None, Protocol::Mail, Status::None, &[Standard::RFC6109]); + (XTiporicevuta, X_TIPORICEVUTA, "X-TipoRicevuta", None, Protocol::Mail, Status::None, &[Standard::RFC6109]); + (XTrasporto, X_TRASPORTO, "X-Trasporto", None, Protocol::Mail, Status::None, &[Standard::RFC6109]); + (XVerificasicurezza, X_VERIFICASICUREZZA, "X-VerificaSicurezza", None, Protocol::Mail, Status::None, &[Standard::RFC6109]); +} + +/// Valid header name ASCII bytes +/// +/// Source: [RFC5322 3.6.8.](https://datatracker.ietf.org/doc/html/rfc5322#autoid-35) +/// ```text +/// field-name = 1*ftext +/// +/// ftext = %d33-57 / ; Printable US-ASCII +/// %d59-126 ; characters not including +/// ; ":". +/// ``` +const HEADER_CHARS: [u8; 128] = [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // x + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 1x + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 2x + 0, 0, 0, b'!', b'"', b'#', b'$', b'%', b'&', b'\'', // 3x + 0, 0, b'*', b'+', 0, b'-', b'.', 0, b'0', b'1', // 4x + b'2', b'3', b'4', b'5', b'6', b'7', b'8', b'9', 0, 0, // 5x + 0, 0, 0, 0, 0, b'a', b'b', b'c', b'd', b'e', // 6x + b'f', b'g', b'h', b'i', b'j', b'k', b'l', b'm', b'n', b'o', // 7x + b'p', b'q', b'r', b's', b't', b'u', b'v', b'w', b'x', b'y', // 8x + b'z', 0, 0, 0, b'^', b'_', b'`', b'a', b'b', b'c', // 9x + b'd', b'e', b'f', b'g', b'h', b'i', b'j', b'k', b'l', b'm', // 10x + b'n', b'o', b'p', b'q', b'r', b's', b't', b'u', b'v', b'w', // 11x + b'x', b'y', b'z', 0, b'|', 0, b'~', 0, // 128 +]; + +impl HeaderName { + /// Returns a `str` representation of the header. + /// + /// The returned string will always be lower case. + #[inline] + pub fn as_str(&self) -> &str { + match self.inner { + Repr::Standard(v) => v.as_str(), + Repr::Custom(ref v) => unsafe { std::str::from_utf8_unchecked(&*v.0) }, + } + } + + /// Returns a `&[u8]` representation of the header. + /// + /// The returned slice will always be lower case. + #[inline] + pub fn as_bytes(&self) -> &[u8] { + match self.inner { + Repr::Standard(v) => v.as_str().as_bytes(), + Repr::Custom(ref v) => v.0.as_ref(), + } + } + + pub fn from_bytes(src: &[u8]) -> Result { + if let Some(std) = StandardHeader::from_bytes(src.trim()) { + Ok(Self { + inner: Repr::Standard(std), + }) + } else { + let mut buf = SmallVec::<[u8; 32]>::new(); + for b in src { + if let Some(b) = HEADER_CHARS.get(*b as usize).filter(|b| **b != 0) { + buf.push(*b); + } else { + return Err(InvalidHeaderName::new()); + } + } + + Ok(Self { + inner: Repr::Custom(Custom(buf)), + }) + } + } +} + +impl FromStr for HeaderName { + type Err = InvalidHeaderName; + + fn from_str(s: &str) -> Result { + HeaderName::from_bytes(s.as_bytes()).map_err(|_| InvalidHeaderName::new()) + } +} + +impl AsRef for HeaderName { + fn as_ref(&self) -> &str { + self.as_str() + } +} + +impl AsRef<[u8]> for HeaderName { + fn as_ref(&self) -> &[u8] { + self.as_str().as_bytes() + } +} + +impl Borrow for HeaderName { + fn borrow(&self) -> &str { + self.as_str() + } +} + +impl std::fmt::Display for HeaderName { + fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(fmt, "{}", self.as_str()) + } +} + +impl<'de> Deserialize<'de> for HeaderName { + fn deserialize(deserializer: D) -> std::result::Result + where + D: Deserializer<'de>, + { + #[derive(Serialize, Deserialize)] + #[serde(untagged)] + enum Helper { + S(String), + B(Vec), + } + if let Ok(s) = ::deserialize(deserializer) { + Self::from_bytes(match &s { + Helper::S(v) => v.as_bytes(), + Helper::B(v) => v.as_slice(), + }) + .map_err(|_| de::Error::custom("invalid header name value")) + } else { + Err(de::Error::custom("invalid header name value")) + } + } +} + +impl Serialize for HeaderName { + fn serialize(&self, serializer: S) -> std::result::Result + where + S: Serializer, + { + serializer.serialize_str(self.as_str()) + } +} + +impl InvalidHeaderName { + const fn new() -> InvalidHeaderName { + InvalidHeaderName + } +} + +impl<'a> From<&'a HeaderName> for HeaderName { + fn from(src: &'a HeaderName) -> Self { + src.clone() + } +} + +impl From<&HeaderName> for Cow<'static, str> { + fn from(src: &HeaderName) -> Self { + match src.inner { + Repr::Standard(s) => Cow::Borrowed(s.as_str()), + Repr::Custom(_) => Cow::Owned(src.to_string()), + } + } +} + +impl<'a> TryFrom<&'a str> for HeaderName { + type Error = InvalidHeaderName; + #[inline] + fn try_from(s: &'a str) -> Result { + Self::from_bytes(s.as_bytes()).map_err(|_| InvalidHeaderName::new()) + } +} + +impl<'a> TryFrom<&'a String> for HeaderName { + type Error = InvalidHeaderName; + #[inline] + fn try_from(s: &'a String) -> Result { + Self::from_bytes(s.as_bytes()).map_err(|_| InvalidHeaderName::new()) + } +} + +impl<'a> TryFrom<&'a [u8]> for HeaderName { + type Error = InvalidHeaderName; + #[inline] + fn try_from(s: &'a [u8]) -> Result { + Self::from_bytes(s).map_err(|_| InvalidHeaderName::new()) + } +} + +impl TryFrom for HeaderName { + type Error = InvalidHeaderName; + + #[inline] + fn try_from(s: String) -> Result { + Self::from_bytes(s.as_bytes()).map_err(|_| InvalidHeaderName::new()) + } +} + +impl TryFrom> for HeaderName { + type Error = InvalidHeaderName; + + #[inline] + fn try_from(vec: Vec) -> Result { + Self::from_bytes(&vec).map_err(|_| InvalidHeaderName::new()) + } +} + +#[doc(hidden)] +impl From for HeaderName { + fn from(src: StandardHeader) -> HeaderName { + HeaderName { + inner: Repr::Standard(src), + } + } +} + +#[doc(hidden)] +impl From for HeaderName { + fn from(src: Custom) -> HeaderName { + HeaderName { + inner: Repr::Custom(src), + } + } +} + +impl<'a> PartialEq<&'a HeaderName> for HeaderName { + #[inline] + fn eq(&self, other: &&'a HeaderName) -> bool { + *self == **other + } +} + +impl<'a> PartialEq for &'a HeaderName { + #[inline] + fn eq(&self, other: &HeaderName) -> bool { + *other == *self + } +} + +impl PartialEq for HeaderName { + /// Performs a case-insensitive comparison of the string against the header + /// name + /// + /// # Examples + /// + /// ``` + /// use melib::email::headers::HeaderName; + /// + /// assert_eq!(HeaderName::CONTENT_LENGTH, "content-length"); + /// assert_eq!(HeaderName::CONTENT_LENGTH, "Content-Length"); + /// assert_ne!(HeaderName::CONTENT_LENGTH, "content length"); + /// ``` + #[inline] + fn eq(&self, other: &str) -> bool { + self.as_str().eq_ignore_ascii_case(other) + } +} + +impl PartialEq for str { + /// Performs a case-insensitive comparison of the string against the header + /// name + /// + /// # Examples + /// + /// ``` + /// use std::convert::TryFrom; + /// + /// use melib::email::headers::HeaderName; + /// + /// assert_eq!(HeaderName::CONTENT_LENGTH, "content-length"); + /// assert_eq!(HeaderName::CONTENT_LENGTH, "Content-Length"); + /// assert_ne!(HeaderName::CONTENT_LENGTH, "content length"); + /// assert_eq!( + /// HeaderName::CONTENT_LENGTH, + /// HeaderName::try_from("content-length").unwrap() + /// ); + /// ``` + #[inline] + fn eq(&self, other: &HeaderName) -> bool { + *other == *self + } +} + +impl<'a> PartialEq<&'a str> for HeaderName { + /// Performs a case-insensitive comparison of the string against the header + /// name + #[inline] + fn eq(&self, other: &&'a str) -> bool { + *self == **other + } +} + +impl<'a> PartialEq for &'a str { + /// Performs a case-insensitive comparison of the string against the header + /// name + #[inline] + fn eq(&self, other: &HeaderName) -> bool { + *other == *self + } +} + +impl Hash for Custom { + #[inline] + fn hash(&self, hasher: &mut H) { + for b in self.0.as_slice() { + hasher.write_u8(b.to_ascii_lowercase()) + } + } +} diff --git a/melib/src/email/mailto.rs b/melib/src/email/mailto.rs index 998b4a262..bf62a2fb4 100644 --- a/melib/src/email/mailto.rs +++ b/melib/src/email/mailto.rs @@ -43,11 +43,11 @@ impl From for Draft { bcc, body, } = val; - ret.set_header("Subject", subject.unwrap_or_default()); - ret.set_header("Cc", cc.unwrap_or_default()); - ret.set_header("Bcc", bcc.unwrap_or_default()); + ret.set_header(HeaderName::SUBJECT, subject.unwrap_or_default()); + ret.set_header(HeaderName::CC, cc.unwrap_or_default()); + ret.set_header(HeaderName::BCC, bcc.unwrap_or_default()); + ret.set_header(HeaderName::TO, address.to_string()); ret.set_body(body.unwrap_or_default()); - ret.set_header("To", address.to_string()); ret } } diff --git a/melib/src/error.rs b/melib/src/error.rs index 3251be0d6..357ed7267 100644 --- a/melib/src/error.rs +++ b/melib/src/error.rs @@ -306,6 +306,7 @@ impl From for NetworkErrorKind { } #[derive(Debug, Copy, PartialEq, Eq, Clone)] +#[non_exhaustive] pub enum ErrorKind { None, External, @@ -317,6 +318,7 @@ pub enum ErrorKind { OSError, NotImplemented, NotSupported, + ValueError, } impl fmt::Display for ErrorKind { @@ -325,16 +327,17 @@ impl fmt::Display for ErrorKind { fmt, "{}", match self { - ErrorKind::None => "None", - ErrorKind::External => "External", - ErrorKind::Authentication => "Authentication", - ErrorKind::Bug => "Bug, please report this!", - ErrorKind::Network(ref inner) => inner.as_str(), - ErrorKind::Timeout => "Timeout", - ErrorKind::OSError => "OS Error", - ErrorKind::Configuration => "Configuration", - ErrorKind::NotImplemented => "Not implemented", - ErrorKind::NotSupported => "Not supported", + Self::None => "None", + Self::External => "External", + Self::Authentication => "Authentication", + Self::Bug => "Bug, please report this!", + Self::Network(ref inner) => inner.as_str(), + Self::Timeout => "Timeout", + Self::OSError => "OS Error", + Self::Configuration => "Configuration", + Self::NotImplemented => "Not implemented", + Self::NotSupported => "Not supported", + Self::ValueError => "Invalid value", } ) } @@ -342,15 +345,15 @@ impl fmt::Display for ErrorKind { impl ErrorKind { pub fn is_network(&self) -> bool { - matches!(self, ErrorKind::Network(_)) + matches!(self, Self::Network(_)) } pub fn is_timeout(&self) -> bool { - matches!(self, ErrorKind::Timeout) + matches!(self, Self::Timeout) } pub fn is_authentication(&self) -> bool { - matches!(self, ErrorKind::Authentication) + matches!(self, Self::Authentication) } } @@ -691,6 +694,15 @@ impl From> for Error { } } +impl From for Error { + #[inline] + fn from(kind: crate::email::InvalidHeaderName) -> Error { + Error::new(kind.to_string()) + .set_source(Some(Arc::new(kind))) + .set_kind(ErrorKind::Network(NetworkErrorKind::InvalidTLSConnection)) + } +} + impl<'a> From<&'a mut Error> for Error { #[inline] fn from(kind: &'a mut Error) -> Error { diff --git a/src/components/mail/compose.rs b/src/components/mail/compose.rs index 3ceebbd9a..42b5d4ff9 100644 --- a/src/components/mail/compose.rs +++ b/src/components/mail/compose.rs @@ -141,7 +141,7 @@ impl fmt::Display for Composer { write!( f, "reply: {}", - (&self.draft.headers()["Subject"]).trim_at_boundary(8) + (&self.draft.headers()[HeaderName::SUBJECT]).trim_at_boundary(8) ) } else { write!(f, "composing") @@ -204,11 +204,11 @@ impl Composer { if v.is_empty() { continue; } - ret.draft.set_header(h, v.into()); + ret.draft.set_header(h.into(), v.into()); } if *account_settings!(context[account_hash].composing.insert_user_agent) { ret.draft.set_header( - "User-Agent", + HeaderName::USER_AGENT, format!("meli {}", option_env!("CARGO_PKG_VERSION").unwrap_or("0.0")), ); } @@ -296,9 +296,9 @@ impl Composer { subject.to_string() } }; - ret.draft.set_header("Subject", subject); + ret.draft.set_header(HeaderName::SUBJECT, subject); ret.draft.set_header( - "References", + HeaderName::REFERENCES, format!( "{} {}", envelope @@ -314,17 +314,19 @@ impl Composer { envelope.message_id_display() ), ); - ret.draft - .set_header("In-Reply-To", envelope.message_id_display().into()); + ret.draft.set_header( + HeaderName::IN_REPLY_TO, + envelope.message_id_display().into(), + ); - if let Some(reply_to) = envelope.other_headers().get("To") { + if let Some(reply_to) = envelope.other_headers().get(HeaderName::TO) { let to: &str = reply_to; let extra_identities = &account.settings.account.extra_identities; if let Some(extra) = extra_identities .iter() .find(|extra| to.contains(extra.as_str())) { - ret.draft.set_header("From", extra.into()); + ret.draft.set_header(HeaderName::FROM, extra.into()); } } @@ -348,13 +350,13 @@ impl Composer { } if let Some(reply_to) = envelope .other_headers() - .get("Mail-Followup-To") + .get(HeaderName::MAIL_FOLLOWUP_TO) .and_then(|v| v.try_into().ok()) { to.insert(reply_to); } else if let Some(reply_to) = envelope .other_headers() - .get("Reply-To") + .get(HeaderName::REPLY_TO) .and_then(|v| v.try_into().ok()) { to.insert(reply_to); @@ -371,7 +373,7 @@ impl Composer { ) { to.remove(&ours); } - ret.draft.set_header("To", { + ret.draft.set_header(HeaderName::TO, { let mut ret: String = to.into_iter() .fold(String::new(), |mut s: String, n: Address| { @@ -383,13 +385,15 @@ impl Composer { ret.pop(); ret }); - 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.set_header("To", reply_to.to_string()); - } else if let Some(reply_to) = envelope.other_headers().get("Reply-To") { - ret.draft.set_header("To", reply_to.to_string()); + ret.draft + .set_header(HeaderName::CC, envelope.field_cc_to_string()); + } else if let Some(reply_to) = envelope.other_headers().get(HeaderName::MAIL_REPLY_TO) { + ret.draft.set_header(HeaderName::TO, reply_to.to_string()); + } else if let Some(reply_to) = envelope.other_headers().get(HeaderName::REPLY_TO) { + ret.draft.set_header(HeaderName::TO, reply_to.to_string()); } else { - ret.draft.set_header("To", envelope.field_from_to_string()); + ret.draft + .set_header(HeaderName::TO, envelope.field_from_to_string()); } ret.draft.body = { let mut ret = attribution_string( @@ -495,7 +499,7 @@ impl Composer { ) -> Self { let mut composer = Composer::with_account(coordinates.0, context); let mut draft: Draft = Draft::default(); - draft.set_header("Subject", format!("Fwd: {}", env.subject())); + draft.set_header(HeaderName::SUBJECT, format!("Fwd: {}", env.subject())); let preamble = format!( r#" ---------- Forwarded message --------- @@ -557,8 +561,15 @@ To: {} self.form.set_cursor(old_cursor); let headers = self.draft.headers(); let account_hash = self.account_hash; - for &k in &["Date", "From", "To", "Cc", "Bcc", "Subject"] { - if k == "To" || k == "Cc" || k == "Bcc" { + for k in &[ + HeaderName::DATE, + HeaderName::FROM, + HeaderName::TO, + HeaderName::CC, + HeaderName::BCC, + HeaderName::SUBJECT, + ] { + if k == HeaderName::TO || k == HeaderName::CC || k == HeaderName::BCC { self.form.push_cl(( k.into(), headers[k].to_string(), @@ -571,7 +582,7 @@ To: {} .collect::>() }), )); - } else if k == "From" { + } else if k == HeaderName::FROM { self.form.push_cl(( k.into(), headers[k].to_string(), @@ -812,10 +823,11 @@ impl Component for Composer { context[self.account_hash].pgp.auto_sign )); } - if !self.draft.headers().contains_key("From") || self.draft.headers()["From"].is_empty() + if !self.draft.headers().contains_key(HeaderName::FROM) + || self.draft.headers()[HeaderName::FROM].is_empty() { self.draft.set_header( - "From", + HeaderName::FROM, context.accounts[&self.account_hash] .settings .account() @@ -1230,7 +1242,8 @@ impl Component for Composer { UIEvent::FinishedUIDialog(id, ref mut result), ) if selector.id() == *id => { if let Some(to_val) = result.downcast_mut::() { - self.draft.set_header("To", std::mem::take(to_val)); + self.draft + .set_header(HeaderName::TO, std::mem::take(to_val)); self.update_form(); } self.mode = ViewMode::Edit; @@ -1381,28 +1394,6 @@ impl Component for Composer { UIEvent::Resize => { self.set_dirty(true); } - /* - /* Switch e-mail From: field to the `left` configured account. */ - UIEvent::Input(Key::Left) if self.cursor == Cursor::From => { - self.draft.headers_mut().insert( - "From".into(), - get_display_name(context, self.account_hash), - ); - self.dirty = true; - return true; - } - /* Switch e-mail From: field to the `right` configured account. */ - UIEvent::Input(Key::Right) if self.cursor == Cursor::From => { - if self.account_cursor + 1 < context.accounts.len() { - self.account_cursor += 1; - self.draft.headers_mut().insert( - "From".into(), - get_display_name(context, self.account_cursor), - ); - self.dirty = true; - } - return true; - }*/ UIEvent::Input(ref key) if self.mode.is_edit() && shortcut!(key == shortcuts[Shortcuts::COMPOSING]["scroll_up"]) => @@ -2494,9 +2485,12 @@ hello world. &mut context, false, ); - assert_eq!(&composer.draft.headers()["Subject"], "RE: your e-mail"); assert_eq!( - &composer.draft.headers()["To"], + &composer.draft.headers()[HeaderName::SUBJECT], + "RE: your e-mail" + ); + assert_eq!( + &composer.draft.headers()[HeaderName::TO], r#"some name "# ); let raw_mail = r#"From: "some name" @@ -2520,9 +2514,12 @@ hello world. &mut context, false, ); - assert_eq!(&composer.draft.headers()["Subject"], "Re: your e-mail"); assert_eq!( - &composer.draft.headers()["To"], + &composer.draft.headers()[HeaderName::SUBJECT], + "Re: your e-mail" + ); + assert_eq!( + &composer.draft.headers()[HeaderName::TO], r#"some name "# ); } diff --git a/src/components/mail/compose/hooks.rs b/src/components/mail/compose/hooks.rs index a917237d4..f766aa90c 100644 --- a/src/components/mail/compose/hooks.rs +++ b/src/components/mail/compose/hooks.rs @@ -275,8 +275,8 @@ mod tests { let mut draft = Draft::default(); draft .set_body("αδφαφσαφασ".to_string()) - .set_header("Subject", "test_update()".into()) - .set_header("Date", "Sun, 16 Jun 2013 17:56:45 +0200".into()); + .set_header(HeaderName::SUBJECT, "test_update()".into()) + .set_header(HeaderName::DATE, "Sun, 16 Jun 2013 17:56:45 +0200".into()); println!("Check that past Date header value produces a warning…"); #[allow(const_item_mutation)] let err_msg = PASTDATEWARN(&mut ctx, &mut draft).unwrap_err().to_string(); @@ -299,8 +299,8 @@ mod tests { let mut draft = Draft::default(); draft .set_body("αδφαφσαφασ".to_string()) - .set_header("Subject", "test_update()".into()) - .set_header("Date", "Sun sds16 Jun 2013 17:56:45 +0200".into()); + .set_header(HeaderName::SUBJECT, "test_update()".into()) + .set_header(HeaderName::DATE, "Sun sds16 Jun 2013 17:56:45 +0200".into()); let mut hook = HEADERWARN; println!("Check for missing/empty From header value…"); @@ -312,7 +312,7 @@ mod tests { "HEADERWARN should complain about From value being empty: {}", err_msg ); - draft.set_header("From", "user ".into()); + draft.set_header(HeaderName::FROM, "user ".into()); println!("Check for missing/empty To header value…"); let err_msg = hook(&mut ctx, &mut draft).unwrap_err().to_string(); @@ -323,7 +323,7 @@ mod tests { "HEADERWARN should complain about To value being empty: {}", err_msg ); - draft.set_header("To", "other user ".into()); + draft.set_header(HeaderName::TO, "other user ".into()); println!("Check for invalid Date header value…"); let err_msg = hook(&mut ctx, &mut draft).unwrap_err().to_string(); @@ -337,11 +337,11 @@ mod tests { draft = Draft::default(); draft .set_body("αδφαφσαφασ".to_string()) - .set_header("From", "user ".into()) - .set_header("To", "other user ".into()) - .set_header("Subject", "test_update()".into()); + .set_header(HeaderName::FROM, "user ".into()) + .set_header(HeaderName::TO, "other user ".into()) + .set_header(HeaderName::SUBJECT, "test_update()".into()); hook(&mut ctx, &mut draft).unwrap(); - draft.set_header("From", "user user@example.com>".into()); + draft.set_header(HeaderName::FROM, "user user@example.com>".into()); println!("Check for invalid From header value…"); let err_msg = hook(&mut ctx, &mut draft).unwrap_err().to_string(); @@ -361,8 +361,8 @@ mod tests { let mut draft = Draft::default(); draft .set_body("αδφαφσαφασ".to_string()) - .set_header("Subject", "Attachments included".into()) - .set_header("Date", "Sun, 16 Jun 2013 17:56:45 +0200".into()); + .set_header(HeaderName::SUBJECT, "Attachments included".into()) + .set_header(HeaderName::DATE, "Sun, 16 Jun 2013 17:56:45 +0200".into()); let mut hook = MISSINGATTACHMENTWARN; @@ -378,7 +378,7 @@ mod tests { ); draft - .set_header("Subject", "Hello.".into()) + .set_header(HeaderName::SUBJECT, "Hello.".into()) .set_body("Attachments included".to_string()); println!( "Check that mentioning attachments in body produces a warning if draft has no \ @@ -394,7 +394,7 @@ mod tests { println!( "Check that mentioning attachments produces no warnings if draft has attachments…" ); - draft.set_header("Subject", "Attachments included".into()); + draft.set_header(HeaderName::SUBJECT, "Attachments included".into()); let mut attachment = AttachmentBuilder::new(b""); attachment .set_raw(b"foobar".to_vec()) @@ -414,7 +414,7 @@ mod tests { let tempdir = tempfile::tempdir().unwrap(); let mut ctx = Context::new_mock(&tempdir); let mut draft = Draft::default(); - draft.set_header("Date", "Sun, 16 Jun 2013 17:56:45 +0200".into()); + draft.set_header(HeaderName::DATE, "Sun, 16 Jun 2013 17:56:45 +0200".into()); let mut hook = EMPTYDRAFTWARN; @@ -427,7 +427,7 @@ mod tests { ); println!("Check that non-empty draft produces no warning…"); - draft.set_header("Subject", "Ping".into()); + draft.set_header(HeaderName::SUBJECT, "Ping".into()); hook(&mut ctx, &mut draft).unwrap(); } } diff --git a/src/components/mail/view.rs b/src/components/mail/view.rs index 1ddcdb67f..77b022b0f 100644 --- a/src/components/mail/view.rs +++ b/src/components/mail/view.rs @@ -2663,7 +2663,7 @@ impl Component for MailView { if let Ok(mailto) = Mailto::try_from(*email) { let mut draft: Draft = mailto.into(); draft.set_header( - "From", + HeaderName::FROM, context.accounts[&self.coordinates.0] .settings .account() diff --git a/src/conf/composing.rs b/src/conf/composing.rs index a00386bd4..34a9c4880 100644 --- a/src/conf/composing.rs +++ b/src/conf/composing.rs @@ -22,7 +22,7 @@ //! Configuration for composing email. use std::collections::HashMap; -use melib::ToggleFlag; +use melib::{email::HeaderName, ToggleFlag}; use super::{ default_vals::{ask, false_val, none, true_val}, @@ -60,7 +60,7 @@ pub struct ComposingSettings { /// Set default header values for new drafts /// Default: empty #[serde(default, alias = "default-header-values")] - pub default_header_values: HashMap, + pub default_header_values: HashMap, /// Wrap header preample when editing a draft in an editor. This allows you /// to write non-plain text email without the preamble creating syntax /// errors. They are stripped when you return from the editor. The diff --git a/src/conf/overrides.rs b/src/conf/overrides.rs index 4e755c7a2..d8d32d6a2 100644 --- a/src/conf/overrides.rs +++ b/src/conf/overrides.rs @@ -23,7 +23,9 @@ #![allow(clippy::derivable_impls)] //! This module is automatically generated by config_macros.rs. + use super::*; +use melib::HeaderName; # [derive (Debug , Serialize , Deserialize , Clone)] # [serde (deny_unknown_fields)] pub struct PagerSettingsOverride { # [doc = " Number of context lines when going to next page."] # [doc = " Default: 0"] # [serde (alias = "pager-context")] # [serde (default)] pub pager_context : Option < usize > , # [doc = " Stop at the end instead of displaying next mail."] # [doc = " Default: false"] # [serde (alias = "pager-stop")] # [serde (default)] pub pager_stop : Option < bool > , # [doc = " Always show headers when scrolling."] # [doc = " Default: true"] # [serde (alias = "headers-sticky")] # [serde (default)] pub headers_sticky : Option < bool > , # [doc = " The height of the pager in mail view, in percent."] # [doc = " Default: 80"] # [serde (alias = "pager-ratio")] # [serde (default)] pub pager_ratio : Option < usize > , # [doc = " A command to pipe mail output through for viewing in pager."] # [doc = " Default: None"] # [serde (deserialize_with = "non_empty_opt_string")] # [serde (default)] pub filter : Option < Option < String > > , # [doc = " A command to pipe html output before displaying it in a pager"] # [doc = " Default: None"] # [serde (deserialize_with = "non_empty_opt_string" , alias = "html-filter")] # [serde (default)] pub html_filter : Option < Option < String > > , # [doc = " Respect \"format=flowed\""] # [doc = " Default: true"] # [serde (alias = "format-flowed")] # [serde (default)] pub format_flowed : Option < bool > , # [doc = " Split long lines that would overflow on the x axis."] # [doc = " Default: true"] # [serde (alias = "split-long-lines")] # [serde (default)] pub split_long_lines : Option < bool > , # [doc = " Minimum text width in columns."] # [doc = " Default: 80"] # [serde (alias = "minimum-width")] # [serde (default)] pub minimum_width : Option < usize > , # [doc = " Choose `text/html` alternative if `text/plain` is empty in"] # [doc = " `multipart/alternative` attachments."] # [doc = " Default: true"] # [serde (alias = "auto-choose-multipart-alternative")] # [serde (default)] pub auto_choose_multipart_alternative : Option < ToggleFlag > , # [doc = " Show Date: in my timezone"] # [doc = " Default: true"] # [serde (alias = "show-date-in-my-timezone")] # [serde (default)] pub show_date_in_my_timezone : Option < ToggleFlag > , # [doc = " A command to launch URLs with. The URL will be given as the first"] # [doc = " argument of the command. Default: None"] # [serde (deserialize_with = "non_empty_opt_string")] # [serde (default)] pub url_launcher : Option < Option < String > > , # [doc = " A command to open html files."] # [doc = " Default: None"] # [serde (deserialize_with = "non_empty_opt_string" , alias = "html-open")] # [serde (default)] pub html_open : Option < Option < String > > } impl Default for PagerSettingsOverride { fn default () -> Self { PagerSettingsOverride { pager_context : None , pager_stop : None , headers_sticky : None , pager_ratio : None , filter : None , html_filter : None , format_flowed : None , split_long_lines : None , minimum_width : None , auto_choose_multipart_alternative : None , show_date_in_my_timezone : None , url_launcher : None , html_open : None } } } @@ -33,7 +35,7 @@ use super::*; # [derive (Debug , Serialize , Deserialize , Clone)] # [serde (deny_unknown_fields)] pub struct ShortcutsOverride { # [serde (default)] pub general : Option < GeneralShortcuts > , # [serde (default)] pub listing : Option < ListingShortcuts > , # [serde (default)] pub composing : Option < ComposingShortcuts > , # [serde (alias = "contact-list")] # [serde (default)] pub contact_list : Option < ContactListShortcuts > , # [serde (alias = "envelope-view")] # [serde (default)] pub envelope_view : Option < EnvelopeViewShortcuts > , # [serde (alias = "thread-view")] # [serde (default)] pub thread_view : Option < ThreadViewShortcuts > , # [serde (default)] pub pager : Option < PagerShortcuts > } impl Default for ShortcutsOverride { fn default () -> Self { ShortcutsOverride { general : None , listing : None , composing : None , contact_list : None , envelope_view : None , thread_view : None , pager : None } } } -# [derive (Debug , Serialize , Deserialize , Clone)] # [serde (deny_unknown_fields)] pub struct ComposingSettingsOverride { # [doc = " A command to pipe new emails to"] # [doc = " Required"] # [serde (default)] pub send_mail : Option < SendMail > , # [doc = " Command to launch editor. Can have arguments. Draft filename is given as"] # [doc = " the last argument. If it's missing, the environment variable $EDITOR is"] # [doc = " looked up."] # [serde (alias = "editor-command" , alias = "editor-cmd" , alias = "editor_cmd")] # [serde (default)] pub editor_command : Option < Option < String > > , # [doc = " Embed editor (for terminal interfaces) instead of forking and waiting."] # [serde (default)] pub embed : Option < bool > , # [doc = " Set \"format=flowed\" in plain text attachments."] # [doc = " Default: true"] # [serde (alias = "format-flowed")] # [serde (default)] pub format_flowed : Option < bool > , # [doc = "Set User-Agent"] # [doc = "Default: empty"] # [serde (alias = "insert_user_agent")] # [serde (default)] pub insert_user_agent : Option < bool > , # [doc = " Set default header values for new drafts"] # [doc = " Default: empty"] # [serde (alias = "default-header-values")] # [serde (default)] pub default_header_values : Option < HashMap < String , String > > , # [doc = " Wrap header preample when editing a draft in an editor. This allows you"] # [doc = " to write non-plain text email without the preamble creating syntax"] # [doc = " errors. They are stripped when you return from the editor. The"] # [doc = " values should be a two element array of strings, a prefix and suffix."] # [doc = " Default: None"] # [serde (alias = "wrap-header-preample")] # [serde (default)] pub wrap_header_preamble : Option < Option < (String , String) > > , # [doc = " Store sent mail after successful submission. This setting is meant to be"] # [doc = " disabled for non-standard behaviour in gmail, which auto-saves sent"] # [doc = " mail on its own. Default: true"] # [serde (default)] pub store_sent_mail : Option < bool > , # [doc = " The attribution line appears above the quoted reply text."] # [doc = " The format specifiers for the replied address are:"] # [doc = " - `%+f` — the sender's name and email address."] # [doc = " - `%+n` — the sender's name (or email address, if no name is included)."] # [doc = " - `%+a` — the sender's email address."] # [doc = " The format string is passed to strftime(3) with the replied envelope's"] # [doc = " date. Default: \"On %a, %0e %b %Y %H:%M, %+f wrote:%n\""] # [serde (default)] pub attribution_format_string : Option < Option < String > > , # [doc = " Whether the strftime call for the attribution string uses the POSIX"] # [doc = " locale instead of the user's active locale"] # [doc = " Default: true"] # [serde (default)] pub attribution_use_posix_locale : Option < bool > , # [doc = " Forward emails as attachment? (Alternative is inline)"] # [doc = " Default: ask"] # [serde (alias = "forward-as-attachment")] # [serde (default)] pub forward_as_attachment : Option < ToggleFlag > , # [doc = " Alternative lists of reply prefixes (etc. [\"Re:\", \"RE:\", ...]) to strip"] # [doc = " Default: `[\"Re:\", \"RE:\", \"Fwd:\", \"Fw:\", \"回复:\", \"回覆:\", \"SV:\", \"Sv:\","] # [doc = " \"VS:\", \"Antw:\", \"Doorst:\", \"VS:\", \"VL:\", \"REF:\", \"TR:\", \"TR:\", \"AW:\","] # [doc = " \"WG:\", \"ΑΠ:\", \"Απ:\", \"απ:\", \"ΠΡΘ:\", \"Πρθ:\", \"πρθ:\", \"ΣΧΕΤ:\", \"Σχετ:\","] # [doc = " \"σχετ:\", \"ΠΡΘ:\", \"Πρθ:\", \"πρθ:\", \"Vá:\", \"Továbbítás:\", \"R:\", \"I:\","] # [doc = " \"RIF:\", \"FS:\", \"BLS:\", \"TRS:\", \"VS:\", \"VB:\", \"RV:\", \"RES:\", \"Res\","] # [doc = " \"ENC:\", \"Odp:\", \"PD:\", \"YNT:\", \"İLT:\", \"ATB:\", \"YML:\"]`"] # [serde (alias = "reply-prefix-list-to-strip")] # [serde (default)] pub reply_prefix_list_to_strip : Option < Option < Vec < String > > > , # [doc = " The prefix to use in reply subjects. The de facto prefix is \"Re:\"."] # [serde (alias = "reply-prefix")] # [serde (default)] pub reply_prefix : Option < String > , # [doc = " Custom `compose-hooks`."] # [serde (alias = "custom-compose-hooks")] # [serde (default)] pub custom_compose_hooks : Option < Vec < ComposeHook > > , # [doc = " Disabled `compose-hooks`."] # [serde (alias = "disabled-compose-hooks")] # [serde (default)] pub disabled_compose_hooks : Option < Vec < String > > } impl Default for ComposingSettingsOverride { fn default () -> Self { ComposingSettingsOverride { send_mail : None , editor_command : None , embed : None , format_flowed : None , insert_user_agent : None , default_header_values : None , wrap_header_preamble : None , store_sent_mail : None , attribution_format_string : None , attribution_use_posix_locale : None , forward_as_attachment : None , reply_prefix_list_to_strip : None , reply_prefix : None , custom_compose_hooks : None , disabled_compose_hooks : None } } } +# [derive (Debug , Serialize , Deserialize , Clone)] # [serde (deny_unknown_fields)] pub struct ComposingSettingsOverride { # [doc = " A command to pipe new emails to"] # [doc = " Required"] # [serde (default)] pub send_mail : Option < SendMail > , # [doc = " Command to launch editor. Can have arguments. Draft filename is given as"] # [doc = " the last argument. If it's missing, the environment variable $EDITOR is"] # [doc = " looked up."] # [serde (alias = "editor-command" , alias = "editor-cmd" , alias = "editor_cmd")] # [serde (default)] pub editor_command : Option < Option < String > > , # [doc = " Embed editor (for terminal interfaces) instead of forking and waiting."] # [serde (default)] pub embed : Option < bool > , # [doc = " Set \"format=flowed\" in plain text attachments."] # [doc = " Default: true"] # [serde (alias = "format-flowed")] # [serde (default)] pub format_flowed : Option < bool > , # [doc = "Set User-Agent"] # [doc = "Default: empty"] # [serde (alias = "insert_user_agent")] # [serde (default)] pub insert_user_agent : Option < bool > , # [doc = " Set default header values for new drafts"] # [doc = " Default: empty"] # [serde (alias = "default-header-values")] # [serde (default)] pub default_header_values : Option < HashMap < HeaderName , String > > , # [doc = " Wrap header preample when editing a draft in an editor. This allows you"] # [doc = " to write non-plain text email without the preamble creating syntax"] # [doc = " errors. They are stripped when you return from the editor. The"] # [doc = " values should be a two element array of strings, a prefix and suffix."] # [doc = " Default: None"] # [serde (alias = "wrap-header-preample")] # [serde (default)] pub wrap_header_preamble : Option < Option < (String , String) > > , # [doc = " Store sent mail after successful submission. This setting is meant to be"] # [doc = " disabled for non-standard behaviour in gmail, which auto-saves sent"] # [doc = " mail on its own. Default: true"] # [serde (default)] pub store_sent_mail : Option < bool > , # [doc = " The attribution line appears above the quoted reply text."] # [doc = " The format specifiers for the replied address are:"] # [doc = " - `%+f` — the sender's name and email address."] # [doc = " - `%+n` — the sender's name (or email address, if no name is included)."] # [doc = " - `%+a` — the sender's email address."] # [doc = " The format string is passed to strftime(3) with the replied envelope's"] # [doc = " date. Default: \"On %a, %0e %b %Y %H:%M, %+f wrote:%n\""] # [serde (default)] pub attribution_format_string : Option < Option < String > > , # [doc = " Whether the strftime call for the attribution string uses the POSIX"] # [doc = " locale instead of the user's active locale"] # [doc = " Default: true"] # [serde (default)] pub attribution_use_posix_locale : Option < bool > , # [doc = " Forward emails as attachment? (Alternative is inline)"] # [doc = " Default: ask"] # [serde (alias = "forward-as-attachment")] # [serde (default)] pub forward_as_attachment : Option < ToggleFlag > , # [doc = " Alternative lists of reply prefixes (etc. [\"Re:\", \"RE:\", ...]) to strip"] # [doc = " Default: `[\"Re:\", \"RE:\", \"Fwd:\", \"Fw:\", \"回复:\", \"回覆:\", \"SV:\", \"Sv:\","] # [doc = " \"VS:\", \"Antw:\", \"Doorst:\", \"VS:\", \"VL:\", \"REF:\", \"TR:\", \"TR:\", \"AW:\","] # [doc = " \"WG:\", \"ΑΠ:\", \"Απ:\", \"απ:\", \"ΠΡΘ:\", \"Πρθ:\", \"πρθ:\", \"ΣΧΕΤ:\", \"Σχετ:\","] # [doc = " \"σχετ:\", \"ΠΡΘ:\", \"Πρθ:\", \"πρθ:\", \"Vá:\", \"Továbbítás:\", \"R:\", \"I:\","] # [doc = " \"RIF:\", \"FS:\", \"BLS:\", \"TRS:\", \"VS:\", \"VB:\", \"RV:\", \"RES:\", \"Res\","] # [doc = " \"ENC:\", \"Odp:\", \"PD:\", \"YNT:\", \"İLT:\", \"ATB:\", \"YML:\"]`"] # [serde (alias = "reply-prefix-list-to-strip")] # [serde (default)] pub reply_prefix_list_to_strip : Option < Option < Vec < String > > > , # [doc = " The prefix to use in reply subjects. The de facto prefix is \"Re:\"."] # [serde (alias = "reply-prefix")] # [serde (default)] pub reply_prefix : Option < String > , # [doc = " Custom `compose-hooks`."] # [serde (alias = "custom-compose-hooks")] # [serde (default)] pub custom_compose_hooks : Option < Vec < ComposeHook > > , # [doc = " Disabled `compose-hooks`."] # [serde (alias = "disabled-compose-hooks")] # [serde (default)] pub disabled_compose_hooks : Option < Vec < String > > } impl Default for ComposingSettingsOverride { fn default () -> Self { ComposingSettingsOverride { send_mail : None , editor_command : None , embed : None , format_flowed : None , insert_user_agent : None , default_header_values : None , wrap_header_preamble : None , store_sent_mail : None , attribution_format_string : None , attribution_use_posix_locale : None , forward_as_attachment : None , reply_prefix_list_to_strip : None , reply_prefix : None , custom_compose_hooks : None , disabled_compose_hooks : None } } } # [derive (Debug , Serialize , Deserialize , Clone)] # [serde (deny_unknown_fields)] pub struct TagsSettingsOverride { # [serde (deserialize_with = "tag_color_de")] # [serde (default)] pub colors : Option < HashMap < TagHash , Color > > , # [serde (deserialize_with = "tag_set_de" , alias = "ignore-tags")] # [serde (default)] pub ignore_tags : Option < HashSet < TagHash > > } impl Default for TagsSettingsOverride { fn default () -> Self { TagsSettingsOverride { colors : None , ignore_tags : None } } }