imap_codec/
body.rs

1use abnf_core::streaming::sp;
2use imap_types::{
3    body::{
4        BasicFields, Body, BodyExtension, BodyStructure, Disposition, Language, Location,
5        MultiPartExtensionData, SinglePartExtensionData, SpecificFields,
6    },
7    core::{IString, NString, NonEmptyVec},
8};
9use nom::{
10    branch::alt,
11    bytes::streaming::{tag, tag_no_case},
12    combinator::{map, opt},
13    multi::{many0, many1, separated_list1},
14    sequence::{delimited, preceded, tuple},
15};
16
17use crate::{
18    core::{nil, nstring, number, string},
19    decode::{IMAPErrorKind, IMAPParseError, IMAPResult},
20    envelope::envelope,
21};
22
23/// `body = "(" (body-type-1part / body-type-mpart) ")"`
24///
25/// Note: This parser is recursively defined. Thus, in order to not overflow the stack,
26/// it is needed to limit how may recursions are allowed. (8 should suffice).
27pub(crate) fn body(
28    remaining_recursions: usize,
29) -> impl Fn(&[u8]) -> IMAPResult<&[u8], BodyStructure> {
30    move |input: &[u8]| body_limited(input, remaining_recursions)
31}
32
33fn body_limited<'a>(
34    input: &'a [u8],
35    remaining_recursions: usize,
36) -> IMAPResult<&'a [u8], BodyStructure> {
37    if remaining_recursions == 0 {
38        return Err(nom::Err::Failure(IMAPParseError {
39            input,
40            kind: IMAPErrorKind::RecursionLimitExceeded,
41        }));
42    }
43
44    let body_type_1part = move |input: &'a [u8]| {
45        body_type_1part_limited(input, remaining_recursions.saturating_sub(1))
46    };
47    let body_type_mpart = move |input: &'a [u8]| {
48        body_type_mpart_limited(input, remaining_recursions.saturating_sub(1))
49    };
50
51    delimited(
52        tag(b"("),
53        alt((body_type_1part, body_type_mpart)),
54        tag(b")"),
55    )(input)
56}
57
58/// `body-type-1part = (
59///                     body-type-basic /
60///                     body-type-msg /
61///                     body-type-text
62///                    )
63///                    [SP body-ext-1part]`
64///
65/// Note: This parser is recursively defined. Thus, in order to not overflow the stack,
66/// it is needed to limit how may recursions are allowed.
67fn body_type_1part_limited<'a>(
68    input: &'a [u8],
69    remaining_recursions: usize,
70) -> IMAPResult<&'a [u8], BodyStructure> {
71    if remaining_recursions == 0 {
72        return Err(nom::Err::Failure(IMAPParseError {
73            input,
74            kind: IMAPErrorKind::RecursionLimitExceeded,
75        }));
76    }
77
78    let body_type_msg = move |input: &'a [u8]| body_type_msg_limited(input, 8);
79
80    let mut parser = tuple((
81        alt((body_type_msg, body_type_text, body_type_basic)),
82        opt(preceded(sp, body_ext_1part)),
83    ));
84
85    let (remaining, ((basic, specific), extension_data)) = parser(input)?;
86
87    Ok((
88        remaining,
89        BodyStructure::Single {
90            body: Body { basic, specific },
91            extension_data,
92        },
93    ))
94}
95
96/// `body-type-basic = media-basic SP body-fields`
97///
98/// MESSAGE subtype MUST NOT be "RFC822"
99pub(crate) fn body_type_basic(input: &[u8]) -> IMAPResult<&[u8], (BasicFields, SpecificFields)> {
100    let mut parser = tuple((media_basic, sp, body_fields));
101
102    let (remaining, ((type_, subtype), _, basic)) = parser(input)?;
103
104    Ok((
105        remaining,
106        (
107            basic,
108            SpecificFields::Basic {
109                r#type: type_,
110                subtype,
111            },
112        ),
113    ))
114}
115
116/// `body-type-msg = media-message SP
117///                 body-fields SP
118///                 envelope SP
119///                 body SP
120///                 body-fld-lines`
121///
122/// Note: This parser is recursively defined. Thus, in order to not overflow the stack,
123/// it is needed to limit how may recursions are allowed. (8 should suffice).
124fn body_type_msg_limited<'a>(
125    input: &'a [u8],
126    remaining_recursions: usize,
127) -> IMAPResult<&'a [u8], (BasicFields, SpecificFields)> {
128    if remaining_recursions == 0 {
129        return Err(nom::Err::Failure(IMAPParseError {
130            input,
131            kind: IMAPErrorKind::RecursionLimitExceeded,
132        }));
133    }
134
135    let body = move |input: &'a [u8]| body_limited(input, remaining_recursions.saturating_sub(1));
136
137    let mut parser = tuple((
138        media_message,
139        sp,
140        body_fields,
141        sp,
142        envelope,
143        sp,
144        body,
145        sp,
146        body_fld_lines,
147    ));
148
149    let (remaining, (_, _, basic, _, envelope, _, body_structure, _, number_of_lines)) =
150        parser(input)?;
151
152    Ok((
153        remaining,
154        (
155            basic,
156            SpecificFields::Message {
157                envelope: Box::new(envelope),
158                body_structure: Box::new(body_structure),
159                number_of_lines,
160            },
161        ),
162    ))
163}
164
165/// `body-type-text = media-text SP
166///                   body-fields SP
167///                   body-fld-lines`
168pub(crate) fn body_type_text(input: &[u8]) -> IMAPResult<&[u8], (BasicFields, SpecificFields)> {
169    let mut parser = tuple((media_text, sp, body_fields, sp, body_fld_lines));
170
171    let (remaining, (subtype, _, basic, _, number_of_lines)) = parser(input)?;
172
173    Ok((
174        remaining,
175        (
176            basic,
177            SpecificFields::Text {
178                subtype,
179                number_of_lines,
180            },
181        ),
182    ))
183}
184
185/// `body-fields = body-fld-param SP
186///                body-fld-id SP
187///                body-fld-desc SP
188///                body-fld-enc SP
189///                body-fld-octets`
190pub(crate) fn body_fields(input: &[u8]) -> IMAPResult<&[u8], BasicFields> {
191    let mut parser = tuple((
192        body_fld_param,
193        sp,
194        body_fld_id,
195        sp,
196        body_fld_desc,
197        sp,
198        body_fld_enc,
199        sp,
200        body_fld_octets,
201    ));
202
203    let (remaining, (parameter_list, _, id, _, description, _, content_transfer_encoding, _, size)) =
204        parser(input)?;
205
206    Ok((
207        remaining,
208        BasicFields {
209            parameter_list,
210            id,
211            description,
212            content_transfer_encoding,
213            size,
214        },
215    ))
216}
217
218/// `body-fld-param = "("
219///                     string SP string
220///                     *(SP string SP string)
221///                   ")" / nil`
222pub(crate) fn body_fld_param(input: &[u8]) -> IMAPResult<&[u8], Vec<(IString, IString)>> {
223    let mut parser = alt((
224        delimited(
225            tag(b"("),
226            separated_list1(
227                sp,
228                map(tuple((string, sp, string)), |(key, _, value)| (key, value)),
229            ),
230            tag(b")"),
231        ),
232        map(nil, |_| vec![]),
233    ));
234
235    let (remaining, parsed_body_fld_param) = parser(input)?;
236
237    Ok((remaining, parsed_body_fld_param))
238}
239
240#[inline]
241/// `body-fld-id = nstring`
242pub(crate) fn body_fld_id(input: &[u8]) -> IMAPResult<&[u8], NString> {
243    nstring(input)
244}
245
246#[inline]
247/// `body-fld-desc = nstring`
248pub(crate) fn body_fld_desc(input: &[u8]) -> IMAPResult<&[u8], NString> {
249    nstring(input)
250}
251
252#[inline]
253/// `body-fld-enc = (
254///                   DQUOTE (
255///                     "7BIT" /
256///                     "8BIT" /
257///                     "BINARY" /
258///                     "BASE64"/
259///                     "QUOTED-PRINTABLE"
260///                   ) DQUOTE
261///                 ) / string`
262///
263/// Simplified...
264///
265/// `body-fld-enc = string`
266///
267/// TODO: why the special case?
268pub(crate) fn body_fld_enc(input: &[u8]) -> IMAPResult<&[u8], IString> {
269    string(input)
270}
271
272#[inline]
273/// `body-fld-octets = number`
274///
275/// # Quirks
276///
277/// The following erroneous messages were observed:
278///
279/// * A negative number, specifically `-1`, in Dovecot.
280#[allow(clippy::needless_return)]
281pub(crate) fn body_fld_octets(input: &[u8]) -> IMAPResult<&[u8], u32> {
282    #[cfg(not(feature = "quirk_rectify_numbers"))]
283    return number(input);
284
285    #[cfg(feature = "quirk_rectify_numbers")]
286    {
287        return alt((
288            number,
289            map(tuple((tag("-"), number)), |(_, _)| {
290                log::warn!("Rectified negative number to 0");
291                0
292            }),
293        ))(input);
294    }
295}
296
297#[inline]
298/// `body-fld-lines = number`
299pub(crate) fn body_fld_lines(input: &[u8]) -> IMAPResult<&[u8], u32> {
300    number(input)
301}
302
303/// ```abnf
304/// body-ext-1part = body-fld-md5
305///                   [SP body-fld-dsp
306///                     [SP body-fld-lang
307///                       [SP body-fld-loc *(SP body-extension)]
308///                     ]
309///                   ]
310/// ```
311///
312/// Note: MUST NOT be returned on non-extensible "BODY" fetch.
313pub(crate) fn body_ext_1part(input: &[u8]) -> IMAPResult<&[u8], SinglePartExtensionData> {
314    map(
315        tuple((
316            body_fld_md5,
317            opt(map(
318                tuple((
319                    preceded(sp, body_fld_dsp),
320                    opt(map(
321                        tuple((
322                            preceded(sp, body_fld_lang),
323                            opt(map(
324                                tuple((
325                                    preceded(sp, body_fld_loc),
326                                    many0(preceded(sp, body_extension(8))),
327                                )),
328                                |(location, extensions)| Location {
329                                    location,
330                                    extensions,
331                                },
332                            )),
333                        )),
334                        |(language, tail)| Language { language, tail },
335                    )),
336                )),
337                |(disposition, tail)| Disposition { disposition, tail },
338            )),
339        )),
340        |(md5, tail)| SinglePartExtensionData { md5, tail },
341    )(input)
342}
343
344#[inline]
345/// `body-fld-md5 = nstring`
346pub(crate) fn body_fld_md5(input: &[u8]) -> IMAPResult<&[u8], NString> {
347    nstring(input)
348}
349
350/// `body-fld-dsp = "(" string SP body-fld-param ")" / nil`
351#[allow(clippy::type_complexity)]
352pub(crate) fn body_fld_dsp(
353    input: &[u8],
354) -> IMAPResult<&[u8], Option<(IString, Vec<(IString, IString)>)>> {
355    alt((
356        delimited(
357            tag(b"("),
358            map(
359                tuple((string, sp, body_fld_param)),
360                |(string, _, body_fld_param)| Some((string, body_fld_param)),
361            ),
362            tag(b")"),
363        ),
364        map(nil, |_| None),
365    ))(input)
366}
367
368/// `body-fld-lang = nstring / "(" string *(SP string) ")"`
369pub(crate) fn body_fld_lang(input: &[u8]) -> IMAPResult<&[u8], Vec<IString>> {
370    alt((
371        map(nstring, |nstring| match nstring.0 {
372            Some(item) => vec![item],
373            None => vec![],
374        }),
375        delimited(tag(b"("), separated_list1(sp, string), tag(b")")),
376    ))(input)
377}
378
379#[inline]
380/// `body-fld-loc = nstring`
381pub(crate) fn body_fld_loc(input: &[u8]) -> IMAPResult<&[u8], NString> {
382    nstring(input)
383}
384
385/// Future expansion.
386///
387/// Client implementations MUST accept body-extension fields.
388/// Server implementations MUST NOT generate body-extension fields except as defined by
389/// future standard or standards-track revisions of this specification.
390///
391/// ```abnf
392/// body-extension = nstring /
393///                  number /
394///                  "(" body-extension *(SP body-extension) ")"
395/// ```
396///
397/// Note: This parser is recursively defined. Thus, in order to not overflow the stack,
398/// it is needed to limit how may recursions are allowed. (8 should suffice).
399pub(crate) fn body_extension(
400    remaining_recursions: usize,
401) -> impl Fn(&[u8]) -> IMAPResult<&[u8], BodyExtension> {
402    move |input: &[u8]| body_extension_limited(input, remaining_recursions)
403}
404
405fn body_extension_limited<'a>(
406    input: &'a [u8],
407    remaining_recursion: usize,
408) -> IMAPResult<&'a [u8], BodyExtension> {
409    if remaining_recursion == 0 {
410        return Err(nom::Err::Failure(IMAPParseError {
411            input,
412            kind: IMAPErrorKind::RecursionLimitExceeded,
413        }));
414    }
415
416    let body_extension =
417        move |input: &'a [u8]| body_extension_limited(input, remaining_recursion.saturating_sub(1));
418
419    alt((
420        map(nstring, BodyExtension::NString),
421        map(number, BodyExtension::Number),
422        map(
423            delimited(tag(b"("), separated_list1(sp, body_extension), tag(b")")),
424            |body_extensions| BodyExtension::List(NonEmptyVec::unvalidated(body_extensions)),
425        ),
426    ))(input)
427}
428
429// ---
430
431/// `body-type-mpart = 1*body SP media-subtype [SP body-ext-mpart]`
432///
433/// Note: This parser is recursively defined. Thus, in order to not overflow the stack,
434/// it is needed to limit how may recursions are allowed.
435fn body_type_mpart_limited(
436    input: &[u8],
437    remaining_recursion: usize,
438) -> IMAPResult<&[u8], BodyStructure> {
439    if remaining_recursion == 0 {
440        return Err(nom::Err::Failure(IMAPParseError {
441            input,
442            kind: IMAPErrorKind::RecursionLimitExceeded,
443        }));
444    }
445
446    let mut parser = tuple((
447        many1(body(remaining_recursion)),
448        sp,
449        media_subtype,
450        opt(preceded(sp, body_ext_mpart)),
451    ));
452
453    let (remaining, (bodies, _, subtype, extension_data)) = parser(input)?;
454
455    Ok((
456        remaining,
457        BodyStructure::Multi {
458            // Safety: `unwrap` can't panic due to the use of `many1`.
459            bodies: NonEmptyVec::try_from(bodies).unwrap(),
460            subtype,
461            extension_data,
462        },
463    ))
464}
465
466/// ```abnf
467/// body-ext-mpart = body-fld-param
468///                   [SP body-fld-dsp
469///                     [SP body-fld-lang
470///                       [SP body-fld-loc *(SP body-extension)]
471///                     ]
472///                   ]
473/// ```
474///
475/// Note: MUST NOT be returned on non-extensible "BODY" fetch.
476pub(crate) fn body_ext_mpart(input: &[u8]) -> IMAPResult<&[u8], MultiPartExtensionData> {
477    map(
478        tuple((
479            body_fld_param,
480            opt(map(
481                tuple((
482                    preceded(sp, body_fld_dsp),
483                    opt(map(
484                        tuple((
485                            preceded(sp, body_fld_lang),
486                            opt(map(
487                                tuple((
488                                    preceded(sp, body_fld_loc),
489                                    many0(preceded(sp, body_extension(8))),
490                                )),
491                                |(location, extensions)| Location {
492                                    location,
493                                    extensions,
494                                },
495                            )),
496                        )),
497                        |(language, tail)| Language { language, tail },
498                    )),
499                )),
500                |(disposition, tail)| Disposition { disposition, tail },
501            )),
502        )),
503        |(parameter_list, tail)| MultiPartExtensionData {
504            parameter_list,
505            tail,
506        },
507    )(input)
508}
509
510// ---
511
512/// `media-basic = (
513///                  ( DQUOTE
514///                    (
515///                      "APPLICATION" /
516///                      "AUDIO" /
517///                      "IMAGE" /
518///                      "MESSAGE" /
519///                      "VIDEO"
520///                    ) DQUOTE
521///                  ) / string
522///                ) SP media-subtype`
523///
524/// Simplified...
525///
526/// `media-basic = string SP media-subtype`
527///
528/// TODO: Why the special case?
529///
530/// Defined in [MIME-IMT]
531pub(crate) fn media_basic(input: &[u8]) -> IMAPResult<&[u8], (IString, IString)> {
532    let mut parser = tuple((string, sp, media_subtype));
533
534    let (remaining, (type_, _, subtype)) = parser(input)?;
535
536    Ok((remaining, (type_, subtype)))
537}
538
539#[inline]
540/// `media-subtype = string`
541///
542/// Defined in [MIME-IMT]
543pub(crate) fn media_subtype(input: &[u8]) -> IMAPResult<&[u8], IString> {
544    string(input)
545}
546
547#[inline]
548/// `media-message = DQUOTE "MESSAGE" DQUOTE SP
549///                  DQUOTE "RFC822" DQUOTE`
550///
551/// Simplified:
552///
553/// `media-message = "\"MESSAGE\" \"RFC822\""`
554///
555/// Defined in [MIME-IMT]
556///
557/// "message" "rfc822" basic specific-for-message-rfc822 extension
558pub(crate) fn media_message(input: &[u8]) -> IMAPResult<&[u8], &[u8]> {
559    tag_no_case(b"\"MESSAGE\" \"RFC822\"")(input)
560}
561
562/// `media-text = DQUOTE "TEXT" DQUOTE SP media-subtype`
563///
564/// Defined in [MIME-IMT]
565///
566/// "text" "?????" basic specific-for-text extension
567pub(crate) fn media_text(input: &[u8]) -> IMAPResult<&[u8], IString> {
568    let mut parser = preceded(tag_no_case(b"\"TEXT\" "), media_subtype);
569
570    let (remaining, media_subtype) = parser(input)?;
571
572    Ok((remaining, media_subtype))
573}
574
575#[cfg(test)]
576mod tests {
577    use std::num::NonZeroU32;
578
579    use imap_types::{
580        core::{Literal, Quoted},
581        fetch::MessageDataItem,
582        response::{Data, Response},
583    };
584
585    use super::*;
586    use crate::testing::{kat_inverse_response, known_answer_test_encode};
587
588    #[test]
589    fn test_parse_media_basic() {
590        media_basic(b"\"application\" \"xxx\"").unwrap();
591        media_basic(b"\"unknown\" \"test\"").unwrap();
592        media_basic(b"\"x\" \"xxx\"").unwrap();
593    }
594
595    #[test]
596    fn test_parse_media_message() {
597        media_message(b"\"message\" \"rfc822\"").unwrap();
598    }
599
600    #[test]
601    fn test_parse_media_text() {
602        media_text(b"\"text\" \"html\"").unwrap();
603    }
604
605    #[test]
606    fn test_parse_body_ext_1part() {
607        for test in [
608            b"nil|xxx".as_ref(),
609            b"\"md5\"|xxx".as_ref(),
610            b"\"md5\" nil|xxx".as_ref(),
611            b"\"md5\" (\"dsp\" nil)|xxx".as_ref(),
612            b"\"md5\" (\"dsp\" (\"key\" \"value\")) nil|xxx".as_ref(),
613            b"\"md5\" (\"dsp\" (\"key\" \"value\")) \"swedish\"|xxx".as_ref(),
614            b"\"md5\" (\"dsp\" (\"key\" \"value\")) (\"german\" \"russian\")|xxx".as_ref(),
615            b"\"md5\" (\"dsp\" (\"key\" \"value\")) (\"german\" \"russian\") nil|xxx".as_ref(),
616            b"\"md5\" (\"dsp\" (\"key\" \"value\")) (\"german\" \"russian\") \"loc\"|xxx".as_ref(),
617            b"\"md5\" (\"dsp\" (\"key\" \"value\")) (\"german\" \"russian\") \"loc\" (1 \"2\" (nil 4))|xxx".as_ref(),
618            b"\"AABB\" NIL NIL NIL 1337|xxx",
619            b"\"AABB\" NIL NIL NIL (1337)|xxx",
620            b"\"AABB\" NIL NIL NIL (1337 1337)|xxx",
621            b"\"AABB\" NIL NIL NIL (1337 (1337 (1337 \"FOO\" {0}\r\n)))|xxx",
622        ]
623        .iter()
624        {
625            let (rem, out) = body_ext_1part(test).unwrap();
626            println!("{:?}", out);
627            assert_eq!(rem, b"|xxx");
628        }
629    }
630
631    #[test]
632    fn test_body_rec() {
633        let _ = body(8)(str::repeat("(", 1_000_000).as_bytes());
634    }
635
636    #[test]
637    fn test_parse_body_ext_mpart() {
638        for test in [
639            b"nil|xxx".as_ref(),
640            b"(\"key\" \"value\")|xxx".as_ref(),
641            b"(\"key\" \"value\") nil|xxx".as_ref(),
642            b"(\"key\" \"value\") (\"dsp\" nil)|xxx".as_ref(),
643            b"(\"key\" \"value\") (\"dsp\" (\"key\" \"value\")) nil|xxx".as_ref(),
644            b"(\"key\" \"value\") (\"dsp\" (\"key\" \"value\")) \"swedish\"|xxx".as_ref(),
645            b"(\"key\" \"value\") (\"dsp\" (\"key\" \"value\")) (\"german\" \"russian\")|xxx".as_ref(),
646            b"(\"key\" \"value\") (\"dsp\" (\"key\" \"value\")) (\"german\" \"russian\") nil|xxx".as_ref(),
647            b"(\"key\" \"value\") (\"dsp\" (\"key\" \"value\")) (\"german\" \"russian\") \"loc\"|xxx".as_ref(),
648            b"(\"key\" \"value\") (\"dsp\" (\"key\" \"value\")) (\"german\" \"russian\") \"loc\" (1 \"2\" (nil 4))|xxx".as_ref(),
649            b"(\"key\" \"value\") (\"dsp\" (\"key\" \"value\")) (\"german\" \"russian\") \"loc\" (1 \"2\" (nil 4) {0}\r\n)|xxx".as_ref(),
650            b"(\"key\" \"value\") (\"dsp\" (\"key\" \"value\")) (\"german\" \"russian\") \"loc\" {0}\r\n {0}\r\n|xxx".as_ref(),
651        ]
652            .iter()
653        {
654            let (rem, out) = body_ext_mpart(test).unwrap();
655            println!("{:?}", out);
656            assert_eq!(rem, b"|xxx");
657        }
658    }
659
660    #[test]
661    fn test_parse_body() {
662        dbg!(body(9)(b"((((((({0}\r\n {0}\r\n NIL NIL NIL {0}\r\n 0 \"FOO\" NIL NIL \"LOCATION\" 1337) \"mixed\") \"mixed\") \"mixed\") \"mixed\") \"mixed\") \"mixed\")|xxx").unwrap());
663    }
664
665    #[test]
666    fn test_kat_inverse_response_data() {
667        kat_inverse_response(&[(
668            b"* 3372220415 FETCH (BODYSTRUCTURE ((((((({0}\r\n {0}\r\n NIL NIL NIL {0}\r\n 0 \"FOO\" NIL NIL \"LOCATION\" 1337) \"mixed\") \"mixed\") \"mixed\") \"mixed\") \"mixed\") \"mixed\"))\r\n".as_ref(),
669            b"".as_ref(),
670            Response::Data(Data::Fetch {
671                seq: NonZeroU32::try_from(3372220415).unwrap(),
672                items: NonEmptyVec::from(MessageDataItem::BodyStructure(
673                    BodyStructure::Multi {
674                        bodies: NonEmptyVec::from(BodyStructure::Multi {
675                            bodies: NonEmptyVec::from(BodyStructure::Multi {
676                                bodies: NonEmptyVec::from(BodyStructure::Multi {
677                                    bodies: NonEmptyVec::from(BodyStructure::Multi {
678                                        bodies: NonEmptyVec::from(BodyStructure::Multi {
679                                            bodies: NonEmptyVec::from(BodyStructure::Single {
680                                                body: Body {
681                                                    basic: BasicFields {
682                                                        parameter_list: vec![],
683                                                        id: NString(None),
684                                                        description: NString(None),
685                                                        content_transfer_encoding: IString::from(
686                                                            Literal::try_from(b"".as_ref())
687                                                                .unwrap(),
688                                                        ),
689                                                        size: 0,
690                                                    },
691                                                    specific: SpecificFields::Basic {
692                                                        r#type: IString::from(
693                                                            Literal::try_from(b"".as_ref())
694                                                                .unwrap(),
695                                                        ),
696                                                        subtype: IString::from(
697                                                            Literal::try_from(b"".as_ref())
698                                                                .unwrap(),
699                                                        ),
700                                                    },
701                                                },
702                                                extension_data: Some(SinglePartExtensionData {
703                                                    md5: NString::try_from("FOO").unwrap(),
704                                                    tail: Some(Disposition{
705                                                        disposition: None,
706                                                        tail: Some(Language {
707                                                            language: vec![],
708                                                            tail: Some(Location{
709                                                                location: NString::try_from("LOCATION").unwrap(),
710                                                                extensions: vec![BodyExtension::Number(1337)],
711                                                            })
712                                                        })
713                                                    })
714                                                }),
715                                            }),
716                                            subtype: IString::try_from("mixed").unwrap(),
717                                            extension_data: None,
718                                        }),
719                                        subtype: IString::try_from("mixed").unwrap(),
720                                        extension_data: None,
721                                    }),
722                                    subtype: IString::try_from("mixed").unwrap(),
723                                    extension_data: None,
724                                }),
725                                subtype: IString::try_from("mixed").unwrap(),
726                                extension_data: None,
727                            }),
728                            subtype: IString::try_from("mixed").unwrap(),
729                            extension_data: None,
730                        }),
731                        subtype: IString::try_from("mixed").unwrap(),
732                        extension_data: None,
733                    },
734                )),
735            }),
736        )]);
737    }
738
739    #[test]
740    fn test_encode_single_part_extension_data() {
741        let tests = [(
742            SinglePartExtensionData {
743                md5: NString(None),
744                tail: Some(Disposition {
745                    disposition: None,
746                    tail: Some(Language {
747                        language: vec![],
748                        tail: Some(Location {
749                            location: NString::from(Quoted::try_from("").unwrap()),
750                            extensions: vec![],
751                        }),
752                    }),
753                }),
754            },
755            b"NIL NIL NIL \"\"".as_ref(),
756        )];
757
758        for test in tests {
759            known_answer_test_encode(test);
760        }
761    }
762
763    #[test]
764    fn test_number_quirk() {
765        assert_eq!(body_fld_octets(b"0)").unwrap().1, 0);
766        assert_eq!(body_fld_octets(b"1)").unwrap().1, 1);
767
768        #[cfg(not(feature = "quirk_rectify_numbers"))]
769        {
770            assert!(dbg!(body_fld_octets(b"-0)")).is_err());
771            assert!(body_fld_octets(b"-1)").is_err());
772            assert!(body_fld_octets(b"-999999)").is_err());
773        }
774
775        #[cfg(feature = "quirk_rectify_numbers")]
776        {
777            assert_eq!(body_fld_octets(b"-0)").unwrap().1, 0);
778            assert_eq!(body_fld_octets(b"-1)").unwrap().1, 0);
779            assert_eq!(body_fld_octets(b"-999999)").unwrap().1, 0);
780        }
781    }
782}