use super::{encoded_words, get_header_value, params, LENIENT_BASE64, MAX_MIME_DEPTH};
use crate::types::ParsedAttachment;
use super::super::wire;
use base64::Engine as _;
fn merge_mime_results(
body_text: &mut Option<String>,
body_html: &mut Option<String>,
attachments: &mut Vec<ParsedAttachment>,
text: Option<String>,
html: Option<String>,
more_attachments: Vec<ParsedAttachment>,
is_alternative: bool,
) {
if (is_alternative || body_text.is_none()) && text.is_some() {
*body_text = text;
}
if (is_alternative || body_html.is_none()) && html.is_some() {
*body_html = html;
}
attachments.extend(more_attachments);
}
#[allow(clippy::too_many_lines)]
pub(crate) fn walk_mime_tree(
body: &[u8],
boundary: &str,
section_prefix: &str,
depth: u32,
is_digest: bool,
is_alternative: bool,
) -> (Option<String>, Option<String>, Vec<ParsedAttachment>) {
if depth > MAX_MIME_DEPTH {
return (None, None, Vec::new());
}
let parts = wire::split_mime_parts(body, boundary);
let mut body_text: Option<String> = None;
let mut body_html: Option<String> = None;
let mut attachments: Vec<ParsedAttachment> = Vec::new();
for (i, part) in parts.iter().enumerate() {
let section_num = i + 1;
let section = if section_prefix.is_empty() {
section_num.to_string()
} else {
format!("{section_prefix}.{section_num}")
};
let (part_header_bytes, part_body) = wire::split_header_body(part);
let part_headers = wire::parse_headers(part_header_bytes);
let default_ct = if is_digest {
"message/rfc822"
} else {
"text/plain; charset=us-ascii"
};
let ct = get_header_value(&part_headers, "content-type")
.unwrap_or_else(|| default_ct.to_string());
let cte = get_header_value(&part_headers, "content-transfer-encoding")
.unwrap_or_else(|| "7bit".to_string());
let cd = get_header_value(&part_headers, "content-disposition").unwrap_or_default();
let content_id = get_header_value(&part_headers, "content-id");
let filename = params::extract_filename(&cd, &ct);
let has_attachment_filename = filename.is_some();
if params::is_multipart(&ct) {
if let Some(inner_boundary) = params::extract_boundary_for_body(&ct, part_body) {
let inner_mime = params::extract_mime_type(&ct);
let inner_digest = inner_mime == "multipart/digest";
let inner_alternative = inner_mime == "multipart/alternative";
let (t, h, a) = walk_mime_tree(
part_body,
&inner_boundary,
§ion,
depth + 1,
inner_digest,
inner_alternative,
);
merge_mime_results(
&mut body_text,
&mut body_html,
&mut attachments,
t,
h,
a,
is_alternative,
);
} else {
let (t, h, a) = extract_simple_body_with_section(
part_body,
"text/plain; charset=us-ascii",
&cte,
&cd,
content_id.as_deref(),
§ion,
);
merge_mime_results(
&mut body_text,
&mut body_html,
&mut attachments,
t,
h,
a,
is_alternative,
);
}
} else {
let mime_type = params::extract_mime_type(&ct);
let is_explicit_attachment = params::is_disposition_type(&cd, "attachment");
let is_explicit_inline = params::is_disposition_type(&cd, "inline");
if !is_explicit_attachment
&& (!has_attachment_filename || is_explicit_inline)
&& mime_type == "text/plain"
&& (is_alternative || body_text.is_none())
{
let decoded = decode_body(part_body, &cte, &ct);
if !decoded.is_empty() {
body_text = Some(decoded);
}
} else if !is_explicit_attachment
&& (!has_attachment_filename || is_explicit_inline)
&& mime_type == "text/html"
&& (is_alternative || body_html.is_none())
{
let decoded = decode_body(part_body, &cte, &ct);
if !decoded.is_empty() {
body_html = Some(decoded);
}
} else if !mime_type.starts_with("multipart/") {
let is_inline = params::is_disposition_type(&cd, "inline")
|| (!is_explicit_attachment && content_id.is_some());
attachments.push(ParsedAttachment {
filename,
content_type: mime_type,
content_id: content_id
.map(|s| s.trim_matches(|c| c == '<' || c == '>').trim().to_string()),
is_inline,
size: Some(part_body.len() as u64),
section: Some(section),
});
}
}
}
(body_text, body_html, attachments)
}
pub(crate) fn extract_simple_body(
body: &[u8],
content_type: &str,
transfer_encoding: &str,
content_disposition: &str,
content_id: Option<&str>,
) -> (Option<String>, Option<String>, Vec<ParsedAttachment>) {
extract_simple_body_with_section(
body,
content_type,
transfer_encoding,
content_disposition,
content_id,
"1",
)
}
fn extract_simple_body_with_section(
body: &[u8],
content_type: &str,
transfer_encoding: &str,
content_disposition: &str,
content_id: Option<&str>,
section: &str,
) -> (Option<String>, Option<String>, Vec<ParsedAttachment>) {
let mime_type = params::extract_mime_type(content_type);
let is_explicit_attachment = params::is_disposition_type(content_disposition, "attachment");
let is_explicit_inline = params::is_disposition_type(content_disposition, "inline");
let filename = params::extract_filename(content_disposition, content_type);
let has_attachment_filename = filename.is_some();
if is_explicit_attachment
|| (has_attachment_filename && !is_explicit_inline)
|| (mime_type != "text/plain" && mime_type != "text/html")
{
let is_inline = params::is_disposition_type(content_disposition, "inline")
|| (!is_explicit_attachment && content_id.is_some());
let attachment = ParsedAttachment {
filename,
content_type: mime_type,
content_id: content_id
.map(|s| s.trim_matches(|c| c == '<' || c == '>').trim().to_string()),
is_inline,
size: Some(body.len() as u64),
section: Some(section.to_string()),
};
return (None, None, vec![attachment]);
}
if body.is_empty() {
return (None, None, Vec::new());
}
let text = decode_body(body, transfer_encoding, content_type);
if text.is_empty() {
return (None, None, Vec::new());
}
if mime_type == "text/html" {
(None, Some(text), Vec::new())
} else {
(Some(text), None, Vec::new())
}
}
pub(crate) fn decode_body(data: &[u8], transfer_encoding: &str, content_type: &str) -> String {
let decoded = decode_transfer_encoding(data, transfer_encoding);
let charset = params::extract_rfc2231_param(content_type, "charset")
.or_else(|| params::extract_rfc2231_continuation(content_type, "charset"))
.or_else(|| params::extract_param(content_type, "charset"))
.unwrap_or_else(|| "us-ascii".to_string());
let text = encoded_words::decode_charset(&charset, &decoded);
if let Some(stripped) = text.strip_suffix("\r\n") {
stripped.to_string()
} else if let Some(stripped) = text.strip_suffix('\n') {
stripped.to_string()
} else if let Some(stripped) = text.strip_suffix('\r') {
stripped.to_string()
} else {
text
}
}
pub(crate) fn decode_transfer_encoding(data: &[u8], encoding: &str) -> Vec<u8> {
let normalized = encoding.trim().to_ascii_lowercase();
let token_end = normalized
.find(|c: char| c == ';' || c == '(' || c.is_ascii_whitespace())
.unwrap_or(normalized.len());
let normalized = normalized[..token_end]
.strip_prefix('"')
.and_then(|s| s.strip_suffix('"'))
.unwrap_or(&normalized[..token_end]);
match normalized {
"base64" => {
let cleaned: Vec<u8> = data
.iter()
.copied()
.filter(|b| b.is_ascii_alphanumeric() || *b == b'+' || *b == b'/' || *b == b'=')
.collect();
LENIENT_BASE64
.decode(&cleaned)
.unwrap_or_else(|_| data.to_vec())
}
"quoted-printable" => decode_quoted_printable(data),
_ => data.to_vec(),
}
}
pub(crate) fn decode_quoted_printable(data: &[u8]) -> Vec<u8> {
let mut result = Vec::with_capacity(data.len());
let mut pending_wsp: Vec<u8> = Vec::new();
let mut i = 0;
while i < data.len() {
if data[i] == b'=' {
if i + 2 < data.len() {
if data[i + 1] == b'\r' && data[i + 2] == b'\n' {
result.append(&mut pending_wsp);
i += 3;
continue;
}
if data[i + 1] == b'\n' {
result.append(&mut pending_wsp);
i += 2;
continue;
}
if data[i + 1] == b'\r' && data[i + 2] != b'\n' {
result.append(&mut pending_wsp);
i += 2;
continue;
}
if let Some(val) = params::decode_hex_pair(data[i + 1], data[i + 2]) {
result.append(&mut pending_wsp);
result.push(val);
i += 3;
continue;
}
} else if i + 1 < data.len() && data[i + 1] == b'\n' {
result.append(&mut pending_wsp);
i += 2;
continue;
} else if i + 1 < data.len() && data[i + 1] == b'\r' {
result.append(&mut pending_wsp);
i += 2;
continue;
} else if i + 1 == data.len() {
break;
}
result.append(&mut pending_wsp);
}
if data[i] == b'\r' && i + 1 < data.len() && data[i + 1] == b'\n' {
pending_wsp.clear();
result.push(b'\r');
result.push(b'\n');
i += 2;
continue;
}
if data[i] == b'\n' {
pending_wsp.clear();
result.push(b'\n');
i += 1;
continue;
}
if data[i] == b' ' || data[i] == b'\t' {
pending_wsp.push(data[i]);
i += 1;
continue;
}
result.append(&mut pending_wsp);
result.push(data[i]);
i += 1;
}
result
}