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;
This commit is contained in:
Nathan Fisher 2023-05-26 01:34:29 -04:00
parent b2f60c61ec
commit 54fb9e79ab
10 changed files with 177 additions and 72 deletions

View file

@ -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<String>,
pub domain: String,

View file

@ -3,6 +3,7 @@ mod certificate_store;
mod fingerprint;
mod host;
mod mailbox;
mod mailuser;
mod message;
pub mod prelude;
mod receiver;

View file

@ -6,6 +6,7 @@ pub enum Error {
MissingSeparator,
EmptyUser,
EmptyHost,
IllegalWhitespace,
ParseHostError(ParseHostError),
}
@ -32,4 +33,3 @@ impl From<ParseHostError> for Error {
Self::ParseHostError(value)
}
}

View file

@ -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)

38
src/mailuser/mod.rs Normal file
View file

@ -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<Self, Self::Err> {
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)
}
}
}

View file

@ -33,4 +33,3 @@ impl From<ParseHostError> for Error {
Self::ParseHostError(value)
}
}

View file

@ -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)

View file

@ -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},

View file

@ -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<ParseHostError> for Error {
fn from(value: ParseHostError) -> Self {
Self::ParseHostError(value)
impl From<ParseMailboxError> 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,
}
}
}

View file

@ -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,45 +34,78 @@ 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 {
let Some(user) = user.strip_prefix("misfin://") else {
return Err(Error::Malformed);
};
Ok(Request {
user,
host,
message,
})
} else {
Err(Error::MissingSeparator)
}
let sender = user.parse()?;
Ok(Request { sender, message })
} else {
Err(Error::MissingSeparator)
}
}
}
impl Request {
pub fn recipients(&self) -> Vec<Mailuser> {
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<Mailbox> {
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<String> {
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\
<bob@gemini.pizza Bob Thornton\n\
: bruce@willis.abc demi@moore.com billy@ray.cyrus\n\
@ 2023-05-09T19:39:15Z\n\
Hello world!\r\n";
fn req() -> Request {
Request {
user: "john".to_string(),
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::<Request>().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::<Request>().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::<Request>().unwrap();
let ts = req.timestamp().unwrap();
assert_eq!(ts, "2023-05-09T19:39:15Z")
}
}