diff --git a/src/fingerprint.rs b/src/fingerprint/error.rs similarity index 51% rename from src/fingerprint.rs rename to src/fingerprint/error.rs index f07e864..1e31d82 100644 --- a/src/fingerprint.rs +++ b/src/fingerprint/error.rs @@ -1,16 +1,7 @@ -use digest::Digest; -use rustls::Certificate; -use sha2::Sha256; -use std::fmt::{self, Write}; -use x509_parser::prelude::*; - -pub trait Fingerprint { - type Error; - - fn fingerprint(&self) -> Result<(String, String), Self::Error>; -} +use {std::fmt, x509_parser::prelude::X509Error}; #[derive(Clone, Debug, PartialEq)] +/// Errors which can occur when fingerprinting a certificate pub enum Error { Fmt, X509(X509Error), @@ -45,21 +36,3 @@ impl From> for Error { Self::X509(value.into()) } } - -impl Fingerprint for Certificate { - type Error = Error; - - fn fingerprint(&self) -> Result<(String, String), Self::Error> { - let (_, pk) = X509Certificate::from_der(self.as_ref())?; - let subject = pk.subject().to_string(); - let key = pk.public_key().subject_public_key.as_ref(); - let mut hasher = Sha256::new(); - hasher.update(key); - let res = hasher.finalize(); - let mut s = String::with_capacity(res.len()); - for c in res { - write!(s, "{c:02x}")?; - } - Ok((subject[3..].to_string(), s)) - } -} diff --git a/src/fingerprint/mod.rs b/src/fingerprint/mod.rs new file mode 100644 index 0000000..2d339ed --- /dev/null +++ b/src/fingerprint/mod.rs @@ -0,0 +1,33 @@ +use digest::Digest; +use rustls::Certificate; +use sha2::Sha256; +use std::fmt::Write; +use x509_parser::prelude::*; + +mod error; +pub use error::Error; + +/// Creates an sha256 fingerprint for a certificate +pub trait Fingerprint { + type Error; + + fn fingerprint(&self) -> Result<(String, String), Self::Error>; +} + +impl Fingerprint for Certificate { + type Error = Error; + + fn fingerprint(&self) -> Result<(String, String), Self::Error> { + let (_, pk) = X509Certificate::from_der(self.as_ref())?; + let subject = pk.subject().to_string(); + let key = pk.public_key().subject_public_key.as_ref(); + let mut hasher = Sha256::new(); + hasher.update(key); + let res = hasher.finalize(); + let mut s = String::with_capacity(res.len()); + for c in res { + write!(s, "{c:02x}")?; + } + Ok((subject[3..].to_string(), s)) + } +} diff --git a/src/host/error.rs b/src/host/error.rs new file mode 100644 index 0000000..e1d8d9e --- /dev/null +++ b/src/host/error.rs @@ -0,0 +1,19 @@ +use std::fmt; + +#[derive(Clone, Debug, PartialEq)] +/// Errors which can occur when parsing a host from a string +pub enum Error { + MissingSeparator, + EmptyDomain, + EmptyTld, + EmptySubdomain, + IllegalWhitespace, +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{self:?}") + } +} + +impl std::error::Error for Error {} diff --git a/src/host.rs b/src/host/mod.rs similarity index 71% rename from src/host.rs rename to src/host/mod.rs index d510cfe..3ac93d2 100644 --- a/src/host.rs +++ b/src/host/mod.rs @@ -1,6 +1,9 @@ use std::{fmt, str::FromStr}; +mod error; +pub use error::Error; #[derive(Clone, Debug, Default, PartialEq)] +/// Represents the fully qualified domain name for this host pub struct Host { pub subdomain: Option, pub domain: String, @@ -17,40 +20,23 @@ impl fmt::Display for Host { } } -#[derive(Clone, Debug, PartialEq)] -pub enum ParseHostError { - MissingSeparator, - EmptyDomain, - EmptyTld, - EmptySubdomain, - IllegalWhitespace, -} - -impl fmt::Display for ParseHostError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{self:?}") - } -} - -impl std::error::Error for ParseHostError {} - impl FromStr for Host { - type Err = ParseHostError; + type Err = Error; fn from_str(s: &str) -> Result { if s.contains(char::is_whitespace) { - return Err(ParseHostError::IllegalWhitespace); + return Err(Error::IllegalWhitespace); } if let Some((domain, tld)) = s.rsplit_once('.') { if domain.is_empty() { - Err(ParseHostError::EmptyDomain) + Err(Error::EmptyDomain) } else if tld.is_empty() { - Err(ParseHostError::EmptyTld) + Err(Error::EmptyTld) } else if let Some((subdomain, domain)) = domain.rsplit_once('.') { if subdomain.is_empty() { - Err(ParseHostError::EmptySubdomain) + Err(Error::EmptySubdomain) } else if domain.is_empty() { - Err(ParseHostError::EmptyDomain) + Err(Error::EmptyDomain) } else { Ok(Host { subdomain: Some(subdomain.to_string()), @@ -66,7 +52,7 @@ impl FromStr for Host { }) } } else { - Err(ParseHostError::MissingSeparator) + Err(Error::MissingSeparator) } } } @@ -79,37 +65,31 @@ mod tests { fn parse_missing_separator() { assert_eq!( "exampledotcom".parse::(), - Err(ParseHostError::MissingSeparator) + Err(Error::MissingSeparator) ); } #[test] fn parse_empty_tld() { - assert_eq!("example.".parse::(), Err(ParseHostError::EmptyTld)); + assert_eq!("example.".parse::(), Err(Error::EmptyTld)); } #[test] fn parse_empty_domain() { - assert_eq!(".com".parse::(), Err(ParseHostError::EmptyDomain)); - assert_eq!( - "example..com".parse::(), - Err(ParseHostError::EmptyDomain) - ); + assert_eq!(".com".parse::(), Err(Error::EmptyDomain)); + assert_eq!("example..com".parse::(), Err(Error::EmptyDomain)); } #[test] fn parse_empty_subdomain() { - assert_eq!( - ".example.com".parse::(), - Err(ParseHostError::EmptySubdomain) - ); + assert_eq!(".example.com".parse::(), Err(Error::EmptySubdomain)); } #[test] fn parse_illegal_whitespace() { assert_eq!( "exam\tple.com".parse::(), - Err(ParseHostError::IllegalWhitespace) + Err(Error::IllegalWhitespace) ); } diff --git a/src/prelude.rs b/src/prelude.rs index a42fb1e..b2c36a3 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -1,9 +1,9 @@ pub use super::{ - fingerprint::{Fingerprint, Error as FingerprintError}, - host::{Host, ParseHostError}, + fingerprint::{Error as FingerprintError, Fingerprint}, + host::{Error as ParseHostError, Host}, receiver, - response::{Response, Error as ParseResponseError}, - request::{Request, ParseRequestError}, + request::{Error as ParseRequestError, Request}, + response::{Error as ParseResponseError, Response}, sender::{CertificateStore, Error as SenderError, Sender, Verifier}, - status::*, + status::{Error as ParseStatusError, Status, Redirect, TemporaryFailure, PermanentFailure, AuthenticationFailure}, }; diff --git a/src/receiver.rs b/src/receiver/mod.rs similarity index 100% rename from src/receiver.rs rename to src/receiver/mod.rs diff --git a/src/request/error.rs b/src/request/error.rs new file mode 100644 index 0000000..63b807c --- /dev/null +++ b/src/request/error.rs @@ -0,0 +1,36 @@ +use {crate::prelude::ParseHostError, std::fmt}; + +#[derive(Clone, Debug, PartialEq)] +/// Errors which can occur when parsing a request +pub enum Error { + MissingSeparator, + EmptyUser, + EmptyHost, + EmptyMessage, + Malformed, + 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/request.rs b/src/request/mod.rs similarity index 59% rename from src/request.rs rename to src/request/mod.rs index 606464f..9844a35 100644 --- a/src/request.rs +++ b/src/request/mod.rs @@ -1,11 +1,18 @@ -use crate::host::{Host, ParseHostError}; +use crate::prelude::Host; use std::{fmt, str::FromStr}; +mod error; +pub use error::Error; + #[derive(Clone, Debug, PartialEq)] +/// The full request as sent by the `Sender` and received by the `Receiver` pub struct Request { - user: String, - host: Host, - message: String, + /// The username of the sender + pub user: String, + /// The fully qualified domain name of the sending server + pub host: Host, + /// The message body + pub message: String, } impl fmt::Display for Request { @@ -18,60 +25,26 @@ impl fmt::Display for Request { } } -#[derive(Clone, Debug, PartialEq)] -pub enum ParseRequestError { - MissingSeparator, - EmptyUser, - EmptyHost, - EmptyMessage, - Malformed, - ParseHostError(ParseHostError), -} - -impl fmt::Display for ParseRequestError { - 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 ParseRequestError { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - match self { - Self::ParseHostError(e) => Some(e), - _ => None, - } - } -} - -impl From for ParseRequestError { - fn from(value: ParseHostError) -> Self { - Self::ParseHostError(value) - } -} - impl FromStr for Request { - type Err = ParseRequestError; + type Err = Error; fn from_str(s: &str) -> Result { if let Some((user, message)) = s.split_once(' ') { let Some(message) = message.strip_suffix("\r\n").map(ToString::to_string) else { - return Err(ParseRequestError::Malformed); + return Err(Error::Malformed); }; if message.is_empty() { - return Err(ParseRequestError::EmptyMessage); + return Err(Error::EmptyMessage); } if let Some((user, host)) = user.rsplit_once('@') { if host.is_empty() { - return Err(ParseRequestError::EmptyHost); + return Err(Error::EmptyHost); } else if user == "misfin://" { - return Err(ParseRequestError::EmptyUser); + return Err(Error::EmptyUser); } let host = host.parse()?; let Some(user) = user.strip_prefix("misfin://").map(ToString::to_string) else { - return Err(ParseRequestError::Malformed); + return Err(Error::Malformed); }; Ok(Request { user, @@ -79,10 +52,10 @@ impl FromStr for Request { message, }) } else { - Err(ParseRequestError::MissingSeparator) + Err(Error::MissingSeparator) } } else { - Err(ParseRequestError::MissingSeparator) + Err(Error::MissingSeparator) } } } @@ -90,6 +63,7 @@ impl FromStr for Request { #[cfg(test)] mod tests { use super::*; + use crate::prelude::ParseHostError; const REQ_STR: &'static str = "misfin://john@misfin.example.com Anyone seen Jane?\r\n"; @@ -118,33 +92,33 @@ mod tests { #[test] fn parse_missing_sep() { let req = "misfin://john@example.comHelloWorld!\r\n".parse::(); - assert_eq!(req, Err(ParseRequestError::MissingSeparator)); + assert_eq!(req, Err(Error::MissingSeparator)); } #[test] fn parse_malformed() { let req = "misfin://john@example.com Hello World!\n".parse::(); - assert_eq!(req, Err(ParseRequestError::Malformed)); + assert_eq!(req, Err(Error::Malformed)); let req = "mail://john@example.com Hello World!\r\n".parse::(); - assert_eq!(req, Err(ParseRequestError::Malformed)); + assert_eq!(req, Err(Error::Malformed)); } #[test] fn parse_empty_user() { let req = "misfin://@example.com Hello World!\r\n".parse::(); - assert_eq!(req, Err(ParseRequestError::EmptyUser)); + assert_eq!(req, Err(Error::EmptyUser)); } #[test] fn parse_empty_host() { let req = "misfin://john@ Hello World!\r\n".parse::(); - assert_eq!(req, Err(ParseRequestError::EmptyHost)); + assert_eq!(req, Err(Error::EmptyHost)); } #[test] fn parse_empty_msg() { let req = "misfin://john@example.com \r\n".parse::(); - assert_eq!(req, Err(ParseRequestError::EmptyMessage)); + assert_eq!(req, Err(Error::EmptyMessage)); } #[test] @@ -152,9 +126,7 @@ mod tests { let req = "misfin://john@example\tfairy.com Hello World!\r\n".parse::(); assert_eq!( req, - Err(ParseRequestError::ParseHostError( - ParseHostError::IllegalWhitespace - )) + Err(Error::ParseHostError(ParseHostError::IllegalWhitespace)) ); } } diff --git a/src/response/error.rs b/src/response/error.rs index 7783943..639dd4d 100644 --- a/src/response/error.rs +++ b/src/response/error.rs @@ -4,10 +4,17 @@ use { }; #[derive(Clone, Debug, PartialEq)] +/// Errors which can occur when parsing the response sent by the receving server +/// back to the sender pub enum Error { + /// The message was too long. The Misfin spec allows a maximum length of + /// 2048 bytes for the message body. TooLong, + /// An error occurred parsing the status code as a number. ParseInt(ParseIntError), + /// The server sent an invalid or unrecognized status code StatusError, + /// The response was malformed Malformed, } @@ -42,4 +49,3 @@ impl From for Error { Self::StatusError } } - diff --git a/src/response.rs b/src/response/mod.rs similarity index 96% rename from src/response.rs rename to src/response/mod.rs index a1c377f..b47b985 100644 --- a/src/response.rs +++ b/src/response/mod.rs @@ -1,10 +1,11 @@ -use std::{fmt, str::FromStr}; use crate::prelude::Status; +use std::{fmt, str::FromStr}; mod error; pub use error::Error; #[derive(Clone, Debug, PartialEq)] +/// Sent from the receiving server back to the sending server pub struct Response { pub status: Status, pub meta: String, diff --git a/src/sender/error.rs b/src/sender/error.rs index ae7e573..13a9ce4 100644 --- a/src/sender/error.rs +++ b/src/sender/error.rs @@ -4,6 +4,7 @@ use { }; #[derive(Debug)] +/// Errors which might occur when sending a message pub enum Error { TlsError(rustls::Error), RequestError(ParseRequestError), @@ -56,4 +57,3 @@ impl From for Error { Self::IoError(value) } } - diff --git a/src/sender.rs b/src/sender/mod.rs similarity index 66% rename from src/sender.rs rename to src/sender/mod.rs index 979c175..0fd84d9 100644 --- a/src/sender.rs +++ b/src/sender/mod.rs @@ -1,22 +1,26 @@ -use crate::{ - request::Request, - response::Response, +pub use self::{ + error::Error, + verifier::{CertificateStore, Verifier}, }; +use crate::{request::Request, response::Response}; use std::io::{Read, Write}; -pub use self::{error::Error, verifier::{CertificateStore, Verifier}}; mod error; mod verifier; #[derive(Debug)] +/// Sends a piece of mail from the sending server to the receiving server pub struct Sender where S: CertificateStore, C: Sized, T: Read + Write + Sized, { + /// The full message text to be sent pub request: Request, + /// Verifies the receiving server's certificate pub verifier: Verifier, + /// The TLS stream used for the connection pub stream: rustls::StreamOwned, } diff --git a/src/sender/verifier.rs b/src/sender/verifier.rs index 29e1a08..4478f08 100644 --- a/src/sender/verifier.rs +++ b/src/sender/verifier.rs @@ -5,14 +5,18 @@ use rustls::{ }; use std::sync::{Arc, Mutex}; +/// An item which stores known certificates pub trait CertificateStore: Send + Sync { fn get(&self, host: &str) -> Option; fn insert(&mut self, host: &str, fingerprint: &str); } #[derive(Debug)] +/// A verifier is used to verify certificates sent by the receiving server +/// during the tls handshake. pub struct Verifier { - store: Arc>, + /// An item which serves as storage for certificates + pub store: Arc>, } impl ServerCertVerifier for Verifier { diff --git a/src/status/error.rs b/src/status/error.rs new file mode 100644 index 0000000..e64f26d --- /dev/null +++ b/src/status/error.rs @@ -0,0 +1,12 @@ +use std::fmt; + +#[derive(Debug, Clone, PartialEq)] +pub struct Error; + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{self:?}") + } +} + +impl std::error::Error for Error {} diff --git a/src/status.rs b/src/status/mod.rs similarity index 91% rename from src/status.rs rename to src/status/mod.rs index a0991d6..ee24b2b 100644 --- a/src/status.rs +++ b/src/status/mod.rs @@ -1,18 +1,10 @@ -use std::fmt; - -#[derive(Debug, Clone, PartialEq)] -pub struct ParseStatusError; - -impl fmt::Display for ParseStatusError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{self:?}") - } -} - -impl std::error::Error for ParseStatusError {} +mod error; +pub use error::Error; #[derive(Debug, Clone, PartialEq)] #[repr(u8)] +/// Status codes sent back to the sender representing how a receiving server has +/// processed a message. pub enum Status { /// These codes are reserved, and must not be sent by a Misfin server. Input = 10, @@ -46,7 +38,7 @@ impl From for u8 { } impl TryFrom for Status { - type Error = ParseStatusError; + type Error = Error; fn try_from(value: u8) -> Result { match value / 10 { @@ -56,12 +48,13 @@ impl TryFrom for Status { 4 => Ok(Self::TemporaryFailure((value % 40).try_into()?)), 5 => Ok(Self::PermanentFailure((value % 50).try_into()?)), 6 => Ok(Self::AuthenticationFailure((value % 60).try_into()?)), - _ => Err(ParseStatusError), + _ => Err(Error), } } } #[derive(Debug, Clone, PartialEq)] +/// Status codes representing that a redirect is required pub enum Redirect { /// The mailbox has moved to a different address, and this message /// should be resent to that address. @@ -73,19 +66,21 @@ pub enum Redirect { } impl TryFrom for Redirect { - type Error = ParseStatusError; + type Error = Error; fn try_from(value: u8) -> Result { match value { 0 => Ok(Self::Temporary), 1 => Ok(Self::Permanent), n if n < 10 => Ok(Self::Other), - _ => Err(ParseStatusError), + _ => Err(Error), } } } #[derive(Debug, Clone, PartialEq)] +/// Status codes representing that a temporary failure has occurred. The sending server should +/// retry sending the message. pub enum TemporaryFailure { /// The mailserver experienced a transient issue, and the message /// should be resent. @@ -105,7 +100,7 @@ pub enum TemporaryFailure { } impl TryFrom for TemporaryFailure { - type Error = ParseStatusError; + type Error = Error; fn try_from(value: u8) -> Result { match value { @@ -116,12 +111,14 @@ impl TryFrom for TemporaryFailure { 4 => Ok(Self::RateLimit), 5 => Ok(Self::MailboxFull), n if n < 10 => Ok(Self::Other), - _ => Err(ParseStatusError), + _ => Err(Error), } } } #[derive(Debug, Clone, PartialEq)] +/// Status codes representing that a permanent failure has occurred and the sending server should +/// not resend the message. pub enum PermanentFailure { PermanentError = 0, MailboxNonexistent = 1, @@ -132,7 +129,7 @@ pub enum PermanentFailure { } impl TryFrom for PermanentFailure { - type Error = ParseStatusError; + type Error = Error; fn try_from(value: u8) -> Result { match value { @@ -142,12 +139,13 @@ impl TryFrom for PermanentFailure { 3 => Ok(Self::DomainNotServiced), 9 => Ok(Self::BadRequest), n if n < 10 => Ok(Self::Other), - _ => Err(ParseStatusError), + _ => Err(Error), } } } #[derive(Debug, Clone, PartialEq)] +/// Status codes representing an authentication failure pub enum AuthenticationFailure { CertificateRequired = 0, UnauthorizedSender = 1, @@ -158,7 +156,7 @@ pub enum AuthenticationFailure { } impl TryFrom for AuthenticationFailure { - type Error = ParseStatusError; + type Error = Error; fn try_from(value: u8) -> Result { match value { @@ -168,7 +166,7 @@ impl TryFrom for AuthenticationFailure { 3 => Ok(Self::IdentityMismatch), 4 => Ok(Self::ProofRequired), n if n < 10 => Ok(Self::Other), - _ => Err(ParseStatusError), + _ => Err(Error), } } }