Commit 90211a3b authored by MrMan's avatar MrMan

Merge branch '34-add-cyrus-sasl-support' into 'master'

Resolve "Add Cyrus SASL support"

Closes #34

See merge request !18
parents b5b53c11 ceb0336b
Pipeline #47776648 passed with stage
in 3 minutes and 20 seconds
This diff is collapsed.
.PHONY: build clean clean-runtime run test watch \
check-tool-cargo check-tool-cargo-watch \
diesel-cli \
builder-image builder-publish image publish registry-login get-version \
container container-shell
builder-image builder-image-publish image publish registry-login get-version \
container container-shell test-container
all: build
......@@ -14,12 +14,16 @@ VERSION=$(shell awk '/version\s+=\s+\"([0-9|\.]+)\"$$/{print $$3}' Cargo.toml)
CARGO := $(shell command -v cargo 2> /dev/null)
CARGO_WATCH := $(shell command -v cargo-watch 2> /dev/null)
ENV=development # | production
IMAGE_NAME:=postmgr
BUILDER_IMAGE_NAME:=postmgr/builder
REGISTRY_PATH=registry.gitlab.com/postmgr
FQ_IMAGE_NAME=$(REGISTRY_PATH)/$(IMAGE_NAME):$(VERSION)
FQ_BUILDER_IMAGE_NAME=$(REGISTRY_PATH)/$(BUILDER_IMAGE_NAME):$(VERSION)
CONTAINER_NAME=postmgr
check-tool-cargo:
ifndef CARGO
$(error "`cargo` is not available please install cargo (https://github.com/rust-lang/cargo/)")
......@@ -69,20 +73,25 @@ registry-login:
publish: registry-login
docker push $(FQ_IMAGE_NAME)
builder-publish: registry-login
builder-image-publish: registry-login
docker push $(FQ_BUILDER_IMAGE_NAME)
container:
docker stop $(CONTAINER_NAME) || true
docker rm $(CONTAINER_NAME) || true
docker run \
-p 2525:25 \
-p 5875:587 \
-e CONFIG_PATH=/usr/src/postmgr/infra/conf/prod.toml \
-e CONFIG_PATH=/usr/src/postmgr/infra/conf/${ENV}.toml \
--name $(CONTAINER_NAME) \
$(FQ_IMAGE_NAME)
container-shell:
docker run -it \
-p 2525:25 \
-p 5875:587 \
-e CONFIG_PATH=/usr/src/postmgr/infra/conf/prod.toml \
-e CONFIG_PATH=/usr/src/postmgr/infra/conf/${ENV}.toml \
$(FQ_IMAGE_NAME) \
/bin/bash
test-container: image container
......@@ -14,6 +14,9 @@ data_dir = "./infra/runtime/data/postfix"
# All on-disk config is stored at or below this folder
config_dir = "./infra/runtime/config/postfix"
# All generated configuration will be copied here
config_output_dir = "./infra/runtime/config/postfix/generated"
# The server's internet hostname (in Postfix, `myhostname`)
internet_hostname = "mail.localhost.localdomain"
......@@ -63,6 +66,9 @@ submission_qmgr_group_name = "submission"
postmaster_user = "root"
[postfix.auth]
backend = "Cyrus" # only "Cyrus" allowed
[postfix.db]
backend = "SQLite" # "SQLite" or "Postgres"
......@@ -78,13 +84,13 @@ path = "./infra/runtime/data/db.sqlite"
#schema = ""
[postfix.milters]
non_smtpd_milters = ["inet:127.0.0.1:8891"] # local mail milters
smtpd_milters = ["inet:127.0.0.1:8891"] # remote mail milters
non_smtpd_milters = [] # ex. ["inet:127.0.0.1:8891"] # local mail milters
smtpd_milters = [] # ex. ["inet:127.0.0.1:8891"] # remote mail milters
[postfix.smtp_tls]
enabled = false # false -> 'may', true -> 'encrypt'
ca_file_path = "/etc/ssl/ca.pem"
session_cache_db = "btree:/var/lib/postfix/smtp_tls_session_cache"
session_cache_db = "btree:/var/lib/postfix/smtp_tls_session_cache.db"
log_level = 1 # higher number = more logging
[postfix.smtpd_tls]
......@@ -93,8 +99,8 @@ ca_file_path = "/etc/ssl/ca.pem"
cert_file_path = "/etc/ssl/cert.pem"
key_file_path = "/etc/ssl/key.pem"
add_received_header = true
sasl_tls_only = true # do not accept SASL over unencrypted connections
session_cache_db = "btree:/var/lib/postfix/smtpd_tls_session_cache"
sasl_tls_only = false # do not accept SASL over unencrypted connections
session_cache_db = "btree:/var/lib/postfix/smtpd_tls_session_cache.db"
log_level = 1
session_cache_timeout_seconds = 3600
......
......@@ -14,6 +14,9 @@ data_dir = "./infra/runtime/data/postfix"
# All on-disk config is stored at or below this folder
config_dir = "./infra/runtime/config/postfix"
# All generated configuration will be copied here
config_output_dir = "/etc/postfix"
# The server's internet hostname (in Postfix, `myhostname`)
internet_hostname = "mail.localhost.localdomain"
......@@ -63,6 +66,9 @@ submission_qmgr_group_name = "submission"
postmaster_user = "root"
[postfix.auth]
backend = "Cyrus" # only "Cyrus" allowed
[postfix.db]
backend = "SQLite" # "SQLite" or "Postgres"
......@@ -84,7 +90,7 @@ smtpd_milters = ["inet:127.0.0.1:8891"] # remote mail milters
[postfix.smtp_tls]
enabled = false # false -> 'may', true -> 'encrypt'
ca_file_path = "/etc/ssl/ca.pem"
session_cache_db = "btree:/var/lib/postfix/smtp_tls_session_cache"
session_cache_db = "btree:/var/lib/postfix/smtp_tls_session_cache.db"
log_level = 1 # higher number = more logging
[postfix.smtpd_tls]
......@@ -93,8 +99,8 @@ ca_file_path = "/etc/ssl/ca.pem"
cert_file_path = "/etc/ssl/cert.pem"
key_file_path = "/etc/ssl/key.pem"
add_received_header = true
sasl_tls_only = true # do not accept SASL over unencrypted connections
session_cache_db = "btree:/var/lib/postfix/smtpd_tls_session_cache"
sasl_tls_only = false # do not accept SASL over unencrypted connections
session_cache_db = "btree:/var/lib/postfix/smtpd_tls_session_cache.db"
log_level = 1
session_cache_timeout_seconds = 3600
......
......@@ -3,9 +3,16 @@ FROM registry.gitlab.com/postmgr/postmgr/builder:0.0.1
WORKDIR /usr/src/postmgr
COPY . .
# Install rsyslog configuration for postfix
COPY infra/docker/syslog/postfix-syslog.conf /etc/rsyslog.d/postfix.conf
# Set up entrypoint
COPY infra/docker/entrypoint.sh /bin/entrypoint.sh
RUN chmod =x /bin/entrypoint.sh
RUN make clean-runtime
RUN cargo build --release
RUN cargo install --force
CMD ["postmgr", "server"]
ENTRYPOINT ["/bin/entrypoint.sh"]
FROM rust:1.26.2
FROM rust:1.27.1
WORKDIR /usr/src/postmgr
COPY . .
# Install postfix
RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get -y install postfix
RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get -y install postfix rsyslog telnet ca-certificates libsasl2-modules
RUN ln -s /usr/sbin/postfix /usr/bin/postfix
# Add submission group for postfix to use
RUN groupadd -g 3000 submission
RUN cargo install
#!/bin/bash
# Start rsyslogd for postfix
rsyslogd &
# Start postmgr
postmgr server
# Create an additional socket in postfix's chroot in order not to break
# mail logging when rsyslog is restarted. If the directory is missing,
# rsyslog will silently skip creating the socket.
$AddUnixListenSocket /var/spool/postfix/dev/log
# Postfix overrides
mail.* -/var/log/postfix.log
mail.info -/var/log/postfix.info.log
mail.warn -/var/log/postfix.warn.log
mail.err /var/log/postfix.err.log
......@@ -4,47 +4,53 @@ use components::Error;
use config::{DBCfg, DBType};
use models::user::MailboxUser;
use models::{ModelWithUUID, PaginatedList, PaginationOptions};
use rusqlite::Connection as SqliteConnection;
use self::sqlite::SQLiteDB;
use std::path::Path;
use std::any::Any;
use rusqlite::Connection as SQLiteConnection;
pub enum DB {
Sqlite(SQLiteDB),
Postgres
SQLite(SQLiteDB),
PostgreSQL
}
impl DB {
pub fn new(cfg: DBCfg) -> DB {
match cfg.backend {
pub fn new(db_cfg: DBCfg) -> Result<DB, Error> where {
match db_cfg.backend {
DBType::SQLite => {
let sqlite_cfg = cfg.sqlite.expect("Invalid/Missing SQLite DB configuration");
DB::Sqlite(SQLiteDB::new(sqlite_cfg))
},
DBType::PostgreSQL => DB::Postgres
let sqlite_cfg = db_cfg.sqlite.ok_or(Error::InvalidOrMissingConfig("sqlite db config missing"))?;
Ok(DB::SQLite(SQLiteDB::new(sqlite_cfg)))
}
_ => Err(Error::NotSupported)
}
}
}
pub trait Connectable<C> {
pub enum DBConnection {
SQLite(SQLiteConnection),
PostgreSQLConnection
}
pub trait Connectable {
// Connect to the relevant database
fn connect(&mut self) -> Result<(), Error>;
// Ensure the Retrieve the DB connection
fn connection(&self) -> Result<&C, Error>;
fn connection(&self) -> Result<&DBConnection, Error>;
}
impl Connectable<SqliteConnection> for DB {
impl Connectable for DB {
fn connect(&mut self) -> Result<(), Error> {
match self {
DB::Sqlite(db) => db.connect(),
_ => panic!("Unknown DB enum type"),
DB::SQLite(db) => db.connect(),
_ => Err(Error::NotSupported)
}
}
fn connection(&self) -> Result<&SqliteConnection, Error> {
fn connection(&self) -> Result<&DBConnection, Error> {
match self {
DB::Sqlite(db) => db.connection(),
_ => panic!("Unknown DB enum type"),
DB::SQLite(db) => db.connection(),
_ => Err(Error::NotSupported)
}
}
}
......@@ -59,7 +65,20 @@ pub trait Migratable<T> {
fn migrate_to_latest(&self, migrations: &[Migration]) -> Result<T, Error>;
}
pub trait MailboxDB {
pub fn make_admin_db(db_cfg: DBCfg) -> Result<Box<AdminDB>, Error> {
match db_cfg.backend {
DBType::SQLite => {
let sqlite_cfg = db_cfg.sqlite.ok_or(Error::InvalidOrMissingConfig("sqlite config for db not provided"))?;
Ok(Box::new(SQLiteDB::new(sqlite_cfg)))
},
_ => Err(Error::NotSupported)
}
}
pub trait MailboxDB
where
Self: Connectable + SupportsVAliasLookup + SupportsVMailboxLookup + SupportsCyrusAuth
{
// Initialize the database connection (if necessary), idempotently performing
// all early/pre-work data changes and migrations necessary for a Postfix data holding db
// Note that this *can* be the same DB as the Admin DB
......@@ -74,57 +93,48 @@ pub trait MailboxDB {
fn remove_mailbox_user_by_email(&self, emails: String) -> Result<(), Error>;
}
pub trait AdminDB {
// Initialize the database connection (if necessary), idempotently performing
// all early/pre-work data changes and migrations necessary for a Admin data holding db
// Note that this *can* be the same DB as the Postfix DB
fn init_admin_db(&self) -> Result<(), Error>;
}
impl MailboxDB for DB {
fn init_mailbox_db(&self) -> Result<(), Error> {
match self {
DB::Sqlite(db) => db.init_mailbox_db(),
_ => panic!("unsupported db enum type"),
DB::SQLite(db) => db.init_mailbox_db(),
_ => Err(Error::NotSupported)
}
}
fn add_mailbox_user(&self, u: MailboxUser) -> Result<ModelWithUUID<MailboxUser>, Error> {
match self {
DB::Sqlite(db) => db.add_mailbox_user(u),
_ => panic!("DB does not support adding mailbox users"),
DB::SQLite(db) => db.add_mailbox_user(u),
_ => Err(Error::NotSupported)
}
}
fn list_mailbox_users(&self, po: &PaginationOptions) -> Result<PaginatedList<ModelWithUUID<MailboxUser>>, Error> {
fn list_mailbox_users(&self, pagination: &PaginationOptions) -> Result<PaginatedList<ModelWithUUID<MailboxUser>>, Error> {
match self {
DB::Sqlite(db) => db.list_mailbox_users(po),
_ => panic!("DB does not support listing mailbox users"),
DB::SQLite(db) => db.list_mailbox_users(pagination),
_ => Err(Error::NotSupported)
}
}
fn get_mailbox_users_by_uuids(&self, uuids: &[String]) -> Result<Vec<ModelWithUUID<MailboxUser>>, Error> {
match self {
DB::Sqlite(db) => db.get_mailbox_users_by_uuids(uuids),
_ => panic!("DB does not support getting mailbox users"),
DB::SQLite(db) => db.get_mailbox_users_by_uuids(uuids),
_ => Err(Error::NotSupported)
}
}
fn remove_mailbox_user_by_email(&self, email: String) -> Result<(), Error> {
fn remove_mailbox_user_by_email(&self, emails: String) -> Result<(), Error> {
match self {
DB::Sqlite(db) => db.remove_mailbox_user_by_email(email),
_ => panic!("DB does not support removing mailbox user by email"),
DB::SQLite(db) => db.remove_mailbox_user_by_email(emails),
_ => Err(Error::NotSupported)
}
}
}
impl AdminDB for DB {
fn init_admin_db(&self) -> Result<(), Error> {
match self {
DB::Sqlite(db) => db.init_admin_db(),
_ => panic!("unsupported db enum type"),
}
}
pub trait AdminDB {
// Initialize the database connection (if necessary), idempotently performing
// all early/pre-work data changes and migrations necessary for a Admin data holding db
// Note that this *can* be the same DB as the Postfix DB
fn init_admin_db(&self) -> Result<(), Error>;
}
// Forward-only SQL migrations
......@@ -165,30 +175,68 @@ impl SQLMigration for Migration {
}
}
pub trait SupportsVirtualAliasLookup where Self: MailboxDB {
pub trait SupportsVAliasLookup {
// Write out the file that postfix will use as configuration for performing virtual alias lookups
fn write_valias_lookup_config_file(&self, output_path: &Path) -> Result<(), Error>;
}
pub trait SupportsVirtualMailboxLookup where Self: MailboxDB {
impl SupportsVAliasLookup for DB {
fn write_valias_lookup_config_file(&self, output_path: &Path) -> Result<(), Error> {
match self {
DB::SQLite(db) => db.write_valias_lookup_config_file(output_path),
_ => Err(Error::NotSupported)
}
}
}
pub trait SupportsVMailboxLookup {
// Write out the file that postfix will use as configuration for performing virtual mailbox lookups
fn write_vmailbox_lookup_config_file(&self, output_path: &Path, mailbox_base_dir: String) -> Result<(), Error>;
}
impl SupportsVirtualAliasLookup for DB {
fn write_valias_lookup_config_file(&self, path: &Path) -> Result<(), Error> {
impl SupportsVMailboxLookup for DB {
fn write_vmailbox_lookup_config_file(&self, output_path: &Path, mailbox_base_dir: String) -> Result<(), Error> {
match self {
DB::Sqlite(db) => db.write_valias_lookup_config_file(path),
_ => panic!("Unknown DB enum type"),
DB::SQLite(db) => db.write_vmailbox_lookup_config_file(output_path, mailbox_base_dir),
_ => Err(Error::NotSupported)
}
}
}
impl SupportsVirtualMailboxLookup for DB {
fn write_vmailbox_lookup_config_file(&self, path: &Path, mailbox_base_dir: String) -> Result<(), Error> {
// TODO: this can probably be generalized in the future to SupportsAuth<T> when Dovecot auth is added
pub trait SupportsCyrusAuth {
type Template;
fn cyrus_config_file_name(&self) -> Result<&'static str, Error>;
fn cyrus_config_file_path(&self, config_dir: &Path) -> Result<String, Error>;
// Write out the config file that cyrus can use
fn write_cyrus_smtpd_config_file(&self, config_dir: &Path) -> Result<(), Error>;
}
impl SupportsCyrusAuth for DB {
type Template = Box<Any>;
fn cyrus_config_file_name(&self) -> Result<&'static str, Error> {
match self {
DB::SQLite(db) => db.cyrus_config_file_name(),
_ => Err(Error::NotSupported)
}
}
fn cyrus_config_file_path(&self, config_dir: &Path) -> Result<String, Error> {
match self {
DB::SQLite(db) => db.cyrus_config_file_path(config_dir),
_ => Err(Error::NotSupported)
}
}
// Write out the config file that cyrus can use
fn write_cyrus_smtpd_config_file(&self, config_dir: &Path) -> Result<(), Error> {
match self {
DB::Sqlite(db) => db.write_vmailbox_lookup_config_file(path, mailbox_base_dir),
_ => panic!("Unknown DB enum type"),
DB::SQLite(db) => db.write_cyrus_smtpd_config_file(config_dir),
_ => Err(Error::NotSupported)
}
}
}
......@@ -4,8 +4,7 @@ pub mod models;
use askama::Template;
use chrono::prelude::Local;
use components::Error;
use components::db::{SQLMigration, Migration};
use components::db::{Versioned, Connectable, Migratable, AdminDB, MailboxDB, HardCodedSQLMigration, SupportsVirtualAliasLookup, SupportsVirtualMailboxLookup};
use components::db::*;
use config::SQLiteDBCfg;
use models::user::MailboxUser;
use models::{ModelWithUUID, PaginatedList, PaginationOptions, DBEntity};
......@@ -20,7 +19,7 @@ use make_absolute_path_from_str;
pub struct SQLiteDB {
cfg: SQLiteDBCfg,
conn: Option<SqliteConnection>
conn: Option<DBConnection>
}
impl SQLiteDB {
......@@ -33,12 +32,12 @@ impl SQLiteDB {
}
}
impl Connectable<SqliteConnection> for SQLiteDB {
impl Connectable for SQLiteDB {
fn connect(&mut self) -> Result<(), Error> {
if self.cfg.in_memory {
// Create in-memory DB
self.conn = Some(SqliteConnection::open_in_memory()?);
self.conn = Some(DBConnection::SQLite(SqliteConnection::open_in_memory()?));
} else {
// Use supplied DB path if provided
......@@ -50,7 +49,7 @@ impl Connectable<SqliteConnection> for SQLiteDB {
}
if self.conn.is_some() { return Ok(()); }
self.conn = Some(SqliteConnection::open(&self.cfg.path)?);
self.conn = Some(DBConnection::SQLite(SqliteConnection::open(&self.cfg.path)?));
}
// Initialize databases
......@@ -60,14 +59,22 @@ impl Connectable<SqliteConnection> for SQLiteDB {
Ok(())
}
fn connection(&self) -> Result<&SqliteConnection, Error> {
fn connection(&self) -> Result<&DBConnection, Error> {
self.conn.as_ref().ok_or(Error::Disconnected)
}
}
fn get_sqlite_connection<'a>(db: &'a Connectable) -> Result<&'a SQLiteConnection, Error> {
match db.connection()? {
&DBConnection::SQLite(ref conn) => Ok(conn),
_ => Err(Error::NotSupported)
}
}
impl Versioned<u32> for SQLiteDB {
fn retrieve_version(&self) -> Result<u32, Error> {
let c = self.connection()?;
let c: &SQLiteConnection = get_sqlite_connection(self)?;
c.query_row("PRAGMA user_version;", &[], |row| {
let version: u32 = row.get(0);
version
......@@ -75,7 +82,7 @@ impl Versioned<u32> for SQLiteDB {
}
fn persist_version(&self, version: u32) -> Result<u32, Error> {
let c = self.connection()?;
let c: &SQLiteConnection = get_sqlite_connection(self)?;
let query = format!("PRAGMA user_version = {};", version);
c.execute(query.as_str(), &[]).map_err(Error::from)?;
Ok(version)
......@@ -84,7 +91,7 @@ impl Versioned<u32> for SQLiteDB {
impl Migratable<u32> for SQLiteDB {
fn run_migration(&self, m: &Migration) -> Result<(), Error> {
let c = self.connection()?;
let c: &SQLiteConnection = get_sqlite_connection(self)?;
c.execute(m.sql().as_str(), &[])?;
Ok(())
}
......@@ -198,7 +205,7 @@ struct SQLiteVirtualAliasLookupConfigTemplate<'a> {
abs_db_path: String
}
impl SupportsVirtualAliasLookup for SQLiteDB {
impl SupportsVAliasLookup for SQLiteDB {
fn write_valias_lookup_config_file(&self, output_path: &Path) -> Result<(), Error> {
let filename = output_path.file_name()
.unwrap_or(OsStr::new("<filename unspecified>"))
......@@ -240,7 +247,7 @@ struct SQLiteVirtualMailboxLookupConfigTemplate<'a> {
mailbox_base_dir: String
}
impl SupportsVirtualMailboxLookup for SQLiteDB {
impl SupportsVMailboxLookup for SQLiteDB {
fn write_vmailbox_lookup_config_file(&self, output_path: &Path, mailbox_base_dir: String) -> Result<(), Error> {
let filename = output_path.file_name()
.unwrap_or(OsStr::new("<filename unspecified>"))
......@@ -271,6 +278,49 @@ impl SupportsVirtualMailboxLookup for SQLiteDB {
}
}
#[derive(Template)]
#[template(path = "config/cyrus_sasl/sqlite_smtpd.conf.jinja")]
pub struct CyrusSMTPDCfgTemplate {
filename: String,
generation_time: String,
abs_db_path: String
}
impl SupportsCyrusAuth for SQLiteDB {
type Template = CyrusSMTPDCfgTemplate;
fn cyrus_config_file_name(&self) -> Result<&'static str, Error> { Ok("smtpd.conf") }
fn cyrus_config_file_path(&self, config_dir: &Path) -> Result<String, Error> {
let output_path = config_dir.join(self.cyrus_config_file_name()?);
output_path.to_str().map(String::from).ok_or(Error::InvalidOrMissingConfig("failed to generate path for cyrus config"))
}
fn write_cyrus_smtpd_config_file(&self, config_dir: &Path) -> Result<(), Error> {
if config_dir.is_absolute() { warn!("starting path while writing cyrus smtpd_config_file is not absolute"); }
let filename = self.cyrus_config_file_name()?;
let template = CyrusSMTPDCfgTemplate {
filename: filename.to_string(),
generation_time: Local::now().to_string(),
abs_db_path: self.make_absolute_db_path()?
};
// Ensure the config directory exists
if !config_dir.exists() { create_dir_all(&config_dir)?; }
// Generate the output path the config will be written to
let output_path_str = self.cyrus_config_file_path(config_dir)?;
// Render the file contents, write them to disk
debug!("writing cyrus smtpd config file @ [{}]", output_path_str);
let mut query_config_file = File::create(output_path_str)?;
query_config_file.write_all(template.render()?.as_bytes())?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use config::SQLiteDBCfg;
......