mirror of
https://github.com/stalwartlabs/mail-server.git
synced 2024-09-20 07:16:18 +08:00
Installation binary first part.
This commit is contained in:
parent
705762c312
commit
843e61139a
44
Cargo.lock
generated
44
Cargo.lock
generated
|
@ -987,6 +987,18 @@ dependencies = [
|
||||||
"rusticata-macros",
|
"rusticata-macros",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dialoguer"
|
||||||
|
version = "0.10.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "59c6f2989294b9a498d3ad5491a79c6deb604617378e1cdc4bfc1c1361fe2f87"
|
||||||
|
dependencies = [
|
||||||
|
"console",
|
||||||
|
"shell-words",
|
||||||
|
"tempfile",
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "digest"
|
name = "digest"
|
||||||
version = "0.9.0"
|
version = "0.9.0"
|
||||||
|
@ -2722,6 +2734,15 @@ version = "0.1.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
|
checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "openssl-src"
|
||||||
|
version = "111.26.0+1.1.1u"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "efc62c9f12b22b8f5208c23a7200a442b2e5999f8bdf80233852122b5a4f6f37"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openssl-sys"
|
name = "openssl-sys"
|
||||||
version = "0.9.90"
|
version = "0.9.90"
|
||||||
|
@ -2730,6 +2751,7 @@ checksum = "374533b0e45f3a7ced10fcaeccca020e66656bc03dac384f852e4e5a7a8104a6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cc",
|
"cc",
|
||||||
"libc",
|
"libc",
|
||||||
|
"openssl-src",
|
||||||
"pkg-config",
|
"pkg-config",
|
||||||
"vcpkg",
|
"vcpkg",
|
||||||
]
|
]
|
||||||
|
@ -3971,6 +3993,12 @@ dependencies = [
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "shell-words"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "shlex"
|
name = "shlex"
|
||||||
version = "1.1.0"
|
version = "1.1.0"
|
||||||
|
@ -4352,6 +4380,22 @@ dependencies = [
|
||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "stalwart-install"
|
||||||
|
version = "0.3.0"
|
||||||
|
dependencies = [
|
||||||
|
"base64 0.21.2",
|
||||||
|
"dialoguer",
|
||||||
|
"indicatif",
|
||||||
|
"libc",
|
||||||
|
"openssl",
|
||||||
|
"pwhash",
|
||||||
|
"rand",
|
||||||
|
"reqwest",
|
||||||
|
"rpassword",
|
||||||
|
"rusqlite",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "static_assertions"
|
name = "static_assertions"
|
||||||
version = "1.1.0"
|
version = "1.1.0"
|
||||||
|
|
|
@ -13,6 +13,7 @@ members = [
|
||||||
"crates/utils",
|
"crates/utils",
|
||||||
"crates/maybe-async",
|
"crates/maybe-async",
|
||||||
"crates/cli",
|
"crates/cli",
|
||||||
|
"crates/install",
|
||||||
"tests",
|
"tests",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -42,7 +42,8 @@ impl ConfigDirectory for Config {
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add queries/filters as lookups
|
// Add queries/filters as lookups
|
||||||
if ["sql", "ldap"].contains(&protocol) {
|
let is_directory = ["sql", "ldap"].contains(&protocol);
|
||||||
|
if is_directory {
|
||||||
let name = if protocol == "sql" { "query" } else { "filter" };
|
let name = if protocol == "sql" { "query" } else { "filter" };
|
||||||
for lookup_id in self.sub_keys(("directory", id, name)) {
|
for lookup_id in self.sub_keys(("directory", id, name)) {
|
||||||
config.lookups.insert(
|
config.lookups.insert(
|
||||||
|
@ -58,9 +59,8 @@ impl ConfigDirectory for Config {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse lookups
|
// Parse lookups
|
||||||
let is_remote = protocol != "memory";
|
|
||||||
for lookup_id in self.sub_keys(("directory", id, "lookup")) {
|
for lookup_id in self.sub_keys(("directory", id, "lookup")) {
|
||||||
let lookup = if is_remote {
|
let lookup = if is_directory {
|
||||||
Lookup::Directory {
|
Lookup::Directory {
|
||||||
directory: directory.clone(),
|
directory: directory.clone(),
|
||||||
query: self
|
query: self
|
||||||
|
@ -120,6 +120,10 @@ impl DirectoryOptions {
|
||||||
Ok(DirectoryOptions {
|
Ok(DirectoryOptions {
|
||||||
catch_all: config.property_or_static((&key, "options.catch-all"), "false")?,
|
catch_all: config.property_or_static((&key, "options.catch-all"), "false")?,
|
||||||
subaddressing: config.property_or_static((&key, "options.subaddressing"), "true")?,
|
subaddressing: config.property_or_static((&key, "options.subaddressing"), "true")?,
|
||||||
|
superuser_group: config
|
||||||
|
.value("options.superuser-group")
|
||||||
|
.unwrap_or("superusers")
|
||||||
|
.to_string(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,7 +21,7 @@ impl ImapDirectory {
|
||||||
let address = config.value_require((&prefix, "address"))?;
|
let address = config.value_require((&prefix, "address"))?;
|
||||||
let tls_implicit: bool = config.property_or_static((&prefix, "tls.implicit"), "false")?;
|
let tls_implicit: bool = config.property_or_static((&prefix, "tls.implicit"), "false")?;
|
||||||
let port: u16 = config
|
let port: u16 = config
|
||||||
.property_or_static((&prefix, "port"), if tls_implicit { "443" } else { "143" })?;
|
.property_or_static((&prefix, "port"), if tls_implicit { "993" } else { "143" })?;
|
||||||
|
|
||||||
let manager = ImapConnectionManager {
|
let manager = ImapConnectionManager {
|
||||||
addr: format!("{address}:{port}"),
|
addr: format!("{address}:{port}"),
|
||||||
|
|
|
@ -304,17 +304,25 @@ impl LdapDirectory {
|
||||||
for entry in rs {
|
for entry in rs {
|
||||||
'outer: for (attr, value) in SearchEntry::construct(entry).attrs {
|
'outer: for (attr, value) in SearchEntry::construct(entry).attrs {
|
||||||
if self.mappings.attr_name.contains(&attr) {
|
if self.mappings.attr_name.contains(&attr) {
|
||||||
if let Some(name) = value.first() {
|
if let Some(group) = value.first() {
|
||||||
if !name.is_empty() {
|
if !group.is_empty() {
|
||||||
names.push(name.to_string());
|
if !group
|
||||||
|
.eq_ignore_ascii_case(&self.opt.superuser_group)
|
||||||
|
{
|
||||||
|
names.push(group.to_string());
|
||||||
|
} else {
|
||||||
|
principal.typ = Type::Superuser;
|
||||||
|
}
|
||||||
break 'outer;
|
break 'outer;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else if !group.eq_ignore_ascii_case(&self.opt.superuser_group) {
|
||||||
names.push(group);
|
names.push(group);
|
||||||
|
} else {
|
||||||
|
principal.typ = Type::Superuser;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
principal.member_of = names;
|
principal.member_of = names;
|
||||||
|
|
|
@ -33,6 +33,7 @@ pub enum Type {
|
||||||
Location,
|
Location,
|
||||||
#[default]
|
#[default]
|
||||||
Other,
|
Other,
|
||||||
|
Superuser,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
@ -135,7 +136,7 @@ impl Debug for Lookup {
|
||||||
impl Type {
|
impl Type {
|
||||||
pub fn to_jmap(&self) -> &'static str {
|
pub fn to_jmap(&self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
Self::Individual => "individual",
|
Self::Individual | Self::Superuser => "individual",
|
||||||
Self::Group => "group",
|
Self::Group => "group",
|
||||||
Self::Resource => "resource",
|
Self::Resource => "resource",
|
||||||
Self::Location => "location",
|
Self::Location => "location",
|
||||||
|
@ -148,6 +149,7 @@ impl Type {
|
||||||
struct DirectoryOptions {
|
struct DirectoryOptions {
|
||||||
catch_all: bool,
|
catch_all: bool,
|
||||||
subaddressing: bool,
|
subaddressing: bool,
|
||||||
|
superuser_group: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Clone, Debug)]
|
#[derive(Default, Clone, Debug)]
|
||||||
|
|
|
@ -12,12 +12,26 @@ impl MemoryDirectory {
|
||||||
prefix: impl AsKey,
|
prefix: impl AsKey,
|
||||||
) -> utils::config::Result<Arc<dyn Directory>> {
|
) -> utils::config::Result<Arc<dyn Directory>> {
|
||||||
let prefix = prefix.as_key();
|
let prefix = prefix.as_key();
|
||||||
let mut directory = MemoryDirectory::default();
|
let mut directory = MemoryDirectory {
|
||||||
|
opt: DirectoryOptions::from_config(config, prefix.clone())?,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
for lookup_id in config.sub_keys((prefix.as_str(), "users")) {
|
for lookup_id in config.sub_keys((prefix.as_str(), "users")) {
|
||||||
let name = config
|
let name = config
|
||||||
.value_require((prefix.as_str(), "users", lookup_id, "name"))?
|
.value_require((prefix.as_str(), "users", lookup_id, "name"))?
|
||||||
.to_string();
|
.to_string();
|
||||||
|
let mut typ = Type::Individual;
|
||||||
|
let mut member_of = Vec::new();
|
||||||
|
|
||||||
|
for (_, group) in config.values((prefix.as_str(), "users", lookup_id, "member-of")) {
|
||||||
|
if !group.eq_ignore_ascii_case(&directory.opt.superuser_group) {
|
||||||
|
member_of.push(group.to_string());
|
||||||
|
} else {
|
||||||
|
typ = Type::Superuser;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
directory.principals.insert(
|
directory.principals.insert(
|
||||||
name.clone(),
|
name.clone(),
|
||||||
Principal {
|
Principal {
|
||||||
|
@ -26,17 +40,14 @@ impl MemoryDirectory {
|
||||||
.values((prefix.as_str(), "users", lookup_id, "secret"))
|
.values((prefix.as_str(), "users", lookup_id, "secret"))
|
||||||
.map(|(_, v)| v.to_string())
|
.map(|(_, v)| v.to_string())
|
||||||
.collect(),
|
.collect(),
|
||||||
typ: Type::Individual,
|
typ,
|
||||||
description: config
|
description: config
|
||||||
.value((prefix.as_str(), "users", lookup_id, "description"))
|
.value((prefix.as_str(), "users", lookup_id, "description"))
|
||||||
.map(|v| v.to_string()),
|
.map(|v| v.to_string()),
|
||||||
quota: config
|
quota: config
|
||||||
.property((prefix.as_str(), "users", lookup_id, "quota"))?
|
.property((prefix.as_str(), "users", lookup_id, "quota"))?
|
||||||
.unwrap_or(0),
|
.unwrap_or(0),
|
||||||
member_of: config
|
member_of,
|
||||||
.values((prefix.as_str(), "users", lookup_id, "member-of"))
|
|
||||||
.map(|(_, v)| v.to_string())
|
|
||||||
.collect(),
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
let mut emails = Vec::new();
|
let mut emails = Vec::new();
|
||||||
|
@ -105,7 +116,6 @@ impl MemoryDirectory {
|
||||||
directory
|
directory
|
||||||
.domains
|
.domains
|
||||||
.extend(config.parse_lookup_list((&prefix, "lookup.domains"))?);
|
.extend(config.parse_lookup_list((&prefix, "lookup.domains"))?);
|
||||||
directory.opt = DirectoryOptions::from_config(config, prefix)?;
|
|
||||||
|
|
||||||
Ok(Arc::new(directory))
|
Ok(Arc::new(directory))
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,6 +39,16 @@ impl Directory for SqlDirectory {
|
||||||
.fetch_all(&self.pool)
|
.fetch_all(&self.pool)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
// Check whether the user is a superuser
|
||||||
|
if let Some(idx) = principal
|
||||||
|
.member_of
|
||||||
|
.iter()
|
||||||
|
.position(|group| group.eq_ignore_ascii_case(&self.opt.superuser_group))
|
||||||
|
{
|
||||||
|
principal.member_of.swap_remove(idx);
|
||||||
|
principal.typ = Type::Superuser;
|
||||||
|
}
|
||||||
|
|
||||||
Ok(Some(principal))
|
Ok(Some(principal))
|
||||||
} else {
|
} else {
|
||||||
Ok(None)
|
Ok(None)
|
||||||
|
|
|
@ -5,7 +5,6 @@ use imap_proto::{protocol::list::Attribute, StatusResponse};
|
||||||
use jmap::{
|
use jmap::{
|
||||||
auth::{acl::EffectiveAcl, AccessToken},
|
auth::{acl::EffectiveAcl, AccessToken},
|
||||||
mailbox::INBOX_ID,
|
mailbox::INBOX_ID,
|
||||||
SUPERUSER_ID,
|
|
||||||
};
|
};
|
||||||
use jmap_proto::{
|
use jmap_proto::{
|
||||||
object::Object,
|
object::Object,
|
||||||
|
@ -43,31 +42,29 @@ impl SessionData {
|
||||||
|
|
||||||
// Fetch shared mailboxes
|
// Fetch shared mailboxes
|
||||||
for &account_id in access_token.shared_accounts(Collection::Mailbox) {
|
for &account_id in access_token.shared_accounts(Collection::Mailbox) {
|
||||||
if account_id != SUPERUSER_ID {
|
match session
|
||||||
match session
|
.fetch_account_mailboxes(
|
||||||
.fetch_account_mailboxes(
|
account_id,
|
||||||
account_id,
|
format!(
|
||||||
format!(
|
"{}/{}",
|
||||||
"{}/{}",
|
session.imap.name_shared,
|
||||||
session.imap.name_shared,
|
session
|
||||||
session
|
.jmap
|
||||||
.jmap
|
.get_account_name(account_id)
|
||||||
.get_account_name(account_id)
|
.await
|
||||||
.await
|
.unwrap_or_default()
|
||||||
.unwrap_or_default()
|
.unwrap_or_else(|| Id::from(account_id).to_string())
|
||||||
.unwrap_or_else(|| Id::from(account_id).to_string())
|
|
||||||
)
|
|
||||||
.into(),
|
|
||||||
access_token,
|
|
||||||
)
|
)
|
||||||
.await
|
.into(),
|
||||||
{
|
access_token,
|
||||||
Ok(account_mailboxes) => {
|
)
|
||||||
mailboxes.push(account_mailboxes);
|
.await
|
||||||
}
|
{
|
||||||
Err(_) => {
|
Ok(account_mailboxes) => {
|
||||||
tracing::warn!(parent: &session.span, account_id = account_id, event = "error", "Failed to retrieve mailboxes.");
|
mailboxes.push(account_mailboxes);
|
||||||
}
|
}
|
||||||
|
Err(_) => {
|
||||||
|
tracing::warn!(parent: &session.span, account_id = account_id, event = "error", "Failed to retrieve mailboxes.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -305,11 +302,10 @@ impl SessionData {
|
||||||
|
|
||||||
// Add new shared account ids
|
// Add new shared account ids
|
||||||
for account_id in has_access_to {
|
for account_id in has_access_to {
|
||||||
if account_id != SUPERUSER_ID
|
if !new_accounts
|
||||||
&& !new_accounts
|
.iter()
|
||||||
.iter()
|
.skip(1)
|
||||||
.skip(1)
|
.any(|m| m.account_id == account_id)
|
||||||
.any(|m| m.account_id == account_id)
|
|
||||||
{
|
{
|
||||||
tracing::debug!(parent: &self.span, "Adding shared account {}", account_id);
|
tracing::debug!(parent: &self.span, "Adding shared account {}", account_id);
|
||||||
added_account_ids.push(account_id);
|
added_account_ids.push(account_id);
|
||||||
|
|
|
@ -42,14 +42,16 @@ impl IMAP {
|
||||||
})
|
})
|
||||||
.into_bytes(),
|
.into_bytes(),
|
||||||
rate_limiter: DashMap::with_capacity_and_hasher_and_shard_amount(
|
rate_limiter: DashMap::with_capacity_and_hasher_and_shard_amount(
|
||||||
config.property("imap.rate-limit.size")?.unwrap_or(2048),
|
config
|
||||||
|
.property("imap.rate-limit.cache.size")?
|
||||||
|
.unwrap_or(2048),
|
||||||
RandomState::default(),
|
RandomState::default(),
|
||||||
config
|
config
|
||||||
.property::<u64>("global.shared-map.shard")?
|
.property::<u64>("global.shared-map.shard")?
|
||||||
.unwrap_or(32)
|
.unwrap_or(32)
|
||||||
.next_power_of_two() as usize,
|
.next_power_of_two() as usize,
|
||||||
),
|
),
|
||||||
rate_requests: config.property_or_static("imap.rate-limit", "1000/1m")?,
|
rate_requests: config.property_or_static("imap.rate-limit.requests", "2000/1m")?,
|
||||||
rate_concurrent: config.property("imap.rate-limit.concurrent")?.unwrap_or(4),
|
rate_concurrent: config.property("imap.rate-limit.concurrent")?.unwrap_or(4),
|
||||||
allow_plain_auth: config.property_or_static("imap.auth.allow-plain-text", "false")?,
|
allow_plain_auth: config.property_or_static("imap.auth.allow-plain-text", "false")?,
|
||||||
}))
|
}))
|
||||||
|
|
25
crates/install/Cargo.toml
Normal file
25
crates/install/Cargo.toml
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
[package]
|
||||||
|
name = "stalwart-install"
|
||||||
|
description = "Stalwart Mail Server installer"
|
||||||
|
authors = ["Stalwart Labs Ltd. <hello@stalw.art>"]
|
||||||
|
license = "AGPL-3.0-only"
|
||||||
|
repository = "https://github.com/stalwartlabs/mail-server"
|
||||||
|
homepage = "https://github.com/stalwartlabs/mail-server"
|
||||||
|
version = "0.3.0"
|
||||||
|
edition = "2021"
|
||||||
|
readme = "README.md"
|
||||||
|
resolver = "2"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
reqwest = { version = "0.11", default-features = false, features = ["rustls-tls-webpki-roots"]}
|
||||||
|
rusqlite = { version = "0.29.0", features = ["bundled"] }
|
||||||
|
rpassword = "7.0"
|
||||||
|
indicatif = "0.17.0"
|
||||||
|
dialoguer = "0.10.4"
|
||||||
|
openssl = { version = "0.10.55", features = ["vendored"] }
|
||||||
|
base64 = "0.21.2"
|
||||||
|
pwhash = "1.0.0"
|
||||||
|
rand = "0.8.5"
|
||||||
|
|
||||||
|
[target.'cfg(not(target_env = "msvc"))'.dependencies]
|
||||||
|
libc = "0.2.147"
|
562
crates/install/src/main.rs
Normal file
562
crates/install/src/main.rs
Normal file
|
@ -0,0 +1,562 @@
|
||||||
|
use std::{
|
||||||
|
fs,
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
time::SystemTime,
|
||||||
|
};
|
||||||
|
|
||||||
|
use base64::{engine::general_purpose, Engine};
|
||||||
|
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 CFG_COMMON: &str = include_str!("../../../resources/config/common.toml");
|
||||||
|
const CFG_DIRECTORY: &str = include_str!("../../../resources/config/directory.toml");
|
||||||
|
const CFG_JMAP: &str = include_str!("../../../resources/config/jmap.toml");
|
||||||
|
const CFG_IMAP: &str = include_str!("../../../resources/config/imap.toml");
|
||||||
|
const CFG_SMTP: &str = include_str!("../../../resources/config/smtp.toml");
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
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", ""],
|
||||||
|
];
|
||||||
|
|
||||||
|
fn main() -> std::io::Result<()> {
|
||||||
|
let c = "fix";
|
||||||
|
/*#[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");
|
||||||
|
|
||||||
|
let component = 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,
|
||||||
|
)?;
|
||||||
|
let mut cfg_file = match component {
|
||||||
|
Component::AllInOne | Component::Imap => {
|
||||||
|
[CFG_COMMON, CFG_DIRECTORY, CFG_JMAP, CFG_IMAP, CFG_SMTP].join("\n")
|
||||||
|
}
|
||||||
|
Component::Jmap => [CFG_COMMON, CFG_DIRECTORY, CFG_JMAP, CFG_SMTP].join("\n"),
|
||||||
|
Component::Smtp => [CFG_COMMON, CFG_DIRECTORY, CFG_SMTP].join("\n"),
|
||||||
|
};
|
||||||
|
let directory = if component != Component::Smtp {
|
||||||
|
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,
|
||||||
|
)?;
|
||||||
|
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,
|
||||||
|
)?;
|
||||||
|
cfg_file = cfg_file
|
||||||
|
.replace(
|
||||||
|
"__BLOB_STORE__",
|
||||||
|
match blob {
|
||||||
|
Blob::Local => "local",
|
||||||
|
_ => "s3",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.replace("__NEXT_HOP__", "local")
|
||||||
|
.replace(
|
||||||
|
"__DIRECTORY__",
|
||||||
|
match directory {
|
||||||
|
Directory::SQL | Directory::None => "sql",
|
||||||
|
Directory::LDAP => "ldap",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.replace(
|
||||||
|
"__SMTP_DIRECTORY__",
|
||||||
|
match directory {
|
||||||
|
Directory::SQL | Directory::None => "sql",
|
||||||
|
Directory::LDAP => "ldap",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.replace(
|
||||||
|
"__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,
|
||||||
|
)?;
|
||||||
|
cfg_file = cfg_file
|
||||||
|
.replace("__NEXT_HOP__", "lmtp")
|
||||||
|
.replace(
|
||||||
|
"__SMTP_DIRECTORY__",
|
||||||
|
match smtp_directory {
|
||||||
|
SmtpDirectory::SQL => "sql",
|
||||||
|
SmtpDirectory::LDAP => "ldap",
|
||||||
|
SmtpDirectory::LMTP => "lmtp",
|
||||||
|
SmtpDirectory::IMAP => "imap",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.replace(
|
||||||
|
"__DIRECTORY__",
|
||||||
|
match smtp_directory {
|
||||||
|
SmtpDirectory::SQL | SmtpDirectory::LMTP | SmtpDirectory::IMAP => "sql",
|
||||||
|
SmtpDirectory::LDAP => "ldap",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.replace("__NEXT_HOP__", "lmtp");
|
||||||
|
match smtp_directory {
|
||||||
|
SmtpDirectory::SQL => Directory::SQL,
|
||||||
|
SmtpDirectory::LDAP => Directory::LDAP,
|
||||||
|
SmtpDirectory::LMTP | SmtpDirectory::IMAP => Directory::None,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let base_path = PathBuf::from(input(
|
||||||
|
"Installation directory",
|
||||||
|
component.default_base_path(),
|
||||||
|
dir_create_if_missing,
|
||||||
|
)?);
|
||||||
|
create_directories(&base_path)?;
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
let cert_path = input(
|
||||||
|
&format!("Where is the TLS certificate for '{hostname}' located?"),
|
||||||
|
&format!("/etc/letsencrypt/live/{hostname}/fullchain.pem"),
|
||||||
|
file_exists,
|
||||||
|
)?;
|
||||||
|
let pk_path = input(
|
||||||
|
&format!("Where is the TLS private key for '{hostname}' located?"),
|
||||||
|
&format!("/etc/letsencrypt/live/{hostname}/privkey.pem"),
|
||||||
|
file_exists,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let dkim_instructions = generate_dkim(&base_path, &domain, &hostname)?;
|
||||||
|
let admin_password = if matches!(directory, Directory::None) {
|
||||||
|
create_auth_db(&base_path, &domain)?.into()
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
// Write config file
|
||||||
|
let cfg_path = base_path.join("etc").join("config.toml");
|
||||||
|
if cfg_path.exists() {
|
||||||
|
// Rename existing config file
|
||||||
|
let backup_path = base_path.join("etc").join(format!(
|
||||||
|
"config.toml.bak.{}",
|
||||||
|
SystemTime::now()
|
||||||
|
.duration_since(SystemTime::UNIX_EPOCH)
|
||||||
|
.map(|d| d.as_secs())
|
||||||
|
.unwrap_or(0)
|
||||||
|
));
|
||||||
|
fs::rename(&cfg_path, backup_path)?;
|
||||||
|
}
|
||||||
|
fs::write(
|
||||||
|
cfg_path,
|
||||||
|
cfg_file
|
||||||
|
.replace("__PATH__", base_path.to_str().unwrap())
|
||||||
|
.replace("__DOMAIN__", &domain)
|
||||||
|
.replace("__HOST__", &hostname)
|
||||||
|
.replace("__CERT_PATH__", &cert_path)
|
||||||
|
.replace("__PK_PATH__", &pk_path),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
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 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())?
|
||||||
|
{
|
||||||
|
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())
|
||||||
|
}
|
||||||
|
|
||||||
|
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_auth_db(path: &Path, domain: &str) -> std::io::Result<String> {
|
||||||
|
let mut path = PathBuf::from(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 [
|
||||||
|
"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(),
|
||||||
|
"CREATE TABLE IF NOT EXISTS group_members (name TEXT NOT NULL, member_of TEXT NOT NULL, PRIMARY KEY (name, member_of))".to_string(),
|
||||||
|
"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(secret)
|
||||||
|
}
|
||||||
|
|
||||||
|
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::process::Command;
|
||||||
|
let pw = libc::getpwnam("stalwart-mail".as_ptr() as *const i8);
|
||||||
|
let gr = libc::getgrnam("stalwart-mail".as_ptr() as *const i8);
|
||||||
|
|
||||||
|
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("stalwart-mail".as_ptr() as *const i8);
|
||||||
|
let gr = libc::getgrnam("stalwart-mail".as_ptr() as *const i8);
|
||||||
|
(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 {
|
||||||
|
match self {
|
||||||
|
Self::AllInOne => "/opt/stalwart-mail",
|
||||||
|
Self::Jmap => "/opt/stalwart-jmap",
|
||||||
|
Self::Imap => "/opt/stalwart-imap",
|
||||||
|
Self::Smtp => "/opt/stalwart-smtp",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -136,10 +136,6 @@ impl crate::Config {
|
||||||
web_socket_timeout: settings.property_or_static("jmap.web-socket.timeout", "10m")?,
|
web_socket_timeout: settings.property_or_static("jmap.web-socket.timeout", "10m")?,
|
||||||
web_socket_heartbeat: settings.property_or_static("jmap.web-socket.heartbeat", "1m")?,
|
web_socket_heartbeat: settings.property_or_static("jmap.web-socket.heartbeat", "1m")?,
|
||||||
push_max_total: settings.property_or_static("jmap.push.max-total", "100")?,
|
push_max_total: settings.property_or_static("jmap.push.max-total", "100")?,
|
||||||
principal_superusers: settings
|
|
||||||
.value("jmap.principal.superusers")
|
|
||||||
.unwrap_or("superusers")
|
|
||||||
.to_string(),
|
|
||||||
principal_allow_lookups: settings
|
principal_allow_lookups: settings
|
||||||
.property("jmap.principal.allow-lookups")?
|
.property("jmap.principal.allow-lookups")?
|
||||||
.unwrap_or(true),
|
.unwrap_or(true),
|
||||||
|
|
|
@ -40,7 +40,7 @@ use store::{
|
||||||
};
|
};
|
||||||
use utils::{listener::limiter::InFlight, map::ttl_dashmap::TtlMap};
|
use utils::{listener::limiter::InFlight, map::ttl_dashmap::TtlMap};
|
||||||
|
|
||||||
use crate::{JMAP, SUPERUSER_ID};
|
use crate::JMAP;
|
||||||
|
|
||||||
use super::{rate_limit::RemoteAddress, AccessToken};
|
use super::{rate_limit::RemoteAddress, AccessToken};
|
||||||
|
|
||||||
|
@ -226,11 +226,7 @@ impl JMAP {
|
||||||
pub async fn map_member_of(&self, names: Vec<String>) -> Result<Vec<u32>, MethodError> {
|
pub async fn map_member_of(&self, names: Vec<String>) -> Result<Vec<u32>, MethodError> {
|
||||||
let mut ids = Vec::with_capacity(names.len());
|
let mut ids = Vec::with_capacity(names.len());
|
||||||
for name in names {
|
for name in names {
|
||||||
if !name.eq_ignore_ascii_case(&self.config.principal_superusers) {
|
ids.push(self.get_account_id(&name).await?);
|
||||||
ids.push(self.get_account_id(&name).await?);
|
|
||||||
} else {
|
|
||||||
ids.push(SUPERUSER_ID);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Ok(ids)
|
Ok(ids)
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,7 +31,7 @@ use aes_gcm_siv::{
|
||||||
AeadInPlace, Aes256GcmSiv, KeyInit, Nonce,
|
AeadInPlace, Aes256GcmSiv, KeyInit, Nonce,
|
||||||
};
|
};
|
||||||
|
|
||||||
use directory::Principal;
|
use directory::{Principal, Type};
|
||||||
use jmap_proto::{
|
use jmap_proto::{
|
||||||
error::method::MethodError,
|
error::method::MethodError,
|
||||||
types::{collection::Collection, id::Id},
|
types::{collection::Collection, id::Id},
|
||||||
|
@ -39,8 +39,6 @@ use jmap_proto::{
|
||||||
use store::blake3;
|
use store::blake3;
|
||||||
use utils::map::bitmap::Bitmap;
|
use utils::map::bitmap::Bitmap;
|
||||||
|
|
||||||
use crate::SUPERUSER_ID;
|
|
||||||
|
|
||||||
pub mod acl;
|
pub mod acl;
|
||||||
pub mod authenticate;
|
pub mod authenticate;
|
||||||
pub mod oauth;
|
pub mod oauth;
|
||||||
|
@ -54,6 +52,7 @@ pub struct AccessToken {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub description: Option<String>,
|
pub description: Option<String>,
|
||||||
pub quota: u32,
|
pub quota: u32,
|
||||||
|
pub is_superuser: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AccessToken {
|
impl AccessToken {
|
||||||
|
@ -65,6 +64,7 @@ impl AccessToken {
|
||||||
name: principal.name,
|
name: principal.name,
|
||||||
description: principal.description,
|
description: principal.description,
|
||||||
quota: principal.quota,
|
quota: principal.quota,
|
||||||
|
is_superuser: principal.typ == Type::Superuser,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -95,9 +95,7 @@ impl AccessToken {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_member(&self, account_id: u32) -> bool {
|
pub fn is_member(&self, account_id: u32) -> bool {
|
||||||
self.primary_id == account_id
|
self.primary_id == account_id || self.member_of.contains(&account_id) || self.is_superuser
|
||||||
|| self.member_of.contains(&account_id)
|
|
||||||
|| self.member_of.contains(&SUPERUSER_ID)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_primary_id(&self, account_id: u32) -> bool {
|
pub fn is_primary_id(&self, account_id: u32) -> bool {
|
||||||
|
@ -105,7 +103,7 @@ impl AccessToken {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_super_user(&self) -> bool {
|
pub fn is_super_user(&self) -> bool {
|
||||||
self.member_of.contains(&SUPERUSER_ID)
|
self.is_superuser
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_shared(&self, account_id: u32) -> bool {
|
pub fn is_shared(&self, account_id: u32) -> bool {
|
||||||
|
|
|
@ -78,7 +78,6 @@ pub mod thread;
|
||||||
pub mod vacation;
|
pub mod vacation;
|
||||||
pub mod websocket;
|
pub mod websocket;
|
||||||
|
|
||||||
pub const SUPERUSER_ID: u32 = 0;
|
|
||||||
pub const LONG_SLUMBER: Duration = Duration::from_secs(60 * 60 * 24);
|
pub const LONG_SLUMBER: Duration = Duration::from_secs(60 * 60 * 24);
|
||||||
|
|
||||||
pub struct JMAP {
|
pub struct JMAP {
|
||||||
|
@ -151,7 +150,6 @@ pub struct Config {
|
||||||
pub oauth_expiry_refresh_token_renew: u64,
|
pub oauth_expiry_refresh_token_renew: u64,
|
||||||
pub oauth_max_auth_attempts: u32,
|
pub oauth_max_auth_attempts: u32,
|
||||||
|
|
||||||
pub principal_superusers: String,
|
|
||||||
pub principal_allow_lookups: bool,
|
pub principal_allow_lookups: bool,
|
||||||
|
|
||||||
pub capabilities: BaseCapabilities,
|
pub capabilities: BaseCapabilities,
|
||||||
|
|
|
@ -755,7 +755,7 @@ impl JMAP {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "test_mode")]
|
#[cfg(feature = "test_mode")]
|
||||||
if mailbox_ids.is_empty() && account_id == crate::SUPERUSER_ID {
|
if mailbox_ids.is_empty() && account_id == 0 {
|
||||||
return Ok(mailbox_ids);
|
return Ok(mailbox_ids);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -23,6 +23,7 @@
|
||||||
|
|
||||||
use std::{borrow::Cow, fmt::Display, net::IpAddr, sync::Arc, time::Instant};
|
use std::{borrow::Cow, fmt::Display, net::IpAddr, sync::Arc, time::Instant};
|
||||||
|
|
||||||
|
use directory::Type;
|
||||||
use http_body_util::{combinators::BoxBody, BodyExt, Empty, Full};
|
use http_body_util::{combinators::BoxBody, BodyExt, Empty, Full};
|
||||||
use hyper::{
|
use hyper::{
|
||||||
body::{self, Bytes},
|
body::{self, Bytes},
|
||||||
|
@ -256,9 +257,16 @@ impl SMTP {
|
||||||
.authenticate(&Credentials::Plain { username, secret })
|
.authenticate(&Credentials::Plain { username, secret })
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(Some(_)) => {
|
Ok(Some(principal)) if principal.typ == Type::Superuser => {
|
||||||
is_authenticated = true;
|
is_authenticated = true;
|
||||||
}
|
}
|
||||||
|
Ok(Some(_)) => {
|
||||||
|
tracing::debug!(
|
||||||
|
context = "management",
|
||||||
|
event = "auth-error",
|
||||||
|
"Insufficient privileges."
|
||||||
|
);
|
||||||
|
}
|
||||||
Ok(None) => {
|
Ok(None) => {
|
||||||
tracing::debug!(
|
tracing::debug!(
|
||||||
context = "management",
|
context = "management",
|
||||||
|
|
|
@ -39,7 +39,7 @@ impl Store {
|
||||||
pub async fn open(config: &Config) -> crate::Result<Self> {
|
pub async fn open(config: &Config) -> crate::Result<Self> {
|
||||||
let db = Self {
|
let db = Self {
|
||||||
conn_pool: Pool::builder()
|
conn_pool: Pool::builder()
|
||||||
.max_size(config.property_or_static("store.db.connection-pool.size", "10")?)
|
.max_size(config.property_or_static("store.db.pool.max-connections", "10")?)
|
||||||
.build(
|
.build(
|
||||||
SqliteConnectionManager::file(
|
SqliteConnectionManager::file(
|
||||||
config
|
config
|
||||||
|
@ -58,7 +58,7 @@ impl Store {
|
||||||
worker_pool: rayon::ThreadPoolBuilder::new()
|
worker_pool: rayon::ThreadPoolBuilder::new()
|
||||||
.num_threads(
|
.num_threads(
|
||||||
config
|
config
|
||||||
.property::<usize>("store.db.worker-pool.size")?
|
.property::<usize>("store.db.pool.workers")?
|
||||||
.filter(|v| *v > 0)
|
.filter(|v| *v > 0)
|
||||||
.unwrap_or_else(num_cpus::get),
|
.unwrap_or_else(num_cpus::get),
|
||||||
)
|
)
|
||||||
|
@ -67,7 +67,7 @@ impl Store {
|
||||||
crate::Error::InternalError(format!("Failed to build worker pool: {}", err))
|
crate::Error::InternalError(format!("Failed to build worker pool: {}", err))
|
||||||
})?,
|
})?,
|
||||||
id_assigner: Arc::new(Mutex::new(LruCache::new(
|
id_assigner: Arc::new(Mutex::new(LruCache::new(
|
||||||
config.property_or_static("store.db.id-cache.size", "1000")?,
|
config.property_or_static("store.db.cache.size", "1000")?,
|
||||||
))),
|
))),
|
||||||
blob: BlobStore::new(config).await?,
|
blob: BlobStore::new(config).await?,
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,10 +1,14 @@
|
||||||
|
#############################################
|
||||||
|
# Server configuration
|
||||||
|
#############################################
|
||||||
|
|
||||||
[server]
|
[server]
|
||||||
hostname = "__HOST__"
|
hostname = "__HOST__"
|
||||||
max-connections = 8192
|
max-connections = 8192
|
||||||
|
|
||||||
[server.run-as]
|
[server.run-as]
|
||||||
user = "__RUN_AS_USER__"
|
user = "stalwart-mail"
|
||||||
group = "__RUN_AS_GROUP__"
|
group = "stalwart-mail"
|
||||||
|
|
||||||
[server.tls]
|
[server.tls]
|
||||||
enable = true
|
enable = true
|
||||||
|
@ -49,5 +53,5 @@ rotate = "daily"
|
||||||
level = "info"
|
level = "info"
|
||||||
|
|
||||||
[certificate."default"]
|
[certificate."default"]
|
||||||
cert = "file://__PATH__/etc/certs/tls.crt"
|
cert = "file://__CERT_PATH__"
|
||||||
private-key = "file://__PATH__/etc/private/tls.key"
|
private-key = "file://__PK_PATH__"
|
||||||
|
|
|
@ -1,13 +1,24 @@
|
||||||
|
#############################################
|
||||||
|
# Directory configuration
|
||||||
|
#############################################
|
||||||
|
|
||||||
[directory."sql"]
|
[directory."sql"]
|
||||||
type = "sql"
|
type = "sql"
|
||||||
address = "sqlite::memory:"
|
address = "sqlite://__PATH__/data/accounts.sqlite3?mode=rwc"
|
||||||
|
|
||||||
[directory."sql".options]
|
[directory."sql".options]
|
||||||
catch-all = true
|
catch-all = true
|
||||||
subaddressing = true
|
subaddressing = true
|
||||||
|
superuser-group = "superusers"
|
||||||
|
|
||||||
[directory."sql".pool]
|
[directory."sql".pool]
|
||||||
max-connections = 10
|
max-connections = 10
|
||||||
|
min-connections = 0
|
||||||
|
#idle-timeout = "10m"
|
||||||
|
|
||||||
|
[directory."sql".cache]
|
||||||
|
entries = 500
|
||||||
|
ttl = {positive = '1h', negative = '10m'}
|
||||||
|
|
||||||
[directory."sql".query]
|
[directory."sql".query]
|
||||||
name = "SELECT name, type, secret, description, quota FROM accounts WHERE name = ? AND active = true"
|
name = "SELECT name, type, secret, description, quota FROM accounts WHERE name = ? AND active = true"
|
||||||
|
@ -35,9 +46,21 @@ base-dn = "dc=example,dc=org"
|
||||||
dn = "cn=serviceuser,ou=svcaccts,dc=example,dc=org"
|
dn = "cn=serviceuser,ou=svcaccts,dc=example,dc=org"
|
||||||
secret = "mysecret"
|
secret = "mysecret"
|
||||||
|
|
||||||
|
[directory."ldap".cache]
|
||||||
|
entries = 500
|
||||||
|
ttl = {positive = '1h', negative = '10m'}
|
||||||
|
|
||||||
[directory."ldap".options]
|
[directory."ldap".options]
|
||||||
catch-all = true
|
catch-all = true
|
||||||
subaddressing = true
|
subaddressing = true
|
||||||
|
superuser-group = "superusers"
|
||||||
|
|
||||||
|
[directory."ldap".pool]
|
||||||
|
max-connections = 10
|
||||||
|
min-connections = 0
|
||||||
|
max-lifetime = "30m"
|
||||||
|
idle-timeout = "10m"
|
||||||
|
connect-timeout = "30s"
|
||||||
|
|
||||||
[directory."ldap".filter]
|
[directory."ldap".filter]
|
||||||
name = "(&(|(objectClass=posixAccount)(objectClass=posixGroup))(uid=?))"
|
name = "(&(|(objectClass=posixAccount)(objectClass=posixGroup))(uid=?))"
|
||||||
|
@ -62,40 +85,52 @@ quota = "diskQuota"
|
||||||
[directory."imap"]
|
[directory."imap"]
|
||||||
type = "imap"
|
type = "imap"
|
||||||
address = "127.0.0.1"
|
address = "127.0.0.1"
|
||||||
port = 9198
|
port = 993
|
||||||
|
|
||||||
[directory."imap".pool]
|
[directory."imap".pool]
|
||||||
max-connections = 5
|
max-connections = 10
|
||||||
|
min-connections = 0
|
||||||
|
max-lifetime = "30m"
|
||||||
|
idle-timeout = "10m"
|
||||||
|
connect-timeout = "30s"
|
||||||
|
|
||||||
[directory."imap".tls]
|
[directory."imap".tls]
|
||||||
implicit = true
|
implicit = true
|
||||||
allow-invalid-certs = true
|
allow-invalid-certs = true
|
||||||
|
|
||||||
[directory."imap".lookup]
|
[directory."imap".cache]
|
||||||
domains = ["example.org"]
|
entries = 500
|
||||||
|
ttl = {positive = '1h', negative = '10m'}
|
||||||
|
|
||||||
[directory."smtp"]
|
[directory."imap".lookup]
|
||||||
|
domains = ["__DOMAIN__"]
|
||||||
|
|
||||||
|
[directory."lmtp"]
|
||||||
type = "lmtp"
|
type = "lmtp"
|
||||||
address = "127.0.0.1"
|
address = "127.0.0.1"
|
||||||
port = 9199
|
port = 11200
|
||||||
|
|
||||||
[directory."smtp".limits]
|
[directory."lmtp".limits]
|
||||||
auth-errors = 3
|
auth-errors = 3
|
||||||
rcpt = 5
|
rcpt = 5
|
||||||
|
|
||||||
[directory."smtp".pool]
|
[directory."lmtp".pool]
|
||||||
max-connections = 5
|
max-connections = 10
|
||||||
|
min-connections = 0
|
||||||
|
max-lifetime = "30m"
|
||||||
|
idle-timeout = "10m"
|
||||||
|
connect-timeout = "30s"
|
||||||
|
|
||||||
[directory."smtp".tls]
|
[directory."lmtp".tls]
|
||||||
implicit = true
|
implicit = false
|
||||||
allow-invalid-certs = true
|
allow-invalid-certs = true
|
||||||
|
|
||||||
[directory."smtp".cache]
|
[directory."lmtp".cache]
|
||||||
entries = 500
|
entries = 500
|
||||||
ttl = {positive = '10s', negative = '5s'}
|
ttl = {positive = '1h', negative = '10m'}
|
||||||
|
|
||||||
[directory."smtp".lookup]
|
[directory."lmtp".lookup]
|
||||||
domains = ["example.org"]
|
domains = ["__DOMAIN__"]
|
||||||
|
|
||||||
[directory."memory"]
|
[directory."memory"]
|
||||||
type = "memory"
|
type = "memory"
|
||||||
|
@ -103,29 +138,30 @@ type = "memory"
|
||||||
[directory."memory".options]
|
[directory."memory".options]
|
||||||
catch-all = true
|
catch-all = true
|
||||||
subaddressing = true
|
subaddressing = true
|
||||||
|
superuser-group = "superusers"
|
||||||
|
|
||||||
[[directory."memory".users]]
|
[[directory."memory".users]]
|
||||||
name = "admin"
|
name = "admin"
|
||||||
description = "Superuser"
|
description = "Superuser"
|
||||||
secret = "changeme"
|
secret = "changeme"
|
||||||
email = ["admin@example.org"]
|
email = ["postmaster@__DOMAIN__"]
|
||||||
member-of = ["superusers"]
|
member-of = ["superusers"]
|
||||||
|
|
||||||
[[directory."memory".users]]
|
[[directory."memory".users]]
|
||||||
name = "jane"
|
name = "jane"
|
||||||
description = "Jane Doe"
|
description = "Jane Doe"
|
||||||
secret = "abcde"
|
secret = "abcde"
|
||||||
email = ["jane@example.org", "jane.doe@example.org"]
|
email = ["jane@__DOMAIN__", "jane.doe@__DOMAIN__"]
|
||||||
email-list = ["info@example.org"]
|
email-list = ["info@__DOMAIN__"]
|
||||||
member-of = ["sales", "support"]
|
member-of = ["sales", "support"]
|
||||||
|
|
||||||
[[directory."memory".users]]
|
[[directory."memory".users]]
|
||||||
name = "bill"
|
name = "bill"
|
||||||
description = "Bill Foobar"
|
description = "Bill Foobar"
|
||||||
secret = "$2y$05$bvIG6Nmid91Mu9RcmmWZfO5HJIMCT8riNW0hEp8f6/FuA2/mHZFpe"
|
secret = "$2y$05$bvIG6Nmid91Mu9RcmmWZfO5HJIMCT8riNW0hEp8f6/FuA2/mHZFpe"
|
||||||
quota = 500000
|
quota = 50000000
|
||||||
email = "bill@example.org"
|
email = ["bill@__DOMAIN__", "bill.foobar@__DOMAIN__"]
|
||||||
email-list = ["info@example.org"]
|
email-list = ["info@__DOMAIN__"]
|
||||||
|
|
||||||
[[directory."memory".groups]]
|
[[directory."memory".groups]]
|
||||||
name = "sales"
|
name = "sales"
|
||||||
|
@ -136,4 +172,4 @@ name = "support"
|
||||||
description = "Support Team"
|
description = "Support Team"
|
||||||
|
|
||||||
[directory."memory".lookup]
|
[directory."memory".lookup]
|
||||||
domains = ["example.org"]
|
domains = ["__DOMAIN__"]
|
||||||
|
|
|
@ -1,9 +1,13 @@
|
||||||
|
#############################################
|
||||||
|
# IMAP server configuration
|
||||||
|
#############################################
|
||||||
|
|
||||||
[server.listener."imap"]
|
[server.listener."imap"]
|
||||||
bind = ["0.0.0.0:143"]
|
bind = ["0.0.0.0:143"]
|
||||||
protocol = "imap"
|
protocol = "imap"
|
||||||
|
|
||||||
[server.listener."imaptls"]
|
[server.listener."imaptls"]
|
||||||
bind = ["0.0.0.0:9993"]
|
bind = ["0.0.0.0:993"]
|
||||||
protocol = "imap"
|
protocol = "imap"
|
||||||
tls.implicit = true
|
tls.implicit = true
|
||||||
|
|
||||||
|
|
|
@ -1,16 +1,27 @@
|
||||||
|
#############################################
|
||||||
|
# JMAP server configuration
|
||||||
|
#############################################
|
||||||
|
|
||||||
[server.listener."jmap"]
|
[server.listener."jmap"]
|
||||||
bind = ["0.0.0.0:__BIND_PORT__"]
|
bind = ["0.0.0.0:8080"]
|
||||||
url = "https://127.0.0.1:__BIND_PORT__"
|
url = "https://__HOST__:8080"
|
||||||
protocol = "jmap"
|
protocol = "jmap"
|
||||||
|
|
||||||
[store.db]
|
[store.db]
|
||||||
path = "__PATH__/db"
|
path = "__PATH__/data/index.sqlite3"
|
||||||
|
|
||||||
|
[store.db.pool]
|
||||||
|
max-connections = 10
|
||||||
|
#workers = 8
|
||||||
|
|
||||||
|
[store.db.cache]
|
||||||
|
size = 1000
|
||||||
|
|
||||||
[store.blob]
|
[store.blob]
|
||||||
type = "local"
|
type = "__BLOB_STORE__"
|
||||||
|
|
||||||
[store.blob.local]
|
[store.blob.local]
|
||||||
path = "__PATH__/blobs"
|
path = "__PATH__/data/blobs"
|
||||||
|
|
||||||
[store.blob.s3]
|
[store.blob.s3]
|
||||||
bucket = "stalwart"
|
bucket = "stalwart"
|
||||||
|
@ -23,7 +34,7 @@ secret-key = "minioadmin"
|
||||||
timeout = "30s"
|
timeout = "30s"
|
||||||
|
|
||||||
[jmap]
|
[jmap]
|
||||||
directory = "sql"
|
directory = "__DIRECTORY__"
|
||||||
|
|
||||||
[jmap.session.cache]
|
[jmap.session.cache]
|
||||||
ttl = "1h"
|
ttl = "1h"
|
||||||
|
@ -74,6 +85,9 @@ max-size = 75000000
|
||||||
[jmap.email.parse]
|
[jmap.email.parse]
|
||||||
max-items = 10
|
max-items = 10
|
||||||
|
|
||||||
|
[jmap.principal]
|
||||||
|
allow-lookups = true
|
||||||
|
|
||||||
[jmap.sieve]
|
[jmap.sieve]
|
||||||
disable-capabilities = []
|
disable-capabilities = []
|
||||||
notification-uris = ["mailto"]
|
notification-uris = ["mailto"]
|
||||||
|
|
|
@ -1,3 +1,7 @@
|
||||||
|
#############################################
|
||||||
|
# SMTP server configuration
|
||||||
|
#############################################
|
||||||
|
|
||||||
[server.listener."smtp"]
|
[server.listener."smtp"]
|
||||||
bind = ["0.0.0.0:25"]
|
bind = ["0.0.0.0:25"]
|
||||||
greeting = "Stalwart SMTP at your service"
|
greeting = "Stalwart SMTP at your service"
|
||||||
|
@ -51,7 +55,7 @@ mt-priority = [ { if = "authenticated-as", ne = "", then = "mixer"},
|
||||||
[session.auth]
|
[session.auth]
|
||||||
mechanisms = [ { if = "listener", ne = "smtp", then = ["plain", "login"]},
|
mechanisms = [ { if = "listener", ne = "smtp", then = ["plain", "login"]},
|
||||||
{ else = [] } ]
|
{ else = [] } ]
|
||||||
directory = [ { if = "listener", ne = "smtp", then = "local" },
|
directory = [ { if = "listener", ne = "smtp", then = "__SMTP_DIRECTORY__" },
|
||||||
{ else = false } ]
|
{ else = false } ]
|
||||||
require = [ { if = "listener", ne = "smtp", then = true},
|
require = [ { if = "listener", ne = "smtp", then = true},
|
||||||
{ else = false } ]
|
{ else = false } ]
|
||||||
|
@ -68,7 +72,7 @@ wait = "5s"
|
||||||
relay = [ { if = "authenticated-as", ne = "", then = true },
|
relay = [ { if = "authenticated-as", ne = "", then = true },
|
||||||
{ else = false } ]
|
{ else = false } ]
|
||||||
max-recipients = 25
|
max-recipients = 25
|
||||||
directory = [ { if = "authenticated-as", ne = "", then = "local" },
|
directory = [ { if = "authenticated-as", ne = "", then = "__SMTP_DIRECTORY__" },
|
||||||
{ else = false } ]
|
{ else = false } ]
|
||||||
|
|
||||||
[session.rcpt.cache]
|
[session.rcpt.cache]
|
||||||
|
@ -157,7 +161,7 @@ expire = "5d"
|
||||||
|
|
||||||
[queue.outbound]
|
[queue.outbound]
|
||||||
#hostname = "__HOST__"
|
#hostname = "__HOST__"
|
||||||
next-hop = [ { if = "rcpt-domain", in-list = "local/domains", then = "lmtp" },
|
next-hop = [ { if = "rcpt-domain", in-list = "__SMTP_DIRECTORY__/domains", then = "__NEXT_HOP__" },
|
||||||
{ else = false } ]
|
{ else = false } ]
|
||||||
ip-strategy = "ipv4-then-ipv6"
|
ip-strategy = "ipv4-then-ipv6"
|
||||||
|
|
||||||
|
@ -267,10 +271,10 @@ max-size = 26214400 # 25 mb
|
||||||
sign = ["rsa"]
|
sign = ["rsa"]
|
||||||
|
|
||||||
[signature."rsa"]
|
[signature."rsa"]
|
||||||
#public-key = "file://__PATH__/etc/certs/dkim.crt"
|
#public-key = "file://__PATH__/etc/dkim/__DOMAIN__.cert"
|
||||||
private-key = "file://__PATH__/etc/private/dkim.key"
|
private-key = "file://__PATH__/etc/dkim/__DOMAIN__.key"
|
||||||
domain = "__DOMAIN__"
|
domain = "__DOMAIN__"
|
||||||
selector = "stalwart_smtp"
|
selector = "stalwart"
|
||||||
headers = ["From", "To", "Date", "Subject", "Message-ID"]
|
headers = ["From", "To", "Date", "Subject", "Message-ID"]
|
||||||
algorithm = "rsa-sha256"
|
algorithm = "rsa-sha256"
|
||||||
canonicalization = "relaxed/relaxed"
|
canonicalization = "relaxed/relaxed"
|
||||||
|
@ -282,8 +286,8 @@ set-body-length = false
|
||||||
report = true
|
report = true
|
||||||
|
|
||||||
[remote."lmtp"]
|
[remote."lmtp"]
|
||||||
address = "__LMTP_HOST__"
|
address = "127.0.0.1"
|
||||||
port = __LMTP_PORT__
|
port = 11200
|
||||||
protocol = "lmtp"
|
protocol = "lmtp"
|
||||||
concurrency = 10
|
concurrency = 10
|
||||||
timeout = "1m"
|
timeout = "1m"
|
||||||
|
@ -296,32 +300,13 @@ allow-invalid-certs = true
|
||||||
#username = ""
|
#username = ""
|
||||||
#secret = ""
|
#secret = ""
|
||||||
|
|
||||||
[database."sql"]
|
|
||||||
#address = "sqlite://__PATH__/etc/sqlite.db?mode=rwc"
|
|
||||||
address = "postgres://postgres:password@localhost/test"
|
|
||||||
max-connections = 10
|
|
||||||
min-connections = 0
|
|
||||||
idle-timeout = "5m"
|
|
||||||
|
|
||||||
[database."sql".lookup]
|
|
||||||
auth = "SELECT secret FROM users WHERE email=?"
|
|
||||||
rcpt = "SELECT EXISTS(SELECT 1 FROM users WHERE email=? LIMIT 1)"
|
|
||||||
vrfy = "SELECT email FROM users WHERE email LIKE '%' || ? || '%' LIMIT 5"
|
|
||||||
expn = "SELECT member FROM mailing_lists WHERE id = ?"
|
|
||||||
domains = "SELECT EXISTS(SELECT 1 FROM domains WHERE name=? LIMIT 1)"
|
|
||||||
|
|
||||||
[database."sql".cache]
|
|
||||||
enable = ["rcpt", "domains"]
|
|
||||||
entries = 1000
|
|
||||||
ttl = {positive = "1d", negative = "1h"}
|
|
||||||
|
|
||||||
[sieve]
|
[sieve]
|
||||||
from-name = "Automated Message"
|
from-name = "Automated Message"
|
||||||
from-addr = "no-reply@__DOMAIN__"
|
from-addr = "no-reply@__DOMAIN__"
|
||||||
return-path = ""
|
return-path = ""
|
||||||
#hostname = "__HOST__"
|
#hostname = "__HOST__"
|
||||||
sign = ["rsa"]
|
sign = ["rsa"]
|
||||||
use-directory = "sql"
|
use-directory = "__SMTP_DIRECTORY__"
|
||||||
|
|
||||||
[sieve.limits]
|
[sieve.limits]
|
||||||
redirects = 3
|
redirects = 3
|
||||||
|
@ -337,14 +322,14 @@ duplicate-expiry = "7d"
|
||||||
connect = '''
|
connect = '''
|
||||||
require ["variables", "extlists", "reject"];
|
require ["variables", "extlists", "reject"];
|
||||||
|
|
||||||
if string :list "${env.remote_ip}" "local/blocked-ips" {
|
if string :list "${env.remote_ip}" "__SMTP_DIRECTORY__/blocked-ips" {
|
||||||
reject "Your IP '${env.remote_ip}' is not welcomed here.";
|
reject "Your IP '${env.remote_ip}' is not welcomed here.";
|
||||||
}
|
}
|
||||||
'''
|
'''
|
||||||
ehlo = '''
|
ehlo = '''
|
||||||
require ["variables", "extlists", "reject"];
|
require ["variables", "extlists", "reject"];
|
||||||
|
|
||||||
if string :list "${env.helo_domain}" "local/blocked-domains" {
|
if string :list "${env.helo_domain}" "__SMTP_DIRECTORY__/blocked-domains" {
|
||||||
reject "551 5.1.1 Your domain '${env.helo_domain}' has been blacklisted.";
|
reject "551 5.1.1 Your domain '${env.helo_domain}' has been blacklisted.";
|
||||||
}
|
}
|
||||||
'''
|
'''
|
||||||
|
@ -383,5 +368,4 @@ data = '''
|
||||||
'''
|
'''
|
||||||
|
|
||||||
[management]
|
[management]
|
||||||
directory = "local"
|
directory = "__DIRECTORY__"
|
||||||
|
|
||||||
|
|
|
@ -182,10 +182,6 @@ description = "Sales Team"
|
||||||
name = "support"
|
name = "support"
|
||||||
description = "Support Team"
|
description = "Support Team"
|
||||||
|
|
||||||
[[directory."local".groups]]
|
|
||||||
name = "superusers"
|
|
||||||
description = "Superusers"
|
|
||||||
|
|
||||||
[oauth]
|
[oauth]
|
||||||
key = "parerga_und_paralipomena"
|
key = "parerga_und_paralipomena"
|
||||||
oauth.auth.max-attempts = 1
|
oauth.auth.max-attempts = 1
|
||||||
|
|
|
@ -50,10 +50,14 @@ const DIRECTORY: &str = r#"
|
||||||
[directory."local"]
|
[directory."local"]
|
||||||
type = "memory"
|
type = "memory"
|
||||||
|
|
||||||
|
[directory."local".options]
|
||||||
|
superuser-group = "superusers"
|
||||||
|
|
||||||
[[directory."local".users]]
|
[[directory."local".users]]
|
||||||
name = "admin"
|
name = "admin"
|
||||||
description = "Superuser"
|
description = "Superuser"
|
||||||
secret = "secret"
|
secret = "secret"
|
||||||
|
member-of = ["superusers"]
|
||||||
|
|
||||||
"#;
|
"#;
|
||||||
|
|
||||||
|
|
|
@ -53,10 +53,14 @@ const DIRECTORY: &str = r#"
|
||||||
[directory."local"]
|
[directory."local"]
|
||||||
type = "memory"
|
type = "memory"
|
||||||
|
|
||||||
|
[directory."local".options]
|
||||||
|
superuser-group = "superusers"
|
||||||
|
|
||||||
[[directory."local".users]]
|
[[directory."local".users]]
|
||||||
name = "admin"
|
name = "admin"
|
||||||
description = "Superuser"
|
description = "Superuser"
|
||||||
secret = "secret"
|
secret = "secret"
|
||||||
|
member-of = ["superusers"]
|
||||||
|
|
||||||
"#;
|
"#;
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue