Merge branch 'odin' of git.hitchhiker-linux.org:jeang3nie/dory into odin

This commit is contained in:
Nathan Fisher 2023-06-09 01:26:00 -04:00
commit 5daa6ca377
31 changed files with 708 additions and 50 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};

374
src/gemtext/parser.rs Normal file
View 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());
}
}

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, 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,7 +10,9 @@ pub enum Error {
EmptyUser, EmptyUser,
EmptyHost, EmptyHost,
EmptyMessage, EmptyMessage,
EmptySender,
ParseHostError(ParseHostError), ParseHostError(ParseHostError),
ParseMailboxError(ParseMailboxError),
MalformedLink, MalformedLink,
} }
@ -24,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,
} }
} }
@ -34,3 +40,9 @@ impl From<ParseHostError> for Error {
Self::ParseHostError(value) Self::ParseHostError(value)
} }
} }
impl From<ParseMailboxError> for Error {
fn from(value: ParseMailboxError) -> Self {
Self::ParseMailboxError(value)
}
}

View file

@ -1,4 +1,4 @@
use crate::prelude::{Host, Mailbox}; use crate::prelude::Mailbox;
use std::{fmt, str::FromStr}; use std::{fmt, str::FromStr};
#[cfg(feature = "serde")] #[cfg(feature = "serde")]
@ -9,7 +9,7 @@ mod link;
mod parser; mod parser;
pub use {error::Error, link::Link, parser::Parser}; pub use {error::Error, link::Link, parser::Parser};
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, Default, PartialEq)]
pub struct Recipients { pub struct Recipients {
pub boxes: Vec<Mailbox>, pub boxes: Vec<Mailbox>,
} }
@ -21,34 +21,19 @@ impl fmt::Display for Recipients {
} }
} }
#[derive(Clone, Debug, PartialEq)] impl FromStr for Recipients {
pub enum Lines { type Err = Error;
Sender(Mailbox),
Recipients(Recipients),
Timestamp(String),
Text(String),
Heading1(String),
Heading2(String),
Heading3(String),
Quote(String),
Preformatted(String),
Link(Link),
}
impl fmt::Display for Lines { fn from_str(s: &str) -> Result<Self, Self::Err> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let mut rec = Recipients { boxes: vec![] };
match self { if let Some(s) = s.strip_prefix(':') {
Self::Sender(m) => writeln!(f, "< {m}"), s.split_whitespace().try_for_each(|r| {
Self::Recipients(r) => writeln!(f, "{r}"), let r = r.parse()?;
Self::Timestamp(t) => writeln!(f, "@ {t}"), rec.boxes.push(r);
Self::Text(t) => writeln!(f, "{t}"), Ok::<(), Error>(())
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}"),
} }
Ok(rec)
} }
} }
@ -59,13 +44,14 @@ pub struct Message {
pub from: Mailbox, pub from: Mailbox,
pub senders: Vec<Mailbox>, pub senders: Vec<Mailbox>,
pub recipients: Vec<Mailbox>, pub recipients: Vec<Mailbox>,
pub timstamp: Option<String>, pub timestamp: Option<String>,
pub title: Option<String>, pub title: Option<String>,
pub body: String, pub body: String,
} }
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}"))?;
@ -75,14 +61,28 @@ 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.timestamp {
writeln!(f, "@ {t}")?;
}
write!(f, "{}\r\n", self.body) write!(f, "{}\r\n", self.body)
} }
} }
impl FromStr for Message { #[cfg(test)]
type Err = String; mod tests {
use super::*;
fn from_str(s: &str) -> Result<Self, Self::Err> { static REC: &'static str = ": joe@example.org, jane@pizza.hut, mark@gemi.dev";
todo!()
#[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);
} }
} }

View file

@ -1,2 +1,125 @@
#[derive(Debug)] use {
pub struct Parser; 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"
);
}
}

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, Lines, 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},
@ -70,7 +70,8 @@ where
let cfg = match client_cert { let cfg = match client_cert {
None => cfg.with_no_client_auth(), None => cfg.with_no_client_auth(),
Some(c) => { 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 cert_chain = vec![rustls_cert];
let key_der = rustls::PrivateKey(c.key); let key_der = rustls::PrivateKey(c.key);
cfg.with_single_cert(cert_chain, key_der)? cfg.with_single_cert(cert_chain, key_der)?
@ -79,8 +80,8 @@ where
let client = ClientConnection::new(Arc::new(cfg), dnsname)?; let client = ClientConnection::new(Arc::new(cfg), dnsname)?;
let mut stream = StreamOwned::new(client, tcp_stream); let mut stream = StreamOwned::new(client, tcp_stream);
stream.write_all(self.request.to_string().as_bytes())?; stream.write_all(self.request.to_string().as_bytes())?;
let mut buf = vec![]; let mut buf = Vec::with_capacity(1024);
stream.read_to_end(&mut buf)?; let _res = stream.read_to_end(&mut buf);
stream.conn.send_close_notify(); stream.conn.send_close_notify();
drop(stream); drop(stream);
let res = buf.try_into()?; let res = buf.try_into()?;

View file

@ -32,7 +32,8 @@ pub enum Status {
impl From<Status> for u8 { impl From<Status> for u8 {
fn from(value: Status) -> Self { fn from(value: Status) -> Self {
match value { match value {
Status::Input | Status::Success => value.into(), Status::Input => 10,
Status::Success => 20,
Status::Redirect(n) => 30 + n as u8, Status::Redirect(n) => 30 + n as u8,
Status::TemporaryFailure(n) => 40 + n as u8, Status::TemporaryFailure(n) => 40 + n as u8,
Status::PermanentFailure(n) => 50 + 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 /// The mailbox has moved to a different address, and all future
/// messages should be sent to that address. /// messages should be sent to that address.
Permanent = 1, Permanent = 1,
Other, Other = 2,
} }
impl TryFrom<u8> for Redirect { impl TryFrom<u8> for Redirect {
@ -102,7 +103,7 @@ pub enum TemporaryFailure {
RateLimit = 4, RateLimit = 4,
/// The mailbox isn't accepting mail right now, but it might in the future. /// The mailbox isn't accepting mail right now, but it might in the future.
MailboxFull = 5, MailboxFull = 5,
Other, Other = 6,
} }
impl TryFrom<u8> for TemporaryFailure { impl TryFrom<u8> for TemporaryFailure {
@ -139,7 +140,7 @@ pub enum PermanentFailure {
DomainNotServiced = 3, DomainNotServiced = 3,
/// Your request is malformed, and won't be accepted by the mailserver. /// Your request is malformed, and won't be accepted by the mailserver.
BadRequest = 9, BadRequest = 9,
Other, Other = 4,
} }
impl TryFrom<u8> for PermanentFailure { 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 /// 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). /// legitimate sender. (This is reserved for a Hashcash style anti-spam measure).
ProofRequired = 4, ProofRequired = 4,
Other, Other = 5,
} }
impl TryFrom<u8> for AuthenticationFailure { impl TryFrom<u8> for AuthenticationFailure {
@ -200,6 +201,123 @@ impl TryFrom<u8> for AuthenticationFailure {
mod tests { mod tests {
use super::*; 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] #[test]
fn parse_status_success() { fn parse_status_success() {
let status = Status::try_from(21).unwrap(); let status = Status::try_from(21).unwrap();

View file

@ -0,0 +1 @@
Richard Stallman

View file

@ -0,0 +1 @@
Janet Weiss

View 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?

View file

@ -0,0 +1 @@
Rick Baker

View 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!

View file

@ -0,0 +1 @@
Jane Doh