/* * Copyright (c) 2023 Stalwart Labs Ltd. * * This file is part of the 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 . * * 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::sync::Arc; use async_trait::async_trait; use utils::config::{cron::SimpleCron, Config}; use crate::{ backend::{fs::FsStore, memory::MemoryStore}, write::purge::{PurgeSchedule, PurgeStore}, LookupStore, QueryStore, Store, Stores, }; #[cfg(feature = "s3")] use crate::backend::s3::S3Store; #[cfg(feature = "postgres")] use crate::backend::postgres::PostgresStore; #[cfg(feature = "mysql")] use crate::backend::mysql::MysqlStore; #[cfg(feature = "sqlite")] use crate::backend::sqlite::SqliteStore; #[cfg(feature = "foundation")] use crate::backend::foundationdb::FdbStore; #[cfg(feature = "rocks")] use crate::backend::rocksdb::RocksDbStore; #[cfg(feature = "elastic")] use crate::backend::elastic::ElasticSearchStore; #[cfg(feature = "redis")] use crate::backend::redis::RedisStore; #[async_trait] pub trait ConfigStore { async fn parse_stores(&self) -> utils::config::Result; async fn parse_purge_schedules( &self, stores: &Stores, store: Option<&str>, blob_store: Option<&str>, ) -> utils::config::Result>; } #[async_trait] impl ConfigStore for Config { #[allow(unused_variables)] #[allow(unreachable_code)] async fn parse_stores(&self) -> utils::config::Result { let mut config = Stores::default(); for id in self.sub_keys("store") { // Parse store if self.property_or_static::(("store", id, "disable"), "false")? { tracing::debug!("Skipping disabled store {id:?}."); continue; } let protocol = self .value_require(("store", id, "type"))? .to_ascii_lowercase(); let prefix = ("store", id); let store_id = id.to_string(); let lookup_store: Store = match protocol.as_str() { #[cfg(feature = "rocks")] "rocksdb" => { let db: Store = RocksDbStore::open(self, prefix).await?.into(); config.stores.insert(store_id.clone(), db.clone()); config .fts_stores .insert(store_id.clone(), db.clone().into()); config .blob_stores .insert(store_id.clone(), db.clone().into()); config.lookup_stores.insert(store_id, db.into()); continue; } #[cfg(feature = "foundation")] "foundationdb" => { let db: Store = FdbStore::open(self, prefix).await?.into(); config.stores.insert(store_id.clone(), db.clone()); config .fts_stores .insert(store_id.clone(), db.clone().into()); config .blob_stores .insert(store_id.clone(), db.clone().into()); config.lookup_stores.insert(store_id, db.into()); continue; } #[cfg(feature = "postgres")] "postgresql" => { let db: Store = PostgresStore::open(self, prefix).await?.into(); config.stores.insert(store_id.clone(), db.clone()); config .fts_stores .insert(store_id.clone(), db.clone().into()); config .blob_stores .insert(store_id.clone(), db.clone().into()); db } #[cfg(feature = "mysql")] "mysql" => { let db: Store = MysqlStore::open(self, prefix).await?.into(); config.stores.insert(store_id.clone(), db.clone()); config .fts_stores .insert(store_id.clone(), db.clone().into()); config .blob_stores .insert(store_id.clone(), db.clone().into()); db } #[cfg(feature = "sqlite")] "sqlite" => { let db: Store = SqliteStore::open(self, prefix).await?.into(); config.stores.insert(store_id.clone(), db.clone()); config .fts_stores .insert(store_id.clone(), db.clone().into()); config .blob_stores .insert(store_id.clone(), db.clone().into()); db } "fs" => { config .blob_stores .insert(store_id, FsStore::open(self, prefix).await?.into()); continue; } #[cfg(feature = "s3")] "s3" => { config .blob_stores .insert(store_id, S3Store::open(self, prefix).await?.into()); continue; } #[cfg(feature = "elastic")] "elasticsearch" => { config.fts_stores.insert( store_id, ElasticSearchStore::open(self, prefix).await?.into(), ); continue; } #[cfg(feature = "redis")] "redis" => { config .lookup_stores .insert(store_id, RedisStore::open(self, prefix).await?.into()); continue; } "memory" => { config .lookup_stores .insert(store_id, MemoryStore::open(self, prefix).await?.into()); continue; } unknown => { tracing::debug!("Unknown directory type: {unknown:?}"); continue; } }; // Add queries as lookup stores let lookup_store: LookupStore = lookup_store.into(); for lookup_id in self.sub_keys(("store", id, "query")) { config.lookup_stores.insert( format!("{store_id}/{lookup_id}"), LookupStore::Query(Arc::new(QueryStore { store: lookup_store.clone(), query: self.property_require(("store", id, "query", lookup_id))?, })), ); } config.lookup_stores.insert(store_id, lookup_store.clone()); // Run init queries on database for (_, query) in self.values(("store", id, "init.execute")) { if let Err(err) = lookup_store.query::(query, Vec::new()).await { tracing::warn!("Failed to initialize store {id:?}: {err}"); } } } Ok(config) } async fn parse_purge_schedules( &self, stores: &Stores, store_id: Option<&str>, blob_store_id: Option<&str>, ) -> utils::config::Result> { let mut schedules = Vec::new(); if let Some(store) = store_id.and_then(|store_id| stores.stores.get(store_id)) { let store_id = store_id.unwrap(); if let Some(cron) = self.property::(("store", store_id, "purge.frequency"))? { schedules.push(PurgeSchedule { cron, store_id: store_id.to_string(), store: PurgeStore::Bitmaps(store.clone()), }); } if let Some(blob_store) = blob_store_id.and_then(|blob_store_id| stores.blob_stores.get(blob_store_id)) { let blob_store_id = blob_store_id.unwrap(); if let Some(cron) = self.property::(("store", blob_store_id, "purge.frequency"))? { schedules.push(PurgeSchedule { cron, store_id: blob_store_id.to_string(), store: PurgeStore::Blobs { store: store.clone(), blob_store: blob_store.clone(), }, }); } } } for (store_id, store) in &stores.lookup_stores { if let Some(cron) = self.property::(("store", store_id.as_str(), "purge.frequency"))? { schedules.push(PurgeSchedule { cron, store_id: store_id.clone(), store: PurgeStore::Lookup(store.clone()), }); } } Ok(schedules) } } impl From for String { fn from(err: crate::Error) -> Self { match err { crate::Error::InternalError(err) => err, crate::Error::AssertValueFailed => unimplemented!(), } } }