use super::{ContentType, Ctx, Headers, MessageRef, Resolver, TextList};
use crate::{
Charset, Encoding, HeaderName,
decoders::{self, html::strip_charset_meta},
store::{KindTag, MessageId, NONE, PartEntry, PartId, flag, role},
};
use std::{borrow::Cow, fmt};
const WHOLE_TEXT_RAW_PER_BYTE: usize = 4;
const DENSE_WHOLE_TEXT_RAW_PER_BYTE: usize = 10;
const DENSITY_SAMPLE_BYTES: usize = 4096;
const DENSE_SAMPLE_BYTES_PER_ESCAPE: usize = 4;
const DENSITY_COUNT_CHUNK: usize = 64;
const _: () = assert!(DENSITY_COUNT_CHUNK <= u8::MAX as usize);
#[derive(Clone, Copy)]
pub struct MessagePart<'m> {
ctx: Ctx<'m>,
id: PartId,
entry: &'m PartEntry,
}
#[derive(Debug, Clone, Copy)]
pub enum PartKind<'m> {
Text,
Html,
Binary,
InlineBinary,
Multipart,
Message(MessageRef<'m>),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum PartRole {
Container,
TextBody,
HtmlBody,
CopiedBody,
MediaBody,
Attachment,
Dropped,
}
macro_rules! flag_set {
($set:ident) => {
impl $set {
pub fn bits(&self) -> u8 {
self.0
}
pub fn is_empty(&self) -> bool {
self.0 == 0
}
pub fn contains(&self, other: $set) -> bool {
self.0 & other.0 == other.0
}
pub fn intersects(&self, other: $set) -> bool {
self.0 & other.0 != 0
}
pub fn names(&self) -> impl Iterator<Item = &'static str> + use<> {
let flags = *self;
<$set>::NAMES
.into_iter()
.filter(move |(value, _)| flags.contains(*value))
.map(|(_, name)| name)
}
}
impl std::ops::BitOr for $set {
type Output = $set;
fn bitor(self, other: $set) -> $set {
$set(self.0 | other.0)
}
}
impl std::ops::BitOrAssign for $set {
fn bitor_assign(&mut self, other: $set) {
self.0 |= other.0;
}
}
};
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub struct PartFlags(u8);
flag_set!(PartFlags);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub struct DecodeProblems(u8);
flag_set!(DecodeProblems);
impl DecodeProblems {
pub const MALFORMED_TRANSFER_ENCODING: DecodeProblems = DecodeProblems(1);
pub const UNKNOWN_TRANSFER_ENCODING: DecodeProblems = DecodeProblems(1 << 1);
pub const UNKNOWN_CHARSET: DecodeProblems = DecodeProblems(1 << 2);
pub const MALFORMED_CHARSET: DecodeProblems = DecodeProblems(1 << 3);
pub(crate) const NAMES: [(DecodeProblems, &'static str); 4] = [
(
DecodeProblems::MALFORMED_TRANSFER_ENCODING,
"malformed_transfer_encoding",
),
(
DecodeProblems::UNKNOWN_TRANSFER_ENCODING,
"unknown_transfer_encoding",
),
(DecodeProblems::UNKNOWN_CHARSET, "unknown_charset"),
(DecodeProblems::MALFORMED_CHARSET, "malformed_charset"),
];
}
impl PartFlags {
pub const MISSING_DELIMITER: PartFlags = PartFlags(flag::MISSING_DELIMITER as u8);
pub const FALLBACK_DELIMITER: PartFlags = PartFlags(flag::FALLBACK_DELIMITER as u8);
pub const NO_BLANK_LINE: PartFlags = PartFlags(flag::NO_BLANK_LINE as u8);
pub const UNTERMINATED: PartFlags = PartFlags(flag::UNTERMINATED as u8);
pub const NESTING_LIMIT: PartFlags = PartFlags(flag::NESTING_LIMIT as u8);
pub const LIMIT_REACHED: PartFlags = PartFlags(flag::LIMIT_REACHED as u8);
pub(crate) const NAMES: [(PartFlags, &'static str); 6] = [
(PartFlags::MISSING_DELIMITER, "missing_delimiter"),
(PartFlags::FALLBACK_DELIMITER, "fallback_delimiter"),
(PartFlags::NO_BLANK_LINE, "no_blank_line"),
(PartFlags::UNTERMINATED, "unterminated"),
(PartFlags::NESTING_LIMIT, "nesting_limit"),
(PartFlags::LIMIT_REACHED, "limit_reached"),
];
}
impl<'m> MessagePart<'m> {
pub(crate) fn new(ctx: Ctx<'m>, id: PartId, entry: &'m PartEntry) -> Self {
MessagePart { ctx, id, entry }
}
#[inline]
fn resolver(&self) -> Resolver<'m> {
self.ctx.resolver(self.entry.message)
}
fn source(&self) -> &'m [u8] {
self.ctx.source_of(self.entry.message)
}
fn slice(&self, start: u32, end: u32) -> &'m [u8] {
self.source()
.get(start as usize..end as usize)
.unwrap_or_default()
}
pub fn id(&self) -> PartId {
self.id
}
pub(crate) fn message_id(&self) -> MessageId {
self.entry.message
}
pub fn message(&self) -> MessageRef<'m> {
self.ctx.message(self.entry.message)
}
pub fn parent(&self) -> Option<MessagePart<'m>> {
(self.entry.parent != NONE)
.then(|| self.ctx.part(self.entry.parent))
.flatten()
}
pub fn kind(&self) -> PartKind<'m> {
match self.entry.kind {
KindTag::Text => PartKind::Text,
KindTag::Html => PartKind::Html,
KindTag::Binary => PartKind::Binary,
KindTag::InlineBinary => PartKind::InlineBinary,
KindTag::Multipart => PartKind::Multipart,
KindTag::Message => PartKind::Message(self.ctx.message(self.entry.children.start)),
}
}
pub fn is_text(&self) -> bool {
matches!(self.entry.kind, KindTag::Text | KindTag::Html)
}
pub fn is_multipart(&self) -> bool {
self.entry.kind == KindTag::Multipart
}
pub fn is_message(&self) -> bool {
self.entry.kind == KindTag::Message
}
pub fn children(
&self,
) -> impl ExactSizeIterator<Item = MessagePart<'m>> + DoubleEndedIterator + use<'m> {
let ctx = self.ctx;
let ids = if self.entry.kind == KindTag::Multipart {
ctx.ids(self.entry.children)
} else {
&[]
};
ids.iter().map(move |id| ctx.part_or_empty(*id))
}
pub fn nested(&self) -> Option<MessageRef<'m>> {
(self.entry.kind == KindTag::Message).then(|| self.ctx.message(self.entry.children.start))
}
#[inline]
pub fn headers(&self) -> Headers<'m> {
Headers::new(
self.resolver(),
self.ctx.data(),
self.ctx
.data()
.headers
.get(self.entry.headers.range())
.unwrap_or_default(),
)
}
fn content(&self, index: u32) -> Option<ContentType<'m>> {
let data = self.ctx.data();
data.content_types
.get(index as usize)
.map(|entry| ContentType::new(self.resolver(), data, entry))
}
pub fn content_type(&self) -> Option<ContentType<'m>> {
self.content(self.entry.content_type)
}
pub fn content_disposition(&self) -> Option<ContentType<'m>> {
self.content(self.entry.content_disposition)
}
pub fn attachment_name(&self) -> Option<&'m str> {
self.content_disposition()
.and_then(|disposition| disposition.attribute("filename"))
.or_else(|| self.content_type()?.attribute("name"))
}
pub fn is_content_type(&self, type_: &str, subtype: &str) -> bool {
self.content_type().is_some_and(|content_type| {
content_type.ctype().eq_ignore_ascii_case(type_)
&& content_type
.subtype()
.is_some_and(|value| value.eq_ignore_ascii_case(subtype))
})
}
pub fn content_id(&self) -> Option<&'m str> {
self.headers().value(HeaderName::ContentId)?.as_text()
}
pub fn content_description(&self) -> Option<&'m str> {
self.headers()
.value(HeaderName::ContentDescription)?
.as_text()
}
pub fn content_location(&self) -> Option<&'m str> {
self.headers().value(HeaderName::ContentLocation)?.as_text()
}
pub fn content_language(&self) -> Option<TextList<'m>> {
self.headers()
.value(HeaderName::ContentLanguage)?
.as_text_list()
}
pub fn content_transfer_encoding(&self) -> Option<&'m str> {
self.headers()
.value(HeaderName::ContentTransferEncoding)?
.as_text()
}
pub fn encoding(&self) -> Encoding {
self.entry.encoding
}
pub fn flags(&self) -> PartFlags {
PartFlags((self.entry.flags & flag::MASK) as u8)
}
pub fn has_problems(&self) -> bool {
self.entry.flags & flag::MASK != 0
}
pub fn in_text_body(&self) -> bool {
self.entry.has_role(role::IN_TEXT_BODY)
}
pub fn in_html_body(&self) -> bool {
self.entry.has_role(role::IN_HTML_BODY)
}
pub fn is_attachment(&self) -> bool {
self.entry.has_role(role::ATTACHMENT)
}
pub fn is_inline(&self) -> bool {
self.entry.has_role(role::INLINE)
}
pub fn role(&self) -> PartRole {
let flags = self.entry.flags;
match self.entry.kind {
KindTag::Multipart => PartRole::Container,
_ if flags & role::COPIED != 0 => PartRole::CopiedBody,
kind if flags & (role::IN_TEXT_BODY | role::IN_HTML_BODY) != 0 => match kind {
KindTag::Html => PartRole::HtmlBody,
KindTag::Text => PartRole::TextBody,
_ => PartRole::MediaBody,
},
_ if flags & role::ATTACHMENT != 0 => PartRole::Attachment,
_ => PartRole::Dropped,
}
}
pub fn boundary(&self) -> Option<&'m str> {
if self.entry.kind == KindTag::Multipart {
self.content_type()?.attribute("boundary")
} else {
None
}
}
pub fn delimiter(&self) -> Option<&'m str> {
self.parent()?.boundary()
}
pub fn raw(&self) -> &'m [u8] {
self.slice(self.entry.offset_header, self.entry.offset_end)
}
pub fn raw_headers(&self) -> &'m [u8] {
self.slice(self.entry.offset_header, self.entry.offset_body)
}
pub fn raw_body(&self) -> &'m [u8] {
self.slice(self.entry.offset_body, self.entry.offset_end)
}
pub fn offset_header(&self) -> u32 {
self.entry.offset_header
}
pub fn offset_body(&self) -> u32 {
self.entry.offset_body
}
pub fn offset_end(&self) -> u32 {
self.entry.offset_end
}
pub fn decoded(&self) -> Cow<'m, [u8]> {
self.entry.encoding.decode(self.raw_body())
}
pub fn decode_into(&self, out: &mut Vec<u8>) {
self.entry.encoding.decode_append(self.raw_body(), out)
}
pub fn decoded_len(&self) -> usize {
self.entry.encoding.decoded_len(self.raw_body())
}
pub fn decoded_checked(&self) -> (Cow<'m, [u8]>, DecodeProblems) {
let (decoded, malformed) = self.entry.encoding.decode_checked(self.raw_body());
let mut problems = DecodeProblems::default();
if malformed {
problems |= DecodeProblems::MALFORMED_TRANSFER_ENCODING;
}
if !self.has_known_transfer_encoding() {
problems |= DecodeProblems::UNKNOWN_TRANSFER_ENCODING;
}
(decoded, problems)
}
pub fn has_known_transfer_encoding(&self) -> bool {
self.headers()
.get(HeaderName::ContentTransferEncoding)
.is_none_or(|header| Encoding::parse(header.raw_value()).is_some())
}
fn charset(&self) -> Option<&'m str> {
self.content_type()?.attribute("charset")
}
fn text_charset(&self) -> Charset {
self.charset()
.and_then(|label| Charset::from_label(label.as_bytes()))
.unwrap_or_default()
}
pub fn text(&self) -> Option<Cow<'m, str>> {
self.is_text()
.then(|| self.text_charset().decode_cow(self.decoded()))
}
pub fn text_checked(&self) -> Option<(Cow<'m, str>, DecodeProblems)> {
if !self.is_text() {
return None;
}
let (decoded, mut problems) = self.decoded_checked();
let charset = match self.charset() {
None => Charset::default(),
Some(label) => Charset::from_label(label.as_bytes()).unwrap_or_else(|| {
problems |= DecodeProblems::UNKNOWN_CHARSET;
Charset::default()
}),
};
if !charset.is_supported() {
problems |= DecodeProblems::UNKNOWN_CHARSET;
}
let (text, malformed) = charset.decode_cow_checked(decoded);
if malformed {
problems |= DecodeProblems::MALFORMED_CHARSET;
}
Some((text, problems))
}
pub fn text_prefix(&self, max_chars: usize) -> Option<Cow<'m, str>> {
self.text_prefix_state(decoders::Limit::Chars(max_chars))
.map(|prefix| prefix.text)
}
pub fn text_prefix_bytes(&self, max_bytes: usize) -> Option<Cow<'m, str>> {
let raw = self.raw_body();
let decode_whole = match self.entry.encoding {
Encoding::None => false,
_ if raw.len() <= max_bytes.saturating_mul(WHOLE_TEXT_RAW_PER_BYTE) => true,
_ if raw.len() > max_bytes.saturating_mul(DENSE_WHOLE_TEXT_RAW_PER_BYTE) => false,
Encoding::QuotedPrintable => {
let sample = raw.get(..DENSITY_SAMPLE_BYTES).unwrap_or(raw);
let (chunks, rest) = sample.as_chunks::<DENSITY_COUNT_CHUNK>();
let escapes = chunks
.iter()
.map(|chunk| {
usize::from(chunk.iter().map(|&byte| u8::from(byte == b'=')).sum::<u8>())
})
.sum::<usize>()
+ rest.iter().filter(|&&byte| byte == b'=').count();
escapes * DENSE_SAMPLE_BYTES_PER_ESCAPE > sample.len()
}
Encoding::Base64 => matches!(
self.text_charset(),
Charset::Utf16 | Charset::Utf16Le | Charset::Utf16Be
),
};
let text = if decode_whole {
self.text()?
} else {
self.text_prefix_state(decoders::Limit::Bytes(max_bytes))?
.text
};
let end = text.floor_char_boundary(max_bytes);
Some(decoders::truncate(text, end))
}
pub(crate) fn text_prefix_state(
&self,
limit: decoders::Limit,
) -> Option<decoders::TextPrefix<'m>> {
self.is_text().then(|| {
decoders::TextPrefix::decode(
self.raw_body(),
self.entry.encoding,
self.text_charset(),
limit,
)
})
}
pub fn text_into(&self, out: &mut String) -> bool {
if !self.is_text() {
return false;
}
self.text_charset().decode_append(&self.decoded(), out);
true
}
pub fn html_utf8(&self) -> Option<Cow<'m, str>> {
if self.entry.kind != KindTag::Html {
return None;
}
Some(decoders::rewrite(self.text()?, strip_charset_meta))
}
}
impl fmt::Debug for MessagePart<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("MessagePart")
.field("id", &self.id)
.field("kind", &self.entry.kind)
.field("offset_header", &self.entry.offset_header)
.field("offset_body", &self.entry.offset_body)
.field("offset_end", &self.entry.offset_end)
.finish()
}
}