use std::borrow::Cow;
pub fn strip_mime_value(input: &str) -> Option<&str> {
strip_token(input).or_else(|| strip_quoted_string(input))
}
fn is_token(s: &str) -> bool {
matches!(strip_token(s), Some(s) if s.is_empty())
}
fn strip_token(input: &str) -> Option<&str> {
fn is_token(c: char) -> bool {
c.is_ascii_graphic()
&& !matches!(
c,
'(' | ')' | '<' | '>' | '@' | ',' | ';' | ':' | '\\' | '"' | '/' | '[' | ']' | '?' | '='
)
}
input
.strip_prefix(is_token)
.map(|s| s.trim_start_matches(is_token))
}
pub fn strip_cfws(input: &str) -> Option<&str> {
enum State { Fws, Comment }
let (mut s, mut state) = if let Some(s) = strip_fws(input) {
(s, State::Fws)
} else if let Some(s) = strip_comment(input) {
(s, State::Comment)
} else {
return None;
};
loop {
match state {
State::Fws => {
if let Some(snext) = strip_comment(s) {
s = snext;
state = State::Comment;
} else {
break;
}
}
State::Comment => {
if let Some(snext) = strip_fws(s) {
s = snext;
state = State::Fws;
} else if let Some(snext) = strip_comment(s) {
s = snext;
} else {
break;
}
}
}
}
Some(s)
}
fn strip_fws(input: &str) -> Option<&str> {
if let Some(s) = strip_wsp(input) {
if let Some(s) = s.strip_prefix(is_crlf) {
strip_wsp(s)
} else {
Some(s)
}
} else {
input.strip_prefix(is_crlf).and_then(strip_wsp)
}
}
fn strip_wsp(input: &str) -> Option<&str> {
input
.strip_prefix(is_wsp)
.map(|s| s.trim_start_matches(is_wsp))
}
fn strip_comment(input: &str) -> Option<&str> {
enum State { Content, Fws }
let mut s = input.strip_prefix('(')?;
let mut state = State::Content;
loop {
match state {
State::Content => {
if let Some(snext) = s.strip_prefix(')') {
s = snext;
break;
} else if let Some(snext) = strip_ccontent(s) {
s = snext;
} else if let Some(snext) = strip_fws(s) {
s = snext;
state = State::Fws;
} else {
return None;
}
}
State::Fws => {
if let Some(snext) = s.strip_prefix(')') {
s = snext;
break;
} else if let Some(snext) = strip_ccontent(s) {
s = snext;
state = State::Content;
} else {
return None;
}
}
}
}
Some(s)
}
fn strip_ccontent(input: &str) -> Option<&str> {
strip_ctext(input)
.or_else(|| strip_quoted_pair(input))
.or_else(|| strip_comment(input))
}
fn strip_ctext(input: &str) -> Option<&str> {
input.strip_prefix(is_ctext)
}
fn is_ctext(c: char) -> bool {
c.is_ascii_graphic() && !matches!(c, '(' | ')' | '\\') || !c.is_ascii()
}
pub fn is_dot_atom(s: &str) -> bool {
matches!(strip_dot_atom(s), Some(s) if s.is_empty())
}
fn strip_dot_atom(input: &str) -> Option<&str> {
let mut s = strip_atext(input)?;
while let Some(snext) = s.strip_prefix('.').and_then(strip_atext) {
s = snext;
}
Some(s)
}
fn strip_atext(input: &str) -> Option<&str> {
input
.strip_prefix(is_atext)
.map(|s| s.trim_start_matches(is_atext))
}
fn is_atext(c: char) -> bool {
c.is_ascii_alphanumeric()
|| matches!(
c,
'!' | '#' | '$' | '%' | '&' | '\'' | '*' | '+' | '-' | '/' | '=' | '?' | '^' | '_' | '`'
| '{' | '|' | '}' | '~'
)
|| !c.is_ascii()
}
pub fn is_quoted_string(s: &str) -> bool {
matches!(strip_quoted_string(s), Some(s) if s.is_empty())
}
fn strip_quoted_string(input: &str) -> Option<&str> {
enum State { Content, Fws }
let mut s = input.strip_prefix('"')?;
let mut state = State::Content;
loop {
match state {
State::Content => {
if let Some(snext) = s.strip_prefix('"') {
s = snext;
break;
} else if let Some(snext) = strip_qcontent(s) {
s = snext;
} else if let Some(snext) = strip_fws(s) {
s = snext;
state = State::Fws;
} else {
return None;
}
}
State::Fws => {
if let Some(snext) = s.strip_prefix('"') {
s = snext;
break;
} else if let Some(snext) = strip_qcontent(s) {
s = snext;
state = State::Content;
} else {
return None;
}
}
}
}
Some(s)
}
fn strip_qcontent(input: &str) -> Option<&str> {
input
.strip_prefix(is_qtext)
.or_else(|| strip_quoted_pair(input))
}
fn is_qtext(c: char) -> bool {
c.is_ascii_graphic() && !matches!(c, '"' | '\\') || !c.is_ascii()
}
fn strip_quoted_pair(input: &str) -> Option<&str> {
input
.strip_prefix('\\')
.and_then(|s| s.strip_prefix(|c| is_vchar(c) || is_wsp(c)))
}
fn is_wsp(c: char) -> bool {
matches!(c, ' ' | '\t')
}
fn is_crlf(c: char) -> bool {
c == '\n'
}
fn is_vchar(c: char) -> bool {
c.is_ascii_graphic() || !c.is_ascii()
}
pub fn escape_comment_word(s: &str) -> Cow<'_, str> {
if s.chars().all(is_ctext) {
s.into()
} else {
let mut result = String::with_capacity(s.len());
for c in s.chars() {
if is_ctext(c) {
result.push(c);
} else if is_wsp(c) || matches!(c, '(' | ')' | '\\') {
result.push('\\');
result.push(c);
} else {
result.extend(c.escape_default());
}
}
result.into()
}
}
pub fn encode_value(s: &str) -> Cow<'_, str> {
if is_dot_atom(s) {
s.into()
} else {
encode_quoted_string(s).into()
}
}
pub fn encode_mime_value(s: &str) -> Cow<'_, str> {
if is_token(s) {
s.into()
} else {
encode_quoted_string(s).into()
}
}
fn encode_quoted_string(s: &str) -> String {
let mut result = String::with_capacity(s.len() + 2);
result.push('"');
for c in s.chars() {
if is_qtext(c) || is_wsp(c) {
result.push(c);
} else if matches!(c, '"' | '\\') {
result.push('\\');
result.push(c);
} else {
result.extend(c.escape_default());
}
}
result.push('"');
result
}
pub fn decode_quoted_string(s: &str) -> String {
debug_assert!(is_quoted_string(s));
let s = &s[1..(s.len() - 1)];
let mut result = String::with_capacity(s.len());
let mut escape = false;
for c in s.chars() {
if escape {
escape = false;
result.push(c);
} else if c == '\\' {
escape = true;
} else if !is_crlf(c) {
result.push(c);
}
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn strip_mime_value_ok() {
assert_eq!(strip_mime_value("abc"), Some(""));
assert_eq!(strip_mime_value("abc<"), Some("<"));
assert_eq!(strip_mime_value(".abc."), Some(""));
assert_eq!(strip_mime_value("-_-"), Some(""));
assert_eq!(strip_mime_value("🎈"), None);
assert_eq!(strip_mime_value("\"🎈\""), Some(""));
assert_eq!(strip_mime_value("\"\\\" <\"\\"), Some("\\"));
}
#[test]
fn strip_fws_ok() {
assert_eq!(strip_fws("x"), None);
assert_eq!(strip_fws("\t"), Some(""));
assert_eq!(strip_fws("\t\tx"), Some("x"));
assert_eq!(strip_fws("\n"), None);
assert_eq!(strip_fws("\nx"), None);
assert_eq!(strip_fws("\n\t"), Some(""));
assert_eq!(strip_fws("\n\tx"), Some("x"));
assert_eq!(strip_fws("\n\t\t"), Some(""));
assert_eq!(strip_fws("\n\t\tx"), Some("x"));
assert_eq!(strip_fws("\t\n"), None);
assert_eq!(strip_fws("\t\nx"), None);
assert_eq!(strip_fws("\t\t\n\t\tx"), Some("x"));
}
#[test]
fn strip_comment_ok() {
assert_eq!(strip_comment("xy"), None);
assert_eq!(strip_comment("(abc) x"), Some(" x"));
assert_eq!(strip_comment("(ab\nc) x"), None);
assert_eq!(strip_comment("(ab\n c) x"), Some(" x"));
assert_eq!(strip_comment("(ab\n \\\\) x"), Some(" x"));
assert_eq!(strip_comment("(ab\n \\ ) x"), Some(" x"));
assert_eq!(strip_comment("(merhaba dünya) x"), Some(" x"));
assert_eq!(strip_comment("(x \n\t(\n\tx)(ab)())x"), Some("x"));
}
#[test]
fn strip_ctext_ok() {
assert_eq!(strip_ctext("x."), Some("."));
assert_eq!(strip_ctext("ß."), Some("."));
assert_eq!(strip_ctext("?."), Some("."));
assert_eq!(strip_ctext("(."), None);
assert_eq!(strip_ctext("\t."), None);
}
#[test]
fn is_dot_atom_ok() {
assert!(!is_dot_atom(""));
assert!(is_dot_atom("x"));
assert!(is_dot_atom("𝔵"));
assert!(!is_dot_atom("."));
assert!(!is_dot_atom(".."));
assert!(!is_dot_atom("x."));
assert!(!is_dot_atom(".y"));
assert!(is_dot_atom("x.y"));
assert!(!is_dot_atom(".x.y"));
assert!(!is_dot_atom("x.y."));
assert!(is_dot_atom("mail.example.org"));
assert!(is_dot_atom("mail.例子.org"));
assert!(!is_dot_atom("mx:mail.example.org"));
}
#[test]
fn strip_quoted_pair_ok() {
assert_eq!(strip_quoted_pair("x"), None);
assert_eq!(strip_quoted_pair("\\"), None);
assert_eq!(strip_quoted_pair("\\x"), Some(""));
assert_eq!(strip_quoted_pair("\\𝔵"), Some(""));
assert_eq!(strip_quoted_pair("\\xy"), Some("y"));
assert_eq!(strip_quoted_pair("\\\\"), Some(""));
assert_eq!(strip_quoted_pair("\\\\y"), Some("y"));
assert_eq!(strip_quoted_pair("\\ x"), Some("x"));
assert_eq!(strip_quoted_pair("\\(x"), Some("x"));
assert_eq!(strip_quoted_pair("\\\n"), None);
}
#[test]
fn escape_comment_word_ok() {
assert_eq!(escape_comment_word("ab c"), "ab\\ c");
assert_eq!(escape_comment_word("ab)c"), "ab\\)c");
assert_eq!(escape_comment_word("ab\"c"), "ab\"c");
assert_eq!(escape_comment_word("a我c"), "a我c");
assert_eq!(escape_comment_word("a\nc"), "a\\nc");
assert_eq!(escape_comment_word("a\x7fc"), "a\\u{7f}c");
assert_eq!(escape_comment_word("\"ab c(xy\\"), "\"ab\\ \\ c\\(xy\\\\");
}
#[test]
fn encode_mime_value_ok() {
assert_eq!(encode_mime_value("my.host"), "my.host");
assert_eq!(encode_mime_value("my.host/1244"), "\"my.host/1244\"");
assert_eq!(encode_mime_value("timed out"), "\"timed out\"");
}
#[test]
fn decode_quoted_string_ok() {
assert_eq!(
decode_quoted_string("\"a bc\\q🚀 \n \\我\\\"\""),
"a bcq🚀 我\""
);
}
}