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}