Wrote message parser

This commit is contained in:
Nathan Fisher 2023-06-07 13:49:19 -04:00
parent ff96b5b56b
commit 4642443d37
11 changed files with 343 additions and 255 deletions

View file

@ -1,6 +1,5 @@
use crate::mailuser::Mailuser;
use rustls::server::ClientCertVerifier; use rustls::server::ClientCertVerifier;
use crate::{mailuser::Mailuser, prelude::CertificateStore};
use std::sync::Mutex; use std::sync::Mutex;
#[derive(Debug)] #[derive(Debug)]

2
src/gemtext/mod.rs Normal file
View file

@ -0,0 +1,2 @@
pub mod parser;
pub use parser::{GemtextNode, Parser};

244
src/gemtext/parser.rs Normal file
View file

@ -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>, 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<String>,
lines: Vec<GemtextNode>,
}
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<GemtextNode> {
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);
}
}
}

View file

@ -2,6 +2,7 @@
pub mod certificate; pub mod certificate;
pub mod connection; pub mod connection;
pub mod fingerprint; pub mod fingerprint;
pub mod gemtext;
pub mod host; pub mod host;
pub mod mailbox; pub mod mailbox;
pub mod mailstore; pub mod mailstore;

View file

@ -1,3 +1,5 @@
use crate::message::Parser as MessageParser;
use super::*; use super::*;
use std::{ use std::{
fs::{self, File}, fs::{self, File},
@ -64,10 +66,13 @@ impl MailStore for Filesystem {
}; };
dir.filter_map(Result::ok).for_each(|e| { dir.filter_map(Result::ok).for_each(|e| {
if let Ok(contents) = fs::read_to_string(e.path()) { if let Ok(contents) = fs::read_to_string(e.path()) {
if let Ok(message) = contents.parse::<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); folder.messages.insert(message.id.clone(), message);
} }
} }
}
}); });
Some(folder) Some(folder)
} }

View file

@ -1,4 +1,7 @@
use {crate::prelude::{ParseHostError, ParseMailboxError}, 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
@ -7,6 +10,7 @@ pub enum Error {
EmptyUser, EmptyUser,
EmptyHost, EmptyHost,
EmptyMessage, EmptyMessage,
EmptySender,
ParseHostError(ParseHostError), ParseHostError(ParseHostError),
ParseMailboxError(ParseMailboxError), ParseMailboxError(ParseMailboxError),
MalformedLink, MalformedLink,
@ -25,6 +29,7 @@ impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self { match self {
Self::ParseHostError(e) => Some(e), Self::ParseHostError(e) => Some(e),
Self::ParseMailboxError(e) => Some(e),
_ => None, _ => None,
} }
} }

View file

@ -7,13 +7,9 @@ use serde::{Deserialize, Serialize};
mod error; mod error;
mod link; mod link;
mod parser; mod parser;
pub use { pub use {error::Error, link::Link, parser::Parser};
error::Error,
link::Link,
parser::{GemtextNode, Parser},
};
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, Default, PartialEq)]
pub struct Recipients { pub struct Recipients {
pub boxes: Vec<Mailbox>, pub boxes: Vec<Mailbox>,
} }
@ -55,6 +51,7 @@ pub struct Message {
impl fmt::Display for Message { impl fmt::Display for Message {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "< {}", self.from)?;
if !self.senders.is_empty() { if !self.senders.is_empty() {
write!(f, "< ")?; write!(f, "< ")?;
self.senders.iter().try_for_each(|s| writeln!(f, "{s}"))?; self.senders.iter().try_for_each(|s| writeln!(f, "{s}"))?;
@ -64,15 +61,10 @@ impl fmt::Display for Message {
self.recipients.iter().try_for_each(|r| write!(f, " {r}"))?; self.recipients.iter().try_for_each(|r| write!(f, " {r}"))?;
writeln!(f)?; writeln!(f)?;
} }
write!(f, "{}\r\n", self.body) 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<Self, Self::Err> {
todo!()
} }
} }

View file

@ -1,244 +1,85 @@
use super::{Link, Recipients}; use {
use crate::prelude::Mailbox; super::{Message, Recipients},
use std::fmt; crate::prelude::Mailbox,
};
#[derive(Debug)] #[derive(Debug, Default)]
pub struct PreBlk<'a> { pub struct Parser {
alt: Option<&'a str>, id: String,
lines: Vec<&'a str>, from: Option<Mailbox>,
} senders: Vec<Mailbox>,
recipients: Recipients,
#[derive(Debug)] timestamp: Option<String>,
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>, 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<String>, title: Option<String>,
lines: Vec<GemtextNode>, body: String,
} }
impl<'a> Parser<'a> { impl Parser {
pub fn new() -> Self { pub fn new(id: &str) -> Self {
Self { let mut p = Self::default();
state: State::Normal, p.id = id.to_string();
title: None, p
lines: vec![],
}
} }
pub fn parse(mut self, raw: &'a str) -> Vec<GemtextNode> { pub fn parse(mut self, content: &str) -> Result<Message, super::Error> {
for line in raw.lines() { let lines = content.lines();
match self.state { for l in lines {
State::Normal => self.parse_normal(line), match l {
State::Preformatted(_) => self.parse_preformatted(line), s if s.starts_with("<") && self.from.is_none() && self.body.is_empty() => {
State::Quote(_) => self.parse_quote(line), 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
} }
if self.from.is_none() {
fn link(&mut self, line: &'a str) { Err(super::Error::EmptySender)
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 { } else {
None Ok(Message {
}; id: self.id,
let preblk = PreBlk { alt, lines: vec![] }; from: self.from.unwrap(),
self.state = State::Preformatted(preblk); senders: self.senders,
} recipients: self.recipients.boxes,
timstamp: self.timestamp,
fn leave_preformatted(&mut self) { title: self.title,
match &self.state { body: self.body,
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);
} }
} }
} }

View file

@ -1,10 +1,11 @@
pub use super::{ pub use super::{
certificate::{Certificate, CertificateStore, ClientCertificateStore}, certificate::{Certificate, CertificateStore, ClientCertificateStore},
fingerprint::{Error as FingerprintError, Fingerprint, GetFingerprint}, fingerprint::{Error as FingerprintError, Fingerprint, GetFingerprint},
gemtext::{GemtextNode, Parser},
host::{Error as ParseHostError, Host}, host::{Error as ParseHostError, Host},
mailbox::{Error as ParseMailboxError, Mailbox}, mailbox::{Error as ParseMailboxError, Mailbox},
mailuser::Mailuser, mailuser::Mailuser,
message::{Error as ParseMessageError, GemtextNode, Message, Recipients}, message::{Error as ParseMessageError, Link, Message, Recipients},
//receiver, //receiver,
request::{Error as ParseRequestError, Request}, request::{Error as ParseRequestError, Request},
response::{Error as ParseResponseError, Response}, response::{Error as ParseResponseError, Response},

View file

@ -1,6 +1,6 @@
use { use {
crate::prelude::{CertificateStore, ClientCertificateStore, Mailuser, Request, Response}, crate::prelude::{CertificateStore, ClientCertificateStore, Mailuser, Request, Response},
rustls::{internal::msgs::codec::Codec, ClientConfig, ClientConnection, StreamOwned}, rustls::{ClientConfig, ClientConnection, StreamOwned},
std::{ std::{
io::{self, Read, Write}, io::{self, Read, Write},
net::{TcpStream, ToSocketAddrs}, net::{TcpStream, ToSocketAddrs},

View file

@ -1,6 +1,4 @@
mod error; mod error;
use std::fmt;
pub use error::Error; pub use error::Error;
#[cfg(feature = "serde")] #[cfg(feature = "serde")]