Files
Stalwart/crates/install/src/main.rs
2023-12-26 20:26:22 +01:00

1130 lines
32 KiB
Rust

/*
* Copyright (c) 2023 Stalwart Labs Ltd.
*
* This file is part of Stalwart Mail Server.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* in the LICENSE file at the top-level directory of this distribution.
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* You can be released from the requirements of the AGPLv3 license by
* purchasing a commercial license. Please contact licensing@stalw.art
* for more details.
*/
use std::{
fmt::{Display, Formatter},
fs,
io::Cursor,
path::{Path, PathBuf},
process::exit,
};
use base64::{engine::general_purpose, Engine};
use clap::{Parser, ValueEnum};
use dialoguer::{console::Term, theme::ColorfulTheme, Input, Select};
use openssl::rsa::Rsa;
use rand::{distributions::Alphanumeric, thread_rng, Rng};
const CONFIG_URL: &str = "https://get.stalw.art/resources/config.zip";
#[cfg(target_os = "linux")]
const SERVICE: &str = include_str!("../../../resources/systemd/stalwart-mail.service");
#[cfg(target_os = "macos")]
const SERVICE: &str = include_str!("../../../resources/systemd/stalwart.mail.plist");
#[cfg(target_os = "linux")]
const ACCOUNT_NAME: &str = "stalwart-mail";
#[cfg(target_os = "macos")]
const ACCOUNT_NAME: &str = "_stalwart-mail";
#[cfg(not(target_env = "msvc"))]
const PKG_EXTENSION: &str = "tar.gz";
#[cfg(target_env = "msvc")]
const PKG_EXTENSION: &str = "zip";
static TARGET: &str = env!("TARGET");
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
enum Component {
AllInOne,
Jmap,
Imap,
Smtp,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Store {
RocksDB,
FoundationDB,
SQLite,
PostgreSQL,
MySQL,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Blob {
Internal,
Filesystem,
S3,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Fts {
Internal,
ElasticSearch,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum SpamDb {
Internal,
Redis,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Directory {
Internal,
Ldap,
PostgreSQL,
MySQL,
SQLite,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum SmtpDirectory {
PostgreSQL,
MySQL,
SQLite,
Ldap,
Lmtp,
Imap,
}
const DIRECTORIES: [[&str; 2]; 6] = [
["bin", ""],
["etc", "dkim"],
["data", "blobs"],
["logs", ""],
["queue", ""],
["reports", ""],
];
#[derive(Debug, Parser)]
#[clap(version, about, long_about = None)]
#[clap(name = "stalwart-cli")]
pub struct Arguments {
#[clap(long, short = 'p')]
path: Option<PathBuf>,
#[clap(long, short = 'c')]
component: Option<Component>,
#[clap(long, short = 'd')]
docker: bool,
}
fn main() -> std::io::Result<()> {
let args = Arguments::parse();
#[cfg(not(target_env = "msvc"))]
unsafe {
if libc::getuid() != 0 {
eprintln!("This program must be run as root.");
std::process::exit(1);
}
}
println!("\nWelcome to the Stalwart Mail Server installer\n");
// Obtain component to install
let (component, skip_download) = if let Some(component) = args.component {
(component, true)
} else {
(
select::<Component>(
"Which components would you like to install?",
&[
"All-in-one mail server (JMAP + IMAP + SMTP)",
"JMAP server",
"IMAP server",
"SMTP server",
],
Component::AllInOne,
)?,
false,
)
};
// Obtain base path
let base_path = if let Some(base_path) = args.path {
base_path
} else {
PathBuf::from(input(
"Installation directory",
component.default_base_path(),
dir_create_if_missing,
)?)
};
create_directories(&base_path)?;
// Download and unpack configuration files
let cfg_path = base_path.join("etc");
if let Err(err) = zip_extract::extract(Cursor::new(download(CONFIG_URL)), &cfg_path, true) {
eprintln!(
"❌ Failed to unpack configuration bundle {}: {}",
CONFIG_URL, err
);
return Ok(());
}
// Build configuration file
let mut download_url = None;
// Obtain database engine
let mut is_internal = false;
if component != Component::Smtp {
let backend = select::<Store>(
"Which database would you like to use?",
&[
"RocksDB (recommended for single-node setups)",
"FoundationDB (recommended for distributed environments)",
"SQLite",
"PostgreSQL",
"MySQL",
],
Store::RocksDB,
)?;
if !skip_download {
download_url = format!(
concat!(
"https://github.com/stalwartlabs/{}",
"/releases/latest/download/stalwart-{}{}-{}.{}"
),
match component {
Component::AllInOne => "mail-server",
Component::Jmap => "jmap-server",
Component::Imap => "imap-server",
Component::Smtp => unreachable!(),
},
match component {
Component::AllInOne => "mail",
Component::Jmap => "jmap",
Component::Imap => "imap",
Component::Smtp => unreachable!(),
},
match backend {
Store::FoundationDB => "-foundationdb",
_ => "",
},
TARGET,
PKG_EXTENSION
)
.into();
}
let store = backend.to_string();
let blob = select::<Blob>(
"Where would you like to store e-mails and other large binaries?",
&[
&store,
"Local file system",
"S3, MinIO or any S3-compatible object storage",
],
Blob::Internal,
)?;
let fts = select::<Fts>(
"Where would you like to store the full-text index?",
&[&store, "ElasticSearch"],
Fts::Internal,
)?;
let spamdb = select::<SpamDb>(
"Where would you like to store the anti-spam database?",
&[&store, "Redis"],
SpamDb::Internal,
)?;
let directory = select::<Directory>(
"Do you already have a directory or database containing your user accounts?",
&[
&format!("No, I want Stalwart to store my user accounts in {store}"),
"Yes, it's an LDAP server",
"Yes, it's an PostgreSQL database",
"Yes, it's an MySQL database",
"Yes, it's an SQLite database",
],
Directory::Internal,
)?;
is_internal = matches!(directory, Directory::Internal);
// Update settings
sed(
cfg_path.join("config.toml"),
&[
("__STORE__", backend.id()),
("__DIRECTORY__", directory.id()),
],
);
sed(
cfg_path.join("jmap").join("store.toml"),
&[
("__BLOB_STORE__", blob.id().unwrap_or("%{DEFAULT_STORE}%")),
("__FTS_STORE__", fts.id().unwrap_or("%{DEFAULT_STORE}%")),
],
);
if let Some(id) = spamdb.id() {
sed(
cfg_path.join("common").join("sieve.toml"),
&[("%{DEFAULT_STORE}%", id)],
);
}
sed(
cfg_path.join("jmap").join("oauth.toml"),
&[(
"__OAUTH_KEY__",
thread_rng()
.sample_iter(Alphanumeric)
.take(64)
.map(char::from)
.collect::<String>(),
)],
);
// Enable stores
for store in [
backend.id().into(),
blob.id(),
fts.id(),
spamdb.id(),
directory.sql_store_id(),
]
.into_iter()
.flatten()
{
sed(
cfg_path.join("store").join(format!("{store}.toml")),
&[("disable = true", "disable = false")],
);
}
// Enable directory
if let Some(sql_id) = directory.sql_store_id() {
sed(
cfg_path.join("directory").join("sql.toml"),
&[
("disable = true", "disable = false"),
("__SQL_STORE__", sql_id),
],
);
} else {
sed(
cfg_path
.join("directory")
.join(format!("{}.toml", directory.id())),
&[("disable = true", "disable = false")],
);
}
} else {
let smtp_directory = select::<SmtpDirectory>(
"How should your local accounts be validated?",
&[
"PostgreSQL database",
"MySQL database",
"SQLite database",
"LDAP directory",
"LMTP server",
"IMAP server",
],
SmtpDirectory::Lmtp,
)?;
let spamdb = select::<SpamDb>(
"Where would you like to store the anti-spam database?",
&["Local database", "Redis"],
SpamDb::Internal,
)?;
// Update settings
sed(
cfg_path.join("config.toml"),
&[
("__STORE__", "rocksdb"),
("__DIRECTORY__", smtp_directory.id()),
],
);
sed(
cfg_path.join("jmap").join("store.toml"),
&[
("__BLOB_STORE__", "%{DEFAULT_STORE}%"),
("__FTS_STORE__", "%{DEFAULT_STORE}%"),
],
);
if let Some(id) = spamdb.id() {
sed(
cfg_path.join("common").join("sieve.toml"),
&[("%{DEFAULT_STORE}%", id)],
);
}
// Enable directory
if let Some(sql_id) = smtp_directory.sql_store_id() {
sed(
cfg_path.join("directory").join("sql.toml"),
&[
("disable = true", "disable = false"),
("__SQL_STORE__", sql_id),
],
);
} else {
sed(
cfg_path
.join("directory")
.join(format!("{}.toml", smtp_directory.id())),
&[("disable = true", "disable = false")],
);
}
// Enable stores
for store in [smtp_directory.sql_store_id(), spamdb.id(), "rocksdb".into()]
.into_iter()
.flatten()
{
sed(
cfg_path.join("store").join(format!("{store}.toml")),
&[("disable = true", "disable = false")],
);
}
if !skip_download {
download_url = format!(
concat!(
"https://github.com/stalwartlabs/smtp-server",
"/releases/latest/download/stalwart-smtp-{}.{}"
),
TARGET, PKG_EXTENSION
)
.into();
}
}
// Download binary
if let Some(download_url) = download_url {
eprintln!("📦 Downloading components...");
for url in [
download_url,
format!(
concat!(
"https://github.com/stalwartlabs/mail-server",
"/releases/latest/download/stalwart-cli-{}.{}"
),
TARGET, PKG_EXTENSION
),
] {
let bytes = download(&url);
let unpack_path = if !args.docker {
base_path.join("bin")
} else {
PathBuf::from("/usr/local/bin")
};
#[cfg(not(target_env = "msvc"))]
if let Err(err) = tar::Archive::new(flate2::bufread::GzDecoder::new(Cursor::new(bytes)))
.unpack(unpack_path)
{
eprintln!("❌ Failed to unpack {}: {}", url, err);
return Ok(());
}
#[cfg(target_env = "msvc")]
if let Err(err) = zip_extract::extract(Cursor::new(bytes), &unpack_path, true) {
eprintln!("❌ Failed to unpack {}: {}", url, err);
return Ok(());
}
}
}
// Obtain domain name
let domain = input(
"What is your main domain name? (you can add others later)",
"yourdomain.org",
not_empty,
)?
.trim()
.to_lowercase();
let hostname = input(
"What is your server hostname?",
&format!("mail.{domain}"),
not_empty,
)?
.trim()
.to_lowercase();
// Obtain TLS certificate path
let (cert_path, pk_path) = if !args.docker {
#[cfg(not(target_env = "msvc"))]
let cert_base_path = format!("/etc/letsencrypt/live/{}/", hostname);
#[cfg(target_env = "msvc")]
let cert_base_path = format!("C:\\Program Files\\Letsencrypt\\live\\{}\\", hostname);
(
input(
&format!("Where is the TLS certificate for '{hostname}' located?"),
&format!("{cert_base_path}fullchain.pem"),
file_exists,
)?,
input(
&format!("Where is the TLS private key for '{hostname}' located?"),
&format!("{cert_base_path}privkey.pem"),
file_exists,
)?,
)
} else {
// Create directories
fs::create_dir_all(base_path.join("etc").join("certs").join(&hostname))?;
(
format!(
"{}/etc/certs/{}/fullchain.pem",
base_path.display(),
hostname
),
format!("{}/etc/certs/{}/privkey.pem", base_path.display(), hostname),
)
};
// Generate DKIM key and instructions
let dkim_instructions = generate_dkim(&base_path, &domain, &hostname)?;
// Update config file
if args.docker {
sed(
cfg_path.join("common").join("server.toml"),
&[
("[server.run-as]", "#[server.run-as]"),
("user = \"stalwart-mail\"", "#user = \"stalwart-mail\""),
("group = \"stalwart-mail\"", "#group = \"stalwart-mail\""),
],
);
sed(
cfg_path.join("smtp").join("listener.toml"),
&[("127.0.0.1:8080", "[::]:8080")],
);
}
sed(
cfg_path.join("config.toml"),
&[
("__BASE_PATH__", base_path.to_str().unwrap()),
("__DOMAIN__", &domain),
("__HOST__", &hostname),
],
);
sed(
cfg_path.join("common").join("tls.toml"),
&[("__CERT_PATH__", &cert_path), ("__PK_PATH__", &pk_path)],
);
// Write service file
if !args.docker {
// Change permissions
#[cfg(not(target_env = "msvc"))]
{
let mut cmd = std::process::Command::new("chown");
cmd.arg("-R")
.arg(format!("{}:{}", ACCOUNT_NAME, ACCOUNT_NAME))
.arg(&base_path);
if let Err(err) = cmd.status() {
eprintln!("Warning: Failed to set permissions: {}", err);
}
let mut cmd = std::process::Command::new("chmod");
cmd.arg("-R")
.arg("770")
.arg(&format!("{}/etc", base_path.display()))
.arg(&format!("{}/data", base_path.display()))
.arg(&format!("{}/queue", base_path.display()))
.arg(&format!("{}/reports", base_path.display()))
.arg(&format!("{}/logs", base_path.display()));
if let Err(err) = cmd.status() {
eprintln!("Warning: Failed to set permissions: {}", err);
}
}
#[cfg(target_os = "linux")]
{
let service_file = format!(
"/etc/systemd/system/stalwart-{}.service",
component.binary_name()
);
let service_name = format!("stalwart-{}", component.binary_name());
match fs::write(
&service_file,
SERVICE
.replace("__PATH__", base_path.to_str().unwrap())
.replace("__NAME__", component.binary_name())
.replace("__TITLE__", component.name()),
) {
Ok(_) => {
if let Err(err) = std::process::Command::new("/bin/systemctl")
.arg("enable")
.arg(service_file)
.status()
.and_then(|_| {
std::process::Command::new("/bin/systemctl")
.arg("restart")
.arg(&service_name)
.status()
})
{
eprintln!("Warning: Failed to start service: {}", err);
}
}
Err(err) => {
eprintln!("Warning: Failed to write service file: {}", err);
}
}
}
#[cfg(target_os = "macos")]
{
let service_file = format!(
"/Library/LaunchDaemons/stalwart.{}.plist",
component.binary_name()
);
let service_name = format!("system/stalwart.{}", component.binary_name());
match fs::write(
&service_file,
SERVICE
.replace("__PATH__", base_path.to_str().unwrap())
.replace("__NAME__", component.binary_name())
.replace("__TITLE__", component.name()),
) {
Ok(_) => {
if let Err(err) = std::process::Command::new("launchctl")
.arg("load")
.arg(service_file)
.status()
.and_then(|_| {
std::process::Command::new("launchctl")
.arg("enable")
.arg(&service_name)
.status()
})
.and_then(|_| {
std::process::Command::new("launchctl")
.arg("start")
.arg(&service_name)
.status()
})
{
eprintln!("Warning: Failed to start service: {}", err);
}
}
Err(err) => {
eprintln!("Warning: Failed to write service file: {}", err);
}
}
}
}
if is_internal {
eprintln!(
"\n🔑 The administrator account is 'admin' and the password can be found in the log files at {}/logs.", base_path.display()
);
}
eprintln!("\n🎉 Installation completed!\n\n{dkim_instructions}\n");
Ok(())
}
fn sed(path: impl AsRef<Path>, replacements: &[(&str, impl AsRef<str>)]) {
let path = path.as_ref();
match fs::read_to_string(path) {
Ok(mut contents) => {
for (from, to) in replacements {
contents = contents.replace(from, to.as_ref());
}
if let Err(err) = fs::write(path, contents) {
eprintln!(
"❌ Failed to write configuration file {}: {}",
path.display(),
err
);
exit(1);
}
}
Err(err) => {
eprintln!(
"❌ Failed to read configuration file {}: {}",
path.display(),
err
);
exit(1);
}
}
}
fn download(url: &str) -> Vec<u8> {
match reqwest::blocking::get(url).and_then(|r| {
if r.status().is_success() {
r.bytes().map(Ok)
} else {
Ok(Err(r))
}
}) {
Ok(Ok(bytes)) => bytes.to_vec(),
Ok(Err(response)) => {
eprintln!(
"❌ Failed to download {}, make sure your platform is supported: {}",
url,
response.status()
);
exit(1);
}
Err(err) => {
eprintln!("❌ Failed to download {}: {}", url, err);
exit(1);
}
}
}
fn select<T: SelectItem>(prompt: &str, items: &[&str], default: T) -> std::io::Result<T> {
if let Some(index) = Select::with_theme(&ColorfulTheme::default())
.items(items)
.with_prompt(prompt)
.default(default.to_index())
.interact_on_opt(&Term::stderr())
.map_err(|err| {
std::io::Error::new(
std::io::ErrorKind::Other,
format!("Failed to read input: {}", err),
)
})?
{
Ok(T::from_index(index))
} else {
eprintln!("Aborted.");
std::process::exit(1);
}
}
fn input(
prompt: &str,
default: &str,
validator: fn(&String) -> Result<(), String>,
) -> std::io::Result<String> {
Input::with_theme(&ColorfulTheme::default())
.with_prompt(prompt)
.default(default.to_string())
.validate_with(validator)
.interact_text_on(&Term::stderr())
.map_err(|err| {
std::io::Error::new(
std::io::ErrorKind::Other,
format!("Failed to read input: {}", err),
)
})
}
fn dir_create_if_missing(path: &String) -> Result<(), String> {
let path = Path::new(path);
if path.is_dir() {
Ok(())
} else if let Err(e) = std::fs::create_dir_all(path) {
Err(format!(
"Failed to create directory {}: {}",
path.display(),
e
))
} else {
Ok(())
}
}
fn file_exists(path: &String) -> Result<(), String> {
let path = Path::new(path);
if path.is_file() {
Ok(())
} else {
Err(format!("File {} does not exist", path.display()))
}
}
#[allow(clippy::ptr_arg)]
fn not_empty(value: &String) -> Result<(), String> {
if value.trim().is_empty() {
Err("Value cannot be empty".to_string())
} else {
Ok(())
}
}
fn create_directories(path: &Path) -> std::io::Result<()> {
for dir in &DIRECTORIES {
let mut path = PathBuf::from(path);
path.push(dir[0]);
if !path.exists() {
fs::create_dir_all(&path)?;
}
if !dir[1].is_empty() {
path.push(dir[1]);
if !path.exists() {
fs::create_dir_all(&path)?;
}
}
}
Ok(())
}
fn generate_dkim(path: &Path, domain: &str, hostname: &str) -> std::io::Result<String> {
let mut path = PathBuf::from(path);
path.push("etc");
path.push("dkim");
fs::create_dir_all(&path)?;
// Generate key
let rsa = Rsa::generate(2048)?;
let mut public = String::new();
general_purpose::STANDARD.encode_string(rsa.public_key_to_der()?, &mut public);
let private = rsa.private_key_to_pem()?;
// Write private key
let mut pk_path = path.clone();
pk_path.push(&format!("{domain}.key"));
fs::write(pk_path, private)?;
// Write public key
let mut pub_path = path.clone();
pub_path.push(&format!("{domain}.cert"));
fs::write(pub_path, public.as_bytes())?;
// Write instructions
let instructions = format!(
"Add the following DNS records to your domain in order to enable DKIM, SPF and DMARC:\n\
\n\
stalwart._domainkey.{domain}. IN TXT \"v=DKIM1; k=rsa; p={public}\"\n\
{domain}. IN TXT \"v=spf1 a:{hostname} mx -all ra=postmaster\"\n\
{hostname}. IN TXT \"v=spf1 a -all ra=postmaster\"\n\
_dmarc.{domain}. IN TXT \"v=DMARC1; p=none; rua=mailto:postmaster@{domain}; ruf=mailto:postmaster@{domain}\"\n\
",
);
let mut txt_path = path.clone();
txt_path.push(&format!("{domain}.readme"));
fs::write(txt_path, instructions.as_bytes())?;
Ok(instructions)
}
/*#[cfg(not(target_env = "msvc"))]
unsafe fn get_uid_gid() -> (libc::uid_t, libc::gid_t) {
use std::{ffi::CString, process::Command};
let c_str = CString::new("stalwart-mail").unwrap();
let pw = libc::getpwnam(c_str.as_ptr());
let gr = libc::getgrnam(c_str.as_ptr());
if pw.is_null() || gr.is_null() {
let mut cmd = Command::new("useradd");
cmd.arg("-r")
.arg("-s")
.arg("/sbin/nologin")
.arg("-M")
.arg("stalwart-mail");
if let Err(e) = cmd.status() {
eprintln!("Failed to create stalwart system account: {}", e);
std::process::exit(1);
}
let pw = libc::getpwnam(c_str.as_ptr());
let gr = libc::getgrnam(c_str.as_ptr());
(pw.as_ref().unwrap().pw_uid, gr.as_ref().unwrap().gr_gid)
} else {
((*pw).pw_uid, ((*gr).gr_gid))
}
}*/
trait SelectItem {
fn from_index(index: usize) -> Self;
fn to_index(&self) -> usize;
}
impl SelectItem for Component {
fn from_index(index: usize) -> Self {
match index {
0 => Self::AllInOne,
1 => Self::Jmap,
2 => Self::Imap,
3 => Self::Smtp,
_ => unreachable!(),
}
}
fn to_index(&self) -> usize {
match self {
Self::AllInOne => 0,
Self::Jmap => 1,
Self::Imap => 2,
Self::Smtp => 3,
}
}
}
impl SelectItem for Store {
fn from_index(index: usize) -> Self {
match index {
0 => Self::RocksDB,
1 => Self::FoundationDB,
2 => Self::SQLite,
3 => Self::PostgreSQL,
4 => Self::MySQL,
_ => unreachable!(),
}
}
fn to_index(&self) -> usize {
match self {
Store::RocksDB => 0,
Store::FoundationDB => 1,
Store::SQLite => 2,
Store::PostgreSQL => 3,
Store::MySQL => 4,
}
}
}
impl SelectItem for Directory {
fn from_index(index: usize) -> Self {
match index {
0 => Self::Internal,
1 => Self::Ldap,
2 => Self::PostgreSQL,
3 => Self::MySQL,
4 => Self::SQLite,
_ => unreachable!(),
}
}
fn to_index(&self) -> usize {
match self {
Directory::Internal => 0,
Directory::Ldap => 1,
Directory::PostgreSQL => 2,
Directory::MySQL => 3,
Directory::SQLite => 4,
}
}
}
impl SelectItem for SmtpDirectory {
fn from_index(index: usize) -> Self {
match index {
0 => Self::PostgreSQL,
1 => Self::MySQL,
2 => Self::SQLite,
3 => Self::Ldap,
4 => Self::Lmtp,
5 => Self::Imap,
_ => unreachable!(),
}
}
fn to_index(&self) -> usize {
match self {
SmtpDirectory::PostgreSQL => 0,
SmtpDirectory::MySQL => 1,
SmtpDirectory::SQLite => 2,
SmtpDirectory::Ldap => 3,
SmtpDirectory::Lmtp => 4,
SmtpDirectory::Imap => 5,
}
}
}
impl SelectItem for Blob {
fn from_index(index: usize) -> Self {
match index {
0 => Self::Internal,
1 => Self::Filesystem,
2 => Self::S3,
_ => unreachable!(),
}
}
fn to_index(&self) -> usize {
match self {
Blob::Internal => 0,
Blob::Filesystem => 1,
Blob::S3 => 2,
}
}
}
impl SelectItem for SpamDb {
fn from_index(index: usize) -> Self {
match index {
0 => Self::Internal,
1 => Self::Redis,
_ => unreachable!(),
}
}
fn to_index(&self) -> usize {
match self {
SpamDb::Internal => 0,
SpamDb::Redis => 1,
}
}
}
impl SelectItem for Fts {
fn from_index(index: usize) -> Self {
match index {
0 => Self::Internal,
1 => Self::ElasticSearch,
_ => unreachable!(),
}
}
fn to_index(&self) -> usize {
match self {
Fts::Internal => 0,
Fts::ElasticSearch => 1,
}
}
}
impl Component {
fn default_base_path(&self) -> &'static str {
#[cfg(not(target_env = "msvc"))]
match self {
Self::AllInOne => "/opt/stalwart-mail",
Self::Jmap => "/opt/stalwart-jmap",
Self::Imap => "/opt/stalwart-imap",
Self::Smtp => "/opt/stalwart-smtp",
}
#[cfg(target_env = "msvc")]
match self {
Self::AllInOne => "C:\\Program Files\\Stalwart Mail",
Self::Jmap => "C:\\Program Files\\Stalwart JMAP",
Self::Imap => "C:\\Program Files\\Stalwart IMAP",
Self::Smtp => "C:\\Program Files\\Stalwart SMTP",
}
}
fn binary_name(&self) -> &'static str {
match self {
Self::AllInOne => "mail",
Self::Jmap => "jmap",
Self::Imap => "imap",
Self::Smtp => "smtp",
}
}
fn name(&self) -> &'static str {
match self {
Self::AllInOne => "Mail",
Self::Jmap => "JMAP",
Self::Imap => "IMAP",
Self::Smtp => "SMTP",
}
}
}
impl Display for Store {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::RocksDB => write!(f, "RocksDB"),
Self::FoundationDB => write!(f, "FoundationDB"),
Self::SQLite => write!(f, "SQLite"),
Self::PostgreSQL => write!(f, "PostgreSQL"),
Self::MySQL => write!(f, "MySQL"),
}
}
}
impl Store {
pub fn id(&self) -> &'static str {
match self {
Self::RocksDB => "rocksdb",
Self::FoundationDB => "foundationdb",
Self::SQLite => "sqlite",
Self::PostgreSQL => "postgresql",
Self::MySQL => "mysql",
}
}
}
impl Directory {
pub fn id(&self) -> &'static str {
match self {
Directory::Internal => "internal",
Directory::Ldap => "ldap",
Directory::PostgreSQL | Directory::MySQL | Directory::SQLite => "sql",
}
}
pub fn sql_store_id(&self) -> Option<&'static str> {
match self {
Directory::PostgreSQL => Some("postgresql"),
Directory::MySQL => Some("mysql"),
Directory::SQLite => Some("sqlite"),
Directory::Internal | Directory::Ldap => None,
}
}
}
impl SmtpDirectory {
pub fn id(&self) -> &'static str {
match self {
SmtpDirectory::Ldap => "ldap",
SmtpDirectory::Lmtp => "lmtp",
SmtpDirectory::Imap => "imap",
SmtpDirectory::PostgreSQL | SmtpDirectory::MySQL | SmtpDirectory::SQLite => "sql",
}
}
pub fn sql_store_id(&self) -> Option<&'static str> {
match self {
SmtpDirectory::PostgreSQL => Some("postgresql"),
SmtpDirectory::MySQL => Some("mysql"),
SmtpDirectory::SQLite => Some("sqlite"),
SmtpDirectory::Ldap | SmtpDirectory::Lmtp | SmtpDirectory::Imap => None,
}
}
}
impl Blob {
pub fn id(&self) -> Option<&'static str> {
match self {
Self::Internal => None,
Self::Filesystem => "fs".into(),
Self::S3 => "s3".into(),
}
}
}
impl Fts {
pub fn id(&self) -> Option<&'static str> {
match self {
Self::Internal => None,
Self::ElasticSearch => "elasticsearch".into(),
}
}
}
impl SpamDb {
pub fn id(&self) -> Option<&'static str> {
match self {
Self::Internal => None,
Self::Redis => "redis".into(),
}
}
}