aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock46
-rw-r--r--Cargo.toml1
-rw-r--r--src/imap/mailbox_view.rs238
-rw-r--r--src/mail/incoming.rs2
-rw-r--r--src/mail/mod.rs4
-rw-r--r--src/mail_parser_tests.rs181
-rw-r--r--src/main.rs6
-rwxr-xr-xtests/inject_emails.sh18
8 files changed, 435 insertions, 61 deletions
diff --git a/Cargo.lock b/Cargo.lock
index b2d5365..ac431e6 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -12,6 +12,15 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -24,6 +33,7 @@ dependencies = [
"anyhow",
"argon2",
"async-trait",
+ "backtrace",
"base64",
"boitalettres",
"chrono",
@@ -344,6 +354,21 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -964,6 +989,12 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1500,6 +1531,15 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1952,6 +1992,12 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
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<T>) -> Result<(SpecialAttrs<'a>,
Ok((attrs, bf))
}
+fn get_message_section<'a>(
+ parsed: &'a Message<'a>,
+ section: &Option<FetchSection>,
+) -> Result<Cow<'a, [u8]>> {
+ 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<R>
+where
+ F: FnOnce(&Message<'_>) -> Result<R>,
+{
+ 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<R>
+where
+ F: FnOnce(&Message<'_>, &MessagePart<'_>) -> Result<R>,
+{
+ 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<IMF<'a>, ()> {
+ 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 <dwsauder@example.com>
+X-Mailer: Mozilla 4.7 [en] (WinNT; I)
+X-Accept-Language: en
+MIME-Version: 1.0
+To: Joe Blow <blow@example.com>
+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: <alex@adnab.me>\r"
+ echo -e "RCPT TO: <lx@staging.deuxfleurs.org>\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)