lettre/message/
body.rs

1use std::{mem, ops::Deref};
2
3use crate::message::header::ContentTransferEncoding;
4
5/// A [`Message`][super::Message] or [`SinglePart`][super::SinglePart] body that has already been encoded.
6#[derive(Debug, Clone)]
7pub struct Body {
8    buf: Vec<u8>,
9    encoding: ContentTransferEncoding,
10}
11
12/// Either a `Vec<u8>` or a `String`.
13///
14/// If the content is valid utf-8 a `String` should be passed, as it
15/// makes for a more efficient `Content-Transfer-Encoding` to be chosen.
16#[derive(Debug, Clone)]
17pub enum MaybeString {
18    /// Binary data
19    Binary(Vec<u8>),
20    /// UTF-8 string
21    String(String),
22}
23
24impl Body {
25    /// Encode the supplied `buf`, making it ready to be sent as a body.
26    ///
27    /// Takes a `Vec<u8>` or a `String`.
28    ///
29    /// Automatically chooses the most efficient encoding between
30    /// `7bit`, `quoted-printable` and `base64`.
31    ///
32    /// If `String` is passed, line endings are converted to `CRLF`.
33    ///
34    /// If `buf` is valid utf-8 a `String` should be supplied, as `String`s
35    /// can be encoded as `7bit` or `quoted-printable`, while `Vec<u8>` always
36    /// get encoded as `base64`.
37    pub fn new<B: Into<MaybeString>>(buf: B) -> Self {
38        let mut buf: MaybeString = buf.into();
39
40        let encoding = buf.encoding(false);
41        buf.encode_crlf();
42        Self::new_impl(buf.into(), encoding)
43    }
44
45    /// Encode the supplied `buf`, using the provided `encoding`.
46    ///
47    /// [`Body::new`] is generally the better option.
48    ///
49    /// If `String` is passed, line endings are converted to `CRLF`.
50    ///
51    /// Returns an [`Err`] giving back the supplied `buf`, in case the chosen
52    /// encoding would have resulted into `buf` being encoded
53    /// into an invalid body.
54    pub fn new_with_encoding<B: Into<MaybeString>>(
55        buf: B,
56        encoding: ContentTransferEncoding,
57    ) -> Result<Self, Vec<u8>> {
58        let mut buf: MaybeString = buf.into();
59
60        let best_encoding = buf.encoding(true);
61        let ok = match (encoding, best_encoding) {
62            (ContentTransferEncoding::SevenBit, ContentTransferEncoding::SevenBit) => true,
63            (
64                ContentTransferEncoding::EightBit,
65                ContentTransferEncoding::SevenBit | ContentTransferEncoding::EightBit,
66            ) => true,
67            (ContentTransferEncoding::SevenBit | ContentTransferEncoding::EightBit, _) => false,
68            (
69                ContentTransferEncoding::QuotedPrintable
70                | ContentTransferEncoding::Base64
71                | ContentTransferEncoding::Binary,
72                _,
73            ) => true,
74        };
75        if !ok {
76            return Err(buf.into());
77        }
78
79        buf.encode_crlf();
80        Ok(Self::new_impl(buf.into(), encoding))
81    }
82
83    /// Builds a new `Body` using a pre-encoded buffer.
84    ///
85    /// **Generally not you want.**
86    ///
87    /// `buf` shouldn't contain non-ascii characters, lines longer than 1000 characters or nul bytes.
88    #[inline]
89    pub fn dangerous_pre_encoded(buf: Vec<u8>, encoding: ContentTransferEncoding) -> Self {
90        Self { buf, encoding }
91    }
92
93    /// Encodes the supplied `buf` using the provided `encoding`
94    fn new_impl(buf: Vec<u8>, encoding: ContentTransferEncoding) -> Self {
95        match encoding {
96            ContentTransferEncoding::SevenBit
97            | ContentTransferEncoding::EightBit
98            | ContentTransferEncoding::Binary => Self { buf, encoding },
99            ContentTransferEncoding::QuotedPrintable => {
100                let encoded = quoted_printable::encode(buf);
101
102                Self::dangerous_pre_encoded(encoded, ContentTransferEncoding::QuotedPrintable)
103            }
104            ContentTransferEncoding::Base64 => {
105                let len = email_encoding::body::base64::encoded_len(buf.len());
106
107                let mut out = String::with_capacity(len);
108                email_encoding::body::base64::encode(&buf, &mut out)
109                    .expect("encode body as base64");
110
111                Self::dangerous_pre_encoded(out.into_bytes(), ContentTransferEncoding::Base64)
112            }
113        }
114    }
115
116    /// Returns the length of this `Body` in bytes.
117    #[inline]
118    pub fn len(&self) -> usize {
119        self.buf.len()
120    }
121
122    /// Returns `true` if this `Body` has a length of zero, `false` otherwise.
123    #[inline]
124    pub fn is_empty(&self) -> bool {
125        self.buf.is_empty()
126    }
127
128    /// Returns the `Content-Transfer-Encoding` of this `Body`.
129    #[inline]
130    pub fn encoding(&self) -> ContentTransferEncoding {
131        self.encoding
132    }
133
134    /// Consumes `Body` and returns the inner `Vec<u8>`
135    #[inline]
136    pub fn into_vec(self) -> Vec<u8> {
137        self.buf
138    }
139}
140
141impl MaybeString {
142    /// Suggests the best `Content-Transfer-Encoding` to be used for this `MaybeString`
143    ///
144    /// The `binary` encoding is never returned
145    fn encoding(&self, supports_utf8: bool) -> ContentTransferEncoding {
146        use email_encoding::body::Encoding;
147
148        let output = match self {
149            Self::String(s) => Encoding::choose(s.as_str(), supports_utf8),
150            Self::Binary(b) => Encoding::choose(b.as_slice(), supports_utf8),
151        };
152
153        match output {
154            Encoding::SevenBit => ContentTransferEncoding::SevenBit,
155            Encoding::EightBit => ContentTransferEncoding::EightBit,
156            Encoding::QuotedPrintable => ContentTransferEncoding::QuotedPrintable,
157            Encoding::Base64 => ContentTransferEncoding::Base64,
158        }
159    }
160
161    /// Encode line endings to CRLF if the variant is `String`
162    fn encode_crlf(&mut self) {
163        match self {
164            Self::String(string) => in_place_crlf_line_endings(string),
165            Self::Binary(_) => {}
166        }
167    }
168}
169
170/// A trait for something that takes an encoded [`Body`].
171///
172/// Used by [`MessageBuilder::body`][super::MessageBuilder::body] and
173/// [`SinglePartBuilder::body`][super::SinglePartBuilder::body],
174/// which can either take something that can be encoded into [`Body`]
175/// or a pre-encoded [`Body`].
176///
177/// If `encoding` is `None` the best encoding between `7bit`, `quoted-printable`
178/// and `base64` is chosen based on the input body. **Best option.**
179///
180/// If `encoding` is `Some` the supplied encoding is used.
181/// **NOTE:** if using the specified `encoding` would result into a malformed
182/// body, this will panic!
183pub trait IntoBody {
184    /// Encode as valid body
185    fn into_body(self, encoding: Option<ContentTransferEncoding>) -> Body;
186}
187
188impl<T> IntoBody for T
189where
190    T: Into<MaybeString>,
191{
192    fn into_body(self, encoding: Option<ContentTransferEncoding>) -> Body {
193        match encoding {
194            Some(encoding) => Body::new_with_encoding(self, encoding).expect("invalid encoding"),
195            None => Body::new(self),
196        }
197    }
198}
199
200impl IntoBody for Body {
201    fn into_body(self, encoding: Option<ContentTransferEncoding>) -> Body {
202        let _ = encoding;
203
204        self
205    }
206}
207
208impl AsRef<[u8]> for Body {
209    #[inline]
210    fn as_ref(&self) -> &[u8] {
211        self.buf.as_ref()
212    }
213}
214
215impl From<Vec<u8>> for MaybeString {
216    #[inline]
217    fn from(b: Vec<u8>) -> Self {
218        Self::Binary(b)
219    }
220}
221
222impl From<String> for MaybeString {
223    #[inline]
224    fn from(s: String) -> Self {
225        Self::String(s)
226    }
227}
228
229impl From<MaybeString> for Vec<u8> {
230    #[inline]
231    fn from(s: MaybeString) -> Self {
232        match s {
233            MaybeString::Binary(b) => b,
234            MaybeString::String(s) => s.into(),
235        }
236    }
237}
238
239impl Deref for MaybeString {
240    type Target = [u8];
241
242    #[inline]
243    fn deref(&self) -> &Self::Target {
244        match self {
245            Self::Binary(b) => b.as_ref(),
246            Self::String(s) => s.as_ref(),
247        }
248    }
249}
250
251/// In place conversion to CRLF line endings
252fn in_place_crlf_line_endings(string: &mut String) {
253    let indices = find_all_lf_char_indices(string);
254
255    for i in indices {
256        // this relies on `indices` being in reverse order
257        string.insert(i, '\r');
258    }
259}
260
261/// Find indices to all places where `\r` should be inserted
262/// in order to make `s` have CRLF line endings
263///
264/// The list is reversed, which is more efficient.
265fn find_all_lf_char_indices(s: &str) -> Vec<usize> {
266    let mut indices = Vec::new();
267
268    let mut found_lf = false;
269    for (i, c) in s.char_indices().rev() {
270        if mem::take(&mut found_lf) && c != '\r' {
271            // the previous character was `\n`, but this isn't a `\r`
272            indices.push(i + c.len_utf8());
273        }
274
275        found_lf = c == '\n';
276    }
277
278    if found_lf {
279        // the first character is `\n`
280        indices.push(0);
281    }
282
283    indices
284}
285
286#[cfg(test)]
287mod test {
288    use pretty_assertions::assert_eq;
289
290    use super::{in_place_crlf_line_endings, Body, ContentTransferEncoding};
291
292    #[test]
293    fn seven_bit_detect() {
294        let encoded = Body::new(String::from("Hello, world!"));
295
296        assert_eq!(encoded.encoding(), ContentTransferEncoding::SevenBit);
297        assert_eq!(encoded.as_ref(), b"Hello, world!");
298    }
299
300    #[test]
301    fn seven_bit_encode() {
302        let encoded = Body::new_with_encoding(
303            String::from("Hello, world!"),
304            ContentTransferEncoding::SevenBit,
305        )
306        .unwrap();
307
308        assert_eq!(encoded.encoding(), ContentTransferEncoding::SevenBit);
309        assert_eq!(encoded.as_ref(), b"Hello, world!");
310    }
311
312    #[test]
313    fn seven_bit_too_long_detect() {
314        let encoded = Body::new("Hello, world!".repeat(100));
315
316        assert_eq!(encoded.encoding(), ContentTransferEncoding::QuotedPrintable);
317        assert_eq!(
318            encoded.as_ref(),
319            concat!(
320                "Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, wor=\r\n",
321                "ld!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, =\r\n",
322                "world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hell=\r\n",
323                "o, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!H=\r\n",
324                "ello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, worl=\r\n",
325                "d!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, w=\r\n",
326                "orld!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello=\r\n",
327                ", world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!He=\r\n",
328                "llo, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world=\r\n",
329                "!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, wo=\r\n",
330                "rld!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello,=\r\n",
331                " world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hel=\r\n",
332                "lo, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!=\r\n",
333                "Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, wor=\r\n",
334                "ld!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, =\r\n",
335                "world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hell=\r\n",
336                "o, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!H=\r\n",
337                "ello, world!Hello, world!"
338            )
339            .as_bytes()
340        );
341    }
342
343    #[test]
344    fn seven_bit_too_long_fail() {
345        let result = Body::new_with_encoding(
346            "Hello, world!".repeat(100),
347            ContentTransferEncoding::SevenBit,
348        );
349
350        assert!(result.is_err());
351    }
352
353    #[test]
354    fn seven_bit_too_long_encode_quotedprintable() {
355        let encoded = Body::new_with_encoding(
356            "Hello, world!".repeat(100),
357            ContentTransferEncoding::QuotedPrintable,
358        )
359        .unwrap();
360
361        assert_eq!(encoded.encoding(), ContentTransferEncoding::QuotedPrintable);
362        assert_eq!(
363            encoded.as_ref(),
364            concat!(
365                "Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, wor=\r\n",
366                "ld!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, =\r\n",
367                "world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hell=\r\n",
368                "o, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!H=\r\n",
369                "ello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, worl=\r\n",
370                "d!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, w=\r\n",
371                "orld!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello=\r\n",
372                ", world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!He=\r\n",
373                "llo, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world=\r\n",
374                "!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, wo=\r\n",
375                "rld!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello,=\r\n",
376                " world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hel=\r\n",
377                "lo, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!=\r\n",
378                "Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, wor=\r\n",
379                "ld!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, =\r\n",
380                "world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hell=\r\n",
381                "o, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!H=\r\n",
382                "ello, world!Hello, world!"
383            )
384            .as_bytes()
385        );
386    }
387
388    #[test]
389    fn seven_bit_invalid() {
390        let result = Body::new_with_encoding(
391            String::from("Привет, мир!"),
392            ContentTransferEncoding::SevenBit,
393        );
394
395        assert!(result.is_err());
396    }
397
398    #[test]
399    fn eight_bit_encode() {
400        let encoded = Body::new_with_encoding(
401            String::from("Привет, мир!"),
402            ContentTransferEncoding::EightBit,
403        )
404        .unwrap();
405
406        assert_eq!(encoded.encoding(), ContentTransferEncoding::EightBit);
407        assert_eq!(encoded.as_ref(), "Привет, мир!".as_bytes());
408    }
409
410    #[test]
411    fn eight_bit_too_long_fail() {
412        let result = Body::new_with_encoding(
413            "Привет, мир!".repeat(200),
414            ContentTransferEncoding::EightBit,
415        );
416
417        assert!(result.is_err());
418    }
419
420    #[test]
421    fn quoted_printable_detect() {
422        let encoded = Body::new(String::from("Questo messaggio è corto"));
423
424        assert_eq!(encoded.encoding(), ContentTransferEncoding::QuotedPrintable);
425        assert_eq!(encoded.as_ref(), b"Questo messaggio =C3=A8 corto");
426    }
427
428    #[test]
429    fn quoted_printable_encode_ascii() {
430        let encoded = Body::new_with_encoding(
431            String::from("Hello, world!"),
432            ContentTransferEncoding::QuotedPrintable,
433        )
434        .unwrap();
435
436        assert_eq!(encoded.encoding(), ContentTransferEncoding::QuotedPrintable);
437        assert_eq!(encoded.as_ref(), b"Hello, world!");
438    }
439
440    #[test]
441    fn quoted_printable_encode_utf8() {
442        let encoded = Body::new_with_encoding(
443            String::from("Привет, мир!"),
444            ContentTransferEncoding::QuotedPrintable,
445        )
446        .unwrap();
447
448        assert_eq!(encoded.encoding(), ContentTransferEncoding::QuotedPrintable);
449        assert_eq!(
450            encoded.as_ref(),
451            b"=D0=9F=D1=80=D0=B8=D0=B2=D0=B5=D1=82, =D0=BC=D0=B8=D1=80!".as_ref()
452        );
453    }
454
455    #[test]
456    fn quoted_printable_encode_line_wrap() {
457        let encoded = Body::new(String::from(
458            "Se lo standard 📬 fosse stato più semplice avremmo finito molto prima.",
459        ));
460
461        assert_eq!(encoded.encoding(), ContentTransferEncoding::QuotedPrintable);
462        println!("{}", std::str::from_utf8(encoded.as_ref()).unwrap());
463        assert_eq!(
464            encoded.as_ref(),
465            concat!(
466                "Se lo standard =F0=9F=93=AC fosse stato pi=C3=B9 semplice avremmo finito mo=\r\n",
467                "lto prima."
468            )
469            .as_bytes()
470        );
471    }
472
473    #[test]
474    fn base64_detect() {
475        let input = Body::new(vec![0; 80]);
476        let encoding = input.encoding();
477        assert_eq!(encoding, ContentTransferEncoding::Base64);
478    }
479
480    #[test]
481    fn base64_encode_bytes() {
482        let encoded =
483            Body::new_with_encoding(vec![0; 80], ContentTransferEncoding::Base64).unwrap();
484
485        assert_eq!(encoded.encoding(), ContentTransferEncoding::Base64);
486        assert_eq!(
487            encoded.as_ref(),
488            concat!(
489                "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\r\n",
490                "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="
491            )
492            .as_bytes()
493        );
494    }
495
496    #[test]
497    fn base64_encode_bytes_wrapping() {
498        let encoded = Body::new_with_encoding(
499            [0, 1, 2, 3, 4, 5, 6, 7, 8, 9].repeat(20),
500            ContentTransferEncoding::Base64,
501        )
502        .unwrap();
503
504        assert_eq!(encoded.encoding(), ContentTransferEncoding::Base64);
505        assert_eq!(
506            encoded.as_ref(),
507            concat!(
508                "AAECAwQFBgcICQABAgMEBQYHCAkAAQIDBAUGBwgJAAECAwQFBgcICQABAgMEBQYHCAkAAQIDBAUG\r\n",
509                "BwgJAAECAwQFBgcICQABAgMEBQYHCAkAAQIDBAUGBwgJAAECAwQFBgcICQABAgMEBQYHCAkAAQID\r\n",
510                "BAUGBwgJAAECAwQFBgcICQABAgMEBQYHCAkAAQIDBAUGBwgJAAECAwQFBgcICQABAgMEBQYHCAkA\r\n",
511                "AQIDBAUGBwgJAAECAwQFBgcICQABAgMEBQYHCAk="
512            )
513            .as_bytes()
514        );
515    }
516
517    #[test]
518    fn base64_encode_ascii() {
519        let encoded = Body::new_with_encoding(
520            String::from("Hello World!"),
521            ContentTransferEncoding::Base64,
522        )
523        .unwrap();
524
525        assert_eq!(encoded.encoding(), ContentTransferEncoding::Base64);
526        assert_eq!(encoded.as_ref(), b"SGVsbG8gV29ybGQh");
527    }
528
529    #[test]
530    fn base64_encode_ascii_wrapping() {
531        let encoded =
532            Body::new_with_encoding("Hello World!".repeat(20), ContentTransferEncoding::Base64)
533                .unwrap();
534
535        assert_eq!(encoded.encoding(), ContentTransferEncoding::Base64);
536        assert_eq!(
537            encoded.as_ref(),
538            concat!(
539                "SGVsbG8gV29ybGQhSGVsbG8gV29ybGQhSGVsbG8gV29ybGQhSGVsbG8gV29ybGQhSGVsbG8gV29y\r\n",
540                "bGQhSGVsbG8gV29ybGQhSGVsbG8gV29ybGQhSGVsbG8gV29ybGQhSGVsbG8gV29ybGQhSGVsbG8g\r\n",
541                "V29ybGQhSGVsbG8gV29ybGQhSGVsbG8gV29ybGQhSGVsbG8gV29ybGQhSGVsbG8gV29ybGQhSGVs\r\n",
542                "bG8gV29ybGQhSGVsbG8gV29ybGQhSGVsbG8gV29ybGQhSGVsbG8gV29ybGQhSGVsbG8gV29ybGQh\r\n",
543                "SGVsbG8gV29ybGQh"
544            )
545            .as_bytes()
546        );
547    }
548
549    #[test]
550    fn crlf() {
551        let mut string = String::from("Send me a ✉️\nwith\nlettre!\n😀");
552
553        in_place_crlf_line_endings(&mut string);
554        assert_eq!(string, "Send me a ✉️\r\nwith\r\nlettre!\r\n😀");
555    }
556
557    #[test]
558    fn harsh_crlf() {
559        let mut string = String::from("\n\nSend me a ✉️\r\n\nwith\n\nlettre!\n\r\n😀");
560
561        in_place_crlf_line_endings(&mut string);
562        assert_eq!(
563            string,
564            "\r\n\r\nSend me a ✉️\r\n\r\nwith\r\n\r\nlettre!\r\n\r\n😀"
565        );
566    }
567
568    #[test]
569    fn crlf_noop() {
570        let mut string = String::from("\r\nSend me a ✉️\r\nwith\r\nlettre!\r\n😀");
571
572        in_place_crlf_line_endings(&mut string);
573        assert_eq!(string, "\r\nSend me a ✉️\r\nwith\r\nlettre!\r\n😀");
574    }
575}