v0.1.2
RustBox CI / rustbox (push) Canceled after 0s

This commit is contained in:
Mateusz Gruszczyński committed 2026-10-31 09:22:42 +01:00
1 parent 698e3c4a1b
commit e3e844ef4f
31 files changed
+287 -75

No files matched your search

+15 -3
View File
@@ -12,10 +12,16 @@ pub fn hash_email(email: &str) -> String {
}
pub fn provision_user_data(data_dir: &str, user_hash: &str) -> Result<PathBuf, String> {
let user_dir = PathBuf::from(data_dir).join(user_hash);
provision_user_data_with_search(data_dir, user_hash, true)
}
let tantivy_dir = user_dir.join("tantivy");
fs::create_dir_all(&tantivy_dir).map_err(|e| format!("failed to create directories: {e}"))?;
pub fn provision_user_data_with_search(
data_dir: &str,
user_hash: &str,
create_search_dir: bool,
) -> Result<PathBuf, String> {
let user_dir = PathBuf::from(data_dir).join(user_hash);
fs::create_dir_all(&user_dir).map_err(|e| format!("failed to create user directory: {e}"))?;
#[cfg(unix)]
{
@@ -26,6 +32,12 @@ pub fn provision_user_data(data_dir: &str, user_hash: &str) -> Result<PathBuf, S
}
}
if create_search_dir {
let tantivy_dir = user_dir.join("tantivy");
fs::create_dir_all(&tantivy_dir)
.map_err(|e| format!("failed to create search directory: {e}"))?;
}
// Ensure this account has its isolated tables in the configured RustBox database.
// The same database backend stores sessions unless session_backend=redis.
let conn = crate::db::pool::open_user_db(data_dir, user_hash)
+59
View File
@@ -46,6 +46,9 @@ pub struct AppConfig {
#[serde(default)]
pub log_dir: Option<String>,
#[serde(default = "default_log_level")]
pub log_level: String,
#[serde(default = "default_database_backend")]
pub database_backend: String,
@@ -76,6 +79,12 @@ pub struct AppConfig {
#[serde(default)]
pub cluster_mode: bool,
#[serde(default)]
pub cluster_shared_data_dir: bool,
#[serde(default = "default_search_index_backend")]
pub search_index_backend: String,
#[serde(default = "default_session_timeout_hours")]
pub session_timeout_hours: u64,
@@ -118,6 +127,9 @@ pub struct AppConfig {
#[serde(default)]
pub outgoing_headers: Vec<String>,
#[serde(default = "default_pgp_enabled")]
pub pgp_enabled: bool,
#[serde(default = "default_pgp_wkd_enabled")]
pub pgp_wkd_enabled: bool,
@@ -199,6 +211,10 @@ fn default_data_dir() -> String {
"/data".to_string()
}
fn default_log_level() -> String {
"info".to_string()
}
fn default_database_backend() -> String {
"sqlite".to_string()
}
@@ -207,6 +223,10 @@ fn default_session_backend() -> String {
"database".to_string()
}
fn default_search_index_backend() -> String {
"tantivy".to_string()
}
fn default_session_timeout_hours() -> u64 {
24
}
@@ -242,6 +262,10 @@ fn default_sieve_timeout_secs() -> u64 {
10
}
fn default_pgp_enabled() -> bool {
true
}
fn default_pgp_wkd_enabled() -> bool {
true
}
@@ -270,6 +294,7 @@ impl Default for AppConfig {
max_message_size_mb: default_max_message_size_mb(),
data_dir: default_data_dir(),
log_dir: None,
log_level: default_log_level(),
database_backend: default_database_backend(),
database_url: None,
session_backend: default_session_backend(),
@@ -280,6 +305,8 @@ impl Default for AppConfig {
redis_password: None,
redis_sentinel_password: None,
cluster_mode: false,
cluster_shared_data_dir: false,
search_index_backend: default_search_index_backend(),
session_timeout_hours: default_session_timeout_hours(),
remember_me_enabled: default_remember_me_enabled(),
remember_me_days: default_remember_me_days(),
@@ -294,6 +321,7 @@ impl Default for AppConfig {
sieve_timeout_secs: default_sieve_timeout_secs(),
trusted_proxies: None,
outgoing_headers: Vec::new(),
pgp_enabled: default_pgp_enabled(),
pgp_wkd_enabled: default_pgp_wkd_enabled(),
pgp_hkp_enabled: false,
pgp_hkp_url: default_pgp_hkp_url(),
@@ -446,6 +474,16 @@ impl AppConfig {
return Err("LOG_DIR / log_dir must be an absolute path".to_string());
}
}
let log_level = self.log_level.trim().to_ascii_lowercase();
if !matches!(
log_level.as_str(),
"trace" | "debug" | "info" | "warn" | "error" | "off"
) {
return Err(
"LOG_LEVEL / log_level must be one of: trace, debug, info, warn, error, off"
.to_string(),
);
}
if self.max_attachment_size_mb == 0 || self.max_attachment_size_mb > 512 {
return Err("MAX_ATTACHMENT_SIZE_MB must be between 1 and 512".to_string());
}
@@ -492,12 +530,33 @@ impl AppConfig {
return Err("Redis sessions require REDIS_URL or REDIS_SENTINEL_NODES + REDIS_SENTINEL_MASTER".to_string());
}
}
if !matches!(self.search_index_backend.as_str(), "tantivy" | "disabled") {
return Err("SEARCH_INDEX_BACKEND must be one of: tantivy, disabled".to_string());
}
if self.cluster_mode && self.database_backend == "sqlite" {
return Err(
"CLUSTER_MODE requires a shared mysql/pgsql application database; Redis only replaces session persistence"
.to_string(),
);
}
if self.cluster_mode && !self.cluster_shared_data_dir {
return Err(
"CLUSTER_MODE requires CLUSTER_SHARED_DATA_DIR=true after data_dir is mounted as shared storage on every node; draft attachments are stored below data_dir"
.to_string(),
);
}
if self.cluster_mode && self.search_index_backend != "disabled" {
return Err(
"CLUSTER_MODE requires SEARCH_INDEX_BACKEND=disabled; the built-in Tantivy index is local and does not coordinate writers across nodes"
.to_string(),
);
}
if self.cluster_mode && self.pgp_enabled {
return Err(
"CLUSTER_MODE requires PGP_ENABLED=false; OpenPGP private keys use node-local filesystem storage"
.to_string(),
);
}
if (self.cluster_mode
|| self.session_backend == "redis"
|| self.database_backend != "sqlite")
+3 -3
View File
@@ -1562,12 +1562,12 @@ fn read_legacy_sqlite(path: &Path) -> Result<Vec<LegacyTableData>, String> {
.map_err(|e| format!("failed to read legacy SQLite schema version: {e}"))?;
if version < BASE_SCHEMA_VERSION {
return Err(format!(
"legacy SQLite schema version {version} is older than the RustBox 0.1.1 baseline {BASE_SCHEMA_VERSION}; upgrade with RustBox 0.1.0 first"
"legacy SQLite schema version {version} is older than the RustBox 0.1.2 baseline {BASE_SCHEMA_VERSION}; upgrade with RustBox 0.1.0 first"
));
}
if version > BASE_SCHEMA_VERSION {
return Err(format!(
"legacy SQLite schema version {version} is newer than the RustBox 0.1.1 baseline {BASE_SCHEMA_VERSION}"
"legacy SQLite schema version {version} is newer than the RustBox 0.1.2 baseline {BASE_SCHEMA_VERSION}"
));
}
let mut output = Vec::new();
@@ -1922,7 +1922,7 @@ fn apply_sqlite_migrations(
) -> Result<(), String> {
if current < BASE_SCHEMA_VERSION {
return Err(format!(
"database schema version {current} is older than the RustBox 0.1.1 baseline {BASE_SCHEMA_VERSION}; upgrade with RustBox 0.1.0 first"
"database schema version {current} is older than the RustBox 0.1.2 baseline {BASE_SCHEMA_VERSION}; upgrade with RustBox 0.1.0 first"
));
}
let latest = latest_schema_version();
+12 -7
View File
@@ -47,7 +47,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
return Ok(());
}
init_tracing(loaded_config.log_dir.as_deref())?;
init_tracing(loaded_config.log_dir.as_deref(), &loaded_config.log_level)?;
loaded_config.validate().map_err(|message| {
std::io::Error::new(
std::io::ErrorKind::InvalidInput,
@@ -86,9 +86,12 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
let imap_client: Arc<dyn imap::client::ImapClient> = Arc::new(RealImapClient::default());
let smtp_client: Arc<dyn smtp::client::SmtpClient> = Arc::new(RealSmtpClient);
let search_engine = Arc::new(search::engine::SearchEngine::new(std::path::PathBuf::from(
&config.data_dir,
)));
let search_base = std::path::PathBuf::from(&config.data_dir);
let search_engine = Arc::new(if config.search_index_backend == "disabled" {
search::engine::SearchEngine::disabled(search_base)
} else {
search::engine::SearchEngine::new(search_base)
});
let event_bus = Arc::new(realtime::events::EventBus::new());
let idle_manager = Arc::new(realtime::idle::IdleManager::new());
@@ -123,7 +126,9 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
Ok(())
}
fn init_tracing(log_dir: Option<&str>) -> Result<(), Box<dyn std::error::Error>> {
fn init_tracing(log_dir: Option<&str>, log_level: &str) -> Result<(), Box<dyn std::error::Error>> {
let env_filter = EnvFilter::try_from_default_env()
.unwrap_or_else(|_| EnvFilter::new(log_level.trim().to_ascii_lowercase()));
if let Some(log_dir) = log_dir.map(str::trim).filter(|value| !value.is_empty()) {
std::fs::create_dir_all(log_dir)?;
let log_path = std::path::Path::new(log_dir).join("rustbox.log");
@@ -136,7 +141,7 @@ fn init_tracing(log_dir: Option<&str>) -> Result<(), Box<dyn std::error::Error>>
}
let file = options.open(log_path)?;
tracing_subscriber::registry()
.with(EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")))
.with(env_filter)
.with(
fmt::layer()
.json()
@@ -146,7 +151,7 @@ fn init_tracing(log_dir: Option<&str>) -> Result<(), Box<dyn std::error::Error>>
.init();
} else {
tracing_subscriber::registry()
.with(EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")))
.with(env_filter)
.with(fmt::layer().json().with_ansi(false))
.init();
}
+1 -1
View File
@@ -126,7 +126,7 @@ async fn https_get_address(
format!("{host}:{port}")
};
let request = format!(
"GET {target} HTTP/1.1\r\nHost: {host_header}\r\nUser-Agent: RustBox/0.1.1\r\nAccept: application/octet-stream, application/pgp-keys, text/plain\r\nConnection: close\r\n\r\n"
"GET {target} HTTP/1.1\r\nHost: {host_header}\r\nUser-Agent: RustBox/0.1.2\r\nAccept: application/octet-stream, application/pgp-keys, text/plain\r\nConnection: close\r\n\r\n"
);
timeout(duration, stream.write_all(request.as_bytes()))
.await
+14 -4
View File
@@ -178,10 +178,14 @@ pub mod db_folder_preferences {
}
pub mod db_folders {
pub const Q_001: &str = "REPLACE INTO folders
pub const Q_001: &str = "INSERT INTO folders
(name, delimiter, parent, flags, is_subscribed,
total_count, unread_count, uid_validity, highest_modseq, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)";
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
ON DUPLICATE KEY UPDATE
delimiter=VALUES(delimiter), parent=VALUES(parent), flags=VALUES(flags),
is_subscribed=VALUES(is_subscribed), total_count=VALUES(total_count), unread_count=VALUES(unread_count),
uid_validity=VALUES(uid_validity), highest_modseq=VALUES(highest_modseq), updated_at=CURRENT_TIMESTAMP";
pub const Q_002: &str = "INSERT IGNORE INTO folders
(name, delimiter, flags, is_subscribed, updated_at)
VALUES (?, ?, ?, 1, CURRENT_TIMESTAMP)";
@@ -262,11 +266,17 @@ pub mod db_mail_preferences {
pub mod db_messages {
pub const MSG_SELECT_COLS: &str = "uid, folder, message_id, in_reply_to, references_header, subject, from_address, from_name, to_addresses, cc_addresses, date, flags, size, has_attachments, snippet, reaction";
pub const Q_001: &str = "REPLACE INTO messages
pub const Q_001: &str = "INSERT INTO messages
(uid, folder, message_id, in_reply_to, references_header,
subject, from_address, from_name, to_addresses, cc_addresses,
date, flags, size, has_attachments, snippet, date_epoch, reaction)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
message_id=VALUES(message_id), in_reply_to=VALUES(in_reply_to), references_header=VALUES(references_header),
subject=VALUES(subject), from_address=VALUES(from_address), from_name=VALUES(from_name),
to_addresses=VALUES(to_addresses), cc_addresses=VALUES(cc_addresses), date=VALUES(date), flags=VALUES(flags),
size=VALUES(size), has_attachments=VALUES(has_attachments), snippet=VALUES(snippet),
date_epoch=VALUES(date_epoch), reaction=VALUES(reaction)";
pub const Q_002: &str = "DELETE FROM messages WHERE folder = ?";
pub const Q_003: &str = "SELECT COALESCE(MAX(uid), 0) FROM messages WHERE folder = ?";
pub const Q_004: &str = "SELECT uid, folder, subject, from_address, from_name, to_addresses, \
+3 -1
View File
@@ -154,7 +154,9 @@ async fn run_sync(
event_bus.publish(user_hash, MailEvent::FolderUpdated).await;
}
if let Err(e) = index_message_bodies(user_hash, creds, config, imap_client, search_engine).await
if search_engine.is_enabled()
&& let Err(e) =
index_message_bodies(user_hash, creds, config, imap_client, search_engine).await
{
tracing::debug!(error = %e, "Deep index phase skipped or failed");
}
+6 -1
View File
@@ -237,7 +237,12 @@ pub async fn login(
}
let user_hash = user_data::hash_email(&body.email);
if let Err(e) = user_data::provision_user_data(&config.data_dir, &user_hash) {
let provision_result = if config.search_index_backend == "tantivy" {
user_data::provision_user_data(&config.data_dir, &user_hash)
} else {
user_data::provision_user_data_with_search(&config.data_dir, &user_hash, false)
};
if let Err(e) = provision_result {
tracing::error!(error = %e, "failed to provision user data directory");
return (
StatusCode::INTERNAL_SERVER_ERROR,
+1 -1
View File
@@ -174,7 +174,7 @@ pub async fn export_ics(
.map_err(|e| AppError::InternalError(format!("Database error: {e}")))?;
let mut body = String::from(
"BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//RustBox//Calendar 0.1.1//EN\r\nCALSCALE:GREGORIAN\r\n",
"BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//RustBox//Calendar 0.1.2//EN\r\nCALSCALE:GREGORIAN\r\n",
);
for event in &events {
append_ics_event(&mut body, event);
+10 -1
View File
@@ -269,7 +269,11 @@ pub async fn search_diagnostics(
let cached_bodies: usize = conn
.query_row(crate::dbq!(routes_diagnostics::Q_003), [], |row| row.get(0))
.map_err(|e| format!("Failed to count cached bodies: {e}"))?;
let indexed_documents = engine.open_user_index(&user_hash)?.document_count();
let indexed_documents = if engine.is_enabled() {
engine.open_user_index(&user_hash)?.document_count()
} else {
0
};
Ok(SearchDiagnosticsResponse {
indexed_documents,
@@ -290,6 +294,11 @@ pub async fn rebuild_search_index(
Extension(config): Extension<Arc<AppConfig>>,
Extension(search_engine): Extension<Arc<SearchEngine>>,
) -> Result<Response, AppError> {
if !search_engine.is_enabled() {
return Err(AppError::ServiceUnavailable(
"Search index rebuild is unavailable because SEARCH_INDEX_BACKEND=disabled".to_string(),
));
}
let data_dir = config.data_dir.clone();
let user_hash = session.user_hash.clone();
let engine = search_engine.clone();
+25
View File
@@ -14,6 +14,16 @@ use crate::db;
use crate::db::pgp::{PgpKeyRecord, PgpPreferences};
use crate::error::AppError;
fn ensure_pgp_enabled(config: &AppConfig) -> Result<(), AppError> {
if config.pgp_enabled {
Ok(())
} else {
Err(AppError::ServiceUnavailable(
"OpenPGP is disabled by administrator policy".to_string(),
))
}
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GenerateRequest {
@@ -137,6 +147,7 @@ pub async fn process_message(
Path((folder, uid)): Path<(String, u32)>,
Json(request): Json<ProcessMessageRequest>,
) -> Result<Json<crate::pgp::ProcessedMessage>, AppError> {
ensure_pgp_enabled(&config)?;
if request.passphrase.len() > 1024 {
return Err(AppError::BadRequest(
"OpenPGP passphrase is too long".to_string(),
@@ -240,6 +251,7 @@ pub async fn list_keys(
Extension(session): Extension<SessionState>,
Extension(config): Extension<Arc<AppConfig>>,
) -> Result<Json<Vec<PgpKeyRecord>>, AppError> {
ensure_pgp_enabled(&config)?;
let database = db::pool::UserDb::new(&config.data_dir, &session.user_hash);
let keys = database
.run(db::pgp::list_keys)
@@ -253,6 +265,7 @@ pub async fn generate_key(
Extension(config): Extension<Arc<AppConfig>>,
Json(request): Json<GenerateRequest>,
) -> Result<Json<PgpKeyRecord>, AppError> {
ensure_pgp_enabled(&config)?;
validate_email(&request.email)?;
if request.passphrase.len() > 1024 {
return Err(AppError::BadRequest(
@@ -275,6 +288,7 @@ pub async fn import_key(
Extension(config): Extension<Arc<AppConfig>>,
Json(request): Json<ImportRequest>,
) -> Result<Json<PgpKeyRecord>, AppError> {
ensure_pgp_enabled(&config)?;
validate_trust(&request.trust)?;
let input = match (request.key_data, request.key_data_base64) {
(Some(text), None) => {
@@ -330,6 +344,7 @@ pub async fn export_key(
Path(fingerprint): Path<String>,
Query(query): Query<ExportQuery>,
) -> Result<Response, AppError> {
ensure_pgp_enabled(&config)?;
let database = db::pool::UserDb::new(&config.data_dir, &session.user_hash);
let normalized = db::pgp::normalize_fingerprint(&fingerprint);
let key = database
@@ -381,6 +396,7 @@ pub async fn test_passphrase(
Path(fingerprint): Path<String>,
Json(request): Json<TestPassphraseRequest>,
) -> Result<Json<TestPassphraseResponse>, AppError> {
ensure_pgp_enabled(&config)?;
if request.passphrase.len() > 1024 {
return Err(AppError::BadRequest(
"OpenPGP passphrase is too long".to_string(),
@@ -417,6 +433,7 @@ pub async fn change_passphrase(
Path(fingerprint): Path<String>,
Json(request): Json<ChangePassphraseRequest>,
) -> Result<Json<TestPassphraseResponse>, AppError> {
ensure_pgp_enabled(&config)?;
if request.current_passphrase.len() > 1024 || request.new_passphrase.len() > 1024 {
return Err(AppError::BadRequest(
"OpenPGP passphrase is too long".to_string(),
@@ -466,6 +483,7 @@ pub async fn delete_key(
Extension(config): Extension<Arc<AppConfig>>,
Path(fingerprint): Path<String>,
) -> Result<Response, AppError> {
ensure_pgp_enabled(&config)?;
let database = db::pool::UserDb::new(&config.data_dir, &session.user_hash);
let normalized = db::pgp::normalize_fingerprint(&fingerprint);
let lookup = normalized.clone();
@@ -505,6 +523,7 @@ pub async fn set_trust(
Path(fingerprint): Path<String>,
Json(request): Json<TrustRequest>,
) -> Result<Json<PgpKeyRecord>, AppError> {
ensure_pgp_enabled(&config)?;
validate_trust(&request.trust)?;
let database = db::pool::UserDb::new(&config.data_dir, &session.user_hash);
let normalized = db::pgp::normalize_fingerprint(&fingerprint);
@@ -529,6 +548,7 @@ pub async fn get_preferences(
Extension(session): Extension<SessionState>,
Extension(config): Extension<Arc<AppConfig>>,
) -> Result<Json<PgpPreferences>, AppError> {
ensure_pgp_enabled(&config)?;
let database = db::pool::UserDb::new(&config.data_dir, &session.user_hash);
let prefs = database
.run(db::pgp::get_preferences)
@@ -542,6 +562,7 @@ pub async fn update_preferences(
Extension(config): Extension<Arc<AppConfig>>,
Json(mut prefs): Json<PgpPreferences>,
) -> Result<Json<PgpPreferences>, AppError> {
ensure_pgp_enabled(&config)?;
if !config.pgp_wkd_enabled {
prefs.wkd_enabled = false;
}
@@ -561,6 +582,7 @@ pub async fn discover_key(
Extension(config): Extension<Arc<AppConfig>>,
Json(request): Json<DiscoverRequest>,
) -> Result<Json<DiscoverResponse>, AppError> {
ensure_pgp_enabled(&config)?;
validate_email(&request.email)?;
let email = request.email.trim().to_ascii_lowercase();
let database = db::pool::UserDb::new(&config.data_dir, &session.user_hash);
@@ -635,6 +657,7 @@ pub async fn recipient_status(
Extension(config): Extension<Arc<AppConfig>>,
Json(request): Json<RecipientsRequest>,
) -> Result<Json<Vec<RecipientStatus>>, AppError> {
ensure_pgp_enabled(&config)?;
if request.emails.len() > 200 {
return Err(AppError::BadRequest(
"Too many OpenPGP recipients".to_string(),
@@ -682,6 +705,7 @@ pub async fn verify(
Extension(config): Extension<Arc<AppConfig>>,
Json(request): Json<VerifyRequest>,
) -> Result<Json<VerifyResponse>, AppError> {
ensure_pgp_enabled(&config)?;
let database = db::pool::UserDb::new(&config.data_dir, &session.user_hash);
let fingerprint = db::pgp::normalize_fingerprint(&request.fingerprint);
let lookup = fingerprint.clone();
@@ -719,6 +743,7 @@ pub async fn decrypt(
Extension(config): Extension<Arc<AppConfig>>,
Json(request): Json<DecryptRequest>,
) -> Result<Json<crate::pgp::DecryptResult>, AppError> {
ensure_pgp_enabled(&config)?;
if request.passphrase.len() > 1024 {
return Err(AppError::BadRequest(
"OpenPGP passphrase is too long".to_string(),
+7
View File
@@ -183,6 +183,13 @@ pub async fn send_message_handler(
}
let pgp_mode = resolve_pgp_mode(req.pgp_mode.as_deref(), selected_identity.as_ref())?;
if !config.pgp_enabled
&& (pgp_mode != PgpMode::None || req.pgp_attach_public_key || req.pgp_attach_recipient_keys)
{
return Err(AppError::ServiceUnavailable(
"OpenPGP is disabled by administrator policy".to_string(),
));
}
let pgp_inline = req.pgp_inline.unwrap_or_else(|| {
selected_identity
.as_ref()
+1 -1
View File
@@ -228,7 +228,7 @@ async fn post_one_click_to_address(
format!("{host}:{port}")
};
let request = format!(
"POST {path} HTTP/1.1\r\nHost: {host_header}\r\nUser-Agent: RustBox/0.1.1\r\nContent-Type: application/x-www-form-urlencoded\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{ONE_CLICK_BODY}",
"POST {path} HTTP/1.1\r\nHost: {host_header}\r\nUser-Agent: RustBox/0.1.2\r\nContent-Type: application/x-www-form-urlencoded\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{ONE_CLICK_BODY}",
ONE_CLICK_BODY.len()
);
+19 -1
View File
@@ -47,6 +47,7 @@ pub struct SearchQuery {
pub struct SearchEngine {
base_dir: PathBuf,
enabled: bool,
}
struct SchemaFields {
@@ -108,10 +109,27 @@ fn build_schema() -> (Schema, SchemaFields) {
impl SearchEngine {
pub fn new(base_dir: PathBuf) -> Self {
Self { base_dir }
Self {
base_dir,
enabled: true,
}
}
pub fn disabled(base_dir: PathBuf) -> Self {
Self {
base_dir,
enabled: false,
}
}
pub fn is_enabled(&self) -> bool {
self.enabled
}
pub fn open_user_index(&self, user_hash: &str) -> Result<UserIndex, String> {
if !self.enabled {
return Err("Tantivy search index is disabled by configuration".to_string());
}
let index_dir = self.base_dir.join(user_hash).join("tantivy");
std::fs::create_dir_all(&index_dir)
.map_err(|e| format!("Failed to create index directory: {e}"))?;
+1 -1
View File
@@ -29,7 +29,7 @@ impl fmt::Display for SieveError {
write!(f, "ManageSieve server rejected the command: {msg}")
}
Self::Timeout => write!(f, "ManageSieve operation timed out"),
Self::ScriptTooLarge => write!(f, "Sieve script exceeds the RustBox 0.1.1 size limit"),
Self::ScriptTooLarge => write!(f, "Sieve script exceeds the RustBox 0.1.2 size limit"),
}
}
}