use super::{
AddressList, ContentType, Ctx, Headers, MessagePart, PartKind, PartRole, Received, TextList,
};
use crate::{
DateTime, MessageBuffers,
decoders::{self, Limit, preview::preview_html_prefix},
html_to_text, preview_html, preview_text,
store::{MessageData, MessageEntry, MessageId, NONE, PartId, Span},
text_to_html,
};
use memchr::{memchr, memchr_iter};
use std::{borrow::Cow, fmt};
const PREVIEW_SLACK: usize = 16;
const PREVIEW_CR_ALLOWANCE: usize = 16;
const PREVIEW_GROWTH: usize = 4;
const PREVIEW_HTML_RATIO: usize = 8;
const PREVIEW_HTML_MIN: usize = 4096;
#[derive(Clone)]
pub struct Message<'x> {
raw: Cow<'x, [u8]>,
data: MessageData,
}
#[derive(Clone, Copy)]
pub struct MessageRef<'m> {
ctx: Ctx<'m>,
id: MessageId,
entry: &'m MessageEntry,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Source {
Raw,
Decoded(PartId),
}
macro_rules! header_getters {
($lt:lifetime, $target:ident, $($captures:tt)*) => {
pub fn subject(&self) -> Option<&$lt str> {
self.$target().subject()
}
pub fn comments(&self) -> Option<&$lt str> {
self.$target().comments()
}
pub fn mime_version(&self) -> Option<&$lt str> {
self.$target().mime_version()
}
pub fn thread_name(&self) -> Option<&$lt str> {
self.$target().thread_name()
}
pub fn from(&self) -> Option<AddressList<$lt>> {
self.$target().from()
}
pub fn to(&self) -> Option<AddressList<$lt>> {
self.$target().to()
}
pub fn cc(&self) -> Option<AddressList<$lt>> {
self.$target().cc()
}
pub fn bcc(&self) -> Option<AddressList<$lt>> {
self.$target().bcc()
}
pub fn reply_to(&self) -> Option<AddressList<$lt>> {
self.$target().reply_to()
}
pub fn sender(&self) -> Option<AddressList<$lt>> {
self.$target().sender()
}
pub fn all_from(&self) -> impl Iterator<Item = AddressList<$lt>> $($captures)* {
self.$target().all_from()
}
pub fn all_to(&self) -> impl Iterator<Item = AddressList<$lt>> $($captures)* {
self.$target().all_to()
}
pub fn all_cc(&self) -> impl Iterator<Item = AddressList<$lt>> $($captures)* {
self.$target().all_cc()
}
pub fn all_bcc(&self) -> impl Iterator<Item = AddressList<$lt>> $($captures)* {
self.$target().all_bcc()
}
pub fn resent_to(&self) -> Option<AddressList<$lt>> {
self.$target().resent_to()
}
pub fn resent_from(&self) -> Option<AddressList<$lt>> {
self.$target().resent_from()
}
pub fn resent_cc(&self) -> Option<AddressList<$lt>> {
self.$target().resent_cc()
}
pub fn resent_bcc(&self) -> Option<AddressList<$lt>> {
self.$target().resent_bcc()
}
pub fn resent_sender(&self) -> Option<AddressList<$lt>> {
self.$target().resent_sender()
}
pub fn list_archive(&self) -> Option<AddressList<$lt>> {
self.$target().list_archive()
}
pub fn list_help(&self) -> Option<AddressList<$lt>> {
self.$target().list_help()
}
pub fn list_id(&self) -> Option<AddressList<$lt>> {
self.$target().list_id()
}
pub fn list_owner(&self) -> Option<AddressList<$lt>> {
self.$target().list_owner()
}
pub fn list_post(&self) -> Option<AddressList<$lt>> {
self.$target().list_post()
}
pub fn list_subscribe(&self) -> Option<AddressList<$lt>> {
self.$target().list_subscribe()
}
pub fn list_unsubscribe(&self) -> Option<AddressList<$lt>> {
self.$target().list_unsubscribe()
}
pub fn date(&self) -> Option<DateTime> {
self.$target().date()
}
pub fn resent_date(&self) -> Option<DateTime> {
self.$target().resent_date()
}
pub fn message_id(&self) -> Option<&$lt str> {
self.$target().message_id()
}
pub fn in_reply_to(&self) -> Option<TextList<$lt>> {
self.$target().in_reply_to()
}
pub fn references(&self) -> Option<TextList<$lt>> {
self.$target().references()
}
pub fn resent_message_id(&self) -> Option<TextList<$lt>> {
self.$target().resent_message_id()
}
pub fn return_path(&self) -> Option<TextList<$lt>> {
self.$target().return_path()
}
pub fn keywords(&self) -> Option<TextList<$lt>> {
self.$target().keywords()
}
pub fn return_address(&self) -> Option<&$lt str> {
self.$target().return_address()
}
pub fn received(&self) -> Option<Received<$lt>> {
self.$target().received()
}
pub fn all_received(&self) -> impl Iterator<Item = Received<$lt>> $($captures)* {
self.$target().all_received()
}
};
}
macro_rules! message_getters {
($lt:lifetime) => {
pub fn root_part(&self) -> MessagePart<$lt> {
self.root().root_part()
}
pub fn headers(&self) -> Headers<$lt> {
self.root().headers()
}
pub fn text_body(&self) -> impl ExactSizeIterator<Item = MessagePart<$lt>> {
self.root().text_body()
}
pub fn html_body(&self) -> impl ExactSizeIterator<Item = MessagePart<$lt>> {
self.root().html_body()
}
pub fn attachments(&self) -> impl ExactSizeIterator<Item = MessagePart<$lt>> {
self.root().attachments()
}
pub fn other_parts(&self) -> impl Iterator<Item = MessagePart<$lt>> {
self.root().other_parts()
}
pub fn has_attachments(&self) -> bool {
self.root().has_attachments()
}
pub fn text_bodies(&self) -> impl Iterator<Item = Cow<$lt, str>> {
self.root().text_bodies()
}
pub fn html_bodies(&self) -> impl Iterator<Item = Cow<$lt, str>> {
self.root().html_bodies()
}
pub fn body_text(&self, index: usize) -> Option<Cow<$lt, str>> {
self.root().body_text(index)
}
pub fn body_html(&self, index: usize) -> Option<Cow<$lt, str>> {
self.root().body_html(index)
}
pub fn body_preview(&self, max_len: usize) -> Option<Cow<$lt, str>> {
self.root().body_preview(max_len)
}
pub fn content_type(&self) -> Option<ContentType<$lt>> {
self.root().root_part().content_type()
}
pub fn content_disposition(&self) -> Option<ContentType<$lt>> {
self.root().root_part().content_disposition()
}
pub fn attachment_name(&self) -> Option<&$lt str> {
self.root().root_part().attachment_name()
}
pub fn is_content_type(&self, type_: &str, subtype: &str) -> bool {
self.root().root_part().is_content_type(type_, subtype)
}
header_getters!($lt, headers,);
};
}
impl Default for Message<'_> {
fn default() -> Self {
Message {
raw: Cow::Borrowed(&[]),
data: MessageData::empty_message(),
}
}
}
impl<'x> Message<'x> {
pub(crate) fn new(raw: Cow<'x, [u8]>, data: MessageData) -> Self {
Message { raw, data }
}
pub(crate) fn split_mut(&mut self) -> (&[u8], &mut MessageData) {
(&self.raw, &mut self.data)
}
fn ctx(&self) -> Ctx<'_> {
Ctx::new(&self.raw, &self.data)
}
pub fn raw(&self) -> &[u8] {
&self.raw
}
pub fn root(&self) -> MessageRef<'_> {
self.ctx().message(0)
}
pub fn messages(&self) -> impl ExactSizeIterator<Item = MessageRef<'_>> + DoubleEndedIterator {
let ctx = self.ctx();
(0..self.data.messages.len() as MessageId).map(move |id| ctx.message(id))
}
pub fn parts(&self) -> impl ExactSizeIterator<Item = MessagePart<'_>> + DoubleEndedIterator {
let ctx = self.ctx();
(0..self.data.parts.len() as PartId).map(move |id| ctx.part_or_empty(id))
}
pub fn part(&self, id: PartId) -> Option<MessagePart<'_>> {
self.ctx().part(id)
}
pub fn header_count(&self) -> usize {
self.data.headers.len()
}
pub fn part_by_boundary(&self, boundary: &str) -> Option<MessagePart<'_>> {
self.parts().find(|part| part.boundary() == Some(boundary))
}
pub fn into_owned(self) -> Message<'static> {
Message {
raw: Cow::Owned(self.raw.into_owned()),
data: self.data,
}
}
pub fn into_buffers(self) -> MessageBuffers {
let mut data = self.data;
data.clear();
MessageBuffers { data }
}
message_getters!('_);
}
impl<'m> MessageRef<'m> {
pub(crate) fn new(ctx: Ctx<'m>, id: MessageId, entry: &'m MessageEntry) -> Self {
MessageRef { ctx, id, entry }
}
fn list(
&self,
skip: u32,
len: u32,
) -> impl ExactSizeIterator<Item = MessagePart<'m>> + use<'m> {
let ctx = self.ctx;
ctx.ids(Span {
start: self.entry.lists + skip,
len,
})
.iter()
.map(move |id| ctx.part_or_empty(*id))
}
pub fn id(&self) -> MessageId {
self.id
}
pub fn root_part(&self) -> MessagePart<'m> {
self.ctx.part_or_empty(self.entry.root)
}
pub fn container(&self) -> Option<MessagePart<'m>> {
(self.entry.container != NONE)
.then(|| self.ctx.part(self.entry.container))
.flatten()
}
pub fn source(&self) -> Source {
self.ctx
.data()
.source_container(self.entry.source)
.map_or(Source::Raw, Source::Decoded)
}
pub fn source_bytes(&self) -> &'m [u8] {
self.ctx.source_of(self.id)
}
pub fn raw(&self) -> &'m [u8] {
self.root_part().raw()
}
pub fn headers(&self) -> Headers<'m> {
self.root_part().headers()
}
pub fn parts(&self) -> impl Iterator<Item = MessagePart<'m>> + use<'m> {
let ctx = self.ctx;
let id = self.id;
let end = if self.entry.parts_end == NONE {
ctx.data().parts.len() as PartId
} else {
self.entry.parts_end
};
let start = if self.entry.root == NONE {
end
} else {
self.entry.root
};
(start..end)
.filter_map(move |part| ctx.part(part))
.filter(move |part| part.message_id() == id)
}
pub fn text_body(&self) -> impl ExactSizeIterator<Item = MessagePart<'m>> + use<'m> {
self.list(0, self.entry.text_len)
}
pub fn html_body(&self) -> impl ExactSizeIterator<Item = MessagePart<'m>> + use<'m> {
self.list(self.entry.text_len, self.entry.html_len)
}
pub fn attachments(&self) -> impl ExactSizeIterator<Item = MessagePart<'m>> + use<'m> {
self.list(
self.entry.text_len + self.entry.html_len,
self.entry.attachments_len,
)
}
pub fn has_attachments(&self) -> bool {
self.attachments().any(|part| {
!part
.content_disposition()
.is_some_and(|disposition| disposition.is_inline())
})
}
pub fn other_parts(&self) -> impl Iterator<Item = MessagePart<'m>> + use<'m> {
self.parts().filter(|part| part.role() == PartRole::Dropped)
}
fn as_text(part: MessagePart<'m>) -> Option<Cow<'m, str>> {
match part.kind() {
PartKind::Text => part.text(),
PartKind::Html => part.text().map(|html| Cow::Owned(html_to_text(&html))),
_ => None,
}
}
fn as_html(part: MessagePart<'m>) -> Option<Cow<'m, str>> {
match part.kind() {
PartKind::Html => part.text(),
PartKind::Text => part.text().map(|text| Cow::Owned(text_to_html(&text))),
_ => None,
}
}
pub fn text_bodies(&self) -> impl Iterator<Item = Cow<'m, str>> + use<'m> {
self.text_body().filter_map(Self::as_text)
}
pub fn html_bodies(&self) -> impl Iterator<Item = Cow<'m, str>> + use<'m> {
self.html_body().filter_map(Self::as_html)
}
pub fn body_text(&self, index: usize) -> Option<Cow<'m, str>> {
self.text_body().nth(index).and_then(Self::as_text)
}
pub fn body_html(&self, index: usize) -> Option<Cow<'m, str>> {
self.html_body().nth(index).and_then(Self::as_html)
}
pub fn body_preview(&self, max_len: usize) -> Option<Cow<'m, str>> {
if self.entry.text_len > 0 {
let part = self.text_body().next()?;
match part.kind() {
PartKind::Text => Self::preview_text_part(part, max_len),
PartKind::Html => Self::preview_html_part(part, max_len),
_ => None,
}
} else if self.entry.html_len > 0 {
let part = self.html_body().next()?;
match part.kind() {
PartKind::Html => Self::preview_html_part(part, max_len),
PartKind::Text => Some(Cow::Owned(preview_html(
&text_to_html(&part.text()?),
max_len,
))),
_ => None,
}
} else {
None
}
}
fn preview_text_part(part: MessagePart<'m>, max_len: usize) -> Option<Cow<'m, str>> {
let mut bytes = max_len
.saturating_add(max_len / PREVIEW_CR_ALLOWANCE)
.saturating_add(PREVIEW_SLACK);
loop {
let prefix = part.text_prefix_state(Limit::Bytes(bytes))?;
if prefix.complete || exceeds_without_cr(prefix.text.as_bytes(), max_len) {
return Some(decoders::rewrite(prefix.text, |text| {
preview_text(text, max_len)
}));
}
bytes = bytes.saturating_mul(PREVIEW_GROWTH);
}
}
fn preview_html_part(part: MessagePart<'m>, max_len: usize) -> Option<Cow<'m, str>> {
let mut bytes = max_len
.saturating_mul(PREVIEW_HTML_RATIO)
.max(PREVIEW_HTML_MIN);
loop {
let prefix = part.text_prefix_state(Limit::Bytes(bytes))?;
if let Some(preview) = preview_html_prefix(&prefix.text, max_len, prefix.complete) {
return Some(Cow::Owned(preview));
}
bytes = bytes.saturating_mul(PREVIEW_GROWTH);
}
}
pub fn content_type(&self) -> Option<ContentType<'m>> {
self.root_part().content_type()
}
pub fn content_disposition(&self) -> Option<ContentType<'m>> {
self.root_part().content_disposition()
}
pub fn attachment_name(&self) -> Option<&'m str> {
self.root_part().attachment_name()
}
pub fn is_content_type(&self, type_: &str, subtype: &str) -> bool {
self.root_part().is_content_type(type_, subtype)
}
header_getters!('m, headers, + use<'m>);
}
fn exceeds_without_cr(text: &[u8], max_len: usize) -> bool {
let needed = max_len.saturating_add(1);
match text.get(..needed) {
Some(window) if memchr(b'\r', window).is_none() => true,
_ => text.len() - memchr_iter(b'\r', text).count() >= needed,
}
}
impl fmt::Debug for Message<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Message")
.field("len", &self.raw.len())
.field("messages", &self.data.messages.len())
.field("parts", &self.data.parts.len())
.field("headers", &self.data.headers.len())
.finish()
}
}
impl fmt::Debug for MessageRef<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("MessageRef")
.field("id", &self.id)
.field("root", &self.entry.root)
.finish()
}
}
#[cfg(test)]
mod tests {
use super::exceeds_without_cr;
use crate::scan::tests::Rng;
use memchr::memchr_iter;
#[test]
fn carriage_returns_do_not_count() {
let mut rng = Rng(0x5eed_cafe_f00d_0001);
for _ in 0..20_000 {
let len = rng.below(40);
let text: Vec<u8> = (0..len)
.map(|_| if rng.below(3) == 0 { b'\r' } else { b'a' })
.collect();
let max_len = rng.below(40);
let expected = text.len() - memchr_iter(b'\r', &text).count() > max_len;
assert_eq!(
exceeds_without_cr(&text, max_len),
expected,
"{text:?} {max_len}"
);
}
}
}