Skip to main content

mail_parser/view/
message.rs

1/*
2 * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
3 *
4 * SPDX-License-Identifier: Apache-2.0 OR MIT
5 */
6
7use 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/// A parsed message: the raw bytes and a store of every nested message, MIME
27/// part and header field, read through small `Copy` views such as
28/// [`MessageRef`], [`MessagePart`] and [`crate::Header`]. Bodies are decoded
29/// on access.
30///
31/// A message is read-only, `Send + Sync`, and covariant in `'x`, the
32/// lifetime of the raw bytes; [`crate::MessageParser::parse_owned`] and
33/// [`Message::into_owned`] give a `Message<'static>`. Every accessor of the
34/// root [`MessageRef`] is also available here: `message.subject()` is
35/// `message.root().subject()`.
36///
37/// ```
38/// use mail_parser::{MessageParser, PartKind};
39///
40/// let raw = concat!(
41///     "From: Ann <ann@example.com>\r\n",
42///     "Subject: Report\r\n",
43///     "Content-Type: multipart/mixed; boundary=\"b\"\r\n",
44///     "\r\n",
45///     "--b\r\n",
46///     "Content-Type: text/plain; charset=utf-8\r\n",
47///     "\r\n",
48///     "See the attachment.\r\n",
49///     "--b\r\n",
50///     "Content-Type: application/pdf; name=report.pdf\r\n",
51///     "Content-Transfer-Encoding: base64\r\n",
52///     "\r\n",
53///     "JVBERi0xLjQK\r\n",
54///     "--b--\r\n",
55/// );
56/// let message = MessageParser::new().parse(raw).expect("message");
57/// assert_eq!(message.subject(), Some("Report"));
58/// let sender = message.from().and_then(|from| from.first());
59/// assert_eq!(sender.and_then(|mailbox| mailbox.name()), Some("Ann"));
60/// assert_eq!(message.body_text(0).as_deref(), Some("See the attachment."));
61/// let attachment = message.attachments().next().expect("attachment");
62/// assert_eq!(attachment.attachment_name(), Some("report.pdf"));
63/// assert!(matches!(attachment.kind(), PartKind::Binary));
64/// assert_eq!(attachment.decoded().as_ref(), b"%PDF-1.4\n");
65/// ```
66#[derive(Clone)]
67pub struct Message<'x> {
68    raw: Cow<'x, [u8]>,
69    data: MessageData,
70}
71
72/// A message inside a [`Message`]: the root message, or a message nested in
73/// a `message/rfc822` (or `message/global`) part. Each message has its own
74/// root part, header fields and body lists.
75#[derive(Clone, Copy)]
76pub struct MessageRef<'m> {
77    ctx: Ctx<'m>,
78    id: MessageId,
79    entry: &'m MessageEntry,
80}
81
82/// The buffer the offsets of a message's parts and headers refer to.
83///
84/// A nested message that was not transfer-encoded is parsed in place, so its
85/// offsets refer to the buffer of the message that contains it: the raw
86/// input, or the decoded body of an encoded ancestor. A base64 or
87/// quoted-printable encoded one is decoded during the parse and its offsets
88/// refer to that decoded body, available from [`MessageRef::source_bytes`].
89#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
90pub enum Source {
91    /// The raw input, [`Message::raw`].
92    Raw,
93    /// The transfer-decoded body of this `message/rfc822` part: the part
94    /// holding the message when the message was transfer-encoded, else the
95    /// closest transfer-encoded `message/rfc822` part it is nested in.
96    Decoded(PartId),
97}
98
99macro_rules! header_getters {
100    ($lt:lifetime, $target:ident, $($captures:tt)*) => {
101        /// The Subject field, RFC 2047 encoded words decoded.
102        pub fn subject(&self) -> Option<&$lt str> {
103            self.$target().subject()
104        }
105
106        /// The Comments field, decoded.
107        pub fn comments(&self) -> Option<&$lt str> {
108            self.$target().comments()
109        }
110
111        /// The MIME-Version field, as written.
112        pub fn mime_version(&self) -> Option<&$lt str> {
113            self.$target().mime_version()
114        }
115
116        /// The subject with reply and forward markers removed; see
117        /// [`crate::thread_name`].
118        pub fn thread_name(&self) -> Option<&$lt str> {
119            self.$target().thread_name()
120        }
121
122        /// The From field.
123        pub fn from(&self) -> Option<AddressList<$lt>> {
124            self.$target().from()
125        }
126
127        /// The To field.
128        pub fn to(&self) -> Option<AddressList<$lt>> {
129            self.$target().to()
130        }
131
132        /// The Cc field.
133        pub fn cc(&self) -> Option<AddressList<$lt>> {
134            self.$target().cc()
135        }
136
137        /// The Bcc field.
138        pub fn bcc(&self) -> Option<AddressList<$lt>> {
139            self.$target().bcc()
140        }
141
142        /// The Reply-To field.
143        pub fn reply_to(&self) -> Option<AddressList<$lt>> {
144            self.$target().reply_to()
145        }
146
147        /// The Sender field.
148        pub fn sender(&self) -> Option<AddressList<$lt>> {
149            self.$target().sender()
150        }
151
152        /// Every From field, in document order.
153        pub fn all_from(&self) -> impl Iterator<Item = AddressList<$lt>> $($captures)* {
154            self.$target().all_from()
155        }
156
157        /// Every To field, in document order.
158        pub fn all_to(&self) -> impl Iterator<Item = AddressList<$lt>> $($captures)* {
159            self.$target().all_to()
160        }
161
162        /// Every Cc field, in document order.
163        pub fn all_cc(&self) -> impl Iterator<Item = AddressList<$lt>> $($captures)* {
164            self.$target().all_cc()
165        }
166
167        /// Every Bcc field, in document order.
168        pub fn all_bcc(&self) -> impl Iterator<Item = AddressList<$lt>> $($captures)* {
169            self.$target().all_bcc()
170        }
171
172        /// The Resent-To field.
173        pub fn resent_to(&self) -> Option<AddressList<$lt>> {
174            self.$target().resent_to()
175        }
176
177        /// The Resent-From field.
178        pub fn resent_from(&self) -> Option<AddressList<$lt>> {
179            self.$target().resent_from()
180        }
181
182        /// The Resent-Cc field.
183        pub fn resent_cc(&self) -> Option<AddressList<$lt>> {
184            self.$target().resent_cc()
185        }
186
187        /// The Resent-Bcc field.
188        pub fn resent_bcc(&self) -> Option<AddressList<$lt>> {
189            self.$target().resent_bcc()
190        }
191
192        /// The Resent-Sender field.
193        pub fn resent_sender(&self) -> Option<AddressList<$lt>> {
194            self.$target().resent_sender()
195        }
196
197        /// The List-Archive field; see [`Headers::list_archive`].
198        pub fn list_archive(&self) -> Option<AddressList<$lt>> {
199            self.$target().list_archive()
200        }
201
202        /// The List-Help field; see [`Headers::list_help`].
203        pub fn list_help(&self) -> Option<AddressList<$lt>> {
204            self.$target().list_help()
205        }
206
207        /// The List-ID field; see [`Headers::list_id`].
208        pub fn list_id(&self) -> Option<AddressList<$lt>> {
209            self.$target().list_id()
210        }
211
212        /// The List-Owner field; see [`Headers::list_owner`].
213        pub fn list_owner(&self) -> Option<AddressList<$lt>> {
214            self.$target().list_owner()
215        }
216
217        /// The List-Post field; see [`Headers::list_post`].
218        pub fn list_post(&self) -> Option<AddressList<$lt>> {
219            self.$target().list_post()
220        }
221
222        /// The List-Subscribe field; see [`Headers::list_subscribe`].
223        pub fn list_subscribe(&self) -> Option<AddressList<$lt>> {
224            self.$target().list_subscribe()
225        }
226
227        /// The List-Unsubscribe field; see [`Headers::list_unsubscribe`].
228        pub fn list_unsubscribe(&self) -> Option<AddressList<$lt>> {
229            self.$target().list_unsubscribe()
230        }
231
232        /// The Date field.
233        pub fn date(&self) -> Option<DateTime> {
234            self.$target().date()
235        }
236
237        /// The Resent-Date field.
238        pub fn resent_date(&self) -> Option<DateTime> {
239            self.$target().resent_date()
240        }
241
242        /// The last identifier of the Message-ID field, without angle
243        /// brackets.
244        pub fn message_id(&self) -> Option<&$lt str> {
245            self.$target().message_id()
246        }
247
248        /// The identifiers of the In-Reply-To field.
249        pub fn in_reply_to(&self) -> Option<TextList<$lt>> {
250            self.$target().in_reply_to()
251        }
252
253        /// The identifiers of the References field.
254        pub fn references(&self) -> Option<TextList<$lt>> {
255            self.$target().references()
256        }
257
258        /// The identifiers of the Resent-Message-ID field.
259        pub fn resent_message_id(&self) -> Option<TextList<$lt>> {
260            self.$target().resent_message_id()
261        }
262
263        /// The Return-Path field; see [`Headers::return_path`].
264        pub fn return_path(&self) -> Option<TextList<$lt>> {
265            self.$target().return_path()
266        }
267
268        /// The phrases of the Keywords field.
269        pub fn keywords(&self) -> Option<TextList<$lt>> {
270            self.$target().keywords()
271        }
272
273        /// The last address of Return-Path, or else the first address of
274        /// From.
275        pub fn return_address(&self) -> Option<&$lt str> {
276            self.$target().return_address()
277        }
278
279        /// The last Received field (the oldest hop).
280        pub fn received(&self) -> Option<Received<$lt>> {
281            self.$target().received()
282        }
283
284        /// Every Received field, in document order (newest hop first).
285        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        /// The root part of the root message; see [`MessageRef::root_part`].
294        pub fn root_part(&self) -> MessagePart<$lt> {
295            self.root().root_part()
296        }
297
298        /// The header fields of the root message.
299        pub fn headers(&self) -> Headers<$lt> {
300            self.root().headers()
301        }
302
303        /// The text body of the root message; see [`MessageRef::text_body`].
304        pub fn text_body(&self) -> impl ExactSizeIterator<Item = MessagePart<$lt>> {
305            self.root().text_body()
306        }
307
308        /// The HTML body of the root message; see [`MessageRef::html_body`].
309        pub fn html_body(&self) -> impl ExactSizeIterator<Item = MessagePart<$lt>> {
310            self.root().html_body()
311        }
312
313        /// The attachments of the root message; see
314        /// [`MessageRef::attachments`].
315        pub fn attachments(&self) -> impl ExactSizeIterator<Item = MessagePart<$lt>> {
316            self.root().attachments()
317        }
318
319        /// The parts of the root message in no list; see
320        /// [`MessageRef::other_parts`].
321        pub fn other_parts(&self) -> impl Iterator<Item = MessagePart<$lt>> {
322            self.root().other_parts()
323        }
324
325        /// Whether the root message has attachments; see
326        /// [`MessageRef::has_attachments`].
327        pub fn has_attachments(&self) -> bool {
328            self.root().has_attachments()
329        }
330
331        /// The text body of the root message as text; see
332        /// [`MessageRef::text_bodies`].
333        pub fn text_bodies(&self) -> impl Iterator<Item = Cow<$lt, str>> {
334            self.root().text_bodies()
335        }
336
337        /// The HTML body of the root message as HTML; see
338        /// [`MessageRef::html_bodies`].
339        pub fn html_bodies(&self) -> impl Iterator<Item = Cow<$lt, str>> {
340            self.root().html_bodies()
341        }
342
343        /// The text of the `index`-th part of the root text body; see
344        /// [`MessageRef::body_text`].
345        pub fn body_text(&self, index: usize) -> Option<Cow<$lt, str>> {
346            self.root().body_text(index)
347        }
348
349        /// The HTML of the `index`-th part of the root HTML body; see
350        /// [`MessageRef::body_html`].
351        pub fn body_html(&self, index: usize) -> Option<Cow<$lt, str>> {
352            self.root().body_html(index)
353        }
354
355        /// A preview of the root message's body; see
356        /// [`MessageRef::body_preview`].
357        pub fn body_preview(&self, max_len: usize) -> Option<Cow<$lt, str>> {
358            self.root().body_preview(max_len)
359        }
360
361        /// The Content-Type of the root part.
362        pub fn content_type(&self) -> Option<ContentType<$lt>> {
363            self.root().root_part().content_type()
364        }
365
366        /// The Content-Disposition of the root part.
367        pub fn content_disposition(&self) -> Option<ContentType<$lt>> {
368            self.root().root_part().content_disposition()
369        }
370
371        /// The attachment name of the root part; see
372        /// [`MessagePart::attachment_name`].
373        pub fn attachment_name(&self) -> Option<&$lt str> {
374            self.root().root_part().attachment_name()
375        }
376
377        /// Whether the Content-Type of the root part is `type_/subtype`
378        /// (ASCII case-insensitive).
379        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    /// An empty message: no bytes, and a root part with no header and an
389    /// empty body, in no body list.
390    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    /// The raw input.
412    pub fn raw(&self) -> &[u8] {
413        &self.raw
414    }
415
416    /// The top-level message, id 0.
417    pub fn root(&self) -> MessageRef<'_> {
418        self.ctx().message(0)
419    }
420
421    /// Every message, the root first, then nested messages in document
422    /// order.
423    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    /// Every part of every message, in document order.
429    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    /// The part with this id; ids number every part of every message in
435    /// document order.
436    pub fn part(&self, id: PartId) -> Option<MessagePart<'_>> {
437        self.ctx().part(id)
438    }
439
440    /// The number of header fields of every part of every message.
441    pub fn header_count(&self) -> usize {
442        self.data.headers.len()
443    }
444
445    /// The multipart part whose `boundary` parameter is `boundary`
446    pub fn part_by_boundary(&self, boundary: &str) -> Option<MessagePart<'_>> {
447        self.parts().find(|part| part.boundary() == Some(boundary))
448    }
449
450    /// Copies borrowed input so the message owns its bytes. Only the raw
451    /// bytes are copied; the parsed store is moved.
452    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    /// Drops the message and keeps its storage, cleared, for the next
460    /// [`crate::MessageParser::parse_with`]; see [`MessageBuffers`].
461    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    /// The id of this message: 0 for the root, then nested messages in
490    /// document order.
491    pub fn id(&self) -> MessageId {
492        self.id
493    }
494
495    /// The root part: the header block of this message and its body.
496    pub fn root_part(&self) -> MessagePart<'m> {
497        self.ctx.part_or_empty(self.entry.root)
498    }
499
500    /// The `message/rfc822` part holding this message; `None` for the root
501    /// message.
502    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    /// The buffer this message's offsets refer to.
509    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    /// The bytes of the buffer this message's offsets refer to.
517    pub fn source_bytes(&self) -> &'m [u8] {
518        self.ctx.source_of(self.id)
519    }
520
521    /// The bytes of this message, from its first header to its end.
522    pub fn raw(&self) -> &'m [u8] {
523        self.root_part().raw()
524    }
525
526    /// The header fields of this message: those of its root part.
527    pub fn headers(&self) -> Headers<'m> {
528        self.root_part().headers()
529    }
530
531    /// The parts of this message (nested messages excluded), in document
532    /// order.
533    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    /// The parts that make up the text body (RFC 8621 `textBody`): text
552    /// parts, HTML parts standing in for a missing text version, and inline
553    /// media, in reading order. [`MessageRef::text_bodies`] gives them as
554    /// text.
555    pub fn text_body(&self) -> impl ExactSizeIterator<Item = MessagePart<'m>> + use<'m> {
556        self.list(0, self.entry.text_len)
557    }
558
559    /// The parts that make up the HTML body (RFC 8621 `htmlBody`): HTML
560    /// parts, text parts standing in for a missing HTML version, and inline
561    /// media, in reading order. [`MessageRef::html_bodies`] gives them as
562    /// HTML.
563    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    /// The attachments (RFC 8621 `attachments`): the leaf parts, nested
568    /// messages included, that are not shown as part of the body, plus inline
569    /// media shown in only one of the two bodies.
570    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    /// Whether a client should offer this message as having attachments:
578    /// RFC 8621 `hasAttachment`, true when [`MessageRef::attachments`] holds
579    /// a part without `Content-Disposition: inline`. Images referenced by
580    /// `cid:` count unless they are marked inline; the RFC's optional
581    /// exception for embedded images is not applied.
582    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    /// The parts of this message that are in no list (issue #107):
591    /// containers excluded, nested messages excluded. For a message from
592    /// [`crate::MessageParser::parse_headers`] this is the root part.
593    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    /// The text body as text: HTML parts converted, non-text parts skipped.
614    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    /// The HTML body as HTML: text parts converted, non-text parts skipped.
619    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    /// The text of the `index`-th part of the text body.
624    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    /// The HTML of the `index`-th part of the HTML body.
629    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    /// A preview of at most `max_len` bytes of the first text body, else of
634    /// the first HTML body, without carriage returns; see [`preview_text`].
635    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    /// The Content-Type of the root part.
687    pub fn content_type(&self) -> Option<ContentType<'m>> {
688        self.root_part().content_type()
689    }
690
691    /// The Content-Disposition of the root part.
692    pub fn content_disposition(&self) -> Option<ContentType<'m>> {
693        self.root_part().content_disposition()
694    }
695
696    /// The attachment name of the root part; see
697    /// [`MessagePart::attachment_name`].
698    pub fn attachment_name(&self) -> Option<&'m str> {
699        self.root_part().attachment_name()
700    }
701
702    /// Whether the Content-Type of the root part is `type_/subtype` (ASCII
703    /// case-insensitive).
704    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}