Merge branch 'odin' of git.hitchhiker-linux.org:jeang3nie/dory into odin
This commit is contained in:
commit
5daa6ca377
31 changed files with 708 additions and 50 deletions
|
@ -1,6 +1,5 @@
|
|||
use crate::mailuser::Mailuser;
|
||||
use rustls::server::ClientCertVerifier;
|
||||
|
||||
use crate::{mailuser::Mailuser, prelude::CertificateStore};
|
||||
use std::sync::Mutex;
|
||||
|
||||
#[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};
|
374
src/gemtext/parser.rs
Normal file
374
src/gemtext/parser.rs
Normal file
|
@ -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>, 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<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),
|
||||
}
|
||||
}
|
||||
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<GemtextNode> {
|
||||
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());
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
use crate::message::Parser as MessageParser;
|
||||
|
||||
use super::*;
|
||||
use std::{
|
||||
fs::{self, File},
|
||||
|
@ -64,10 +66,13 @@ 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::<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);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
Some(folder)
|
||||
}
|
||||
|
|
|
@ -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<ParseHostError> for Error {
|
|||
Self::ParseHostError(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ParseMailboxError> for Error {
|
||||
fn from(value: ParseMailboxError) -> Self {
|
||||
Self::ParseMailboxError(value)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<Mailbox>,
|
||||
}
|
||||
|
@ -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<Self, Self::Err> {
|
||||
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<Mailbox>,
|
||||
pub recipients: Vec<Mailbox>,
|
||||
pub timstamp: Option<String>,
|
||||
pub timestamp: Option<String>,
|
||||
pub title: Option<String>,
|
||||
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<Self, Self::Err> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<Mailbox>,
|
||||
senders: Vec<Mailbox>,
|
||||
recipients: Recipients,
|
||||
timestamp: Option<String>,
|
||||
title: Option<String>,
|
||||
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<Message, super::Error> {
|
||||
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"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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},
|
||||
|
|
|
@ -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()?;
|
||||
|
|
|
@ -32,7 +32,8 @@ pub enum Status {
|
|||
impl From<Status> 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<u8> 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<u8> 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<u8> 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<u8> for AuthenticationFailure {
|
||||
|
@ -200,6 +201,123 @@ impl TryFrom<u8> 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();
|
||||
|
|
0
test/mailstore/mail.gmi.org/dick/Inbox/623273.gmi
Normal file
0
test/mailstore/mail.gmi.org/dick/Inbox/623273.gmi
Normal file
0
test/mailstore/mail.gmi.org/dick/Inbox/738127.gmi
Normal file
0
test/mailstore/mail.gmi.org/dick/Inbox/738127.gmi
Normal file
0
test/mailstore/mail.gmi.org/dick/Lists/158468.gmi
Normal file
0
test/mailstore/mail.gmi.org/dick/Lists/158468.gmi
Normal file
0
test/mailstore/mail.gmi.org/dick/Lists/431652.gmi
Normal file
0
test/mailstore/mail.gmi.org/dick/Lists/431652.gmi
Normal file
1
test/mailstore/mail.gmi.org/dick/blurb
Normal file
1
test/mailstore/mail.gmi.org/dick/blurb
Normal file
|
@ -0,0 +1 @@
|
|||
Richard Stallman
|
0
test/mailstore/mail.gmi.org/jane/Inbox/488571.gmi
Normal file
0
test/mailstore/mail.gmi.org/jane/Inbox/488571.gmi
Normal file
0
test/mailstore/mail.gmi.org/jane/Inbox/867017.gmi
Normal file
0
test/mailstore/mail.gmi.org/jane/Inbox/867017.gmi
Normal file
0
test/mailstore/mail.gmi.org/jane/Lists/476324.gmi
Normal file
0
test/mailstore/mail.gmi.org/jane/Lists/476324.gmi
Normal file
0
test/mailstore/mail.gmi.org/jane/Lists/891249.gmi
Normal file
0
test/mailstore/mail.gmi.org/jane/Lists/891249.gmi
Normal file
1
test/mailstore/mail.gmi.org/jane/blurb
Normal file
1
test/mailstore/mail.gmi.org/jane/blurb
Normal file
|
@ -0,0 +1 @@
|
|||
Janet Weiss
|
5
test/mailstore/misfin.example.org/dick/Inbox/619310.gmi
Normal file
5
test/mailstore/misfin.example.org/dick/Inbox/619310.gmi
Normal file
|
@ -0,0 +1,5 @@
|
|||
< joe@gemini.example.org
|
||||
@ 2023-06-07T16:09:42Z
|
||||
# How 'bout dose Bears?
|
||||
When are they coming for dinner anyway?
|
||||
|
0
test/mailstore/misfin.example.org/dick/Inbox/868379.gmi
Normal file
0
test/mailstore/misfin.example.org/dick/Inbox/868379.gmi
Normal file
0
test/mailstore/misfin.example.org/dick/Lists/448785.gmi
Normal file
0
test/mailstore/misfin.example.org/dick/Lists/448785.gmi
Normal file
0
test/mailstore/misfin.example.org/dick/Lists/645815.gmi
Normal file
0
test/mailstore/misfin.example.org/dick/Lists/645815.gmi
Normal file
1
test/mailstore/misfin.example.org/dick/blurb
Normal file
1
test/mailstore/misfin.example.org/dick/blurb
Normal file
|
@ -0,0 +1 @@
|
|||
Rick Baker
|
0
test/mailstore/misfin.example.org/jane/Inbox/252366.gmi
Normal file
0
test/mailstore/misfin.example.org/jane/Inbox/252366.gmi
Normal file
0
test/mailstore/misfin.example.org/jane/Inbox/654064.gmi
Normal file
0
test/mailstore/misfin.example.org/jane/Inbox/654064.gmi
Normal file
0
test/mailstore/misfin.example.org/jane/Lists/620426.gmi
Normal file
0
test/mailstore/misfin.example.org/jane/Lists/620426.gmi
Normal file
13
test/mailstore/misfin.example.org/jane/Lists/700070.gmi
Normal file
13
test/mailstore/misfin.example.org/jane/Lists/700070.gmi
Normal file
|
@ -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!
|
1
test/mailstore/misfin.example.org/jane/blurb
Normal file
1
test/mailstore/misfin.example.org/jane/blurb
Normal file
|
@ -0,0 +1 @@
|
|||
Jane Doh
|
Loading…
Add table
Reference in a new issue