From 54fb9e79ab9d4477e2ec90739226c1852201baa0 Mon Sep 17 00:00:00 2001 From: Nathan Fisher Date: Fri, 26 May 2023 01:34:29 -0400 Subject: [PATCH] Add methods to get recipients, senders and timestamp from Request; Add Mailuser type; Use Mailuser instead of String and Host in Request to represent the sender; --- src/host/mod.rs | 24 ++++----- src/lib.rs | 1 + src/mailbox/error.rs | 2 +- src/mailbox/mod.rs | 26 ++++++--- src/mailuser/mod.rs | 38 +++++++++++++ src/message/error.rs | 1 - src/message/mod.rs | 16 ++---- src/prelude.rs | 1 + src/request/error.rs | 17 ++++-- src/request/mod.rs | 123 +++++++++++++++++++++++++++++++------------ 10 files changed, 177 insertions(+), 72 deletions(-) create mode 100644 src/mailuser/mod.rs diff --git a/src/host/mod.rs b/src/host/mod.rs index a0a4c36..c7c98a4 100644 --- a/src/host/mod.rs +++ b/src/host/mod.rs @@ -1,16 +1,3 @@ -//! This module contains a data structure representing dns -//! hosts which can be readily parsed from or converted to strings. -//! # Examples -//! Parse a host from a given string -//! ``` -//! use dory::host::Host; -//! -//! let host_str = "misfin.example.com"; -//! let host: Host = host_str.parse().unwrap(); -//! assert_eq!(host.subdomain.unwrap().as_str(), "misfin"); -//! assert_eq!(host.domain.as_str(), "example"); -//! assert_eq!(host.tld.as_str(), "com"); -//! ``` use std::{fmt, str::FromStr}; mod error; pub use error::Error; @@ -21,6 +8,17 @@ use serde::{Deserialize, Serialize}; #[derive(Clone, Debug, Default, PartialEq)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] /// Represents the fully qualified domain name for this host +/// # Examples +/// Parse a host from a given string +/// ``` +/// use dory::prelude::Host; +/// +/// let host_str = "misfin.example.com"; +/// let host: Host = host_str.parse().unwrap(); +/// assert_eq!(host.subdomain.unwrap().as_str(), "misfin"); +/// assert_eq!(host.domain.as_str(), "example"); +/// assert_eq!(host.tld.as_str(), "com"); +/// ``` pub struct Host { pub subdomain: Option, pub domain: String, diff --git a/src/lib.rs b/src/lib.rs index a67b410..13cd7e8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,6 +3,7 @@ mod certificate_store; mod fingerprint; mod host; mod mailbox; +mod mailuser; mod message; pub mod prelude; mod receiver; diff --git a/src/mailbox/error.rs b/src/mailbox/error.rs index 524aee2..64fe3fc 100644 --- a/src/mailbox/error.rs +++ b/src/mailbox/error.rs @@ -6,6 +6,7 @@ pub enum Error { MissingSeparator, EmptyUser, EmptyHost, + IllegalWhitespace, ParseHostError(ParseHostError), } @@ -32,4 +33,3 @@ impl From for Error { Self::ParseHostError(value) } } - diff --git a/src/mailbox/mod.rs b/src/mailbox/mod.rs index 8ad0226..201da2c 100644 --- a/src/mailbox/mod.rs +++ b/src/mailbox/mod.rs @@ -1,5 +1,5 @@ -use std::{fmt, str::FromStr}; use crate::prelude::Host; +use std::{fmt, str::FromStr}; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; @@ -17,12 +17,11 @@ pub struct Mailbox { impl fmt::Display for Mailbox { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "{}@{}", - self.username, - self.host, - ) + if let Some(ref blurb) = self.blurb { + write!(f, "{}@{} {}", self.username, self.host, blurb,) + } else { + write!(f, "{}@{}", self.username, self.host,) + } } } @@ -34,20 +33,31 @@ impl FromStr for Mailbox { if let Some((username, mut host)) = s.split_once('@') { if username.is_empty() { Err(Error::EmptyUser) + } else if username.contains(|c: char| c.is_whitespace()) { + Err(Error::IllegalWhitespace) } 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 if h.contains(|c: char| c.is_whitespace()) { + return Err(Error::IllegalWhitespace); } else { host = h; if !b.is_empty() { + if b.contains("\n") { + return Err(Error::IllegalWhitespace); + } blurb = Some(b.to_string()); } } } let host = host.parse()?; - Ok(Self { username, host, blurb }) + Ok(Self { + username, + host, + blurb, + }) } } else { Err(Error::MissingSeparator) diff --git a/src/mailuser/mod.rs b/src/mailuser/mod.rs new file mode 100644 index 0000000..0da7a4e --- /dev/null +++ b/src/mailuser/mod.rs @@ -0,0 +1,38 @@ +use crate::prelude::{Host, ParseMailboxError as Error}; +use std::{fmt, str::FromStr}; + +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, PartialEq)] +#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))] +pub struct Mailuser { + pub username: String, + pub host: Host, +} + +impl fmt::Display for Mailuser { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}@{}", self.username, self.host,) + } +} + +impl FromStr for Mailuser { + type Err = Error; + + fn from_str(s: &str) -> Result { + if let Some((username, host)) = s.split_once('@') { + if username.is_empty() { + Err(Error::EmptyUser) + } else if host.is_empty() { + Err(Error::EmptyHost) + } else { + let username = username.to_string(); + let host = host.parse()?; + Ok(Self { username, host }) + } + } else { + Err(Error::MissingSeparator) + } + } +} diff --git a/src/message/error.rs b/src/message/error.rs index 57b8bb7..368ef53 100644 --- a/src/message/error.rs +++ b/src/message/error.rs @@ -33,4 +33,3 @@ impl From for Error { Self::ParseHostError(value) } } - diff --git a/src/message/mod.rs b/src/message/mod.rs index caa51a4..7cc5c36 100644 --- a/src/message/mod.rs +++ b/src/message/mod.rs @@ -1,5 +1,5 @@ -use std::{fmt, str::FromStr}; use crate::prelude::{Host, Mailbox}; +use std::{fmt, str::FromStr}; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; @@ -15,9 +15,7 @@ pub struct Recipients { 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}") - }) + self.boxes.iter().try_for_each(|b| write!(f, " {b}")) } } @@ -49,7 +47,7 @@ impl fmt::Display for Lines { writeln!(f, "> {l}")?; } Ok(()) - }, + } Self::Preformatted(p) => writeln!(f, "```\n{p}\n```"), } } @@ -68,15 +66,11 @@ 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") - })?; + 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}") - })?; + 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 14babeb..f8eed48 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -3,6 +3,7 @@ pub use super::{ fingerprint::{Error as FingerprintError, Fingerprint, GetFingerprint}, host::{Error as ParseHostError, Host}, mailbox::{Error as ParseMailboxError, Mailbox}, + mailuser::Mailuser, message::{Error as ParseMessageError, Lines, Message, Recipients}, //receiver, request::{Error as ParseRequestError, Request}, diff --git a/src/request/error.rs b/src/request/error.rs index 9620f09..8ce079b 100644 --- a/src/request/error.rs +++ b/src/request/error.rs @@ -1,4 +1,7 @@ -use {crate::prelude::ParseHostError, std::fmt}; +use { + crate::prelude::{ParseHostError, ParseMailboxError}, + std::fmt, +}; #[derive(Debug, PartialEq)] /// Errors which can occur when parsing a request @@ -29,8 +32,14 @@ impl std::error::Error for Error { } } -impl From for Error { - fn from(value: ParseHostError) -> Self { - Self::ParseHostError(value) +impl From for Error { + fn from(value: ParseMailboxError) -> Self { + match value { + ParseMailboxError::ParseHostError(e) => Self::ParseHostError(e), + ParseMailboxError::EmptyUser => Self::EmptyUser, + ParseMailboxError::EmptyHost => Self::EmptyHost, + ParseMailboxError::IllegalWhitespace => Self::Malformed, + ParseMailboxError::MissingSeparator => Self::MissingSeparator, + } } } diff --git a/src/request/mod.rs b/src/request/mod.rs index 27e0a57..60ee5fe 100644 --- a/src/request/mod.rs +++ b/src/request/mod.rs @@ -1,4 +1,4 @@ -use crate::prelude::Host; +use crate::prelude::{Mailbox, Mailuser}; use std::{fmt, str::FromStr}; #[cfg(feature = "serde")] @@ -11,21 +11,15 @@ pub use error::Error; #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] /// The full request as sent by the `Sender` and received by the `Receiver` pub struct Request { - /// The username of the sender - pub user: String, - /// The fully qualified domain name of the sending server - pub host: Host, + /// The sender of the message + pub sender: Mailuser, /// The message body pub message: String, } impl fmt::Display for Request { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "misfin://{}@{} {}\r\n", - self.user, self.host, self.message - ) + write!(f, "misfin://{} {}\r\n", self.sender, self.message) } } @@ -40,44 +34,77 @@ impl FromStr for Request { if message.is_empty() { return Err(Error::EmptyMessage); } - if let Some((user, host)) = user.rsplit_once('@') { - if host.is_empty() { - return Err(Error::EmptyHost); - } else if user == "misfin://" { - return Err(Error::EmptyUser); - } - let host = host.parse()?; - let Some(user) = user.strip_prefix("misfin://").map(ToString::to_string) else { - return Err(Error::Malformed); - }; - Ok(Request { - user, - host, - message, - }) - } else { - Err(Error::MissingSeparator) - } + let Some(user) = user.strip_prefix("misfin://") else { + return Err(Error::Malformed); + }; + let sender = user.parse()?; + Ok(Request { sender, message }) } else { Err(Error::MissingSeparator) } } } +impl Request { + pub fn recipients(&self) -> Vec { + let mut recipients = vec![]; + self.message.lines().for_each(|l| { + if l.starts_with(':') { + l[1..].trim().split_whitespace().for_each(|u| { + if let Ok(user) = u.parse() { + recipients.push(user); + } + }); + } + }); + recipients + } + + pub fn senders(&self) -> Vec { + let mut senders = vec![]; + self.message.lines().for_each(|l| { + if l.starts_with('<') { + if let Ok(mbox) = l[1..].trim().parse() { + senders.push(mbox); + } + } + }); + senders + } + + pub fn timestamp(&self) -> Option { + let mut ts = None; + self.message.lines().for_each(|l| { + if l.starts_with('@') { + ts = Some(l[1..].trim().to_string()); + } + }); + ts + } +} + #[cfg(test)] mod tests { use super::*; - use crate::prelude::ParseHostError; + use crate::prelude::{Host, ParseHostError}; const REQ_STR: &'static str = "misfin://john@misfin.example.com Anyone seen Jane?\r\n"; + const REQ_COMPLEX: &'static str = "misfin://john@misfin.example.com \ + < jane@misfin.example.xyz Jane Doe\n\ + Request { Request { - user: "john".to_string(), - host: Host { - subdomain: Some("misfin".into()), - domain: "example".into(), - tld: "com".into(), + sender: Mailuser { + username: "john".to_string(), + host: Host { + subdomain: Some("misfin".into()), + domain: "example".into(), + tld: "com".into(), + }, }, message: "Anyone seen Jane?".to_string(), } @@ -133,4 +160,32 @@ mod tests { Err(Error::ParseHostError(ParseHostError::IllegalWhitespace)) ); } + + #[test] + fn recipients() { + let req = REQ_COMPLEX.parse::().unwrap(); + let recipients = req.recipients(); + assert_eq!(recipients.len(), 3); + let first = recipients.iter().next().unwrap(); + assert_eq!(first.username, "bruce"); + assert!(first.host.subdomain.is_none()); + assert_eq!(first.host.domain, "willis"); + assert_eq!(first.host.tld, "abc"); + } + + #[test] + fn senders() { + let req = REQ_COMPLEX.parse::().unwrap(); + let senders = req.senders(); + assert_eq!(senders.len(), 2); + let last = senders.iter().last().unwrap(); + assert_eq!(last.host.tld, "pizza"); + } + + #[test] + fn timestamp() { + let req = REQ_COMPLEX.parse::().unwrap(); + let ts = req.timestamp().unwrap(); + assert_eq!(ts, "2023-05-09T19:39:15Z") + } }