Commit d7794aec authored by MrMan's avatar MrMan

Progress recfactoring dovecot into component

parent 0dfcbfa4
pub mod sqlite;
use components::Error;
use config::{DBCfg, DBType};
use config::{DBCfg, DBType, DovecotDBSettings};
use models::user::MailboxUser;
use models::{ModelWithUUID, PaginatedList, PaginationOptions};
use self::sqlite::SQLiteDB;
use std::path::Path;
use std::any::Any;
use rusqlite::Connection as SQLiteConnection;
pub enum DB {
......@@ -77,7 +76,7 @@ pub fn make_admin_db(db_cfg: DBCfg) -> Result<Box<AdminDB>, Error> {
pub trait MailboxDB
where
Self: Connectable + SupportsVAliasLookup + SupportsVMailboxLookup + SupportsDovecotAuth
Self: Connectable + SupportsVAliasLookup + SupportsVMailboxLookup
{
// Initialize the database connection (if necessary), idempotently performing
// all early/pre-work data changes and migrations necessary for a Postfix data holding db
......@@ -204,55 +203,15 @@ impl SupportsVMailboxLookup for DB {
}
// TODO: this can probably be generalized in the future to SupportsAuth<T> when Dovecot auth is added
pub trait SupportsDovecotAuth {
type ConfigTemplate;
type SQLConfigTemplate;
fn dovecot_sasl_path(&self) -> Result<&'static str, Error>;
fn dovecot_config_dir(&self) -> Result<&'static str, Error>;
fn dovecot_config_filename(&self) -> Result<&'static str, Error>;
fn dovecot_sql_config_filename(&self) -> Result<&'static str, Error>;
fn write_dovecot_config_files(&self) -> Result<(), Error>;
pub trait SupportsDovecotAuth where Self: MailboxDB {
/// Generate/Retrieve settings for dovecot to use to connecto the DB
fn dovecot_db_settings(&self) -> Result<DovecotDBSettings, Error>;
}
impl SupportsDovecotAuth for DB {
type ConfigTemplate = Box<Any>;
type SQLConfigTemplate = Box<Any>;
fn dovecot_sasl_path(&self) -> Result<&'static str, Error> {
match self {
DB::SQLite(db) => db.dovecot_sasl_path(),
_ => Err(Error::NotSupported)
}
}
fn dovecot_config_dir(&self) -> Result<&'static str, Error> {
match self {
DB::SQLite(db) => db.dovecot_config_dir(),
_ => Err(Error::NotSupported)
}
}
fn dovecot_config_filename(&self) -> Result<&'static str, Error> {
match self {
DB::SQLite(db) => db.dovecot_config_filename(),
_ => Err(Error::NotSupported)
}
}
fn dovecot_sql_config_filename(&self) -> Result<&'static str, Error> {
match self {
DB::SQLite(db) => db.dovecot_config_filename(),
_ => Err(Error::NotSupported)
}
}
fn write_dovecot_config_files(&self) -> Result<(), Error> {
fn dovecot_db_settings(&self) -> Result<DovecotDBSettings, Error> {
match self {
DB::SQLite(db) => db.write_dovecot_config_files(),
DB::SQLite(db) => db.dovecot_db_settings(),
_ => Err(Error::NotSupported)
}
}
......
......@@ -14,7 +14,7 @@ use self::schema::{ADMIN_MIGRATIONS, MAILBOX_MIGRATIONS};
use std::ffi::OsStr;
use std::fs::{create_dir_all, File};
use std::io::Write;
use std::path::{Path, PathBuf};
use std::path::Path;
use make_absolute_path_from_str;
pub struct SQLiteDB {
......@@ -278,85 +278,10 @@ impl SupportsVMailboxLookup for SQLiteDB {
}
}
#[derive(Template)]
#[template(path = "config/dovecot/dovecot.conf")]
pub struct DovecotConfTemplate {
filename: String,
generation_time: String,
sql_config_file_abs_path: String,
abs_db_path: String
}
#[derive(Template)]
#[template(path = "config/dovecot/dovecot-sql.conf.ext")]
pub struct DovecotSQLConfTemplate {
filename: String,
generation_time: String,
abs_db_path: String
}
impl SupportsDovecotAuth for SQLiteDB {
type ConfigTemplate = DovecotConfTemplate;
type SQLConfigTemplate = DovecotSQLConfTemplate;
fn dovecot_sasl_path(&self) -> Result<&'static str, Error> { Ok("private/auth") }
fn dovecot_config_dir(&self) -> Result<&'static str, Error> { Ok("/etc/dovecot") }
fn dovecot_config_filename(&self) -> Result<&'static str, Error> { Ok("dovecot.conf") }
fn dovecot_sql_config_filename(&self) -> Result<&'static str, Error> { Ok("dovecot-sql.conf.ext") }
fn write_dovecot_config_files(&self) -> Result<(), Error> {
// Create the dovecot config directory if it doesn't exist
let config_dir = PathBuf::from(self.dovecot_config_dir()?);
if !config_dir.exists() { create_dir_all(&config_dir)?; }
let config_filename = self.dovecot_config_filename()?;
let sql_config_filename = self.dovecot_sql_config_filename()?;
// Build path & string to represent dovecot config file path
let config_file_abs_path = PathBuf::from(&config_dir).join(config_filename);
let config_file_abs_path_str = config_file_abs_path
.to_str()
.map(String::from)
.ok_or(Error::InvalidEnvironment("invalid dovecot config file path"))?;
// Build path & string to represent dovecto SQL config file path
let sql_config_file_abs_path = PathBuf::from(&config_dir).join(sql_config_filename);
let sql_config_file_abs_path_str = sql_config_file_abs_path
.to_str()
.map(String::from)
.ok_or(Error::InvalidEnvironment("invalid dovecot sql config file path"))?;
if !config_file_abs_path.is_absolute() { warn!("dovecot config file path is not absolute"); }
if !sql_config_file_abs_path.is_absolute() { warn!("dovecot sql config file path is not absolute"); }
// Build the template for the main config file
let template = DovecotConfTemplate {
filename: config_filename.to_string(),
generation_time: Local::now().to_string(),
sql_config_file_abs_path: sql_config_file_abs_path_str.clone(),
abs_db_path: self.make_absolute_db_path()?
};
// Write the main config file to disk
debug!("writing dovecot config file @ [{}]", config_file_abs_path_str);
let mut config_file = File::create(config_file_abs_path)?;
config_file.write_all(template.render()?.as_bytes())?;
// Build the template for the SQLite config file
let template = DovecotSQLConfTemplate {
filename: config_filename.to_string(),
generation_time: Local::now().to_string(),
abs_db_path: self.make_absolute_db_path()?
};
// Write the main config file to disk
debug!("writing dovecot sql config file @ [{}]", sql_config_file_abs_path_str);
let mut config_file = File::create(sql_config_file_abs_path)?;
config_file.write_all(template.render()?.as_bytes())?;
Ok(())
fn dovecot_db_settings(&self) -> Result<DovecotDBSettings, Error> {
// TODO: generate the DB settings for dovecot based on this backend
Err(Error::NotSupported)
}
}
......
use std::fs::{create_dir_all, File, copy};
use std::io::Write;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio, Child as ChildProcess};
use std::sync::mpsc::{sync_channel, SyncSender, Receiver};
use askama::Template;
use chrono::prelude::Local;
use make_absolute_path;
use simple_signal::{Signal};
use simple_signal;
use components::db::*;
use components::*;
use config::{DovecotCfg, DovecotDBSettings};
const DOVECOT_CONF_FILENAME: &'static str = "dovecot.conf";
const DOVECOT_SQL_CONF_FILENAME: &'static str = "dovecot-sql.conf.ext";
#[derive(Clone, Debug, Deserialize, Serialize)]
pub enum DovecotCmd {
Lifecycle(ComponentCmd),
}
pub struct Dovecot {
cfg: DovecotCfg,
cmd_bus_rx: Receiver<ReqResp<DovecotCmd>>,
cmd_bus_tx: SyncSender<ReqResp<DovecotCmd>>,
pid: Option<u32>,
command: Option<Command>,
process: Option<ChildProcess>,
}
impl Dovecot {
pub fn new(cfg: DovecotCfg, db: Option<DB>) -> Result<Dovecot, Error> {
// Create command bus channels
let (cmd_bus_tx, cmd_bus_rx) = sync_channel(0);
Ok(Dovecot {
cfg,
cmd_bus_tx,
cmd_bus_rx,
pid: None,
command: None,
process: None,
})
}
}
impl Component for Dovecot {
fn get_name(&self) -> &str { "Dovecot" }
fn start(&mut self) -> Result<(), Error> {
if let Some(_) = self.process { return Err(Error::AlreadyRunningError); }
self.spawn_child_process()?;
// Control loop, this never exits
for req in self.cmd_bus_rx.iter() {
match req.msg {
DovecotCmd::Lifecycle(ComponentCmd::Shutdown) => {
debug!("received shutdown command to component");
break;
},
}
}
// Stop the child process since we've left the control loop
self.stop()?;
Ok(())
}
fn stop(&mut self) -> Result<(), Error> {
if self.process.is_none() { return Err(Error::NotRunningError); }
// assuming that if a process is present a PID is present
let pid = self.pid.unwrap();
debug!("killing postfix process (PID {})...", &pid);
if let Some(ref mut p) = self.process {
// TODO: Figure out a better way to do this that doesn't use the binary directly
// would be nice to have multiple postmgr instances running at one time without
// manpages say sending SIGTERM would do but rust can't handle signals yet...
// https://github.com/rust-lang/rfcs/issues/1368
let stop_output = Command::new(&self.cfg.doveadm_bin_path)
.arg("stop")
.output()
.expect("failed to run `doveadm stop`");
if !stop_output.status.success() {
debug!("`doveadm stop` failed, sending SIGTERM and killing the process from rust");
Command::new("kill")
.arg("-TERM")
.arg(&pid.to_string())
.output()
.expect("failed to send kill signal to postfix");
p.kill()?;
}
// Wait for the process to exit completely
p.wait()?;
}
return Ok(());
}
}
impl HasReqRespCommandBus for Dovecot {
type Msg = DovecotCmd;
fn get_cmd_bus_tx(&self) -> Result<SyncSender<ReqResp<Self::Msg>>, Error> {
Ok(self.cmd_bus_tx.clone())
}
}
impl ChildProcessManager for Dovecot {
fn spawn_child_process(&mut self) -> Result<&Option<ChildProcess>, Error> {
debug!("spawning postfix process...");
// Build arguments to start dovecot in *non* daemonized mode so we can control it
let config_output_dir = make_absolute_path(PathBuf::from(&self.cfg.config_output_dir))?;
let args = &["-F"];
// Build & save command
let mut cmd = Command::new(&self.cfg.bin_path);
cmd.args(args);
cmd.stdin(Stdio::null());
cmd.stdout(Stdio::inherit());
debug!("command: [{:#?}]!", cmd);
self.command = Some(cmd);
// Actually run the command
// NOTE: unfortunately we have to build it all over again since Command can't be cloned
let child = Command::new(&self.cfg.bin_path)
.args(args)
.stdin(Stdio::null())
//.stdout(Stdio::inherit())
.spawn()?;
self.pid = Some(child.id());
self.process = Some(child);
// Setup signal handlers with a channel
let dovecot_tx = self.get_cmd_bus_tx()?;
simple_signal::set_handler(&[Signal::Int, Signal::Term], move |_signals| {
info!("Received SIGINT/SIGTERM, forwarding to all components");
dovecot_tx.send(ReqResp{
msg: DovecotCmd::Lifecycle(ComponentCmd::Shutdown),
response_chan: None
}).expect("failed to send shutdown command to dovecot component");
});
Ok(&self.process)
}
fn get_pid(&self) -> Option<u32> { self.pid }
fn get_command(&self) -> &Option<Command> { &self.command }
fn get_process(&self) -> &Option<ChildProcess> { &self.process }
}
impl Configurable<DovecotCfg> for Dovecot {
fn update_config(&mut self, _cfg: Option<DovecotCfg>) -> Result<(), Error> {
info!("updating dovecot configuration...");
// Generate & install configuration files
self.generate_config_in_dir(None)?;
self.install_config()?;
// Run `doveadm reload`
let output = Command::new(&self.cfg.doveadm_bin_path)
.args(&["reload"])
.output()?;
Ok(())
}
fn get_config(&self) -> &DovecotCfg { &self.cfg }
}
#[derive(Template)]
#[template(path = "config/dovecot/dovecot.conf.jinja")]
struct DovecotConfTemplate<'a> {
filename: &'a str,
generation_time: String,
mail_location: &'a String,
unix_socket_path: &'a String,
userdb: &'a Option<DovecotDBSettings>,
passdb: &'a Option<DovecotDBSettings>,
}
#[derive(Template)]
#[template(path = "config/dovecot/dovecot-sql-userdb.conf.ext.jinja")]
struct DovecotUserDBConfTemplate<'a> {
filename: &'a str,
generation_time: String,
userdb: &'a Option<DovecotDBSettings>,
}
#[derive(Template)]
#[template(path = "config/dovecot/dovecot-sql-passdb.conf.ext.jinja")]
struct DovecotPassDBConfTemplate<'a> {
filename: &'a str,
generation_time: String,
passdb: &'a Option<DovecotDBSettings>,
}
impl FileConfigurable<DovecotCfg> for Dovecot {
fn generate_config_in_dir(&self, dir: Option<String>) -> Result<(), Error> {
info!("updating on-disk configuration for dovecot");
// Ensure configuration directory exists
let config_dir_abs_path = ensure_directory(&self.cfg.config_output_dir);
// Build dovecot conf template
let main_conf_tmpl = DovecotConfTemplate {
filename: DOVECOT_CONF_FILENAME,
generation_time: Local::now().to_string(),
mail_location: &self.cfg.mail_location,
unix_socket_path: &self.cfg.unix_socket_path,
userdb: &self.cfg.userdb,
passdb: &self.cfg.passdb,
};
// Build userdb SQL configuration template
let userdb_conf_tmpl = DovecotUserDBConfTemplate {
filename: DOVECOT_USERDB_CONF_FILENAME,
generation_time: Local::now().to_string(),
userdb: &self.cfg.userdb,
};
// Build passdb SQL configuration template
let passdb_conf_tmpl = DovecotPassDBConfTemplate {
filename: DOVECOT_PASSDB_CONF_FILENAME,
generation_time: Local::now().to_string(),
passdb: &self.cfg.passdb,
};
// Generate file paths for the dovecot.conf file
let output_dir = Path::new(&self.cfg.config_output_dir);
let paths_and_tmpl: Vec<(Path, Template)> = vec!([
(output_dir.join(DOVECOT_CONF_FILENAME), main_conf_tmpl),
(output_dir.join(DOVECOT_USERDB_CONF_FILENAME), userdb_conf_tmpl),
(output_dir.join(DOVECOT_PASSDB_CONF_FILENAME), passdb_conf_tmpl),
]);
// let dovecot_conf_path = Path::new(&self.cfg.config_output_dir).join(DOVECOT_CONF_FILENAME);
// let dovecot_conf_file_path = make_absolute_path(dovecot_conf_path.to_path_buf())?;
// // Generate file paths for the userdb conf
// let dovecot_sql_conf_path = Path::new(&self.cfg.config_output_dir).join(DOVECOT_SQL_CONF_FILENAME);
// let dovecot_sql_conf_file_path = make_absolute_path(dovecot_sql_conf_path.to_path_buf())?;
// // Generate paths for configuration files
// let dovecot_conf_path = config_dir_abs_path.join(dovecot_conf.filename);
// let dovecot_sql_conf_path = config_dir_abs_path.join(dovecot_sql_conf.filename);
// // Render the file contents
// debug!("{} will be written to [{:?}]", &dovecot_conf.filename, dovecot_conf_path.to_str());
// debug!("{} will be written to [{:?}]", &dovecot_sql_conf.filename, dovecot_sql_conf_path.to_str());
// let mut dovecot_conf_file = File::create(dovecot_conf_path)?;
// let mut dovecot_sql_conf_file = File::create(dovecot_sql_conf_path)?;
// dovecot_conf_file.write_all(dovecot_conf.render()?.as_bytes())?;
// dovecot_sql_conf_file.write_all(dovecot_sql_conf.render()?.as_bytes())?;
debug!("successfully wrote dovecot configuration files to disk");
Ok(())
}
fn install_config(&self) -> Result<(), Error> {
// Generate the directories the files are going to go into
let config_dir_path = Path::new(&self.cfg.config_dir);
let config_output_dir_path = Path::new(&self.cfg.config_output_dir);
debug!(
"installing config file(s) from directory [{:?}] to [{:?}]",
config_dir_path.to_str(),
config_output_dir_path.to_str(),
);
// Ensure config output path exists
debug!("ensuring configuration output path [{:?}] exists...", config_output_dir_path);
if !config_output_dir_path.exists() { create_dir_all(&config_output_dir_path)?; }
// Copy dovecot.conf
let dovecot_conf_from = config_dir_path.clone().join(DOVECOT_CONF_FILENAME);
let dovecot_conf_to = config_output_dir_path.clone().join(DOVECOT_CONF_FILENAME);
debug!("copying dovecot.conf from [{:?}] to [{:?}]", dovecot_conf_from, dovecot_conf_to);
copy(&dovecot_conf_from, &dovecot_conf_to)?;
// Copy dovecot-sql.conf.ext
let dovecot_sql_conf_from = config_dir_path.clone().join(DOVECOT_SQL_CONF_FILENAME);
let dovecot_sql_conf_to = config_output_dir_path.clone().join(DOVECOT_SQL_CONF_FILENAME);
debug!("copying dovecot-sql.conf.ext from [{:?}] to [{:?}]", dovecot_sql_conf_from, dovecot_sql_conf_to);
copy(&dovecot_sql_conf_from, &dovecot_sql_conf_to)?;
Ok(())
}
fn config_dir_path(&self) -> String { self.cfg.config_output_dir.clone() }
}
pub mod postfix;
pub mod dovecot;
pub mod web_admin;
pub mod db;
......
......@@ -7,7 +7,6 @@ use std::sync::mpsc::{sync_channel, SyncSender, Receiver};
use askama::Template;
use chrono::prelude::Local;
use make_absolute_path;
use make_absolute_path_from_str;
use simple_signal::{Signal};
use simple_signal;
......@@ -121,7 +120,7 @@ impl Component for Postfix {
.expect("failed to run postfix stop");
if !stop_output.status.success() {
debug!("`postfi stop` failed, sending SIGTERM and killing the process from rust-side");
debug!("`postfi stop` failed, sending SIGTERM and killing the process from rust");
Command::new("kill")
.arg("-TERM")
.arg(&pid.to_string())
......@@ -253,7 +252,8 @@ struct PostfixMainCFTemplate<'a> {
valias_lookup_cfg_file_path: String,
vmailbox_lookup_cfg_file_path: String,
sasl_cfg_file_path: String,
smtpd_sasl_type: String,
smtpd_sasl_path: String,
db: &'a DBCfg
}
......@@ -277,9 +277,6 @@ impl FileConfigurable<PostfixCfg> for Postfix {
let vmailbox_lookup_cfg_path = Path::new(&self.cfg.config_dir).join("vmailbox_lookup.cf");
let vmailbox_lookup_cfg_file_path = make_absolute_path(vmailbox_lookup_cfg_path.to_path_buf())?;
// Get the postfix config dir absolute path
let sasl_cfg_file_path = String::from(self.db.dovecot_sasl_path()?);
let main_cf = PostfixMainCFTemplate {
filename: "main.cf",
generation_time: Local::now().to_string(),
......@@ -315,7 +312,10 @@ impl FileConfigurable<PostfixCfg> for Postfix {
valias_lookup_cfg_file_path,
vmailbox_lookup_cfg_file_path,
sasl_cfg_file_path,
// FIXME: should be generated/determined based on postfix cfg
// Another trait? Box up these config options into some sort of SASLCfg?
smtpd_sasl_type: String::from("dovecot"),
smtpd_sasl_path: String::from("private/auth"),
db: &self.cfg.db
};
......@@ -331,9 +331,6 @@ impl FileConfigurable<PostfixCfg> for Postfix {
self.db.write_valias_lookup_config_file(valias_lookup_cfg_path.as_path())?;
self.db.write_vmailbox_lookup_config_file(vmailbox_lookup_cfg_path.as_path(), &self.cfg.mail_spool_directory)?;
// Generate DB-specific configuration for Dovecot
self.db.write_dovecot_config_files()?;
// Generate the directories the files are going to go into
let config_dir = dir.unwrap_or(self.config_dir_path());
let config_dir_path = Path::new(&config_dir);
......
use actix_web::error::*;
use actix_web::http::Method;
use actix_web::{App, HttpRequest, HttpResponse, Json, Result, Path, FromRequest, State};
use actix_web::{App, HttpRequest, HttpResponse, Json, Result, Path, State};
use components::ReqResp;
use components::postfix::PostfixCmd;
use components::dovecot::DovecotCmd;
use components::web_admin::AppState;
use models::user::MailboxUser;
use models::ModelWithUUID;
......@@ -13,10 +14,12 @@ use serde_json::json;
const DEFAULT_OPERATION_TIMEOUT: Duration = Duration::from_secs(5);
pub fn app(
postfix_tx: SyncSender<ReqResp<PostfixCmd>>
postfix_tx: SyncSender<ReqResp<PostfixCmd>>,
dovecot_tx: SyncSender<ReqResp<DovecotCmd>>,
) -> App<AppState> {
let state = AppState {
postfix_tx: Some(postfix_tx)
postfix_tx: Some(postfix_tx),
dovecot_tx: Some(dovecot_tx),
};
App::with_state(state)
......
......@@ -3,6 +3,7 @@ mod root;
use actix_web::server;
use components::postfix::PostfixCmd;
use components::dovecot::DovecotCmd;
use components::web_admin::api::v1::{app as api_v1_app};
use components::web_admin::root::{app as root_app};
use components::{Component, ReqResp, ComponentCmd, Error};
......@@ -13,8 +14,11 @@ use std::sync::mpsc::SyncSender;
pub struct WebAdmin {
cfg: WebAdminCfg,
// For communicating with a postfix component
/// For communicating with a postfix component
postfix_tx_chan: SyncSender<ReqResp<PostfixCmd>>,
/// For communicating with a dovecot component
dovecot_tx_chan: SyncSender<ReqResp<DovecotCmd>>,
}