Skip to main content

mail_parser/view/
part.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::{ContentType, Ctx, Headers, MessageRef, Resolver, TextList};
8use crate::{
9    Charset, Encoding, HeaderName,
10    decoders::{self, html::strip_charset_meta},
11    store::{KindTag, MessageId, NONE, PartEntry, PartId, flag, role},
12};
13use std::{borrow::Cow, fmt};
14
15const WHOLE_TEXT_RAW_PER_BYTE: usize = 4;
16const DENSE_WHOLE_TEXT_RAW_PER_BYTE: usize = 10;
17const DENSITY_SAMPLE_BYTES: usize = 4096;
18const DENSE_SAMPLE_BYTES_PER_ESCAPE: usize = 4;
19const DENSITY_COUNT_CHUNK: usize = 64;
20const _: () = assert!(DENSITY_COUNT_CHUNK <= u8::MAX as usize);
21
22/// A MIME part: a header block and a body. Parts are numbered in document
23/// order across nested messages ([`MessagePart::id`]), and every message has
24/// a root part holding its own header fields.
25///
26/// Bodies are decoded on access: [`MessagePart::decoded`] for the transfer
27/// encoding, [`MessagePart::text`] for text parts (transfer encoding and
28/// charset), [`MessagePart::text_prefix`] and [`MessagePart::decoded_len`]
29/// to avoid decoding the whole body.
30#[derive(Clone, Copy)]
31pub struct MessagePart<'m> {
32    ctx: Ctx<'m>,
33    id: PartId,
34    entry: &'m PartEntry,
35}
36
37/// What a part contains.
38#[derive(Debug, Clone, Copy)]
39pub enum PartKind<'m> {
40    /// A text part (any `text/*` except HTML, or a part without type). Also
41    /// a `message/rfc822` part that is not transfer-encoded and whose nested
42    /// message was empty or had no header block (flagged
43    /// [`PartFlags::NO_BLANK_LINE`]), or was cut by the part limit
44    /// ([`PartFlags::LIMIT_REACHED`]).
45    Text,
46    /// A `text/html` part.
47    Html,
48    /// A non-text part. Also a `message/rfc822` part that was not parsed:
49    /// beyond the depth limit, or transfer-encoded and beyond the encoded
50    /// nesting limit, with encoded nested parsing turned off, or holding a
51    /// message that was empty or cut by the part limit.
52    Binary,
53    /// A non-text part marked to be rendered inline; see
54    /// [`MessagePart::is_inline`].
55    InlineBinary,
56    /// A multipart container; see [`MessagePart::children`].
57    Multipart,
58    /// A nested `message/rfc822` or `message/global`.
59    Message(MessageRef<'m>),
60}
61
62/// How the body classification used a part (issue #107). Every part has
63/// exactly one role; the roles of a message's parts partition them.
64///
65/// The role is a summary of the list memberships
66/// ([`MessagePart::in_text_body`], [`MessagePart::in_html_body`],
67/// [`MessagePart::is_attachment`]). [`PartRole::CopiedBody`] takes priority
68/// over [`PartRole::TextBody`] and [`PartRole::HtmlBody`]: a text or HTML
69/// part copied into the other list is in both lists.
70#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
71pub enum PartRole {
72    /// A multipart; its children have their own roles.
73    Container,
74    /// A text part of the body lists.
75    TextBody,
76    /// An HTML part of the body lists.
77    HtmlBody,
78    /// A body part of a `multipart/alternative` that had no version of the
79    /// other type, copied into the other body list as its stand-in; it is in
80    /// both body lists.
81    CopiedBody,
82    /// An image, audio or video part displayed in the body lists (it may
83    /// also be an attachment; see [`MessageRef::attachments`]).
84    MediaBody,
85    /// A part offered as an attachment and in no body list.
86    Attachment,
87    /// A part in no list.
88    Dropped,
89}
90
91macro_rules! flag_set {
92    ($set:ident) => {
93        impl $set {
94            /// The flags as a bit mask.
95            pub fn bits(&self) -> u8 {
96                self.0
97            }
98
99            /// Whether no flag is set.
100            pub fn is_empty(&self) -> bool {
101                self.0 == 0
102            }
103
104            /// Whether every flag of `other` is set.
105            pub fn contains(&self, other: $set) -> bool {
106                self.0 & other.0 == other.0
107            }
108
109            /// Whether at least one flag of `other` is set.
110            pub fn intersects(&self, other: $set) -> bool {
111                self.0 & other.0 != 0
112            }
113
114            /// Names of the flags that are set, in bit order.
115            pub fn names(&self) -> impl Iterator<Item = &'static str> + use<> {
116                let flags = *self;
117                <$set>::NAMES
118                    .into_iter()
119                    .filter(move |(value, _)| flags.contains(*value))
120                    .map(|(_, name)| name)
121            }
122        }
123
124        impl std::ops::BitOr for $set {
125            type Output = $set;
126
127            fn bitor(self, other: $set) -> $set {
128                $set(self.0 | other.0)
129            }
130        }
131
132        impl std::ops::BitOrAssign for $set {
133            fn bitor_assign(&mut self, other: $set) {
134                self.0 |= other.0;
135            }
136        }
137    };
138}
139
140/// What happened while a part was parsed.
141#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
142pub struct PartFlags(u8);
143
144flag_set!(PartFlags);
145
146/// Problems found while decoding a body, returned by
147/// [`MessagePart::decoded_checked`] and [`MessagePart::text_checked`]. A
148/// non-empty set is what RFC 8621 calls `isEncodingProblem`.
149#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
150pub struct DecodeProblems(u8);
151
152flag_set!(DecodeProblems);
153
154impl DecodeProblems {
155    /// The transfer encoding had malformed sections: bytes outside the
156    /// base64 alphabet were skipped, or malformed quoted-printable escapes
157    /// were kept as written.
158    pub const MALFORMED_TRANSFER_ENCODING: DecodeProblems = DecodeProblems(1);
159    /// The Content-Transfer-Encoding is none of `7bit`, `8bit`, `binary`,
160    /// `base64` and `quoted-printable`; the body was left as written.
161    pub const UNKNOWN_TRANSFER_ENCODING: DecodeProblems = DecodeProblems(1 << 1);
162    /// The charset is unknown, or not supported by this build (the
163    /// multi-byte charsets need the `full_encoding` feature); the text was
164    /// decoded as UTF-8.
165    pub const UNKNOWN_CHARSET: DecodeProblems = DecodeProblems(1 << 2);
166    /// Byte sequences that are not valid in the charset were replaced by
167    /// U+FFFD. For UTF-7 also: 8-bit bytes were read as Latin-1, or an
168    /// unterminated shift sequence at the end was dropped.
169    pub const MALFORMED_CHARSET: DecodeProblems = DecodeProblems(1 << 3);
170
171    pub(crate) const NAMES: [(DecodeProblems, &'static str); 4] = [
172        (
173            DecodeProblems::MALFORMED_TRANSFER_ENCODING,
174            "malformed_transfer_encoding",
175        ),
176        (
177            DecodeProblems::UNKNOWN_TRANSFER_ENCODING,
178            "unknown_transfer_encoding",
179        ),
180        (DecodeProblems::UNKNOWN_CHARSET, "unknown_charset"),
181        (DecodeProblems::MALFORMED_CHARSET, "malformed_charset"),
182    ];
183}
184
185impl PartFlags {
186    /// No delimiter of the enclosing multipart ended this part; it runs to
187    /// the parent's next delimiter or to the end of the input. On a
188    /// multipart: its first delimiter was its close delimiter, so it has no
189    /// children and its content is the preamble in
190    /// [`MessagePart::raw_body`].
191    pub const MISSING_DELIMITER: PartFlags = PartFlags(flag::MISSING_DELIMITER as u8);
192    /// The part ended at a delimiter found in the middle of a line. On a
193    /// multipart: its first delimiter was found in the middle of a line,
194    /// because no delimiter at the start of a line opened it (only its close
195    /// delimiter, or a delimiter of an enclosing multipart, was found).
196    pub const FALLBACK_DELIMITER: PartFlags = PartFlags(flag::FALLBACK_DELIMITER as u8);
197    /// The header block did not end with a blank line: it runs to the end of
198    /// the input, or a delimiter cut it, and the body is empty. On a
199    /// `message/rfc822` part that is not [`PartKind::Message`]: the nested
200    /// message was empty or had no header block, so the part keeps its body
201    /// as text ([`PartKind::Text`], or [`PartKind::Binary`] when it was
202    /// transfer-encoded).
203    pub const NO_BLANK_LINE: PartFlags = PartFlags(flag::NO_BLANK_LINE as u8);
204    /// A multipart without its close delimiter. Not set on the multiparts
205    /// left open when the part limit stopped the parse.
206    pub const UNTERMINATED: PartFlags = PartFlags(flag::UNTERMINATED as u8);
207    /// A transfer-encoded nested message beyond the encoded nesting limit.
208    pub const NESTING_LIMIT: PartFlags = PartFlags(flag::NESTING_LIMIT as u8);
209    /// A limit was reached here. Depth limit: this multipart or
210    /// `message/rfc822` part was not opened and is kept as a leaf. Part
211    /// limit: the parse stopped inside this part, which holds the content
212    /// that was not parsed. That is the last part when it is a leaf child of
213    /// the innermost open multipart (it then runs to the end of its source),
214    /// else that multipart, or the `message/rfc822` part whose nested
215    /// message had no part yet; the containers still open all run to the
216    /// end of their source.
217    pub const LIMIT_REACHED: PartFlags = PartFlags(flag::LIMIT_REACHED as u8);
218
219    pub(crate) const NAMES: [(PartFlags, &'static str); 6] = [
220        (PartFlags::MISSING_DELIMITER, "missing_delimiter"),
221        (PartFlags::FALLBACK_DELIMITER, "fallback_delimiter"),
222        (PartFlags::NO_BLANK_LINE, "no_blank_line"),
223        (PartFlags::UNTERMINATED, "unterminated"),
224        (PartFlags::NESTING_LIMIT, "nesting_limit"),
225        (PartFlags::LIMIT_REACHED, "limit_reached"),
226    ];
227}
228
229impl<'m> MessagePart<'m> {
230    pub(crate) fn new(ctx: Ctx<'m>, id: PartId, entry: &'m PartEntry) -> Self {
231        MessagePart { ctx, id, entry }
232    }
233
234    #[inline]
235    fn resolver(&self) -> Resolver<'m> {
236        self.ctx.resolver(self.entry.message)
237    }
238
239    fn source(&self) -> &'m [u8] {
240        self.ctx.source_of(self.entry.message)
241    }
242
243    fn slice(&self, start: u32, end: u32) -> &'m [u8] {
244        self.source()
245            .get(start as usize..end as usize)
246            .unwrap_or_default()
247    }
248
249    /// The id of this part in [`crate::Message::parts`]: parts of every
250    /// message are numbered in document order.
251    pub fn id(&self) -> PartId {
252        self.id
253    }
254
255    pub(crate) fn message_id(&self) -> MessageId {
256        self.entry.message
257    }
258
259    /// The message this part belongs to.
260    pub fn message(&self) -> MessageRef<'m> {
261        self.ctx.message(self.entry.message)
262    }
263
264    /// The enclosing multipart; `None` for the root part of a message.
265    pub fn parent(&self) -> Option<MessagePart<'m>> {
266        (self.entry.parent != NONE)
267            .then(|| self.ctx.part(self.entry.parent))
268            .flatten()
269    }
270
271    /// What the part contains, from its Content-Type (and
272    /// Content-Disposition for [`PartKind::InlineBinary`]).
273    pub fn kind(&self) -> PartKind<'m> {
274        match self.entry.kind {
275            KindTag::Text => PartKind::Text,
276            KindTag::Html => PartKind::Html,
277            KindTag::Binary => PartKind::Binary,
278            KindTag::InlineBinary => PartKind::InlineBinary,
279            KindTag::Multipart => PartKind::Multipart,
280            KindTag::Message => PartKind::Message(self.ctx.message(self.entry.children.start)),
281        }
282    }
283
284    /// Whether the part is [`PartKind::Text`] or [`PartKind::Html`].
285    pub fn is_text(&self) -> bool {
286        matches!(self.entry.kind, KindTag::Text | KindTag::Html)
287    }
288
289    /// Whether the part is [`PartKind::Multipart`].
290    pub fn is_multipart(&self) -> bool {
291        self.entry.kind == KindTag::Multipart
292    }
293
294    /// Whether the part is [`PartKind::Message`].
295    pub fn is_message(&self) -> bool {
296        self.entry.kind == KindTag::Message
297    }
298
299    /// Children of a multipart, in order.
300    pub fn children(
301        &self,
302    ) -> impl ExactSizeIterator<Item = MessagePart<'m>> + DoubleEndedIterator + use<'m> {
303        let ctx = self.ctx;
304        let ids = if self.entry.kind == KindTag::Multipart {
305            ctx.ids(self.entry.children)
306        } else {
307            &[]
308        };
309        ids.iter().map(move |id| ctx.part_or_empty(*id))
310    }
311
312    /// The nested message of a `message/rfc822` part.
313    pub fn nested(&self) -> Option<MessageRef<'m>> {
314        (self.entry.kind == KindTag::Message).then(|| self.ctx.message(self.entry.children.start))
315    }
316
317    /// The header fields of this part.
318    #[inline]
319    pub fn headers(&self) -> Headers<'m> {
320        Headers::new(
321            self.resolver(),
322            self.ctx.data(),
323            self.ctx
324                .data()
325                .headers
326                .get(self.entry.headers.range())
327                .unwrap_or_default(),
328        )
329    }
330
331    fn content(&self, index: u32) -> Option<ContentType<'m>> {
332        let data = self.ctx.data();
333        data.content_types
334            .get(index as usize)
335            .map(|entry| ContentType::new(self.resolver(), data, entry))
336    }
337
338    /// The last Content-Type field, when it parsed.
339    pub fn content_type(&self) -> Option<ContentType<'m>> {
340        self.content(self.entry.content_type)
341    }
342
343    /// The last Content-Disposition field, when it parsed.
344    pub fn content_disposition(&self) -> Option<ContentType<'m>> {
345        self.content(self.entry.content_disposition)
346    }
347
348    /// The Content-Disposition `filename`, else the Content-Type `name`.
349    pub fn attachment_name(&self) -> Option<&'m str> {
350        self.content_disposition()
351            .and_then(|disposition| disposition.attribute("filename"))
352            .or_else(|| self.content_type()?.attribute("name"))
353    }
354
355    /// Whether the Content-Type is `type_/subtype` (ASCII case-insensitive).
356    pub fn is_content_type(&self, type_: &str, subtype: &str) -> bool {
357        self.content_type().is_some_and(|content_type| {
358            content_type.ctype().eq_ignore_ascii_case(type_)
359                && content_type
360                    .subtype()
361                    .is_some_and(|value| value.eq_ignore_ascii_case(subtype))
362        })
363    }
364
365    /// The Content-ID field, without angle brackets.
366    pub fn content_id(&self) -> Option<&'m str> {
367        self.headers().value(HeaderName::ContentId)?.as_text()
368    }
369
370    /// The Content-Description field, decoded.
371    pub fn content_description(&self) -> Option<&'m str> {
372        self.headers()
373            .value(HeaderName::ContentDescription)?
374            .as_text()
375    }
376
377    /// The Content-Location field.
378    pub fn content_location(&self) -> Option<&'m str> {
379        self.headers().value(HeaderName::ContentLocation)?.as_text()
380    }
381
382    /// The languages of the Content-Language field.
383    pub fn content_language(&self) -> Option<TextList<'m>> {
384        self.headers()
385            .value(HeaderName::ContentLanguage)?
386            .as_text_list()
387    }
388
389    /// The Content-Transfer-Encoding field as text: unfolded, trimmed, with
390    /// encoded words decoded (the [`crate::HeaderForm::Text`] form), in the
391    /// case it was written in. [`MessagePart::encoding`] gives the transfer
392    /// encoding the body is decoded with.
393    pub fn content_transfer_encoding(&self) -> Option<&'m str> {
394        self.headers()
395            .value(HeaderName::ContentTransferEncoding)?
396            .as_text()
397    }
398
399    /// The declared transfer encoding: base64, quoted-printable, or none
400    /// (`7bit`, `8bit`, `binary`, no field, or an unknown value).
401    pub fn encoding(&self) -> Encoding {
402        self.entry.encoding
403    }
404
405    /// What happened while the part was parsed.
406    pub fn flags(&self) -> PartFlags {
407        PartFlags((self.entry.flags & flag::MASK) as u8)
408    }
409
410    /// Whether the structure of this part was damaged: any of its
411    /// [`PartFlags`] is set (this replaces 0.11's `is_encoding_problem`).
412    /// Problems found while decoding the body are reported by
413    /// [`MessagePart::decoded_checked`] and [`MessagePart::text_checked`].
414    pub fn has_problems(&self) -> bool {
415        self.entry.flags & flag::MASK != 0
416    }
417
418    /// Whether the part is in the text body of its message
419    /// ([`MessageRef::text_body`]).
420    pub fn in_text_body(&self) -> bool {
421        self.entry.has_role(role::IN_TEXT_BODY)
422    }
423
424    /// Whether the part is in the HTML body of its message
425    /// ([`MessageRef::html_body`]).
426    pub fn in_html_body(&self) -> bool {
427        self.entry.has_role(role::IN_HTML_BODY)
428    }
429
430    /// Whether the part is in the attachments of its message
431    /// ([`MessageRef::attachments`]).
432    pub fn is_attachment(&self) -> bool {
433        self.entry.has_role(role::ATTACHMENT)
434    }
435
436    /// Whether the part is marked to be rendered inside the message rather
437    /// than offered as a download (issue #70): `Content-Disposition: inline`,
438    /// or no disposition on a child of `multipart/related` other than the
439    /// first (the target of a `cid:` reference). `Content-Disposition:
440    /// attachment` is never inline.
441    ///
442    /// This answers the disposition question only. It does not say whether
443    /// the part is shown in a body list or offered as an attachment; see
444    /// [`MessagePart::role`] and the list membership accessors for that.
445    pub fn is_inline(&self) -> bool {
446        self.entry.has_role(role::INLINE)
447    }
448
449    /// The classifier's decision for this part. A part copied into the
450    /// other body list of a `multipart/alternative` is reported as
451    /// [`PartRole::CopiedBody`], although it is also in its own list.
452    pub fn role(&self) -> PartRole {
453        let flags = self.entry.flags;
454        match self.entry.kind {
455            KindTag::Multipart => PartRole::Container,
456            _ if flags & role::COPIED != 0 => PartRole::CopiedBody,
457            kind if flags & (role::IN_TEXT_BODY | role::IN_HTML_BODY) != 0 => match kind {
458                KindTag::Html => PartRole::HtmlBody,
459                KindTag::Text => PartRole::TextBody,
460                _ => PartRole::MediaBody,
461            },
462            _ if flags & role::ATTACHMENT != 0 => PartRole::Attachment,
463            _ => PartRole::Dropped,
464        }
465    }
466
467    /// The `boundary` parameter of a multipart part.
468    pub fn boundary(&self) -> Option<&'m str> {
469        if self.entry.kind == KindTag::Multipart {
470            self.content_type()?.attribute("boundary")
471        } else {
472            None
473        }
474    }
475
476    /// The boundary of the enclosing multipart, whose delimiter starts this
477    /// part.
478    pub fn delimiter(&self) -> Option<&'m str> {
479        self.parent()?.boundary()
480    }
481
482    /// Headers and body, excluding the line break before the next
483    /// delimiter.
484    pub fn raw(&self) -> &'m [u8] {
485        self.slice(self.entry.offset_header, self.entry.offset_end)
486    }
487
488    /// The header block, from the first header to the start of the body
489    /// (the blank line included). The part ends, like any part, before the
490    /// line break that precedes its delimiter; when that delimiter cuts the
491    /// header block ([`PartFlags::NO_BLANK_LINE`]), or ends the part and its
492    /// container right after the blank line, the header block stops there
493    /// too and excludes that line break, which the last header field
494    /// ([`crate::Header::offset_end`]) or the blank line still covers.
495    pub fn raw_headers(&self) -> &'m [u8] {
496        self.slice(self.entry.offset_header, self.entry.offset_body)
497    }
498
499    /// The body as written (still transfer-encoded), excluding the line
500    /// break before the next delimiter.
501    pub fn raw_body(&self) -> &'m [u8] {
502        self.slice(self.entry.offset_body, self.entry.offset_end)
503    }
504
505    /// Offset of the first header of this part in the source buffer of its
506    /// message ([`MessageRef::source`]).
507    pub fn offset_header(&self) -> u32 {
508        self.entry.offset_header
509    }
510
511    /// Offset of the body of this part in the source buffer of its message.
512    pub fn offset_body(&self) -> u32 {
513        self.entry.offset_body
514    }
515
516    /// Offset of the end of this part in the source buffer of its message.
517    pub fn offset_end(&self) -> u32 {
518        self.entry.offset_end
519    }
520
521    /// The transfer-decoded body; borrowed when nothing needs decoding.
522    /// Malformed input is decoded on a best-effort basis: bytes outside the
523    /// base64 alphabet are skipped, malformed quoted-printable escapes are
524    /// kept as written. [`MessagePart::decoded_checked`] also reports those
525    /// problems.
526    pub fn decoded(&self) -> Cow<'m, [u8]> {
527        self.entry.encoding.decode(self.raw_body())
528    }
529
530    /// Appends the transfer-decoded body to `out`, for reusing a buffer in
531    /// a loop.
532    pub fn decode_into(&self, out: &mut Vec<u8>) {
533        self.entry.encoding.decode_append(self.raw_body(), out)
534    }
535
536    /// Length of the transfer-decoded body. Base64 and quoted-printable
537    /// bodies are measured without being decoded, except base64 with bytes
538    /// outside the alphabet.
539    pub fn decoded_len(&self) -> usize {
540        self.entry.encoding.decoded_len(self.raw_body())
541    }
542
543    /// [`MessagePart::decoded`] together with the problems found while
544    /// decoding: malformed base64 or quoted-printable, or an unknown
545    /// Content-Transfer-Encoding. The bytes are the ones `decoded()`
546    /// returns; `decoded()` stays the faster call when the problems are not
547    /// needed.
548    pub fn decoded_checked(&self) -> (Cow<'m, [u8]>, DecodeProblems) {
549        let (decoded, malformed) = self.entry.encoding.decode_checked(self.raw_body());
550        let mut problems = DecodeProblems::default();
551        if malformed {
552            problems |= DecodeProblems::MALFORMED_TRANSFER_ENCODING;
553        }
554        if !self.has_known_transfer_encoding() {
555            problems |= DecodeProblems::UNKNOWN_TRANSFER_ENCODING;
556        }
557        (decoded, problems)
558    }
559
560    /// False when the part declares a Content-Transfer-Encoding other than
561    /// `7bit`, `8bit`, `binary`, `base64` or `quoted-printable`, the
562    /// condition [`DecodeProblems::UNKNOWN_TRANSFER_ENCODING`] reports,
563    /// without decoding the body. True when the field is absent.
564    pub fn has_known_transfer_encoding(&self) -> bool {
565        self.headers()
566            .get(HeaderName::ContentTransferEncoding)
567            .is_none_or(|header| Encoding::parse(header.raw_value()).is_some())
568    }
569
570    fn charset(&self) -> Option<&'m str> {
571        self.content_type()?.attribute("charset")
572    }
573
574    fn text_charset(&self) -> Charset {
575        self.charset()
576            .and_then(|label| Charset::from_label(label.as_bytes()))
577            .unwrap_or_default()
578    }
579
580    /// The text of a text or HTML part: transfer-decoded, then converted
581    /// from its charset (UTF-8 when the charset is missing or unknown; byte
582    /// sequences that are not valid in the charset become U+FFFD). Borrowed
583    /// when the body needs no conversion. `None` for other parts.
584    /// [`MessagePart::text_checked`] also reports decoding problems.
585    pub fn text(&self) -> Option<Cow<'m, str>> {
586        self.is_text()
587            .then(|| self.text_charset().decode_cow(self.decoded()))
588    }
589
590    /// [`MessagePart::text`] together with the problems found while decoding
591    /// it: those of [`MessagePart::decoded_checked`], an unknown charset,
592    /// and byte sequences that are not valid in the charset. The text is the
593    /// one `text()` returns. `None` for parts that are not text.
594    ///
595    /// For JMAP, `isEncodingProblem` is `!problems.is_empty()`.
596    pub fn text_checked(&self) -> Option<(Cow<'m, str>, DecodeProblems)> {
597        if !self.is_text() {
598            return None;
599        }
600        let (decoded, mut problems) = self.decoded_checked();
601        let charset = match self.charset() {
602            None => Charset::default(),
603            Some(label) => Charset::from_label(label.as_bytes()).unwrap_or_else(|| {
604                problems |= DecodeProblems::UNKNOWN_CHARSET;
605                Charset::default()
606            }),
607        };
608        if !charset.is_supported() {
609            problems |= DecodeProblems::UNKNOWN_CHARSET;
610        }
611        let (text, malformed) = charset.decode_cow_checked(decoded);
612        if malformed {
613            problems |= DecodeProblems::MALFORMED_CHARSET;
614        }
615        Some((text, problems))
616    }
617
618    /// The first `max_chars` characters of [`MessagePart::text`], decoding
619    /// only as much of the body as they need. `None` for other parts.
620    pub fn text_prefix(&self, max_chars: usize) -> Option<Cow<'m, str>> {
621        self.text_prefix_state(decoders::Limit::Chars(max_chars))
622            .map(|prefix| prefix.text)
623    }
624
625    /// [`MessagePart::text`] cut at [`str::floor_char_boundary`] of
626    /// `max_bytes`: the longest prefix that is at most `max_bytes` bytes
627    /// long and ends on a character boundary. `None` for other parts.
628    pub fn text_prefix_bytes(&self, max_bytes: usize) -> Option<Cow<'m, str>> {
629        let raw = self.raw_body();
630        let decode_whole = match self.entry.encoding {
631            Encoding::None => false,
632            _ if raw.len() <= max_bytes.saturating_mul(WHOLE_TEXT_RAW_PER_BYTE) => true,
633            _ if raw.len() > max_bytes.saturating_mul(DENSE_WHOLE_TEXT_RAW_PER_BYTE) => false,
634            Encoding::QuotedPrintable => {
635                let sample = raw.get(..DENSITY_SAMPLE_BYTES).unwrap_or(raw);
636                let (chunks, rest) = sample.as_chunks::<DENSITY_COUNT_CHUNK>();
637                let escapes = chunks
638                    .iter()
639                    .map(|chunk| {
640                        usize::from(chunk.iter().map(|&byte| u8::from(byte == b'=')).sum::<u8>())
641                    })
642                    .sum::<usize>()
643                    + rest.iter().filter(|&&byte| byte == b'=').count();
644                escapes * DENSE_SAMPLE_BYTES_PER_ESCAPE > sample.len()
645            }
646            Encoding::Base64 => matches!(
647                self.text_charset(),
648                Charset::Utf16 | Charset::Utf16Le | Charset::Utf16Be
649            ),
650        };
651        let text = if decode_whole {
652            self.text()?
653        } else {
654            self.text_prefix_state(decoders::Limit::Bytes(max_bytes))?
655                .text
656        };
657        let end = text.floor_char_boundary(max_bytes);
658        Some(decoders::truncate(text, end))
659    }
660
661    pub(crate) fn text_prefix_state(
662        &self,
663        limit: decoders::Limit,
664    ) -> Option<decoders::TextPrefix<'m>> {
665        self.is_text().then(|| {
666            decoders::TextPrefix::decode(
667                self.raw_body(),
668                self.entry.encoding,
669                self.text_charset(),
670                limit,
671            )
672        })
673    }
674
675    /// Appends the text of a text or HTML part to `out`; returns `false`
676    /// for other parts.
677    pub fn text_into(&self, out: &mut String) -> bool {
678        if !self.is_text() {
679            return false;
680        }
681        self.text_charset().decode_append(&self.decoded(), out);
682        true
683    }
684
685    /// The HTML of an HTML part with its `<meta>` charset declarations
686    /// rewritten to UTF-8 (issue #109), so that a browser reading the
687    /// decoded text does not apply the original charset a second time.
688    /// `None` for other parts.
689    pub fn html_utf8(&self) -> Option<Cow<'m, str>> {
690        if self.entry.kind != KindTag::Html {
691            return None;
692        }
693        Some(decoders::rewrite(self.text()?, strip_charset_meta))
694    }
695}
696
697impl fmt::Debug for MessagePart<'_> {
698    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
699        f.debug_struct("MessagePart")
700            .field("id", &self.id)
701            .field("kind", &self.entry.kind)
702            .field("offset_header", &self.entry.offset_header)
703            .field("offset_body", &self.entry.offset_body)
704            .field("offset_end", &self.entry.offset_end)
705            .finish()
706    }
707}