1use super::{
8 AddressList, ContentType, Ctx, Headers, MessagePart, PartKind, PartRole, Received, TextList,
9};
10use crate::{
11 DateTime, MessageBuffers,
12 decoders::{self, Limit, preview::preview_html_prefix},
13 html_to_text, preview_html, preview_text,
14 store::{MessageData, MessageEntry, MessageId, NONE, PartId, Span},
15 text_to_html,
16};
17use memchr::{memchr, memchr_iter};
18use std::{borrow::Cow, fmt};
19
20const PREVIEW_SLACK: usize = 16;
21const PREVIEW_CR_ALLOWANCE: usize = 16;
22const PREVIEW_GROWTH: usize = 4;
23const PREVIEW_HTML_RATIO: usize = 8;
24const PREVIEW_HTML_MIN: usize = 4096;
25
26#[derive(Clone)]
67pub struct Message<'x> {
68 raw: Cow<'x, [u8]>,
69 data: MessageData,
70}
71
72#[derive(Clone, Copy)]
76pub struct MessageRef<'m> {
77 ctx: Ctx<'m>,
78 id: MessageId,
79 entry: &'m MessageEntry,
80}
81
82#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
90pub enum Source {
91 Raw,
93 Decoded(PartId),
97}
98
99macro_rules! header_getters {
100 ($lt:lifetime, $target:ident, $($captures:tt)*) => {
101 pub fn subject(&self) -> Option<&$lt str> {
103 self.$target().subject()
104 }
105
106 pub fn comments(&self) -> Option<&$lt str> {
108 self.$target().comments()
109 }
110
111 pub fn mime_version(&self) -> Option<&$lt str> {
113 self.$target().mime_version()
114 }
115
116 pub fn thread_name(&self) -> Option<&$lt str> {
119 self.$target().thread_name()
120 }
121
122 pub fn from(&self) -> Option<AddressList<$lt>> {
124 self.$target().from()
125 }
126
127 pub fn to(&self) -> Option<AddressList<$lt>> {
129 self.$target().to()
130 }
131
132 pub fn cc(&self) -> Option<AddressList<$lt>> {
134 self.$target().cc()
135 }
136
137 pub fn bcc(&self) -> Option<AddressList<$lt>> {
139 self.$target().bcc()
140 }
141
142 pub fn reply_to(&self) -> Option<AddressList<$lt>> {
144 self.$target().reply_to()
145 }
146
147 pub fn sender(&self) -> Option<AddressList<$lt>> {
149 self.$target().sender()
150 }
151
152 pub fn all_from(&self) -> impl Iterator<Item = AddressList<$lt>> $($captures)* {
154 self.$target().all_from()
155 }
156
157 pub fn all_to(&self) -> impl Iterator<Item = AddressList<$lt>> $($captures)* {
159 self.$target().all_to()
160 }
161
162 pub fn all_cc(&self) -> impl Iterator<Item = AddressList<$lt>> $($captures)* {
164 self.$target().all_cc()
165 }
166
167 pub fn all_bcc(&self) -> impl Iterator<Item = AddressList<$lt>> $($captures)* {
169 self.$target().all_bcc()
170 }
171
172 pub fn resent_to(&self) -> Option<AddressList<$lt>> {
174 self.$target().resent_to()
175 }
176
177 pub fn resent_from(&self) -> Option<AddressList<$lt>> {
179 self.$target().resent_from()
180 }
181
182 pub fn resent_cc(&self) -> Option<AddressList<$lt>> {
184 self.$target().resent_cc()
185 }
186
187 pub fn resent_bcc(&self) -> Option<AddressList<$lt>> {
189 self.$target().resent_bcc()
190 }
191
192 pub fn resent_sender(&self) -> Option<AddressList<$lt>> {
194 self.$target().resent_sender()
195 }
196
197 pub fn list_archive(&self) -> Option<AddressList<$lt>> {
199 self.$target().list_archive()
200 }
201
202 pub fn list_help(&self) -> Option<AddressList<$lt>> {
204 self.$target().list_help()
205 }
206
207 pub fn list_id(&self) -> Option<AddressList<$lt>> {
209 self.$target().list_id()
210 }
211
212 pub fn list_owner(&self) -> Option<AddressList<$lt>> {
214 self.$target().list_owner()
215 }
216
217 pub fn list_post(&self) -> Option<AddressList<$lt>> {
219 self.$target().list_post()
220 }
221
222 pub fn list_subscribe(&self) -> Option<AddressList<$lt>> {
224 self.$target().list_subscribe()
225 }
226
227 pub fn list_unsubscribe(&self) -> Option<AddressList<$lt>> {
229 self.$target().list_unsubscribe()
230 }
231
232 pub fn date(&self) -> Option<DateTime> {
234 self.$target().date()
235 }
236
237 pub fn resent_date(&self) -> Option<DateTime> {
239 self.$target().resent_date()
240 }
241
242 pub fn message_id(&self) -> Option<&$lt str> {
245 self.$target().message_id()
246 }
247
248 pub fn in_reply_to(&self) -> Option<TextList<$lt>> {
250 self.$target().in_reply_to()
251 }
252
253 pub fn references(&self) -> Option<TextList<$lt>> {
255 self.$target().references()
256 }
257
258 pub fn resent_message_id(&self) -> Option<TextList<$lt>> {
260 self.$target().resent_message_id()
261 }
262
263 pub fn return_path(&self) -> Option<TextList<$lt>> {
265 self.$target().return_path()
266 }
267
268 pub fn keywords(&self) -> Option<TextList<$lt>> {
270 self.$target().keywords()
271 }
272
273 pub fn return_address(&self) -> Option<&$lt str> {
276 self.$target().return_address()
277 }
278
279 pub fn received(&self) -> Option<Received<$lt>> {
281 self.$target().received()
282 }
283
284 pub fn all_received(&self) -> impl Iterator<Item = Received<$lt>> $($captures)* {
286 self.$target().all_received()
287 }
288 };
289}
290
291macro_rules! message_getters {
292 ($lt:lifetime) => {
293 pub fn root_part(&self) -> MessagePart<$lt> {
295 self.root().root_part()
296 }
297
298 pub fn headers(&self) -> Headers<$lt> {
300 self.root().headers()
301 }
302
303 pub fn text_body(&self) -> impl ExactSizeIterator<Item = MessagePart<$lt>> {
305 self.root().text_body()
306 }
307
308 pub fn html_body(&self) -> impl ExactSizeIterator<Item = MessagePart<$lt>> {
310 self.root().html_body()
311 }
312
313 pub fn attachments(&self) -> impl ExactSizeIterator<Item = MessagePart<$lt>> {
316 self.root().attachments()
317 }
318
319 pub fn other_parts(&self) -> impl Iterator<Item = MessagePart<$lt>> {
322 self.root().other_parts()
323 }
324
325 pub fn has_attachments(&self) -> bool {
328 self.root().has_attachments()
329 }
330
331 pub fn text_bodies(&self) -> impl Iterator<Item = Cow<$lt, str>> {
334 self.root().text_bodies()
335 }
336
337 pub fn html_bodies(&self) -> impl Iterator<Item = Cow<$lt, str>> {
340 self.root().html_bodies()
341 }
342
343 pub fn body_text(&self, index: usize) -> Option<Cow<$lt, str>> {
346 self.root().body_text(index)
347 }
348
349 pub fn body_html(&self, index: usize) -> Option<Cow<$lt, str>> {
352 self.root().body_html(index)
353 }
354
355 pub fn body_preview(&self, max_len: usize) -> Option<Cow<$lt, str>> {
358 self.root().body_preview(max_len)
359 }
360
361 pub fn content_type(&self) -> Option<ContentType<$lt>> {
363 self.root().root_part().content_type()
364 }
365
366 pub fn content_disposition(&self) -> Option<ContentType<$lt>> {
368 self.root().root_part().content_disposition()
369 }
370
371 pub fn attachment_name(&self) -> Option<&$lt str> {
374 self.root().root_part().attachment_name()
375 }
376
377 pub fn is_content_type(&self, type_: &str, subtype: &str) -> bool {
380 self.root().root_part().is_content_type(type_, subtype)
381 }
382
383 header_getters!($lt, headers,);
384 };
385}
386
387impl Default for Message<'_> {
388 fn default() -> Self {
391 Message {
392 raw: Cow::Borrowed(&[]),
393 data: MessageData::empty_message(),
394 }
395 }
396}
397
398impl<'x> Message<'x> {
399 pub(crate) fn new(raw: Cow<'x, [u8]>, data: MessageData) -> Self {
400 Message { raw, data }
401 }
402
403 pub(crate) fn split_mut(&mut self) -> (&[u8], &mut MessageData) {
404 (&self.raw, &mut self.data)
405 }
406
407 fn ctx(&self) -> Ctx<'_> {
408 Ctx::new(&self.raw, &self.data)
409 }
410
411 pub fn raw(&self) -> &[u8] {
413 &self.raw
414 }
415
416 pub fn root(&self) -> MessageRef<'_> {
418 self.ctx().message(0)
419 }
420
421 pub fn messages(&self) -> impl ExactSizeIterator<Item = MessageRef<'_>> + DoubleEndedIterator {
424 let ctx = self.ctx();
425 (0..self.data.messages.len() as MessageId).map(move |id| ctx.message(id))
426 }
427
428 pub fn parts(&self) -> impl ExactSizeIterator<Item = MessagePart<'_>> + DoubleEndedIterator {
430 let ctx = self.ctx();
431 (0..self.data.parts.len() as PartId).map(move |id| ctx.part_or_empty(id))
432 }
433
434 pub fn part(&self, id: PartId) -> Option<MessagePart<'_>> {
437 self.ctx().part(id)
438 }
439
440 pub fn header_count(&self) -> usize {
442 self.data.headers.len()
443 }
444
445 pub fn part_by_boundary(&self, boundary: &str) -> Option<MessagePart<'_>> {
447 self.parts().find(|part| part.boundary() == Some(boundary))
448 }
449
450 pub fn into_owned(self) -> Message<'static> {
453 Message {
454 raw: Cow::Owned(self.raw.into_owned()),
455 data: self.data,
456 }
457 }
458
459 pub fn into_buffers(self) -> MessageBuffers {
462 let mut data = self.data;
463 data.clear();
464 MessageBuffers { data }
465 }
466
467 message_getters!('_);
468}
469
470impl<'m> MessageRef<'m> {
471 pub(crate) fn new(ctx: Ctx<'m>, id: MessageId, entry: &'m MessageEntry) -> Self {
472 MessageRef { ctx, id, entry }
473 }
474
475 fn list(
476 &self,
477 skip: u32,
478 len: u32,
479 ) -> impl ExactSizeIterator<Item = MessagePart<'m>> + use<'m> {
480 let ctx = self.ctx;
481 ctx.ids(Span {
482 start: self.entry.lists + skip,
483 len,
484 })
485 .iter()
486 .map(move |id| ctx.part_or_empty(*id))
487 }
488
489 pub fn id(&self) -> MessageId {
492 self.id
493 }
494
495 pub fn root_part(&self) -> MessagePart<'m> {
497 self.ctx.part_or_empty(self.entry.root)
498 }
499
500 pub fn container(&self) -> Option<MessagePart<'m>> {
503 (self.entry.container != NONE)
504 .then(|| self.ctx.part(self.entry.container))
505 .flatten()
506 }
507
508 pub fn source(&self) -> Source {
510 self.ctx
511 .data()
512 .source_container(self.entry.source)
513 .map_or(Source::Raw, Source::Decoded)
514 }
515
516 pub fn source_bytes(&self) -> &'m [u8] {
518 self.ctx.source_of(self.id)
519 }
520
521 pub fn raw(&self) -> &'m [u8] {
523 self.root_part().raw()
524 }
525
526 pub fn headers(&self) -> Headers<'m> {
528 self.root_part().headers()
529 }
530
531 pub fn parts(&self) -> impl Iterator<Item = MessagePart<'m>> + use<'m> {
534 let ctx = self.ctx;
535 let id = self.id;
536 let end = if self.entry.parts_end == NONE {
537 ctx.data().parts.len() as PartId
538 } else {
539 self.entry.parts_end
540 };
541 let start = if self.entry.root == NONE {
542 end
543 } else {
544 self.entry.root
545 };
546 (start..end)
547 .filter_map(move |part| ctx.part(part))
548 .filter(move |part| part.message_id() == id)
549 }
550
551 pub fn text_body(&self) -> impl ExactSizeIterator<Item = MessagePart<'m>> + use<'m> {
556 self.list(0, self.entry.text_len)
557 }
558
559 pub fn html_body(&self) -> impl ExactSizeIterator<Item = MessagePart<'m>> + use<'m> {
564 self.list(self.entry.text_len, self.entry.html_len)
565 }
566
567 pub fn attachments(&self) -> impl ExactSizeIterator<Item = MessagePart<'m>> + use<'m> {
571 self.list(
572 self.entry.text_len + self.entry.html_len,
573 self.entry.attachments_len,
574 )
575 }
576
577 pub fn has_attachments(&self) -> bool {
583 self.attachments().any(|part| {
584 !part
585 .content_disposition()
586 .is_some_and(|disposition| disposition.is_inline())
587 })
588 }
589
590 pub fn other_parts(&self) -> impl Iterator<Item = MessagePart<'m>> + use<'m> {
594 self.parts().filter(|part| part.role() == PartRole::Dropped)
595 }
596
597 fn as_text(part: MessagePart<'m>) -> Option<Cow<'m, str>> {
598 match part.kind() {
599 PartKind::Text => part.text(),
600 PartKind::Html => part.text().map(|html| Cow::Owned(html_to_text(&html))),
601 _ => None,
602 }
603 }
604
605 fn as_html(part: MessagePart<'m>) -> Option<Cow<'m, str>> {
606 match part.kind() {
607 PartKind::Html => part.text(),
608 PartKind::Text => part.text().map(|text| Cow::Owned(text_to_html(&text))),
609 _ => None,
610 }
611 }
612
613 pub fn text_bodies(&self) -> impl Iterator<Item = Cow<'m, str>> + use<'m> {
615 self.text_body().filter_map(Self::as_text)
616 }
617
618 pub fn html_bodies(&self) -> impl Iterator<Item = Cow<'m, str>> + use<'m> {
620 self.html_body().filter_map(Self::as_html)
621 }
622
623 pub fn body_text(&self, index: usize) -> Option<Cow<'m, str>> {
625 self.text_body().nth(index).and_then(Self::as_text)
626 }
627
628 pub fn body_html(&self, index: usize) -> Option<Cow<'m, str>> {
630 self.html_body().nth(index).and_then(Self::as_html)
631 }
632
633 pub fn body_preview(&self, max_len: usize) -> Option<Cow<'m, str>> {
636 if self.entry.text_len > 0 {
637 let part = self.text_body().next()?;
638 match part.kind() {
639 PartKind::Text => Self::preview_text_part(part, max_len),
640 PartKind::Html => Self::preview_html_part(part, max_len),
641 _ => None,
642 }
643 } else if self.entry.html_len > 0 {
644 let part = self.html_body().next()?;
645 match part.kind() {
646 PartKind::Html => Self::preview_html_part(part, max_len),
647 PartKind::Text => Some(Cow::Owned(preview_html(
648 &text_to_html(&part.text()?),
649 max_len,
650 ))),
651 _ => None,
652 }
653 } else {
654 None
655 }
656 }
657
658 fn preview_text_part(part: MessagePart<'m>, max_len: usize) -> Option<Cow<'m, str>> {
659 let mut bytes = max_len
660 .saturating_add(max_len / PREVIEW_CR_ALLOWANCE)
661 .saturating_add(PREVIEW_SLACK);
662 loop {
663 let prefix = part.text_prefix_state(Limit::Bytes(bytes))?;
664 if prefix.complete || exceeds_without_cr(prefix.text.as_bytes(), max_len) {
665 return Some(decoders::rewrite(prefix.text, |text| {
666 preview_text(text, max_len)
667 }));
668 }
669 bytes = bytes.saturating_mul(PREVIEW_GROWTH);
670 }
671 }
672
673 fn preview_html_part(part: MessagePart<'m>, max_len: usize) -> Option<Cow<'m, str>> {
674 let mut bytes = max_len
675 .saturating_mul(PREVIEW_HTML_RATIO)
676 .max(PREVIEW_HTML_MIN);
677 loop {
678 let prefix = part.text_prefix_state(Limit::Bytes(bytes))?;
679 if let Some(preview) = preview_html_prefix(&prefix.text, max_len, prefix.complete) {
680 return Some(Cow::Owned(preview));
681 }
682 bytes = bytes.saturating_mul(PREVIEW_GROWTH);
683 }
684 }
685
686 pub fn content_type(&self) -> Option<ContentType<'m>> {
688 self.root_part().content_type()
689 }
690
691 pub fn content_disposition(&self) -> Option<ContentType<'m>> {
693 self.root_part().content_disposition()
694 }
695
696 pub fn attachment_name(&self) -> Option<&'m str> {
699 self.root_part().attachment_name()
700 }
701
702 pub fn is_content_type(&self, type_: &str, subtype: &str) -> bool {
705 self.root_part().is_content_type(type_, subtype)
706 }
707
708 header_getters!('m, headers, + use<'m>);
709}
710
711fn exceeds_without_cr(text: &[u8], max_len: usize) -> bool {
712 let needed = max_len.saturating_add(1);
713 match text.get(..needed) {
714 Some(window) if memchr(b'\r', window).is_none() => true,
715 _ => text.len() - memchr_iter(b'\r', text).count() >= needed,
716 }
717}
718
719impl fmt::Debug for Message<'_> {
720 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
721 f.debug_struct("Message")
722 .field("len", &self.raw.len())
723 .field("messages", &self.data.messages.len())
724 .field("parts", &self.data.parts.len())
725 .field("headers", &self.data.headers.len())
726 .finish()
727 }
728}
729
730impl fmt::Debug for MessageRef<'_> {
731 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
732 f.debug_struct("MessageRef")
733 .field("id", &self.id)
734 .field("root", &self.entry.root)
735 .finish()
736 }
737}
738
739#[cfg(test)]
740mod tests {
741 use super::exceeds_without_cr;
742 use crate::scan::tests::Rng;
743 use memchr::memchr_iter;
744
745 #[test]
746 fn carriage_returns_do_not_count() {
747 let mut rng = Rng(0x5eed_cafe_f00d_0001);
748 for _ in 0..20_000 {
749 let len = rng.below(40);
750 let text: Vec<u8> = (0..len)
751 .map(|_| if rng.below(3) == 0 { b'\r' } else { b'a' })
752 .collect();
753 let max_len = rng.below(40);
754 let expected = text.len() - memchr_iter(b'\r', &text).count() > max_len;
755 assert_eq!(
756 exceeds_without_cr(&text, max_len),
757 expected,
758 "{text:?} {max_len}"
759 );
760 }
761 }
762}