feat: implement Smol Mail server in Rust with nix flake deployment
This commit is contained in:
commit
71ad7e04c0
14 changed files with 2592 additions and 0 deletions
123
src/main.rs
Normal file
123
src/main.rs
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
//! Smol Mail server.
|
||||
|
||||
mod channel;
|
||||
mod crypto;
|
||||
mod proto;
|
||||
mod ratelimit;
|
||||
mod server;
|
||||
mod session;
|
||||
mod store;
|
||||
|
||||
use std::io::Write;
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::fs::OpenOptionsExt;
|
||||
|
||||
use clap::{Parser, Subcommand};
|
||||
|
||||
use crate::proto::DEFAULT_PORT;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(about = "Smol Mail server")]
|
||||
struct Cli {
|
||||
#[arg(short, long, global = true)]
|
||||
verbose: bool,
|
||||
|
||||
#[command(subcommand)]
|
||||
command: Command,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum Command {
|
||||
/// Generate the server's static X25519 key
|
||||
Keygen {
|
||||
#[arg(long, default_value = "server.key")]
|
||||
key: String,
|
||||
#[arg(long)]
|
||||
force: bool,
|
||||
},
|
||||
/// Run the mailbox server
|
||||
Serve {
|
||||
#[arg(long, default_value = "server.key")]
|
||||
key: String,
|
||||
#[arg(long, default_value = "mail.db")]
|
||||
db: String,
|
||||
#[arg(long, default_value = "127.0.0.1")]
|
||||
host: String,
|
||||
#[arg(long, default_value_t = DEFAULT_PORT)]
|
||||
port: u16,
|
||||
#[arg(long = "max-envelope", default_value_t = 1 << 20)]
|
||||
max_envelope: usize,
|
||||
#[arg(long, default_value_t = 64 << 20)]
|
||||
quota: i64,
|
||||
#[arg(long = "retention-days", default_value_t = 30)]
|
||||
retention_days: i64,
|
||||
#[arg(long = "invite-token")]
|
||||
invite_token: Option<String>,
|
||||
#[arg(long = "rate-connections", default_value_t = 120)]
|
||||
rate_connections: u32,
|
||||
#[arg(long = "rate-sends", default_value_t = 60)]
|
||||
rate_sends: u32,
|
||||
},
|
||||
}
|
||||
|
||||
fn main() -> anyhow::Result<()> {
|
||||
let cli = Cli::parse();
|
||||
|
||||
env_logger::Builder::new()
|
||||
.filter_level(if cli.verbose {
|
||||
log::LevelFilter::Debug
|
||||
} else {
|
||||
log::LevelFilter::Info
|
||||
})
|
||||
.format_timestamp_secs()
|
||||
.init();
|
||||
|
||||
match cli.command {
|
||||
Command::Keygen { key, force } => cmd_keygen(&key, force),
|
||||
Command::Serve {
|
||||
key,
|
||||
db,
|
||||
host,
|
||||
port,
|
||||
max_envelope,
|
||||
quota,
|
||||
retention_days,
|
||||
invite_token,
|
||||
rate_connections,
|
||||
rate_sends,
|
||||
} => server::run(server::ServeArgs {
|
||||
key_path: key,
|
||||
db_path: db,
|
||||
host,
|
||||
port,
|
||||
max_envelope,
|
||||
quota,
|
||||
retention_days,
|
||||
invite_token,
|
||||
rate_connections,
|
||||
rate_sends,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn cmd_keygen(key_path: &str, force: bool) -> anyhow::Result<()> {
|
||||
if std::path::Path::new(key_path).exists() && !force {
|
||||
anyhow::bail!("{key_path} exists; refusing to overwrite (use --force)");
|
||||
}
|
||||
|
||||
let (private, public) = crypto::generate_static_key();
|
||||
|
||||
// Written 0600 before any bytes land, so the key is never briefly readable.
|
||||
let mut opts = std::fs::OpenOptions::new();
|
||||
opts.write(true).create(true).truncate(true);
|
||||
#[cfg(unix)]
|
||||
opts.mode(0o600);
|
||||
let mut file = opts.open(key_path)?;
|
||||
file.write_all(&private)?;
|
||||
|
||||
println!("private key: {key_path}");
|
||||
println!("public key: {}", crypto::b32(&public));
|
||||
println!();
|
||||
println!("Publish the public key through a trusted channel; clients pin it (SPEC.md sec 4).");
|
||||
Ok(())
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue