melib: Add standard header constants in email::headers #221

Merged
Manos Pitsidianakis merged 2 commits from standard-header-names into master 2023-05-30 19:52:30 +03:00
24 changed files with 2441 additions and 743 deletions

View File

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

View File

@ -231,8 +231,8 @@ impl Address {
.collect::<_>()
}
pub fn list_try_from(val: &str) -> Result<Vec<Address>> {
Ok(parser::address::rfc2822address_list(val.as_bytes())?
pub fn list_try_from<T: AsRef<[u8]>>(val: T) -> Result<Vec<Address>> {
Ok(parser::address::rfc2822address_list(val.as_ref())?
.1
.to_vec())
}
@ -380,6 +380,7 @@ impl core::fmt::Debug for Address {
impl TryFrom<&str> for Address {
type Error = Error;
fn try_from(val: &str) -> Result<Address> {
Ok(parser::address::address(val.as_bytes())?.1)
}

View File

@ -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(), "-->".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();

View File

@ -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>(S);
/// Case insensitive wrapper for a header name. As of `RFC5322` it's
/// guaranteed 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 = Error;
fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
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<Self, Self::Error> {
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<S: AsRef<[u8]>> HeaderKey for HeaderNameType<S> {
impl HeaderKey for HeaderName {
fn to_key(&self) -> &[u8] {
self.0.as_ref()
self.as_bytes()
}
}
@ -151,119 +67,102 @@ impl<'a> Borrow<dyn HeaderKey + 'a> for HeaderName {
}
}
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()
}
}
}
/// 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<HeaderName, String>);
impl std::ops::Index<usize> 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<HeaderName> 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 empty() -> Self {
Self::default()
}
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_mut<T: TryInto<HeaderName> + std::fmt::Debug>(
&mut self,
key: T,
) -> Option<&mut String>
where
<T as TryInto<HeaderName>>::Error: std::fmt::Debug,
{
let k = key.try_into().expect("Invalid bytes in header name.");
(self.0).get_mut(&k)
}
pub fn contains_key(&self, key: &str) -> bool {
(self.0).contains_key(HeaderNameType(key).borrow() as &dyn HeaderKey)
pub fn get<T: TryInto<HeaderName> + std::fmt::Debug>(&self, key: T) -> Option<&str>
where
<T as TryInto<HeaderName>>::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 remove(&mut self, key: &str) -> Option<String> {
(self.0).remove(HeaderNameType(key).borrow() as &dyn HeaderKey)
pub fn contains_key<T: TryInto<HeaderName> + std::fmt::Debug>(&self, key: T) -> bool
where
<T as TryInto<HeaderName>>::Error: std::fmt::Debug,
{
let k = key.try_into().expect("Invalid bytes in header name.");
(self.0).contains_key(&k)
}
pub fn remove<T: TryInto<HeaderName> + std::fmt::Debug>(&mut self, key: T) -> Option<String>
where
<T as TryInto<HeaderName>>::Error: std::fmt::Debug,
{
let k = key.try_into().expect("Invalid bytes in header name.");
(self.0).remove(&k)
}
pub fn into_inner(self) -> indexmap::IndexMap<HeaderName, String> {
self.0
}
}

View File

@ -0,0 +1,809 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
/*! 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<Custom>,
}
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
enum Repr<T> {
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<Self> {
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<Self> {
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<Self, InvalidHeaderName> {
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)),
})
}
}
pub const fn is_standard(&self) -> bool {
matches!(
self,
Self {
inner: Repr::Standard(_)
}
)
}
}
impl FromStr for HeaderName {
type Err = InvalidHeaderName;
fn from_str(s: &str) -> Result<HeaderName, InvalidHeaderName> {
HeaderName::from_bytes(s.as_bytes()).map_err(|_| InvalidHeaderName::new())
}
}
impl AsRef<str> 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<str> 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<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Serialize, Deserialize)]
#[serde(untagged)]
enum Helper {
S(String),
B(Vec<u8>),
}
if let Ok(s) = <Helper>::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<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
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, Self::Error> {
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, Self::Error> {
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, Self::Error> {
Self::from_bytes(s).map_err(|_| InvalidHeaderName::new())
}
}
impl TryFrom<String> for HeaderName {
type Error = InvalidHeaderName;
#[inline]
fn try_from(s: String) -> Result<Self, Self::Error> {
Self::from_bytes(s.as_bytes()).map_err(|_| InvalidHeaderName::new())
}
}
impl TryFrom<Vec<u8>> for HeaderName {
type Error = InvalidHeaderName;
#[inline]
fn try_from(vec: Vec<u8>) -> Result<Self, Self::Error> {
Self::from_bytes(&vec).map_err(|_| InvalidHeaderName::new())
}
}
#[doc(hidden)]
impl From<StandardHeader> for HeaderName {
fn from(src: StandardHeader) -> HeaderName {
HeaderName {
inner: Repr::Standard(src),
}
}
}
#[doc(hidden)]
impl From<Custom> 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<HeaderName> for &'a HeaderName {
#[inline]
fn eq(&self, other: &HeaderName) -> bool {
*other == *self
}
}
impl PartialEq<str> 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<HeaderName> 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<HeaderName> 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<H: Hasher>(&self, hasher: &mut H) {
for b in self.0.as_slice() {
hasher.write_u8(b.to_ascii_lowercase())
}
}
}

View File

@ -19,35 +19,111 @@
* along with meli. If not, see <http://www.gnu.org/licenses/>.
*/
/*! Parsing of `mailto` addresses */
//! Parsing of `mailto` addresses.
//!
//! Conforming to [RFC6068](https://www.rfc-editor.org/rfc/rfc6068) which obsoletes
//! [RFC2368](https://www.rfc-editor.org/rfc/rfc2368).
use std::convert::TryFrom;
use super::*;
use crate::{
email::headers::HeaderMap,
percent_encoding::{AsciiSet, CONTROLS},
};
#[derive(Debug, Clone)]
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct Mailto {
pub address: Address,
pub subject: Option<String>,
pub cc: Option<String>,
pub bcc: Option<String>,
pub address: Vec<Address>,
pub body: Option<String>,
pub headers: HeaderMap,
}
impl Mailto {
pub const IGNORE_HEADERS: &[HeaderName] = &[
HeaderName::FROM,
HeaderName::DATE,
HeaderName::MESSAGE_ID,
HeaderName::APPARENTLY_TO,
HeaderName::ARC_AUTHENTICATION_RESULTS,
HeaderName::ARC_MESSAGE_SIGNATURE,
HeaderName::ARC_SEAL,
HeaderName::AUTHENTICATION_RESULTS,
HeaderName::AUTOFORWARDED,
HeaderName::AUTO_SUBMITTED,
HeaderName::AUTOSUBMITTED,
HeaderName::BASE,
HeaderName::CONTENT_ALTERNATIVE,
HeaderName::CONTENT_BASE,
HeaderName::CONTENT_DESCRIPTION,
HeaderName::CONTENT_DISPOSITION,
HeaderName::CONTENT_DURATION,
HeaderName::CONTENT_FEATURES,
HeaderName::CONTENT_ID,
HeaderName::CONTENT_IDENTIFIER,
HeaderName::CONTENT_LANGUAGE,
HeaderName::CONTENT_LENGTH,
HeaderName::CONTENT_LOCATION,
HeaderName::CONTENT_MD5,
HeaderName::CONTENT_RETURN,
HeaderName::CONTENT_TRANSFER_ENCODING,
HeaderName::CONTENT_TRANSLATION_TYPE,
HeaderName::CONTENT_TYPE,
HeaderName::DELIVERED_TO,
HeaderName::DKIM_SIGNATURE,
HeaderName::ENCRYPTED,
HeaderName::FORWARDED,
HeaderName::MAIL_FOLLOWUP_TO,
HeaderName::MAIL_REPLY_TO,
HeaderName::MIME_VERSION,
HeaderName::ORIGINAL_ENCODED_INFORMATION_TYPES,
HeaderName::ORIGINAL_FROM,
HeaderName::ORIGINAL_MESSAGE_ID,
HeaderName::ORIGINAL_RECIPIENT,
HeaderName::ORIGINAL_SUBJECT,
HeaderName::ORIGINATOR_RETURN_ADDRESS,
HeaderName::RECEIVED,
HeaderName::RECEIVED_SPF,
HeaderName::RESENT_BCC,
HeaderName::RESENT_CC,
HeaderName::RESENT_DATE,
HeaderName::RESENT_FROM,
HeaderName::RESENT_MESSAGE_ID,
HeaderName::RESENT_REPLY_TO,
HeaderName::RESENT_SENDER,
HeaderName::RESENT_TO,
HeaderName::RETURN_PATH,
HeaderName::SENDER,
HeaderName::USER_AGENT,
];
pub const MAILTO_CHARSET: &AsciiSet = &CONTROLS
.add(b' ')
.add(b'"')
.add(b'"')
.add(b'#')
.add(b'%')
.add(b'/')
.add(b'<')
.add(b'>')
.add(b'?')
.add(b'`')
.add(b'{')
.add(b'}');
}
impl From<Mailto> for Draft {
fn from(val: Mailto) -> Self {
let mut ret = Draft::default();
let Mailto {
address,
subject,
cc,
bcc,
address: _,
body,
headers,
} = 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());
for (hdr, val) in headers.into_inner() {
ret.set_header(hdr, val);
}
ret.set_body(body.unwrap_or_default());
ret.set_header("To", address.to_string());
ret
}
}
@ -76,81 +152,304 @@ impl TryFrom<&[u8]> for Mailto {
}
}
impl TryFrom<&str> for Mailto {
type Error = String;
fn try_from(value: &str) -> std::result::Result<Self, Self::Error> {
let parse_res = super::parser::generic::mailto(value.as_bytes()).map(|(_, v)| v);
if let Ok(res) = parse_res {
Ok(res)
} else {
debug!(
"parser::mailto returned error while parsing {}:\n{:?}",
value,
parse_res.as_ref().err().unwrap()
);
Err(format!("{:?}", parse_res.err().unwrap()))
}
}
}
#[cfg(test)]
mod tests {
use HeaderName as HDR;
use super::*;
#[test]
fn test_mailto() {
let test_address = super::parser::address::address(b"info@example.com")
.map(|(_, v)| v)
.unwrap();
let mailto = Mailto::try_from(&b"mailto:info@example.com?subject=email%20subject"[0..])
.expect("Could not parse mailto link.");
let Mailto {
ref address,
ref subject,
ref cc,
ref bcc,
ref body,
} = mailto;
macro_rules! addr {
($lit:literal) => {
Address::try_from($lit).unwrap()
};
}
macro_rules! mlt {
($lit:literal) => {
Mailto::try_from($lit).expect("Could not parse mailto link.")
};
}
macro_rules! hdr {
($lit:literal) => {
HeaderName::try_from($lit).expect("Could not parse header name.")
};
}
macro_rules! hdrmap {
($(($field:literal, $val:literal)),+) => {{
let mut m = HeaderMap::empty();
$(
m.insert(hdr!($field), $val.into());
)+
m
}};
}
macro_rules! test_case {
($mailto:literal, addresses => $($addr:literal),*; body => $body:expr; $(($field:literal, $val:literal)),+) => {{
let addresses = &[
$(
addr!($addr)
),*
];
let Mailto {
address,
body,
headers,
} = mlt!($mailto);
assert_eq!(
(address.as_slice(), body.as_ref().map(|b| b.as_str()), headers),
(addresses.as_slice(), $body, hdrmap!($(($field, $val)),*))
);
}}
}
test_case!("mailto:info@example.com?subject=email%20subject",
addresses=> "info@example.com";
body => None;
("To", "info@example.com"), ("Subject", "email subject")
);
test_case!("mailto:info@example.com?cc=8cc9@example.com",
addresses=> "info@example.com";
body => None;
("To", "info@example.com"), ("Cc", "8cc9@example.com")
);
test_case!("mailto:info@example.com?bcc=7bcc8@example.com&body=line%20first%0Abut%20not%0Alast",
addresses=> "info@example.com";
body => Some("line first\nbut not\nlast");
("To", "info@example.com"), ("Bcc", "7bcc8@example.com")
);
test_case!("mailto:info@example.com?In-Reply-To=%3C20230526204845.673031-1-manos.pitsidianakis@linaro.org%3E&Cc=kraxel%40redhat.com%2Cqemu-devel%40nongnu.org&Subject=Re%3A%20%5BPATCH%5D%20Add%20virtio-sound%20and%20virtio-sound-pci%20devices",
addresses=> "info@example.com";
body => None;
("To", "info@example.com"), ("Subject", "Re: [PATCH] Add virtio-sound and virtio-sound-pci devices"), ("Cc", "kraxel@redhat.com,qemu-devel@nongnu.org"), ("In-Reply-To", "<20230526204845.673031-1-manos.pitsidianakis@linaro.org>")
);
assert_eq!(
(
address,
subject.as_ref().map(String::as_str),
cc.as_ref().map(String::as_str),
bcc.as_ref().map(String::as_str),
body.as_ref().map(String::as_str),
),
(&test_address, Some("email subject"), None, None, None)
mlt!("mailto:chris@example.com%2C%20tony@example.com"),
mlt!("mailto:?to=chris@example.com%2C%20tony@example.com")
);
let mailto = Mailto::try_from(&b"mailto:info@example.com?cc=8cc9@example.com"[0..])
.expect("Could not parse mailto link.");
let Mailto {
ref address,
ref subject,
ref cc,
ref bcc,
ref body,
} = mailto;
assert_eq!(
(
address,
subject.as_ref().map(String::as_str),
cc.as_ref().map(String::as_str),
bcc.as_ref().map(String::as_str),
body.as_ref().map(String::as_str),
),
(&test_address, None, Some("8cc9@example.com"), None, None)
/* address plus to= should be ignored */
assert!(
Mailto::try_from("mailto:?to=chris@example.com%2C%20tony@example.com")
!= Mailto::try_from("mailto:chris@example.com?to=tony@example.com"),
"{:?} == {:?}",
Mailto::try_from("mailto:?to=chris@example.com%2C%20tony@example.com"),
Mailto::try_from("mailto:chris@example.com?to=tony@example.com")
);
let mailto = Mailto::try_from(
&b"mailto:info@example.com?bcc=7bcc8@example.com&body=line%20first%0Abut%20not%0Alast"
[0..],
)
.expect("Could not parse mailto link.");
let Mailto {
ref address,
ref subject,
ref cc,
ref bcc,
ref body,
} = mailto;
// URLs for an ordinary individual mailing address:
test_case!("mailto:chris@example.com",
addresses=> "chris@example.com";
body => None;
("To", "chris@example.com")
);
// A URL for a mail response system that requires the name of the file in the
// subject:
test_case!("mailto:infobot@example.com?subject=current-issue",
addresses => "infobot@example.com";
body => None;
("To", "infobot@example.com"), ("Subject", "current-issue")
);
// A mail response system that requires a "send" request in the body:
test_case!("mailto:infobot@example.com?body=send%20current-issue",
addresses => "infobot@example.com";
body => Some("send current-issue");
("To", "infobot@example.com")
);
//A similar URL could have two lines with different "send" requests (in this
// case, "send current-issue" and, on the next line, "send index".)
test_case!("mailto:infobot@example.com?body=send%20current-issue%0D%0Asend%20index",
addresses => "infobot@example.com";
body => Some("send current-issue\r\nsend index");
("To", "infobot@example.com")
);
// An interesting use of your mailto URL is when browsing archives of messages.
// Each browsed message might contain a mailto URL like:
test_case!("mailto:foobar@example.com?In-Reply-To=%3c3469A91.D10AF4C@example.com%3e",
addresses => "foobar@example.com";
body => None;
("To", "foobar@example.com"), ("In-Reply-To", "<3469A91.D10AF4C@example.com>")
);
// A request to subscribe to a mailing list:
test_case!("mailto:majordomo@example.com?body=subscribe%20bamboo-l",
addresses => "majordomo@example.com";
body => Some("subscribe bamboo-l");
("To", "majordomo@example.com")
);
// A URL for a single user which includes a CC of another user:
test_case!("mailto:joe@example.com?cc=bob@example.com&body=hello",
addresses => "joe@example.com";
body => Some("hello");
("To", "joe@example.com"), ("Cc", "bob@example.com")
);
// Another way of expressing the same thing:
test_case!("mailto:?to=joe@example.com&cc=bob@example.com&body=hello",
addresses => "joe@example.com";
body => Some("hello");
("To", "joe@example.com"), ("Cc", "bob@example.com")
);
// Note the use of the "&" reserved character, above. The following example,
// by using "?" twice, is incorrect: <mailto:joe@example.com?cc=bob@
// example.com?body=hello> ; WRONG!
assert!(Mailto::try_from("mailto:joe@example.com?cc=bob@example.com?body=hello").is_err());
// <a href="mailto:?to=joe@xyz.com&amp;cc=bob@xyz.com&amp;body=hello"> assert
// these are equal
test_case!("mailto:?to=joe@example.com&amp;cc=bob@example.com&amp;body=hello",
addresses => "joe@example.com";
body => Some("hello");
("To", "joe@example.com"), ("Cc", "bob@example.com")
);
// To indicate the address "gorby%kremvax@example.com" one would do:
// <mailto:gorby%25kremvax@example.com>
test_case!("mailto:gorby%25kremvax@example.com",
addresses => "gorby%kremvax@example.com";
body => None;
("To", "gorby%kremvax@example.com")
);
// Custom header is ignored
// <mailto:address@example.com?blat=foop>
test_case!("mailto:address@example.com?blat=foop",
addresses => "address@example.com";
body => None;
("To", "address@example.com")
);
// 6.2. Examples of Complicated Email Addresses
assert_eq!(
(
address,
subject.as_ref().map(String::as_str),
cc.as_ref().map(String::as_str),
bcc.as_ref().map(String::as_str),
body.as_ref().map(String::as_str),
),
(
&test_address,
None,
None,
Some("7bcc8@example.com"),
Some("line first\nbut not\nlast")
)
mlt!("mailto:%22not%40me%22@example.org").address,
vec![addr!(r#""not@me"@example.org"#)]
);
// Email address: "oh\\no"@example.org; corresponding 'mailto' URI:
// <mailto:%22oh%5C%5Cno%22@example.org>.
// Email address: "\\\"it's\ ugly\\\""@example.org; corresponding
// 'mailto' URI:
// <mailto:%22%5C%5C%5C%22it's%5C%20ugly%5C%5C%5C%22%22@example.org>.
// [tag:FIXME]
//assert_eq!(
// mlt!("mailto:%22%5C%5C%5C%22it's%5C%20ugly%5C%5C%5C%22%22@example.org").
// address, vec![addr!(r#"\"it's ugly\"@example.org"#)]
//);
// When an email address itself includes an "&" (ampersand) character, that
// character has to be percent-encoded. For example, the 'mailto' URI
// to send mail to "Mike&family@example.org" is
// <mailto:Mike%26family@example.org>.
assert_eq!(
mlt!("mailto:Mike%26family@example.org").address,
vec![addr!("Mike&family@example.org")]
);
// Sending a mail with the subject "coffee" in French, i.e., "cafe" where the
// final e is an e-acute, using UTF-8 and percent-encoding:
// <mailto:user@example.org?subject=caf%C3%A9>
assert_eq!(
&mlt!("mailto:user@example.org?subject=caf%C3%A9").headers[HDR::SUBJECT],
"café"
);
// The same subject, this time using an encoded-word (escaping the "="
// and "?" characters used in the encoded-word syntax, because they are
// reserved):
// [tag:FIXME]
// <mailto:user@example.org?subject=%3D%3Futf-8%3FQ%3Fcaf%3DC3%3DA9%3F%3D>
assert_eq!(
&mlt!("mailto:user@example.org?subject=%3D%3Futf-8%3FQ%3Fcaf%3DC3%3DA9%3F%3D").headers
[HDR::SUBJECT],
"=?utf-8?Q?caf=C3=A9?="
);
// The same subject, this time encoded as iso-8859-1:
// <mailto:user@example.org?subject=%3D%3Fiso-8859-1%3FQ%3Fcaf%3DE9%3F%3D>
// [tag:FIXME]
assert_eq!(
&mlt!("mailto:user@example.org?subject=%3D%3Fiso-8859-1%3FQ%3Fcaf%3DE9%3F%3D").headers
[HDR::SUBJECT],
"=?iso-8859-1?Q?caf=E9?="
);
// Going back to straight UTF-8 and adding a body with the same value:
//
// <mailto:user@example.org?subject=caf%C3%A9&body=caf%C3%A9>
test_case!("mailto:user@example.org?subject=caf%C3%A9&body=caf%C3%A9",
addresses => "user@example.org";
body => Some("café");
("To", "user@example.org"),
("Subject", "café")
);
// The following example uses the Japanese word "natto" (Unicode
// characters U+7D0D U+8C46) as a domain name label, sending a mail to a
// user at "natto".example.org:
// <mailto:user@%E7%B4%8D%E8%B1%86.example.org?subject=Test&body=NATTO>
// When constructing the email, the domain name label is converted to
// punycode. The resulting message may look as follows:
// From: sender@example.net
// To: user@xn--99zt52a.example.org
// Subject: Test
// Content-Type: text/plain
// Content-Transfer-Encoding: 7bit
//
// NATTO
test_case!("mailto:user@%E7%B4%8D%E8%B1%86.example.org?subject=Test&body=NATTO",
addresses => "user@納豆.example.org";
body => Some("NATTO");
("To", "user@納豆.example.org"),
("Subject", "Test")
);
}
}

View File

@ -19,8 +19,9 @@
* along with meli. If not, see <http://www.gnu.org/licenses/>.
*/
/*! Parsers for email. See submodules */
use std::borrow::Cow;
//! Parsers for email. See submodules.
use std::{borrow::Cow, convert::TryFrom, fmt::Write};
use nom::{
branch::alt,
@ -34,7 +35,16 @@ use nom::{
};
use smallvec::SmallVec;
use crate::error::{Error, Result, ResultIntoError};
use crate::{
email::{
address::Address,
headers::{HeaderMap, HeaderName},
mailto::Mailto,
},
error::{Error, Result, ResultIntoError},
html_escape::HtmlEntity,
percent_encoding::percent_decode,
};
macro_rules! to_str {
($l:expr) => {{
@ -913,85 +923,157 @@ pub mod generic {
}
}
use crate::email::{address::Address, mailto::Mailto};
pub fn mailto(mut input: &[u8]) -> IResult<&[u8], Mailto> {
let orig_input = input;
if !input.starts_with(b"mailto:") {
return Err(nom::Err::Error(
(input, "mailto(): input doesn't start with `mailto:`").into(),
));
}
let mut body = None;
let mut headers = HeaderMap::empty();
let mut address: Vec<Address>;
if String::from_utf8_lossy(input).matches('?').count() > 1 {
return Err(nom::Err::Error(
(input, "mailto(): Using '?' twice is invalid.").into(),
));
}
input = &input[b"mailto:".len()..];
let mut decoded_owned = percent_decode(input).decode_utf8().unwrap().to_string();
let end = input.iter().position(|e| *e == b'?').unwrap_or(input.len());
let address: Address;
let mut substitutions = vec![];
for (i, _) in decoded_owned.match_indices('&') {
if let Some(j) = HtmlEntity::ALL
.iter()
.position(|e| decoded_owned[i..].starts_with(e))
{
substitutions.push((i, HtmlEntity::ALL[j].len(), HtmlEntity::GLYPHS[j]));
}
}
if let Ok((_, addr)) = crate::email::parser::address::address(&input[..end]) {
for (i, len, g) in substitutions.into_iter().rev() {
decoded_owned.replace_range(i..(i + len), g);
}
let mut decoded = decoded_owned.as_str();
let end = decoded.as_bytes().iter().position(|e| *e == b'?');
let end_or_len = end.unwrap_or(decoded.len());
if let Ok(addr) = Address::list_try_from(&decoded[..end_or_len]) {
address = addr;
input = if input[end..].is_empty() {
&input[end..]
decoded = if decoded[end_or_len..].is_empty() {
&decoded[end_or_len..]
} else {
&input[end + 1..]
&decoded[end_or_len + 1..]
};
} else if end.is_some() {
decoded = &decoded[1..];
address = vec![];
} else {
return Err(nom::Err::Error(
(input, "mailto(): address not found in input").into(),
(
input,
format!("input {:?}", String::from_utf8_lossy(orig_input)),
)
.into(),
));
}
let mut subject = None;
let mut cc = None;
let mut bcc = None;
let mut body = None;
while !input.is_empty() {
let tag = if let Some(tag_pos) = input.iter().position(|e| *e == b'=') {
let ret = &input[0..tag_pos];
input = &input[tag_pos + 1..];
if !address.is_empty() {
let mut full_address = String::new();
for address in &address {
write!(&mut full_address, "{}, ", address)
.expect("Could not write into a String, are you out of memory?");
}
if full_address.ends_with(", ") {
let len = full_address.len();
full_address.truncate(len - ", ".len());
}
headers.insert(HeaderName::TO, full_address);
}
while !decoded.is_empty() {
if decoded.starts_with("&amp;") {
decoded = &decoded["&amp;".len()..];
continue;
}
let tag = if let Some(tag_pos) = decoded.as_bytes().iter().position(|e| *e == b'=') {
let ret = &decoded[0..tag_pos];
decoded = &decoded[tag_pos + 1..];
ret
} else {
return Err(nom::Err::Error(
(input, "mailto(): extra characters found in input").into(),
(
input,
format!("mailto(): extra characters found in input: {}", decoded),
)
.into(),
));
};
let value_end = input.iter().position(|e| *e == b'&').unwrap_or(input.len());
let value_end = decoded
.as_bytes()
.iter()
.position(|e| *e == b'&')
.unwrap_or(decoded.len());
let value = String::from_utf8_lossy(&input[..value_end]).to_string();
let value = decoded[..value_end].to_string();
match tag {
b"subject" if subject.is_none() => {
subject = Some(value.replace("%20", " "));
}
b"cc" if cc.is_none() => {
cc = Some(value);
}
b"bcc" if bcc.is_none() => {
bcc = Some(value);
}
b"body" if body.is_none() => {
/* FIXME:
* Parse escaped characters properly.
*/
body = Some(value.replace("%20", " ").replace("%0A", "\n"));
}
_ => {
return Err(nom::Err::Error(
(input, "mailto(): unknown tag in input").into(),
));
"body" if body.is_none() => {
body = Some(value);
}
other => match HeaderName::try_from(other) {
Ok(hdr) if hdr == HeaderName::TO => {
if !headers.contains_key(&hdr) {
if let Ok(address_val) = Address::list_try_from(value.as_str()) {
address.extend(address_val.into_iter());
}
headers.insert(HeaderName::TO, value);
}
}
Ok(hdr) if hdr.is_standard() => {
if Mailto::IGNORE_HEADERS.contains(&hdr) {
log::warn!(
"parsing mailto(): header {} is not allowed in mailto URIs for \
safety and will be ignored. Value was {:?}",
hdr,
value
);
}
if !headers.contains_key(&hdr) {
headers.insert(hdr, value);
}
}
Ok(hdr) => {
log::warn!(
"parsing mailto(): header {} is not a known header and it will be \
ignored.Value was {:?}",
hdr,
value
);
}
_ => {
return Err(nom::Err::Error(
(input, "mailto(): unknown tag in input").into(),
));
}
},
}
if input[value_end..].is_empty() {
if decoded[value_end..].is_empty() {
break;
}
input = &input[value_end + 1..];
decoded = &decoded[value_end + 1..];
}
Ok((
input,
Mailto {
address,
subject,
cc,
bcc,
body,
headers,
},
))
}

View File

@ -306,6 +306,7 @@ impl From<isahc::http::StatusCode> 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<nom::Err<(&str, nom::error::ErrorKind)>> for Error {
}
}
impl From<crate::email::InvalidHeaderName> 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 {

View File

@ -39,8 +39,6 @@
//! - Basic mail account configuration to use with
//! [`backends`](./backends/index.html) (see module
//! [`conf`](./conf/index.html))
//! - Parser combinators (see module [`parsec`](./parsec/index.html))
//! - A `ShellExpandTrait` to expand paths like a shell.
//! - A `debug` macro that works like `std::dbg` but for multiple threads. (see
//! [`debug` macro](./macro.debug.html))
@ -84,11 +82,8 @@ pub mod dbg {
#[cfg(feature = "unicode_algorithms")]
pub mod text_processing;
pub mod datetime;
pub use datetime::UnixTimestamp;
pub use utils::{datetime::UnixTimestamp, *};
#[macro_use]
mod logging;
pub use self::logging::{LogLevel, StderrLogger};
pub mod addressbook;
@ -106,16 +101,15 @@ pub mod error;
pub use crate::error::*;
pub mod thread;
pub use thread::*;
pub mod connections;
pub mod parsec;
pub mod search;
#[macro_use]
mod utils;
#[cfg(feature = "gpgme")]
pub mod gpgme;
#[cfg(feature = "smtp")]
pub mod smtp;
#[cfg(feature = "sqlite3")]
pub mod sqlite3;
#[macro_use]
extern crate serde_derive;
@ -165,289 +159,3 @@ impl core::fmt::Display for Bytes {
}
pub use shellexpand::ShellExpandTrait;
pub mod shellexpand {
#[cfg(not(any(target_os = "netbsd", target_os = "macos")))]
use std::os::unix::io::AsRawFd;
use std::{
ffi::OsStr,
os::unix::ffi::OsStrExt,
path::{Path, PathBuf},
};
use smallvec::SmallVec;
pub trait ShellExpandTrait {
fn expand(&self) -> PathBuf;
fn complete(&self, force: bool) -> SmallVec<[String; 128]>;
}
impl ShellExpandTrait for Path {
fn expand(&self) -> PathBuf {
let mut ret = PathBuf::new();
for c in self.components() {
let c_to_str = c.as_os_str().to_str();
match c_to_str {
Some("~") => {
if let Ok(home_dir) = std::env::var("HOME") {
ret.push(home_dir)
} else {
return PathBuf::new();
}
}
Some(var) if var.starts_with('$') => {
let env_name = var.split_at(1).1;
if env_name.chars().all(char::is_uppercase) {
ret.push(std::env::var(env_name).unwrap_or_default());
} else {
ret.push(c);
}
}
Some(_) => {
ret.push(c);
}
None => {
/* path is invalid */
return PathBuf::new();
}
}
}
ret
}
#[cfg(target_os = "linux")]
fn complete(&self, force: bool) -> SmallVec<[String; 128]> {
use libc::dirent64;
use nix::fcntl::OFlag;
const BUF_SIZE: ::libc::size_t = 8 << 10;
let (prefix, _match) = if self.as_os_str().as_bytes().ends_with(b"/.") {
(self.components().as_path(), OsStr::from_bytes(b"."))
} else if self.exists() && (!force || self.as_os_str().as_bytes().ends_with(b"/")) {
return SmallVec::new();
} else {
let last_component = self
.components()
.last()
.map(|c| c.as_os_str())
.unwrap_or_else(|| OsStr::from_bytes(b""));
let prefix = if let Some(p) = self.parent() {
p
} else {
return SmallVec::new();
};
(prefix, last_component)
};
let dir = match ::nix::dir::Dir::openat(
::libc::AT_FDCWD,
prefix,
OFlag::O_DIRECTORY | OFlag::O_NOATIME | OFlag::O_RDONLY | OFlag::O_CLOEXEC,
::nix::sys::stat::Mode::S_IRUSR | ::nix::sys::stat::Mode::S_IXUSR,
)
.or_else(|_| {
::nix::dir::Dir::openat(
::libc::AT_FDCWD,
prefix,
OFlag::O_DIRECTORY | OFlag::O_RDONLY | OFlag::O_CLOEXEC,
::nix::sys::stat::Mode::S_IRUSR | ::nix::sys::stat::Mode::S_IXUSR,
)
}) {
Ok(dir) => dir,
Err(err) => {
debug!(prefix);
debug!(err);
return SmallVec::new();
}
};
let mut buf: Vec<u8> = Vec::with_capacity(BUF_SIZE);
let mut entries = SmallVec::new();
loop {
let n: i64 = unsafe {
::libc::syscall(
::libc::SYS_getdents64,
dir.as_raw_fd(),
buf.as_ptr(),
BUF_SIZE - 256,
)
};
if n < 0 {
return SmallVec::new();
} else if n == 0 {
break;
}
let n = n as usize;
unsafe {
buf.set_len(n);
}
let mut pos = 0;
while pos < n {
let dir = unsafe { std::mem::transmute::<&[u8], &[dirent64]>(&buf[pos..]) };
let entry = unsafe { std::ffi::CStr::from_ptr(dir[0].d_name.as_ptr()) };
if entry.to_bytes() != b"." && entry.to_bytes() != b".." {
if entry.to_bytes().starts_with(_match.as_bytes()) {
if dir[0].d_type == ::libc::DT_DIR && !entry.to_bytes().ends_with(b"/")
{
let mut s = unsafe {
String::from_utf8_unchecked(
entry.to_bytes()[_match.as_bytes().len()..].to_vec(),
)
};
s.push('/');
entries.push(s);
} else {
entries.push(unsafe {
String::from_utf8_unchecked(
entry.to_bytes()[_match.as_bytes().len()..].to_vec(),
)
});
}
}
}
pos += dir[0].d_reclen as usize;
}
// https://github.com/romkatv/gitstatus/blob/caf44f7aaf33d0f46e6749e50595323c277e0908/src/dir.cc
// "It's tempting to bail here if n + sizeof(linux_dirent64) +
// 512 <= n. After all, there was enough space
// for another entry but SYS_getdents64 didn't write it, so this
// must be the end of the directory listing,
// right? Unfortunately, no. SYS_getdents64 is finicky.
// It sometimes writes a partial list of entries even if the
// full list would fit."
}
entries
}
#[cfg(not(target_os = "linux"))]
fn complete(&self, force: bool) -> SmallVec<[String; 128]> {
let mut entries = SmallVec::new();
let (prefix, _match) = {
if self.exists() && (!force || self.as_os_str().as_bytes().ends_with(b"/")) {
// println!("{} {:?}", self.display(), self.components().last());
return entries;
} else {
let last_component = self
.components()
.last()
.map(|c| c.as_os_str())
.unwrap_or_else(|| OsStr::from_bytes(b""));
let prefix = if let Some(p) = self.parent() {
p
} else {
return entries;
};
(prefix, last_component)
}
};
if force && self.is_dir() && !self.as_os_str().as_bytes().ends_with(b"/") {
entries.push("/".to_string());
}
if let Ok(iter) = std::fs::read_dir(&prefix) {
for entry in iter.flatten() {
if entry.path().as_os_str().as_bytes() != b"."
&& entry.path().as_os_str().as_bytes() != b".."
&& entry
.path()
.as_os_str()
.as_bytes()
.starts_with(_match.as_bytes())
{
if entry.path().is_dir()
&& !entry.path().as_os_str().as_bytes().ends_with(b"/")
{
let mut s = unsafe {
String::from_utf8_unchecked(
entry.path().as_os_str().as_bytes()[_match.as_bytes().len()..]
.to_vec(),
)
};
s.push('/');
entries.push(s);
} else {
entries.push(unsafe {
String::from_utf8_unchecked(
entry.path().as_os_str().as_bytes()[_match.as_bytes().len()..]
.to_vec(),
)
});
}
}
}
}
entries
}
}
#[test]
fn test_shellexpandtrait() {
assert!(Path::new("~").expand().complete(false).is_empty());
assert!(!Path::new("~").expand().complete(true).is_empty());
}
}
#[macro_export]
macro_rules! declare_u64_hash {
($type_name:ident) => {
#[derive(
Hash,
Eq,
PartialEq,
Debug,
Ord,
PartialOrd,
Default,
Serialize,
Deserialize,
Copy,
Clone,
)]
#[repr(transparent)]
pub struct $type_name(pub u64);
impl $type_name {
#[inline(always)]
pub fn from_bytes(bytes: &[u8]) -> Self {
use std::{collections::hash_map::DefaultHasher, hash::Hasher};
let mut h = DefaultHasher::new();
h.write(bytes);
Self(h.finish())
}
#[inline(always)]
pub const fn to_be_bytes(self) -> [u8; 8] {
self.0.to_be_bytes()
}
#[inline(always)]
pub const fn is_null(self) -> bool {
self.0 == 0
}
}
impl core::fmt::Display for $type_name {
fn fmt(&self, fmt: &mut core::fmt::Formatter) -> core::fmt::Result {
write!(fmt, "{}", self.0)
}
}
#[cfg(feature = "sqlite3")]
impl rusqlite::types::ToSql for $type_name {
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput> {
Ok(rusqlite::types::ToSqlOutput::from(self.0 as i64))
}
}
#[cfg(feature = "sqlite3")]
impl rusqlite::types::FromSql for $type_name {
fn column_result(
value: rusqlite::types::ValueRef,
) -> rusqlite::types::FromSqlResult<Self> {
let b: i64 = rusqlite::types::FromSql::column_result(value)?;
Ok($type_name(b as u64))
}
}
};
}

View File

@ -0,0 +1,155 @@
/*
* meli - lib.rs
*
* Copyright 2017 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/>.
*/
//! Utility modules for general use.
pub mod connections;
pub mod datetime;
#[macro_use]
pub mod logging;
pub mod parsec;
pub mod percent_encoding;
pub mod shellexpand;
#[cfg(feature = "sqlite3")]
pub mod sqlite3;
pub mod html_escape {
//! HTML Coded Character Set
/// Numeric and Special Graphic Entity Set
///
/// ```text
/// GLYPH NAME SYNTAX DESCRIPTION
/// < lt &lt; Less than sign
/// > gt &gt; Greater than sign
/// & amp &amp; Ampersand
/// " quot &quot; Double quote sign
/// ```
///
/// Source: <https://www.w3.org/MarkUp/html-spec/html-spec_9.html#SEC9.7.1>
#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
pub enum HtmlEntity {
/// Less than sign
Lt,
/// Greater than sign
Gt,
/// Ampersand
Amp,
/// Double quote sign
Quot,
}
impl HtmlEntity {
pub const ALL: [&str; 4] = ["&lt;", "&gt;", "&amp;", "&quot;"];
pub const GLYPHS: [&str; 4] = ["<", ">", "&", "\""];
pub const fn glyph(self) -> char {
match self {
Self::Lt => '<',
Self::Gt => '>',
Self::Amp => '&',
Self::Quot => '"',
}
}
pub const fn name(self) -> &'static str {
match self {
Self::Lt => "lt",
Self::Gt => "gt",
Self::Amp => "amp",
Self::Quot => "quot",
}
}
pub const fn syntax(self) -> &'static str {
match self {
Self::Lt => "&lt;",
Self::Gt => "&gt;",
Self::Amp => "&amp;",
Self::Quot => "&quot;",
}
}
}
}
#[macro_export]
macro_rules! declare_u64_hash {
($type_name:ident) => {
#[derive(
Hash,
Eq,
PartialEq,
Debug,
Ord,
PartialOrd,
Default,
Serialize,
Deserialize,
Copy,
Clone,
)]
#[repr(transparent)]
pub struct $type_name(pub u64);
impl $type_name {
#[inline(always)]
pub fn from_bytes(bytes: &[u8]) -> Self {
use std::{collections::hash_map::DefaultHasher, hash::Hasher};
let mut h = DefaultHasher::new();
h.write(bytes);
Self(h.finish())
}
#[inline(always)]
pub const fn to_be_bytes(self) -> [u8; 8] {
self.0.to_be_bytes()
}
#[inline(always)]
pub const fn is_null(self) -> bool {
self.0 == 0
}
}
impl core::fmt::Display for $type_name {
fn fmt(&self, fmt: &mut core::fmt::Formatter) -> core::fmt::Result {
write!(fmt, "{}", self.0)
}
}
#[cfg(feature = "sqlite3")]
impl rusqlite::types::ToSql for $type_name {
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput> {
Ok(rusqlite::types::ToSqlOutput::from(self.0 as i64))
}
}
#[cfg(feature = "sqlite3")]
impl rusqlite::types::FromSql for $type_name {
fn column_result(
value: rusqlite::types::ValueRef,
) -> rusqlite::types::FromSqlResult<Self> {
let b: i64 = rusqlite::types::FromSql::column_result(value)?;
Ok($type_name(b as u64))
}
}
};
}

View File

@ -0,0 +1,488 @@
// Copyright 2013-2016 The rust-url developers.
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.
//! URLs use special characters to indicate the parts of the request.
//! For example, a `?` question mark marks the end of a path and the start of a
//! query string. In order for that character to exist inside a path, it needs
//! to be encoded differently.
//!
//! Percent encoding replaces reserved characters with the `%` escape character
//! followed by a byte value as two hexadecimal digits.
//! For example, an ASCII space is replaced with `%20`.
//!
//! When encoding, the set of characters that can (and should, for readability)
//! be left alone depends on the context.
//! The `?` question mark mentioned above is not a separator when used literally
//! inside of a query string, and therefore does not need to be encoded.
//! The [`AsciiSet`] parameter of [`percent_encode`] and [`utf8_percent_encode`]
//! lets callers configure this.
//!
//! This crate deliberately does not provide many different sets.
//! Users should consider in what context the encoded string will be used,
//! read relevant specifications, and define their own set.
//! This is done by using the `add` method of an existing set.
//!
//! # Examples
//!
//! ```rust
//! use melib::percent_encoding::{percent_decode_str, utf8_percent_encode, AsciiSet, CONTROLS};
//!
//! /// https://url.spec.whatwg.org/#fragment-percent-encode-set
//! const FRAGMENT: &AsciiSet = &CONTROLS.add(b' ').add(b'"').add(b'<').add(b'>').add(b'`');
//!
//! assert_eq!(
//! utf8_percent_encode("foo <bar>", FRAGMENT).to_string(),
//! "foo%20%3Cbar%3E"
//! );
//!
//! assert_eq!(
//! percent_decode_str("foo%20%3Cbar%3E").decode_utf8().unwrap(),
//! "foo <bar>"
//! );
//! ```
use std::{borrow::Cow, fmt, mem, slice, str};
/// Represents a set of characters or bytes in the ASCII range.
///
/// This is used in [`percent_encode`] and [`utf8_percent_encode`].
/// This is similar to [percent-encode sets](https://url.spec.whatwg.org/#percent-encoded-bytes).
///
/// Use the `add` method of an existing set to define a new set. For example:
///
/// ```
/// use melib::percent_encoding::{AsciiSet, CONTROLS};
///
/// /// https://url.spec.whatwg.org/#fragment-percent-encode-set
/// const FRAGMENT: &AsciiSet = &CONTROLS.add(b' ').add(b'"').add(b'<').add(b'>').add(b'`');
/// ```
pub struct AsciiSet {
mask: [Chunk; ASCII_RANGE_LEN / BITS_PER_CHUNK],
}
type Chunk = u32;
const ASCII_RANGE_LEN: usize = 0x80;
const BITS_PER_CHUNK: usize = 8 * mem::size_of::<Chunk>();
impl AsciiSet {
/// Called with UTF-8 bytes rather than code points.
/// Not used for non-ASCII bytes.
const fn contains(&self, byte: u8) -> bool {
let chunk = self.mask[byte as usize / BITS_PER_CHUNK];
let mask = 1 << (byte as usize % BITS_PER_CHUNK);
(chunk & mask) != 0
}
fn should_percent_encode(&self, byte: u8) -> bool {
!byte.is_ascii() || self.contains(byte)
}
pub const fn add(&self, byte: u8) -> Self {
let mut mask = self.mask;
mask[byte as usize / BITS_PER_CHUNK] |= 1 << (byte as usize % BITS_PER_CHUNK);
AsciiSet { mask }
}
pub const fn remove(&self, byte: u8) -> Self {
let mut mask = self.mask;
mask[byte as usize / BITS_PER_CHUNK] &= !(1 << (byte as usize % BITS_PER_CHUNK));
AsciiSet { mask }
}
}
/// The set of 0x00 to 0x1F (C0 controls), and 0x7F (DEL).
///
/// Note that this includes the newline and tab characters, but not the space
/// 0x20.
///
/// <https://url.spec.whatwg.org/#c0-control-percent-encode-set>
pub const CONTROLS: &AsciiSet = &AsciiSet {
mask: [
!0_u32, // C0: 0x00 to 0x1F (32 bits set)
0,
0,
1 << (0x7F_u32 % 32), // DEL: 0x7F (one bit set)
],
};
macro_rules! static_assert {
($( $bool: expr, )+) => {
fn _static_assert() {
$(
let _ = mem::transmute::<[u8; $bool as usize], u8>;
)+
}
}
}
static_assert! {
CONTROLS.contains(0x00),
CONTROLS.contains(0x1F),
!CONTROLS.contains(0x20),
!CONTROLS.contains(0x7E),
CONTROLS.contains(0x7F),
}
/// Everything that is not an ASCII letter or digit.
///
/// This is probably more eager than necessary in any context.
pub const NON_ALPHANUMERIC: &AsciiSet = &CONTROLS
.add(b' ')
.add(b'!')
.add(b'"')
.add(b'#')
.add(b'$')
.add(b'%')
.add(b'&')
.add(b'\'')
.add(b'(')
.add(b')')
.add(b'*')
.add(b'+')
.add(b',')
.add(b'-')
.add(b'.')
.add(b'/')
.add(b':')
.add(b';')
.add(b'<')
.add(b'=')
.add(b'>')
.add(b'?')
.add(b'@')
.add(b'[')
.add(b'\\')
.add(b']')
.add(b'^')
.add(b'_')
.add(b'`')
.add(b'{')
.add(b'|')
.add(b'}')
.add(b'~');
/// Return the percent-encoding of the given byte.
///
/// This is unconditional, unlike `percent_encode()` which has an `AsciiSet`
/// parameter.
///
/// # Examples
///
/// ```
/// use melib::percent_encoding::percent_encode_byte;
///
/// assert_eq!(
/// "foo bar"
/// .bytes()
/// .map(percent_encode_byte)
/// .collect::<String>(),
/// "%66%6F%6F%20%62%61%72"
/// );
/// ```
#[inline]
pub fn percent_encode_byte(byte: u8) -> &'static str {
static ENC_TABLE: &[u8; 768] = b"\
%00%01%02%03%04%05%06%07%08%09%0A%0B%0C%0D%0E%0F\
%10%11%12%13%14%15%16%17%18%19%1A%1B%1C%1D%1E%1F\
%20%21%22%23%24%25%26%27%28%29%2A%2B%2C%2D%2E%2F\
%30%31%32%33%34%35%36%37%38%39%3A%3B%3C%3D%3E%3F\
%40%41%42%43%44%45%46%47%48%49%4A%4B%4C%4D%4E%4F\
%50%51%52%53%54%55%56%57%58%59%5A%5B%5C%5D%5E%5F\
%60%61%62%63%64%65%66%67%68%69%6A%6B%6C%6D%6E%6F\
%70%71%72%73%74%75%76%77%78%79%7A%7B%7C%7D%7E%7F\
%80%81%82%83%84%85%86%87%88%89%8A%8B%8C%8D%8E%8F\
%90%91%92%93%94%95%96%97%98%99%9A%9B%9C%9D%9E%9F\
%A0%A1%A2%A3%A4%A5%A6%A7%A8%A9%AA%AB%AC%AD%AE%AF\
%B0%B1%B2%B3%B4%B5%B6%B7%B8%B9%BA%BB%BC%BD%BE%BF\
%C0%C1%C2%C3%C4%C5%C6%C7%C8%C9%CA%CB%CC%CD%CE%CF\
%D0%D1%D2%D3%D4%D5%D6%D7%D8%D9%DA%DB%DC%DD%DE%DF\
%E0%E1%E2%E3%E4%E5%E6%E7%E8%E9%EA%EB%EC%ED%EE%EF\
%F0%F1%F2%F3%F4%F5%F6%F7%F8%F9%FA%FB%FC%FD%FE%FF\
";
let index = usize::from(byte) * 3;
// SAFETY: ENC_TABLE is ascii-only, so any subset if it should be
// ascii-only too, which is valid utf8.
unsafe { str::from_utf8_unchecked(&ENC_TABLE[index..index + 3]) }
}
/// Percent-encode the given bytes with the given set.
///
/// Non-ASCII bytes and bytes in `ascii_set` are encoded.
///
/// The return type:
///
/// * Implements `Iterator<Item = &str>` and therefore has a
/// `.collect::<String>()` method,
/// * Implements `Display` and therefore has a `.to_string()` method,
/// * Implements `Into<Cow<str>>` borrowing `input` when none of its bytes are
/// encoded.
///
/// # Examples
///
/// ```
/// use melib::percent_encoding::{percent_encode, NON_ALPHANUMERIC};
///
/// assert_eq!(
/// percent_encode(b"foo bar?", NON_ALPHANUMERIC).to_string(),
/// "foo%20bar%3F"
/// );
/// ```
#[inline]
pub fn percent_encode<'a>(input: &'a [u8], ascii_set: &'static AsciiSet) -> PercentEncode<'a> {
PercentEncode {
bytes: input,
ascii_set,
}
}
/// Percent-encode the UTF-8 encoding of the given string.
///
/// See [`percent_encode`] regarding the return type.
///
/// # Examples
///
/// ```
/// use melib::percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC};
///
/// assert_eq!(
/// utf8_percent_encode("foo bar?", NON_ALPHANUMERIC).to_string(),
/// "foo%20bar%3F"
/// );
/// ```
#[inline]
pub fn utf8_percent_encode<'a>(input: &'a str, ascii_set: &'static AsciiSet) -> PercentEncode<'a> {
percent_encode(input.as_bytes(), ascii_set)
}
/// The return type of [`percent_encode`] and [`utf8_percent_encode`].
#[derive(Clone)]
pub struct PercentEncode<'a> {
bytes: &'a [u8],
ascii_set: &'static AsciiSet,
}
impl<'a> Iterator for PercentEncode<'a> {
type Item = &'a str;
fn next(&mut self) -> Option<&'a str> {
if let Some((&first_byte, remaining)) = self.bytes.split_first() {
if self.ascii_set.should_percent_encode(first_byte) {
self.bytes = remaining;
Some(percent_encode_byte(first_byte))
} else {
// The unsafe blocks here are appropriate because the bytes are
// confirmed as a subset of UTF-8 in should_percent_encode.
for (i, &byte) in remaining.iter().enumerate() {
if self.ascii_set.should_percent_encode(byte) {
// 1 for first_byte + i for previous iterations of this loop
let (unchanged_slice, remaining) = self.bytes.split_at(1 + i);
self.bytes = remaining;
return Some(unsafe { str::from_utf8_unchecked(unchanged_slice) });
}
}
let unchanged_slice = self.bytes;
self.bytes = &[][..];
Some(unsafe { str::from_utf8_unchecked(unchanged_slice) })
}
} else {
None
}
}
fn size_hint(&self) -> (usize, Option<usize>) {
if self.bytes.is_empty() {
(0, Some(0))
} else {
(1, Some(self.bytes.len()))
}
}
}
impl<'a> fmt::Display for PercentEncode<'a> {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
for c in (*self).clone() {
formatter.write_str(c)?
}
Ok(())
}
}
impl<'a> From<PercentEncode<'a>> for Cow<'a, str> {
fn from(mut iter: PercentEncode<'a>) -> Self {
match iter.next() {
None => "".into(),
Some(first) => match iter.next() {
None => first.into(),
Some(second) => {
let mut string = first.to_owned();
string.push_str(second);
string.extend(iter);
string.into()
}
},
}
}
}
/// Percent-decode the given string.
///
/// <https://url.spec.whatwg.org/#string-percent-decode>
///
/// See [`percent_decode`] regarding the return type.
#[inline]
pub fn percent_decode_str(input: &str) -> PercentDecode<'_> {
percent_decode(input.as_bytes())
}
/// Percent-decode the given bytes.
///
/// <https://url.spec.whatwg.org/#percent-decode>
///
/// Any sequence of `%` followed by two hexadecimal digits is decoded.
/// The return type:
///
/// * Implements `Into<Cow<u8>>` borrowing `input` when it contains no
/// percent-encoded sequence,
/// * Implements `Iterator<Item = u8>` and therefore has a
/// `.collect::<Vec<u8>>()` method,
/// * Has `decode_utf8()` and `decode_utf8_lossy()` methods.
///
/// # Examples
///
/// ```
/// use melib::percent_encoding::percent_decode;
///
/// assert_eq!(
/// percent_decode(b"foo%20bar%3f").decode_utf8().unwrap(),
/// "foo bar?"
/// );
/// ```
#[inline]
pub fn percent_decode(input: &[u8]) -> PercentDecode<'_> {
PercentDecode {
bytes: input.iter(),
}
}
/// The return type of [`percent_decode`].
#[derive(Clone, Debug)]
pub struct PercentDecode<'a> {
bytes: slice::Iter<'a, u8>,
}
fn after_percent_sign(iter: &mut slice::Iter<'_, u8>) -> Option<u8> {
let mut cloned_iter = iter.clone();
let h = char::from(*cloned_iter.next()?).to_digit(16)?;
let l = char::from(*cloned_iter.next()?).to_digit(16)?;
*iter = cloned_iter;
Some(h as u8 * 0x10 + l as u8)
}
impl<'a> Iterator for PercentDecode<'a> {
type Item = u8;
fn next(&mut self) -> Option<u8> {
self.bytes.next().map(|&byte| {
if byte == b'%' {
after_percent_sign(&mut self.bytes).unwrap_or(byte)
} else {
byte
}
})
}
fn size_hint(&self) -> (usize, Option<usize>) {
let bytes = self.bytes.len();
((bytes + 2) / 3, Some(bytes))
}
}
impl<'a> From<PercentDecode<'a>> for Cow<'a, [u8]> {
fn from(iter: PercentDecode<'a>) -> Self {
match iter.if_any() {
Some(vec) => Cow::Owned(vec),
None => Cow::Borrowed(iter.bytes.as_slice()),
}
}
}
impl<'a> PercentDecode<'a> {
/// If the percent-decoding is different from the input, return it as a new
/// bytes vector.
fn if_any(&self) -> Option<Vec<u8>> {
let mut bytes_iter = self.bytes.clone();
while bytes_iter.any(|&b| b == b'%') {
if let Some(decoded_byte) = after_percent_sign(&mut bytes_iter) {
let initial_bytes = self.bytes.as_slice();
let unchanged_bytes_len = initial_bytes.len() - bytes_iter.len() - 3;
let mut decoded = initial_bytes[..unchanged_bytes_len].to_owned();
decoded.push(decoded_byte);
decoded.extend(PercentDecode { bytes: bytes_iter });
return Some(decoded);
}
}
// Nothing to decode
None
}
/// Decode the result of percent-decoding as UTF-8.
///
/// This is return `Err` when the percent-decoded bytes are not well-formed
/// in UTF-8.
pub fn decode_utf8(self) -> Result<Cow<'a, str>, str::Utf8Error> {
match self.clone().into() {
Cow::Borrowed(bytes) => match str::from_utf8(bytes) {
Ok(s) => Ok(s.into()),
Err(e) => Err(e),
},
Cow::Owned(bytes) => match String::from_utf8(bytes) {
Ok(s) => Ok(s.into()),
Err(e) => Err(e.utf8_error()),
},
}
}
/// Decode the result of percent-decoding as UTF-8, lossily.
///
/// Invalid UTF-8 percent-encoded byte sequences will be replaced <20> U+FFFD,
/// the replacement character.
pub fn decode_utf8_lossy(self) -> Cow<'a, str> {
decode_utf8_lossy(self.clone().into())
}
}
fn decode_utf8_lossy(input: Cow<'_, [u8]>) -> Cow<'_, str> {
// Note: This function is duplicated in `form_urlencoded/src/query_encoding.rs`.
match input {
Cow::Borrowed(bytes) => String::from_utf8_lossy(bytes),
Cow::Owned(bytes) => {
match String::from_utf8_lossy(&bytes) {
Cow::Borrowed(utf8) => {
// If from_utf8_lossy returns a Cow::Borrowed, then we can
// be sure our original bytes were valid UTF-8. This is because
// if the bytes were invalid UTF-8 from_utf8_lossy would have
// to allocate a new owned string to back the Cow so it could
// replace invalid bytes with a placeholder.
// First we do a debug_assert to confirm our description above.
let raw_utf8: *const [u8] = utf8.as_bytes();
debug_assert!(raw_utf8 == &*bytes as *const [u8]);
// Given we know the original input bytes are valid UTF-8,
// and we have ownership of those bytes, we re-use them and
// return a Cow::Owned here.
Cow::Owned(unsafe { String::from_utf8_unchecked(bytes) })
}
Cow::Owned(s) => Cow::Owned(s),
}
}
}
}

View File

@ -0,0 +1,240 @@
/*
* meli - lib.rs
*
* Copyright 2017 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/>.
*/
//! A `ShellExpandTrait` to expand paths like a shell.
#[cfg(not(any(target_os = "netbsd", target_os = "macos")))]
use std::os::unix::io::AsRawFd;
use std::{
ffi::OsStr,
os::unix::ffi::OsStrExt,
path::{Path, PathBuf},
};
use smallvec::SmallVec;
pub trait ShellExpandTrait {
fn expand(&self) -> PathBuf;
fn complete(&self, force: bool) -> SmallVec<[String; 128]>;
}
impl ShellExpandTrait for Path {
fn expand(&self) -> PathBuf {
let mut ret = PathBuf::new();
for c in self.components() {
let c_to_str = c.as_os_str().to_str();
match c_to_str {
Some("~") => {
if let Ok(home_dir) = std::env::var("HOME") {
ret.push(home_dir)
} else {
return PathBuf::new();
}
}
Some(var) if var.starts_with('$') => {
let env_name = var.split_at(1).1;
if env_name.chars().all(char::is_uppercase) {
ret.push(std::env::var(env_name).unwrap_or_default());
} else {
ret.push(c);
}
}
Some(_) => {
ret.push(c);
}
None => {
/* path is invalid */
return PathBuf::new();
}
}
}
ret
}
#[cfg(target_os = "linux")]
fn complete(&self, force: bool) -> SmallVec<[String; 128]> {
use libc::dirent64;
use nix::fcntl::OFlag;
const BUF_SIZE: ::libc::size_t = 8 << 10;
let (prefix, _match) = if self.as_os_str().as_bytes().ends_with(b"/.") {
(self.components().as_path(), OsStr::from_bytes(b"."))
} else if self.exists() && (!force || self.as_os_str().as_bytes().ends_with(b"/")) {
return SmallVec::new();
} else {
let last_component = self
.components()
.last()
.map(|c| c.as_os_str())
.unwrap_or_else(|| OsStr::from_bytes(b""));
let prefix = if let Some(p) = self.parent() {
p
} else {
return SmallVec::new();
};
(prefix, last_component)
};
let dir = match ::nix::dir::Dir::openat(
::libc::AT_FDCWD,
prefix,
OFlag::O_DIRECTORY | OFlag::O_NOATIME | OFlag::O_RDONLY | OFlag::O_CLOEXEC,
::nix::sys::stat::Mode::S_IRUSR | ::nix::sys::stat::Mode::S_IXUSR,
)
.or_else(|_| {
::nix::dir::Dir::openat(
::libc::AT_FDCWD,
prefix,
OFlag::O_DIRECTORY | OFlag::O_RDONLY | OFlag::O_CLOEXEC,
::nix::sys::stat::Mode::S_IRUSR | ::nix::sys::stat::Mode::S_IXUSR,
)
}) {
Ok(dir) => dir,
Err(err) => {
debug!(prefix);
debug!(err);
return SmallVec::new();
}
};
let mut buf: Vec<u8> = Vec::with_capacity(BUF_SIZE);
let mut entries = SmallVec::new();
loop {
let n: i64 = unsafe {
::libc::syscall(
::libc::SYS_getdents64,
dir.as_raw_fd(),
buf.as_ptr(),
BUF_SIZE - 256,
)
};
if n < 0 {
return SmallVec::new();
} else if n == 0 {
break;
}
let n = n as usize;
unsafe {
buf.set_len(n);
}
let mut pos = 0;
while pos < n {
let dir = unsafe { std::mem::transmute::<&[u8], &[dirent64]>(&buf[pos..]) };
let entry = unsafe { std::ffi::CStr::from_ptr(dir[0].d_name.as_ptr()) };
if entry.to_bytes() != b"." && entry.to_bytes() != b".." {
if entry.to_bytes().starts_with(_match.as_bytes()) {
if dir[0].d_type == ::libc::DT_DIR && !entry.to_bytes().ends_with(b"/") {
let mut s = unsafe {
String::from_utf8_unchecked(
entry.to_bytes()[_match.as_bytes().len()..].to_vec(),
)
};
s.push('/');
entries.push(s);
} else {
entries.push(unsafe {
String::from_utf8_unchecked(
entry.to_bytes()[_match.as_bytes().len()..].to_vec(),
)
});
}
}
}
pos += dir[0].d_reclen as usize;
}
// https://github.com/romkatv/gitstatus/blob/caf44f7aaf33d0f46e6749e50595323c277e0908/src/dir.cc
// "It's tempting to bail here if n + sizeof(linux_dirent64) +
// 512 <= n. After all, there was enough space
// for another entry but SYS_getdents64 didn't write it, so this
// must be the end of the directory listing,
// right? Unfortunately, no. SYS_getdents64 is finicky.
// It sometimes writes a partial list of entries even if the
// full list would fit."
}
entries
}
#[cfg(not(target_os = "linux"))]
fn complete(&self, force: bool) -> SmallVec<[String; 128]> {
let mut entries = SmallVec::new();
let (prefix, _match) = {
if self.exists() && (!force || self.as_os_str().as_bytes().ends_with(b"/")) {
// println!("{} {:?}", self.display(), self.components().last());
return entries;
} else {
let last_component = self
.components()
.last()
.map(|c| c.as_os_str())
.unwrap_or_else(|| OsStr::from_bytes(b""));
let prefix = if let Some(p) = self.parent() {
p
} else {
return entries;
};
(prefix, last_component)
}
};
if force && self.is_dir() && !self.as_os_str().as_bytes().ends_with(b"/") {
entries.push("/".to_string());
}
if let Ok(iter) = std::fs::read_dir(&prefix) {
for entry in iter.flatten() {
if entry.path().as_os_str().as_bytes() != b"."
&& entry.path().as_os_str().as_bytes() != b".."
&& entry
.path()
.as_os_str()
.as_bytes()
.starts_with(_match.as_bytes())
{
if entry.path().is_dir() && !entry.path().as_os_str().as_bytes().ends_with(b"/")
{
let mut s = unsafe {
String::from_utf8_unchecked(
entry.path().as_os_str().as_bytes()[_match.as_bytes().len()..]
.to_vec(),
)
};
s.push('/');
entries.push(s);
} else {
entries.push(unsafe {
String::from_utf8_unchecked(
entry.path().as_os_str().as_bytes()[_match.as_bytes().len()..]
.to_vec(),
)
});
}
}
}
}
entries
}
}
#[test]
fn test_shellexpandtrait() {
assert!(Path::new("~").expand().complete(false).is_empty());
assert!(!Path::new("~").expand().complete(true).is_empty());
}

View File

@ -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());
}
}
@ -341,20 +343,20 @@ impl Composer {
melib::email::parser::generic::mailto(list_post_addr)
.map(|(_, m)| m.address)
{
to.insert(list_address);
to.extend(list_address.into_iter());
}
}
}
}
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(
@ -435,19 +439,18 @@ impl Composer {
if let Some(actions) = list_management::ListActions::detect(&parent_message) {
if let Some(post) = actions.post {
if let list_management::ListAction::Email(list_post_addr) = post[0] {
if let Ok(list_address) = melib::email::parser::generic::mailto(list_post_addr)
.map(|(_, m)| m.address)
{
let list_address_string = list_address.to_string();
if let Ok((_, mailto)) = melib::email::parser::generic::mailto(list_post_addr) {
let mut addresses = vec![(
parent_message.from()[0].clone(),
parent_message.field_from_to_string(),
)];
for add in mailto.address {
let add_s = add.to_string();
addresses.push((add, add_s));
}
ret.mode = ViewMode::SelectRecipients(UIDialog::new(
"select recipients",
vec![
(
parent_message.from()[0].clone(),
parent_message.field_from_to_string(),
),
(list_address, list_address_string),
],
addresses,
false,
Some(Box::new(move |id: ComponentId, results: &[Address]| {
Some(UIEvent::FinishedUIDialog(
@ -495,7 +498,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 +560,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 +581,7 @@ To: {}
.collect::<Vec<AutoCompleteEntry>>()
}),
));
} else if k == "From" {
} else if k == HeaderName::FROM {
self.form.push_cl((
k.into(),
headers[k].to_string(),
@ -812,10 +822,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 +1241,8 @@ impl Component for Composer {
UIEvent::FinishedUIDialog(id, ref mut result),
) if selector.id() == *id => {
if let Some(to_val) = result.downcast_mut::<String>() {
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 +1393,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 +2484,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 <some@example.com>"#
);
let raw_mail = r#"From: "some name" <some@example.com>
@ -2520,9 +2513,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 <some@example.com>"#
);
}

View File

@ -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 <user@example.com>".into());
draft.set_header(HeaderName::FROM, "user <user@example.com>".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 <user@example.com>".into());
draft.set_header(HeaderName::TO, "other user <user@example.com>".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 <user@example.com>".into())
.set_header("To", "other user <user@example.com>".into())
.set_header("Subject", "test_update()".into());
.set_header(HeaderName::FROM, "user <user@example.com>".into())
.set_header(HeaderName::TO, "other user <user@example.com>".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();
}
}

View File

@ -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()

View File

@ -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<String, String>,
pub default_header_values: HashMap<HeaderName, String>,
/// 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

View File

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

View File

@ -37,7 +37,10 @@ use melib::backends::{AccountHash, BackendEventConsumer};
use smallvec::SmallVec;
use super::*;
use crate::{jobs::JobExecutor, terminal::screen::Screen};
use crate::{
jobs::JobExecutor,
terminal::{get_events, screen::Screen},
};
struct InputHandler {
pipe: (RawFd, RawFd),

View File

@ -157,17 +157,18 @@ use std::os::unix::io::{AsRawFd, RawFd};
use nix::poll::{poll, PollFd, PollFlags};
use termion::input::TermReadEventsAndRaw;
/*
* If we fork (for example start $EDITOR) we want the input-thread to stop
* reading from stdin. The best way I came up with right now is to send a
* signal to the thread that is read in the first input in stdin after the
* fork, and then the thread kills itself. The parent process spawns a new
* input-thread when the child returns.
*
* The main loop uses try_wait_on_child() to check if child has exited.
*/
/// The thread function that listens for user input and forwards it to the main
/// event loop.
///
/// If we fork (for example start `$EDITOR`) we want the `input-thread` to stop
/// reading from stdin. The best way I came up with right now is to send a
/// signal to the thread that is read in the first input in stdin after the
/// fork, and then the thread kills itself. The parent process spawns a new
/// input-thread when the child returns.
///
/// The main loop uses [`State::try_wait_on_child`] to check if child has
/// exited.
pub fn get_events(
mut closure: impl FnMut((Key, Vec<u8>)),
rx: &Receiver<InputCommand>,