Wrote message parser
This commit is contained in:
parent
ff96b5b56b
commit
4642443d37
11 changed files with 343 additions and 255 deletions
|
@ -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
2
src/gemtext/mod.rs
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
pub mod parser;
|
||||||
|
pub use parser::{GemtextNode, Parser};
|
244
src/gemtext/parser.rs
Normal file
244
src/gemtext/parser.rs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,18 +61,13 @@ 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)?;
|
||||||
}
|
}
|
||||||
|
if let Some(ref t) = self.timstamp {
|
||||||
|
writeln!(f, "@ {t}")?;
|
||||||
|
}
|
||||||
write!(f, "{}\r\n", self.body)
|
write!(f, "{}\r\n", self.body)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromStr for Message {
|
|
||||||
type Err = String;
|
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
|
@ -1,244 +1,85 @@
|
||||||
use super::{Link, Recipients};
|
use {
|
||||||
use crate::prelude::Mailbox;
|
super::{Message, Recipients},
|
||||||
use std::fmt;
|
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>, 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 {
|
#[derive(Debug, Default)]
|
||||||
let Some(line) = text.strip_prefix(':') else {
|
pub struct Parser {
|
||||||
return Self::Text(text.to_string());
|
id: String,
|
||||||
};
|
from: Option<Mailbox>,
|
||||||
let split = line.split_whitespace();
|
senders: Vec<Mailbox>,
|
||||||
let mut recipients: Recipients = Recipients { boxes: vec![] };
|
recipients: Recipients,
|
||||||
for s in split {
|
timestamp: Option<String>,
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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},
|
||||||
|
|
|
@ -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},
|
||||||
|
|
|
@ -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")]
|
||||||
|
|
Loading…
Add table
Reference in a new issue