1 parent
698e3c4a1b
commit
e3e844ef4f
31 files changed
+287
-75
No files matched your search
+15
-3
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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, \
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
@@ -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
@@ -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"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user