diff --git a/Cargo.toml b/Cargo.toml index 75649b5..d213fb5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,7 @@ edition = "2021" [dependencies] digest = "0.10.7" sha2 = "0.10.6" +time = "0.3.21" x509-parser = "0.15.0" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/src/lib.rs b/src/lib.rs index 6a4b595..a67b410 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,6 +2,8 @@ mod certificate_store; mod fingerprint; mod host; +mod mailbox; +mod message; pub mod prelude; mod receiver; mod request; diff --git a/src/mailbox/error.rs b/src/mailbox/error.rs new file mode 100644 index 0000000..524aee2 --- /dev/null +++ b/src/mailbox/error.rs @@ -0,0 +1,35 @@ +use {crate::prelude::ParseHostError, std::fmt}; + +#[derive(Debug, PartialEq)] +/// Errors which can occur when parsing a request +pub enum Error { + MissingSeparator, + EmptyUser, + EmptyHost, + ParseHostError(ParseHostError), +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::ParseHostError(e) => write!(f, "{e}"), + _ => write!(f, "{self:?}"), + } + } +} + +impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Self::ParseHostError(e) => Some(e), + _ => None, + } + } +} + +impl From for Error { + fn from(value: ParseHostError) -> Self { + Self::ParseHostError(value) + } +} + diff --git a/src/mailbox/mod.rs b/src/mailbox/mod.rs new file mode 100644 index 0000000..8ad0226 --- /dev/null +++ b/src/mailbox/mod.rs @@ -0,0 +1,56 @@ +use std::{fmt, str::FromStr}; +use crate::prelude::Host; + +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +mod error; +pub use error::Error; + +#[derive(Debug, Clone, PartialEq)] +#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))] +pub struct Mailbox { + pub username: String, + pub host: Host, + pub blurb: Option, +} + +impl fmt::Display for Mailbox { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "{}@{}", + self.username, + self.host, + ) + } +} + +impl FromStr for Mailbox { + type Err = Error; + + fn from_str(s: &str) -> Result { + let mut blurb = None; + if let Some((username, mut host)) = s.split_once('@') { + if username.is_empty() { + Err(Error::EmptyUser) + } else { + let username = username.to_string(); + if let Some((h, b)) = host.split_once(|c: char| c.is_whitespace()) { + if h.is_empty() { + return Err(Error::EmptyHost); + } else { + host = h; + if !b.is_empty() { + blurb = Some(b.to_string()); + } + } + } + let host = host.parse()?; + Ok(Self { username, host, blurb }) + } + } else { + Err(Error::MissingSeparator) + } + } +} diff --git a/src/message/error.rs b/src/message/error.rs new file mode 100644 index 0000000..57b8bb7 --- /dev/null +++ b/src/message/error.rs @@ -0,0 +1,36 @@ +use {crate::prelude::ParseHostError, std::fmt}; + +#[derive(Debug, PartialEq)] +/// Errors which can occur when parsing a request +pub enum Error { + MissingSeparator, + EmptyUser, + EmptyHost, + EmptyMessage, + ParseHostError(ParseHostError), +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::ParseHostError(e) => write!(f, "{e}"), + _ => write!(f, "{self:?}"), + } + } +} + +impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Self::ParseHostError(e) => Some(e), + _ => None, + } + } +} + +impl From for Error { + fn from(value: ParseHostError) -> Self { + Self::ParseHostError(value) + } +} + diff --git a/src/message/mod.rs b/src/message/mod.rs new file mode 100644 index 0000000..caa51a4 --- /dev/null +++ b/src/message/mod.rs @@ -0,0 +1,84 @@ +use std::{fmt, str::FromStr}; +use crate::prelude::{Host, Mailbox}; + +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +mod error; +pub use error::Error; + +#[derive(Clone, Debug, PartialEq)] +pub struct Recipients { + pub boxes: Vec, +} + +impl fmt::Display for Recipients { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, ":")?; + self.boxes.iter().try_for_each(|b| { + write!(f, " {b}") + }) + } +} + +#[derive(Clone, Debug, PartialEq)] +pub enum Lines { + Sender(Mailbox), + Recipients(Recipients), + Timestamp(String), + Text(String), + Heading1(String), + Heading2(String), + Heading3(String), + Quote(String), + Preformatted(String), +} + +impl fmt::Display for Lines { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Sender(m) => writeln!(f, "< {m}"), + Self::Recipients(r) => writeln!(f, "{r}"), + Self::Timestamp(t) => writeln!(f, "@ {t}"), + Self::Text(t) => writeln!(f, "{t}"), + Self::Heading1(h) => writeln!(f, "# {h}"), + Self::Heading2(h) => writeln!(f, "## {h}"), + Self::Heading3(h) => writeln!(f, "### {h}"), + Self::Quote(q) => { + for l in q.lines() { + writeln!(f, "> {l}")?; + } + Ok(()) + }, + Self::Preformatted(p) => writeln!(f, "```\n{p}\n```"), + } + } +} + +#[derive(Debug, Clone, PartialEq)] +#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))] +pub struct Message { + pub senders: Vec, + pub recipients: Vec, + pub timstamp: Option, + pub body: String, +} + +impl fmt::Display for Message { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if !self.senders.is_empty() { + write!(f, "< ")?; + self.senders.iter().try_for_each(|s| { + write!(f, "{s}\n") + })?; + } + if !self.recipients.is_empty() { + write!(f, ": ")?; + self.recipients.iter().try_for_each(|r| { + write!(f, " {r}") + })?; + write!(f, "\n")?; + } + write!(f, "{}\r\n", self.body) + } +} diff --git a/src/prelude.rs b/src/prelude.rs index 8f57e98..14babeb 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -2,6 +2,8 @@ pub use super::{ certificate_store::CertificateStore, fingerprint::{Error as FingerprintError, Fingerprint, GetFingerprint}, host::{Error as ParseHostError, Host}, + mailbox::{Error as ParseMailboxError, Mailbox}, + message::{Error as ParseMessageError, Lines, Message, Recipients}, //receiver, request::{Error as ParseRequestError, Request}, response::{Error as ParseResponseError, Response},