Compare commits
10 commits
dcb182e4d0
...
67b7a2d0ea
Author | SHA1 | Date | |
---|---|---|---|
![]() |
67b7a2d0ea | ||
![]() |
8cc3c13389 | ||
![]() |
8dcd8455d4 | ||
![]() |
49b6728c78 | ||
![]() |
68cccc840b | ||
![]() |
af49f327ec | ||
![]() |
64b5051341 | ||
![]() |
c6675cb024 | ||
f17bda9349 | |||
![]() |
3a25914ec5 |
15 changed files with 545 additions and 89 deletions
|
@ -5,13 +5,20 @@ edition = "2021"
|
|||
|
||||
[dependencies]
|
||||
digest = "0.10"
|
||||
libc = "0.2.146"
|
||||
rustls-pemfile = "1.0"
|
||||
sha2 = "0.10"
|
||||
time = "0.3"
|
||||
tinyrand = "0.5"
|
||||
x509-parser = "0.15"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies.pw]
|
||||
git = "https://codeberg.org/jeang3nie/pw_rs.git"
|
||||
default-features = false
|
||||
features = [ "libc" ]
|
||||
|
||||
[dependencies.rustls]
|
||||
version = "0.21"
|
||||
features = [ "dangerous_configuration" ]
|
||||
|
|
|
@ -1,2 +1,78 @@
|
|||
use {
|
||||
crate::prelude::{IdError, ParseMessageError, ParseRequestError},
|
||||
std::{fmt, io, string::FromUtf8Error},
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Error;
|
||||
pub enum Error {
|
||||
Message(ParseMessageError),
|
||||
Id(IdError),
|
||||
Io(io::Error),
|
||||
Request(ParseRequestError),
|
||||
Storage(String),
|
||||
Rustls(rustls::Error),
|
||||
Utf8,
|
||||
}
|
||||
|
||||
impl fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Message(e) => write!(f, "{e}"),
|
||||
Self::Id(e) => write!(f, "{e}"),
|
||||
Self::Io(e) => write!(f, "{e}"),
|
||||
Self::Storage(e) => write!(f, "{e}"),
|
||||
Self::Request(e) => write!(f, "{e}"),
|
||||
Self::Rustls(e) => write!(f, "{e}"),
|
||||
Self::Utf8 => write!(f, "Utf8 error"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for Error {
|
||||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||
match self {
|
||||
Self::Message(e) => Some(e),
|
||||
Self::Id(e) => Some(e),
|
||||
Self::Io(e) => Some(e),
|
||||
Self::Request(e) => Some(e),
|
||||
Self::Rustls(e) => Some(e),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<IdError> for Error {
|
||||
fn from(value: IdError) -> Self {
|
||||
Self::Id(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<io::Error> for Error {
|
||||
fn from(value: io::Error) -> Self {
|
||||
Self::Io(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ParseMessageError> for Error {
|
||||
fn from(value: ParseMessageError) -> Self {
|
||||
Self::Message(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ParseRequestError> for Error {
|
||||
fn from(value: ParseRequestError) -> Self {
|
||||
Self::Request(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<rustls::Error> for Error {
|
||||
fn from(value: rustls::Error) -> Self {
|
||||
Self::Rustls(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<FromUtf8Error> for Error {
|
||||
fn from(_value: FromUtf8Error) -> Self {
|
||||
Self::Utf8
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,6 +3,18 @@ pub mod builder;
|
|||
pub mod error;
|
||||
pub mod verifier;
|
||||
|
||||
use {
|
||||
crate::{
|
||||
prelude::{ClientCertificateStore, Id, MailStore, MessageParser},
|
||||
request::Request,
|
||||
response::Response,
|
||||
},
|
||||
std::{
|
||||
io::Read,
|
||||
sync::{Arc, Mutex},
|
||||
},
|
||||
};
|
||||
|
||||
pub use self::{
|
||||
builder::Builder,
|
||||
error::Error,
|
||||
|
@ -14,4 +26,30 @@ pub struct Connection {
|
|||
pub inner: rustls::ServerConnection,
|
||||
}
|
||||
|
||||
impl Connection {}
|
||||
impl Connection {
|
||||
pub fn handle_request<C: ClientCertificateStore + FingerPrintStore, S: MailStore>(
|
||||
&mut self,
|
||||
verifier: Verifier<C>,
|
||||
mailstore: Arc<Mutex<S>>,
|
||||
) -> Result<Response, Error> {
|
||||
let mut buf: Vec<u8> = Vec::with_capacity(2048);
|
||||
self.inner.reader().read_to_end(&mut buf)?;
|
||||
let raw = String::from_utf8(buf)?;
|
||||
let request: Request = raw.parse()?;
|
||||
let res = if let Ok(mut store) = mailstore.lock() {
|
||||
if store.has_mailuser(&request.recipient.to_string()) {
|
||||
let id = Id::new()?;
|
||||
let msg = MessageParser::new(&id.to_string()).parse(&request.message)?;
|
||||
store
|
||||
.add_message(&request.recipient.to_string(), "Inbox", msg)
|
||||
.map_err(|e| Error::Storage(e.to_string()))?;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
} else {
|
||||
false
|
||||
};
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
use crate::mailuser::Mailuser;
|
||||
use rustls::server::ClientCertVerifier;
|
||||
use std::sync::Mutex;
|
||||
use crate::{fingerprint::GetFingerprint, mailuser::Mailuser};
|
||||
use rustls::server::{ClientCertVerified, ClientCertVerifier};
|
||||
use std::{
|
||||
io::Read,
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
use x509_parser::prelude::*;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Verifier<S: FingerPrintStore> {
|
||||
|
@ -23,7 +27,29 @@ impl<S: FingerPrintStore> ClientCertVerifier for Verifier<S> {
|
|||
end_entity: &rustls::Certificate,
|
||||
intermediates: &[rustls::Certificate],
|
||||
now: std::time::SystemTime,
|
||||
) -> Result<rustls::server::ClientCertVerified, rustls::Error> {
|
||||
) -> Result<ClientCertVerified, rustls::Error> {
|
||||
let fingerprint = end_entity.fingerprint()?;
|
||||
if let Ok(store) = self.store.lock() {
|
||||
if let Some(user) = store.get_mailuser(&fingerprint.fingerprint) {
|
||||
let (_, pk) = X509Certificate::from_der(end_entity.as_ref()).map_err(|e| {
|
||||
rustls::Error::InvalidCertificate(rustls::CertificateError::Other(Arc::new(e)))
|
||||
})?;
|
||||
let subject = pk.subject();
|
||||
let mut name_match = false;
|
||||
subject.iter_common_name().for_each(|n| {
|
||||
let mut val = n.attr_value().data;
|
||||
let mut name = String::new();
|
||||
if val.read_to_string(&mut name).is_ok() {
|
||||
name_match = name == user.to_string();
|
||||
}
|
||||
});
|
||||
if !name_match {
|
||||
return Err(rustls::Error::InvalidCertificate(
|
||||
rustls::CertificateError::NotValidForName,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
use {std::fmt, x509_parser::prelude::X509Error};
|
||||
use {
|
||||
std::{fmt, sync::Arc},
|
||||
x509_parser::prelude::X509Error,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
/// Errors which can occur when fingerprinting a certificate
|
||||
|
@ -38,3 +41,15 @@ impl From<x509_parser::nom::Err<x509_parser::error::X509Error>> for Error {
|
|||
Self::X509(value.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Error> for rustls::Error {
|
||||
fn from(value: Error) -> Self {
|
||||
match value {
|
||||
Error::Fmt => Self::General(String::from("Format Error")),
|
||||
Error::InvalidForDate => Self::InvalidCertificate(rustls::CertificateError::Expired),
|
||||
Error::X509(e) => {
|
||||
Self::InvalidCertificate(rustls::CertificateError::Other(Arc::new(e)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use crate::prelude::{DateTime, Link, Mailbox, Recipients};
|
||||
use crate::prelude::{DateTime, Id, Link, Mailbox, Recipients};
|
||||
use std::fmt;
|
||||
|
||||
#[derive(Debug)]
|
||||
|
@ -30,6 +30,12 @@ pub enum GemtextNode {
|
|||
Link(Link),
|
||||
}
|
||||
|
||||
impl From<Id> for GemtextNode {
|
||||
fn from(value: Id) -> Self {
|
||||
Self::Timestamp(value.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for GemtextNode {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
|
|
|
@ -1,23 +1,23 @@
|
|||
use std::io::BufReader;
|
||||
|
||||
use crate::prelude::Certificate;
|
||||
|
||||
use {
|
||||
super::*,
|
||||
crate::{
|
||||
message::Parser as MessageParser,
|
||||
prelude::{ClientCertificateStore, ParseMailboxError},
|
||||
prelude::{Certificate, ClientCertificateStore},
|
||||
},
|
||||
rustls_pemfile::{read_one, Item},
|
||||
std::{
|
||||
ffi::OsString,
|
||||
fs::{self, File},
|
||||
io::{self, BufWriter, Write},
|
||||
io::{self, BufReader, BufWriter, Write},
|
||||
iter,
|
||||
os::unix::fs::DirBuilderExt,
|
||||
path::{Path, PathBuf},
|
||||
},
|
||||
};
|
||||
mod error;
|
||||
use std::ffi::CString;
|
||||
|
||||
pub use error::Error;
|
||||
|
||||
pub trait MultiDomain: MailStore {
|
||||
type Error;
|
||||
|
@ -29,6 +29,43 @@ pub trait MultiDomain: MailStore {
|
|||
) -> Result<(), <Self as filesystem::MultiDomain>::Error>;
|
||||
}
|
||||
|
||||
/// The Filestore storage backend uses the filesystem to map folders to domains,
|
||||
/// mail users and folders of messages. It can also be used as a `ClientCertificateStore`.
|
||||
/// The layout of files inside a filesystem mailstore is as below.
|
||||
/// ```Sh
|
||||
/// test/mailstore/
|
||||
/// └── misfin.example.org
|
||||
/// ├── dick
|
||||
/// │ ├── Inbox
|
||||
/// │ │ ├── 619310.gmi
|
||||
/// │ │ └── 868379.gmi
|
||||
/// │ ├── blurb
|
||||
/// │ └── certificate.pem
|
||||
/// └── jane
|
||||
/// ├── Inbox
|
||||
/// │ ├── 252366.gmi
|
||||
/// │ └── 654064.gmi
|
||||
/// ├── blurb
|
||||
/// └── certificate.pem
|
||||
/// ```
|
||||
/// In this made up example the Misfin server in question serves the domain
|
||||
/// "misfin.example.org", which has the users "dick" and "jane", whose mailbox
|
||||
/// addresses would be "dick@misfin.example.org" and "jane@misfin.example.org".
|
||||
/// Within each account folder any subfolders are to be considered as mail folders,
|
||||
/// with the default folder for delivering new mail being "Inbox". The "blurb"
|
||||
/// file (optional) contains the display name of the user, and their client
|
||||
/// certificate is stored in pem-encoded ascii armor format in "certificate.pem",
|
||||
/// if the `Filesystem` is also to be used as a `ClientCertificateStore`.
|
||||
///
|
||||
/// The permissions on each individual account folder are set to 0o2700 at the
|
||||
/// time of creation, such that the folder is read/write for the user and group
|
||||
/// and any entries created within will have the same group membership. If the
|
||||
/// username of the account corresponds to a group name on the system, and the
|
||||
/// user which the server runs as is a member of this group, then said group will
|
||||
/// own the directory. In this way the server will be able to write incoming
|
||||
/// mail into the folder and the user will be able to read and manage their
|
||||
/// folders and messages, without being able to see any other user's mail and
|
||||
/// without the server requiring elevated privileges.
|
||||
#[derive(Clone, Debug)]
|
||||
#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
|
||||
pub struct Filesystem {
|
||||
|
@ -66,7 +103,7 @@ impl From<OsString> for Filesystem {
|
|||
}
|
||||
|
||||
impl MailStore for Filesystem {
|
||||
type Error = io::Error;
|
||||
type Error = Error;
|
||||
|
||||
fn users(&self) -> Vec<Mailuser> {
|
||||
let mut users = vec![];
|
||||
|
@ -149,6 +186,19 @@ impl MailStore for Filesystem {
|
|||
Some(folder)
|
||||
}
|
||||
|
||||
fn create_folder(&self, user: &str, folder: &str) -> Result<(), Error> {
|
||||
if self.has_mailuser(user) {
|
||||
let user: Mailbox = user.parse()?;
|
||||
let user: Mailuser = user.into();
|
||||
let mut path = self.path.clone();
|
||||
path.push(&user.host.to_string());
|
||||
path.push(&user.username);
|
||||
path.push(folder);
|
||||
fs::create_dir_all(&path)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_message(&self, user: &str, folder: &str, id: &str) -> Option<Message> {
|
||||
self.get_folder(user, folder)
|
||||
.and_then(|f| f.messages.get(id).cloned())
|
||||
|
@ -160,26 +210,23 @@ impl MailStore for Filesystem {
|
|||
folder: &str,
|
||||
message: Message,
|
||||
) -> Result<(), Self::Error> {
|
||||
let Some((user, host)) = user.rsplit_once('@') else {
|
||||
return Err(io::Error::new(io::ErrorKind::Other, "Invalid username"));
|
||||
};
|
||||
let mb: Mailbox = user.parse()?;
|
||||
let mut path = self.path.clone();
|
||||
path.push(host);
|
||||
path.push(user);
|
||||
path.push(&mb.host.to_string());
|
||||
path.push(&mb.username);
|
||||
path.push(folder);
|
||||
path.push(&message.id);
|
||||
path.set_extension("gmi");
|
||||
let mut fd = File::create(path)?;
|
||||
write!(fd, "{message}")
|
||||
write!(fd, "{message}")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn delete_message(&mut self, user: &str, folder: &str, id: &str) -> Result<(), Self::Error> {
|
||||
let Some((user, host)) = user.rsplit_once('@') else {
|
||||
return Err(io::Error::new(io::ErrorKind::Other, "Invalid username"));
|
||||
};
|
||||
let mb: Mailbox = user.parse()?;
|
||||
let mut path = self.path.clone();
|
||||
path.push(host);
|
||||
path.push(user);
|
||||
path.push(&mb.host.to_string());
|
||||
path.push(&mb.username);
|
||||
path.push(folder);
|
||||
path.push(id);
|
||||
path.set_extension("gmi");
|
||||
|
@ -195,13 +242,11 @@ impl MailStore for Filesystem {
|
|||
id: &str,
|
||||
folder1: &str,
|
||||
folder2: &str,
|
||||
) -> Result<(), io::Error> {
|
||||
let Some((user, host)) = user.rsplit_once('@') else {
|
||||
return Err(io::Error::new(io::ErrorKind::Other, "Invalid username"));
|
||||
};
|
||||
) -> Result<(), Self::Error> {
|
||||
let mb: Mailbox = user.parse()?;
|
||||
let mut infile = self.path.clone();
|
||||
infile.push(host);
|
||||
infile.push(user);
|
||||
infile.push(&mb.host.to_string());
|
||||
infile.push(&mb.username);
|
||||
let mut outfile = infile.clone();
|
||||
infile.push(folder1);
|
||||
infile.push(id);
|
||||
|
@ -215,35 +260,45 @@ impl MailStore for Filesystem {
|
|||
}
|
||||
|
||||
fn add_user(&mut self, user: &str) -> Result<(), Self::Error> {
|
||||
let mb: Mailbox = user
|
||||
.parse()
|
||||
.map_err(|e: ParseMailboxError| io::Error::new(io::ErrorKind::Other, e.to_string()))?;
|
||||
let mb: Mailbox = user.parse()?;
|
||||
let mut path = self.path.clone();
|
||||
path.push(&mb.host.to_string());
|
||||
path.push(&mb.username);
|
||||
path.push("Inbox");
|
||||
fs::create_dir_all(&path)?;
|
||||
fs::DirBuilder::new().recursive(true).create(&path)?;
|
||||
let p = CString::new(path.to_str().ok_or(Error::Utf8)?.to_string())?;
|
||||
if let Some(pw) = pw::Passwd::getpw()? {
|
||||
let groups = pw.groups()?;
|
||||
if let Some(gr) = groups.iter().find(|g| g.name == mb.username) {
|
||||
chown(p.clone(), pw.uid, gr.gid)?;
|
||||
}
|
||||
}
|
||||
// We have to explicitly call `chown` after creating the directory,
|
||||
// rather than setting permissions during creation, as the umask
|
||||
// might squash some of the bits we're specifically trying to set.
|
||||
chmod(p, 0o2770)?;
|
||||
if let Some(ref blurb) = mb.blurb {
|
||||
let _last = path.pop();
|
||||
path.push("blurb");
|
||||
let fd = File::options()
|
||||
.create(true)
|
||||
.write(true)
|
||||
.truncate(true)
|
||||
.open(path)?;
|
||||
.open(&path)?;
|
||||
let mut writer = BufWriter::new(fd);
|
||||
writer.write_all(blurb.as_bytes())?;
|
||||
}
|
||||
let _last = path.pop();
|
||||
path.push("Inbox");
|
||||
fs::create_dir_all(&path)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn remove_user(&mut self, user: &str) -> bool {
|
||||
let Ok(mu) = user.parse::<Mailbox>() else {
|
||||
let Ok(mb) = user.parse::<Mailbox>() else {
|
||||
return false;
|
||||
};
|
||||
let mut path = self.path.clone();
|
||||
path.push(&mu.host.to_string());
|
||||
path.push(&mu.username);
|
||||
path.push(&mb.host.to_string());
|
||||
path.push(&mb.username);
|
||||
if path.exists() {
|
||||
if fs::remove_dir_all(path).is_ok() {
|
||||
return true;
|
||||
|
@ -367,6 +422,8 @@ impl ClientCertificateStore for Filesystem {
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::os::unix::prelude::PermissionsExt;
|
||||
|
||||
use super::*;
|
||||
|
||||
fn store() -> Filesystem {
|
||||
|
@ -460,7 +517,29 @@ mod tests {
|
|||
.add_user("rob@misfin.example.org Rob Zombie")
|
||||
.unwrap();
|
||||
assert!(store().has_mailuser("rob@misfin.example.org"));
|
||||
let permissions = fs::metadata("test/mailstore/misfin.example.org/rob")
|
||||
.unwrap()
|
||||
.permissions();
|
||||
assert_eq!(permissions.mode(), 0o42770);
|
||||
assert!(store().remove_user("rob@misfin.example.org"));
|
||||
assert!(!store().has_mailuser("rob@misfin.example.org"));
|
||||
}
|
||||
}
|
||||
|
||||
fn chown(path: CString, uid: u32, gid: u32) -> Result<(), io::Error> {
|
||||
unsafe {
|
||||
if libc::chown(path.as_ptr(), uid, gid) != 0 {
|
||||
return Err(io::Error::last_os_error());
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn chmod(path: CString, mode: u32) -> Result<(), io::Error> {
|
||||
unsafe {
|
||||
if libc::chmod(path.as_ptr(), mode) != 0 {
|
||||
return Err(io::Error::last_os_error());
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
|
67
src/mailstore/filesystem/error.rs
Normal file
67
src/mailstore/filesystem/error.rs
Normal file
|
@ -0,0 +1,67 @@
|
|||
use {
|
||||
crate::prelude::ParseMailboxError,
|
||||
std::{ffi::NulError, fmt, io, str::Utf8Error},
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
Io(io::Error),
|
||||
MailBox(ParseMailboxError),
|
||||
FFi(NulError),
|
||||
Permissions(pw::Error),
|
||||
Utf8,
|
||||
}
|
||||
|
||||
impl fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Io(e) => write!(f, "Filesystem error: {e}"),
|
||||
Self::FFi(e) => write!(f, "Filesystem error: {e}"),
|
||||
Self::MailBox(e) => write!(f, "Filesystem error: {e}"),
|
||||
Self::Permissions(e) => write!(f, "Filesystem error: {e}"),
|
||||
Self::Utf8 => write!(f, "Filesystem error: Utf8 failure"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for Error {
|
||||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||
match self {
|
||||
Self::Io(e) => Some(e),
|
||||
Self::FFi(e) => Some(e),
|
||||
Self::MailBox(e) => Some(e),
|
||||
Self::Permissions(e) => Some(e),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<io::Error> for Error {
|
||||
fn from(value: io::Error) -> Self {
|
||||
Self::Io(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<NulError> for Error {
|
||||
fn from(value: NulError) -> Self {
|
||||
Self::FFi(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ParseMailboxError> for Error {
|
||||
fn from(value: ParseMailboxError) -> Self {
|
||||
Self::MailBox(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<pw::Error> for Error {
|
||||
fn from(value: pw::Error) -> Self {
|
||||
Self::Permissions(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Utf8Error> for Error {
|
||||
fn from(_value: Utf8Error) -> Self {
|
||||
Self::Utf8
|
||||
}
|
||||
}
|
|
@ -6,10 +6,11 @@ use std::{collections::HashMap, io, str::FromStr};
|
|||
#[cfg(feature = "serde")]
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub mod filesystem;
|
||||
mod filesystem;
|
||||
pub use filesystem::{Error as FilesystemError, Filesystem, MultiDomain};
|
||||
|
||||
pub trait MailStore {
|
||||
type Error;
|
||||
type Error: std::error::Error;
|
||||
|
||||
/// Retreives a list of all valid mailusers on this server
|
||||
fn users(&self) -> Vec<Mailuser>;
|
||||
|
@ -19,6 +20,8 @@ pub trait MailStore {
|
|||
fn has_mailuser(&self, mailuser: &str) -> bool;
|
||||
/// Retreives all of the messages for this user
|
||||
fn get_folder(&self, user: &str, folder: &str) -> Option<Folder>;
|
||||
/// Creates a folder for the specified user, if it doesn't already exist
|
||||
fn create_folder(&self, user: &str, folder: &str) -> Result<(), Self::Error>;
|
||||
/// Checks whether this server has a message with the given title for this user and if so
|
||||
/// returns the message
|
||||
fn get_message(&self, user: &str, folder: &str, id: &str) -> Option<Message>;
|
||||
|
@ -66,12 +69,6 @@ pub struct Folder {
|
|||
pub messages: HashMap<String, Message>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||
pub struct MultiDomain {
|
||||
pub domains: HashMap<String, Domain>,
|
||||
}
|
||||
|
||||
impl MailStore for Domain {
|
||||
type Error = io::Error;
|
||||
|
||||
|
@ -97,6 +94,19 @@ impl MailStore for Domain {
|
|||
.and_then(|u| u.folders.get(folder).cloned())
|
||||
}
|
||||
|
||||
fn create_folder(&self, user: &str, folder: &str) -> Result<(), io::Error> {
|
||||
self.users.get(user).cloned().and_then(|mut u| {
|
||||
u.folders.insert(
|
||||
folder.to_string(),
|
||||
Folder {
|
||||
name: folder.to_string(),
|
||||
messages: HashMap::new(),
|
||||
},
|
||||
)
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_message(&self, user: &str, folder: &str, title: &str) -> Option<Message> {
|
||||
self.users
|
||||
.get(user)
|
||||
|
|
110
src/message/id.rs
Normal file
110
src/message/id.rs
Normal file
|
@ -0,0 +1,110 @@
|
|||
use {
|
||||
std::{
|
||||
error, fmt,
|
||||
fs::File,
|
||||
io::{self, Read},
|
||||
time::{SystemTime, SystemTimeError, UNIX_EPOCH},
|
||||
},
|
||||
tinyrand::{RandRange, Seeded, StdRand},
|
||||
};
|
||||
|
||||
const ALPHABET: [char; 67] = [
|
||||
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's',
|
||||
't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L',
|
||||
'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '0', '1', '2', '3', '4',
|
||||
'5', '6', '7', '8', '9', '@', '%', '&', '=', '+',
|
||||
];
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Id {
|
||||
pub time: u64,
|
||||
pub id: [char; 8],
|
||||
}
|
||||
|
||||
impl fmt::Display for Id {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}.", self.time)?;
|
||||
self.id.iter().try_for_each(|c| write!(f, "{c}"))
|
||||
}
|
||||
}
|
||||
|
||||
impl Id {
|
||||
pub fn new() -> Result<Self, Error> {
|
||||
let time = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs();
|
||||
let mut rand = StdRand::seed(random_seed()?);
|
||||
let mut id = ['0', '0', '0', '0', '0', '0', '0', '0'];
|
||||
for item in &mut id {
|
||||
if let Some(c) = ALPHABET.get(rand.next_range(0..68)) {
|
||||
*item = *c;
|
||||
}
|
||||
}
|
||||
Ok(Self { time, id })
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
Io(io::Error),
|
||||
Time(SystemTimeError),
|
||||
}
|
||||
|
||||
impl fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Io(e) => write!(f, "{e}"),
|
||||
Self::Time(e) => write!(f, "{e}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl error::Error for Error {
|
||||
fn source(&self) -> Option<&(dyn error::Error + 'static)> {
|
||||
match self {
|
||||
Self::Io(e) => Some(e),
|
||||
Self::Time(e) => Some(e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<io::Error> for Error {
|
||||
fn from(value: io::Error) -> Self {
|
||||
Self::Io(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SystemTimeError> for Error {
|
||||
fn from(value: SystemTimeError) -> Self {
|
||||
Self::Time(value)
|
||||
}
|
||||
}
|
||||
|
||||
fn u64_from_bytes(b: [u8; 8]) -> u64 {
|
||||
u64::from(b[0])
|
||||
| (u64::from(b[1]) << 8)
|
||||
| (u64::from(b[2]) << 16)
|
||||
| (u64::from(b[3]) << 24)
|
||||
| (u64::from(b[4]) << 32)
|
||||
| (u64::from(b[5]) << 40)
|
||||
| (u64::from(b[6]) << 48)
|
||||
| (u64::from(b[7]) << 56)
|
||||
}
|
||||
|
||||
fn random_seed() -> io::Result<u64> {
|
||||
let mut buf = [0, 0, 0, 0, 0, 0, 0, 0];
|
||||
let mut fd = File::open("/dev/urandom")?;
|
||||
fd.read_exact(&mut buf)?;
|
||||
Ok(u64_from_bytes(buf))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn new_id() {
|
||||
let a = Id::new().unwrap();
|
||||
let b = Id::new().unwrap();
|
||||
assert_eq!(a.time / 10, b.time / 10);
|
||||
assert_ne!(a.id, b.id);
|
||||
}
|
||||
}
|
|
@ -6,9 +6,15 @@ use std::{fmt, str::FromStr};
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
mod error;
|
||||
mod id;
|
||||
mod link;
|
||||
mod parser;
|
||||
pub use {error::Error, link::Link, parser::Parser};
|
||||
pub use {
|
||||
error::Error,
|
||||
id::{Error as IdError, Id},
|
||||
link::Link,
|
||||
parser::Parser,
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct Recipients {
|
||||
|
|
|
@ -22,6 +22,7 @@ impl Parser {
|
|||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::missing_panics_doc)]
|
||||
pub fn parse(mut self, content: &str) -> Result<Message, super::Error> {
|
||||
let lines = content.lines();
|
||||
for l in lines {
|
||||
|
|
|
@ -4,8 +4,11 @@ pub use super::{
|
|||
gemtext::{GemtextNode, Parser as GemtextParser},
|
||||
host::{Error as ParseHostError, Host},
|
||||
mailbox::{Error as ParseMailboxError, Mailbox},
|
||||
mailstore::{Account, Domain, Filesystem, FilesystemError, Folder, MailStore},
|
||||
mailuser::Mailuser,
|
||||
message::{Error as ParseMessageError, Link, Message, Recipients},
|
||||
message::{
|
||||
Error as ParseMessageError, Id, IdError, Link, Message, Parser as MessageParser, Recipients,
|
||||
},
|
||||
//receiver,
|
||||
request::{Error as ParseRequestError, Request},
|
||||
response::{Error as ParseResponseError, Response},
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
//! A container representing a moment in ISO-8601 format Time
|
||||
#![allow(clippy::module_name_repetitions)]
|
||||
use crate::prelude::Id;
|
||||
use std::{fmt, str::FromStr};
|
||||
|
||||
mod error;
|
||||
|
@ -8,13 +10,13 @@ pub use {error::Error, parser::Parser};
|
|||
#[cfg(feature = "serde")]
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
const SECS_PER_DAY: i64 = 86400;
|
||||
const SECS_PER_DAY: u64 = 86400;
|
||||
|
||||
fn days_since_epoch(ts: i64) -> i64 {
|
||||
fn days_since_epoch(ts: u64) -> u64 {
|
||||
ts / SECS_PER_DAY
|
||||
}
|
||||
|
||||
fn is_leap(year: i64) -> bool {
|
||||
fn is_leap(year: u32) -> bool {
|
||||
year % 4 == 0 && (year % 100 != 0 || year % 16 == 0)
|
||||
}
|
||||
|
||||
|
@ -22,7 +24,7 @@ fn is_leap(year: i64) -> bool {
|
|||
/// given month is February, then we also need to know
|
||||
/// whether it is a leap year or not, hence this function
|
||||
/// takes the year as an argument as well.
|
||||
pub fn days_in_month(month: u8, year: i64) -> i64 {
|
||||
pub fn days_in_month(month: u8, year: u32) -> u8 {
|
||||
assert!(month < 13);
|
||||
match month {
|
||||
1 | 3 | 5 | 7 | 10 | 12 => 31,
|
||||
|
@ -142,8 +144,8 @@ impl fmt::Display for DateTime {
|
|||
|
||||
impl PartialOrd for DateTime {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||
let a: i64 = (*self).into();
|
||||
let b: i64 = (*other).into();
|
||||
let a: u64 = (*self).into();
|
||||
let b: u64 = (*other).into();
|
||||
a.partial_cmp(&b)
|
||||
}
|
||||
}
|
||||
|
@ -156,12 +158,8 @@ impl FromStr for DateTime {
|
|||
}
|
||||
}
|
||||
|
||||
impl From<i64> for DateTime {
|
||||
fn from(ts: i64) -> Self {
|
||||
// Only allow using this function for positive timestamp values,
|
||||
// ie no dates before 1970-01-01
|
||||
assert!(ts.is_positive());
|
||||
|
||||
impl From<u64> for DateTime {
|
||||
fn from(ts: u64) -> Self {
|
||||
let mut year = 1970;
|
||||
let mut days = days_since_epoch(ts);
|
||||
loop {
|
||||
|
@ -174,9 +172,11 @@ impl From<i64> for DateTime {
|
|||
}
|
||||
}
|
||||
let mut month = 1;
|
||||
while days_in_month(month, year) <= days {
|
||||
days -= days_in_month(month, year);
|
||||
let mut m_days = u64::from(days_in_month(month, year));
|
||||
while m_days <= days {
|
||||
days -= m_days;
|
||||
month += 1;
|
||||
m_days = u64::from(days_in_month(month, year));
|
||||
}
|
||||
let seconds = ts % SECS_PER_DAY;
|
||||
let minutes = seconds / 60;
|
||||
|
@ -184,8 +184,8 @@ impl From<i64> for DateTime {
|
|||
let hour = minutes / 60;
|
||||
let minute = minutes % 60;
|
||||
DateTime {
|
||||
year: year as u32,
|
||||
month: month.try_into().unwrap(),
|
||||
year,
|
||||
month,
|
||||
day: (days + 1).try_into().unwrap(),
|
||||
hour: Some((hour).try_into().unwrap()),
|
||||
minute: Some((minute).try_into().unwrap()),
|
||||
|
@ -195,20 +195,28 @@ impl From<i64> for DateTime {
|
|||
}
|
||||
}
|
||||
|
||||
impl From<DateTime> for i64 {
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
impl From<Id> for DateTime {
|
||||
fn from(value: Id) -> Self {
|
||||
let time = value.time;
|
||||
time.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DateTime> for u64 {
|
||||
fn from(dt: DateTime) -> Self {
|
||||
let mut seconds = dt.second.unwrap_or(0).into();
|
||||
seconds += dt.minute.unwrap_or(0) as i64 * 60;
|
||||
seconds += (dt.hour.unwrap_or(1)) as i64 * 3600;
|
||||
seconds += SECS_PER_DAY * (dt.day as i64 - 1);
|
||||
seconds += u64::from(dt.minute.unwrap_or(0)) * 60;
|
||||
seconds += u64::from(dt.hour.unwrap_or(1)) * 3600;
|
||||
seconds += SECS_PER_DAY * (u64::from(dt.day) - 1);
|
||||
let mut m = 1;
|
||||
while m < dt.month {
|
||||
seconds += SECS_PER_DAY * days_in_month(m, dt.year.into());
|
||||
seconds += SECS_PER_DAY * u64::from(days_in_month(m, dt.year));
|
||||
m += 1;
|
||||
}
|
||||
let mut y = 1970;
|
||||
while y < dt.year {
|
||||
if is_leap(y.into()) {
|
||||
if is_leap(y) {
|
||||
seconds += SECS_PER_DAY * 366;
|
||||
} else {
|
||||
seconds += SECS_PER_DAY * 365;
|
||||
|
@ -216,9 +224,9 @@ impl From<DateTime> for i64 {
|
|||
y += 1;
|
||||
}
|
||||
if let Some(TimeZone::Offset(offset)) = dt.tz {
|
||||
let mut offset_seconds = offset.hours as i64 * 3600;
|
||||
let mut offset_seconds = u64::from(offset.hours) * 3600;
|
||||
if let Some(offset_minutes) = offset.minutes {
|
||||
offset_seconds += offset_minutes as i64 * 60;
|
||||
offset_seconds += u64::from(offset_minutes) * 60;
|
||||
}
|
||||
match offset.sign {
|
||||
Sign::Positive => seconds += offset_seconds,
|
||||
|
@ -237,7 +245,7 @@ impl DateTime {
|
|||
/// offset is applied and the result returned.
|
||||
pub fn normalize(&mut self) -> &Self {
|
||||
if let Some(TimeZone::Offset(_)) = self.tz {
|
||||
*self = i64::from(*self).into();
|
||||
*self = u64::from(*self).into();
|
||||
}
|
||||
self
|
||||
}
|
||||
|
|
|
@ -55,7 +55,7 @@ impl<'a> Parser<'a> {
|
|||
|
||||
#[allow(clippy::missing_panics_doc)]
|
||||
fn validate_day(&self, day: u8) -> Result<(), Error> {
|
||||
let max = days_in_month(self.month.unwrap(), self.year.unwrap() as i64) as u8;
|
||||
let max = days_in_month(self.month.unwrap(), self.year.unwrap());
|
||||
if day > max {
|
||||
Err(Error::InvalidDay)
|
||||
} else {
|
||||
|
@ -316,18 +316,22 @@ impl<'a> Parser<'a> {
|
|||
}
|
||||
_ => {}
|
||||
}
|
||||
if self.year.is_none() || self.month.is_none() || self.day.is_none() {
|
||||
return Err(Error::Truncated);
|
||||
if let Some(year) = self.year {
|
||||
if let Some(month) = self.month {
|
||||
if let Some(day) = self.day {
|
||||
return Ok(DateTime {
|
||||
year,
|
||||
month,
|
||||
day,
|
||||
hour: self.hour,
|
||||
minute: self.minute,
|
||||
second: self.second,
|
||||
tz: self.tz,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(DateTime {
|
||||
year: self.year.unwrap(),
|
||||
month: self.month.unwrap(),
|
||||
day: self.day.unwrap(),
|
||||
hour: self.hour,
|
||||
minute: self.minute,
|
||||
second: self.second,
|
||||
tz: self.tz,
|
||||
})
|
||||
Err(Error::Truncated)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue