From 24c6607304323f0dd3c70bf952bfc5775735ca70 Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Fri, 15 Jul 2022 16:15:48 +0200 Subject: Some more FETCH things work --- Cargo.lock | 46 +++++++++ Cargo.toml | 1 + src/imap/mailbox_view.rs | 238 +++++++++++++++++++++++++++++++++++------------ src/mail/incoming.rs | 2 +- src/mail/mod.rs | 4 + src/mail_parser_tests.rs | 181 +++++++++++++++++++++++++++++++++++ src/main.rs | 6 +- tests/inject_emails.sh | 18 ++++ 8 files changed, 435 insertions(+), 61 deletions(-) create mode 100644 src/mail_parser_tests.rs create mode 100755 tests/inject_emails.sh diff --git a/Cargo.lock b/Cargo.lock index b2d5365..ac431e6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,6 +11,15 @@ dependencies = [ "nom 6.1.2", ] +[[package]] +name = "addr2line" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ecd88a8c8378ca913a680cd98f0f13ac67383d35993f86c90a70e3f137816b" +dependencies = [ + "gimli", +] + [[package]] name = "adler" version = "1.0.2" @@ -24,6 +33,7 @@ dependencies = [ "anyhow", "argon2", "async-trait", + "backtrace", "base64", "boitalettres", "chrono", @@ -343,6 +353,21 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +[[package]] +name = "backtrace" +version = "0.3.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab84319d616cfb654d03394f38ab7e6f0919e181b1b57e1fd15e7fb4077d9a7" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + [[package]] name = "base64" version = "0.13.0" @@ -963,6 +988,12 @@ dependencies = [ "wasi 0.11.0+wasi-snapshot-preview1", ] +[[package]] +name = "gimli" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78cc372d058dcf6d5ecd98510e7fbc9e5aec4d21de70f65fea8fecebcd881bd4" + [[package]] name = "gloo-timers" version = "0.2.4" @@ -1499,6 +1530,15 @@ dependencies = [ "libc", ] +[[package]] +name = "object" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21158b2c33aa6d4561f1c0a6ea283ca92bc54802a93b263e910746d679a7eb53" +dependencies = [ + "memchr", +] + [[package]] name = "once_cell" version = "1.12.0" @@ -1951,6 +1991,12 @@ dependencies = [ "tokio", ] +[[package]] +name = "rustc-demangle" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342" + [[package]] name = "rustc_version" version = "0.4.0" diff --git a/Cargo.toml b/Cargo.toml index 5398f34..b3ca40d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,7 @@ description = "Encrypted mail storage over Garage" anyhow = "1.0.28" argon2 = "0.3" async-trait = "0.1" +backtrace = "0.3" base64 = "0.13" clap = { version = "3.1.18", features = ["derive", "env"] } duplexify = "1.1.0" diff --git a/src/imap/mailbox_view.rs b/src/imap/mailbox_view.rs index d801580..6182cbc 100644 --- a/src/imap/mailbox_view.rs +++ b/src/imap/mailbox_view.rs @@ -274,30 +274,28 @@ impl MailboxView { FetchAttribute::Rfc822Size => { attributes.push(MessageAttribute::Rfc822Size(meta.rfc822_size as u32)) } - FetchAttribute::Rfc822Header => attributes.push( - MessageAttribute::Rfc822Header(NString(Some(IString::Literal( - meta.headers - .clone() - .try_into() - .or(Err(Error::msg("IString conversion error")))?, - )))), - ), + FetchAttribute::Rfc822Header => { + attributes.push(MessageAttribute::Rfc822Header(NString( + meta.headers.to_vec().try_into().ok().map(IString::Literal), + ))) + } FetchAttribute::Rfc822Text => { let r = parsed .raw_message.get(parsed.offset_body..parsed.offset_end) - .ok_or(Error::msg("Unable to extract email body, cursors out of bound. This is a bug."))? - .try_into() - .or(Err(Error::msg("IString conversion error")))?; + .ok_or(Error::msg("Unable to extract email body, cursors out of bound. This is a bug."))?; - attributes.push(MessageAttribute::Rfc822Text(NString(Some( - IString::Literal(r), - )))) - } - FetchAttribute::Rfc822 => { - attributes.push(MessageAttribute::Rfc822(NString(Some(IString::Literal( - body.as_ref().unwrap().clone().try_into().unwrap(), - ))))) + attributes.push(MessageAttribute::Rfc822Text(NString( + r.try_into().ok().map(IString::Literal), + ))); } + FetchAttribute::Rfc822 => attributes.push(MessageAttribute::Rfc822(NString( + body.as_ref() + .unwrap() + .clone() + .try_into() + .ok() + .map(IString::Literal), + ))), FetchAttribute::Envelope => { attributes.push(MessageAttribute::Envelope(message_envelope(&parsed))) } @@ -313,49 +311,48 @@ impl MailboxView { peek, } => { // @TODO Add missing section specifiers - let text = match section { - Some(FetchSection::Text(None)) => { - parsed - .raw_message.get(parsed.offset_body..parsed.offset_end) - .ok_or(Error::msg("Unable to extract email body, cursors out of bound. This is a bug."))? + match get_message_section(&parsed, section) { + Ok(text) => { + let seen_flag = Flag::Seen.to_string(); + if !peek && !flags.iter().any(|x| *x == seen_flag) { + // Add \Seen flag + self.mailbox.add_flags(uuid, &[seen_flag]).await?; + } + + let (text, origin) = match partial { + Some((begin, len)) => { + if *begin as usize > text.len() { + (&[][..], Some(*begin)) + } else if (*begin + len.get()) as usize >= text.len() { + (&text[*begin as usize..], Some(*begin)) + } else { + ( + &text[*begin as usize + ..(*begin + len.get()) as usize], + Some(*begin), + ) + } + } + None => (&text[..], None), + }; + + let data = + NString(text.to_vec().try_into().ok().map(IString::Literal)); + attributes.push(MessageAttribute::BodyExt { + section: section.clone(), + origin, + data, + }) } - Some(FetchSection::Header(None)) => { - parsed - .raw_message.get(..parsed.offset_body) - .ok_or(Error::msg("Unable to extract email body, cursors out of bound. This is a bug."))? + Err(e) => { + tracing::error!( + "Could not get section {:?} of message {}: {}", + section, + uuid, + e + ); } - None => &parsed.raw_message, - _ => bail!("Unimplemented: section {:?}", section), - }; - - let seen_flag = Flag::Seen.to_string(); - if !peek && !flags.iter().any(|x| *x == seen_flag) { - // Add \Seen flag - self.mailbox.add_flags(uuid, &[seen_flag]).await?; } - - let (text, origin) = match partial { - Some((begin, len)) => { - if *begin as usize > text.len() { - (&[][..], Some(*begin)) - } else if (*begin + len.get()) as usize >= text.len() { - (&text[*begin as usize..], Some(*begin)) - } else { - ( - &text[*begin as usize..(*begin + len.get()) as usize], - Some(*begin), - ) - } - } - None => (text, None), - }; - - let is = IString::try_from(std::str::from_utf8(text)?).unwrap(); - attributes.push(MessageAttribute::BodyExt { - section: section.clone(), - origin, - data: NString(Some(is)), - }) } FetchAttribute::InternalDate => { attributes.push(MessageAttribute::InternalDate(MyDateTime( @@ -972,6 +969,129 @@ fn headers_to_basic_fields<'a, T>(bp: &'a Part) -> Result<(SpecialAttrs<'a>, Ok((attrs, bf)) } +fn get_message_section<'a>( + parsed: &'a Message<'a>, + section: &Option, +) -> Result> { + match section { + Some(FetchSection::Text(None)) => Ok(parsed + .raw_message + .get(parsed.offset_body..parsed.offset_end) + .ok_or(Error::msg( + "Unable to extract email body, cursors out of bound. This is a bug.", + ))? + .into()), + Some(FetchSection::Text(Some(part))) => { + subpart_msg_fn(parsed, part.0.as_slice(), |part_msg| { + Ok(part_msg + .raw_message + .get(part_msg.offset_body..parsed.offset_end) + .ok_or(Error::msg( + "Unable to extract email body, cursors out of bound. This is a bug.", + ))? + .to_vec() + .into()) + }) + } + Some(FetchSection::Header(part)) => subpart_msg_fn( + parsed, + part.as_ref().map(|p| p.0.as_slice()).unwrap_or(&[]), + |part_msg| { + Ok(part_msg + .raw_message + .get(..part_msg.offset_body) + .ok_or(Error::msg( + "Unable to extract email header, cursors out of bound. This is a bug.", + ))? + .to_vec() + .into()) + }, + ), + Some(FetchSection::Part(part)) => subpart_fn(parsed, part.0.as_slice(), |_msg, part| { + let bytes = match part { + MessagePart::Text(p) | MessagePart::Html(p) => p.body.as_bytes().to_vec(), + MessagePart::Binary(p) | MessagePart::InlineBinary(p) => p.body.to_vec(), + MessagePart::Message(Part { + body: MessageAttachment::Raw(r), + .. + }) => r.to_vec(), + MessagePart::Message(Part { + body: MessageAttachment::Parsed(p), + .. + }) => p.raw_message.to_vec(), + MessagePart::Multipart(_) => bail!("Multipart part has no body"), + }; + Ok(bytes.into()) + }), + Some(FetchSection::Mime(part)) => subpart_fn(parsed, part.0.as_slice(), |msg, part| { + let raw_headers = match part { + MessagePart::Text(p) | MessagePart::Html(p) => &p.headers_raw, + MessagePart::Binary(p) | MessagePart::InlineBinary(p) => &p.headers_raw, + MessagePart::Message(p) => &p.headers_raw, + MessagePart::Multipart(m) => &m.headers_raw, + }; + let mut ret = vec![]; + for (name, body) in raw_headers { + ret.extend(name.as_str().as_bytes()); + ret.extend(b": "); + ret.extend(&msg.raw_message[body.start..body.end]); + } + ret.extend(b"\r\n"); + Ok(ret.into()) + }), + None => Ok(parsed.raw_message.clone()), + _ => bail!("Unimplemented: section {:?}", section), + } +} + +fn subpart_msg_fn<'a, F, R>(msg: &Message<'a>, path: &[NonZeroU32], f: F) -> Result +where + F: FnOnce(&Message<'_>) -> Result, +{ + if path.is_empty() { + f(msg) + } else { + let part = msg + .parts + .get(path[0].get() as usize - 1) + .ok_or(anyhow!("No such subpart: {}", path[0]))?; + if matches!(part, MessagePart::Message(_)) { + let part_msg = part + .parse_message() + .ok_or(anyhow!("Cannot parse subpart: {}", path[0]))?; + subpart_msg_fn(&part_msg, &path[1..], f) + } else { + bail!("Subpart is not a message: {}", path[0]); + } + } +} + +fn subpart_fn<'a, F, R>(msg: &Message<'a>, path: &[NonZeroU32], f: F) -> Result +where + F: FnOnce(&Message<'_>, &MessagePart<'_>) -> Result, +{ + if path.is_empty() { + bail!("Unexpected empty path"); + } else { + let part = msg + .parts + .get(path[0].get() as usize - 1) + .ok_or(anyhow!("No such subpart: {}", path[0]))?; + if path.len() == 1 { + f(msg, part) + } else { + if matches!(part, MessagePart::Message(_)) { + let part_msg = part + .parse_message() + .ok_or(anyhow!("Cannot parse subpart: {}", path[0]))?; + subpart_fn(&part_msg, &path[1..], f) + } else { + bail!("Subpart is not a message: {}", path[0]); + } + } + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/mail/incoming.rs b/src/mail/incoming.rs index 66513bf..8f074f5 100644 --- a/src/mail/incoming.rs +++ b/src/mail/incoming.rs @@ -392,7 +392,7 @@ async fn k2v_lock_loop_internal( let res = futures::try_join!(watch_lock_loop, lock_notify_loop, take_lock_loop); - info!("lock loop exited: {:?}, releasing", res); + info!("lock loop exited, releasing"); if !held_tx.is_closed() { warn!("wierd..."); diff --git a/src/mail/mod.rs b/src/mail/mod.rs index b6054b0..3b0ae73 100644 --- a/src/mail/mod.rs +++ b/src/mail/mod.rs @@ -1,4 +1,5 @@ use std::convert::TryFrom; +use std::io::Write; pub mod incoming; pub mod mailbox; @@ -17,6 +18,9 @@ impl<'a> TryFrom<&'a [u8]> for IMF<'a> { type Error = (); fn try_from(body: &'a [u8]) -> Result, ()> { + eprintln!("---- BEGIN PARSED MESSAGE ----"); + let _ = std::io::stderr().write_all(body); + eprintln!("---- END PARSED MESSAGE ----"); let parsed = mail_parser::Message::parse(body).ok_or(())?; Ok(Self { raw: body, parsed }) } diff --git a/src/mail_parser_tests.rs b/src/mail_parser_tests.rs new file mode 100644 index 0000000..56e394f --- /dev/null +++ b/src/mail_parser_tests.rs @@ -0,0 +1,181 @@ +use mail_parser::Message; + +#[test] +fn test1() { + let input = br#"Content-Type: multipart/mixed; boundary="1234567890123456789012345678901234567890123456789012345678901234567890123456789012" + +--1234567890123456789012345678901234567890123456789012345678901234567890123456789012 +Content-Type: multipart/mixed; boundary="123456789012345678901234567890123456789012345678901234567890123456789012345678901" + +--123456789012345678901234567890123456789012345678901234567890123456789012345678901 +Content-Type: multipart/mixed; boundary="12345678901234567890123456789012345678901234567890123456789012345678901234567890" + +--12345678901234567890123456789012345678901234567890123456789012345678901234567890 +Content-Type: text/plain + +1 +--1234567890123456789012345678901234567890123456789012345678901234567890123456789012 +Content-Type: text/plain + +22 +--123456789012345678901234567890123456789012345678901234567890123456789012345678901 +Content-Type: text/plain + +333 +--12345678901234567890123456789012345678901234567890123456789012345678901234567890 +Content-Type: text/plain + +4444 +"#; + + let message = Message::parse(input); + dbg!(message); +} + +#[test] +fn test2() { + let input = br#"Message-ID: <39235E1C.1DC7EA90@example.com> +Date: Wed, 17 May 2000 23:06:04 -0400 +From: Doug Sauder +X-Mailer: Mozilla 4.7 [en] (WinNT; I) +X-Accept-Language: en +MIME-Version: 1.0 +To: Joe Blow +Subject: Test message from Netscape Communicator 4.7 +Content-Type: multipart/mixed; + boundary="------------A1FCDEE154E03D875E5D6779" + +This is a multi-part message in MIME format. +--------------A1FCDEE154E03D875E5D6779 +Content-Type: text/plain; charset=iso-8859-1 +Content-Transfer-Encoding: quoted-printable + +Die Hasen und die Fr=F6sche + +Die Hasen klagten einst =FCber ihre mi=DFliche Lage; "wir leben", sprach = +ein +Redner, "in steter Furcht vor Menschen und Tieren, eine Beute der Hunde, +der Adler, ja fast aller Raubtiere! Unsere stete Angst ist =E4rger als de= +r +Tod selbst. Auf, la=DFt uns ein f=FCr allemal sterben." + +In einem nahen Teich wollten sie sich nun ers=E4ufen; sie eilten ihm zu; +allein das au=DFerordentliche Get=F6se und ihre wunderbare Gestalt +erschreckte eine Menge Fr=F6sche, die am Ufer sa=DFen, so sehr, da=DF sie= + aufs +schnellste untertauchten. + +"Halt", rief nun eben dieser Sprecher, "wir wollen das Ers=E4ufen noch ei= +n +wenig aufschieben, denn auch uns f=FCrchten, wie ihr seht, einige Tiere, +welche also wohl noch ungl=FCcklicher sein m=FCssen als wir." + + + +--------------A1FCDEE154E03D875E5D6779 +Content-Type: image/png; + name="redball.png" +Content-Transfer-Encoding: base64 +Content-Disposition: inline; + filename="redball.png" + +iVBORw0KGgoAAAANSUhEUgAAABsAAAAbCAMAAAC6CgRnAAADAFBMVEX///8AAAABAAALAAAV +AAAaAAAXAAARAAAKAAADAAAcAAAyAABEAABNAABIAAA9AAAjAAAWAAAmAABhAAB7AACGAACH +AAB9AAB0AABgAAA5AAAUAAAGAAAnAABLAABvAACQAAClAAC7AAC/AACrAAChAACMAABzAABb +AAAuAAAIAABMAAB3AACZAAC0GRnKODjVPT3bKSndBQW4AACoAAB5AAAxAAAYAAAEAABFAACa +AAC7JCTRYWHfhITmf3/mVlbqHx/SAAC5AACjAABdAABCAAAoAAAJAABnAAC6Dw/QVFTek5Pl +rKzpmZntZWXvJSXXAADBAACxAACcAABtAABTAAA2AAAbAAAFAABKAACBAADLICDdZ2fonJzr +pqbtiorvUVHvFBTRAADDAAC2AAB4AABeAABAAAAiAABXAACSAADCAADaGxvoVVXseHjveHjv +V1fvJibhAADOAAC3AACnAACVAABHAAArAAAPAACdAADFAADhBQXrKCjvPDzvNTXvGxvjAADQ +AADJAAC1AACXAACEAABsAABPAAASAAACAABiAADpAADvAgLnAADYAADLAAC6AACwAABwAAAT +AAAkAABYAADIAADTAADNAACzAACDAABuAAAeAAB+AADAAACkAACNAAB/AABpAABQAAAwAACR +AACpAAC8AACqAACbAABlAABJAAAqAAAOAAA0AACsAACvAACtAACmAACJAAB6AABrAABaAAA+ +AAApAABqAACCAACfAACeAACWAACPAAB8AAAZAAAHAABVAACOAACKAAA4AAAQAAA/AAByAACA +AABcAAA3AAAsAABmAABDAABWAAAgAAAzAAA8AAA6AAAfAAAMAAAdAAANAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD8 +LtlFAAAAAXRSTlMAQObYZgAAABZ0RVh0U29mdHdhcmUAZ2lmMnBuZyAyLjAuMT1evmgAAAII +SURBVHicY2CAg/8QwIABmJhZWFnZ2Dk4MaU5uLh5eHn5+LkFBDlQJf8zC/EIi4iKiUtI8koJ +Scsgyf5nlpWTV1BUUlZRVVPX4NFk1UJIyghp6+jq6RsYGhmbKJgK85mZW8Dk/rNaSlhZ29ja +2Ts4Ojkr6Li4urFDNf53N/Ow8vTy9vH18w8IDAoWDQkNC4+ASP5ni4wKio6JjYtPSExKTnFW +SE1LF4A69n9GZlZ2Tm5efkFhUXFySWlZlEd5RSVY7j+TkGRVdU1tXX1DY1Ozcktpa1t7h2Yn +OAj+d7l1tyo79vT29SdNSJ44SbFVdHIo9xSIHNPUaWqTpifNSJrZnK00S0U1a/acUG5piNz/ +uXLzVJ2qm6dXz584S2WB1cJFi5cshZr539xVftnyFKUVTi2TVjqvyhJLXb1m7TqoHPt6F/HW +0g0bN63crGqVtWXrtu07BJihcsw71+zanRW8Z89eq337RQ/Ip60xO3gIElX/LbikDm8T36Kw +bNmRo7O3zpHkPSZwHBqL//8flz1x2OOkyKJTi7aqbzutfUZI2gIuF8F2lr/D5dw2+fZdwpl8 +YVOlI+CJ4/9/joOyYed5QzMvhGqnm2V0WiClm///D0lfXHtJ6vLlK9w7rx7vQk5SQJbFtSms +1y9evXid7QZacgOxmSxktNzdtSwwU+J/VICaCPFIYU3XAJhIOtjf5sfyAAAAJXRFWHRDb21t +ZW50AGNsaXAyZ2lmIHYuMC42IGJ5IFl2ZXMgUGlndWV0NnM7vAAAAABJRU5ErkJggg== +--------------A1FCDEE154E03D875E5D6779 +Content-Type: image/png; + name="greenball.png" +Content-Transfer-Encoding: base64 +Content-Disposition: inline; + filename="greenball.png" + +iVBORw0KGgoAAAANSUhEUgAAABsAAAAbCAMAAAC6CgRnAAADAFBMVEX///8AAAAAEAAAGAAA +IQAACAAAMQAAQgAAUgAAWgAASgAIYwAIcwAIewAQjAAIawAAOQAAYwAQlAAQnAAhpQAQpQAh +rQBCvRhjxjFjxjlSxiEpzgAYvQAQrQAYrQAhvQCU1mOt1nuE1lJK3hgh1gAYxgAYtQAAKQBC +zhDO55Te563G55SU52NS5yEh3gAYzgBS3iGc52vW75y974yE71JC7xCt73ul3nNa7ykh5wAY +1gAx5wBS7yFr7zlK7xgp5wAp7wAx7wAIhAAQtQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAp +1fnZAAAAAXRSTlMAQObYZgAAABZ0RVh0U29mdHdhcmUAZ2lmMnBuZyAyLjAuMT1evmgAAAFt +SURBVHicddJtV8IgFAdwD2zIgMEE1+NcqdsoK+m5tCyz7/+ZiLmHsyzvq53zO/cy+N9ery1b +Ve9PWQA9z4MQ+H8Yoj7GASZ95IHfaBGmLOSchyIgyOu22mgQSjUcDuNYcoGjLiLK1cHh0fHJ +aTKKOcMItgYxT89OzsfjyTTLC8UF0c2ZNmKquJhczq6ub+YmSVUYRF59GeDastu7+9nD41Nm +kiJ2jc2J3kAWZ9Pr55fH18XSmRuKUTXUaqHy7O19tfr4NFle/w3YDrWRUIlZrL/W86XJkyJV +G9EaEjIx2XyZmZJGioeUaL+2AY8TY8omR6nkLKhu70zjUKVJXsp3quS2DVSJWNh3zzJKCyex +I0ZxBP3afE0ElyqOlZJyw8r3BE2SFiJCyxA434SCkg65RhdeQBljQtCg39LWrA90RDDG1EWr +YUO23hMANUKRRl61E529cR++D2G5LK002dr/qrcfu9u0V3bxn/XdhR/NYeeN0ggsLAAAACV0 +RVh0Q29tbWVudABjbGlwMmdpZiB2LjAuNiBieSBZdmVzIFBpZ3VldDZzO7wAAAAASUVORK5C +YII= +--------------A1FCDEE154E03D875E5D6779 +Content-Type: image/png; + name="blueball.png" +Content-Transfer-Encoding: base64 +Content-Disposition: inline; + filename="blueball.png" + +iVBORw0KGgoAAAANSUhEUgAAABsAAAAbCAMAAAC6CgRnAAADAFBMVEX///8AAAgAABAAABgA +AAAACCkAEEIAEEoACDEAEFIIIXMIKXsIKYQIIWsAGFoACDkIIWMQOZwYQqUYQq0YQrUQOaUQ +MZQAGFIQMYwpUrU5Y8Y5Y84pWs4YSs4YQs4YQr1Ca8Z7nNacvd6Mtd5jlOcxa94hUt4YStYY +QsYQMaUAACHO5+/n7++cxu9ShO8pWucQOa1Ke86tzt6lzu9ajO8QMZxahNat1ufO7++Mve9K +e+8YOaUYSsaMvee15++Uve8AAClajOdzpe9rnO8IKYwxY+8pWu8IIXsAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADB +Mg1VAAAAAXRSTlMAQObYZgAAABZ0RVh0U29mdHdhcmUAZ2lmMnBuZyAyLjAuMT1evmgAAAGI +SURBVHicddJtV5swGAbgEk6AJhBSk4bMCUynBSLaqovbrG/bfPn/vyh70lbsscebL5xznTsh +5BmNhgQoRChwo50EOIohUYLDj4zHhKYQkrEoQdvock4ne0IKMVUpKZLQDeqSTIsv+18PyqqW +Uw2IBsRM7307PPp+fDJrWtnpLDJvewYxnewfnvanZ+fzpmwXijC8KbqEa3Fx2ff91Y95U9XC +UpaDeQwiMpHXP/v+1++bWVPWQoGFawtjury9vru/f/C1Vi7ezT0WWpQHf/7+u/G71aLThK/M +jRxmT6KdzZ9fGk9yatMsTgZLl3XVgFRAC6spj/13enssqJVtWVa3NdBSacL8+VZmYqKmdd1C +SYoOiMOSGwtzlqqlFFIuOqv0a1ZEZrUkWICLLFW266y1KvWE1zV/iDAH1EopnVLCiygZCIom +H3NCKX0lnI+B1iuuzCGTxwXjnDO4d7NpbX42YJJHkBwmAm2TxwAZg40J3+Xtbv1rgOAZwG0N +xW62p+lT+Yi747sD/wEUVMzYmWkOvwAAACV0RVh0Q29tbWVudABjbGlwMmdpZiB2LjAuNiBi +eSBZdmVzIFBpZ3VldDZzO7wAAAAASUVORK5CYII= +--------------A1FCDEE154E03D875E5D6779-- +"#; + + let message = Message::parse(input).unwrap(); + //dbg!(&message); + let part = message.parts.get(0).unwrap(); + //dbg!(&part); + let part_msg = part.parse_message().unwrap(); +} diff --git a/src/main.rs b/src/main.rs index a4e22ff..50eb2ed 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,6 +9,9 @@ mod mail; mod server; mod time; +#[cfg(test)] +mod mail_parser_tests; + use std::path::PathBuf; use anyhow::{bail, Result}; @@ -121,7 +124,8 @@ async fn main() -> Result<()> { // Abort on panic (same behavior as in Go) std::panic::set_hook(Box::new(|panic_info| { - tracing::error!("{}", panic_info.to_string()); + eprintln!("{}", panic_info.to_string()); + eprintln!("{:?}", backtrace::Backtrace::new()); std::process::abort(); })); diff --git a/tests/inject_emails.sh b/tests/inject_emails.sh new file mode 100755 index 0000000..1b1f5c7 --- /dev/null +++ b/tests/inject_emails.sh @@ -0,0 +1,18 @@ +#!/bin/sh + +cd $(dirname $0) + +function mail_lmtp_session ( + echo -e "LHLO localhost\r" + for mail in $(find emails -name '*.eml'); do + echo -e "MAIL FROM: \r" + echo -e "RCPT TO: \r" + echo -e "DATA\r" + cat $mail + echo -e "\r" + echo -e ".\r" + done + echo -e "QUIT\r" +) + +mail_lmtp_session | tee >(nc localhost 12024) -- cgit v1.2.3