diff --git a/src/connection/verifier.rs b/src/connection/verifier.rs index 745916b..3e40f3e 100644 --- a/src/connection/verifier.rs +++ b/src/connection/verifier.rs @@ -1,6 +1,5 @@ +use crate::mailuser::Mailuser; use rustls::server::ClientCertVerifier; - -use crate::{mailuser::Mailuser, prelude::CertificateStore}; use std::sync::Mutex; #[derive(Debug)] diff --git a/src/gemtext/mod.rs b/src/gemtext/mod.rs new file mode 100644 index 0000000..4bcc143 --- /dev/null +++ b/src/gemtext/mod.rs @@ -0,0 +1,2 @@ +pub mod parser; +pub use parser::{GemtextNode, Parser}; diff --git a/src/gemtext/parser.rs b/src/gemtext/parser.rs new file mode 100644 index 0000000..ee08f0c --- /dev/null +++ b/src/gemtext/parser.rs @@ -0,0 +1,374 @@ +use crate::prelude::{Link, Mailbox, Recipients}; +use std::fmt; + +#[derive(Debug)] +pub struct PreBlk<'a> { + alt: Option<&'a str>, + lines: Vec<&'a str>, +} + +#[derive(Debug)] +enum State<'a> { + Normal, + Preformatted(PreBlk<'a>), + Quote(Vec<&'a str>), +} + +#[derive(Clone, Debug, PartialEq)] +pub enum GemtextNode { + Sender(Mailbox), + Recipients(Recipients), + Timestamp(String), + Text(String), + Heading1(String), + Heading2(String), + Heading3(String), + ListItem(String), + Quote(String), + Preformatted(Option, String), + Link(Link), +} + +impl fmt::Display for GemtextNode { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Recipients(r) => writeln!(f, "{r}"), + Self::Sender(m) => writeln!(f, "{m}"), + 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::ListItem(l) => writeln!(f, "* {l}"), + Self::Quote(q) => writeln!(f, "> {q}"), + Self::Preformatted(a, p) => match a { + None => writeln!(f, "```\n{}\n```", p), + Some(alt) => writeln!(f, "```{alt}\n{}\n```", p), + }, + Self::Link(l) => writeln!(f, "=> {l}"), + } + } +} + +impl<'a> GemtextNode { + fn parse_link(text: &'a str) -> Self { + if let Ok(link) = text.parse() { + Self::Link(link) + } else { + Self::Text(text.to_string()) + } + } + + fn parse_heading(text: &'a str) -> Self { + if let Some((h, s)) = text.split_once(char::is_whitespace) { + match h { + "#" => Self::Heading1(s.to_string()), + "##" => Self::Heading2(s.to_string()), + "###" => Self::Heading3(s.to_string()), + _ => Self::Text(text.to_string()), + } + } else { + Self::Text(text.to_string()) + } + } + + fn parse_list_item(text: &'a str) -> Self { + match text.split_once(char::is_whitespace) { + Some((pre, s)) if pre == "*" => GemtextNode::ListItem(s.to_string()), + _ => GemtextNode::Text(text.to_string()), + } + } + + fn parse_senders(text: &'a str) -> Self { + let Some(line) = text.strip_prefix('<').map(|x| x.trim()) else { + return Self::Text(text.to_string()); + }; + if let Ok(user) = line.parse() { + Self::Sender(user) + } else { + Self::Text(text.to_string()) + } + } + + fn parse_recipients(text: &'a str) -> Self { + let Some(line) = text.strip_prefix(':') else { + return Self::Text(text.to_string()); + }; + let split = line.split_whitespace(); + let mut recipients: Recipients = Recipients { boxes: vec![] }; + for s in split { + if let Ok(m) = s.parse() { + recipients.boxes.push(m); + } else { + return Self::Text(text.to_string()); + } + } + Self::Recipients(recipients) + } + + fn parse_timestamp(text: &'a str) -> Self { + let Some(line) = text.strip_prefix('@').map(|x| x.trim()) else { + return Self::Text(text.to_string()); + }; + Self::Timestamp(line.to_string()) + } +} + +#[derive(Debug)] +pub struct Parser<'a> { + state: State<'a>, + title: Option, + lines: Vec, +} + +impl<'a> Parser<'a> { + pub fn new() -> Self { + Self { + state: State::Normal, + title: None, + lines: vec![], + } + } + + pub fn parse(mut self, raw: &'a str) -> Vec { + for line in raw.lines() { + match self.state { + State::Normal => self.parse_normal(line), + State::Preformatted(_) => self.parse_preformatted(line), + State::Quote(_) => self.parse_quote(line), + } + } + match self.state { + State::Normal => {} + State::Preformatted(_) => self.leave_preformatted(), + State::Quote(q) => { + let quote = q.join("\n").to_string(); + self.lines.push(GemtextNode::Quote(quote)); + } + } + self.lines + } + + fn link(&mut self, line: &'a str) { + self.lines.push(GemtextNode::parse_link(line)); + } + + fn heading(&mut self, line: &'a str) { + let line = GemtextNode::parse_heading(line); + if self.title.is_none() { + match &line { + GemtextNode::Heading1(t) => self.title = Some(t.clone()), + _ => {} + } + } + self.lines.push(line); + } + + fn list_item(&mut self, line: &'a str) { + self.lines.push(GemtextNode::parse_list_item(line)); + } + + fn enter_quote(&mut self, line: &'a str) { + match line.split_once(char::is_whitespace) { + Some((prefix, suffix)) if prefix == ">" => self.state = State::Quote(vec![suffix]), + _ => self.lines.push(GemtextNode::Text(line.to_string())), + } + } + + fn leave_quote(&mut self, line: &'a str) { + match &mut self.state { + State::Quote(q) => { + let quote = q.join("\n").to_string(); + self.lines.push(GemtextNode::Quote(quote)); + } + _ => panic!("Attempt to parse as quote when not in quote mode"), + } + self.state = State::Normal; + self.lines.push(GemtextNode::Text(line.to_string())); + } + + fn enter_preformatted(&mut self, line: &'a str) { + let alt = if line.len() > 3 { + Some(line[3..].trim()) + } else { + None + }; + let preblk = PreBlk { alt, lines: vec![] }; + self.state = State::Preformatted(preblk); + } + + fn leave_preformatted(&mut self) { + match &self.state { + State::Preformatted(v) => { + let s = v.lines.join("\n").to_string(); + self.lines + .push(GemtextNode::Preformatted(v.alt.map(str::to_string), s)); + self.state = State::Normal; + } + _ => panic!("Attempted to leave preformatted mode when not in preformatted mode"), + } + } + + fn senders(&mut self, line: &'a str) { + self.lines.push(GemtextNode::parse_senders(line)); + } + + fn recipients(&mut self, line: &'a str) { + self.lines.push(GemtextNode::parse_recipients(line)); + } + + fn timestamp(&mut self, line: &'a str) { + self.lines.push(GemtextNode::parse_timestamp(line)); + } + + fn parse_normal(&mut self, line: &'a str) { + match line { + s if s.starts_with("=>") => self.link(s), + s if s.starts_with('#') => self.heading(s), + s if s.starts_with('*') => self.list_item(s), + s if s.starts_with('>') => self.enter_quote(s), + s if s.starts_with("```") => self.enter_preformatted(s), + s if s.starts_with('<') => self.senders(s), + s if s.starts_with(':') => self.recipients(s), + s if s.starts_with('@') => self.timestamp(s), + s => self.lines.push(GemtextNode::Text(s.to_string())), + } + } + + fn parse_preformatted(&mut self, line: &'a str) { + if line.starts_with("```") { + self.leave_preformatted(); + } else { + match &mut self.state { + State::Preformatted(p) => p.lines.push(line), + _ => panic!("Attempt to parse as preformatted when not in preformatted mode"), + } + } + } + + fn parse_quote(&mut self, line: &'a str) { + if let Some(suffix) = line.strip_prefix('>') { + match &mut self.state { + State::Quote(q) => q.push(suffix.trim()), + _ => panic!("Attempt to parse as quote when not in quote mode"), + } + } else { + self.leave_quote(line); + } + } +} + +#[cfg(test)] +mod tests { + use crate::prelude::Host; + + use super::*; + + static RAW: &'static str = + include_str!("../../test/mailstore/misfin.example.org/jane/Lists/700070.gmi"); + + fn parse_raw() -> Vec { + Parser::new().parse(RAW) + } + + #[test] + fn nodes() { + let nodes = parse_raw(); + let mut nodes = nodes.iter(); + assert_eq!( + nodes.next().cloned().unwrap(), + GemtextNode::Sender(Mailbox { + username: "ed".to_string(), + host: Host { + subdomain: None, + domain: "iron".to_string(), + tld: "maiden".to_string() + }, + blurb: Some("Eddy the Head".to_string()) + }) + ); + assert_eq!( + nodes.next().cloned().unwrap(), + GemtextNode::Sender(Mailbox { + username: "bruce".to_string(), + host: Host { + subdomain: None, + domain: "dickinson".to_string(), + tld: "jam".to_string(), + }, + blurb: Some("Bruce Dickinson".to_string()), + }) + ); + assert_eq!( + nodes.next().cloned().unwrap(), + GemtextNode::Recipients(Recipients { + boxes: vec![ + Mailbox { + username: "nicko".to_string(), + host: Host { + subdomain: None, + domain: "iron".to_string(), + tld: "maiden".to_string(), + }, + blurb: None, + }, + Mailbox { + username: "steve".to_string(), + host: Host { + subdomain: None, + domain: "iron".to_string(), + tld: "maiden".to_string() + }, + blurb: None, + } + ] + }) + ); + assert_eq!( + nodes.next().cloned().unwrap(), + GemtextNode::Timestamp("2023-06-07T21:27:15Z".to_string()), + ); + assert_eq!( + nodes.next().cloned().unwrap(), + GemtextNode::Text("".to_string()), + ); + assert_eq!( + nodes.next().cloned().unwrap(), + GemtextNode::Heading1("Checking in".to_string()), + ); + assert_eq!( + nodes.next().cloned().unwrap(), + GemtextNode::Text("The new album's in the can. A few things:".to_string()), + ); + assert_eq!( + nodes.next().cloned().unwrap(), + GemtextNode::ListItem("Needs more cowbell".to_string()), + ); + assert_eq!( + nodes.next().cloned().unwrap(), + GemtextNode::ListItem( + "Adrian's guitar is too loud, drowning out the cowbell".to_string() + ), + ); + assert_eq!( + nodes.next().cloned().unwrap(), + GemtextNode::ListItem("Steve has a fever".to_string()), + ); + assert_eq!( + nodes.next().cloned().unwrap(), + GemtextNode::Link(Link { + url: "gemini://private.iron.maiden/clip.mp3".to_string(), + display: Some("clip".to_string()) + }) + ); + assert_eq!( + nodes.next().cloned().unwrap(), + GemtextNode::Text("".to_string()), + ); + assert_eq!( + nodes.next().cloned().unwrap(), + GemtextNode::Quote("You're gonna want that cowbell!".to_string()), + ); + assert!(nodes.next().is_none()); + } +} diff --git a/src/lib.rs b/src/lib.rs index ea9e661..1f96a01 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,6 +2,7 @@ pub mod certificate; pub mod connection; pub mod fingerprint; +pub mod gemtext; pub mod host; pub mod mailbox; pub mod mailstore; diff --git a/src/mailstore/filesystem.rs b/src/mailstore/filesystem.rs index 8cb1ea7..5308d1a 100644 --- a/src/mailstore/filesystem.rs +++ b/src/mailstore/filesystem.rs @@ -1,3 +1,5 @@ +use crate::message::Parser as MessageParser; + use super::*; use std::{ fs::{self, File}, @@ -64,8 +66,11 @@ impl MailStore for Filesystem { }; dir.filter_map(Result::ok).for_each(|e| { if let Ok(contents) = fs::read_to_string(e.path()) { - if let Ok(message) = contents.parse::() { - folder.messages.insert(message.id.clone(), message); + if let Some(p) = e.path().to_str() { + let parser = MessageParser::new(p); + if let Ok(message) = parser.parse(&contents) { + folder.messages.insert(message.id.clone(), message); + } } } }); diff --git a/src/message/error.rs b/src/message/error.rs index 20d4b7c..8b4bb28 100644 --- a/src/message/error.rs +++ b/src/message/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 @@ -7,7 +10,9 @@ pub enum Error { EmptyUser, EmptyHost, EmptyMessage, + EmptySender, ParseHostError(ParseHostError), + ParseMailboxError(ParseMailboxError), MalformedLink, } @@ -24,6 +29,7 @@ impl std::error::Error for Error { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { match self { Self::ParseHostError(e) => Some(e), + Self::ParseMailboxError(e) => Some(e), _ => None, } } @@ -34,3 +40,9 @@ impl From for Error { Self::ParseHostError(value) } } + +impl From for Error { + fn from(value: ParseMailboxError) -> Self { + Self::ParseMailboxError(value) + } +} diff --git a/src/message/mod.rs b/src/message/mod.rs index 35554ec..a9b3a41 100644 --- a/src/message/mod.rs +++ b/src/message/mod.rs @@ -1,4 +1,4 @@ -use crate::prelude::{Host, Mailbox}; +use crate::prelude::Mailbox; use std::{fmt, str::FromStr}; #[cfg(feature = "serde")] @@ -9,7 +9,7 @@ mod link; mod parser; pub use {error::Error, link::Link, parser::Parser}; -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, Default, PartialEq)] pub struct Recipients { pub boxes: Vec, } @@ -21,34 +21,19 @@ impl fmt::Display for Recipients { } } -#[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), - Link(Link), -} +impl FromStr for Recipients { + type Err = Error; -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) => writeln!(f, "> {q}"), - Self::Preformatted(p) => writeln!(f, "```\n{p}\n```"), - Self::Link(l) => writeln!(f, "=> {l}"), + fn from_str(s: &str) -> Result { + let mut rec = Recipients { boxes: vec![] }; + if let Some(s) = s.strip_prefix(':') { + s.split_whitespace().try_for_each(|r| { + let r = r.parse()?; + rec.boxes.push(r); + Ok::<(), Error>(()) + })?; } + Ok(rec) } } @@ -59,13 +44,14 @@ pub struct Message { pub from: Mailbox, pub senders: Vec, pub recipients: Vec, - pub timstamp: Option, + pub timestamp: Option, pub title: Option, pub body: String, } impl fmt::Display for Message { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + writeln!(f, "< {}", self.from)?; if !self.senders.is_empty() { write!(f, "< ")?; self.senders.iter().try_for_each(|s| writeln!(f, "{s}"))?; @@ -75,14 +61,28 @@ impl fmt::Display for Message { self.recipients.iter().try_for_each(|r| write!(f, " {r}"))?; writeln!(f)?; } + if let Some(ref t) = self.timestamp { + writeln!(f, "@ {t}")?; + } write!(f, "{}\r\n", self.body) } } -impl FromStr for Message { - type Err = String; +#[cfg(test)] +mod tests { + use super::*; - fn from_str(s: &str) -> Result { - todo!() + static REC: &'static str = ": joe@example.org, jane@pizza.hut, mark@gemi.dev"; + + #[test] + fn parse_recipients() { + let rec: Recipients = REC.parse().unwrap(); + assert!(rec.boxes.len() == 3); + } + + #[test] + fn print_recipients() { + let rec: Recipients = REC.parse().unwrap(); + assert_eq!(rec.to_string(), REC); } } diff --git a/src/message/parser.rs b/src/message/parser.rs index 50868b4..dd5ef49 100644 --- a/src/message/parser.rs +++ b/src/message/parser.rs @@ -1,2 +1,125 @@ -#[derive(Debug)] -pub struct Parser; +use { + super::{Message, Recipients}, + crate::prelude::Mailbox, +}; + +#[derive(Debug, Default)] +pub struct Parser { + id: String, + from: Option, + senders: Vec, + recipients: Recipients, + timestamp: Option, + title: Option, + body: String, +} + +impl Parser { + pub fn new(id: &str) -> Self { + let mut p = Self::default(); + p.id = id.to_string(); + p + } + + pub fn parse(mut self, content: &str) -> Result { + let lines = content.lines(); + for l in lines { + match l { + s if s.starts_with("<") && self.from.is_none() && self.body.is_empty() => { + let s = s.strip_prefix('<').unwrap().trim(); + let from: Mailbox = s.parse()?; + self.from = Some(from); + } + s if s.starts_with("<") && self.body.is_empty() => { + let sndr = s.strip_prefix('<').unwrap().trim(); + let from: Mailbox = sndr.parse()?; + self.senders.push(from); + } + s if s.starts_with(":") && self.body.is_empty() => { + self.recipients = s.parse()?; + } + s if s.starts_with("@") && self.timestamp.is_none() && self.body.is_empty() => { + self.timestamp = Some(s.strip_prefix("@").unwrap().trim().to_string()); + } + s if s.starts_with("###") && self.title.is_none() => { + if let Some(t) = s.strip_prefix("###").map(|x| x.trim().to_string()) { + self.title = Some(t); + } + if !self.body.is_empty() { + self.body.push('\n'); + } + self.body.push_str(s); + } + s if s.starts_with("##") && self.title.is_none() => { + if let Some(t) = s.strip_prefix("##").map(|x| x.trim().to_string()) { + self.title = Some(t); + } + if !self.body.is_empty() { + self.body.push('\n'); + } + self.body.push_str(s); + } + s if s.starts_with("#") && self.title.is_none() => { + if let Some(t) = s.strip_prefix("#").map(|x| x.trim().to_string()) { + self.title = Some(t); + } + if !self.body.is_empty() { + self.body.push('\n'); + } + self.body.push_str(s); + } + s => { + if !self.body.is_empty() { + self.body.push('\n'); + } + self.body.push_str(s); + } + } + } + if self.from.is_none() { + Err(super::Error::EmptySender) + } else { + Ok(Message { + id: self.id, + from: self.from.unwrap(), + senders: self.senders, + recipients: self.recipients.boxes, + timestamp: self.timestamp, + title: self.title, + body: self.body, + }) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + const RAW: &'static str = + include_str!("../../test/mailstore/misfin.example.org/dick/Inbox/619310.gmi"); + + #[test] + fn parse_message() { + let msg: Message = Parser::new("619310").parse(RAW).unwrap(); + assert_eq!(msg.id, "619310"); + assert_eq!(msg.from.to_string(), "joe@gemini.example.org"); + assert_eq!(msg.timestamp.unwrap(), "2023-06-07T16:09:42Z"); + assert!(msg.senders.is_empty()); + assert!(msg.recipients.is_empty()); + assert_eq!(msg.title.unwrap(), "How 'bout dose Bears?"); + assert_eq!( + msg.body, + "# How 'bout dose Bears?\nWhen are they coming for dinner anyway?\n" + ); + } + + #[test] + fn print_message() { + let msg: Message = Parser::new("ox42sc69").parse(RAW).unwrap(); + assert_eq!( + msg.to_string(), + "< joe@gemini.example.org\n@ 2023-06-07T16:09:42Z\n# How 'bout dose Bears?\nWhen are they coming for dinner anyway?\n\r\n" + ); + } +} diff --git a/src/prelude.rs b/src/prelude.rs index e0ec573..55b36cd 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -1,10 +1,11 @@ pub use super::{ certificate::{Certificate, CertificateStore, ClientCertificateStore}, fingerprint::{Error as FingerprintError, Fingerprint, GetFingerprint}, + gemtext::{GemtextNode, Parser}, host::{Error as ParseHostError, Host}, mailbox::{Error as ParseMailboxError, Mailbox}, mailuser::Mailuser, - message::{Error as ParseMessageError, Lines, Message, Recipients}, + message::{Error as ParseMessageError, Link, Message, Recipients}, //receiver, request::{Error as ParseRequestError, Request}, response::{Error as ParseResponseError, Response}, diff --git a/src/sender/mod.rs b/src/sender/mod.rs index fa3a762..b9daef1 100644 --- a/src/sender/mod.rs +++ b/src/sender/mod.rs @@ -1,6 +1,6 @@ use { crate::prelude::{CertificateStore, ClientCertificateStore, Mailuser, Request, Response}, - rustls::{internal::msgs::codec::Codec, ClientConfig, ClientConnection, StreamOwned}, + rustls::{ClientConfig, ClientConnection, StreamOwned}, std::{ io::{self, Read, Write}, net::{TcpStream, ToSocketAddrs}, @@ -70,7 +70,8 @@ where let cfg = match client_cert { None => cfg.with_no_client_auth(), Some(c) => { - let rustls_cert = rustls::Certificate::read_bytes(&c.der)?; + //let rustls_cert = rustls::Certificate::read_bytes(&c.der)?; + let rustls_cert = rustls::Certificate(c.der); let cert_chain = vec![rustls_cert]; let key_der = rustls::PrivateKey(c.key); cfg.with_single_cert(cert_chain, key_der)? @@ -79,8 +80,8 @@ where let client = ClientConnection::new(Arc::new(cfg), dnsname)?; let mut stream = StreamOwned::new(client, tcp_stream); stream.write_all(self.request.to_string().as_bytes())?; - let mut buf = vec![]; - stream.read_to_end(&mut buf)?; + let mut buf = Vec::with_capacity(1024); + let _res = stream.read_to_end(&mut buf); stream.conn.send_close_notify(); drop(stream); let res = buf.try_into()?; diff --git a/src/status/mod.rs b/src/status/mod.rs index ca88b1d..ffe642a 100644 --- a/src/status/mod.rs +++ b/src/status/mod.rs @@ -32,7 +32,8 @@ pub enum Status { impl From for u8 { fn from(value: Status) -> Self { match value { - Status::Input | Status::Success => value.into(), + Status::Input => 10, + Status::Success => 20, Status::Redirect(n) => 30 + n as u8, Status::TemporaryFailure(n) => 40 + n as u8, Status::PermanentFailure(n) => 50 + n as u8, @@ -67,7 +68,7 @@ pub enum Redirect { /// The mailbox has moved to a different address, and all future /// messages should be sent to that address. Permanent = 1, - Other, + Other = 2, } impl TryFrom for Redirect { @@ -102,7 +103,7 @@ pub enum TemporaryFailure { RateLimit = 4, /// The mailbox isn't accepting mail right now, but it might in the future. MailboxFull = 5, - Other, + Other = 6, } impl TryFrom for TemporaryFailure { @@ -139,7 +140,7 @@ pub enum PermanentFailure { DomainNotServiced = 3, /// Your request is malformed, and won't be accepted by the mailserver. BadRequest = 9, - Other, + Other = 4, } impl TryFrom for PermanentFailure { @@ -177,7 +178,7 @@ pub enum AuthenticationFailure { /// The mailserver needs you to complete a task to confirm that you are a /// legitimate sender. (This is reserved for a Hashcash style anti-spam measure). ProofRequired = 4, - Other, + Other = 5, } impl TryFrom for AuthenticationFailure { @@ -200,6 +201,123 @@ impl TryFrom for AuthenticationFailure { mod tests { use super::*; + #[test] + fn to_number_success() { + let num: u8 = Status::Success.into(); + assert_eq!(num, 20); + } + + #[test] + fn to_number_redirect() { + assert_eq!(u8::from(Status::Redirect(Redirect::Temporary)), 30); + assert_eq!(u8::from(Status::Redirect(Redirect::Permanent)), 31); + assert_eq!(u8::from(Status::Redirect(Redirect::Other)), 32); + } + + #[test] + fn to_number_temporary() { + assert_eq!( + u8::from(Status::TemporaryFailure(TemporaryFailure::TemporaryError)), + 40 + ); + assert_eq!( + u8::from(Status::TemporaryFailure( + TemporaryFailure::ServerUnavailable + )), + 41 + ); + assert_eq!( + u8::from(Status::TemporaryFailure(TemporaryFailure::CgiError)), + 42 + ); + assert_eq!( + u8::from(Status::TemporaryFailure(TemporaryFailure::ProxyError)), + 43 + ); + assert_eq!( + u8::from(Status::TemporaryFailure(TemporaryFailure::RateLimit)), + 44 + ); + assert_eq!( + u8::from(Status::TemporaryFailure(TemporaryFailure::MailboxFull)), + 45 + ); + assert_eq!( + u8::from(Status::TemporaryFailure(TemporaryFailure::Other)), + 46 + ); + } + + #[test] + fn to_number_permanent() { + assert_eq!( + u8::from(Status::PermanentFailure(PermanentFailure::PermanentError)), + 50 + ); + assert_eq!( + u8::from(Status::PermanentFailure( + PermanentFailure::MailboxNonexistent + )), + 51 + ); + assert_eq!( + u8::from(Status::PermanentFailure(PermanentFailure::MailboxGone)), + 52 + ); + assert_eq!( + u8::from(Status::PermanentFailure( + PermanentFailure::DomainNotServiced + )), + 53 + ); + assert_eq!( + u8::from(Status::PermanentFailure(PermanentFailure::BadRequest)), + 59 + ); + assert_eq!( + u8::from(Status::PermanentFailure(PermanentFailure::Other)), + 54 + ); + } + + #[test] + fn to_number_auth() { + assert_eq!( + u8::from(Status::AuthenticationFailure( + AuthenticationFailure::CertificateRequired + )), + 60 + ); + assert_eq!( + u8::from(Status::AuthenticationFailure( + AuthenticationFailure::UnauthorizedSender + )), + 61 + ); + assert_eq!( + u8::from(Status::AuthenticationFailure( + AuthenticationFailure::CertificateInvalid + )), + 62 + ); + assert_eq!( + u8::from(Status::AuthenticationFailure( + AuthenticationFailure::IdentityMismatch + )), + 63 + ); + assert_eq!( + u8::from(Status::AuthenticationFailure( + AuthenticationFailure::ProofRequired + )), + 64 + ); + assert_eq!( + u8::from(Status::AuthenticationFailure(AuthenticationFailure::Other)), + 65 + ); + } + #[test] fn parse_status_success() { let status = Status::try_from(21).unwrap(); diff --git a/test/mailstore/mail.gmi.org/dick/Inbox/623273.gmi b/test/mailstore/mail.gmi.org/dick/Inbox/623273.gmi new file mode 100644 index 0000000..e69de29 diff --git a/test/mailstore/mail.gmi.org/dick/Inbox/738127.gmi b/test/mailstore/mail.gmi.org/dick/Inbox/738127.gmi new file mode 100644 index 0000000..e69de29 diff --git a/test/mailstore/mail.gmi.org/dick/Lists/158468.gmi b/test/mailstore/mail.gmi.org/dick/Lists/158468.gmi new file mode 100644 index 0000000..e69de29 diff --git a/test/mailstore/mail.gmi.org/dick/Lists/431652.gmi b/test/mailstore/mail.gmi.org/dick/Lists/431652.gmi new file mode 100644 index 0000000..e69de29 diff --git a/test/mailstore/mail.gmi.org/dick/blurb b/test/mailstore/mail.gmi.org/dick/blurb new file mode 100644 index 0000000..418b2fa --- /dev/null +++ b/test/mailstore/mail.gmi.org/dick/blurb @@ -0,0 +1 @@ +Richard Stallman diff --git a/test/mailstore/mail.gmi.org/jane/Inbox/488571.gmi b/test/mailstore/mail.gmi.org/jane/Inbox/488571.gmi new file mode 100644 index 0000000..e69de29 diff --git a/test/mailstore/mail.gmi.org/jane/Inbox/867017.gmi b/test/mailstore/mail.gmi.org/jane/Inbox/867017.gmi new file mode 100644 index 0000000..e69de29 diff --git a/test/mailstore/mail.gmi.org/jane/Lists/476324.gmi b/test/mailstore/mail.gmi.org/jane/Lists/476324.gmi new file mode 100644 index 0000000..e69de29 diff --git a/test/mailstore/mail.gmi.org/jane/Lists/891249.gmi b/test/mailstore/mail.gmi.org/jane/Lists/891249.gmi new file mode 100644 index 0000000..e69de29 diff --git a/test/mailstore/mail.gmi.org/jane/blurb b/test/mailstore/mail.gmi.org/jane/blurb new file mode 100644 index 0000000..b549676 --- /dev/null +++ b/test/mailstore/mail.gmi.org/jane/blurb @@ -0,0 +1 @@ +Janet Weiss diff --git a/test/mailstore/misfin.example.org/dick/Inbox/619310.gmi b/test/mailstore/misfin.example.org/dick/Inbox/619310.gmi new file mode 100644 index 0000000..900b67e --- /dev/null +++ b/test/mailstore/misfin.example.org/dick/Inbox/619310.gmi @@ -0,0 +1,5 @@ +< joe@gemini.example.org +@ 2023-06-07T16:09:42Z +# How 'bout dose Bears? +When are they coming for dinner anyway? + diff --git a/test/mailstore/misfin.example.org/dick/Inbox/868379.gmi b/test/mailstore/misfin.example.org/dick/Inbox/868379.gmi new file mode 100644 index 0000000..e69de29 diff --git a/test/mailstore/misfin.example.org/dick/Lists/448785.gmi b/test/mailstore/misfin.example.org/dick/Lists/448785.gmi new file mode 100644 index 0000000..e69de29 diff --git a/test/mailstore/misfin.example.org/dick/Lists/645815.gmi b/test/mailstore/misfin.example.org/dick/Lists/645815.gmi new file mode 100644 index 0000000..e69de29 diff --git a/test/mailstore/misfin.example.org/dick/blurb b/test/mailstore/misfin.example.org/dick/blurb new file mode 100644 index 0000000..a956b32 --- /dev/null +++ b/test/mailstore/misfin.example.org/dick/blurb @@ -0,0 +1 @@ +Rick Baker diff --git a/test/mailstore/misfin.example.org/jane/Inbox/252366.gmi b/test/mailstore/misfin.example.org/jane/Inbox/252366.gmi new file mode 100644 index 0000000..e69de29 diff --git a/test/mailstore/misfin.example.org/jane/Inbox/654064.gmi b/test/mailstore/misfin.example.org/jane/Inbox/654064.gmi new file mode 100644 index 0000000..e69de29 diff --git a/test/mailstore/misfin.example.org/jane/Lists/620426.gmi b/test/mailstore/misfin.example.org/jane/Lists/620426.gmi new file mode 100644 index 0000000..e69de29 diff --git a/test/mailstore/misfin.example.org/jane/Lists/700070.gmi b/test/mailstore/misfin.example.org/jane/Lists/700070.gmi new file mode 100644 index 0000000..78dc6c7 --- /dev/null +++ b/test/mailstore/misfin.example.org/jane/Lists/700070.gmi @@ -0,0 +1,13 @@ +< ed@iron.maiden Eddy the Head +< bruce@dickinson.jam Bruce Dickinson +: nicko@iron.maiden steve@iron.maiden +@ 2023-06-07T21:27:15Z + +# Checking in +The new album's in the can. A few things: +* Needs more cowbell +* Adrian's guitar is too loud, drowning out the cowbell +* Steve has a fever +=> gemini://private.iron.maiden/clip.mp3 clip + +> You're gonna want that cowbell! diff --git a/test/mailstore/misfin.example.org/jane/blurb b/test/mailstore/misfin.example.org/jane/blurb new file mode 100644 index 0000000..176be3c --- /dev/null +++ b/test/mailstore/misfin.example.org/jane/blurb @@ -0,0 +1 @@ +Jane Doh