Skip to main content

stalwart_lib/common/src/config/
mod.rs

1/*
2 * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
3 *
4 * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
5 */
6
7use self::{
8    imap::ImapConfig, jmap::settings::JmapConfig, scripts::Scripting, smtp::SmtpConfig,
9    storage::Storage,
10};
11use crate::common::GroupwareConfig;
12use crate::common::{
13    Core, Network, Security, auth::oauth::config::OAuthConfig, expr::*,
14    listener::tls::AcmeProviders, manager::config::ConfigManager,
15};
16use crate::directory::{Directories, Directory};
17use crate::store::{BlobBackend, BlobStore, InMemoryStore, SearchStore, Store, Stores};
18use crate::utils::config::{Config, utils::AsKey};
19use arc_swap::ArcSwap;
20use hyper::HeaderMap;
21use ring::signature::{EcdsaKeyPair, RsaKeyPair};
22use spamfilter::SpamFilterConfig;
23use std::sync::Arc;
24use telemetry::Metrics;
25
26pub mod groupware;
27pub mod imap;
28pub mod inner;
29pub mod jmap;
30pub mod network;
31pub mod scripts;
32pub mod server;
33pub mod smtp;
34pub mod spamfilter;
35pub mod storage;
36pub mod telemetry;
37
38pub(crate) const CONNECTION_VARS: &[u32; 9] = &[
39    V_LISTENER,
40    V_REMOTE_IP,
41    V_REMOTE_PORT,
42    V_LOCAL_IP,
43    V_LOCAL_PORT,
44    V_PROTOCOL,
45    V_TLS,
46    V_ASN,
47    V_COUNTRY,
48];
49
50impl Core {
51    pub async fn parse(
52        config: &mut Config,
53        mut stores: Stores,
54        config_manager: ConfigManager,
55    ) -> Self {
56        let mut data = config
57            .value_require("storage.data")
58            .map(|id| id.to_string())
59            .and_then(|id| {
60                if let Some(store) = stores.stores.get(&id) {
61                    store.clone().into()
62                } else {
63                    config.new_parse_error("storage.data", format!("Data store {id:?} not found"));
64                    None
65                }
66            })
67            .unwrap_or_default();
68
69        #[cfg(not(feature = "enterprise"))]
70        let is_enterprise = false;
71
72        // SPDX-SnippetBegin
73        // SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
74        // SPDX-License-Identifier: LicenseRef-SEL
75        #[cfg(feature = "enterprise")]
76        let enterprise =
77            crate::common::enterprise::Enterprise::parse(config, &config_manager, &stores, &data)
78                .await;
79
80        #[cfg(feature = "enterprise")]
81        let is_enterprise = enterprise.is_some();
82
83        #[cfg(feature = "enterprise")]
84        if !is_enterprise {
85            if data.is_enterprise_store() {
86                config
87                    .new_build_error("storage.data", "SQL read replicas is an Enterprise feature");
88                data = Store::None;
89            }
90            stores.disable_enterprise_only();
91        }
92        // SPDX-SnippetEnd
93
94        let mut blob = config
95            .value_require("storage.blob")
96            .map(|id| id.to_string())
97            .and_then(|id| {
98                if let Some(store) = stores.blob_stores.get(&id) {
99                    store.clone().into()
100                } else {
101                    config.new_parse_error("storage.blob", format!("Blob store {id:?} not found"));
102                    None
103                }
104            })
105            .unwrap_or_default();
106        let mut lookup = config
107            .value_require("storage.lookup")
108            .map(|id| id.to_string())
109            .and_then(|id| {
110                if let Some(store) = stores.in_memory_stores.get(&id) {
111                    store.clone().into()
112                } else {
113                    config.new_parse_error(
114                        "storage.lookup",
115                        format!("In-memory store {id:?} not found"),
116                    );
117                    None
118                }
119            })
120            .unwrap_or_default();
121        let mut fts = config
122            .value_require("storage.fts")
123            .map(|id| id.to_string())
124            .and_then(|id| {
125                if let Some(store) = stores.search_stores.get(&id) {
126                    store.clone().into()
127                } else {
128                    config.new_parse_error(
129                        "storage.fts",
130                        format!("Full-text store {id:?} not found"),
131                    );
132                    None
133                }
134            })
135            .unwrap_or_default();
136        let pubsub = config
137            .value("cluster.coordinator")
138            .map(|id| id.to_string())
139            .and_then(|id| {
140                if let Some(store) = stores.pubsub_stores.get(&id) {
141                    store.clone().into()
142                } else {
143                    config.new_parse_error(
144                        "cluster.coordinator",
145                        format!("Coordinator backend {id:?} not found"),
146                    );
147                    None
148                }
149            })
150            .unwrap_or_default();
151        let mut directories =
152            Directories::parse(config, &stores, data.clone(), is_enterprise).await;
153        let directory = config
154            .value_require("storage.directory")
155            .map(|id| id.to_string())
156            .and_then(|id| {
157                if let Some(directory) = directories.directories.get(&id) {
158                    directory.clone().into()
159                } else {
160                    config.new_parse_error(
161                        "storage.directory",
162                        format!("Directory {id:?} not found"),
163                    );
164                    None
165                }
166            })
167            .unwrap_or_else(|| Arc::new(Directory::default()));
168        directories
169            .directories
170            .insert("*".to_string(), directory.clone());
171
172        // If any of the stores are missing, disable all stores to avoid data loss
173        if matches!(data, Store::None)
174            || matches!(&blob.backend, BlobBackend::Store(Store::None))
175            || matches!(lookup, InMemoryStore::Store(Store::None))
176            || matches!(fts, SearchStore::Store(Store::None))
177        {
178            data = Store::default();
179            blob = BlobStore::default();
180            lookup = InMemoryStore::default();
181            fts = SearchStore::default();
182            config.new_build_error(
183                "storage.*",
184                "One or more stores are missing, disabling all stores",
185            )
186        }
187
188        let groupware = GroupwareConfig::parse(config);
189        Self {
190            // SPDX-SnippetBegin
191            // SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
192            // SPDX-License-Identifier: LicenseRef-SEL
193            #[cfg(feature = "enterprise")]
194            enterprise,
195            // SPDX-SnippetEnd
196            sieve: Scripting::parse(config, &stores).await,
197            network: Network::parse(config),
198            smtp: SmtpConfig::parse(config).await,
199            jmap: JmapConfig::parse(config, &groupware),
200            imap: ImapConfig::parse(config),
201            oauth: OAuthConfig::parse(config),
202            acme: AcmeProviders::parse(config),
203            metrics: Metrics::parse(config),
204            spam: SpamFilterConfig::parse(config).await,
205            groupware,
206            storage: Storage {
207                data,
208                blob,
209                fts,
210                lookup,
211                pubsub,
212                directory,
213                directories: directories.directories,
214                purge_schedules: stores.purge_schedules,
215                config: config_manager,
216                stores: stores.stores,
217                lookups: stores.in_memory_stores,
218                blobs: stores.blob_stores,
219                ftss: stores.search_stores,
220            },
221        }
222    }
223
224    pub fn into_shared(self) -> ArcSwap<Self> {
225        ArcSwap::from_pointee(self)
226    }
227}
228
229pub fn build_rsa_keypair(pem: &str) -> Result<RsaKeyPair, String> {
230    match rustls_pemfile::read_one(&mut pem.as_bytes()) {
231        Ok(Some(rustls_pemfile::Item::Pkcs1Key(key))) => {
232            RsaKeyPair::from_der(key.secret_pkcs1_der())
233                .map_err(|err| format!("Failed to parse PKCS1 RSA key: {err}"))
234        }
235        Ok(Some(rustls_pemfile::Item::Pkcs8Key(key))) => {
236            RsaKeyPair::from_pkcs8(key.secret_pkcs8_der())
237                .map_err(|err| format!("Failed to parse PKCS8 RSA key: {err}"))
238        }
239        Err(err) => Err(format!("Failed to read PEM: {err}")),
240        Ok(Some(key)) => Err(format!("Unsupported key type: {key:?}")),
241        Ok(None) => Err("No RSA key found in PEM".to_string()),
242    }
243}
244
245pub fn build_ecdsa_pem(
246    alg: &'static ring::signature::EcdsaSigningAlgorithm,
247    pem: &str,
248) -> Result<EcdsaKeyPair, String> {
249    match rustls_pemfile::read_one(&mut pem.as_bytes()) {
250        Ok(Some(rustls_pemfile::Item::Pkcs8Key(key))) => EcdsaKeyPair::from_pkcs8(
251            alg,
252            key.secret_pkcs8_der(),
253            &ring::rand::SystemRandom::new(),
254        )
255        .map_err(|err| format!("Failed to parse PKCS8 ECDSA key: {err}")),
256        Err(err) => Err(format!("Failed to read PEM: {err}")),
257        Ok(Some(key)) => Err(format!("Unsupported key type: {key:?}")),
258        Ok(None) => Err("No ECDSA key found in PEM".to_string()),
259    }
260}