aboutsummaryrefslogtreecommitdiff
path: root/aerogramme
diff options
context:
space:
mode:
Diffstat (limited to 'aerogramme')
-rw-r--r--aerogramme/Cargo.toml12
-rw-r--r--aerogramme/src/k2v_util.rs26
-rw-r--r--aerogramme/src/lib.rs19
-rw-r--r--aerogramme/src/main.rs407
-rw-r--r--aerogramme/src/server.rs147
5 files changed, 611 insertions, 0 deletions
diff --git a/aerogramme/Cargo.toml b/aerogramme/Cargo.toml
new file mode 100644
index 0000000..e408aec
--- /dev/null
+++ b/aerogramme/Cargo.toml
@@ -0,0 +1,12 @@
+[package]
+name = "aerogramme"
+version = "0.3.0"
+authors = ["Alex Auvolat <alex@adnab.me>", "Quentin Dufour <quentin@dufour.io>"]
+edition = "2021"
+license = "EUPL-1.2"
+description = "A robust email server"
+
+[[test]]
+name = "behavior"
+path = "tests/behavior.rs"
+harness = false
diff --git a/aerogramme/src/k2v_util.rs b/aerogramme/src/k2v_util.rs
new file mode 100644
index 0000000..3cd969b
--- /dev/null
+++ b/aerogramme/src/k2v_util.rs
@@ -0,0 +1,26 @@
+/*
+use anyhow::Result;
+// ---- UTIL: function to wait for a value to have changed in K2V ----
+
+pub async fn k2v_wait_value_changed(
+ k2v: &storage::RowStore,
+ key: &storage::RowRef,
+) -> Result<CausalValue> {
+ loop {
+ if let Some(ct) = prev_ct {
+ match k2v.poll_item(pk, sk, ct.clone(), None).await? {
+ None => continue,
+ Some(cv) => return Ok(cv),
+ }
+ } else {
+ match k2v.read_item(pk, sk).await {
+ Err(k2v_client::Error::NotFound) => {
+ k2v.insert_item(pk, sk, vec![0u8], None).await?;
+ }
+ Err(e) => return Err(e.into()),
+ Ok(cv) => return Ok(cv),
+ }
+ }
+ }
+}
+*/
diff --git a/aerogramme/src/lib.rs b/aerogramme/src/lib.rs
new file mode 100644
index 0000000..f065478
--- /dev/null
+++ b/aerogramme/src/lib.rs
@@ -0,0 +1,19 @@
+#![feature(type_alias_impl_trait)]
+#![feature(async_fn_in_trait)]
+#![feature(async_closure)]
+#![feature(trait_alias)]
+
+pub mod auth;
+pub mod bayou;
+pub mod config;
+pub mod cryptoblob;
+pub mod dav;
+pub mod imap;
+pub mod k2v_util;
+pub mod lmtp;
+pub mod login;
+pub mod mail;
+pub mod server;
+pub mod storage;
+pub mod timestamp;
+pub mod user;
diff --git a/aerogramme/src/main.rs b/aerogramme/src/main.rs
new file mode 100644
index 0000000..43b4dca
--- /dev/null
+++ b/aerogramme/src/main.rs
@@ -0,0 +1,407 @@
+use std::io::Read;
+use std::path::PathBuf;
+
+use anyhow::{bail, Context, Result};
+use clap::{Parser, Subcommand};
+use nix::{sys::signal, unistd::Pid};
+
+use aerogramme::config::*;
+use aerogramme::login::{static_provider::*, *};
+use aerogramme::server::Server;
+
+#[derive(Parser, Debug)]
+#[clap(author, version, about, long_about = None)]
+struct Args {
+ #[clap(subcommand)]
+ command: Command,
+
+ /// A special mode dedicated to developers, NOT INTENDED FOR PRODUCTION
+ #[clap(long)]
+ dev: bool,
+
+ #[clap(
+ short,
+ long,
+ env = "AEROGRAMME_CONFIG",
+ default_value = "aerogramme.toml"
+ )]
+ /// Path to the main Aerogramme configuration file
+ config_file: PathBuf,
+}
+
+#[derive(Subcommand, Debug)]
+enum Command {
+ #[clap(subcommand)]
+ /// A daemon to be run by the end user, on a personal device
+ Companion(CompanionCommand),
+
+ #[clap(subcommand)]
+ /// A daemon to be run by the service provider, on a server
+ Provider(ProviderCommand),
+
+ #[clap(subcommand)]
+ /// Specific tooling, should not be part of a normal workflow, for debug & experimentation only
+ Tools(ToolsCommand),
+ //Test,
+}
+
+#[derive(Subcommand, Debug)]
+enum ToolsCommand {
+ /// Manage crypto roots
+ #[clap(subcommand)]
+ CryptoRoot(CryptoRootCommand),
+
+ PasswordHash {
+ #[clap(env = "AEROGRAMME_PASSWORD")]
+ maybe_password: Option<String>,
+ },
+}
+
+#[derive(Subcommand, Debug)]
+enum CryptoRootCommand {
+ /// Generate a new crypto-root protected with a password
+ New {
+ #[clap(env = "AEROGRAMME_PASSWORD")]
+ maybe_password: Option<String>,
+ },
+ /// Generate a new clear text crypto-root, store it securely!
+ NewClearText,
+ /// Change the password of a crypto key
+ ChangePassword {
+ #[clap(env = "AEROGRAMME_OLD_PASSWORD")]
+ maybe_old_password: Option<String>,
+
+ #[clap(env = "AEROGRAMME_NEW_PASSWORD")]
+ maybe_new_password: Option<String>,
+
+ #[clap(short, long, env = "AEROGRAMME_CRYPTO_ROOT")]
+ crypto_root: String,
+ },
+ /// From a given crypto-key, derive one containing only the public key
+ DeriveIncoming {
+ #[clap(short, long, env = "AEROGRAMME_CRYPTO_ROOT")]
+ crypto_root: String,
+ },
+}
+
+#[derive(Subcommand, Debug)]
+enum CompanionCommand {
+ /// Runs the IMAP proxy
+ Daemon,
+ Reload {
+ #[clap(short, long, env = "AEROGRAMME_PID")]
+ pid: Option<i32>,
+ },
+ Wizard,
+ #[clap(subcommand)]
+ Account(AccountManagement),
+}
+
+#[derive(Subcommand, Debug)]
+enum ProviderCommand {
+ /// Runs the IMAP+LMTP server daemon
+ Daemon,
+ /// Reload the daemon
+ Reload {
+ #[clap(short, long, env = "AEROGRAMME_PID")]
+ pid: Option<i32>,
+ },
+ /// Manage static accounts
+ #[clap(subcommand)]
+ Account(AccountManagement),
+}
+
+#[derive(Subcommand, Debug)]
+enum AccountManagement {
+ /// Add an account
+ Add {
+ #[clap(short, long)]
+ login: String,
+ #[clap(short, long)]
+ setup: PathBuf,
+ },
+ /// Delete an account
+ Delete {
+ #[clap(short, long)]
+ login: String,
+ },
+ /// Change password for a given account
+ ChangePassword {
+ #[clap(env = "AEROGRAMME_OLD_PASSWORD")]
+ maybe_old_password: Option<String>,
+
+ #[clap(env = "AEROGRAMME_NEW_PASSWORD")]
+ maybe_new_password: Option<String>,
+
+ #[clap(short, long)]
+ login: String,
+ },
+}
+
+#[cfg(tokio_unstable)]
+fn tracer() {
+ console_subscriber::init();
+}
+
+#[cfg(not(tokio_unstable))]
+fn tracer() {
+ tracing_subscriber::fmt::init();
+}
+
+#[tokio::main]
+async fn main() -> Result<()> {
+ if std::env::var("RUST_LOG").is_err() {
+ std::env::set_var("RUST_LOG", "main=info,aerogramme=info,k2v_client=info")
+ }
+
+ // Abort on panic (same behavior as in Go)
+ std::panic::set_hook(Box::new(|panic_info| {
+ eprintln!("{}", panic_info);
+ eprintln!("{:?}", backtrace::Backtrace::new());
+ std::process::abort();
+ }));
+
+ tracer();
+
+ let args = Args::parse();
+ let any_config = if args.dev {
+ use std::net::*;
+ AnyConfig::Provider(ProviderConfig {
+ pid: None,
+ imap: None,
+ imap_unsecure: Some(ImapUnsecureConfig {
+ bind_addr: SocketAddr::new(IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)), 1143),
+ }),
+ dav_unsecure: Some(DavUnsecureConfig {
+ bind_addr: SocketAddr::new(IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)), 8087),
+ }),
+ lmtp: Some(LmtpConfig {
+ bind_addr: SocketAddr::new(IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)), 1025),
+ hostname: "example.tld".to_string(),
+ }),
+ auth: Some(AuthConfig {
+ bind_addr: SocketAddr::new(
+ IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)),
+ 12345,
+ ),
+ }),
+ users: UserManagement::Demo,
+ })
+ } else {
+ read_config(args.config_file)?
+ };
+
+ match (&args.command, any_config) {
+ (Command::Companion(subcommand), AnyConfig::Companion(config)) => match subcommand {
+ CompanionCommand::Daemon => {
+ let server = Server::from_companion_config(config).await?;
+ server.run().await?;
+ }
+ CompanionCommand::Reload { pid } => reload(*pid, config.pid)?,
+ CompanionCommand::Wizard => {
+ unimplemented!();
+ }
+ CompanionCommand::Account(cmd) => {
+ let user_file = config.users.user_list;
+ account_management(&args.command, cmd, user_file)?;
+ }
+ },
+ (Command::Provider(subcommand), AnyConfig::Provider(config)) => match subcommand {
+ ProviderCommand::Daemon => {
+ let server = Server::from_provider_config(config).await?;
+ server.run().await?;
+ }
+ ProviderCommand::Reload { pid } => reload(*pid, config.pid)?,
+ ProviderCommand::Account(cmd) => {
+ let user_file = match config.users {
+ UserManagement::Static(conf) => conf.user_list,
+ _ => {
+ panic!("Only static account management is supported from Aerogramme.")
+ }
+ };
+ account_management(&args.command, cmd, user_file)?;
+ }
+ },
+ (Command::Provider(_), AnyConfig::Companion(_)) => {
+ bail!("Your want to run a 'Provider' command but your configuration file has role 'Companion'.");
+ }
+ (Command::Companion(_), AnyConfig::Provider(_)) => {
+ bail!("Your want to run a 'Companion' command but your configuration file has role 'Provider'.");
+ }
+ (Command::Tools(subcommand), _) => match subcommand {
+ ToolsCommand::PasswordHash { maybe_password } => {
+ let password = match maybe_password {
+ Some(pwd) => pwd.clone(),
+ None => rpassword::prompt_password("Enter password: ")?,
+ };
+ println!("{}", hash_password(&password)?);
+ }
+ ToolsCommand::CryptoRoot(crcommand) => match crcommand {
+ CryptoRootCommand::New { maybe_password } => {
+ let password = match maybe_password {
+ Some(pwd) => pwd.clone(),
+ None => {
+ let password = rpassword::prompt_password("Enter password: ")?;
+ let password_confirm =
+ rpassword::prompt_password("Confirm password: ")?;
+ if password != password_confirm {
+ bail!("Passwords don't match.");
+ }
+ password
+ }
+ };
+ let crypto_keys = CryptoKeys::init();
+ let cr = CryptoRoot::create_pass(&password, &crypto_keys)?;
+ println!("{}", cr.0);
+ }
+ CryptoRootCommand::NewClearText => {
+ let crypto_keys = CryptoKeys::init();
+ let cr = CryptoRoot::create_cleartext(&crypto_keys);
+ println!("{}", cr.0);
+ }
+ CryptoRootCommand::ChangePassword {
+ maybe_old_password,
+ maybe_new_password,
+ crypto_root,
+ } => {
+ let old_password = match maybe_old_password {
+ Some(pwd) => pwd.to_string(),
+ None => rpassword::prompt_password("Enter old password: ")?,
+ };
+
+ let new_password = match maybe_new_password {
+ Some(pwd) => pwd.to_string(),
+ None => {
+ let password = rpassword::prompt_password("Enter new password: ")?;
+ let password_confirm =
+ rpassword::prompt_password("Confirm new password: ")?;
+ if password != password_confirm {
+ bail!("Passwords don't match.");
+ }
+ password
+ }
+ };
+
+ let keys = CryptoRoot(crypto_root.to_string()).crypto_keys(&old_password)?;
+ let cr = CryptoRoot::create_pass(&new_password, &keys)?;
+ println!("{}", cr.0);
+ }
+ CryptoRootCommand::DeriveIncoming { crypto_root } => {
+ let pubkey = CryptoRoot(crypto_root.to_string()).public_key()?;
+ let cr = CryptoRoot::create_incoming(&pubkey);
+ println!("{}", cr.0);
+ }
+ },
+ },
+ }
+
+ Ok(())
+}
+
+fn reload(pid: Option<i32>, pid_path: Option<PathBuf>) -> Result<()> {
+ let final_pid = match (pid, pid_path) {
+ (Some(pid), _) => pid,
+ (_, Some(path)) => {
+ let mut f = std::fs::OpenOptions::new().read(true).open(path)?;
+ let mut pidstr = String::new();
+ f.read_to_string(&mut pidstr)?;
+ pidstr.parse::<i32>()?
+ }
+ _ => bail!("Unable to infer your daemon's PID"),
+ };
+ let pid = Pid::from_raw(final_pid);
+ signal::kill(pid, signal::Signal::SIGUSR1)?;
+ Ok(())
+}
+
+fn account_management(root: &Command, cmd: &AccountManagement, users: PathBuf) -> Result<()> {
+ let mut ulist: UserList =
+ read_config(users.clone()).context(format!("'{:?}' must be a user database", users))?;
+
+ match cmd {
+ AccountManagement::Add { login, setup } => {
+ tracing::debug!(user = login, "will-create");
+ let stp: SetupEntry = read_config(setup.clone())
+ .context(format!("'{:?}' must be a setup file", setup))?;
+ tracing::debug!(user = login, "loaded setup entry");
+
+ let password = match stp.clear_password {
+ Some(pwd) => pwd,
+ None => {
+ let password = rpassword::prompt_password("Enter password: ")?;
+ let password_confirm = rpassword::prompt_password("Confirm password: ")?;
+ if password != password_confirm {
+ bail!("Passwords don't match.");
+ }
+ password
+ }
+ };
+
+ let crypto_keys = CryptoKeys::init();
+ let crypto_root = match root {
+ Command::Provider(_) => CryptoRoot::create_pass(&password, &crypto_keys)?,
+ Command::Companion(_) => CryptoRoot::create_cleartext(&crypto_keys),
+ _ => unreachable!(),
+ };
+
+ let hash = hash_password(password.as_str()).context("unable to hash password")?;
+
+ ulist.insert(
+ login.clone(),
+ UserEntry {
+ email_addresses: stp.email_addresses,
+ password: hash,
+ crypto_root: crypto_root.0,
+ storage: stp.storage,
+ },
+ );
+
+ write_config(users.clone(), &ulist)?;
+ }
+ AccountManagement::Delete { login } => {
+ tracing::debug!(user = login, "will-delete");
+ ulist.remove(login);
+ write_config(users.clone(), &ulist)?;
+ }
+ AccountManagement::ChangePassword {
+ maybe_old_password,
+ maybe_new_password,
+ login,
+ } => {
+ let mut user = ulist.remove(login).context("user must exist first")?;
+
+ let old_password = match maybe_old_password {
+ Some(pwd) => pwd.to_string(),
+ None => rpassword::prompt_password("Enter old password: ")?,
+ };
+
+ if !verify_password(&old_password, &user.password)? {
+ bail!(format!("invalid password for login {}", login));
+ }
+
+ let crypto_keys = CryptoRoot(user.crypto_root).crypto_keys(&old_password)?;
+
+ let new_password = match maybe_new_password {
+ Some(pwd) => pwd.to_string(),
+ None => {
+ let password = rpassword::prompt_password("Enter new password: ")?;
+ let password_confirm = rpassword::prompt_password("Confirm new password: ")?;
+ if password != password_confirm {
+ bail!("Passwords don't match.");
+ }
+ password
+ }
+ };
+ let new_hash = hash_password(&new_password)?;
+ let new_crypto_root = CryptoRoot::create_pass(&new_password, &crypto_keys)?;
+
+ user.password = new_hash;
+ user.crypto_root = new_crypto_root.0;
+
+ ulist.insert(login.clone(), user);
+ write_config(users.clone(), &ulist)?;
+ }
+ };
+
+ Ok(())
+}
diff --git a/aerogramme/src/server.rs b/aerogramme/src/server.rs
new file mode 100644
index 0000000..09e91ad
--- /dev/null
+++ b/aerogramme/src/server.rs
@@ -0,0 +1,147 @@
+use std::io::Write;
+use std::path::PathBuf;
+use std::sync::Arc;
+
+use anyhow::Result;
+use futures::try_join;
+use log::*;
+use tokio::sync::watch;
+
+use crate::auth;
+use crate::config::*;
+use crate::dav;
+use crate::imap;
+use crate::lmtp::*;
+use crate::login::ArcLoginProvider;
+use crate::login::{demo_provider::*, ldap_provider::*, static_provider::*};
+
+pub struct Server {
+ lmtp_server: Option<Arc<LmtpServer>>,
+ imap_unsecure_server: Option<imap::Server>,
+ imap_server: Option<imap::Server>,
+ auth_server: Option<auth::AuthServer>,
+ dav_unsecure_server: Option<dav::Server>,
+ pid_file: Option<PathBuf>,
+}
+
+impl Server {
+ pub async fn from_companion_config(config: CompanionConfig) -> Result<Self> {
+ tracing::info!("Init as companion");
+ let login = Arc::new(StaticLoginProvider::new(config.users).await?);
+
+ let lmtp_server = None;
+ let imap_unsecure_server = Some(imap::new_unsecure(config.imap, login.clone()));
+ Ok(Self {
+ lmtp_server,
+ imap_unsecure_server,
+ imap_server: None,
+ auth_server: None,
+ dav_unsecure_server: None,
+ pid_file: config.pid,
+ })
+ }
+
+ pub async fn from_provider_config(config: ProviderConfig) -> Result<Self> {
+ tracing::info!("Init as provider");
+ let login: ArcLoginProvider = match config.users {
+ UserManagement::Demo => Arc::new(DemoLoginProvider::new()),
+ UserManagement::Static(x) => Arc::new(StaticLoginProvider::new(x).await?),
+ UserManagement::Ldap(x) => Arc::new(LdapLoginProvider::new(x)?),
+ };
+
+ let lmtp_server = config.lmtp.map(|lmtp| LmtpServer::new(lmtp, login.clone()));
+ let imap_unsecure_server = config
+ .imap_unsecure
+ .map(|imap| imap::new_unsecure(imap, login.clone()));
+ let imap_server = config
+ .imap
+ .map(|imap| imap::new(imap, login.clone()))
+ .transpose()?;
+ let auth_server = config
+ .auth
+ .map(|auth| auth::AuthServer::new(auth, login.clone()));
+ let dav_unsecure_server = config
+ .dav_unsecure
+ .map(|dav_config| dav::new_unsecure(dav_config, login.clone()));
+
+ Ok(Self {
+ lmtp_server,
+ imap_unsecure_server,
+ imap_server,
+ dav_unsecure_server,
+ auth_server,
+ pid_file: config.pid,
+ })
+ }
+
+ pub async fn run(self) -> Result<()> {
+ let pid = std::process::id();
+ tracing::info!(pid = pid, "Starting main loops");
+
+ // write the pid file
+ if let Some(pid_file) = self.pid_file {
+ let mut file = std::fs::OpenOptions::new()
+ .write(true)
+ .create(true)
+ .truncate(true)
+ .open(pid_file)?;
+ file.write_all(pid.to_string().as_bytes())?;
+ drop(file);
+ }
+
+ let (exit_signal, provoke_exit) = watch_ctrl_c();
+ let _exit_on_err = move |err: anyhow::Error| {
+ error!("Error: {}", err);
+ let _ = provoke_exit.send(true);
+ };
+
+ try_join!(
+ async {
+ match self.lmtp_server.as_ref() {
+ None => Ok(()),
+ Some(s) => s.run(exit_signal.clone()).await,
+ }
+ },
+ async {
+ match self.imap_unsecure_server {
+ None => Ok(()),
+ Some(s) => s.run(exit_signal.clone()).await,
+ }
+ },
+ async {
+ match self.imap_server {
+ None => Ok(()),
+ Some(s) => s.run(exit_signal.clone()).await,
+ }
+ },
+ async {
+ match self.auth_server {
+ None => Ok(()),
+ Some(a) => a.run(exit_signal.clone()).await,
+ }
+ },
+ async {
+ match self.dav_unsecure_server {
+ None => Ok(()),
+ Some(s) => s.run(exit_signal.clone()).await,
+ }
+ }
+ )?;
+
+ Ok(())
+ }
+}
+
+pub fn watch_ctrl_c() -> (watch::Receiver<bool>, Arc<watch::Sender<bool>>) {
+ let (send_cancel, watch_cancel) = watch::channel(false);
+ let send_cancel = Arc::new(send_cancel);
+ let send_cancel_2 = send_cancel.clone();
+ tokio::spawn(async move {
+ tokio::signal::ctrl_c()
+ .await
+ .expect("failed to install CTRL+C signal handler");
+ info!("Received CTRL+C, shutting down.");
+ send_cancel.send(true).unwrap();
+ });
+ (watch_cancel, send_cancel_2)
+}