use super::{encoded_words, get_header_value};
use crate::types::Address;
pub(crate) fn extract_from(headers: &[(String, String)]) -> Vec<Address> {
headers
.iter()
.filter(|(k, _)| k == "from")
.flat_map(|(_, v)| decode_address_names(parse_address_list(v)))
.collect()
}
pub(crate) fn extract_sender(headers: &[(String, String)]) -> Option<Address> {
let value = get_header_value(headers, "sender")?;
let addrs = decode_address_names(parse_address_list(&value));
addrs.into_iter().next()
}
pub(crate) fn extract_address_list(headers: &[(String, String)], name: &str) -> Vec<Address> {
headers
.iter()
.filter(|(k, _)| k == name)
.flat_map(|(_, v)| decode_address_names(parse_address_list(v)))
.collect()
}
fn decode_address_names(addrs: Vec<Address>) -> Vec<Address> {
addrs
}
pub fn parse_address_list(input: &str) -> Vec<Address> {
let mut addresses = Vec::new();
let mut current = String::new();
let mut in_quotes = false;
let mut escaped = false;
let mut angle_depth: i32 = 0;
let mut paren_depth: i32 = 0;
let mut in_group = false;
let mut in_brackets = false;
for ch in input.chars() {
if escaped {
current.push(ch);
escaped = false;
continue;
}
match ch {
'\\' if in_quotes || paren_depth > 0 => {
escaped = true;
current.push(ch);
}
'"' if paren_depth == 0 => {
in_quotes = !in_quotes;
current.push(ch);
}
'(' if !in_quotes => {
paren_depth += 1;
current.push(ch);
}
')' if !in_quotes && paren_depth > 0 => {
paren_depth -= 1;
current.push(ch);
}
'[' if !in_quotes && paren_depth == 0 => {
in_brackets = true;
current.push(ch);
}
']' if !in_quotes && paren_depth == 0 && in_brackets => {
in_brackets = false;
current.push(ch);
}
'<' if !in_quotes && paren_depth == 0 => {
angle_depth += 1;
current.push(ch);
}
'>' if !in_quotes && paren_depth == 0 && angle_depth > 0 => {
angle_depth -= 1;
current.push(ch);
}
':' if !in_quotes
&& angle_depth == 0
&& paren_depth == 0
&& !in_group
&& !in_brackets =>
{
if contains_at_outside_quotes(current.trim()) {
current.push(ch);
} else {
in_group = true;
current.clear();
}
}
';' if !in_quotes
&& angle_depth == 0
&& paren_depth == 0
&& in_group
&& !in_brackets =>
{
if let Some(addr) = parse_single_address(¤t) {
addresses.push(addr);
}
current.clear();
in_group = false;
}
',' if !in_quotes && angle_depth == 0 && paren_depth == 0 && !in_brackets => {
if let Some(addr) = parse_single_address(¤t) {
addresses.push(addr);
}
current.clear();
}
_ => current.push(ch),
}
}
if let Some(addr) = parse_single_address(¤t) {
addresses.push(addr);
}
addresses
}
pub(crate) fn parse_single_address(input: &str) -> Option<Address> {
let input = input.trim();
if input.is_empty() {
return None;
}
if let Some(angle_start) = input.rfind('<') {
if let Some(angle_end) = input.rfind('>') {
if angle_end > angle_start {
let mut email = input[angle_start + 1..angle_end].trim().to_string();
if email.starts_with('@') {
if let Some(colon) = email.find(':') {
email = email[colon + 1..].trim().to_string();
}
}
let name_part = input[..angle_start].trim();
let name = normalize_display_name_phrase(name_part);
if !email.is_empty() {
return Some(Address { name, email });
}
}
}
}
if contains_at_outside_quotes(input) {
if let Some(paren_start) = find_paren_outside_quotes(input) {
let email_part = input[..paren_start].trim();
let comment_and_rest = input[paren_start..].trim();
let name = if !email_part.is_empty() && contains_at_outside_quotes(email_part) {
extract_comment_text(comment_and_rest)
.map(|n| encoded_words::decode_encoded_words(&n))
} else if email_part.is_empty() || !contains_at_outside_quotes(email_part) {
let after_comment = strip_comments(comment_and_rest);
if contains_at_outside_quotes(after_comment.trim()) {
extract_comment_text(comment_and_rest)
.map(|n| encoded_words::decode_encoded_words(&n))
} else {
None
}
} else {
None
};
let stripped = strip_comments(input);
let email = stripped.trim().to_string();
if !email.is_empty() && contains_at_outside_quotes(&email) {
return Some(Address { name, email });
}
}
return Some(Address {
name: None,
email: input.to_string(),
});
}
None
}
pub(crate) fn extract_comment_text(s: &str) -> Option<String> {
let s = s.trim();
if !s.starts_with('(') {
return None;
}
let mut depth: u32 = 0;
let mut result = String::new();
let mut escaped = false;
let mut started = false;
for c in s.chars() {
if escaped {
escaped = false;
result.push(c);
continue;
}
match c {
'\\' => {
escaped = true;
}
'(' => {
if started {
result.push(c);
}
depth = depth.saturating_add(1);
started = true;
}
')' => {
depth = depth.saturating_sub(1);
if depth == 0 {
break;
}
result.push(c);
}
_ => {
if depth > 0 {
result.push(c);
}
}
}
}
let trimmed = result.trim().to_string();
if trimmed.is_empty() {
None
} else {
Some(trimmed)
}
}
pub(crate) fn contains_at_outside_quotes(s: &str) -> bool {
let mut in_quotes = false;
let mut paren_depth: u32 = 0;
let mut escaped = false;
for c in s.chars() {
if escaped {
escaped = false;
continue;
}
match c {
'\\' if in_quotes || paren_depth > 0 => escaped = true,
'"' if paren_depth == 0 => in_quotes = !in_quotes,
'(' if !in_quotes => paren_depth = paren_depth.saturating_add(1),
')' if !in_quotes && paren_depth > 0 => paren_depth -= 1,
'@' if !in_quotes && paren_depth == 0 => return true,
_ => {}
}
}
false
}
pub(crate) fn find_paren_outside_quotes(s: &str) -> Option<usize> {
let mut in_quotes = false;
let mut escaped = false;
for (i, c) in s.char_indices() {
if escaped {
escaped = false;
continue;
}
match c {
'\\' if in_quotes => escaped = true,
'"' => in_quotes = !in_quotes,
'(' if !in_quotes => return Some(i),
_ => {}
}
}
None
}
pub(crate) fn strip_comments(input: &str) -> String {
let mut result = String::with_capacity(input.len());
let mut depth: u32 = 0;
let mut escaped = false;
let mut in_quotes = false;
for c in input.chars() {
if escaped {
escaped = false;
if depth == 0 {
result.push(c);
}
continue;
}
if in_quotes && depth == 0 {
match c {
'\\' => {
escaped = true;
result.push(c);
}
'"' => {
in_quotes = false;
result.push(c);
}
_ => result.push(c),
}
continue;
}
match c {
'\\' => {
escaped = true;
if depth == 0 {
result.push(c);
}
}
'"' if depth == 0 => {
in_quotes = true;
result.push(c);
}
'(' => depth = depth.saturating_add(1),
')' if depth > 0 => depth = depth.saturating_sub(1),
_ if depth == 0 => result.push(c),
_ => {}
}
}
result
}
pub(crate) fn normalize_display_name_phrase(name_part: &str) -> Option<String> {
let stripped = strip_comments(name_part);
let mut segments: Vec<String> = Vec::new();
let mut raw = String::new();
let mut quoted = String::new();
let mut in_quotes = false;
let mut escaped = false;
for c in stripped.chars() {
if in_quotes {
if escaped {
quoted.push(c);
escaped = false;
continue;
}
match c {
'\\' => {
escaped = true;
quoted.push(c);
}
'"' => {
let unescaped = unescape_quoted_string("ed);
if !unescaped.is_empty() {
segments.push(unescaped);
}
quoted.clear();
in_quotes = false;
}
_ => quoted.push(c),
}
} else if c == '"' {
push_decoded_phrase_segment(&mut segments, &raw);
raw.clear();
in_quotes = true;
} else {
raw.push(c);
}
}
if in_quotes {
raw.push('"');
raw.push_str("ed);
}
push_decoded_phrase_segment(&mut segments, &raw);
if segments.is_empty() {
None
} else {
Some(segments.join(" "))
}
}
fn normalize_phrase_whitespace(input: &str) -> String {
input.split_ascii_whitespace().collect::<Vec<_>>().join(" ")
}
fn push_decoded_phrase_segment(segments: &mut Vec<String>, raw: &str) {
let normalized = normalize_phrase_whitespace(raw);
if normalized.is_empty() {
return;
}
let decoded = encoded_words::decode_encoded_words(&normalized);
let decoded = normalize_phrase_whitespace(&decoded);
if !decoded.is_empty() {
segments.push(decoded);
}
}
pub(crate) fn unescape_quoted_string(input: &str) -> String {
let mut result = String::with_capacity(input.len());
let mut chars = input.chars();
while let Some(c) = chars.next() {
if c == '\\' {
if let Some(next) = chars.next() {
result.push(next);
} else {
result.push(c);
}
} else {
result.push(c);
}
}
result
}