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:
parent
b2f60c61ec
commit
54fb9e79ab
10 changed files with 177 additions and 72 deletions
|
@ -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};
|
use std::{fmt, str::FromStr};
|
||||||
mod error;
|
mod error;
|
||||||
pub use error::Error;
|
pub use error::Error;
|
||||||
|
@ -21,6 +8,17 @@ use serde::{Deserialize, Serialize};
|
||||||
#[derive(Clone, Debug, Default, PartialEq)]
|
#[derive(Clone, Debug, Default, PartialEq)]
|
||||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||||
/// Represents the fully qualified domain name for this host
|
/// 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 struct Host {
|
||||||
pub subdomain: Option<String>,
|
pub subdomain: Option<String>,
|
||||||
pub domain: String,
|
pub domain: String,
|
||||||
|
|
|
@ -3,6 +3,7 @@ mod certificate_store;
|
||||||
mod fingerprint;
|
mod fingerprint;
|
||||||
mod host;
|
mod host;
|
||||||
mod mailbox;
|
mod mailbox;
|
||||||
|
mod mailuser;
|
||||||
mod message;
|
mod message;
|
||||||
pub mod prelude;
|
pub mod prelude;
|
||||||
mod receiver;
|
mod receiver;
|
||||||
|
|
|
@ -6,6 +6,7 @@ pub enum Error {
|
||||||
MissingSeparator,
|
MissingSeparator,
|
||||||
EmptyUser,
|
EmptyUser,
|
||||||
EmptyHost,
|
EmptyHost,
|
||||||
|
IllegalWhitespace,
|
||||||
ParseHostError(ParseHostError),
|
ParseHostError(ParseHostError),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -32,4 +33,3 @@ impl From<ParseHostError> for Error {
|
||||||
Self::ParseHostError(value)
|
Self::ParseHostError(value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
use std::{fmt, str::FromStr};
|
|
||||||
use crate::prelude::Host;
|
use crate::prelude::Host;
|
||||||
|
use std::{fmt, str::FromStr};
|
||||||
|
|
||||||
#[cfg(feature = "serde")]
|
#[cfg(feature = "serde")]
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
@ -17,12 +17,11 @@ pub struct Mailbox {
|
||||||
|
|
||||||
impl fmt::Display for Mailbox {
|
impl fmt::Display for Mailbox {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
write!(
|
if let Some(ref blurb) = self.blurb {
|
||||||
f,
|
write!(f, "{}@{} {}", self.username, self.host, blurb,)
|
||||||
"{}@{}",
|
} else {
|
||||||
self.username,
|
write!(f, "{}@{}", self.username, self.host,)
|
||||||
self.host,
|
}
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -34,20 +33,31 @@ impl FromStr for Mailbox {
|
||||||
if let Some((username, mut host)) = s.split_once('@') {
|
if let Some((username, mut host)) = s.split_once('@') {
|
||||||
if username.is_empty() {
|
if username.is_empty() {
|
||||||
Err(Error::EmptyUser)
|
Err(Error::EmptyUser)
|
||||||
|
} else if username.contains(|c: char| c.is_whitespace()) {
|
||||||
|
Err(Error::IllegalWhitespace)
|
||||||
} else {
|
} else {
|
||||||
let username = username.to_string();
|
let username = username.to_string();
|
||||||
if let Some((h, b)) = host.split_once(|c: char| c.is_whitespace()) {
|
if let Some((h, b)) = host.split_once(|c: char| c.is_whitespace()) {
|
||||||
if h.is_empty() {
|
if h.is_empty() {
|
||||||
return Err(Error::EmptyHost);
|
return Err(Error::EmptyHost);
|
||||||
|
} else if h.contains(|c: char| c.is_whitespace()) {
|
||||||
|
return Err(Error::IllegalWhitespace);
|
||||||
} else {
|
} else {
|
||||||
host = h;
|
host = h;
|
||||||
if !b.is_empty() {
|
if !b.is_empty() {
|
||||||
|
if b.contains("\n") {
|
||||||
|
return Err(Error::IllegalWhitespace);
|
||||||
|
}
|
||||||
blurb = Some(b.to_string());
|
blurb = Some(b.to_string());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let host = host.parse()?;
|
let host = host.parse()?;
|
||||||
Ok(Self { username, host, blurb })
|
Ok(Self {
|
||||||
|
username,
|
||||||
|
host,
|
||||||
|
blurb,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Err(Error::MissingSeparator)
|
Err(Error::MissingSeparator)
|
||||||
|
|
38
src/mailuser/mod.rs
Normal file
38
src/mailuser/mod.rs
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -33,4 +33,3 @@ impl From<ParseHostError> for Error {
|
||||||
Self::ParseHostError(value)
|
Self::ParseHostError(value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
use std::{fmt, str::FromStr};
|
|
||||||
use crate::prelude::{Host, Mailbox};
|
use crate::prelude::{Host, Mailbox};
|
||||||
|
use std::{fmt, str::FromStr};
|
||||||
|
|
||||||
#[cfg(feature = "serde")]
|
#[cfg(feature = "serde")]
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
@ -15,9 +15,7 @@ pub struct Recipients {
|
||||||
impl fmt::Display for Recipients {
|
impl fmt::Display for Recipients {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
write!(f, ":")?;
|
write!(f, ":")?;
|
||||||
self.boxes.iter().try_for_each(|b| {
|
self.boxes.iter().try_for_each(|b| write!(f, " {b}"))
|
||||||
write!(f, " {b}")
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -49,7 +47,7 @@ impl fmt::Display for Lines {
|
||||||
writeln!(f, "> {l}")?;
|
writeln!(f, "> {l}")?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
},
|
}
|
||||||
Self::Preformatted(p) => writeln!(f, "```\n{p}\n```"),
|
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 {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
if !self.senders.is_empty() {
|
if !self.senders.is_empty() {
|
||||||
write!(f, "< ")?;
|
write!(f, "< ")?;
|
||||||
self.senders.iter().try_for_each(|s| {
|
self.senders.iter().try_for_each(|s| write!(f, "{s}\n"))?;
|
||||||
write!(f, "{s}\n")
|
|
||||||
})?;
|
|
||||||
}
|
}
|
||||||
if !self.recipients.is_empty() {
|
if !self.recipients.is_empty() {
|
||||||
write!(f, ": ")?;
|
write!(f, ": ")?;
|
||||||
self.recipients.iter().try_for_each(|r| {
|
self.recipients.iter().try_for_each(|r| write!(f, " {r}"))?;
|
||||||
write!(f, " {r}")
|
|
||||||
})?;
|
|
||||||
write!(f, "\n")?;
|
write!(f, "\n")?;
|
||||||
}
|
}
|
||||||
write!(f, "{}\r\n", self.body)
|
write!(f, "{}\r\n", self.body)
|
||||||
|
|
|
@ -3,6 +3,7 @@ pub use super::{
|
||||||
fingerprint::{Error as FingerprintError, Fingerprint, GetFingerprint},
|
fingerprint::{Error as FingerprintError, Fingerprint, GetFingerprint},
|
||||||
host::{Error as ParseHostError, Host},
|
host::{Error as ParseHostError, Host},
|
||||||
mailbox::{Error as ParseMailboxError, Mailbox},
|
mailbox::{Error as ParseMailboxError, Mailbox},
|
||||||
|
mailuser::Mailuser,
|
||||||
message::{Error as ParseMessageError, Lines, Message, Recipients},
|
message::{Error as ParseMessageError, Lines, Message, Recipients},
|
||||||
//receiver,
|
//receiver,
|
||||||
request::{Error as ParseRequestError, Request},
|
request::{Error as ParseRequestError, Request},
|
||||||
|
|
|
@ -1,4 +1,7 @@
|
||||||
use {crate::prelude::ParseHostError, std::fmt};
|
use {
|
||||||
|
crate::prelude::{ParseHostError, ParseMailboxError},
|
||||||
|
std::fmt,
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Debug, PartialEq)]
|
#[derive(Debug, PartialEq)]
|
||||||
/// Errors which can occur when parsing a request
|
/// Errors which can occur when parsing a request
|
||||||
|
@ -29,8 +32,14 @@ impl std::error::Error for Error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<ParseHostError> for Error {
|
impl From<ParseMailboxError> for Error {
|
||||||
fn from(value: ParseHostError) -> Self {
|
fn from(value: ParseMailboxError) -> Self {
|
||||||
Self::ParseHostError(value)
|
match value {
|
||||||
|
ParseMailboxError::ParseHostError(e) => Self::ParseHostError(e),
|
||||||
|
ParseMailboxError::EmptyUser => Self::EmptyUser,
|
||||||
|
ParseMailboxError::EmptyHost => Self::EmptyHost,
|
||||||
|
ParseMailboxError::IllegalWhitespace => Self::Malformed,
|
||||||
|
ParseMailboxError::MissingSeparator => Self::MissingSeparator,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use crate::prelude::Host;
|
use crate::prelude::{Mailbox, Mailuser};
|
||||||
use std::{fmt, str::FromStr};
|
use std::{fmt, str::FromStr};
|
||||||
|
|
||||||
#[cfg(feature = "serde")]
|
#[cfg(feature = "serde")]
|
||||||
|
@ -11,21 +11,15 @@ pub use error::Error;
|
||||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||||
/// The full request as sent by the `Sender` and received by the `Receiver`
|
/// The full request as sent by the `Sender` and received by the `Receiver`
|
||||||
pub struct Request {
|
pub struct Request {
|
||||||
/// The username of the sender
|
/// The sender of the message
|
||||||
pub user: String,
|
pub sender: Mailuser,
|
||||||
/// The fully qualified domain name of the sending server
|
|
||||||
pub host: Host,
|
|
||||||
/// The message body
|
/// The message body
|
||||||
pub message: String,
|
pub message: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for Request {
|
impl fmt::Display for Request {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
write!(
|
write!(f, "misfin://{} {}\r\n", self.sender, self.message)
|
||||||
f,
|
|
||||||
"misfin://{}@{} {}\r\n",
|
|
||||||
self.user, self.host, self.message
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -40,44 +34,77 @@ impl FromStr for Request {
|
||||||
if message.is_empty() {
|
if message.is_empty() {
|
||||||
return Err(Error::EmptyMessage);
|
return Err(Error::EmptyMessage);
|
||||||
}
|
}
|
||||||
if let Some((user, host)) = user.rsplit_once('@') {
|
let Some(user) = user.strip_prefix("misfin://") else {
|
||||||
if host.is_empty() {
|
return Err(Error::Malformed);
|
||||||
return Err(Error::EmptyHost);
|
};
|
||||||
} else if user == "misfin://" {
|
let sender = user.parse()?;
|
||||||
return Err(Error::EmptyUser);
|
Ok(Request { sender, message })
|
||||||
}
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
Err(Error::MissingSeparator)
|
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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
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_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 {
|
fn req() -> Request {
|
||||||
Request {
|
Request {
|
||||||
user: "john".to_string(),
|
sender: Mailuser {
|
||||||
host: Host {
|
username: "john".to_string(),
|
||||||
subdomain: Some("misfin".into()),
|
host: Host {
|
||||||
domain: "example".into(),
|
subdomain: Some("misfin".into()),
|
||||||
tld: "com".into(),
|
domain: "example".into(),
|
||||||
|
tld: "com".into(),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
message: "Anyone seen Jane?".to_string(),
|
message: "Anyone seen Jane?".to_string(),
|
||||||
}
|
}
|
||||||
|
@ -133,4 +160,32 @@ mod tests {
|
||||||
Err(Error::ParseHostError(ParseHostError::IllegalWhitespace))
|
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")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue