From 4642443d37e8a451edf63926c29372e9631a9f35 Mon Sep 17 00:00:00 2001 From: Nathan Fisher Date: Wed, 7 Jun 2023 13:49:19 -0400 Subject: [PATCH] Wrote message parser --- src/connection/verifier.rs | 3 +- src/gemtext/mod.rs | 2 + src/gemtext/parser.rs | 244 +++++++++++++++++++++++++++++ src/lib.rs | 1 + src/mailstore/filesystem.rs | 9 +- src/message/error.rs | 7 +- src/message/mod.rs | 20 +-- src/message/parser.rs | 305 +++++++++--------------------------- src/prelude.rs | 3 +- src/sender/mod.rs | 2 +- src/status/mod.rs | 2 - 11 files changed, 343 insertions(+), 255 deletions(-) create mode 100644 src/gemtext/mod.rs create mode 100644 src/gemtext/parser.rs 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..86e2d03 --- /dev/null +++ b/src/gemtext/parser.rs @@ -0,0 +1,244 @@ +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('<') 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 { + todo!() + } +} + +#[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), + } + } + 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 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('@') => {} + 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); + } + } +} diff --git a/src/lib.rs b/src/lib.rs index bea027e..21dc3e0 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 0097d6a..8b4bb28 100644 --- a/src/message/error.rs +++ b/src/message/error.rs @@ -1,4 +1,7 @@ -use {crate::prelude::{ParseHostError, ParseMailboxError}, std::fmt}; +use { + crate::prelude::{ParseHostError, ParseMailboxError}, + std::fmt, +}; #[derive(Debug, PartialEq)] /// Errors which can occur when parsing a request @@ -7,6 +10,7 @@ pub enum Error { EmptyUser, EmptyHost, EmptyMessage, + EmptySender, ParseHostError(ParseHostError), ParseMailboxError(ParseMailboxError), MalformedLink, @@ -25,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, } } diff --git a/src/message/mod.rs b/src/message/mod.rs index f9f1ece..abc383e 100644 --- a/src/message/mod.rs +++ b/src/message/mod.rs @@ -7,13 +7,9 @@ use serde::{Deserialize, Serialize}; mod error; mod link; mod parser; -pub use { - error::Error, - link::Link, - parser::{GemtextNode, Parser}, -}; +pub use {error::Error, link::Link, parser::Parser}; -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, Default, PartialEq)] pub struct Recipients { pub boxes: Vec, } @@ -55,6 +51,7 @@ pub struct Message { 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}"))?; @@ -64,18 +61,13 @@ impl fmt::Display for Message { self.recipients.iter().try_for_each(|r| write!(f, " {r}"))?; writeln!(f)?; } + if let Some(ref t) = self.timstamp { + writeln!(f, "@ {t}")?; + } write!(f, "{}\r\n", self.body) } } -impl FromStr for Message { - type Err = String; - - fn from_str(s: &str) -> Result { - todo!() - } -} - #[cfg(test)] mod tests { use super::*; diff --git a/src/message/parser.rs b/src/message/parser.rs index 2913c42..b016989 100644 --- a/src/message/parser.rs +++ b/src/message/parser.rs @@ -1,244 +1,85 @@ -use super::{Link, Recipients}; -use crate::prelude::Mailbox; -use std::fmt; +use { + super::{Message, Recipients}, + crate::prelude::Mailbox, +}; -#[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('<') 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 { - todo!() - } -} - -#[derive(Debug)] -pub struct Parser<'a> { - state: State<'a>, +#[derive(Debug, Default)] +pub struct Parser { + id: String, + from: Option, + senders: Vec, + recipients: Recipients, + timestamp: Option, title: Option, - lines: Vec, + body: String, } -impl<'a> Parser<'a> { - pub fn new() -> Self { - Self { - state: State::Normal, - title: None, - lines: vec![], - } +impl Parser { + pub fn new(id: &str) -> Self { + let mut p = Self::default(); + p.id = id.to_string(); + p } - 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), + 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); + } + self.body.push_str(s); + self.body.push('\n'); + } + 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); + } + self.body.push_str(s); + self.body.push('\n'); + } + 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); + } + self.body.push_str(s); + self.body.push('\n'); + } + s => { + self.body.push_str(s); + self.body.push('\n'); + } } } - 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()) + if self.from.is_none() { + Err(super::Error::EmptySender) } 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 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('@') => {}, - 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); + Ok(Message { + id: self.id, + from: self.from.unwrap(), + senders: self.senders, + recipients: self.recipients.boxes, + timstamp: self.timestamp, + title: self.title, + body: self.body, + }) } } } diff --git a/src/prelude.rs b/src/prelude.rs index f89579c..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, GemtextNode, 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 6aab624..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}, diff --git a/src/status/mod.rs b/src/status/mod.rs index 1ddb6f7..36a3ed3 100644 --- a/src/status/mod.rs +++ b/src/status/mod.rs @@ -1,6 +1,4 @@ mod error; -use std::fmt; - pub use error::Error; #[cfg(feature = "serde")]