diff --git a/src/imap/mailbox_view.rs b/src/imap/mailbox_view.rs index b0ad95a..70dedf0 100644 --- a/src/imap/mailbox_view.rs +++ b/src/imap/mailbox_view.rs @@ -1,4 +1,5 @@ use std::borrow::Borrow; +use std::collections::HashMap; use std::num::NonZeroU32; use std::sync::Arc; @@ -278,64 +279,12 @@ impl MailboxView { FetchAttribute::Envelope => { attributes.push(MessageAttribute::Envelope(message_envelope(&parsed))) } - FetchAttribute::Body => { - /* - * CAPTURE: - b fetch 29878:29879 (BODY) - * 29878 FETCH (BODY (("text" "plain" ("charset" "utf-8") NIL NIL "quoted-printable" 3264 82)("text" "html" ("charset" "utf-8") NIL NIL "quoted-printable" 31834 643) "alternative")) - * 29879 FETCH (BODY ("text" "html" ("charset" "us-ascii") NIL NIL "7bit" 4107 131)) - ^^^^^^^^^^^^^^^^^^^^^^ ^^^ ^^^ ^^^^^^ ^^^^ ^^^ - | | | | | | number of lines - | | | | | size - | | | | content transfer encoding - | | | description - | | id - | parameter list - b OK Fetch completed (0.001 + 0.000 secs). - * - */ - - /*match parsed.structure { - Part(part_id) => { - match parsed.parts.get(part_id)? { - Text( - let fb = FetchBody { - parameter_list: vec![], - id: NString(None), - descritpion: NString(None), - // Default value is 7bit" - // https://datatracker.ietf.org/doc/html/rfc2045#section-6.1 - content_transfer_encoding: IString::try_from("7bit").unwrap(), - }; - } - } - List(part_list) => todo!(), - MultiPart((first, rest)) => todo!(), - }*/ - - // @TODO This is a stub - let is = IString::try_from("test").unwrap(); - let b = BodyStructure::Single { - body: FetchBody { - basic: BasicFields { - parameter_list: vec![], - id: NString(Some(is.clone())), - description: NString(Some(is.clone())), - content_transfer_encoding: is.clone(), - size: 1, - }, - specific: SpecificFields::Text { - // @FIXME I do not understand yet how this part works - subtype: is, - number_of_lines: 1, - }, - }, - // Always None for Body, can be populated for BodyStructure - extension: None, - }; - - attributes.push(MessageAttribute::Body(b)); - } + FetchAttribute::Body => attributes.push(MessageAttribute::Body( + build_imap_email_struct(&parsed, &parsed.structure)?, + )), + FetchAttribute::BodyStructure => attributes.push(MessageAttribute::Body( + build_imap_email_struct(&parsed, &parsed.structure)?, + )), FetchAttribute::BodyExt { section, partial, @@ -350,30 +299,6 @@ impl MailboxView { data: NString(Some(is)), }) } - FetchAttribute::BodyStructure => { - // @TODO This is a stub - let is = IString::try_from("test").unwrap(); - let b = BodyStructure::Single { - body: FetchBody { - basic: BasicFields { - parameter_list: vec![], - id: NString(Some(is.clone())), - description: NString(Some(is.clone())), - content_transfer_encoding: is.clone(), - size: 1, - }, - specific: SpecificFields::Text { - // @FIXME I do not understand yet how this part works - subtype: is, - number_of_lines: 1, - }, - }, - // Always None for Body, can be populated for BodyStructure - extension: None, - }; - - attributes.push(MessageAttribute::BodyStructure(b)); - } FetchAttribute::InternalDate => { attributes.push(MessageAttribute::InternalDate(MyDateTime( Utc.fix() @@ -564,6 +489,20 @@ fn convert_address(a: &mail_parser::Addr<'_>) -> Address { ) } +/* +--CAPTURE-- +b fetch 29878:29879 (BODY) +* 29878 FETCH (BODY (("text" "plain" ("charset" "utf-8") NIL NIL "quoted-printable" 3264 82)("text" "html" ("charset" "utf-8") NIL NIL "quoted-printable" 31834 643) "alternative")) +* 29879 FETCH (BODY ("text" "html" ("charset" "us-ascii") NIL NIL "7bit" 4107 131)) + ^^^^^^^^^^^^^^^^^^^^^^ ^^^ ^^^ ^^^^^^ ^^^^ ^^^ + | | | | | | number of lines + | | | | | size + | | | | content transfer encoding + | | | description + | | id + | parameter list +b OK Fetch completed (0.001 + 0.000 secs). +*/ fn build_imap_email_struct<'a>( msg: &Message<'a>, node: &MessageStructure, @@ -574,47 +513,89 @@ fn build_imap_email_struct<'a>( "Email part referenced in email structure is missing" ))?; match part { - MessagePart::Text(bp) => Ok(BodyStructure::Single { - body: FetchBody { - basic: BasicFields { - parameter_list: vec![], //@TODO - id: match bp.headers_rfc.get(&RfcHeader::ContentId) { - Some(HeaderValue::Text(v)) => { - NString(IString::try_from(v.clone().into_owned()).ok()) - } - _ => NString(None), - }, - description: NString(None), //@TODO - content_transfer_encoding: match bp - .headers_rfc - .get(&RfcHeader::ContentTransferEncoding) - { - Some(HeaderValue::Text(v)) => { - IString::try_from(v.clone().into_owned()) - .unwrap_or(unchecked_istring("7bit")) - } - _ => unchecked_istring("7bit"), - }, - size: u32::try_from(bp.len())?, - }, - specific: SpecificFields::Text { - subtype: match bp.headers_rfc.get(&RfcHeader::ContentType) { - Some(HeaderValue::ContentType(ContentType { - c_subtype: Some(st), - .. - })) => IString::try_from(st.clone().into_owned()) - .unwrap_or(unchecked_istring("plain")), - _ => unchecked_istring("plain"), - }, - number_of_lines: u32::try_from(bp.get_text_contents().lines().count())?, - }, - }, - extension: None, - }), MessagePart::Multipart(_) => { unreachable!("A multipart entry can not be found here.") } - _ => todo!(), + MessagePart::Text(bp) => { + // Extract subtype+attributes from Content-Type header + let (st, ct_attrs) = match bp.headers_rfc.get(&RfcHeader::ContentType) { + Some(HeaderValue::ContentType(c)) => { + (c.c_subtype.as_ref(), c.attributes.as_ref()) + } + _ => (None, None), + }; + + // Transform the Content-Type attributes into IMAP's parameter list + // Also check if there is a charset defined. + let (charset_found, mut parameter_list) = ct_attrs + .map(|attr| { + attr.iter().fold( + (false, vec![]), + |(charset_found, mut param_list), (k, v)| { + let nk = k.to_lowercase(); + match ( + IString::try_from(k.as_ref()), + IString::try_from(v.as_ref()), + ) { + (Ok(ik), Ok(iv)) => param_list.push((ik, iv)), + _ => (), + }; + + (charset_found || nk == "charset", param_list) + }, + ) + }) + .unwrap_or((false, vec![])); + + // If the charset is not defined, set it to "us-ascii" + if !charset_found { + parameter_list + .push((unchecked_istring("charset"), unchecked_istring("us-ascii"))); + } + + // If the subtype is not defined, set it to "plain" + let subtype = st + .map(|st| IString::try_from(st.clone().into_owned()).ok()) + .flatten() + .unwrap_or(unchecked_istring("plain")); + + Ok(BodyStructure::Single { + body: FetchBody { + basic: BasicFields { + parameter_list, + id: match bp.headers_rfc.get(&RfcHeader::ContentId) { + Some(HeaderValue::Text(v)) => { + NString(IString::try_from(v.clone().into_owned()).ok()) + } + _ => NString(None), + }, + description: NString(None), //@TODO + content_transfer_encoding: match bp + .headers_rfc + .get(&RfcHeader::ContentTransferEncoding) + { + Some(HeaderValue::Text(v)) => { + IString::try_from(v.clone().into_owned()) + .unwrap_or(unchecked_istring("7bit")) + } + _ => unchecked_istring("7bit"), + }, + size: u32::try_from(bp.len())?, + }, + specific: SpecificFields::Text { + subtype, + number_of_lines: u32::try_from( + bp.get_text_contents().lines().count(), + )?, + }, + }, + extension: None, + }) + }, + MessagePart::Html(_) => todo!(), + MessagePart::Binary(_) => todo!(), + MessagePart::InlineBinary(_) => todo!(), + MessagePart::Message(_) => todo!(), } } MessageStructure::List(l) => todo!(),