Files
Stalwart/crates/install/src/main.rs
mdecimus e2b781566d v0.4.0
2023-10-25 11:26:28 +02:00

976 lines
30 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::{
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 pwhash::sha512_crypt;
use rand::{distributions::Alphanumeric, thread_rng, Rng};
use rusqlite::{Connection, OpenFlags};
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 Backend {
SQLite,
FoundationDB,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Blob {
Local,
MinIO,
S3,
Gcs,
Azure,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Directory {
Sql,
Ldap,
None,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum SmtpDirectory {
Sql,
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 directory = if component != Component::Smtp {
if !skip_download {
let backend = select::<Backend>(
"Which database engine would you like to use?",
&[
"SQLite (single node, replicated with Litestream)",
"FoundationDB (distributed and fault-tolerant)",
],
Backend::SQLite,
)?;
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 {
Backend::SQLite => "sqlite",
Backend::FoundationDB => "foundationdb",
},
TARGET,
PKG_EXTENSION
)
.into();
}
let blob = select::<Blob>(
"Where would you like to store e-mails and blobs?",
&[
"Local disk using Maildir",
"MinIO (or any S3-compatible object storage)",
"Amazon S3",
"Google Cloud Storage",
"Azure Blob Storage",
],
Blob::Local,
)?;
let directory = select::<Directory>(
"Do you already have a directory or database containing your accounts?",
&[
"Yes, it's an SQL database",
"Yes, it's an LDAP directory",
"No, create a new directory for me",
],
Directory::None,
)?;
// Update settings
if blob != Blob::Local {
sed(
cfg_path.join("jmap").join("store.toml"),
&[("\"local\"", "\"s3\"")],
);
}
if directory == Directory::Ldap {
sed(cfg_path.join("config.toml"), &[("/sql.toml", "/ldap.toml")]);
}
sed(
cfg_path.join("jmap").join("oauth.toml"),
&[(
"__OAUTH_KEY__",
thread_rng()
.sample_iter(Alphanumeric)
.take(64)
.map(char::from)
.collect::<String>(),
)],
);
directory
} else {
let smtp_directory = select::<SmtpDirectory>(
"How should your local accounts be validated?",
&[
"SQL database",
"LDAP directory",
"LMTP server",
"IMAP server",
],
SmtpDirectory::Lmtp,
)?;
if smtp_directory == SmtpDirectory::Ldap {
sed(cfg_path.join("config.toml"), &[("/sql.toml", "/ldap.toml")]);
}
match smtp_directory {
SmtpDirectory::Ldap => {
sed(cfg_path.join("config.toml"), &[("/sql.toml", "/ldap.toml")]);
}
SmtpDirectory::Lmtp | SmtpDirectory::Imap => {
let d_type = if smtp_directory == SmtpDirectory::Lmtp {
"lmtp"
} else {
"imap"
};
sed(
cfg_path.join("smtp").join("queue.toml"),
&[("default", d_type)],
);
sed(
cfg_path.join("smtp").join("session.toml"),
&[("default", d_type)],
);
sed(
cfg_path.join("config.toml"),
&[("\"%{BASE_PATH}%/etc/directory/", format!("\"%{{BASE_PATH}}%/etc/directory/{d_type}.toml\",\n\t\"%{{BASE_PATH}}%/etc/directory/"))],
);
}
SmtpDirectory::Sql => (),
}
if !skip_download {
download_url = format!(
concat!(
"https://github.com/stalwartlabs/smtp-server",
"/releases/latest/download/stalwart-smtp-{}.{}"
),
TARGET, PKG_EXTENSION
)
.into();
}
match smtp_directory {
SmtpDirectory::Sql => Directory::Sql,
SmtpDirectory::Ldap => Directory::Ldap,
SmtpDirectory::Lmtp | SmtpDirectory::Imap => Directory::None,
}
};
// 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)?;
// Create authentication and spam filter SQLite databases
let admin_password = create_databases(
&base_path,
if matches!(directory, Directory::None) {
Some(&domain)
} else {
None
},
)?;
// 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);
}
}
}
}
eprintln!("\n🎉 Installation completed!\n\n{dkim_instructions}\n");
if let Some(admin_password) = admin_password {
eprintln!("🔑 The administrator account is 'admin' with password '{admin_password}'.\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 create_databases(base_path: &Path, domain: Option<&str>) -> std::io::Result<Option<String>> {
// Create Spam database
let path = PathBuf::from(base_path)
.join("data")
.join("spamfilter.sqlite3");
let conn = Connection::open_with_flags(path, OpenFlags::default()).map_err(|err| {
std::io::Error::new(
std::io::ErrorKind::Other,
format!("Failed to open database: {}", err),
)
})?;
for query in [
concat!(
"CREATE TABLE IF NOT EXISTS bayes_tokens (h1 INTEGER NOT NULL, ",
"h2 INTEGER NOT NULL, ws INTEGER, wh INTEGER, PRIMARY KEY (h1, h2))",
),
concat!(
"CREATE TABLE IF NOT EXISTS seen_ids (id STRING NOT NULL PRIMARY KEY",
", ttl DATETIME NOT NULL)",
),
concat!(
"CREATE TABLE IF NOT EXISTS reputation (token STRING NOT NULL PRIMARY KEY",
", score FLOAT NOT NULL DEFAULT '0', count INT(11) NOT NULL ",
"DEFAULT '0', ttl DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP)",
),
] {
conn.execute(query, []).map_err(|err| {
std::io::Error::new(
std::io::ErrorKind::Other,
format!("Failed to create database: {}", err),
)
})?;
}
if let Some(domain) = domain {
// Create accounts database
let mut path = PathBuf::from(base_path);
path.push("data");
if !path.exists() {
fs::create_dir_all(&path)?;
}
path.push("accounts.sqlite3");
let conn = Connection::open_with_flags(path, OpenFlags::default()).map_err(|err| {
std::io::Error::new(
std::io::ErrorKind::Other,
format!("Failed to open database: {}", err),
)
})?;
let secret = thread_rng()
.sample_iter(Alphanumeric)
.take(12)
.map(char::from)
.collect::<String>();
let hashed_secret = sha512_crypt::hash(&secret).unwrap();
for query in [
concat!("CREATE TABLE IF NOT EXISTS accounts (name TEXT PRIMARY KEY, secret TEXT, description TEXT, ","type TEXT NOT NULL, quota INTEGER DEFAULT 0, active BOOLEAN DEFAULT 1)").to_string(),
concat!("CREATE TABLE IF NOT EXISTS group_members (name TEXT NOT NULL, member_of ","TEXT NOT NULL, PRIMARY KEY (name, member_of))").to_string(),
concat!("CREATE TABLE IF NOT EXISTS emails (name TEXT NOT NULL, address TEXT NOT NULL",", type TEXT, PRIMARY KEY (name, address))").to_string(),
format!("INSERT OR REPLACE INTO accounts (name, secret, description, type) VALUES ('admin', '{hashed_secret}', 'Postmaster', 'individual')"),
format!("INSERT OR REPLACE INTO emails (name, address, type) VALUES ('admin', 'postmaster@{domain}', 'primary')"),
"INSERT OR IGNORE INTO group_members (name, member_of) VALUES ('admin', 'superusers')".to_string()
] {
conn.execute(&query, []).map_err(|err| {
std::io::Error::new(
std::io::ErrorKind::Other,
format!("Failed to create database: {}", err),
)
})?;
}
Ok(Some(secret))
} else {
Ok(None)
}
}
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 Backend {
fn from_index(index: usize) -> Self {
match index {
0 => Self::SQLite,
1 => Self::FoundationDB,
_ => unreachable!(),
}
}
fn to_index(&self) -> usize {
match self {
Self::SQLite => 0,
Self::FoundationDB => 1,
}
}
}
impl SelectItem for Directory {
fn from_index(index: usize) -> Self {
match index {
0 => Self::Sql,
1 => Self::Ldap,
2 => Self::None,
_ => unreachable!(),
}
}
fn to_index(&self) -> usize {
match self {
Self::Sql => 0,
Self::Ldap => 1,
Self::None => 2,
}
}
}
impl SelectItem for SmtpDirectory {
fn from_index(index: usize) -> Self {
match index {
0 => Self::Sql,
1 => Self::Ldap,
2 => Self::Lmtp,
3 => Self::Imap,
_ => unreachable!(),
}
}
fn to_index(&self) -> usize {
match self {
SmtpDirectory::Sql => 0,
SmtpDirectory::Ldap => 1,
SmtpDirectory::Lmtp => 2,
SmtpDirectory::Imap => 3,
}
}
}
impl SelectItem for Blob {
fn from_index(index: usize) -> Self {
match index {
0 => Blob::Local,
1 => Blob::MinIO,
2 => Blob::S3,
3 => Blob::Gcs,
4 => Blob::Azure,
_ => unreachable!(),
}
}
fn to_index(&self) -> usize {
match self {
Blob::Local => 0,
Blob::MinIO => 1,
Blob::S3 => 2,
Blob::Gcs => 3,
Blob::Azure => 4,
}
}
}
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",
}
}
}