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

This commit is contained in:
Mateusz Gruszczyński committed 2026-10-31 00:37:16 +01:00
1 parent ecc201ef50
commit 698e3c4a1b
124 files changed
+4593 -2721

No files matched your search

+9 -15
View File
@@ -26,9 +26,8 @@ pub fn provision_user_data(data_dir: &str, user_hash: &str) -> Result<PathBuf, S
}
}
// User data is always SQLite and must only receive the user-schema
// migrations from db::pool. Session migrations are handled separately by
// session_store for the configured session database backend.
// 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)
.map_err(|e| format!("failed to initialize user database: {e}"))?;
drop(conn);
@@ -75,8 +74,8 @@ mod tests {
assert!(user_dir.exists(), "user directory should exist");
assert!(
user_dir.join("db.sqlite").exists(),
"db.sqlite should exist"
tmp.path().join("rustbox.sqlite").exists(),
"global rustbox.sqlite should exist for the default SQLite backend"
);
assert!(
user_dir.join("tantivy").is_dir(),
@@ -94,7 +93,7 @@ mod tests {
let dir2 = provision_user_data(data_dir, &user_hash).expect("second call failed");
assert_eq!(dir1, dir2);
assert!(dir2.join("db.sqlite").exists());
assert!(tmp.path().join("rustbox.sqlite").exists());
assert!(dir2.join("tantivy").is_dir());
}
@@ -104,17 +103,12 @@ mod tests {
let data_dir = tmp.path().to_str().unwrap();
let user_hash = hash_email("meta@example.com");
let user_dir = provision_user_data(data_dir, &user_hash).expect("provisioning failed");
let conn =
rusqlite::Connection::open(user_dir.join("db.sqlite")).expect("failed to open db");
provision_user_data(data_dir, &user_hash).expect("provisioning failed");
let conn = crate::db::pool::open_user_db(data_dir, &user_hash)
.expect("failed to open account database");
let value: String = conn
.query_row(
crate::queries::sqlite::queries::auth_user_data::Q_001,
[],
|row| row.get(0),
)
.query_row(crate::dbq!(auth_user_data::Q_001), [], |row| row.get(0))
.expect("failed to query user_meta");
assert_eq!(value, "1");
+19 -5
View File
@@ -492,11 +492,11 @@ impl AppConfig {
return Err("Redis sessions require REDIS_URL or REDIS_SENTINEL_NODES + REDIS_SENTINEL_MASTER".to_string());
}
}
if self.cluster_mode
&& self.session_backend == "database"
&& self.database_backend == "sqlite"
{
return Err("CLUSTER_MODE with database sessions requires mysql/pgsql, or configure SESSION_BACKEND=redis".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.session_backend == "redis"
@@ -809,4 +809,18 @@ mod tests {
vec![TrustedProxy::Ip("127.0.0.1".parse().unwrap())]
);
}
#[test]
fn cluster_mode_rejects_local_sqlite_even_with_redis_sessions() {
let config = AppConfig {
database_backend: "sqlite".to_string(),
session_backend: "redis".to_string(),
redis_url: Some("redis://127.0.0.1:6379/".to_string()),
session_encryption_key: Some("configured-shared-key".to_string()),
cluster_mode: true,
..AppConfig::default()
};
let error = config.validate().unwrap_err();
assert!(error.contains("shared mysql/pgsql application database"));
}
}
+54 -51
View File
@@ -1,5 +1,5 @@
use crate::db::pool::{Connection, DbValue, ToDbValue, params};
use crate::queries::sqlite::queries::db_calendar::{EVENT_SELECT_COLS, SETTINGS_SELECT_COLS};
use rusqlite::{Connection, params};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
@@ -126,7 +126,7 @@ pub struct MeetingTemplate {
pub created_at: String,
}
fn row_to_event(row: &rusqlite::Row<'_>) -> rusqlite::Result<CalendarEvent> {
fn row_to_event(row: &crate::db::pool::Row<'_>) -> Result<CalendarEvent, crate::db::pool::DbError> {
let all_day_int: i32 = row.get(6)?;
Ok(CalendarEvent {
id: row.get(0)?,
@@ -152,7 +152,9 @@ fn row_to_event(row: &rusqlite::Row<'_>) -> rusqlite::Result<CalendarEvent> {
})
}
fn row_to_settings(row: &rusqlite::Row<'_>) -> rusqlite::Result<CalendarSettings> {
fn row_to_settings(
row: &crate::db::pool::Row<'_>,
) -> Result<CalendarSettings, crate::db::pool::DbError> {
let caldav_enabled_int: i32 = row.get(10)?;
Ok(CalendarSettings {
default_view: row.get(0)?,
@@ -170,7 +172,9 @@ fn row_to_settings(row: &rusqlite::Row<'_>) -> rusqlite::Result<CalendarSettings
})
}
fn row_to_template(row: &rusqlite::Row<'_>) -> rusqlite::Result<MeetingTemplate> {
fn row_to_template(
row: &crate::db::pool::Row<'_>,
) -> Result<MeetingTemplate, crate::db::pool::DbError> {
let is_default_int: i32 = row.get(4)?;
Ok(MeetingTemplate {
id: row.get(0)?,
@@ -187,7 +191,8 @@ pub fn list_events(
start: &str,
end: &str,
) -> Result<Vec<CalendarEvent>, String> {
let sql = crate::rustbox_query_db_calendar_001!();
let sql =
crate::dbq!(db_calendar::FORMAT_001).replace("{EVENT_SELECT_COLS}", EVENT_SELECT_COLS);
let mut stmt = conn
.prepare(&sql)
.map_err(|e| format!("Failed to prepare list_events: {e}"))?;
@@ -203,7 +208,8 @@ pub fn list_events(
}
pub fn list_all_events(conn: &Connection) -> Result<Vec<CalendarEvent>, String> {
let sql = crate::rustbox_query_db_calendar_002!();
let sql =
crate::dbq!(db_calendar::FORMAT_002).replace("{EVENT_SELECT_COLS}", EVENT_SELECT_COLS);
let mut stmt = conn
.prepare(&sql)
.map_err(|e| format!("Failed to prepare list_all_events: {e}"))?;
@@ -219,12 +225,13 @@ pub fn list_all_events(conn: &Connection) -> Result<Vec<CalendarEvent>, String>
}
pub fn get_event(conn: &Connection, id: &str) -> Result<Option<CalendarEvent>, String> {
let sql = crate::rustbox_query_db_calendar_003!();
let sql =
crate::dbq!(db_calendar::FORMAT_003).replace("{EVENT_SELECT_COLS}", EVENT_SELECT_COLS);
let result = conn.query_row(&sql, params![id], row_to_event);
match result {
Ok(e) => Ok(Some(e)),
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
Err(crate::db::pool::DbError::QueryReturnedNoRows) => Ok(None),
Err(e) => Err(format!("Failed to get event: {e}")),
}
}
@@ -232,15 +239,11 @@ pub fn get_event(conn: &Connection, id: &str) -> Result<Option<CalendarEvent>, S
pub fn create_event(conn: &Connection, data: &CreateEvent) -> Result<CalendarEvent, String> {
let id = Uuid::new_v4().to_string();
let now: String = conn
.query_row(
crate::queries::sqlite::queries::db_calendar::Q_001,
[],
|row| row.get(0),
)
.query_row(crate::dbq!(db_calendar::Q_001), [], |row| row.get(0))
.map_err(|e| format!("Failed to get current time: {e}"))?;
conn.execute(
crate::queries::sqlite::queries::db_calendar::Q_002,
crate::dbq!(db_calendar::Q_002),
params![
id,
data.title,
@@ -275,14 +278,14 @@ pub fn update_event(
data: &UpdateEvent,
) -> Result<Option<CalendarEvent>, String> {
let mut sets = Vec::new();
let mut values: Vec<Box<dyn rusqlite::types::ToSql>> = Vec::new();
let mut values: Vec<DbValue> = Vec::new();
let mut idx = 1u32;
macro_rules! maybe_set {
($field:ident, $col:expr) => {
if let Some(ref v) = data.$field {
sets.push(format!("{} = ?{idx}", $col));
values.push(Box::new(v.clone()));
values.push(v.to_db_value());
idx += 1;
}
};
@@ -302,13 +305,13 @@ pub fn update_event(
if let Some(v) = data.all_day {
sets.push(format!("all_day = ?{idx}"));
values.push(Box::new(v as i32));
values.push((v as i32).to_db_value());
idx += 1;
}
if let Some(v) = data.reminder_minutes {
sets.push(format!("reminder_minutes = ?{idx}"));
values.push(Box::new(v));
values.push(v.to_db_value());
idx += 1;
}
@@ -318,12 +321,15 @@ pub fn update_event(
sets.push("updated_at = datetime('now')".to_string());
let sql = crate::rustbox_query_db_calendar_004!(sets.join(", "), idx);
values.push(Box::new(id.to_string()));
let sql = format!(
"UPDATE calendar_events SET {} WHERE id = ?{}",
sets.join(", "),
idx
);
values.push(id.to_db_value());
let params_refs: Vec<&dyn rusqlite::types::ToSql> = values.iter().map(|b| b.as_ref()).collect();
let updated = conn
.execute(&sql, params_refs.as_slice())
.execute(&sql, values)
.map_err(|e| format!("Failed to update event: {e}"))?;
if updated == 0 {
@@ -335,10 +341,7 @@ pub fn update_event(
pub fn delete_event(conn: &Connection, id: &str) -> Result<bool, String> {
let deleted = conn
.execute(
crate::queries::sqlite::queries::db_calendar::Q_003,
params![id],
)
.execute(crate::dbq!(db_calendar::Q_003), params![id])
.map_err(|e| format!("Failed to delete event: {e}"))?;
Ok(deleted > 0)
}
@@ -347,21 +350,23 @@ pub fn find_event_by_source_uid(
conn: &Connection,
source_uid: &str,
) -> Result<Option<CalendarEvent>, String> {
let sql = crate::rustbox_query_db_calendar_005!();
let sql =
crate::dbq!(db_calendar::FORMAT_005).replace("{EVENT_SELECT_COLS}", EVENT_SELECT_COLS);
let result = conn.query_row(&sql, params![source_uid], row_to_event);
match result {
Ok(e) => Ok(Some(e)),
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
Err(crate::db::pool::DbError::QueryReturnedNoRows) => Ok(None),
Err(e) => Err(format!("Failed to find event by source_uid: {e}")),
}
}
pub fn get_calendar_settings(conn: &Connection) -> Result<CalendarSettings, String> {
conn.execute(crate::queries::sqlite::queries::db_calendar::Q_004, [])
conn.execute(crate::dbq!(db_calendar::Q_004), [])
.map_err(|e| format!("Failed to ensure calendar_settings row: {e}"))?;
let sql = crate::rustbox_query_db_calendar_006!();
let sql = crate::dbq!(db_calendar::FORMAT_006)
.replace("{SETTINGS_SELECT_COLS}", SETTINGS_SELECT_COLS);
conn.query_row(&sql, [], row_to_settings)
.map_err(|e| format!("Failed to get calendar settings: {e}"))
}
@@ -370,18 +375,18 @@ pub fn update_calendar_settings(
conn: &Connection,
data: &UpdateCalendarSettings,
) -> Result<CalendarSettings, String> {
conn.execute(crate::queries::sqlite::queries::db_calendar::Q_005, [])
conn.execute(crate::dbq!(db_calendar::Q_005), [])
.map_err(|e| format!("Failed to ensure calendar_settings row: {e}"))?;
let mut sets = Vec::new();
let mut values: Vec<Box<dyn rusqlite::types::ToSql>> = Vec::new();
let mut values: Vec<DbValue> = Vec::new();
let mut idx = 1u32;
macro_rules! maybe_set {
($field:ident, $col:expr) => {
if let Some(ref v) = data.$field {
sets.push(format!("{} = ?{idx}", $col));
values.push(Box::new(v.clone()));
values.push(v.to_db_value());
idx += 1;
}
};
@@ -400,13 +405,13 @@ pub fn update_calendar_settings(
if let Some(v) = data.week_starts_on {
sets.push(format!("week_starts_on = ?{idx}"));
values.push(Box::new(v));
values.push(v.to_db_value());
idx += 1;
}
if let Some(v) = data.caldav_enabled {
sets.push(format!("caldav_enabled = ?{idx}"));
values.push(Box::new(v as i32));
values.push((v as i32).to_db_value());
#[allow(unused_assignments)]
{
idx += 1;
@@ -415,10 +420,11 @@ pub fn update_calendar_settings(
if !sets.is_empty() {
sets.push("updated_at = datetime('now')".to_string());
let sql = crate::rustbox_query_db_calendar_007!(sets.join(", "));
let params_refs: Vec<&dyn rusqlite::types::ToSql> =
values.iter().map(|b| b.as_ref()).collect();
conn.execute(&sql, params_refs.as_slice())
let sql = format!(
"UPDATE calendar_settings SET {} WHERE id = 1",
sets.join(", ")
);
conn.execute(&sql, values)
.map_err(|e| format!("Failed to update calendar settings: {e}"))?;
}
@@ -427,7 +433,7 @@ pub fn update_calendar_settings(
pub fn list_meeting_templates(conn: &Connection) -> Result<Vec<MeetingTemplate>, String> {
let mut stmt = conn
.prepare(crate::queries::sqlite::queries::db_calendar::Q_006)
.prepare(crate::dbq!(db_calendar::Q_006))
.map_err(|e| format!("Failed to prepare list_meeting_templates: {e}"))?;
let rows = stmt
.query_map([], row_to_template)
@@ -446,24 +452,21 @@ pub fn create_meeting_template(
url_template: &str,
icon: &str,
) -> Result<MeetingTemplate, String> {
conn.execute(
crate::queries::sqlite::queries::db_calendar::Q_007,
params![name, url_template, icon],
)
.map_err(|e| format!("Failed to create meeting template: {e}"))?;
let id = conn
.insert_id(
crate::dbq!(db_calendar::Q_007),
params![name, url_template, icon],
)
.map_err(|e| format!("Failed to create meeting template: {e}"))?;
let id = conn.last_insert_rowid();
let sql = crate::queries::sqlite::queries::db_calendar::Q_008;
let sql = crate::dbq!(db_calendar::Q_008);
conn.query_row(sql, params![id], row_to_template)
.map_err(|e| format!("Failed to read created template: {e}"))
}
pub fn delete_meeting_template(conn: &Connection, id: i64) -> Result<bool, String> {
let deleted = conn
.execute(
crate::queries::sqlite::queries::db_calendar::Q_009,
params![id],
)
.execute(crate::dbq!(db_calendar::Q_009), params![id])
.map_err(|e| format!("Failed to delete meeting template: {e}"))?;
Ok(deleted > 0)
}
+10 -19
View File
@@ -1,4 +1,4 @@
use rusqlite::{Connection, params};
use crate::db::pool::{Connection, params};
use serde::Serialize;
use crate::db::contacts::Contact;
@@ -13,17 +13,14 @@ pub struct ContactGroup {
}
pub fn create_group(conn: &Connection, id: &str, name: &str) -> Result<(), String> {
conn.execute(
crate::queries::sqlite::queries::db_contact_groups::Q_001,
params![id, name],
)
.map_err(|e| format!("Failed to create contact group: {e}"))?;
conn.execute(crate::dbq!(db_contact_groups::Q_001), params![id, name])
.map_err(|e| format!("Failed to create contact group: {e}"))?;
Ok(())
}
pub fn list_groups(conn: &Connection) -> Result<Vec<ContactGroup>, String> {
let mut stmt = conn
.prepare(crate::queries::sqlite::queries::db_contact_groups::Q_002)
.prepare(crate::dbq!(db_contact_groups::Q_002))
.map_err(|e| format!("Failed to prepare list_groups: {e}"))?;
let rows = stmt
@@ -47,27 +44,21 @@ pub fn list_groups(conn: &Connection) -> Result<Vec<ContactGroup>, String> {
pub fn update_group(conn: &Connection, id: &str, name: &str) -> Result<bool, String> {
let updated = conn
.execute(
crate::queries::sqlite::queries::db_contact_groups::Q_003,
params![name, id],
)
.execute(crate::dbq!(db_contact_groups::Q_003), params![name, id])
.map_err(|e| format!("Failed to update contact group: {e}"))?;
Ok(updated > 0)
}
pub fn delete_group(conn: &Connection, id: &str) -> Result<bool, String> {
let deleted = conn
.execute(
crate::queries::sqlite::queries::db_contact_groups::Q_004,
params![id],
)
.execute(crate::dbq!(db_contact_groups::Q_004), params![id])
.map_err(|e| format!("Failed to delete contact group: {e}"))?;
Ok(deleted > 0)
}
pub fn add_member(conn: &Connection, group_id: &str, contact_id: &str) -> Result<(), String> {
conn.execute(
crate::queries::sqlite::queries::db_contact_groups::Q_005,
crate::dbq!(db_contact_groups::Q_005),
params![group_id, contact_id],
)
.map_err(|e| format!("Failed to add group member: {e}"))?;
@@ -77,7 +68,7 @@ pub fn add_member(conn: &Connection, group_id: &str, contact_id: &str) -> Result
pub fn remove_member(conn: &Connection, group_id: &str, contact_id: &str) -> Result<bool, String> {
let deleted = conn
.execute(
crate::queries::sqlite::queries::db_contact_groups::Q_006,
crate::dbq!(db_contact_groups::Q_006),
params![group_id, contact_id],
)
.map_err(|e| format!("Failed to remove group member: {e}"))?;
@@ -86,7 +77,7 @@ pub fn remove_member(conn: &Connection, group_id: &str, contact_id: &str) -> Res
pub fn list_group_members(conn: &Connection, group_id: &str) -> Result<Vec<Contact>, String> {
let mut stmt = conn
.prepare(crate::queries::sqlite::queries::db_contact_groups::Q_007)
.prepare(crate::dbq!(db_contact_groups::Q_007))
.map_err(|e| format!("Failed to prepare list_group_members: {e}"))?;
let rows = stmt
@@ -131,7 +122,7 @@ pub fn list_contact_groups(
contact_id: &str,
) -> Result<Vec<ContactGroup>, String> {
let mut stmt = conn
.prepare(crate::queries::sqlite::queries::db_contact_groups::Q_008)
.prepare(crate::dbq!(db_contact_groups::Q_008))
.map_err(|e| format!("Failed to prepare list_contact_groups: {e}"))?;
let rows = stmt
+33 -43
View File
@@ -1,5 +1,5 @@
use crate::db::pool::{Connection, params};
use crate::queries::sqlite::queries::db_contacts::CONTACT_SELECT_COLS;
use rusqlite::{Connection, params};
use serde::Serialize;
use uuid::Uuid;
@@ -33,7 +33,7 @@ pub struct Contact {
pub has_photo: bool,
}
fn row_to_contact(row: &rusqlite::Row<'_>) -> rusqlite::Result<Contact> {
fn row_to_contact(row: &crate::db::pool::Row<'_>) -> Result<Contact, crate::db::pool::DbError> {
Ok(Contact {
id: row.get(0)?,
email: row.get(1)?,
@@ -60,14 +60,14 @@ fn row_to_contact(row: &rusqlite::Row<'_>) -> rusqlite::Result<Contact> {
}
fn escape_like(term: &str) -> String {
term.replace('\\', "\\\\")
.replace('%', "\\%")
.replace('_', "\\_")
term.replace('!', "!!")
.replace('%', "!%")
.replace('_', "!_")
}
pub fn upsert_contact(conn: &Connection, contact: &Contact) -> Result<(), String> {
conn.execute(
crate::queries::sqlite::queries::db_contacts::Q_001,
crate::dbq!(db_contacts::Q_001),
params![
contact.id,
contact.email,
@@ -95,23 +95,25 @@ pub fn upsert_contact(conn: &Connection, contact: &Contact) -> Result<(), String
}
pub fn get_contact(conn: &Connection, id: &str) -> Result<Option<Contact>, String> {
let sql = crate::rustbox_query_db_contacts_001!();
let sql =
crate::dbq!(db_contacts::FORMAT_001).replace("{CONTACT_SELECT_COLS}", CONTACT_SELECT_COLS);
let result = conn.query_row(&sql, params![id], row_to_contact);
match result {
Ok(c) => Ok(Some(c)),
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
Err(crate::db::pool::DbError::QueryReturnedNoRows) => Ok(None),
Err(e) => Err(format!("Failed to get contact: {e}")),
}
}
pub fn get_contact_by_email(conn: &Connection, email: &str) -> Result<Option<Contact>, String> {
let sql = crate::rustbox_query_db_contacts_002!();
let sql =
crate::dbq!(db_contacts::FORMAT_002).replace("{CONTACT_SELECT_COLS}", CONTACT_SELECT_COLS);
let result = conn.query_row(&sql, params![email], row_to_contact);
match result {
Ok(c) => Ok(Some(c)),
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
Err(crate::db::pool::DbError::QueryReturnedNoRows) => Ok(None),
Err(e) => Err(format!("Failed to get contact by email: {e}")),
}
}
@@ -126,13 +128,15 @@ pub fn list_contacts(
Some(q) if !q.is_empty() => {
let escaped = escape_like(q);
let pattern = format!("%{escaped}%");
let sql = crate::rustbox_query_db_contacts_003!();
let sql = crate::dbq!(db_contacts::FORMAT_003)
.replace("{CONTACT_SELECT_COLS}", CONTACT_SELECT_COLS);
drop(pattern);
(sql, Some(escaped))
}
_ => {
let sql = crate::rustbox_query_db_contacts_004!();
let sql = crate::dbq!(db_contacts::FORMAT_004)
.replace("{CONTACT_SELECT_COLS}", CONTACT_SELECT_COLS);
(sql, None)
}
};
@@ -170,10 +174,7 @@ pub fn list_contacts(
pub fn delete_contact(conn: &Connection, id: &str) -> Result<bool, String> {
let deleted = conn
.execute(
crate::queries::sqlite::queries::db_contacts::Q_002,
params![id],
)
.execute(crate::dbq!(db_contacts::Q_002), params![id])
.map_err(|e| format!("Failed to delete contact: {e}"))?;
Ok(deleted > 0)
}
@@ -182,7 +183,8 @@ pub fn search_contacts(conn: &Connection, query: &str, limit: u32) -> Result<Vec
let escaped = escape_like(query);
let pattern = format!("%{escaped}%");
let sql = crate::rustbox_query_db_contacts_005!();
let sql =
crate::dbq!(db_contacts::FORMAT_005).replace("{CONTACT_SELECT_COLS}", CONTACT_SELECT_COLS);
let mut stmt = conn
.prepare(&sql)
@@ -206,7 +208,7 @@ pub fn search_known_addresses(
let escaped = escape_like(query);
let pattern = format!("%{escaped}%");
let sql = crate::queries::sqlite::queries::db_contacts::Q_003;
let sql = crate::dbq!(db_contacts::Q_003);
let mut stmt = conn
.prepare(sql)
@@ -237,7 +239,7 @@ pub fn populate_known_addresses(
) -> Result<(), String> {
if !from_address.is_empty() {
conn.execute(
crate::queries::sqlite::queries::db_contacts::Q_004,
crate::dbq!(db_contacts::Q_004),
params![from_address, from_name],
)
.map_err(|e| format!("Failed to insert known address (from): {e}"))?;
@@ -262,11 +264,8 @@ fn insert_addresses_from_json(conn: &Connection, json: &str) -> Result<(), Strin
continue;
}
let name = entry.get("name").and_then(|v| v.as_str()).unwrap_or("");
conn.execute(
crate::queries::sqlite::queries::db_contacts::Q_005,
params![email, name],
)
.map_err(|e| format!("Failed to insert known address (json): {e}"))?;
conn.execute(crate::dbq!(db_contacts::Q_005), params![email, name])
.map_err(|e| format!("Failed to insert known address (json): {e}"))?;
}
Ok(())
@@ -274,22 +273,16 @@ fn insert_addresses_from_json(conn: &Connection, json: &str) -> Result<(), Strin
#[allow(dead_code)]
pub fn increment_contact_count(conn: &Connection, email: &str) -> Result<(), String> {
conn.execute(
crate::queries::sqlite::queries::db_contacts::Q_006,
params![email],
)
.map_err(|e| format!("Failed to increment contact count: {e}"))?;
conn.execute(crate::dbq!(db_contacts::Q_006), params![email])
.map_err(|e| format!("Failed to increment contact count: {e}"))?;
Ok(())
}
#[allow(dead_code)]
pub fn auto_add_contact(conn: &Connection, email: &str, name: &str) -> Result<(), String> {
let id = Uuid::new_v4().to_string();
conn.execute(
crate::queries::sqlite::queries::db_contacts::Q_007,
params![id, email, name],
)
.map_err(|e| format!("Failed to auto-add contact: {e}"))?;
conn.execute(crate::dbq!(db_contacts::Q_007), params![id, email, name])
.map_err(|e| format!("Failed to auto-add contact: {e}"))?;
Ok(())
}
@@ -300,7 +293,7 @@ pub fn set_contact_photo(
data: &[u8],
) -> Result<(), String> {
conn.execute(
crate::queries::sqlite::queries::db_contacts::Q_008,
crate::dbq!(db_contacts::Q_008),
params![contact_id, content_type, data],
)
.map_err(|e| format!("Failed to save contact photo: {e}"))?;
@@ -312,23 +305,20 @@ pub fn get_contact_photo(
contact_id: &str,
) -> Result<Option<(String, Vec<u8>)>, String> {
match conn.query_row(
crate::queries::sqlite::queries::db_contacts::Q_009,
crate::dbq!(db_contacts::Q_009),
params![contact_id],
|row| Ok((row.get(0)?, row.get(1)?)),
) {
Ok(value) => Ok(Some(value)),
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
Err(crate::db::pool::DbError::QueryReturnedNoRows) => Ok(None),
Err(e) => Err(format!("Failed to load contact photo: {e}")),
}
}
pub fn delete_contact_photo(conn: &Connection, contact_id: &str) -> Result<bool, String> {
conn.execute(
crate::queries::sqlite::queries::db_contacts::Q_010,
params![contact_id],
)
.map(|n| n > 0)
.map_err(|e| format!("Failed to delete contact photo: {e}"))
conn.execute(crate::dbq!(db_contacts::Q_010), params![contact_id])
.map(|n| n > 0)
.map_err(|e| format!("Failed to delete contact photo: {e}"))
}
#[cfg(test)]
+73 -77
View File
@@ -1,4 +1,4 @@
use rusqlite::Connection;
use crate::db::pool::{Connection, DbValue, ToDbValue};
use serde::{Deserialize, Deserializer, Serialize};
#[derive(Debug, Clone, Serialize)]
@@ -184,70 +184,66 @@ fn defaults() -> DisplayPreferences {
}
pub fn get_preferences(conn: &Connection) -> Result<DisplayPreferences, String> {
let result = conn.query_row(
crate::queries::sqlite::queries::db_display_preferences::Q_001,
[],
|row| {
Ok(DisplayPreferences {
density: row.get(0)?,
theme: row.get(1)?,
language: row.get(2)?,
compose_format: row.get(3)?,
deep_index: row.get::<_, i32>(4)? != 0,
animation_mode: row.get(5)?,
preview_pane: row.get(6)?,
ui_scale: row.get(7)?,
color_preset: row.get(8)?,
accent_color: row.get(9)?,
background_color: row.get(10)?,
panel_color: row.get(11)?,
text_color: row.get(12)?,
font_family: row.get(13)?,
font_size: row.get(14)?,
row_height: row.get(15)?,
folder_pane_width: row.get(16)?,
message_pane_width: row.get(17)?,
border_radius: row.get(18)?,
contrast: row.get(19)?,
show_avatars: row.get::<_, i32>(20)? != 0,
show_attachment_icons: row.get::<_, i32>(21)? != 0,
show_flags: row.get::<_, i32>(22)? != 0,
reduce_motion: row.get::<_, i32>(23)? != 0,
date_format: row.get(24)?,
time_format: row.get(25)?,
compose_autosave_ms: row.get(26)?,
default_priority: row.get(27)?,
request_mdn_default: row.get::<_, i32>(28)? != 0,
attachment_reminder: row.get::<_, i32>(29)? != 0,
icon_size: row.get(30)?,
toolbar_density: row.get(31)?,
sidebar_item_height: row.get(32)?,
reader_max_width: row.get(33)?,
composer_width: row.get(34)?,
selection_color: row.get(35)?,
unread_font_weight: row.get(36)?,
unread_badge_style: row.get(37)?,
compact_headers: row.get::<_, i32>(38)? != 0,
show_borders: row.get::<_, i32>(39)? != 0,
scrollbar_density: row.get(40)?,
monochrome: row.get::<_, i32>(41)? != 0,
folder_background: row.get(42)?,
message_background: row.get(43)?,
reader_background: row.get(44)?,
compose_font_family: row.get(45)?,
reply_position: row.get(46)?,
quote_style: row.get(47)?,
forward_mode: row.get(48)?,
spellcheck_enabled: row.get::<_, i32>(49)? != 0,
interface_skin: row.get(50)?,
preset_scale_unlocked: row.get::<_, i32>(51)? != 0,
updated_at: row.get(52)?,
})
},
);
let result = conn.query_row(crate::dbq!(db_display_preferences::Q_001), [], |row| {
Ok(DisplayPreferences {
density: row.get(0)?,
theme: row.get(1)?,
language: row.get(2)?,
compose_format: row.get(3)?,
deep_index: row.get::<_, i32>(4)? != 0,
animation_mode: row.get(5)?,
preview_pane: row.get(6)?,
ui_scale: row.get(7)?,
color_preset: row.get(8)?,
accent_color: row.get(9)?,
background_color: row.get(10)?,
panel_color: row.get(11)?,
text_color: row.get(12)?,
font_family: row.get(13)?,
font_size: row.get(14)?,
row_height: row.get(15)?,
folder_pane_width: row.get(16)?,
message_pane_width: row.get(17)?,
border_radius: row.get(18)?,
contrast: row.get(19)?,
show_avatars: row.get::<_, i32>(20)? != 0,
show_attachment_icons: row.get::<_, i32>(21)? != 0,
show_flags: row.get::<_, i32>(22)? != 0,
reduce_motion: row.get::<_, i32>(23)? != 0,
date_format: row.get(24)?,
time_format: row.get(25)?,
compose_autosave_ms: row.get(26)?,
default_priority: row.get(27)?,
request_mdn_default: row.get::<_, i32>(28)? != 0,
attachment_reminder: row.get::<_, i32>(29)? != 0,
icon_size: row.get(30)?,
toolbar_density: row.get(31)?,
sidebar_item_height: row.get(32)?,
reader_max_width: row.get(33)?,
composer_width: row.get(34)?,
selection_color: row.get(35)?,
unread_font_weight: row.get(36)?,
unread_badge_style: row.get(37)?,
compact_headers: row.get::<_, i32>(38)? != 0,
show_borders: row.get::<_, i32>(39)? != 0,
scrollbar_density: row.get(40)?,
monochrome: row.get::<_, i32>(41)? != 0,
folder_background: row.get(42)?,
message_background: row.get(43)?,
reader_background: row.get(44)?,
compose_font_family: row.get(45)?,
reply_position: row.get(46)?,
quote_style: row.get(47)?,
forward_mode: row.get(48)?,
spellcheck_enabled: row.get::<_, i32>(49)? != 0,
interface_skin: row.get(50)?,
preset_scale_unlocked: row.get::<_, i32>(51)? != 0,
updated_at: row.get(52)?,
})
});
match result {
Ok(value) => Ok(value),
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(defaults()),
Err(crate::db::pool::DbError::QueryReturnedNoRows) => Ok(defaults()),
Err(error) => Err(format!("Failed to get display preferences: {error}")),
}
}
@@ -274,20 +270,17 @@ pub fn update_preferences(
conn: &Connection,
data: &UpdateDisplayPreferences,
) -> Result<DisplayPreferences, String> {
conn.execute(
crate::queries::sqlite::queries::db_display_preferences::Q_002,
[],
)
.map_err(|e| format!("Failed to ensure preferences row: {e}"))?;
conn.execute(crate::dbq!(db_display_preferences::Q_002), [])
.map_err(|e| format!("Failed to ensure preferences row: {e}"))?;
let mut sets = Vec::new();
let mut values: Vec<Box<dyn rusqlite::types::ToSql>> = Vec::new();
let mut values: Vec<DbValue> = Vec::new();
let mut idx = 1usize;
macro_rules! set_text {
($field:ident, $column:literal) => {
if let Some(ref value) = data.$field {
sets.push(format!(concat!($column, " = ?{}"), idx));
values.push(Box::new(value.clone()));
values.push(value.to_db_value());
idx += 1;
}
};
@@ -296,7 +289,7 @@ pub fn update_preferences(
($field:ident, $column:literal) => {
if let Some(value) = data.$field {
sets.push(format!(concat!($column, " = ?{}"), idx));
values.push(Box::new(value));
values.push(value.to_db_value());
idx += 1;
}
};
@@ -305,7 +298,7 @@ pub fn update_preferences(
($field:ident, $column:literal) => {
if let Some(value) = data.$field {
sets.push(format!(concat!($column, " = ?{}"), idx));
values.push(Box::new(value as i32));
values.push((value as i32).to_db_value());
idx += 1;
}
};
@@ -499,7 +492,7 @@ pub fn update_preferences(
set_bool!(deep_index, "deep_index");
if let Some(ref value) = data.animation_mode {
sets.push(format!("animation_mode = ?{}", idx));
values.push(Box::new(value.clone()));
values.push(value.to_db_value());
idx += 1;
}
set_text!(preview_pane, "preview_pane");
@@ -549,10 +542,13 @@ pub fn update_preferences(
return get_preferences(conn);
}
sets.push("updated_at = datetime('now')".to_string());
let sql = crate::rustbox_query_db_display_preferences_001!(sets.join(", "), idx);
values.push(Box::new(1_i32));
let refs: Vec<&dyn rusqlite::types::ToSql> = values.iter().map(|v| v.as_ref()).collect();
conn.execute(&sql, refs.as_slice())
let sql = format!(
"UPDATE display_preferences SET {} WHERE id = ?{}",
sets.join(", "),
idx
);
values.push(1_i32.to_db_value());
conn.execute(&sql, values)
.map_err(|e| format!("Failed to update display preferences: {e}"))?;
get_preferences(conn)
}
+34 -44
View File
@@ -1,4 +1,4 @@
use rusqlite::{Connection, params};
use crate::db::pool::{Connection, params};
use serde::Serialize;
#[derive(Debug, Clone, Serialize)]
@@ -72,7 +72,7 @@ pub fn upsert_draft(
) -> Result<(), String> {
validate_draft_id(id)?;
conn.execute(
crate::queries::sqlite::queries::db_drafts::Q_001,
crate::dbq!(db_drafts::Q_001),
params![
id,
to_addresses,
@@ -97,46 +97,42 @@ pub fn upsert_draft(
}
pub fn get_draft(conn: &Connection, id: &str) -> Result<Option<Draft>, String> {
let result = conn.query_row(
crate::queries::sqlite::queries::db_drafts::Q_002,
params![id],
|row| {
Ok(Draft {
id: row.get(0)?,
to_addresses: row.get(1)?,
cc_addresses: row.get(2)?,
bcc_addresses: row.get(3)?,
subject: row.get(4)?,
text_body: row.get(5)?,
html_body: row.get(6)?,
in_reply_to: row.get(7)?,
references_header: row.get(8)?,
request_mdn: row.get::<_, i64>(9)? != 0,
priority: row.get(10)?,
compose_format: row.get(11)?,
pgp_mode: row.get(12)?,
pgp_inline: row.get::<_, i64>(13)? != 0,
pgp_attach_public_key: row.get::<_, i64>(14)? != 0,
pgp_attach_recipient_keys: row.get::<_, i64>(15)? != 0,
remote_folder: row.get(16)?,
remote_uid: row.get(17)?,
remote_uid_validity: row.get(18)?,
created_at: row.get(19)?,
updated_at: row.get(20)?,
})
},
);
let result = conn.query_row(crate::dbq!(db_drafts::Q_002), params![id], |row| {
Ok(Draft {
id: row.get(0)?,
to_addresses: row.get(1)?,
cc_addresses: row.get(2)?,
bcc_addresses: row.get(3)?,
subject: row.get(4)?,
text_body: row.get(5)?,
html_body: row.get(6)?,
in_reply_to: row.get(7)?,
references_header: row.get(8)?,
request_mdn: row.get::<_, i64>(9)? != 0,
priority: row.get(10)?,
compose_format: row.get(11)?,
pgp_mode: row.get(12)?,
pgp_inline: row.get::<_, i64>(13)? != 0,
pgp_attach_public_key: row.get::<_, i64>(14)? != 0,
pgp_attach_recipient_keys: row.get::<_, i64>(15)? != 0,
remote_folder: row.get(16)?,
remote_uid: row.get(17)?,
remote_uid_validity: row.get(18)?,
created_at: row.get(19)?,
updated_at: row.get(20)?,
})
});
match result {
Ok(draft) => Ok(Some(draft)),
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
Err(crate::db::pool::DbError::QueryReturnedNoRows) => Ok(None),
Err(e) => Err(format!("Failed to get draft: {e}")),
}
}
pub fn list_drafts(conn: &Connection) -> Result<Vec<Draft>, String> {
let mut stmt = conn
.prepare(crate::queries::sqlite::queries::db_drafts::Q_003)
.prepare(crate::dbq!(db_drafts::Q_003))
.map_err(|e| format!("Failed to prepare list_drafts: {e}"))?;
let rows = stmt
@@ -183,7 +179,7 @@ pub fn set_remote_location(
) -> Result<(), String> {
let updated = conn
.execute(
crate::queries::sqlite::queries::db_drafts::Q_004,
crate::dbq!(db_drafts::Q_004),
params![id, folder, uid, uid_validity],
)
.map_err(|e| format!("Failed to update remote draft location: {e}"))?;
@@ -195,10 +191,7 @@ pub fn set_remote_location(
pub fn delete_draft(conn: &Connection, id: &str) -> Result<bool, String> {
let deleted = conn
.execute(
crate::queries::sqlite::queries::db_drafts::Q_005,
params![id],
)
.execute(crate::dbq!(db_drafts::Q_005), params![id])
.map_err(|e| format!("Failed to delete draft: {e}"))?;
Ok(deleted > 0)
}
@@ -213,7 +206,7 @@ pub fn add_draft_attachment(
file_path: &str,
) -> Result<(), String> {
conn.execute(
crate::queries::sqlite::queries::db_drafts::Q_006,
crate::dbq!(db_drafts::Q_006),
params![id, draft_id, filename, content_type, size, file_path],
)
.map_err(|e| format!("Failed to add draft attachment: {e}"))?;
@@ -225,7 +218,7 @@ pub fn get_draft_attachments(
draft_id: &str,
) -> Result<Vec<DraftAttachment>, String> {
let mut stmt = conn
.prepare(crate::queries::sqlite::queries::db_drafts::Q_007)
.prepare(crate::dbq!(db_drafts::Q_007))
.map_err(|e| format!("Failed to prepare get_draft_attachments: {e}"))?;
let rows = stmt
@@ -251,10 +244,7 @@ pub fn get_draft_attachments(
pub fn delete_draft_attachment(conn: &Connection, id: &str) -> Result<bool, String> {
let deleted = conn
.execute(
crate::queries::sqlite::queries::db_drafts::Q_008,
params![id],
)
.execute(crate::dbq!(db_drafts::Q_008), params![id])
.map_err(|e| format!("Failed to delete draft attachment: {e}"))?;
Ok(deleted > 0)
}
+19 -32
View File
@@ -1,4 +1,4 @@
use rusqlite::{Connection, OptionalExtension, params};
use crate::db::pool::{Connection, OptionalExtension, params};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize)]
@@ -13,11 +13,9 @@ pub struct UpdateFolderPreferences {
}
fn get_override(conn: &Connection, folder: &str) -> Result<Option<bool>, String> {
conn.query_row(
crate::queries::sqlite::queries::db_folder_preferences::Q_001,
[folder],
|row| row.get::<_, Option<i32>>(0),
)
conn.query_row(crate::dbq!(db_folder_preferences::Q_001), [folder], |row| {
row.get::<_, Option<i32>>(0)
})
.optional()
.map(|value| value.flatten().map(|value| value != 0))
.map_err(|error| format!("Failed to read folder preferences: {error}"))
@@ -28,33 +26,27 @@ pub fn effective_flagged_on_top(conn: &Connection, folder: &str) -> Result<bool,
return Ok(value);
}
let global = conn
.query_row(
crate::queries::sqlite::queries::db_folder_preferences::Q_002,
[],
|row| row.get::<_, i32>(0),
)
.query_row(crate::dbq!(db_folder_preferences::Q_002), [], |row| {
row.get::<_, i32>(0)
})
.unwrap_or(0);
Ok(global != 0)
}
pub fn get_preferences(conn: &Connection, folder: &str) -> Result<FolderPreferences, String> {
let exists: bool = conn
.query_row(
crate::queries::sqlite::queries::db_folder_preferences::Q_003,
[folder],
|row| row.get(0),
)
.query_row(crate::dbq!(db_folder_preferences::Q_003), [folder], |row| {
row.get(0)
})
.map_err(|error| format!("Failed to validate folder: {error}"))?;
if !exists {
return Err(format!("Folder does not exist: {folder}"));
}
let flagged_on_top = get_override(conn, folder)?;
let effective_flagged_on_top = flagged_on_top.unwrap_or(
conn.query_row(
crate::queries::sqlite::queries::db_folder_preferences::Q_004,
[],
|row| row.get::<_, i32>(0),
)
conn.query_row(crate::dbq!(db_folder_preferences::Q_004), [], |row| {
row.get::<_, i32>(0)
})
.unwrap_or(0)
!= 0,
);
@@ -70,11 +62,9 @@ pub fn update_preferences(
data: &UpdateFolderPreferences,
) -> Result<FolderPreferences, String> {
let exists: bool = conn
.query_row(
crate::queries::sqlite::queries::db_folder_preferences::Q_005,
[folder],
|row| row.get(0),
)
.query_row(crate::dbq!(db_folder_preferences::Q_005), [folder], |row| {
row.get(0)
})
.map_err(|error| format!("Failed to validate folder: {error}"))?;
if !exists {
return Err(format!("Folder does not exist: {folder}"));
@@ -83,17 +73,14 @@ pub fn update_preferences(
match data.flagged_on_top {
Some(value) => {
conn.execute(
crate::queries::sqlite::queries::db_folder_preferences::Q_006,
crate::dbq!(db_folder_preferences::Q_006),
params![folder, value as i32],
)
.map_err(|error| format!("Failed to update folder preferences: {error}"))?;
}
None => {
conn.execute(
crate::queries::sqlite::queries::db_folder_preferences::Q_007,
[folder],
)
.map_err(|error| format!("Failed to clear folder preferences: {error}"))?;
conn.execute(crate::dbq!(db_folder_preferences::Q_007), [folder])
.map_err(|error| format!("Failed to clear folder preferences: {error}"))?;
}
}
+63 -107
View File
@@ -1,4 +1,4 @@
use rusqlite::{Connection, OptionalExtension, params};
use crate::db::pool::{Connection, DbValue, OptionalExtension, ToDbValue, params};
use serde::Serialize;
#[derive(Debug, Clone, Serialize)]
@@ -28,7 +28,7 @@ pub fn upsert_folder(
highest_modseq: u64,
) -> Result<(), String> {
conn.execute(
crate::queries::sqlite::queries::db_folders::Q_001,
crate::dbq!(db_folders::Q_001),
params![
folder_name,
delimiter,
@@ -52,7 +52,7 @@ pub fn insert_folder_if_new(
flags_csv: &str,
) -> Result<(), String> {
conn.execute(
crate::queries::sqlite::queries::db_folders::Q_002,
crate::dbq!(db_folders::Q_002),
params![folder_name, delimiter, flags_csv],
)
.map_err(|e| format!("Failed to insert folder: {e}"))?;
@@ -61,7 +61,7 @@ pub fn insert_folder_if_new(
pub fn get_all_folders(conn: &Connection) -> Result<Vec<CachedFolder>, String> {
let mut stmt = conn
.prepare(crate::queries::sqlite::queries::db_folders::Q_003)
.prepare(crate::dbq!(db_folders::Q_003))
.map_err(|e| format!("Failed to prepare get_all_folders: {e}"))?;
let rows = stmt
@@ -92,28 +92,28 @@ pub fn get_all_folders(conn: &Connection) -> Result<Vec<CachedFolder>, String> {
pub fn remove_stale_folders(conn: &Connection, current_names: &[String]) -> Result<usize, String> {
if current_names.is_empty() {
let deleted = conn
.execute(crate::queries::sqlite::queries::db_folders::Q_004, [])
.execute(crate::dbq!(db_folders::Q_004), [])
.map_err(|e| format!("Failed to delete all folders: {e}"))?;
return Ok(deleted);
}
let placeholders: Vec<String> = (1..=current_names.len()).map(|i| format!("?{i}")).collect();
let sql = crate::rustbox_query_db_folders_001!(placeholders.join(", "));
let sql = format!(
"DELETE FROM folders WHERE name NOT IN ({})",
placeholders.join(", ")
);
let params: Vec<&dyn rusqlite::types::ToSql> = current_names
.iter()
.map(|n| n as &dyn rusqlite::types::ToSql)
.collect();
let params: Vec<DbValue> = current_names.iter().map(ToDbValue::to_db_value).collect();
let deleted = conn
.execute(&sql, params.as_slice())
.execute(&sql, params)
.map_err(|e| format!("Failed to remove stale folders: {e}"))?;
Ok(deleted)
}
pub fn touch_all_folders(conn: &Connection) -> Result<(), String> {
conn.execute(crate::queries::sqlite::queries::db_folders::Q_005, [])
conn.execute(crate::dbq!(db_folders::Q_005), [])
.map_err(|e| format!("Failed to touch folders: {e}"))?;
Ok(())
}
@@ -125,7 +125,7 @@ pub fn update_folder_status(
total_count: u32,
) -> Result<(), String> {
conn.execute(
crate::queries::sqlite::queries::db_folders::Q_006,
crate::dbq!(db_folders::Q_006),
params![uid_validity, total_count, name],
)
.map_err(|e| format!("Failed to update folder status: {e}"))?;
@@ -140,7 +140,7 @@ pub fn update_folder_sync_status(
highest_modseq: u64,
) -> Result<(), String> {
conn.execute(
crate::queries::sqlite::queries::db_folders::Q_007,
crate::dbq!(db_folders::Q_007),
params![uid_validity, total_count, highest_modseq as i64, name],
)
.map_err(|e| format!("Failed to update folder sync status: {e}"))?;
@@ -150,35 +150,29 @@ pub fn update_folder_sync_status(
pub fn refresh_unread_count(conn: &Connection, folder_name: &str) -> Result<(), String> {
let unread: u32 = conn
.query_row(
crate::queries::sqlite::queries::db_folders::Q_008,
crate::dbq!(db_folders::Q_008),
params![folder_name],
|row| row.get(0),
)
.map_err(|e| format!("Failed to count unread messages: {e}"))?;
conn.execute(
crate::queries::sqlite::queries::db_folders::Q_009,
params![unread, folder_name],
)
.map_err(|e| format!("Failed to update unread_count: {e}"))?;
conn.execute(crate::dbq!(db_folders::Q_009), params![unread, folder_name])
.map_err(|e| format!("Failed to update unread_count: {e}"))?;
Ok(())
}
pub fn adjust_unread_count(conn: &Connection, folder_name: &str, delta: i32) -> Result<(), String> {
conn.execute(
crate::queries::sqlite::queries::db_folders::Q_010,
params![delta, folder_name],
)
.map_err(|e| format!("Failed to adjust unread_count: {e}"))?;
conn.execute(crate::dbq!(db_folders::Q_010), params![delta, folder_name])
.map_err(|e| format!("Failed to adjust unread_count: {e}"))?;
Ok(())
}
pub fn is_folder_cache_fresh(conn: &Connection, max_age_secs: u32) -> Result<bool, String> {
let fresh: bool = conn
.query_row(
crate::queries::sqlite::queries::db_folders::Q_011,
params![format!("-{max_age_secs} seconds")],
crate::dbq!(db_folders::Q_011),
params![max_age_secs],
|row| row.get(0),
)
.map_err(|e| format!("Failed to check folder cache freshness: {e}"))?;
@@ -192,8 +186,8 @@ pub fn is_folder_fresh(
) -> Result<bool, String> {
let fresh: bool = conn
.query_row(
crate::queries::sqlite::queries::db_folders::Q_012,
params![folder_name, format!("-{max_age_secs} seconds")],
crate::dbq!(db_folders::Q_012),
params![folder_name, max_age_secs],
|row| row.get(0),
)
.map_err(|e| format!("Failed to check folder freshness: {e}"))?;
@@ -206,7 +200,7 @@ pub fn is_folder_messages_invalidated(
) -> Result<bool, String> {
let invalidated: bool = conn
.query_row(
crate::queries::sqlite::queries::db_folders::Q_013,
crate::dbq!(db_folders::Q_013),
params![folder_name],
|row| row.get(0),
)
@@ -215,53 +209,40 @@ pub fn is_folder_messages_invalidated(
}
pub fn invalidate_folder_freshness(conn: &Connection, folder_name: &str) -> Result<(), String> {
conn.execute(
crate::queries::sqlite::queries::db_folders::Q_014,
params![folder_name],
)
.map_err(|e| format!("Failed to invalidate folder freshness: {e}"))?;
conn.execute(crate::dbq!(db_folders::Q_014), params![folder_name])
.map_err(|e| format!("Failed to invalidate folder freshness: {e}"))?;
Ok(())
}
pub fn get_folder(conn: &Connection, name: &str) -> Result<Option<CachedFolder>, String> {
let result = conn.query_row(
crate::queries::sqlite::queries::db_folders::Q_015,
params![name],
|row| {
let is_subscribed_int: i32 = row.get(4)?;
let highest_modseq_int: i64 = row.get(8)?;
Ok(CachedFolder {
name: row.get(0)?,
delimiter: row.get(1)?,
parent: row.get(2)?,
flags: row.get(3)?,
is_subscribed: is_subscribed_int != 0,
total_count: row.get(5)?,
unread_count: row.get(6)?,
uid_validity: row.get(7)?,
highest_modseq: highest_modseq_int as u64,
})
},
);
let result = conn.query_row(crate::dbq!(db_folders::Q_015), params![name], |row| {
let is_subscribed_int: i32 = row.get(4)?;
let highest_modseq_int: i64 = row.get(8)?;
Ok(CachedFolder {
name: row.get(0)?,
delimiter: row.get(1)?,
parent: row.get(2)?,
flags: row.get(3)?,
is_subscribed: is_subscribed_int != 0,
total_count: row.get(5)?,
unread_count: row.get(6)?,
uid_validity: row.get(7)?,
highest_modseq: highest_modseq_int as u64,
})
});
match result {
Ok(folder) => Ok(Some(folder)),
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
Err(crate::db::pool::DbError::QueryReturnedNoRows) => Ok(None),
Err(e) => Err(format!("Failed to get folder: {e}")),
}
}
pub fn delete_folder_and_messages(conn: &Connection, folder_name: &str) -> Result<(), String> {
conn.execute(
crate::queries::sqlite::queries::db_folders::Q_016,
params![folder_name],
)
.map_err(|e| format!("Failed to delete folder messages: {e}"))?;
conn.execute(
crate::queries::sqlite::queries::db_folders::Q_017,
params![folder_name],
)
.map_err(|e| format!("Failed to delete folder: {e}"))?;
conn.execute(crate::dbq!(db_folders::Q_016), params![folder_name])
.map_err(|e| format!("Failed to delete folder messages: {e}"))?;
conn.execute(crate::dbq!(db_folders::Q_017), params![folder_name])
.map_err(|e| format!("Failed to delete folder: {e}"))?;
Ok(())
}
@@ -273,44 +254,26 @@ pub fn rename_folder_in_cache(
let tx = conn
.unchecked_transaction()
.map_err(|e| format!("Failed to start folder rename transaction: {e}"))?;
tx.execute_batch(crate::queries::sqlite::queries::db_folders::Q_018)
tx.execute_batch(crate::dbq!(db_folders::Q_018))
.map_err(|e| format!("Failed to defer folder rename foreign keys: {e}"))?;
let inserted = tx
.execute(
crate::queries::sqlite::queries::db_folders::Q_019,
params![new_name, old_name],
)
.execute(crate::dbq!(db_folders::Q_019), params![new_name, old_name])
.map_err(|e| format!("Failed to create renamed folder cache entry: {e}"))?;
if inserted == 0 {
return Err(format!("Folder not found in cache: {old_name}"));
}
tx.execute(
crate::queries::sqlite::queries::db_folders::Q_020,
params![new_name, old_name],
)
.map_err(|e| format!("Failed to rename messages folder: {e}"))?;
tx.execute(
crate::queries::sqlite::queries::db_folders::Q_021,
params![new_name, old_name],
)
.map_err(|e| format!("Failed to rename message tag folder references: {e}"))?;
tx.execute(
crate::queries::sqlite::queries::db_folders::Q_022,
params![new_name, old_name],
)
.ok();
tx.execute(
crate::queries::sqlite::queries::db_folders::Q_023,
params![new_name, old_name],
)
.map_err(|e| format!("Failed to update child folder parent: {e}"))?;
tx.execute(
crate::queries::sqlite::queries::db_folders::Q_024,
params![old_name],
)
.map_err(|e| format!("Failed to remove old folder cache entry: {e}"))?;
tx.execute(crate::dbq!(db_folders::Q_020), params![new_name, old_name])
.map_err(|e| format!("Failed to rename messages folder: {e}"))?;
tx.execute(crate::dbq!(db_folders::Q_021), params![new_name, old_name])
.map_err(|e| format!("Failed to rename message tag folder references: {e}"))?;
tx.execute(crate::dbq!(db_folders::Q_022), params![new_name, old_name])
.ok();
tx.execute(crate::dbq!(db_folders::Q_023), params![new_name, old_name])
.map_err(|e| format!("Failed to update child folder parent: {e}"))?;
tx.execute(crate::dbq!(db_folders::Q_024), params![old_name])
.map_err(|e| format!("Failed to remove old folder cache entry: {e}"))?;
tx.commit()
.map_err(|e| format!("Failed to commit folder rename: {e}"))?;
Ok(())
@@ -320,11 +283,9 @@ pub fn find_folder_by_attribute(
conn: &Connection,
attribute: &str,
) -> Result<Option<String>, String> {
conn.query_row(
crate::queries::sqlite::queries::db_folders::Q_025,
[attribute],
|row| row.get(0),
)
conn.query_row(crate::dbq!(db_folders::Q_025), [attribute], |row| {
row.get(0)
})
.optional()
.map_err(|e| e.to_string())
}
@@ -442,17 +403,12 @@ mod tests {
fn sync_metadata_preserves_message_sync_timestamp() {
let conn = open_test_db();
insert_folder_if_new(&conn, "Sent", None, "\\Sent").unwrap();
conn.execute(crate::queries::sqlite::queries::db_folders::Q_026, [])
.unwrap();
conn.execute(crate::dbq!(db_folders::Q_026), []).unwrap();
update_folder_sync_status(&conn, "Sent", 1, 5, 42).unwrap();
let timestamp: String = conn
.query_row(
crate::queries::sqlite::queries::db_folders::Q_027,
[],
|row| row.get(0),
)
.query_row(crate::dbq!(db_folders::Q_027), [], |row| row.get(0))
.unwrap();
assert_eq!(timestamp, "2000-01-01 00:00:00");
assert!(!is_folder_fresh(&conn, "Sent", 30).unwrap());
+9 -12
View File
@@ -1,4 +1,4 @@
use rusqlite::{Connection, OptionalExtension, Transaction, TransactionBehavior};
use crate::db::pool::{Connection, OptionalExtension, Transaction};
#[derive(Clone, Copy, PartialEq, Eq)]
pub(crate) struct HeaderSync {
@@ -8,11 +8,9 @@ pub(crate) struct HeaderSync {
impl HeaderSync {
pub(crate) fn capture(conn: &Connection) -> Result<Self, String> {
let generation = conn
.query_row(
crate::queries::sqlite::queries::db_header_sync::Q_001,
[],
|row| row.get::<_, String>(0),
)
.query_row(crate::dbq!(db_header_sync::Q_001), [], |row| {
row.get::<_, String>(0)
})
.optional()
.map_err(|e| e.to_string())?
.map(|value| value.parse::<u64>().map_err(|e| e.to_string()))
@@ -21,18 +19,17 @@ impl HeaderSync {
Ok(Self { generation })
}
pub(crate) fn begin_write(self, conn: &Connection) -> Result<Transaction<'_>, String> {
let transaction = Transaction::new_unchecked(conn, TransactionBehavior::Immediate)
.map_err(|e| e.to_string())?;
if Self::capture(conn)?.generation != self.generation {
pub(crate) fn begin_write(self, conn: &Connection) -> Result<Transaction, String> {
let transaction = conn.unchecked_transaction().map_err(|e| e.to_string())?;
if Self::capture(&transaction)?.generation != self.generation {
return Err("Header sync superseded by cache removal; retry on next sync".into());
}
Ok(transaction)
}
}
pub(crate) fn invalidate(conn: &Transaction<'_>) -> Result<(), String> {
conn.execute(crate::queries::sqlite::queries::db_header_sync::Q_002, [])
pub(crate) fn invalidate(conn: &Transaction) -> Result<(), String> {
conn.execute(crate::dbq!(db_header_sync::Q_002), [])
.map_err(|e| e.to_string())?;
Ok(())
}
+47 -49
View File
@@ -1,5 +1,5 @@
use crate::db::pool::{Connection, DbValue, ToDbValue, params};
use crate::queries::sqlite::queries::db_identities::IDENTITY_SELECT_COLS;
use rusqlite::{Connection, params};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize)]
@@ -95,7 +95,7 @@ fn validate_signature_position(value: &str) -> Result<(), String> {
}
}
fn row_to_identity(row: &rusqlite::Row<'_>) -> rusqlite::Result<Identity> {
fn row_to_identity(row: &crate::db::pool::Row<'_>) -> Result<Identity, crate::db::pool::DbError> {
Ok(Identity {
id: row.get(0)?,
display_name: row.get(1)?,
@@ -120,7 +120,8 @@ fn row_to_identity(row: &rusqlite::Row<'_>) -> rusqlite::Result<Identity> {
}
pub fn list_identities(conn: &Connection) -> Result<Vec<Identity>, String> {
let sql = crate::rustbox_query_db_identities_001!();
let sql = crate::dbq!(db_identities::FORMAT_001)
.replace("{IDENTITY_SELECT_COLS}", IDENTITY_SELECT_COLS);
let mut stmt = conn
.prepare(&sql)
.map_err(|e| format!("Failed to prepare list_identities: {e}"))?;
@@ -132,20 +133,22 @@ pub fn list_identities(conn: &Connection) -> Result<Vec<Identity>, String> {
}
pub fn get_identity(conn: &Connection, id: i64) -> Result<Option<Identity>, String> {
let sql = crate::rustbox_query_db_identities_002!();
let sql = crate::dbq!(db_identities::FORMAT_002)
.replace("{IDENTITY_SELECT_COLS}", IDENTITY_SELECT_COLS);
match conn.query_row(&sql, params![id], row_to_identity) {
Ok(identity) => Ok(Some(identity)),
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
Err(crate::db::pool::DbError::QueryReturnedNoRows) => Ok(None),
Err(e) => Err(format!("Failed to get identity: {e}")),
}
}
#[allow(dead_code)]
pub fn get_default_identity(conn: &Connection) -> Result<Option<Identity>, String> {
let sql = crate::rustbox_query_db_identities_003!();
let sql = crate::dbq!(db_identities::FORMAT_003)
.replace("{IDENTITY_SELECT_COLS}", IDENTITY_SELECT_COLS);
match conn.query_row(&sql, [], row_to_identity) {
Ok(identity) => Ok(Some(identity)),
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
Err(crate::db::pool::DbError::QueryReturnedNoRows) => Ok(None),
Err(e) => Err(format!("Failed to get default identity: {e}")),
}
}
@@ -155,29 +158,29 @@ pub fn create_identity(conn: &Connection, data: &CreateIdentity) -> Result<Ident
if data.is_default {
clear_default(conn)?;
}
conn.execute(
crate::queries::sqlite::queries::db_identities::Q_001,
params![
data.display_name,
data.email,
data.signature_html,
data.is_default as i32,
data.reply_to,
data.bcc,
data.organization,
data.signature_text,
data.signature_new as i32,
data.signature_reply as i32,
data.signature_forward as i32,
data.signature_position,
data.pgp_fingerprint,
data.pgp_sign_default as i32,
data.pgp_encrypt_default as i32,
data.pgp_inline_default as i32
],
)
.map_err(|e| format!("Failed to create identity: {e}"))?;
let id = conn.last_insert_rowid();
let id = conn
.insert_id(
crate::dbq!(db_identities::Q_001),
params![
data.display_name,
data.email,
data.signature_html,
data.is_default as i32,
data.reply_to,
data.bcc,
data.organization,
data.signature_text,
data.signature_new as i32,
data.signature_reply as i32,
data.signature_forward as i32,
data.signature_position,
data.pgp_fingerprint,
data.pgp_sign_default as i32,
data.pgp_encrypt_default as i32,
data.pgp_inline_default as i32
],
)
.map_err(|e| format!("Failed to create identity: {e}"))?;
get_identity(conn, id)?.ok_or_else(|| "Failed to read back created identity".to_string())
}
@@ -196,13 +199,13 @@ pub fn update_identity(
validate_signature_position(value)?;
}
let mut sets = Vec::new();
let mut values: Vec<Box<dyn rusqlite::types::ToSql>> = Vec::new();
let mut values: Vec<DbValue> = Vec::new();
let mut idx = 1usize;
macro_rules! text {
($field:ident,$column:literal) => {
if let Some(ref value) = data.$field {
sets.push(format!(concat!($column, " = ?{}"), idx));
values.push(Box::new(value.clone()));
values.push(value.to_db_value());
idx += 1;
}
};
@@ -211,7 +214,7 @@ pub fn update_identity(
($field:ident,$column:literal) => {
if let Some(value) = data.$field {
sets.push(format!(concat!($column, " = ?{}"), idx));
values.push(Box::new(value as i32));
values.push((value as i32).to_db_value());
idx += 1;
}
};
@@ -230,7 +233,7 @@ pub fn update_identity(
text!(signature_position, "signature_position");
if let Some(ref value) = data.pgp_fingerprint {
sets.push(format!("pgp_fingerprint = ?{}", idx));
values.push(Box::new(value.clone()));
values.push(value.to_db_value());
idx += 1;
}
boolean!(pgp_sign_default, "pgp_sign_default");
@@ -240,38 +243,33 @@ pub fn update_identity(
return get_identity(conn, id);
}
sets.push("updated_at = datetime('now')".to_string());
let sql = crate::rustbox_query_db_identities_004!(sets.join(", "), idx);
values.push(Box::new(id));
let refs: Vec<&dyn rusqlite::types::ToSql> =
values.iter().map(|value| value.as_ref()).collect();
conn.execute(&sql, refs.as_slice())
let sql = format!(
"UPDATE identities SET {} WHERE id = ?{}",
sets.join(", "),
idx
);
values.push(id.to_db_value());
conn.execute(&sql, values)
.map_err(|e| format!("Failed to update identity: {e}"))?;
get_identity(conn, id)
}
pub fn delete_identity(conn: &Connection, id: i64) -> Result<bool, String> {
Ok(conn
.execute(
crate::queries::sqlite::queries::db_identities::Q_002,
params![id],
)
.execute(crate::dbq!(db_identities::Q_002), params![id])
.map_err(|e| format!("Failed to delete identity: {e}"))?
> 0)
}
pub fn has_identities(conn: &Connection) -> Result<bool, String> {
let count: i64 = conn
.query_row(
crate::queries::sqlite::queries::db_identities::Q_003,
[],
|row| row.get(0),
)
.query_row(crate::dbq!(db_identities::Q_003), [], |row| row.get(0))
.map_err(|e| format!("Failed to count identities: {e}"))?;
Ok(count > 0)
}
fn clear_default(conn: &Connection) -> Result<(), String> {
conn.execute(crate::queries::sqlite::queries::db_identities::Q_004, [])
conn.execute(crate::dbq!(db_identities::Q_004), [])
.map_err(|e| format!("Failed to clear default identity: {e}"))?;
Ok(())
}
+20 -27
View File
@@ -1,4 +1,4 @@
use rusqlite::Connection;
use crate::db::pool::{Connection, params};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize)]
@@ -31,25 +31,21 @@ pub struct UpdateMailPreferences {
}
pub fn get_preferences(conn: &Connection) -> Result<MailPreferences, String> {
conn.query_row(
crate::queries::sqlite::queries::db_mail_preferences::Q_001,
[],
|row| {
Ok(MailPreferences {
messages_per_page: row.get::<_, u32>(0)?,
mark_read_delay_ms: row.get::<_, u64>(1)?,
remote_images: row.get(2)?,
preferred_body: row.get(3)?,
thread_mode: row.get(4)?,
collapse_quotes: row.get::<_, i32>(5)? != 0,
show_snippets: row.get::<_, i32>(6)? != 0,
default_sort: row.get(7)?,
flagged_on_top: row.get::<_, i32>(8)? != 0,
sync_imap_keywords: row.get::<_, i32>(9)? != 0,
updated_at: row.get(10)?,
})
},
)
conn.query_row(crate::dbq!(db_mail_preferences::Q_001), [], |row| {
Ok(MailPreferences {
messages_per_page: row.get::<_, u32>(0)?,
mark_read_delay_ms: row.get::<_, u64>(1)?,
remote_images: row.get(2)?,
preferred_body: row.get(3)?,
thread_mode: row.get(4)?,
collapse_quotes: row.get::<_, i32>(5)? != 0,
show_snippets: row.get::<_, i32>(6)? != 0,
default_sort: row.get(7)?,
flagged_on_top: row.get::<_, i32>(8)? != 0,
sync_imap_keywords: row.get::<_, i32>(9)? != 0,
updated_at: row.get(10)?,
})
})
.map_err(|error| format!("Failed to read mail preferences: {error}"))
}
@@ -57,11 +53,8 @@ pub fn update_preferences(
conn: &Connection,
data: &UpdateMailPreferences,
) -> Result<MailPreferences, String> {
conn.execute(
crate::queries::sqlite::queries::db_mail_preferences::Q_002,
[],
)
.map_err(|error| format!("Failed to initialize mail preferences: {error}"))?;
conn.execute(crate::dbq!(db_mail_preferences::Q_002), [])
.map_err(|error| format!("Failed to initialize mail preferences: {error}"))?;
if let Some(value) = data.messages_per_page
&& !(20..=200).contains(&value)
@@ -130,8 +123,8 @@ pub fn update_preferences(
.unwrap_or(current.sync_imap_keywords);
conn.execute(
crate::queries::sqlite::queries::db_mail_preferences::Q_003,
rusqlite::params![
crate::dbq!(db_mail_preferences::Q_003),
params![
messages_per_page,
mark_read_delay_ms,
remote_images,
+76 -74
View File
@@ -1,5 +1,7 @@
use crate::db::pool::{
Connection, DatabaseBackend, DbValue, ToDbValue, configured_backend, params,
};
use crate::queries::sqlite::queries::db_messages::MSG_SELECT_COLS;
use rusqlite::{Connection, params};
use serde::Serialize;
#[derive(Debug, Clone)]
@@ -35,7 +37,9 @@ pub struct CachedMessage {
pub reaction: Option<String>,
}
fn row_to_cached_message(row: &rusqlite::Row<'_>) -> rusqlite::Result<CachedMessage> {
fn row_to_cached_message(
row: &crate::db::pool::Row<'_>,
) -> Result<CachedMessage, crate::db::pool::DbError> {
let has_attachments_int: i32 = row.get(13)?;
Ok(CachedMessage {
uid: row.get(0)?,
@@ -79,7 +83,7 @@ pub fn upsert_message(
) -> Result<(), String> {
let date_epoch = parse_date_to_epoch(date);
conn.execute(
crate::queries::sqlite::queries::db_messages::Q_001,
crate::dbq!(db_messages::Q_001),
params![
uid,
folder,
@@ -105,37 +109,30 @@ pub fn upsert_message(
}
pub fn remove_after_mutation(conn: &Connection, folder: &str, uid: u32) -> Result<(), String> {
let transaction =
rusqlite::Transaction::new_unchecked(conn, rusqlite::TransactionBehavior::Immediate)
.map_err(|e| e.to_string())?;
delete_message(conn, folder, uid)?;
let transaction = conn.unchecked_transaction().map_err(|e| e.to_string())?;
delete_message(&transaction, folder, uid)?;
super::header_sync::invalidate(&transaction)?;
super::folders::invalidate_folder_freshness(conn, folder)?;
super::folders::invalidate_folder_freshness(&transaction, folder)?;
transaction.commit().map_err(|e| e.to_string())
}
pub fn delete_folder_messages(conn: &Connection, folder: &str) -> Result<usize, String> {
let deleted = conn
.execute(
crate::queries::sqlite::queries::db_messages::Q_002,
params![folder],
)
.execute(crate::dbq!(db_messages::Q_002), params![folder])
.map_err(|e| format!("Failed to delete folder messages: {e}"))?;
Ok(deleted)
}
pub fn max_uid(conn: &Connection, folder: &str) -> Result<u32, String> {
conn.query_row(
crate::queries::sqlite::queries::db_messages::Q_003,
params![folder],
|row| row.get(0),
)
conn.query_row(crate::dbq!(db_messages::Q_003), params![folder], |row| {
row.get(0)
})
.map_err(|e| format!("Failed to get max uid: {e}"))
}
pub fn list_index_source_messages(conn: &Connection) -> Result<Vec<IndexSourceMessage>, String> {
let mut stmt = conn
.prepare(crate::queries::sqlite::queries::db_messages::Q_004)
.prepare(crate::dbq!(db_messages::Q_004))
.map_err(|e| format!("Failed to prepare index rebuild query: {e}"))?;
let rows = stmt
@@ -253,7 +250,7 @@ pub fn get_messages(
per_page: u32,
) -> Result<Vec<CachedMessage>, String> {
let offset = page * per_page;
let sql = crate::rustbox_query_db_messages_001!();
let sql = crate::dbq!(db_messages::FORMAT_001).replace("{MSG_SELECT_COLS}", MSG_SELECT_COLS);
let mut stmt = conn
.prepare(&sql)
@@ -298,13 +295,20 @@ pub fn get_messages_sorted(
_ => return Err(format!("Unsupported message sort: {sort}")),
};
let order_by = if flagged_on_top {
format!("CASE WHEN instr(flags, '\\Flagged') > 0 THEN 0 ELSE 1 END ASC, {base_order}")
let flagged_test = match configured_backend() {
DatabaseBackend::Mysql => "LOCATE(CONCAT(CHAR(92), 'Flagged'), flags) > 0",
DatabaseBackend::Pgsql => "strpos(flags, '\\Flagged') > 0",
DatabaseBackend::Sqlite => "instr(flags, '\\Flagged') > 0",
};
format!("CASE WHEN {flagged_test} THEN 0 ELSE 1 END ASC, {base_order}")
} else {
base_order.to_string()
};
let offset = page * per_page;
let sql = crate::rustbox_query_db_messages_002!(&order_by);
let sql = crate::dbq!(db_messages::FORMAT_002)
.replace("{MSG_SELECT_COLS}", MSG_SELECT_COLS)
.replace("{order_by}", &order_by);
let mut stmt = conn
.prepare(&sql)
.map_err(|e| format!("Failed to prepare sorted get_messages: {e}"))?;
@@ -316,21 +320,15 @@ pub fn get_messages_sorted(
}
pub fn count_messages(conn: &Connection, folder: &str) -> Result<u32, String> {
conn.query_row(
crate::queries::sqlite::queries::db_messages::Q_005,
params![folder],
|row| row.get(0),
)
conn.query_row(crate::dbq!(db_messages::Q_005), params![folder], |row| {
row.get(0)
})
.map_err(|e| format!("Failed to count messages: {e}"))
}
pub fn count_all_messages(conn: &Connection) -> Result<usize, String> {
let count: i64 = conn
.query_row(
crate::queries::sqlite::queries::db_messages::Q_006,
[],
|row| row.get(0),
)
.query_row(crate::dbq!(db_messages::Q_006), [], |row| row.get(0))
.map_err(|e| format!("Failed to count cached messages: {e}"))?;
usize::try_from(count).map_err(|_| "Cached message count is out of range".to_string())
}
@@ -342,7 +340,7 @@ pub fn update_message_flags(
flags_csv: &str,
) -> Result<(), String> {
conn.execute(
crate::queries::sqlite::queries::db_messages::Q_007,
crate::dbq!(db_messages::Q_007),
params![flags_csv, folder, uid],
)
.map_err(|e| format!("Failed to update message flags: {e}"))?;
@@ -361,7 +359,7 @@ pub fn cache_message_body(
email_theme: Option<i32>,
) -> Result<(), String> {
conn.execute(
crate::queries::sqlite::queries::db_messages::Q_008,
crate::dbq!(db_messages::Q_008),
params![
html,
text,
@@ -383,7 +381,7 @@ pub fn update_email_theme(
email_theme: i32,
) -> Result<(), String> {
conn.execute(
crate::queries::sqlite::queries::db_messages::Q_009,
crate::dbq!(db_messages::Q_009),
params![email_theme, folder, uid],
)
.map_err(|e| format!("Failed to update email theme: {e}"))?;
@@ -404,7 +402,7 @@ pub fn get_cached_body(
uid: u32,
) -> Result<Option<CachedBody>, String> {
let result = conn.query_row(
crate::queries::sqlite::queries::db_messages::Q_010,
crate::dbq!(db_messages::Q_010),
params![folder, uid],
|row| {
let cached: i32 = row.get(0)?;
@@ -424,7 +422,7 @@ pub fn get_cached_body(
match result {
Ok(body) => Ok(body),
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
Err(crate::db::pool::DbError::QueryReturnedNoRows) => Ok(None),
Err(e) => Err(format!("Failed to get cached body: {e}")),
}
}
@@ -434,29 +432,26 @@ pub fn get_single_message(
folder: &str,
uid: u32,
) -> Result<Option<CachedMessage>, String> {
let sql = crate::rustbox_query_db_messages_003!();
let sql = crate::dbq!(db_messages::FORMAT_003).replace("{MSG_SELECT_COLS}", MSG_SELECT_COLS);
let result = conn.query_row(&sql, params![folder, uid], row_to_cached_message);
match result {
Ok(msg) => Ok(Some(msg)),
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
Err(crate::db::pool::DbError::QueryReturnedNoRows) => Ok(None),
Err(e) => Err(format!("Failed to get single message: {e}")),
}
}
pub fn delete_message(conn: &Connection, folder: &str, uid: u32) -> Result<(), String> {
conn.execute(
crate::queries::sqlite::queries::db_messages::Q_011,
params![folder, uid],
)
.map_err(|e| format!("Failed to delete message: {e}"))?;
conn.execute(crate::dbq!(db_messages::Q_011), params![folder, uid])
.map_err(|e| format!("Failed to delete message: {e}"))?;
Ok(())
}
pub fn get_unindexed_messages(conn: &Connection, limit: u32) -> Result<Vec<(String, u32)>, String> {
let mut stmt = conn
.prepare(crate::queries::sqlite::queries::db_messages::Q_012)
.prepare(crate::dbq!(db_messages::Q_012))
.map_err(|e| format!("Failed to prepare get_unindexed_messages: {e}"))?;
let rows = stmt
@@ -477,7 +472,7 @@ pub fn get_all_uids_and_flags(
folder: &str,
) -> Result<Vec<(u32, String)>, String> {
let mut stmt = conn
.prepare(crate::queries::sqlite::queries::db_messages::Q_013)
.prepare(crate::dbq!(db_messages::Q_013))
.map_err(|e| format!("Failed to prepare get_all_uids_and_flags: {e}"))?;
let rows = stmt
.query_map(params![folder], |row| {
@@ -496,9 +491,12 @@ pub fn get_thread_messages(
conn: &Connection,
target_message_id: &str,
) -> Result<Vec<CachedMessage>, String> {
let escaped_target = target_message_id.replace("%", "\\%").replace("_", "\\_");
let escaped_target = target_message_id
.replace('!', "!!")
.replace("%", "!%")
.replace("_", "!_");
let like_pattern = format!("%{}%", escaped_target);
let sql = crate::rustbox_query_db_messages_004!();
let sql = crate::dbq!(db_messages::FORMAT_004).replace("{MSG_SELECT_COLS}", MSG_SELECT_COLS);
let mut stmt = conn
.prepare(&sql)
@@ -606,7 +604,7 @@ pub fn search_messages_sqlite(
limit: usize,
) -> Result<Vec<CachedMessage>, String> {
let mut conditions = Vec::new();
let mut param_values: Vec<Box<dyn rusqlite::types::ToSql>> = Vec::new();
let mut param_values: Vec<DbValue> = Vec::new();
let mut idx = 1u32;
conditions.push("LOWER(folder) NOT IN ('trash', 'spam', 'junk')".to_string());
@@ -616,19 +614,19 @@ pub fn search_messages_sqlite(
conditions.push(format!(
"(subject LIKE ?{idx} OR from_name LIKE ?{idx} OR from_address LIKE ?{idx} OR to_addresses LIKE ?{idx})"
));
param_values.push(Box::new(pattern));
param_values.push(pattern.to_db_value());
idx += 1;
}
if let Some(subject) = subject.filter(|value| !value.trim().is_empty()) {
conditions.push(format!("subject LIKE ?{idx}"));
param_values.push(Box::new(format!("%{subject}%")));
param_values.push(format!("%{subject}%").to_db_value());
idx += 1;
}
if let Some(f) = folder {
conditions.push(format!("folder = ?{idx}"));
param_values.push(Box::new(f.to_string()));
param_values.push(f.to_db_value());
idx += 1;
}
@@ -643,47 +641,57 @@ pub fn search_messages_sqlite(
} else {
format!("%{f}%")
};
param_values.push(Box::new(val));
param_values.push(val.to_db_value());
idx += 1;
}
if let Some(t) = to {
conditions.push(format!("to_addresses LIKE ?{idx}"));
param_values.push(Box::new(format!("%{t}%")));
param_values.push(format!("%{t}%").to_db_value());
idx += 1;
}
if let Some(df) = date_from {
conditions.push(format!("date_epoch >= ?{idx}"));
param_values.push(Box::new(df));
param_values.push(df.to_db_value());
idx += 1;
}
if let Some(dt) = date_to {
conditions.push(format!("date_epoch <= ?{idx}"));
param_values.push(Box::new(dt));
param_values.push(dt.to_db_value());
idx += 1;
}
if let Some(ha) = has_attachment {
conditions.push(format!("has_attachments = ?{idx}"));
param_values.push(Box::new(ha as i32));
param_values.push((ha as i32).to_db_value());
idx += 1;
}
if let Some(want_unread) = unread {
let seen = match configured_backend() {
DatabaseBackend::Mysql => "LOCATE(CONCAT(CHAR(92), 'Seen'), flags)",
DatabaseBackend::Pgsql => "strpos(flags, '\\Seen')",
DatabaseBackend::Sqlite => "instr(flags, '\\Seen')",
};
conditions.push(if want_unread {
"flags NOT LIKE '%\\Seen%'".to_string()
format!("{seen} = 0")
} else {
"flags LIKE '%\\Seen%'".to_string()
format!("{seen} > 0")
});
}
if let Some(want_flagged) = flagged {
let flagged = match configured_backend() {
DatabaseBackend::Mysql => "LOCATE(CONCAT(CHAR(92), 'Flagged'), flags)",
DatabaseBackend::Pgsql => "strpos(flags, '\\Flagged')",
DatabaseBackend::Sqlite => "instr(flags, '\\Flagged')",
};
conditions.push(if want_flagged {
"flags LIKE '%\\Flagged%'".to_string()
format!("{flagged} > 0")
} else {
"flags NOT LIKE '%\\Flagged%'".to_string()
format!("{flagged} = 0")
});
}
@@ -695,21 +703,19 @@ pub fn search_messages_sqlite(
format!("WHERE {}", conditions.join(" AND "))
};
let sql = crate::rustbox_query_db_messages_005!(&where_clause, param_values.len() + 1);
let sql = format!(
"SELECT {MSG_SELECT_COLS} FROM messages {where_clause} ORDER BY date_epoch DESC LIMIT ?{}",
param_values.len() + 1
);
param_values.push(Box::new(limit as i64));
let params_refs: Vec<&dyn rusqlite::types::ToSql> =
param_values.iter().map(|b| b.as_ref()).collect();
param_values.push((limit as i64).to_db_value());
let mut stmt = conn
.prepare(&sql)
.map_err(|e| format!("Failed to prepare search query: {e}"))?;
let rows = stmt
.query_map(rusqlite::params_from_iter(params_refs), |row| {
row_to_cached_message(row)
})
.query_map(param_values, row_to_cached_message)
.map_err(|e| format!("Failed to execute search query: {e}"))?;
let mut results = Vec::new();
@@ -720,12 +726,8 @@ pub fn search_messages_sqlite(
}
pub fn cached_storage_bytes(conn: &Connection) -> Result<Option<u64>, String> {
conn.query_row(
crate::queries::sqlite::queries::db_messages::Q_014,
[],
|row| row.get(0),
)
.map_err(|e| e.to_string())
conn.query_row(crate::dbq!(db_messages::Q_014), [], |row| row.get(0))
.map_err(|e| e.to_string())
}
#[cfg(test)]
+32 -41
View File
@@ -1,4 +1,4 @@
use rusqlite::Connection;
use crate::db::pool::{Connection, DbValue, ToDbValue};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize)]
@@ -23,28 +23,24 @@ pub struct UpdateNotificationPreferences {
}
pub fn get_preferences(conn: &Connection) -> Result<NotificationPreferences, String> {
let result = conn.query_row(
crate::queries::sqlite::queries::db_notification_preferences::Q_001,
[],
|row| {
let enabled_int: i32 = row.get(0)?;
let sound_int: i32 = row.get(1)?;
let folders_json: String = row.get(2)?;
let dnd_enabled_int: i32 = row.get(3)?;
let dnd_start: String = row.get(4)?;
let dnd_end: String = row.get(5)?;
let updated_at: String = row.get(6)?;
Ok((
enabled_int,
sound_int,
folders_json,
dnd_enabled_int,
dnd_start,
dnd_end,
updated_at,
))
},
);
let result = conn.query_row(crate::dbq!(db_notification_preferences::Q_001), [], |row| {
let enabled_int: i32 = row.get(0)?;
let sound_int: i32 = row.get(1)?;
let folders_json: String = row.get(2)?;
let dnd_enabled_int: i32 = row.get(3)?;
let dnd_start: String = row.get(4)?;
let dnd_end: String = row.get(5)?;
let updated_at: String = row.get(6)?;
Ok((
enabled_int,
sound_int,
folders_json,
dnd_enabled_int,
dnd_start,
dnd_end,
updated_at,
))
});
match result {
Ok((
@@ -68,7 +64,7 @@ pub fn get_preferences(conn: &Connection) -> Result<NotificationPreferences, Str
updated_at,
})
}
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(NotificationPreferences {
Err(crate::db::pool::DbError::QueryReturnedNoRows) => Ok(NotificationPreferences {
enabled: true,
sound: false,
folders: vec!["INBOX".to_string()],
@@ -85,36 +81,33 @@ pub fn update_preferences(
conn: &Connection,
data: &UpdateNotificationPreferences,
) -> Result<NotificationPreferences, String> {
conn.execute(
crate::queries::sqlite::queries::db_notification_preferences::Q_002,
[],
)
.map_err(|e| format!("Failed to ensure preferences row: {e}"))?;
conn.execute(crate::dbq!(db_notification_preferences::Q_002), [])
.map_err(|e| format!("Failed to ensure preferences row: {e}"))?;
let mut sets = Vec::new();
let mut values: Vec<Box<dyn rusqlite::types::ToSql>> = Vec::new();
let mut values: Vec<DbValue> = Vec::new();
let mut idx = 1;
if let Some(enabled) = data.enabled {
sets.push(format!("enabled = ?{idx}"));
values.push(Box::new(enabled as i32));
values.push((enabled as i32).to_db_value());
idx += 1;
}
if let Some(sound) = data.sound {
sets.push(format!("sound = ?{idx}"));
values.push(Box::new(sound as i32));
values.push((sound as i32).to_db_value());
idx += 1;
}
if let Some(ref folders) = data.folders {
let folders_json = serde_json::to_string(folders)
.map_err(|e| format!("Failed to serialize folders: {e}"))?;
sets.push(format!("folders = ?{idx}"));
values.push(Box::new(folders_json));
values.push(folders_json.to_db_value());
idx += 1;
}
if let Some(dnd_enabled) = data.dnd_enabled {
sets.push(format!("dnd_enabled = ?{idx}"));
values.push(Box::new(dnd_enabled as i32));
values.push((dnd_enabled as i32).to_db_value());
idx += 1;
}
if let Some(ref dnd_start) = data.dnd_start {
@@ -122,7 +115,7 @@ pub fn update_preferences(
return Err("Invalid DND start time".to_string());
}
sets.push(format!("dnd_start = ?{idx}"));
values.push(Box::new(dnd_start.clone()));
values.push(dnd_start.to_db_value());
idx += 1;
}
if let Some(ref dnd_end) = data.dnd_end {
@@ -130,7 +123,7 @@ pub fn update_preferences(
return Err("Invalid DND end time".to_string());
}
sets.push(format!("dnd_end = ?{idx}"));
values.push(Box::new(dnd_end.clone()));
values.push(dnd_end.to_db_value());
idx += 1;
}
@@ -140,12 +133,10 @@ pub fn update_preferences(
sets.push("updated_at = datetime('now')".to_string());
let set_clause = sets.join(", ");
let sql = crate::rustbox_query_db_notification_preferences_001!(&set_clause, idx);
values.push(Box::new(1_i32));
let sql = format!("UPDATE notification_preferences SET {set_clause} WHERE id = ?{idx}");
values.push(1_i32.to_db_value());
let params_refs: Vec<&dyn rusqlite::types::ToSql> = values.iter().map(|v| v.as_ref()).collect();
conn.execute(&sql, params_refs.as_slice())
conn.execute(&sql, values)
.map_err(|e| format!("Failed to update notification preferences: {e}"))?;
get_preferences(conn)
+14 -14
View File
@@ -1,5 +1,5 @@
use crate::db::pool::{Connection, params};
use crate::queries::sqlite::queries::db_pgp::KEY_COLS;
use rusqlite::{Connection, params};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize)]
@@ -31,7 +31,7 @@ pub struct PgpPreferences {
pub require_trusted_encryption: bool,
}
fn row_to_key(row: &rusqlite::Row<'_>) -> rusqlite::Result<PgpKeyInternal> {
fn row_to_key(row: &crate::db::pool::Row<'_>) -> Result<PgpKeyInternal, crate::db::pool::DbError> {
Ok(PgpKeyInternal {
record: PgpKeyRecord {
fingerprint: row.get(0)?,
@@ -51,7 +51,7 @@ fn row_to_key(row: &rusqlite::Row<'_>) -> rusqlite::Result<PgpKeyInternal> {
pub fn list_keys(conn: &Connection) -> Result<Vec<PgpKeyRecord>, String> {
let mut stmt = conn
.prepare(&crate::rustbox_query_db_pgp_001!())
.prepare(&crate::dbq!(db_pgp::FORMAT_001).replace("{KEY_COLS}", KEY_COLS))
.map_err(|e| format!("Failed to prepare PGP key list: {e}"))?;
let rows = stmt
.query_map([], row_to_key)
@@ -65,17 +65,17 @@ pub fn list_keys(conn: &Connection) -> Result<Vec<PgpKeyRecord>, String> {
pub fn get_key(conn: &Connection, fingerprint: &str) -> Result<Option<PgpKeyInternal>, String> {
let normalized = normalize_fingerprint(fingerprint);
let sql = crate::rustbox_query_db_pgp_002!();
let sql = crate::dbq!(db_pgp::FORMAT_002).replace("{KEY_COLS}", KEY_COLS);
match conn.query_row(&sql, params![normalized], row_to_key) {
Ok(v) => Ok(Some(v)),
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
Err(crate::db::pool::DbError::QueryReturnedNoRows) => Ok(None),
Err(e) => Err(format!("Failed to get PGP key: {e}")),
}
}
pub fn find_keys_by_email(conn: &Connection, email: &str) -> Result<Vec<PgpKeyRecord>, String> {
let mut stmt = conn
.prepare(&crate::rustbox_query_db_pgp_003!())
.prepare(&crate::dbq!(db_pgp::FORMAT_003).replace("{KEY_COLS}", KEY_COLS))
.map_err(|e| format!("Failed to prepare PGP email lookup: {e}"))?;
let rows = stmt
.query_map(params![email.trim()], row_to_key)
@@ -93,7 +93,7 @@ pub fn upsert_key(
secret_key_path: Option<&str>,
) -> Result<(), String> {
conn.execute(
crate::queries::sqlite::queries::db_pgp::Q_001,
crate::dbq!(db_pgp::Q_001),
params![
record.fingerprint,
record.key_id,
@@ -113,7 +113,7 @@ pub fn upsert_key(
pub fn delete_key(conn: &Connection, fingerprint: &str) -> Result<bool, String> {
Ok(conn
.execute(
crate::queries::sqlite::queries::db_pgp::Q_002,
crate::dbq!(db_pgp::Q_002),
params![normalize_fingerprint(fingerprint)],
)
.map_err(|e| format!("Failed to delete PGP key: {e}"))?
@@ -126,7 +126,7 @@ pub fn set_trust(conn: &Connection, fingerprint: &str, trust: &str) -> Result<bo
}
Ok(conn
.execute(
crate::queries::sqlite::queries::db_pgp::Q_003,
crate::dbq!(db_pgp::Q_003),
params![normalize_fingerprint(fingerprint), trust],
)
.map_err(|e| format!("Failed to update PGP trust: {e}"))?
@@ -134,7 +134,7 @@ pub fn set_trust(conn: &Connection, fingerprint: &str, trust: &str) -> Result<bo
}
pub fn get_preferences(conn: &Connection) -> Result<PgpPreferences, String> {
conn.query_row(crate::queries::sqlite::queries::db_pgp::Q_004, [], |row| {
conn.query_row(crate::dbq!(db_pgp::Q_004), [], |row| {
Ok(PgpPreferences {
wkd_enabled: row.get::<_, i32>(0)? != 0,
hkp_enabled: row.get::<_, i32>(1)? != 0,
@@ -149,7 +149,7 @@ pub fn update_preferences(
prefs: &PgpPreferences,
) -> Result<PgpPreferences, String> {
conn.execute(
crate::queries::sqlite::queries::db_pgp::Q_005,
crate::dbq!(db_pgp::Q_005),
params![
prefs.wkd_enabled as i32,
prefs.hkp_enabled as i32,
@@ -172,17 +172,17 @@ pub fn find_key_internal_by_email(
conn: &Connection,
email: &str,
) -> Result<Option<PgpKeyInternal>, String> {
let sql = crate::rustbox_query_db_pgp_004!();
let sql = crate::dbq!(db_pgp::FORMAT_004).replace("{KEY_COLS}", KEY_COLS);
match conn.query_row(&sql, params![email.trim()], row_to_key) {
Ok(value) => Ok(Some(value)),
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
Err(crate::db::pool::DbError::QueryReturnedNoRows) => Ok(None),
Err(e) => Err(format!("Failed to query PGP key by email: {e}")),
}
}
pub fn list_secret_keys(conn: &Connection) -> Result<Vec<PgpKeyInternal>, String> {
let mut stmt = conn
.prepare(&crate::rustbox_query_db_pgp_005!())
.prepare(&crate::dbq!(db_pgp::FORMAT_005).replace("{KEY_COLS}", KEY_COLS))
.map_err(|e| format!("Failed to prepare PGP secret key list: {e}"))?;
let rows = stmt
.query_map([], row_to_key)
+2361 -290
View File
File diff suppressed because it is too large. Load diff
+20 -23
View File
@@ -1,4 +1,4 @@
use rusqlite::{Connection, params};
use crate::db::pool::{Connection, params};
use serde::Serialize;
#[derive(Debug, Clone, Serialize)]
@@ -11,7 +11,7 @@ pub struct ResponseTemplate {
pub updated_at: String,
}
fn from_row(row: &rusqlite::Row<'_>) -> rusqlite::Result<ResponseTemplate> {
fn from_row(row: &crate::db::pool::Row<'_>) -> Result<ResponseTemplate, crate::db::pool::DbError> {
Ok(ResponseTemplate {
id: row.get(0)?,
name: row.get(1)?,
@@ -24,7 +24,7 @@ fn from_row(row: &rusqlite::Row<'_>) -> rusqlite::Result<ResponseTemplate> {
pub fn list(conn: &Connection) -> Result<Vec<ResponseTemplate>, String> {
let mut stmt = conn
.prepare(crate::queries::sqlite::queries::db_response_templates::Q_001)
.prepare(crate::dbq!(db_response_templates::Q_001))
.map_err(|e| format!("Failed to prepare response templates: {e}"))?;
let rows = stmt
.query_map([], from_row)
@@ -35,12 +35,12 @@ pub fn list(conn: &Connection) -> Result<Vec<ResponseTemplate>, String> {
pub fn get(conn: &Connection, id: i64) -> Result<Option<ResponseTemplate>, String> {
match conn.query_row(
crate::queries::sqlite::queries::db_response_templates::Q_002,
crate::dbq!(db_response_templates::Q_002),
params![id],
from_row,
) {
Ok(value) => Ok(Some(value)),
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
Err(crate::db::pool::DbError::QueryReturnedNoRows) => Ok(None),
Err(e) => Err(format!("Failed to get response template: {e}")),
}
}
@@ -66,19 +66,19 @@ pub fn create(
"text" | "" => "text",
_ => return Err("Invalid response template format".to_string()),
};
conn.execute(
crate::queries::sqlite::queries::db_response_templates::Q_003,
params![name, body, format],
)
.map_err(|e| {
if e.to_string().contains("UNIQUE constraint failed") {
"A response template with this name already exists".to_string()
} else {
format!("Failed to create response template: {e}")
}
})?;
get(conn, conn.last_insert_rowid())?
.ok_or_else(|| "Failed to read response template".to_string())
let id = conn
.insert_id(
crate::dbq!(db_response_templates::Q_003),
params![name, body, format],
)
.map_err(|e| {
if e.to_string().contains("UNIQUE constraint failed") {
"A response template with this name already exists".to_string()
} else {
format!("Failed to create response template: {e}")
}
})?;
get(conn, id)?.ok_or_else(|| "Failed to read response template".to_string())
}
pub fn update(
@@ -105,7 +105,7 @@ pub fn update(
};
let changed = conn
.execute(
crate::queries::sqlite::queries::db_response_templates::Q_004,
crate::dbq!(db_response_templates::Q_004),
params![name, body, format, id],
)
.map_err(|e| {
@@ -123,10 +123,7 @@ pub fn update(
pub fn delete(conn: &Connection, id: i64) -> Result<bool, String> {
Ok(conn
.execute(
crate::queries::sqlite::queries::db_response_templates::Q_005,
params![id],
)
.execute(crate::dbq!(db_response_templates::Q_005), params![id])
.map_err(|e| format!("Failed to delete response template: {e}"))?
> 0)
}
+13 -16
View File
@@ -1,4 +1,4 @@
use rusqlite::{Connection, params};
use crate::db::pool::{Connection, params};
use serde::Serialize;
#[derive(Debug, Clone, Serialize)]
@@ -10,7 +10,7 @@ pub struct SavedSearch {
pub updated_at: String,
}
fn row_to_saved(row: &rusqlite::Row<'_>) -> rusqlite::Result<SavedSearch> {
fn row_to_saved(row: &crate::db::pool::Row<'_>) -> Result<SavedSearch, crate::db::pool::DbError> {
Ok(SavedSearch {
id: row.get(0)?,
name: row.get(1)?,
@@ -22,7 +22,7 @@ fn row_to_saved(row: &rusqlite::Row<'_>) -> rusqlite::Result<SavedSearch> {
pub fn list(conn: &Connection) -> Result<Vec<SavedSearch>, String> {
let mut stmt = conn
.prepare(crate::queries::sqlite::queries::db_saved_searches::Q_001)
.prepare(crate::dbq!(db_saved_searches::Q_001))
.map_err(|e| format!("Failed to prepare saved searches: {e}"))?;
let rows = stmt
.query_map([], row_to_saved)
@@ -37,12 +37,12 @@ pub fn create(conn: &Connection, name: &str, query_string: &str) -> Result<Saved
if name.is_empty() || query_string.is_empty() {
return Err("Saved search name and query are required".to_string());
}
conn.execute(
crate::queries::sqlite::queries::db_saved_searches::Q_002,
params![name, query_string],
)
.map_err(|e| format!("Failed to save search: {e}"))?;
let id = conn.last_insert_rowid();
let id = conn
.insert_id(
crate::dbq!(db_saved_searches::Q_002),
params![name, query_string],
)
.map_err(|e| format!("Failed to save search: {e}"))?;
get(conn, id)?.ok_or_else(|| "Failed to read saved search".to_string())
}
@@ -59,7 +59,7 @@ pub fn update(
}
let changed = conn
.execute(
crate::queries::sqlite::queries::db_saved_searches::Q_003,
crate::dbq!(db_saved_searches::Q_003),
params![name, query_string, id],
)
.map_err(|e| format!("Failed to update saved search: {e}"))?;
@@ -71,22 +71,19 @@ pub fn update(
pub fn get(conn: &Connection, id: i64) -> Result<Option<SavedSearch>, String> {
match conn.query_row(
crate::queries::sqlite::queries::db_saved_searches::Q_004,
crate::dbq!(db_saved_searches::Q_004),
params![id],
row_to_saved,
) {
Ok(value) => Ok(Some(value)),
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
Err(crate::db::pool::DbError::QueryReturnedNoRows) => Ok(None),
Err(e) => Err(format!("Failed to get saved search: {e}")),
}
}
pub fn delete(conn: &Connection, id: i64) -> Result<bool, String> {
Ok(conn
.execute(
crate::queries::sqlite::queries::db_saved_searches::Q_005,
params![id],
)
.execute(crate::dbq!(db_saved_searches::Q_005), params![id])
.map_err(|e| format!("Failed to delete saved search: {e}"))?
> 0)
}
+13 -23
View File
@@ -1,4 +1,4 @@
use rusqlite::{Connection, OptionalExtension, params};
use crate::db::pool::{Connection, OptionalExtension, params};
use serde::{Deserialize, Serialize};
const ROLES: &[&str] = &["sent", "drafts", "trash", "junk", "archive"];
@@ -20,11 +20,9 @@ pub fn get_role(conn: &Connection, role: &str) -> Result<Option<String>, String>
if !valid_role(role) {
return Err(format!("Invalid special folder role: {role}"));
}
conn.query_row(
crate::queries::sqlite::queries::db_special_folders::Q_001,
[role],
|row| row.get(0),
)
conn.query_row(crate::dbq!(db_special_folders::Q_001), [role], |row| {
row.get(0)
})
.optional()
.map_err(|e| format!("Failed to read special folder {role}: {e}"))
}
@@ -46,27 +44,22 @@ pub fn set_role(conn: &Connection, role: &str, folder_name: Option<&str>) -> Res
match folder_name.map(str::trim).filter(|value| !value.is_empty()) {
Some(folder) => {
let exists: bool = conn
.query_row(
crate::queries::sqlite::queries::db_special_folders::Q_002,
[folder],
|row| row.get(0),
)
.query_row(crate::dbq!(db_special_folders::Q_002), [folder], |row| {
row.get(0)
})
.map_err(|e| format!("Failed to validate folder {folder}: {e}"))?;
if !exists {
return Err(format!("Folder does not exist: {folder}"));
}
conn.execute(
crate::queries::sqlite::queries::db_special_folders::Q_003,
crate::dbq!(db_special_folders::Q_003),
params![role, folder],
)
.map_err(|e| format!("Failed to set special folder {role}: {e}"))?;
}
None => {
conn.execute(
crate::queries::sqlite::queries::db_special_folders::Q_004,
[role],
)
.map_err(|e| format!("Failed to clear special folder {role}: {e}"))?;
conn.execute(crate::dbq!(db_special_folders::Q_004), [role])
.map_err(|e| format!("Failed to clear special folder {role}: {e}"))?;
}
}
Ok(())
@@ -87,7 +80,7 @@ pub fn rename_folder_reference(
new_name: &str,
) -> Result<(), String> {
conn.execute(
crate::queries::sqlite::queries::db_special_folders::Q_005,
crate::dbq!(db_special_folders::Q_005),
params![new_name, old_name],
)
.map_err(|e| format!("Failed to update special folder reference: {e}"))?;
@@ -95,11 +88,8 @@ pub fn rename_folder_reference(
}
pub fn clear_folder_reference(conn: &Connection, folder_name: &str) -> Result<(), String> {
conn.execute(
crate::queries::sqlite::queries::db_special_folders::Q_006,
[folder_name],
)
.map_err(|e| format!("Failed to clear special folder reference: {e}"))?;
conn.execute(crate::dbq!(db_special_folders::Q_006), [folder_name])
.map_err(|e| format!("Failed to clear special folder reference: {e}"))?;
Ok(())
}
+26 -43
View File
@@ -1,6 +1,6 @@
use std::collections::HashMap;
use rusqlite::{Connection, params};
use crate::db::pool::{Connection, DbValue, ToDbValue, params};
use serde::Serialize;
use crate::db::messages::CachedMessage;
@@ -33,7 +33,7 @@ pub fn imap_keyword_for_tag_id(id: &str) -> String {
pub fn create_tag(conn: &Connection, id: &str, name: &str, color: &str) -> Result<(), String> {
let imap_keyword = imap_keyword_for_tag_id(id);
conn.execute(
crate::queries::sqlite::queries::db_tags::Q_001,
crate::dbq!(db_tags::Q_001),
params![id, name, color, imap_keyword],
)
.map_err(|e| format!("Failed to create tag: {e}"))?;
@@ -42,7 +42,7 @@ pub fn create_tag(conn: &Connection, id: &str, name: &str, color: &str) -> Resul
pub fn list_tags(conn: &Connection) -> Result<Vec<Tag>, String> {
let mut stmt = conn
.prepare(crate::queries::sqlite::queries::db_tags::Q_002)
.prepare(crate::dbq!(db_tags::Q_002))
.map_err(|e| format!("Failed to prepare list_tags: {e}"))?;
let rows = stmt
@@ -76,7 +76,7 @@ pub fn get_message_refs_by_tag(
tag_id: &str,
) -> Result<Vec<(u32, String)>, String> {
let mut stmt = conn
.prepare(crate::queries::sqlite::queries::db_tags::Q_003)
.prepare(crate::dbq!(db_tags::Q_003))
.map_err(|e| format!("Failed to prepare tag message refs: {e}"))?;
let rows = stmt
.query_map(params![tag_id], |row| Ok((row.get(0)?, row.get(1)?)))
@@ -146,17 +146,14 @@ pub fn reconcile_imap_keywords(
pub fn update_tag(conn: &Connection, id: &str, name: &str, color: &str) -> Result<bool, String> {
let updated = conn
.execute(
crate::queries::sqlite::queries::db_tags::Q_004,
params![name, color, id],
)
.execute(crate::dbq!(db_tags::Q_004), params![name, color, id])
.map_err(|e| format!("Failed to update tag: {e}"))?;
Ok(updated > 0)
}
pub fn delete_tag(conn: &Connection, id: &str) -> Result<bool, String> {
let deleted = conn
.execute(crate::queries::sqlite::queries::db_tags::Q_005, params![id])
.execute(crate::dbq!(db_tags::Q_005), params![id])
.map_err(|e| format!("Failed to delete tag: {e}"))?;
Ok(deleted > 0)
}
@@ -167,11 +164,8 @@ pub fn add_tag_to_message(
uid: u32,
folder: &str,
) -> Result<(), String> {
conn.execute(
crate::queries::sqlite::queries::db_tags::Q_006,
params![tag_id, uid, folder],
)
.map_err(|e| format!("Failed to add tag to message: {e}"))?;
conn.execute(crate::dbq!(db_tags::Q_006), params![tag_id, uid, folder])
.map_err(|e| format!("Failed to add tag to message: {e}"))?;
Ok(())
}
@@ -182,10 +176,7 @@ pub fn remove_tag_from_message(
folder: &str,
) -> Result<bool, String> {
let deleted = conn
.execute(
crate::queries::sqlite::queries::db_tags::Q_007,
params![tag_id, uid, folder],
)
.execute(crate::dbq!(db_tags::Q_007), params![tag_id, uid, folder])
.map_err(|e| format!("Failed to remove tag from message: {e}"))?;
Ok(deleted > 0)
}
@@ -196,7 +187,7 @@ pub fn get_message_tags(
folder: &str,
) -> Result<Vec<MessageTag>, String> {
let mut stmt = conn
.prepare(crate::queries::sqlite::queries::db_tags::Q_008)
.prepare(crate::dbq!(db_tags::Q_008))
.map_err(|e| format!("Failed to prepare get_message_tags: {e}"))?;
let rows = stmt
@@ -236,22 +227,22 @@ pub fn get_tags_for_messages(
})
.collect();
let sql = crate::rustbox_query_db_tags_001!(conditions.join(" OR "));
let sql = format!(
"SELECT mt.message_uid, mt.message_folder, t.id, t.name, t.color FROM message_tags mt JOIN tags t ON t.id = mt.tag_id WHERE {} ORDER BY t.name ASC",
conditions.join(" OR ")
);
let mut stmt = conn
.prepare(&sql)
.map_err(|e| format!("Failed to prepare get_tags_for_messages: {e}"))?;
let mut param_values: Vec<Box<dyn rusqlite::types::ToSql>> = Vec::new();
let mut param_values: Vec<DbValue> = Vec::new();
for &(uid, folder) in messages {
param_values.push(Box::new(uid));
param_values.push(Box::new(folder.to_string()));
param_values.push(uid.to_db_value());
param_values.push(folder.to_db_value());
}
let params_ref: Vec<&dyn rusqlite::types::ToSql> =
param_values.iter().map(|p| p.as_ref()).collect();
let rows = stmt
.query_map(params_ref.as_slice(), |row| {
.query_map(param_values, |row| {
let uid: u32 = row.get(0)?;
let folder: String = row.get(1)?;
let tag = MessageTag {
@@ -279,7 +270,7 @@ pub fn get_messages_by_tag(
) -> Result<Vec<CachedMessage>, String> {
let offset = page * per_page;
let mut stmt = conn
.prepare(crate::queries::sqlite::queries::db_tags::Q_009)
.prepare(crate::dbq!(db_tags::Q_009))
.map_err(|e| format!("Failed to prepare get_messages_by_tag: {e}"))?;
let rows = stmt
@@ -314,11 +305,9 @@ pub fn get_messages_by_tag(
}
pub fn count_messages_by_tag(conn: &Connection, tag_id: &str) -> Result<u32, String> {
conn.query_row(
crate::queries::sqlite::queries::db_tags::Q_010,
params![tag_id],
|row| row.get(0),
)
conn.query_row(crate::dbq!(db_tags::Q_010), params![tag_id], |row| {
row.get(0)
})
.map_err(|e| format!("Failed to count messages by tag: {e}"))
}
@@ -328,16 +317,10 @@ mod tests {
use crate::db::pool::open_test_db;
fn insert_test_message(conn: &Connection, folder: &str, uid: u32) {
conn.execute(
crate::queries::sqlite::queries::db_tags::Q_011,
params![folder],
)
.unwrap();
conn.execute(
crate::queries::sqlite::queries::db_tags::Q_012,
params![uid, folder],
)
.unwrap();
conn.execute(crate::dbq!(db_tags::Q_011), params![folder])
.unwrap();
conn.execute(crate::dbq!(db_tags::Q_012), params![uid, folder])
.unwrap();
}
#[test]
+6 -12
View File
@@ -1,8 +1,8 @@
use rusqlite::Connection;
use crate::db::pool::Connection;
pub fn list(conn: &Connection) -> Result<Vec<String>, String> {
let mut stmt = conn
.prepare(crate::queries::sqlite::queries::db_trusted_senders::Q_001)
.prepare(crate::dbq!(db_trusted_senders::Q_001))
.map_err(|e| format!("Failed to prepare trusted sender list: {e}"))?;
let rows = stmt
.query_map([], |row| row.get::<_, String>(0))
@@ -12,20 +12,14 @@ pub fn list(conn: &Connection) -> Result<Vec<String>, String> {
}
pub fn add(conn: &Connection, email: &str) -> Result<(), String> {
conn.execute(
crate::queries::sqlite::queries::db_trusted_senders::Q_002,
[email],
)
.map_err(|e| format!("Failed to trust sender: {e}"))?;
conn.execute(crate::dbq!(db_trusted_senders::Q_002), [email])
.map_err(|e| format!("Failed to trust sender: {e}"))?;
Ok(())
}
pub fn remove(conn: &Connection, email: &str) -> Result<(), String> {
conn.execute(
crate::queries::sqlite::queries::db_trusted_senders::Q_003,
[email],
)
.map_err(|e| format!("Failed to remove trusted sender: {e}"))?;
conn.execute(crate::dbq!(db_trusted_senders::Q_003), [email])
.map_err(|e| format!("Failed to remove trusted sender: {e}"))?;
Ok(())
}
+3
View File
@@ -54,6 +54,9 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
format!("invalid RustBox configuration: {message}"),
)
})?;
db::pool::configure(&loaded_config)
.await
.map_err(std::io::Error::other)?;
let config = Arc::new(loaded_config);
let persistence = session_store::from_config(&config)
+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.0\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.1\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
+71 -70
View File
@@ -11,7 +11,7 @@ pub mod db_calendar {
working_hours_end, time_format, zoom_link, teams_link, meet_link, caldav_url, \
caldav_username, caldav_enabled, updated_at";
pub const Q_001: &str = "SELECT CURRENT_TIMESTAMP";
pub const Q_001: &str = "SELECT CAST(CURRENT_TIMESTAMP AS CHAR)";
pub const Q_002: &str = "INSERT INTO calendar_events
(id, title, description, location, start_time, end_time, all_day,
recurrence_rule, attendees, organizer, status, source, source_uid,
@@ -45,7 +45,7 @@ pub mod db_calendar {
pub mod db_contact_groups {
pub const Q_001: &str = "INSERT INTO contact_groups (id, name) VALUES (?, ?)";
pub const Q_002: &str = "SELECT g.id, g.name, g.created_at, g.updated_at,
(SELECT COUNT(*) FROM contact_group_members m WHERE m.group_id = g.id) AS member_count
(SELECT CAST(COUNT(*) AS SIGNED) FROM contact_group_members m WHERE m.group_id = g.id) AS member_count
FROM contact_groups g
ORDER BY g.name ASC";
pub const Q_003: &str =
@@ -62,7 +62,7 @@ pub mod db_contact_groups {
WHERE m.group_id = ?
ORDER BY c.name ASC";
pub const Q_008: &str = "SELECT g.id, g.name, g.created_at, g.updated_at,
(SELECT COUNT(*) FROM contact_group_members m2 WHERE m2.group_id = g.id) AS member_count
(SELECT CAST(COUNT(*) AS SIGNED) FROM contact_group_members m2 WHERE m2.group_id = g.id) AS member_count
FROM contact_groups g
JOIN contact_group_members m ON m.group_id = g.id
WHERE m.contact_id = ?
@@ -75,29 +75,30 @@ pub mod db_contacts {
(id, email, name, company, notes, phone, job_title, address, website, birthday, is_favorite, last_contacted,
contact_count, source, created_at, updated_at, emails_json, phones_json, addresses_json)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET
email=excluded.email, name=excluded.name, company=excluded.company, notes=excluded.notes,
phone=excluded.phone, job_title=excluded.job_title, address=excluded.address, website=excluded.website, birthday=excluded.birthday,
is_favorite=excluded.is_favorite, last_contacted=excluded.last_contacted, contact_count=excluded.contact_count,
source=excluded.source, updated_at=excluded.updated_at, emails_json=excluded.emails_json,
phones_json=excluded.phones_json, addresses_json=excluded.addresses_json";
ON DUPLICATE KEY UPDATE
email=VALUES(email), name=VALUES(name), company=VALUES(company), notes=VALUES(notes),
phone=VALUES(phone), job_title=VALUES(job_title), address=VALUES(address), website=VALUES(website), birthday=VALUES(birthday),
is_favorite=VALUES(is_favorite), last_contacted=VALUES(last_contacted), contact_count=VALUES(contact_count),
source=VALUES(source), updated_at=VALUES(updated_at), emails_json=VALUES(emails_json),
phones_json=VALUES(phones_json), addresses_json=VALUES(addresses_json)";
pub const Q_002: &str = "DELETE FROM contacts WHERE id = ?";
pub const Q_003: &str = r#"
SELECT email, name
FROM known_addresses
WHERE (email LIKE ? ESCAPE '\' OR name LIKE ? ESCAPE '\')
WHERE (email LIKE ?1 ESCAPE '!' OR name LIKE ?1 ESCAPE '!')
AND email NOT IN (SELECT email FROM contacts)
AND NOT EXISTS (
SELECT 1 FROM contacts c, json_each(c.emails_json) alt
SELECT 1 FROM contacts c
JOIN JSON_TABLE(COALESCE(NULLIF(c.emails_json, ''), '[]'), '$[*]' COLUMNS(value VARCHAR(320) PATH '$')) alt
WHERE lower(alt.value) = lower(known_addresses.email)
)
ORDER BY email ASC
LIMIT ?
LIMIT ?2
"#;
pub const Q_004: &str = "INSERT INTO known_addresses (email, name) VALUES (?, ?)
ON CONFLICT(email) DO UPDATE SET name = excluded.name WHERE excluded.name != ''";
ON DUPLICATE KEY UPDATE name = IF(VALUES(name) <> '', VALUES(name), name)";
pub const Q_005: &str = "INSERT INTO known_addresses (email, name) VALUES (?, ?)
ON CONFLICT(email) DO UPDATE SET name = excluded.name WHERE excluded.name != ''";
ON DUPLICATE KEY UPDATE name = IF(VALUES(name) <> '', VALUES(name), name)";
pub const Q_006: &str = "UPDATE contacts
SET contact_count = contact_count + 1,
last_contacted = CURRENT_TIMESTAMP,
@@ -105,24 +106,24 @@ pub mod db_contacts {
WHERE email = ?";
pub const Q_007: &str = "INSERT IGNORE INTO contacts (id, email, name, source)
VALUES (?, ?, ?, 'auto')";
pub const Q_008: &str = "INSERT INTO contact_photos (contact_id, content_type, data, updated_at) VALUES (?, ?, ?, CURRENT_TIMESTAMP) ON CONFLICT(contact_id) DO UPDATE SET content_type=excluded.content_type, data=excluded.data, updated_at=CURRENT_TIMESTAMP";
pub const Q_008: &str = "INSERT INTO contact_photos (contact_id, content_type, data, updated_at) VALUES (?, ?, ?, CURRENT_TIMESTAMP) ON DUPLICATE KEY UPDATE content_type=VALUES(content_type), data=VALUES(data), updated_at=CURRENT_TIMESTAMP";
pub const Q_009: &str = "SELECT content_type, data FROM contact_photos WHERE contact_id = ?";
pub const Q_010: &str = "DELETE FROM contact_photos WHERE contact_id = ?";
pub const FORMAT_001: &str = "SELECT {CONTACT_SELECT_COLS} FROM contacts WHERE id = ?";
pub const FORMAT_002: &str = "SELECT {CONTACT_SELECT_COLS} FROM contacts \
WHERE lower(email) = lower(?) \
OR EXISTS (SELECT 1 FROM json_each(contacts.emails_json) WHERE lower(value) = lower(?))";
WHERE lower(email) = lower(?1) \
OR EXISTS (SELECT 1 FROM JSON_TABLE(COALESCE(NULLIF(contacts.emails_json, ''), '[]'), '$[*]' COLUMNS(value VARCHAR(320) PATH '$')) alt WHERE lower(alt.value) = lower(?1))";
pub const FORMAT_003: &str = "SELECT {CONTACT_SELECT_COLS} FROM contacts
WHERE name LIKE ? ESCAPE '\\' OR email LIKE ? ESCAPE '\\' OR emails_json LIKE ? ESCAPE '\\'
WHERE name LIKE ?1 ESCAPE '!' OR email LIKE ?1 ESCAPE '!' OR emails_json LIKE ?1 ESCAPE '!'
ORDER BY name ASC
LIMIT ? OFFSET ?";
LIMIT ?2 OFFSET ?3";
pub const FORMAT_004: &str = "SELECT {CONTACT_SELECT_COLS} FROM contacts
ORDER BY name ASC
LIMIT ? OFFSET ?";
pub const FORMAT_005: &str = "SELECT {CONTACT_SELECT_COLS} FROM contacts
WHERE name LIKE ? ESCAPE '\\' OR email LIKE ? ESCAPE '\\' OR emails_json LIKE ? ESCAPE '\\'
WHERE name LIKE ?1 ESCAPE '!' OR email LIKE ?1 ESCAPE '!' OR emails_json LIKE ?1 ESCAPE '!'
ORDER BY name ASC
LIMIT ?";
LIMIT ?2";
}
pub mod db_display_preferences {
@@ -134,29 +135,28 @@ pub mod db_display_preferences {
pub mod db_drafts {
pub const Q_001: &str = "INSERT INTO drafts (id, to_addresses, cc_addresses, bcc_addresses, subject, text_body, html_body, in_reply_to, references_header, request_mdn, priority, compose_format, pgp_mode, pgp_inline, pgp_attach_public_key, pgp_attach_recipient_keys, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
ON CONFLICT(id) DO UPDATE SET
to_addresses = excluded.to_addresses,
cc_addresses = excluded.cc_addresses,
bcc_addresses = excluded.bcc_addresses,
subject = excluded.subject,
text_body = excluded.text_body,
html_body = excluded.html_body,
in_reply_to = excluded.in_reply_to,
references_header = excluded.references_header,
request_mdn = excluded.request_mdn,
priority = excluded.priority,
compose_format = excluded.compose_format,
pgp_mode = excluded.pgp_mode,
pgp_inline = excluded.pgp_inline,
pgp_attach_public_key = excluded.pgp_attach_public_key,
pgp_attach_recipient_keys = excluded.pgp_attach_recipient_keys,
ON DUPLICATE KEY UPDATE
to_addresses = VALUES(to_addresses),
cc_addresses = VALUES(cc_addresses),
bcc_addresses = VALUES(bcc_addresses),
subject = VALUES(subject),
text_body = VALUES(text_body),
html_body = VALUES(html_body),
in_reply_to = VALUES(in_reply_to),
references_header = VALUES(references_header),
request_mdn = VALUES(request_mdn),
priority = VALUES(priority),
compose_format = VALUES(compose_format),
pgp_mode = VALUES(pgp_mode),
pgp_inline = VALUES(pgp_inline),
pgp_attach_public_key = VALUES(pgp_attach_public_key),
pgp_attach_recipient_keys = VALUES(pgp_attach_recipient_keys),
updated_at = CURRENT_TIMESTAMP";
pub const Q_002: &str = "SELECT id, to_addresses, cc_addresses, bcc_addresses, subject, text_body, html_body, in_reply_to, references_header, request_mdn, priority, compose_format, pgp_mode, pgp_inline, pgp_attach_public_key, pgp_attach_recipient_keys, remote_folder, remote_uid, remote_uid_validity, created_at, updated_at
FROM drafts WHERE id = ?";
pub const Q_003: &str = "SELECT id, to_addresses, cc_addresses, bcc_addresses, subject, text_body, html_body, in_reply_to, references_header, request_mdn, priority, compose_format, pgp_mode, pgp_inline, pgp_attach_public_key, pgp_attach_recipient_keys, remote_folder, remote_uid, remote_uid_validity, created_at, updated_at
FROM drafts ORDER BY updated_at DESC";
pub const Q_004: &str =
"UPDATE drafts SET remote_folder = ?, remote_uid = ?, remote_uid_validity = ? WHERE id = ?";
pub const Q_004: &str = "UPDATE drafts SET remote_folder = ?2, remote_uid = ?3, remote_uid_validity = ?4 WHERE id = ?1";
pub const Q_005: &str = "DELETE FROM drafts WHERE id = ?";
pub const Q_006: &str =
"INSERT INTO draft_attachments (id, draft_id, filename, content_type, size, file_path)
@@ -173,7 +173,7 @@ pub mod db_folder_preferences {
pub const Q_003: &str = "SELECT EXISTS(SELECT 1 FROM folders WHERE name = ?)";
pub const Q_004: &str = "SELECT flagged_on_top FROM mail_preferences WHERE id = 1";
pub const Q_005: &str = "SELECT EXISTS(SELECT 1 FROM folders WHERE name = ?)";
pub const Q_006: &str = "INSERT INTO folder_preferences(folder_name, flagged_on_top, updated_at) VALUES(?, ?, CURRENT_TIMESTAMP)\n ON CONFLICT(folder_name) DO UPDATE SET flagged_on_top = excluded.flagged_on_top, updated_at = CURRENT_TIMESTAMP";
pub const Q_006: &str = "INSERT INTO folder_preferences(folder_name, flagged_on_top, updated_at) VALUES(?, ?, CURRENT_TIMESTAMP)\n ON DUPLICATE KEY UPDATE flagged_on_top = VALUES(flagged_on_top), updated_at = CURRENT_TIMESTAMP";
pub const Q_007: &str = "DELETE FROM folder_preferences WHERE folder_name = ?";
}
@@ -196,17 +196,16 @@ pub mod db_folders {
pub const Q_007: &str =
"UPDATE folders SET uid_validity = ?, total_count = ?, highest_modseq = ?
WHERE name = ?";
pub const Q_008: &str =
"SELECT COUNT(*) FROM messages WHERE folder = ? AND flags NOT LIKE '%\\Seen%'";
pub const Q_008: &str = "SELECT CAST(COUNT(*) AS SIGNED) FROM messages WHERE folder = ? AND LOCATE(CONCAT(CHAR(92), 'Seen'), flags) = 0";
pub const Q_009: &str = "UPDATE folders SET unread_count = ? WHERE name = ?";
pub const Q_010: &str = "UPDATE folders SET unread_count = GREATEST(0, CAST(unread_count AS SIGNED) + ?) WHERE name = ?";
pub const Q_011: &str = "SELECT EXISTS(
SELECT 1 FROM folders
WHERE updated_at > datetime('now', ?)
WHERE updated_at > DATE_FORMAT(DATE_SUB(CURRENT_TIMESTAMP, INTERVAL ? SECOND), '%Y-%m-%d %H:%i:%s')
)";
pub const Q_012: &str = "SELECT EXISTS(
SELECT 1 FROM folders
WHERE name = ? AND messages_updated_at > datetime('now', ?)
WHERE name = ? AND messages_updated_at > DATE_FORMAT(DATE_SUB(CURRENT_TIMESTAMP, INTERVAL ? SECOND), '%Y-%m-%d %H:%i:%s')
)";
pub const Q_013: &str = "SELECT messages_updated_at IS NULL FROM folders WHERE name = ?";
pub const Q_014: &str = "UPDATE folders SET messages_updated_at = NULL WHERE name = ?";
@@ -216,7 +215,7 @@ pub mod db_folders {
WHERE name = ?";
pub const Q_016: &str = "DELETE FROM messages WHERE folder = ?";
pub const Q_017: &str = "DELETE FROM folders WHERE name = ?";
pub const Q_018: &str = "PRAGMA defer_foreign_keys = ON;";
pub const Q_018: &str = "SELECT 1";
pub const Q_019: &str = "INSERT INTO folders (name, delimiter, parent, flags, is_subscribed, total_count, unread_count, uid_validity, highest_modseq, updated_at, messages_updated_at)
SELECT ?, delimiter, parent, flags, is_subscribed, total_count, unread_count, uid_validity, highest_modseq, CURRENT_TIMESTAMP, messages_updated_at
FROM folders WHERE name = ?";
@@ -237,14 +236,14 @@ pub mod db_header_sync {
pub const Q_001: &str = "SELECT value FROM user_meta WHERE key = 'header_sync_generation'";
pub const Q_002: &str =
"INSERT INTO user_meta (key, value) VALUES ('header_sync_generation', '1')
ON CONFLICT(key) DO UPDATE SET value = CAST(value AS INTEGER) + 1";
ON DUPLICATE KEY UPDATE value = CAST(value AS SIGNED) + 1";
}
pub mod db_identities {
pub const IDENTITY_SELECT_COLS: &str = "id, display_name, email, signature_html, is_default, created_at, updated_at, reply_to, bcc, organization, signature_text, signature_new, signature_reply, signature_forward, signature_position, pgp_fingerprint, pgp_sign_default, pgp_encrypt_default, pgp_inline_default";
pub const Q_001: &str = "INSERT INTO identities (display_name,email,signature_html,is_default,reply_to,bcc,organization,signature_text,signature_new,signature_reply,signature_forward,signature_position,pgp_fingerprint,pgp_sign_default,pgp_encrypt_default,pgp_inline_default) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)";
pub const Q_002: &str = "DELETE FROM identities WHERE id = ?";
pub const Q_003: &str = "SELECT COUNT(*) FROM identities";
pub const Q_003: &str = "SELECT CAST(COUNT(*) AS SIGNED) FROM identities";
pub const Q_004: &str =
"UPDATE identities SET is_default = 0, updated_at = CURRENT_TIMESTAMP WHERE is_default = 1";
pub const FORMAT_001: &str =
@@ -273,8 +272,8 @@ pub mod db_messages {
pub const Q_004: &str = "SELECT uid, folder, subject, from_address, from_name, to_addresses, \
COALESCE(NULLIF(body_text, ''), snippet, ''), date_epoch, has_attachments \
FROM messages ORDER BY folder, uid";
pub const Q_005: &str = "SELECT COUNT(*) FROM messages WHERE folder = ?";
pub const Q_006: &str = "SELECT COUNT(*) FROM messages";
pub const Q_005: &str = "SELECT CAST(COUNT(*) AS SIGNED) FROM messages WHERE folder = ?";
pub const Q_006: &str = "SELECT CAST(COUNT(*) AS SIGNED) FROM messages";
pub const Q_007: &str = "UPDATE messages SET flags = ? WHERE folder = ? AND uid = ?";
pub const Q_008: &str = "UPDATE messages
SET body_html = ?, body_text = ?, body_cached = 1,
@@ -291,7 +290,7 @@ pub mod db_messages {
ORDER BY date_epoch DESC
LIMIT ?";
pub const Q_013: &str = "SELECT uid, flags FROM messages WHERE folder = ?";
pub const Q_014: &str = "SELECT SUM(size) FROM messages";
pub const Q_014: &str = "SELECT CAST(SUM(size) AS SIGNED) FROM messages";
pub const FORMAT_001: &str = "SELECT {MSG_SELECT_COLS}
FROM messages
WHERE folder = ?
@@ -307,9 +306,9 @@ pub mod db_messages {
WHERE folder = ? AND uid = ?";
pub const FORMAT_004: &str = "SELECT {MSG_SELECT_COLS}
FROM messages
WHERE message_id = ?
OR in_reply_to = ?
OR references_header LIKE ? ESCAPE '\\'
WHERE message_id = ?1
OR in_reply_to = ?1
OR references_header LIKE ?2 ESCAPE '!'
ORDER BY date_epoch ASC";
pub const FORMAT_005: &str = "SELECT {MSG_SELECT_COLS}
FROM messages
@@ -326,9 +325,9 @@ pub mod db_notification_preferences {
pub mod db_pgp {
pub const KEY_COLS: &str = "fingerprint,key_id,email,user_id,public_key_armor,secret_key_path,has_secret,trust,source,created_at,updated_at";
pub const Q_001: &str = "INSERT INTO pgp_keys (fingerprint,key_id,email,user_id,public_key_armor,secret_key_path,has_secret,trust,source) VALUES (?,?,?,?,?,?,?,?,?)\n ON CONFLICT(fingerprint) DO UPDATE SET key_id=excluded.key_id,email=CASE WHEN excluded.email='' THEN pgp_keys.email ELSE excluded.email END,user_id=CASE WHEN excluded.user_id='' THEN pgp_keys.user_id ELSE excluded.user_id END,public_key_armor=excluded.public_key_armor,secret_key_path=COALESCE(excluded.secret_key_path,pgp_keys.secret_key_path),has_secret=MAX(pgp_keys.has_secret,excluded.has_secret),trust=excluded.trust,source=excluded.source,updated_at=CURRENT_TIMESTAMP";
pub const Q_001: &str = "INSERT INTO pgp_keys (fingerprint,key_id,email,user_id,public_key_armor,secret_key_path,has_secret,trust,source) VALUES (?,?,?,?,?,?,?,?,?)\n ON DUPLICATE KEY UPDATE key_id=VALUES(key_id),email=CASE WHEN VALUES(email)='' THEN email ELSE VALUES(email) END,user_id=CASE WHEN VALUES(user_id)='' THEN user_id ELSE VALUES(user_id) END,public_key_armor=VALUES(public_key_armor),secret_key_path=COALESCE(VALUES(secret_key_path),secret_key_path),has_secret=GREATEST(has_secret,VALUES(has_secret)),trust=VALUES(trust),source=VALUES(source),updated_at=CURRENT_TIMESTAMP";
pub const Q_002: &str = "DELETE FROM pgp_keys WHERE replace(upper(fingerprint),' ','')=?";
pub const Q_003: &str = "UPDATE pgp_keys SET trust=?,updated_at=CURRENT_TIMESTAMP WHERE replace(upper(fingerprint),' ','')=?";
pub const Q_003: &str = "UPDATE pgp_keys SET trust=?2,updated_at=CURRENT_TIMESTAMP WHERE replace(upper(fingerprint),' ','')=?1";
pub const Q_004: &str =
"SELECT wkd_enabled,hkp_enabled,require_trusted_encryption FROM pgp_preferences WHERE id=1";
pub const Q_005: &str = "UPDATE pgp_preferences SET wkd_enabled=?,hkp_enabled=?,require_trusted_encryption=?,updated_at=CURRENT_TIMESTAMP WHERE id=1";
@@ -343,11 +342,12 @@ pub mod db_pgp {
pub mod db_pool {
pub const Q_001: &str = "SELECT 0";
pub const Q_002: &str = "SELECT CURRENT_TIMESTAMP";
pub const Q_002: &str = "SELECT CAST(CURRENT_TIMESTAMP AS CHAR)";
pub const Q_003: &str = "SELECT 1";
pub const Q_004: &str = "SELECT 1";
pub const Q_005: &str = "SHOW TABLES";
pub const Q_006: &str = "SELECT 1";
pub const Q_007: &str = "SELECT CAST(COUNT(*) AS SIGNED) FROM information_schema.tables WHERE table_schema = DATABASE()";
}
pub mod db_response_templates {
@@ -374,7 +374,7 @@ pub mod db_saved_searches {
pub mod db_special_folders {
pub const Q_001: &str = "SELECT folder_name FROM special_folders WHERE role = ?";
pub const Q_002: &str = "SELECT EXISTS(SELECT 1 FROM folders WHERE name = ?)";
pub const Q_003: &str = "INSERT INTO special_folders(role, folder_name, updated_at) VALUES(?, ?, CURRENT_TIMESTAMP)\n ON CONFLICT(role) DO UPDATE SET folder_name = excluded.folder_name, updated_at = CURRENT_TIMESTAMP";
pub const Q_003: &str = "INSERT INTO special_folders(role, folder_name, updated_at) VALUES(?, ?, CURRENT_TIMESTAMP)\n ON DUPLICATE KEY UPDATE folder_name = VALUES(folder_name), updated_at = CURRENT_TIMESTAMP";
pub const Q_004: &str = "DELETE FROM special_folders WHERE role = ?";
pub const Q_005: &str = "UPDATE special_folders SET folder_name = ?, updated_at = CURRENT_TIMESTAMP WHERE folder_name = ?";
pub const Q_006: &str = "DELETE FROM special_folders WHERE folder_name = ?";
@@ -384,7 +384,7 @@ pub mod db_tags {
pub const Q_001: &str = "INSERT INTO tags (id, name, color, imap_keyword) VALUES (?, ?, ?, ?)";
pub const Q_002: &str =
"SELECT t.id, t.name, t.color, t.imap_keyword, t.created_at, t.updated_at,
(SELECT COUNT(*) FROM message_tags mt WHERE mt.tag_id = t.id) AS message_count
(SELECT CAST(COUNT(*) AS SIGNED) FROM message_tags mt WHERE mt.tag_id = t.id) AS message_count
FROM tags t
ORDER BY t.name ASC";
pub const Q_003: &str = "SELECT message_uid, message_folder FROM message_tags WHERE tag_id = ?";
@@ -409,7 +409,7 @@ pub mod db_tags {
WHERE mt.tag_id = ?
ORDER BY m.date_epoch DESC
LIMIT ? OFFSET ?";
pub const Q_010: &str = "SELECT COUNT(*) FROM message_tags WHERE tag_id = ?";
pub const Q_010: &str = "SELECT CAST(COUNT(*) AS SIGNED) FROM message_tags WHERE tag_id = ?";
pub const Q_011: &str = "INSERT IGNORE INTO folders (name) VALUES (?)";
pub const Q_012: &str = "INSERT INTO messages (uid, folder, subject, from_address, from_name, to_addresses, date, flags, size, has_attachments, snippet)
VALUES (?, ?, 'Test', 'test@ex.com', 'Test', '[]', '2024-01-01', '', 100, 0, '')";
@@ -427,23 +427,23 @@ pub mod db_trusted_senders {
}
pub mod routes_contacts {
pub const Q_001: &str = "SELECT CURRENT_TIMESTAMP";
pub const Q_002: &str = "SELECT CURRENT_TIMESTAMP";
pub const Q_003: &str = "SELECT CURRENT_TIMESTAMP";
pub const Q_001: &str = "SELECT CAST(CURRENT_TIMESTAMP AS CHAR)";
pub const Q_002: &str = "SELECT CAST(CURRENT_TIMESTAMP AS CHAR)";
pub const Q_003: &str = "SELECT CAST(CURRENT_TIMESTAMP AS CHAR)";
}
pub mod routes_diagnostics {
pub const Q_001: &str = "SELECT COUNT(*),
COALESCE(SUM(CASE WHEN messages_updated_at IS NULL THEN 1 ELSE 0 END), 0),
CASE WHEN MAX(updated_at) IS NULL THEN NULL ELSE replace(MAX(updated_at), ' ', 'T') || 'Z' END,
CASE WHEN MAX(messages_updated_at) IS NULL THEN NULL ELSE replace(MAX(messages_updated_at), ' ', 'T') || 'Z' END
pub const Q_001: &str = "SELECT CAST(COUNT(*) AS SIGNED),
CAST(COALESCE(SUM(CASE WHEN messages_updated_at IS NULL THEN 1 ELSE 0 END), 0) AS SIGNED),
CASE WHEN MAX(updated_at) IS NULL THEN NULL ELSE CONCAT(replace(MAX(updated_at), ' ', 'T'), 'Z') END,
CASE WHEN MAX(messages_updated_at) IS NULL THEN NULL ELSE CONCAT(replace(MAX(messages_updated_at), ' ', 'T'), 'Z') END
FROM folders";
pub const Q_002: &str = "SELECT COUNT(*) FROM messages";
pub const Q_003: &str = "SELECT COUNT(*) FROM messages WHERE body_cached = 1";
pub const Q_002: &str = "SELECT CAST(COUNT(*) AS SIGNED) FROM messages";
pub const Q_003: &str = "SELECT CAST(COUNT(*) AS SIGNED) FROM messages WHERE body_cached = 1";
}
pub mod routes_folder_mgmt {
pub const Q_001: &str = "UPDATE messages SET flags = CASE WHEN flags = '' THEN ? ELSE flags || ',' || ? END WHERE folder = ? AND instr(flags, ?) = 0";
pub const Q_001: &str = "UPDATE messages SET flags = CASE WHEN flags = '' THEN ?2 ELSE CONCAT(flags, ',', ?2) END WHERE folder = ?1 AND LOCATE(?2, flags) = 0";
pub const Q_002: &str = "UPDATE folders SET unread_count = 0 WHERE name = ?";
pub const Q_003: &str = "SELECT uid FROM messages WHERE folder = ?";
pub const Q_004: &str = "UPDATE folders SET total_count = 0, unread_count = 0, messages_updated_at = CURRENT_TIMESTAMP WHERE name = ?";
@@ -479,7 +479,8 @@ pub mod sessions {
"SELECT * FROM sessions WHERE browser_id = ? ORDER BY created_at_unix";
pub const BY_USER: &str =
"SELECT * FROM sessions WHERE user_hash = ? ORDER BY last_accessed_unix DESC";
pub const BROWSER_EXISTS: &str = "SELECT COUNT(*) FROM sessions WHERE browser_id = ?";
pub const BROWSER_EXISTS: &str =
"SELECT CAST(COUNT(*) AS SIGNED) FROM sessions WHERE browser_id = ?";
pub const DELETE_ACCOUNT: &str = "DELETE FROM sessions WHERE account_id = ?";
pub const DELETE_BROWSER: &str = "DELETE FROM sessions WHERE browser_id = ?";
pub const DELETE_EXPIRED: &str =
+72 -57
View File
@@ -10,7 +10,7 @@ pub mod db_calendar {
pub const SETTINGS_SELECT_COLS: &str = "default_view, week_starts_on, working_hours_start, \
working_hours_end, time_format, zoom_link, teams_link, meet_link, caldav_url, \
caldav_username, caldav_enabled, updated_at";
pub const Q_001: &str = "SELECT CURRENT_TIMESTAMP";
pub const Q_001: &str = "SELECT CURRENT_TIMESTAMP::text";
pub const Q_002: &str = "INSERT INTO calendar_events
(id, title, description, location, start_time, end_time, all_day,
recurrence_rule, attendees, organizer, status, source, source_uid,
@@ -18,8 +18,10 @@ pub mod db_calendar {
reminder_minutes, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20)";
pub const Q_003: &str = "DELETE FROM calendar_events WHERE id = $1";
pub const Q_004: &str = "INSERT OR IGNORE INTO calendar_settings (id) VALUES (1)";
pub const Q_005: &str = "INSERT OR IGNORE INTO calendar_settings (id) VALUES (1)";
pub const Q_004: &str =
"INSERT INTO calendar_settings (id) VALUES (1) ON CONFLICT (id) DO NOTHING";
pub const Q_005: &str =
"INSERT INTO calendar_settings (id) VALUES (1) ON CONFLICT (id) DO NOTHING";
pub const Q_006: &str = "SELECT id, name, url_template, icon, is_default, created_at
FROM meeting_templates ORDER BY is_default DESC, name ASC";
pub const Q_007: &str =
@@ -48,10 +50,9 @@ pub mod db_contact_groups {
FROM contact_groups g
ORDER BY g.name ASC";
pub const Q_003: &str =
"UPDATE contact_groups SET name = $1, updated_at = CURRENT_TIMESTAMP WHERE id = $2";
"UPDATE contact_groups SET name = $1, updated_at = CURRENT_TIMESTAMP::text WHERE id = $2";
pub const Q_004: &str = "DELETE FROM contact_groups WHERE id = $1";
pub const Q_005: &str =
"INSERT OR IGNORE INTO contact_group_members (group_id, contact_id) VALUES ($1, $2)";
pub const Q_005: &str = "INSERT INTO contact_group_members (group_id, contact_id) VALUES ($1, $2) ON CONFLICT (group_id, contact_id) DO NOTHING";
pub const Q_006: &str =
"DELETE FROM contact_group_members WHERE group_id = $1 AND contact_id = $2";
pub const Q_007: &str = "SELECT c.id, c.email, c.name, c.company, c.notes, c.phone, c.job_title, c.address, c.website, c.birthday, c.is_favorite,
@@ -84,10 +85,11 @@ pub mod db_contacts {
pub const Q_003: &str = r#"
SELECT email, name
FROM known_addresses
WHERE (email LIKE $1 ESCAPE '\' OR name LIKE $1 ESCAPE '\')
WHERE (email LIKE $1 ESCAPE '!' OR name LIKE $1 ESCAPE '!')
AND email NOT IN (SELECT email FROM contacts)
AND NOT EXISTS (
SELECT 1 FROM contacts c, json_each(c.emails_json) alt
SELECT 1 FROM contacts c
CROSS JOIN LATERAL jsonb_array_elements_text(COALESCE(NULLIF(c.emails_json, ''), '[]')::jsonb) AS alt(value)
WHERE lower(alt.value) = lower(known_addresses.email)
)
ORDER BY email ASC
@@ -99,40 +101,41 @@ pub mod db_contacts {
ON CONFLICT(email) DO UPDATE SET name = excluded.name WHERE excluded.name != ''";
pub const Q_006: &str = "UPDATE contacts
SET contact_count = contact_count + 1,
last_contacted = CURRENT_TIMESTAMP,
updated_at = CURRENT_TIMESTAMP
last_contacted = CURRENT_TIMESTAMP::text,
updated_at = CURRENT_TIMESTAMP::text
WHERE email = $1";
pub const Q_007: &str = "INSERT OR IGNORE INTO contacts (id, email, name, source)
VALUES ($1, $2, $3, 'auto')";
pub const Q_008: &str = "INSERT INTO contact_photos (contact_id, content_type, data, updated_at) VALUES ($1, $2, $3, CURRENT_TIMESTAMP) ON CONFLICT(contact_id) DO UPDATE SET content_type=excluded.content_type, data=excluded.data, updated_at=CURRENT_TIMESTAMP";
pub const Q_007: &str = "INSERT INTO contacts (id, email, name, source)
VALUES ($1, $2, $3, 'auto') ON CONFLICT (id) DO NOTHING";
pub const Q_008: &str = "INSERT INTO contact_photos (contact_id, content_type, data, updated_at) VALUES ($1, $2, $3, CURRENT_TIMESTAMP::text) ON CONFLICT(contact_id) DO UPDATE SET content_type=excluded.content_type, data=excluded.data, updated_at=CURRENT_TIMESTAMP::text";
pub const Q_009: &str = "SELECT content_type, data FROM contact_photos WHERE contact_id = $1";
pub const Q_010: &str = "DELETE FROM contact_photos WHERE contact_id = $1";
pub const FORMAT_001: &str = "SELECT {CONTACT_SELECT_COLS} FROM contacts WHERE id = $1";
pub const FORMAT_002: &str = "SELECT {CONTACT_SELECT_COLS} FROM contacts \
WHERE lower(email) = lower($1) \
OR EXISTS (SELECT 1 FROM json_each(contacts.emails_json) WHERE lower(value) = lower($1))";
OR EXISTS (SELECT 1 FROM jsonb_array_elements_text(COALESCE(NULLIF(contacts.emails_json, ''), '[]')::jsonb) AS alt(value) WHERE lower(alt.value) = lower($1))";
pub const FORMAT_003: &str = "SELECT {CONTACT_SELECT_COLS} FROM contacts
WHERE name LIKE $1 ESCAPE '\\' OR email LIKE $1 ESCAPE '\\' OR emails_json LIKE $1 ESCAPE '\\'
WHERE name LIKE $1 ESCAPE '!' OR email LIKE $1 ESCAPE '!' OR emails_json LIKE $1 ESCAPE '!'
ORDER BY name ASC
LIMIT $2 OFFSET $3";
pub const FORMAT_004: &str = "SELECT {CONTACT_SELECT_COLS} FROM contacts
ORDER BY name ASC
LIMIT $1 OFFSET $2";
pub const FORMAT_005: &str = "SELECT {CONTACT_SELECT_COLS} FROM contacts
WHERE name LIKE $1 ESCAPE '\\' OR email LIKE $1 ESCAPE '\\' OR emails_json LIKE $1 ESCAPE '\\'
WHERE name LIKE $1 ESCAPE '!' OR email LIKE $1 ESCAPE '!' OR emails_json LIKE $1 ESCAPE '!'
ORDER BY name ASC
LIMIT $2";
}
pub mod db_display_preferences {
pub const Q_001: &str = "SELECT density, theme, language, compose_format, deep_index, animation_mode, preview_pane, ui_scale, color_preset, accent_color, background_color, panel_color, text_color, font_family, font_size, row_height, folder_pane_width, message_pane_width, border_radius, contrast, show_avatars, show_attachment_icons, show_flags, reduce_motion, date_format, time_format, compose_autosave_ms, default_priority, request_mdn_default, attachment_reminder, icon_size, toolbar_density, sidebar_item_height, reader_max_width, composer_width, selection_color, unread_font_weight, unread_badge_style, compact_headers, show_borders, scrollbar_density, monochrome, folder_background, message_background, reader_background, compose_font_family, reply_position, quote_style, forward_mode, spellcheck_enabled, interface_skin, preset_scale_unlocked, updated_at FROM display_preferences WHERE id = 1";
pub const Q_002: &str = "INSERT OR IGNORE INTO display_preferences (id) VALUES (1)";
pub const Q_002: &str =
"INSERT INTO display_preferences (id) VALUES (1) ON CONFLICT (id) DO NOTHING";
pub const FORMAT_001: &str = "UPDATE display_preferences SET {} WHERE id = ${}";
}
pub mod db_drafts {
pub const Q_001: &str = "INSERT INTO drafts (id, to_addresses, cc_addresses, bcc_addresses, subject, text_body, html_body, in_reply_to, references_header, request_mdn, priority, compose_format, pgp_mode, pgp_inline, pgp_attach_public_key, pgp_attach_recipient_keys, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, CURRENT_TIMESTAMP)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, CURRENT_TIMESTAMP::text)
ON CONFLICT(id) DO UPDATE SET
to_addresses = excluded.to_addresses,
cc_addresses = excluded.cc_addresses,
@@ -149,7 +152,7 @@ pub mod db_drafts {
pgp_inline = excluded.pgp_inline,
pgp_attach_public_key = excluded.pgp_attach_public_key,
pgp_attach_recipient_keys = excluded.pgp_attach_recipient_keys,
updated_at = CURRENT_TIMESTAMP";
updated_at = CURRENT_TIMESTAMP::text";
pub const Q_002: &str = "SELECT id, to_addresses, cc_addresses, bcc_addresses, subject, text_body, html_body, in_reply_to, references_header, request_mdn, priority, compose_format, pgp_mode, pgp_inline, pgp_attach_public_key, pgp_attach_recipient_keys, remote_folder, remote_uid, remote_uid_validity, created_at, updated_at
FROM drafts WHERE id = $1";
pub const Q_003: &str = "SELECT id, to_addresses, cc_addresses, bcc_addresses, subject, text_body, html_body, in_reply_to, references_header, request_mdn, priority, compose_format, pgp_mode, pgp_inline, pgp_attach_public_key, pgp_attach_recipient_keys, remote_folder, remote_uid, remote_uid_validity, created_at, updated_at
@@ -171,40 +174,43 @@ pub mod db_folder_preferences {
pub const Q_003: &str = "SELECT EXISTS(SELECT 1 FROM folders WHERE name = $1)";
pub const Q_004: &str = "SELECT flagged_on_top FROM mail_preferences WHERE id = 1";
pub const Q_005: &str = "SELECT EXISTS(SELECT 1 FROM folders WHERE name = $1)";
pub const Q_006: &str = "INSERT INTO folder_preferences(folder_name, flagged_on_top, updated_at) VALUES($1, $2, CURRENT_TIMESTAMP)\n ON CONFLICT(folder_name) DO UPDATE SET flagged_on_top = excluded.flagged_on_top, updated_at = CURRENT_TIMESTAMP";
pub const Q_006: &str = "INSERT INTO folder_preferences(folder_name, flagged_on_top, updated_at) VALUES($1, $2, CURRENT_TIMESTAMP::text)\n ON CONFLICT(folder_name) DO UPDATE SET flagged_on_top = excluded.flagged_on_top, updated_at = CURRENT_TIMESTAMP::text";
pub const Q_007: &str = "DELETE FROM folder_preferences WHERE folder_name = $1";
}
pub mod db_folders {
pub const Q_001: &str = "INSERT OR 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 ($1, $2, $3, $4, $5, $6, $7, $8, $9, CURRENT_TIMESTAMP)";
pub const Q_002: &str = "INSERT OR IGNORE INTO folders
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, CURRENT_TIMESTAMP::text)
ON CONFLICT (name) DO UPDATE SET delimiter=excluded.delimiter, parent=excluded.parent, flags=excluded.flags,
is_subscribed=excluded.is_subscribed, total_count=excluded.total_count, unread_count=excluded.unread_count,
uid_validity=excluded.uid_validity, highest_modseq=excluded.highest_modseq, updated_at=CURRENT_TIMESTAMP::text";
pub const Q_002: &str = "INSERT INTO folders
(name, delimiter, flags, is_subscribed, updated_at)
VALUES ($1, $2, $3, 1, CURRENT_TIMESTAMP)";
VALUES ($1, $2, $3, 1, CURRENT_TIMESTAMP::text) ON CONFLICT (name) DO NOTHING";
pub const Q_003: &str = "SELECT name, delimiter, parent, flags, is_subscribed,
total_count, unread_count, uid_validity, highest_modseq
FROM folders
ORDER BY name";
pub const Q_004: &str = "DELETE FROM folders";
pub const Q_005: &str = "UPDATE folders SET updated_at = CURRENT_TIMESTAMP";
pub const Q_006: &str = "UPDATE folders SET uid_validity = $1, total_count = $2, messages_updated_at = CURRENT_TIMESTAMP
pub const Q_005: &str = "UPDATE folders SET updated_at = CURRENT_TIMESTAMP::text";
pub const Q_006: &str = "UPDATE folders SET uid_validity = $1, total_count = $2, messages_updated_at = CURRENT_TIMESTAMP::text
WHERE name = $3";
pub const Q_007: &str =
"UPDATE folders SET uid_validity = $1, total_count = $2, highest_modseq = $3
WHERE name = $4";
pub const Q_008: &str =
"SELECT COUNT(*) FROM messages WHERE folder = $1 AND flags NOT LIKE '%\\Seen%'";
"SELECT COUNT(*) FROM messages WHERE folder = $1 AND strpos(flags, chr(92) || 'Seen') = 0";
pub const Q_009: &str = "UPDATE folders SET unread_count = $1 WHERE name = $2";
pub const Q_010: &str = "UPDATE folders SET unread_count = GREATEST(0, CAST(unread_count AS INTEGER) + $1) WHERE name = $2";
pub const Q_011: &str = "SELECT EXISTS(
SELECT 1 FROM folders
WHERE updated_at > datetime('now', $1)
WHERE updated_at > to_char(CURRENT_TIMESTAMP - ($1 * INTERVAL '1 second'), 'YYYY-MM-DD HH24:MI:SS')
)";
pub const Q_012: &str = "SELECT EXISTS(
SELECT 1 FROM folders
WHERE name = $1 AND messages_updated_at > datetime('now', $2)
WHERE name = $1 AND messages_updated_at > to_char(CURRENT_TIMESTAMP - ($2 * INTERVAL '1 second'), 'YYYY-MM-DD HH24:MI:SS')
)";
pub const Q_013: &str = "SELECT messages_updated_at IS NULL FROM folders WHERE name = $1";
pub const Q_014: &str = "UPDATE folders SET messages_updated_at = NULL WHERE name = $1";
@@ -214,9 +220,9 @@ pub mod db_folders {
WHERE name = $1";
pub const Q_016: &str = "DELETE FROM messages WHERE folder = $1";
pub const Q_017: &str = "DELETE FROM folders WHERE name = $1";
pub const Q_018: &str = "PRAGMA defer_foreign_keys = ON;";
pub const Q_018: &str = "SELECT 1";
pub const Q_019: &str = "INSERT INTO folders (name, delimiter, parent, flags, is_subscribed, total_count, unread_count, uid_validity, highest_modseq, updated_at, messages_updated_at)
SELECT $1, delimiter, parent, flags, is_subscribed, total_count, unread_count, uid_validity, highest_modseq, CURRENT_TIMESTAMP, messages_updated_at
SELECT $1, delimiter, parent, flags, is_subscribed, total_count, unread_count, uid_validity, highest_modseq, CURRENT_TIMESTAMP::text, messages_updated_at
FROM folders WHERE name = $2";
pub const Q_020: &str = "UPDATE messages SET folder = $1 WHERE folder = $2";
pub const Q_021: &str = "UPDATE message_tags SET message_folder = $1 WHERE message_folder = $2";
@@ -243,8 +249,7 @@ pub mod db_identities {
pub const Q_001: &str = "INSERT INTO identities (display_name,email,signature_html,is_default,reply_to,bcc,organization,signature_text,signature_new,signature_reply,signature_forward,signature_position,pgp_fingerprint,pgp_sign_default,pgp_encrypt_default,pgp_inline_default) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16)";
pub const Q_002: &str = "DELETE FROM identities WHERE id = $1";
pub const Q_003: &str = "SELECT COUNT(*) FROM identities";
pub const Q_004: &str =
"UPDATE identities SET is_default = 0, updated_at = CURRENT_TIMESTAMP WHERE is_default = 1";
pub const Q_004: &str = "UPDATE identities SET is_default = 0, updated_at = CURRENT_TIMESTAMP::text WHERE is_default = 1";
pub const FORMAT_001: &str =
"SELECT {IDENTITY_SELECT_COLS} FROM identities ORDER BY is_default DESC, email ASC";
pub const FORMAT_002: &str = "SELECT {IDENTITY_SELECT_COLS} FROM identities WHERE id = $1";
@@ -255,17 +260,23 @@ pub mod db_identities {
pub mod db_mail_preferences {
pub const Q_001: &str = "SELECT messages_per_page, mark_read_delay_ms, remote_images, preferred_body, thread_mode, collapse_quotes, show_snippets, default_sort, flagged_on_top, sync_imap_keywords, updated_at FROM mail_preferences WHERE id = 1";
pub const Q_002: &str = "INSERT OR IGNORE INTO mail_preferences (id) VALUES (1)";
pub const Q_003: &str = "UPDATE mail_preferences SET messages_per_page = $1, mark_read_delay_ms = $2, remote_images = $3, preferred_body = $4, thread_mode = $5, collapse_quotes = $6, show_snippets = $7, default_sort = $8, flagged_on_top = $9, sync_imap_keywords = $10, updated_at = CURRENT_TIMESTAMP WHERE id = 1";
pub const Q_002: &str =
"INSERT INTO mail_preferences (id) VALUES (1) ON CONFLICT (id) DO NOTHING";
pub const Q_003: &str = "UPDATE mail_preferences SET messages_per_page = $1, mark_read_delay_ms = $2, remote_images = $3, preferred_body = $4, thread_mode = $5, collapse_quotes = $6, show_snippets = $7, default_sort = $8, flagged_on_top = $9, sync_imap_keywords = $10, updated_at = CURRENT_TIMESTAMP::text WHERE id = 1";
}
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 = "INSERT OR 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 ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17)";
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17)
ON CONFLICT (uid, folder) DO UPDATE SET message_id=excluded.message_id, in_reply_to=excluded.in_reply_to,
references_header=excluded.references_header, subject=excluded.subject, from_address=excluded.from_address,
from_name=excluded.from_name, to_addresses=excluded.to_addresses, cc_addresses=excluded.cc_addresses,
date=excluded.date, flags=excluded.flags, size=excluded.size, has_attachments=excluded.has_attachments,
snippet=excluded.snippet, date_epoch=excluded.date_epoch, reaction=excluded.reaction";
pub const Q_002: &str = "DELETE FROM messages WHERE folder = $1";
pub const Q_003: &str = "SELECT COALESCE(MAX(uid), 0) FROM messages WHERE folder = $1";
pub const Q_004: &str = "SELECT uid, folder, subject, from_address, from_name, to_addresses, \
@@ -289,7 +300,7 @@ pub mod db_messages {
ORDER BY date_epoch DESC
LIMIT $1";
pub const Q_013: &str = "SELECT uid, flags FROM messages WHERE folder = $1";
pub const Q_014: &str = "SELECT SUM(size) FROM messages";
pub const Q_014: &str = "SELECT SUM(size)::bigint FROM messages";
pub const FORMAT_001: &str = "SELECT {MSG_SELECT_COLS}
FROM messages
WHERE folder = $1
@@ -307,7 +318,7 @@ pub mod db_messages {
FROM messages
WHERE message_id = $1
OR in_reply_to = $1
OR references_header LIKE $2 ESCAPE '\\'
OR references_header LIKE $2 ESCAPE '!'
ORDER BY date_epoch ASC";
pub const FORMAT_005: &str = "SELECT {MSG_SELECT_COLS}
FROM messages
@@ -318,19 +329,20 @@ pub mod db_messages {
pub mod db_notification_preferences {
pub const Q_001: &str = "SELECT enabled, sound, folders, dnd_enabled, dnd_start, dnd_end, updated_at FROM notification_preferences WHERE id = 1";
pub const Q_002: &str = "INSERT OR IGNORE INTO notification_preferences (id) VALUES (1)";
pub const Q_002: &str =
"INSERT INTO notification_preferences (id) VALUES (1) ON CONFLICT (id) DO NOTHING";
pub const FORMAT_001: &str =
"UPDATE notification_preferences SET {set_clause} WHERE id = ${idx}";
}
pub mod db_pgp {
pub const KEY_COLS: &str = "fingerprint,key_id,email,user_id,public_key_armor,secret_key_path,has_secret,trust,source,created_at,updated_at";
pub const Q_001: &str = "INSERT INTO pgp_keys (fingerprint,key_id,email,user_id,public_key_armor,secret_key_path,has_secret,trust,source) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9)\n ON CONFLICT(fingerprint) DO UPDATE SET key_id=excluded.key_id,email=CASE WHEN excluded.email='' THEN pgp_keys.email ELSE excluded.email END,user_id=CASE WHEN excluded.user_id='' THEN pgp_keys.user_id ELSE excluded.user_id END,public_key_armor=excluded.public_key_armor,secret_key_path=COALESCE(excluded.secret_key_path,pgp_keys.secret_key_path),has_secret=MAX(pgp_keys.has_secret,excluded.has_secret),trust=excluded.trust,source=excluded.source,updated_at=CURRENT_TIMESTAMP";
pub const Q_001: &str = "INSERT INTO pgp_keys (fingerprint,key_id,email,user_id,public_key_armor,secret_key_path,has_secret,trust,source) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9)\n ON CONFLICT(fingerprint) DO UPDATE SET key_id=excluded.key_id,email=CASE WHEN excluded.email='' THEN pgp_keys.email ELSE excluded.email END,user_id=CASE WHEN excluded.user_id='' THEN pgp_keys.user_id ELSE excluded.user_id END,public_key_armor=excluded.public_key_armor,secret_key_path=COALESCE(excluded.secret_key_path,pgp_keys.secret_key_path),has_secret=GREATEST(pgp_keys.has_secret,excluded.has_secret),trust=excluded.trust,source=excluded.source,updated_at=CURRENT_TIMESTAMP::text";
pub const Q_002: &str = "DELETE FROM pgp_keys WHERE replace(upper(fingerprint),' ','')=$1";
pub const Q_003: &str = "UPDATE pgp_keys SET trust=$2,updated_at=CURRENT_TIMESTAMP WHERE replace(upper(fingerprint),' ','')=$1";
pub const Q_003: &str = "UPDATE pgp_keys SET trust=$2,updated_at=CURRENT_TIMESTAMP::text WHERE replace(upper(fingerprint),' ','')=$1";
pub const Q_004: &str =
"SELECT wkd_enabled,hkp_enabled,require_trusted_encryption FROM pgp_preferences WHERE id=1";
pub const Q_005: &str = "UPDATE pgp_preferences SET wkd_enabled=$1,hkp_enabled=$2,require_trusted_encryption=$3,updated_at=CURRENT_TIMESTAMP WHERE id=1";
pub const Q_005: &str = "UPDATE pgp_preferences SET wkd_enabled=$1,hkp_enabled=$2,require_trusted_encryption=$3,updated_at=CURRENT_TIMESTAMP::text WHERE id=1";
pub const FORMAT_001: &str =
"SELECT {KEY_COLS} FROM pgp_keys ORDER BY has_secret DESC, lower(email), fingerprint";
pub const FORMAT_002: &str =
@@ -342,11 +354,13 @@ pub mod db_pgp {
pub mod db_pool {
pub const Q_001: &str = "SELECT 0";
pub const Q_002: &str = "SELECT CURRENT_TIMESTAMP";
pub const Q_002: &str = "SELECT CURRENT_TIMESTAMP::text";
pub const Q_003: &str = "SELECT 1";
pub const Q_004: &str = "SELECT 1";
pub const Q_005: &str = "SELECT tablename FROM pg_catalog.pg_tables WHERE schemaname = current_schema() ORDER BY tablename";
pub const Q_006: &str = "SELECT 1";
pub const Q_007: &str =
"SELECT COUNT(*) FROM pg_catalog.pg_tables WHERE schemaname = current_schema()";
}
pub mod db_response_templates {
@@ -355,7 +369,7 @@ pub mod db_response_templates {
pub const Q_002: &str =
"SELECT id,name,body,format,created_at,updated_at FROM response_templates WHERE id=$1";
pub const Q_003: &str = "INSERT INTO response_templates(name,body,format) VALUES ($1,$2,$3)";
pub const Q_004: &str = "UPDATE response_templates SET name=$1,body=$2,format=$3,updated_at=CURRENT_TIMESTAMP WHERE id=$4";
pub const Q_004: &str = "UPDATE response_templates SET name=$1,body=$2,format=$3,updated_at=CURRENT_TIMESTAMP::text WHERE id=$4";
pub const Q_005: &str = "DELETE FROM response_templates WHERE id=$1";
}
@@ -363,7 +377,7 @@ pub mod db_saved_searches {
pub const Q_001: &str =
"SELECT id,name,query_string,created_at,updated_at FROM saved_searches ORDER BY name ";
pub const Q_002: &str = "INSERT INTO saved_searches(name,query_string) VALUES ($1,$2)";
pub const Q_003: &str = "UPDATE saved_searches SET name=$1,query_string=$2,updated_at=CURRENT_TIMESTAMP WHERE id=$3";
pub const Q_003: &str = "UPDATE saved_searches SET name=$1,query_string=$2,updated_at=CURRENT_TIMESTAMP::text WHERE id=$3";
pub const Q_004: &str =
"SELECT id,name,query_string,created_at,updated_at FROM saved_searches WHERE id=$1";
pub const Q_005: &str = "DELETE FROM saved_searches WHERE id=$1";
@@ -372,9 +386,9 @@ pub mod db_saved_searches {
pub mod db_special_folders {
pub const Q_001: &str = "SELECT folder_name FROM special_folders WHERE role = $1";
pub const Q_002: &str = "SELECT EXISTS(SELECT 1 FROM folders WHERE name = $1)";
pub const Q_003: &str = "INSERT INTO special_folders(role, folder_name, updated_at) VALUES($1, $2, CURRENT_TIMESTAMP)\n ON CONFLICT(role) DO UPDATE SET folder_name = excluded.folder_name, updated_at = CURRENT_TIMESTAMP";
pub const Q_003: &str = "INSERT INTO special_folders(role, folder_name, updated_at) VALUES($1, $2, CURRENT_TIMESTAMP::text)\n ON CONFLICT(role) DO UPDATE SET folder_name = excluded.folder_name, updated_at = CURRENT_TIMESTAMP::text";
pub const Q_004: &str = "DELETE FROM special_folders WHERE role = $1";
pub const Q_005: &str = "UPDATE special_folders SET folder_name = $1, updated_at = CURRENT_TIMESTAMP WHERE folder_name = $2";
pub const Q_005: &str = "UPDATE special_folders SET folder_name = $1, updated_at = CURRENT_TIMESTAMP::text WHERE folder_name = $2";
pub const Q_006: &str = "DELETE FROM special_folders WHERE folder_name = $1";
}
@@ -389,9 +403,9 @@ pub mod db_tags {
pub const Q_003: &str =
"SELECT message_uid, message_folder FROM message_tags WHERE tag_id = $1";
pub const Q_004: &str =
"UPDATE tags SET name = $1, color = $2, updated_at = CURRENT_TIMESTAMP WHERE id = $3";
"UPDATE tags SET name = $1, color = $2, updated_at = CURRENT_TIMESTAMP::text WHERE id = $3";
pub const Q_005: &str = "DELETE FROM tags WHERE id = $1";
pub const Q_006: &str = "INSERT OR IGNORE INTO message_tags (tag_id, message_uid, message_folder) VALUES ($1, $2, $3)";
pub const Q_006: &str = "INSERT INTO message_tags (tag_id, message_uid, message_folder) VALUES ($1, $2, $3) ON CONFLICT (tag_id, message_uid, message_folder) DO NOTHING";
pub const Q_007: &str =
"DELETE FROM message_tags WHERE tag_id = $1 AND message_uid = $2 AND message_folder = $3";
pub const Q_008: &str = "SELECT t.id, t.name, t.color
@@ -409,7 +423,7 @@ pub mod db_tags {
ORDER BY m.date_epoch DESC
LIMIT $2 OFFSET $3";
pub const Q_010: &str = "SELECT COUNT(*) FROM message_tags WHERE tag_id = $1";
pub const Q_011: &str = "INSERT OR IGNORE INTO folders (name) VALUES ($1)";
pub const Q_011: &str = "INSERT INTO folders (name) VALUES ($1) ON CONFLICT (name) DO NOTHING";
pub const Q_012: &str = "INSERT INTO messages (uid, folder, subject, from_address, from_name, to_addresses, date, flags, size, has_attachments, snippet)
VALUES ($1, $2, 'Test', 'test@ex.com', 'Test', '[]', '2024-01-01', '', 100, 0, '')";
pub const FORMAT_001: &str = "SELECT mt.message_uid, mt.message_folder, t.id, t.name, t.color
@@ -421,14 +435,15 @@ pub mod db_tags {
pub mod db_trusted_senders {
pub const Q_001: &str = "SELECT email FROM trusted_senders ORDER BY lower(email)";
pub const Q_002: &str = "INSERT OR IGNORE INTO trusted_senders(email) VALUES($1)";
pub const Q_002: &str =
"INSERT INTO trusted_senders(email) VALUES($1) ON CONFLICT (email) DO NOTHING";
pub const Q_003: &str = "DELETE FROM trusted_senders WHERE email = $1 ";
}
pub mod routes_contacts {
pub const Q_001: &str = "SELECT CURRENT_TIMESTAMP";
pub const Q_002: &str = "SELECT CURRENT_TIMESTAMP";
pub const Q_003: &str = "SELECT CURRENT_TIMESTAMP";
pub const Q_001: &str = "SELECT CURRENT_TIMESTAMP::text";
pub const Q_002: &str = "SELECT CURRENT_TIMESTAMP::text";
pub const Q_003: &str = "SELECT CURRENT_TIMESTAMP::text";
}
pub mod routes_diagnostics {
@@ -442,10 +457,10 @@ pub mod routes_diagnostics {
}
pub mod routes_folder_mgmt {
pub const Q_001: &str = "UPDATE messages SET flags = CASE WHEN flags = '' THEN $2 ELSE flags || ',' || $2 END WHERE folder = $1 AND instr(flags, $2) = 0";
pub const Q_001: &str = "UPDATE messages SET flags = CASE WHEN flags = '' THEN $2 ELSE flags || ',' || $2 END WHERE folder = $1 AND POSITION($2 IN flags) = 0";
pub const Q_002: &str = "UPDATE folders SET unread_count = 0 WHERE name = $1";
pub const Q_003: &str = "SELECT uid FROM messages WHERE folder = $1";
pub const Q_004: &str = "UPDATE folders SET total_count = 0, unread_count = 0, messages_updated_at = CURRENT_TIMESTAMP WHERE name = $1";
pub const Q_004: &str = "UPDATE folders SET total_count = 0, unread_count = 0, messages_updated_at = CURRENT_TIMESTAMP::text WHERE name = $1";
}
pub mod routes_messages_mod {
+14 -12
View File
@@ -84,7 +84,7 @@ pub mod db_contacts {
pub const Q_003: &str = r#"
SELECT email, name
FROM known_addresses
WHERE (email LIKE ?1 ESCAPE '\' OR name LIKE ?1 ESCAPE '\')
WHERE (email LIKE ?1 ESCAPE '!' OR name LIKE ?1 ESCAPE '!')
AND email NOT IN (SELECT email FROM contacts)
AND NOT EXISTS (
SELECT 1 FROM contacts c, json_each(c.emails_json) alt
@@ -112,14 +112,14 @@ pub mod db_contacts {
WHERE lower(email) = lower(?1) \
OR EXISTS (SELECT 1 FROM json_each(contacts.emails_json) WHERE lower(value) = lower(?1))";
pub const FORMAT_003: &str = "SELECT {CONTACT_SELECT_COLS} FROM contacts
WHERE name LIKE ?1 ESCAPE '\\' OR email LIKE ?1 ESCAPE '\\' OR emails_json LIKE ?1 ESCAPE '\\'
WHERE name LIKE ?1 ESCAPE '!' OR email LIKE ?1 ESCAPE '!' OR emails_json LIKE ?1 ESCAPE '!'
ORDER BY name ASC
LIMIT ?2 OFFSET ?3";
pub const FORMAT_004: &str = "SELECT {CONTACT_SELECT_COLS} FROM contacts
ORDER BY name ASC
LIMIT ?1 OFFSET ?2";
pub const FORMAT_005: &str = "SELECT {CONTACT_SELECT_COLS} FROM contacts
WHERE name LIKE ?1 ESCAPE '\\' OR email LIKE ?1 ESCAPE '\\' OR emails_json LIKE ?1 ESCAPE '\\'
WHERE name LIKE ?1 ESCAPE '!' OR email LIKE ?1 ESCAPE '!' OR emails_json LIKE ?1 ESCAPE '!'
ORDER BY name ASC
LIMIT ?2";
}
@@ -200,11 +200,11 @@ pub mod db_folders {
pub const Q_010: &str = "UPDATE folders SET unread_count = MAX(0, CAST(unread_count AS INTEGER) + ?1) WHERE name = ?2";
pub const Q_011: &str = "SELECT EXISTS(
SELECT 1 FROM folders
WHERE updated_at > datetime('now', ?1)
WHERE updated_at > datetime('now', printf('-%d seconds', ?1))
)";
pub const Q_012: &str = "SELECT EXISTS(
SELECT 1 FROM folders
WHERE name = ?1 AND messages_updated_at > datetime('now', ?2)
WHERE name = ?1 AND messages_updated_at > datetime('now', printf('-%d seconds', ?2))
)";
pub const Q_013: &str = "SELECT messages_updated_at IS NULL FROM folders WHERE name = ?1";
pub const Q_014: &str = "UPDATE folders SET messages_updated_at = NULL WHERE name = ?1";
@@ -307,7 +307,7 @@ pub mod db_messages {
FROM messages
WHERE message_id = ?1
OR in_reply_to = ?1
OR references_header LIKE ?2 ESCAPE '\\'
OR references_header LIKE ?2 ESCAPE '!'
ORDER BY date_epoch ASC";
pub const FORMAT_005: &str = "SELECT {MSG_SELECT_COLS}
FROM messages
@@ -347,6 +347,8 @@ pub mod db_pool {
pub const Q_004: &str = "PRAGMA foreign_keys=ON;";
pub const Q_005: &str = "SELECT name FROM sqlite_master WHERE type='table' ORDER BY name";
pub const Q_006: &str = "PRAGMA foreign_keys";
pub const Q_007: &str =
"SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'";
pub fn set_user_version(version: u32) -> String {
format!("PRAGMA user_version = {version};")
@@ -532,11 +534,11 @@ macro_rules! rustbox_query_db_contacts_002 {
#[macro_export]
macro_rules! rustbox_query_db_contacts_003 {
() => { format!("SELECT {CONTACT_SELECT_COLS} FROM contacts
WHERE name LIKE ?1 ESCAPE '\\' OR email LIKE ?1 ESCAPE '\\' OR emails_json LIKE ?1 ESCAPE '\\'
WHERE name LIKE ?1 ESCAPE '!' OR email LIKE ?1 ESCAPE '!' OR emails_json LIKE ?1 ESCAPE '!'
ORDER BY name ASC
LIMIT ?2 OFFSET ?3") };
($($args:tt)+) => { format!("SELECT {CONTACT_SELECT_COLS} FROM contacts
WHERE name LIKE ?1 ESCAPE '\\' OR email LIKE ?1 ESCAPE '\\' OR emails_json LIKE ?1 ESCAPE '\\'
WHERE name LIKE ?1 ESCAPE '!' OR email LIKE ?1 ESCAPE '!' OR emails_json LIKE ?1 ESCAPE '!'
ORDER BY name ASC
LIMIT ?2 OFFSET ?3", $($args)+) };
}
@@ -554,11 +556,11 @@ macro_rules! rustbox_query_db_contacts_004 {
#[macro_export]
macro_rules! rustbox_query_db_contacts_005 {
() => { format!("SELECT {CONTACT_SELECT_COLS} FROM contacts
WHERE name LIKE ?1 ESCAPE '\\' OR email LIKE ?1 ESCAPE '\\' OR emails_json LIKE ?1 ESCAPE '\\'
WHERE name LIKE ?1 ESCAPE '!' OR email LIKE ?1 ESCAPE '!' OR emails_json LIKE ?1 ESCAPE '!'
ORDER BY name ASC
LIMIT ?2") };
($($args:tt)+) => { format!("SELECT {CONTACT_SELECT_COLS} FROM contacts
WHERE name LIKE ?1 ESCAPE '\\' OR email LIKE ?1 ESCAPE '\\' OR emails_json LIKE ?1 ESCAPE '\\'
WHERE name LIKE ?1 ESCAPE '!' OR email LIKE ?1 ESCAPE '!' OR emails_json LIKE ?1 ESCAPE '!'
ORDER BY name ASC
LIMIT ?2", $($args)+) };
}
@@ -643,13 +645,13 @@ macro_rules! rustbox_query_db_messages_004 {
FROM messages
WHERE message_id = ?1
OR in_reply_to = ?1
OR references_header LIKE ?2 ESCAPE '\\'
OR references_header LIKE ?2 ESCAPE '!'
ORDER BY date_epoch ASC") };
($($args:tt)+) => { format!("SELECT {MSG_SELECT_COLS}
FROM messages
WHERE message_id = ?1
OR in_reply_to = ?1
OR references_header LIKE ?2 ESCAPE '\\'
OR references_header LIKE ?2 ESCAPE '!'
ORDER BY date_epoch ASC", $($args)+) };
}
+16 -11
View File
@@ -318,20 +318,20 @@ async fn sync_condstore(
let mut folder_changed = !changed.is_empty();
for (uid, mut flags) in changed {
flags.sort();
db::messages::update_message_flags(conn, &name, uid, &flags.join(","))?;
db::messages::update_message_flags(&transaction, &name, uid, &flags.join(","))?;
if sync_keywords {
db::tags::import_imap_keywords(conn, uid, &name, &flags)?;
db::tags::import_imap_keywords(&transaction, uid, &name, &flags)?;
}
}
if let Some(imap_uids) = imap_uids {
for (uid, _) in db::messages::get_all_uids_and_flags(conn, &name)? {
for (uid, _) in db::messages::get_all_uids_and_flags(&transaction, &name)? {
if !imap_uids.contains(&uid) {
db::messages::delete_message(conn, &name, uid)?;
db::messages::delete_message(&transaction, &name, uid)?;
folder_changed = true;
}
}
}
let max_uid = db::messages::max_uid(conn, &name)?;
let max_uid = db::messages::max_uid(&transaction, &name)?;
let new_messages = status.uid_next > max_uid.saturating_add(1);
let modseq = if new_modseq > 0 {
new_modseq
@@ -339,14 +339,14 @@ async fn sync_condstore(
status.highest_modseq
};
db::folders::update_folder_sync_status(
conn,
&transaction,
&name,
status.uid_validity,
status.exists,
modseq,
)?;
if new_messages {
db::folders::invalidate_folder_freshness(conn, &name)?;
db::folders::invalidate_folder_freshness(&transaction, &name)?;
folder_changed = true;
}
transaction.commit().map_err(|e| e.to_string())?;
@@ -448,7 +448,7 @@ async fn sync_full(
for (uid, cached_flags_csv) in &cached {
match imap_map.get(uid) {
None => {
db::messages::delete_message(conn, folder_name, *uid)?;
db::messages::delete_message(&transaction, folder_name, *uid)?;
folder_changed = true;
tracing::debug!(
folder = %folder_name,
@@ -467,7 +467,7 @@ async fn sync_full(
if cached_normalized != imap_flags_csv {
db::messages::update_message_flags(
conn,
&transaction,
folder_name,
*uid,
&imap_flags_csv,
@@ -475,14 +475,19 @@ async fn sync_full(
folder_changed = true;
}
if sync_keywords {
db::tags::import_imap_keywords(conn, *uid, folder_name, imap_flags)?;
db::tags::import_imap_keywords(
&transaction,
*uid,
folder_name,
imap_flags,
)?;
}
}
}
}
db::folders::update_folder_sync_status(
conn,
&transaction,
folder_name,
status.uid_validity,
status.exists,
+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.0//EN\r\nCALSCALE:GREGORIAN\r\n",
"BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//RustBox//Calendar 0.1.1//EN\r\nCALSCALE:GREGORIAN\r\n",
);
for event in &events {
append_ics_event(&mut body, event);
+3 -15
View File
@@ -184,11 +184,7 @@ pub async fn create_contact_handler(
.map_err(|e| AppError::InternalError(format!("Database error: {e}")))?;
let now: String = conn
.query_row(
crate::queries::sqlite::queries::routes_contacts::Q_001,
[],
|row| row.get(0),
)
.query_row(crate::dbq!(routes_contacts::Q_001), [], |row| row.get(0))
.map_err(|e| AppError::InternalError(format!("Database error: {e}")))?;
let id = body.id.unwrap_or_else(|| Uuid::new_v4().to_string());
let email = body.email.trim().to_string();
@@ -361,11 +357,7 @@ pub async fn merge_contacts_handler(
target.last_contacted = source.last_contacted.clone();
}
target.updated_at = tx
.query_row(
crate::queries::sqlite::queries::routes_contacts::Q_002,
[],
|row| row.get(0),
)
.query_row(crate::dbq!(routes_contacts::Q_002), [], |row| row.get(0))
.map_err(|e| AppError::InternalError(format!("Database error: {e}")))?;
for group in db::contact_groups::list_contact_groups(&tx, &source.id)
@@ -966,11 +958,7 @@ pub async fn import_contacts_handler(
.map_err(|e| AppError::InternalError(format!("Database error: {e}")))?;
let now: String = conn
.query_row(
crate::queries::sqlite::queries::routes_contacts::Q_003,
[],
|row| row.get(0),
)
.query_row(crate::dbq!(routes_contacts::Q_003), [], |row| row.get(0))
.map_err(|e| AppError::InternalError(format!("Database error: {e}")))?;
let parsed = parse_vcards(&vcf_content);
+5 -15
View File
@@ -221,19 +221,13 @@ pub async fn sync_diagnostics(
Option<String>,
Option<String>,
) = conn
.query_row(
crate::queries::sqlite::queries::routes_diagnostics::Q_001,
[],
|row| Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?)),
)
.query_row(crate::dbq!(routes_diagnostics::Q_001), [], |row| {
Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?))
})
.map_err(|e| format!("Failed to read folder sync metadata: {e}"))?;
let cached_messages: i64 = conn
.query_row(
crate::queries::sqlite::queries::routes_diagnostics::Q_002,
[],
|row| row.get(0),
)
.query_row(crate::dbq!(routes_diagnostics::Q_002), [], |row| row.get(0))
.map_err(|e| format!("Failed to count cached messages: {e}"))?;
Ok(SyncDiagnosticsResponse {
@@ -273,11 +267,7 @@ pub async fn search_diagnostics(
.filter(|message| UserIndex::is_excluded_folder(&message.folder))
.count();
let cached_bodies: usize = conn
.query_row(
crate::queries::sqlite::queries::routes_diagnostics::Q_003,
[],
|row| row.get(0),
)
.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();
+1 -1
View File
@@ -316,7 +316,7 @@ pub(crate) async fn remove_remote_draft_snapshot(
}
fn load_snapshot_attachments(
conn: &rusqlite::Connection,
conn: &crate::db::pool::Connection,
draft_id: &str,
) -> Result<Vec<AttachmentData>, String> {
let records = db::drafts::get_draft_attachments(conn, draft_id)?;
+7 -13
View File
@@ -199,15 +199,12 @@ pub async fn mark_all_read(
let conn = db::pool::open_user_db(&config.data_dir, &session.user_hash)
.map_err(|e| AppError::InternalError(format!("Database error: {e}")))?;
conn.execute(
crate::queries::sqlite::queries::routes_folder_mgmt::Q_001,
rusqlite::params![&name, "\\Seen"],
)
.map_err(|e| AppError::InternalError(format!("Database error: {e}")))?;
conn.execute(
crate::queries::sqlite::queries::routes_folder_mgmt::Q_002,
[&name],
crate::dbq!(routes_folder_mgmt::Q_001),
crate::db::pool::params![&name, "\\Seen"],
)
.map_err(|e| AppError::InternalError(format!("Database error: {e}")))?;
conn.execute(crate::dbq!(routes_folder_mgmt::Q_002), [&name])
.map_err(|e| AppError::InternalError(format!("Database error: {e}")))?;
Ok(Json(MessageResponse { status: "read" }).into_response())
}
@@ -229,7 +226,7 @@ pub async fn empty_folder(
.map_err(|e| AppError::InternalError(format!("Database error: {e}")))?;
let uids: Vec<u32> = {
let mut statement = conn
.prepare(crate::queries::sqlite::queries::routes_folder_mgmt::Q_003)
.prepare(crate::dbq!(routes_folder_mgmt::Q_003))
.map_err(|e| AppError::InternalError(format!("Database error: {e}")))?;
let rows = statement
.query_map([&name], |row| row.get(0))
@@ -238,11 +235,8 @@ pub async fn empty_folder(
};
db::messages::delete_folder_messages(&conn, &name)
.map_err(|e| AppError::InternalError(format!("Database error: {e}")))?;
conn.execute(
crate::queries::sqlite::queries::routes_folder_mgmt::Q_004,
[&name],
)
.map_err(|e| AppError::InternalError(format!("Database error: {e}")))?;
conn.execute(crate::dbq!(routes_folder_mgmt::Q_004), [&name])
.map_err(|e| AppError::InternalError(format!("Database error: {e}")))?;
if let Ok(index) = search_engine.open_user_index(&session.user_hash) {
for uid in uids {
+1 -1
View File
@@ -91,7 +91,7 @@ pub async fn delete_identity_handler(
}
fn normalize_and_validate_pgp_key(
conn: &rusqlite::Connection,
conn: &crate::db::pool::Connection,
value: &mut Option<String>,
) -> Result<(), AppError> {
let Some(raw) = value.as_ref() else {
+3 -3
View File
@@ -322,7 +322,7 @@ pub async fn list_messages(
}
fn upsert_and_index_headers(
conn: &rusqlite::Transaction<'_>,
conn: &crate::db::pool::Transaction,
folder: &str,
headers: &[crate::imap::types::ImapMessageHeader],
search_engine: &Arc<SearchEngine>,
@@ -853,8 +853,8 @@ pub async fn update_flags(
let current_flags_csv: String = conn
.query_row(
crate::queries::sqlite::queries::routes_messages_mod::Q_001,
rusqlite::params![&folder, uid],
crate::dbq!(routes_messages_mod::Q_001),
crate::db::pool::params![&folder, uid],
|row| row.get(0),
)
.unwrap_or_default();
+2 -2
View File
@@ -481,8 +481,8 @@ pub async fn delete_key(
database
.run(move |conn| {
conn.execute(
crate::queries::sqlite::queries::routes_pgp::Q_001,
rusqlite::params![delete_fp],
crate::dbq!(routes_pgp::Q_001),
crate::db::pool::params![delete_fp],
)
.map_err(|e| format!("Failed to detach OpenPGP key from identities: {e}"))?;
db::pgp::delete_key(conn, &normalized)
+3 -3
View File
@@ -411,7 +411,7 @@ async fn cleanup_draft(data_dir: &str, user_hash: &str, draft_id: &str) {
}
fn resolve_recipient_public_keys(
conn: &rusqlite::Connection,
conn: &crate::db::pool::Connection,
to: &[String],
cc: &[String],
) -> Result<Vec<(String, String, String)>, AppError> {
@@ -437,7 +437,7 @@ fn resolve_recipient_public_keys(
}
fn resolve_sender_public_key(
conn: &rusqlite::Connection,
conn: &crate::db::pool::Connection,
identity: Option<&crate::db::identities::Identity>,
sender_email: &str,
) -> Result<(String, String), AppError> {
@@ -486,7 +486,7 @@ fn resolve_pgp_mode(
#[allow(clippy::too_many_arguments)]
fn build_pgp_send_config(
conn: &rusqlite::Connection,
conn: &crate::db::pool::Connection,
data_dir: &str,
user_hash: &str,
mode: PgpMode,
+2 -2
View File
@@ -1,8 +1,8 @@
use std::sync::Arc;
use crate::db::pool::Connection;
use axum::response::{IntoResponse, Response};
use axum::{Extension, Json};
use rusqlite::Connection;
use crate::auth::session::SessionState;
use crate::config::AppConfig;
@@ -23,7 +23,7 @@ fn configured_folder_if_present(
};
let exists: bool = conn
.query_row(
crate::queries::sqlite::queries::routes_special_folders::Q_001,
crate::dbq!(routes_special_folders::Q_001),
[folder],
|row| row.get(0),
)
+1 -1
View File
@@ -107,7 +107,7 @@ fn imap_credentials(session: &SessionState) -> ImapCredentials {
}
}
fn keyword_sync_enabled(conn: &rusqlite::Connection) -> bool {
fn keyword_sync_enabled(conn: &crate::db::pool::Connection) -> bool {
db::mail_preferences::get_preferences(conn)
.map(|prefs| prefs.sync_imap_keywords)
.unwrap_or(false)
+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.0\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.1\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()
);
+22 -20
View File
@@ -6,6 +6,7 @@ use sqlx::{FromRow, MySqlPool, PgPool};
use super::{PersistedSession, SessionPersistence};
use crate::config::AppConfig;
use crate::db::pool::{DatabaseBackend, global_schema_for_backend};
use crate::queries;
#[derive(Clone)]
@@ -49,21 +50,19 @@ impl TryFrom<DbSessionRow> for PersistedSession {
password_ciphertext: row.password_ciphertext,
user_hash: row.user_hash,
imap_host: row.imap_host,
imap_port: u16::try_from(row.imap_port)
.map_err(|_| "invalid imap_port in session database")?,
imap_port: u16::try_from(row.imap_port).map_err(|_| "invalid imap_port in database")?,
imap_tls: row.imap_tls != 0,
imap_tls_verify: row.imap_tls_verify != 0,
smtp_host: row.smtp_host,
smtp_port: u16::try_from(row.smtp_port)
.map_err(|_| "invalid smtp_port in session database")?,
smtp_port: u16::try_from(row.smtp_port).map_err(|_| "invalid smtp_port in database")?,
smtp_tls: row.smtp_tls != 0,
smtp_tls_verify: row.smtp_tls_verify != 0,
timeout_secs: u64::try_from(row.timeout_secs)
.map_err(|_| "invalid timeout_secs in session database")?,
.map_err(|_| "invalid timeout_secs in database")?,
created_at_unix: u64::try_from(row.created_at_unix)
.map_err(|_| "invalid created_at_unix in session database")?,
.map_err(|_| "invalid created_at_unix in database")?,
last_accessed_unix: u64::try_from(row.last_accessed_unix)
.map_err(|_| "invalid last_accessed_unix in session database")?,
.map_err(|_| "invalid last_accessed_unix in database")?,
user_agent: row.user_agent,
})
}
@@ -133,8 +132,8 @@ where
F: FnOnce(Connection) -> Result<T, String> + Send + 'static,
{
tokio::task::spawn_blocking(move || {
let conn = Connection::open(path)
.map_err(|e| format!("failed to open SQLite session database: {e}"))?;
let conn =
Connection::open(path).map_err(|e| format!("failed to open SQLite database: {e}"))?;
conn.busy_timeout(std::time::Duration::from_secs(5))
.map_err(|e| format!("failed to configure SQLite busy timeout: {e}"))?;
operation(conn)
@@ -154,9 +153,11 @@ impl DatabaseSessionStore {
"sqlite" => {
let path = sqlite_path(config)?;
let migration_path = path.clone();
sqlite_task(migration_path, |conn| {
conn.execute_batch(include_str!("../../migrations/sqlite/V047__sessions.sql"))
.map_err(|e| format!("SQLite session migration failed: {e}"))?;
let schema = global_schema_for_backend(DatabaseBackend::Sqlite)?;
sqlite_task(migration_path, move |conn| {
conn.execute_batch(&schema).map_err(|e| {
format!("SQLite database schema initialization failed: {e}")
})?;
Ok(())
})
.await?;
@@ -169,11 +170,12 @@ impl DatabaseSessionStore {
.ok_or("DATABASE_URL required for mysql")?;
let pool = MySqlPool::connect(url)
.await
.map_err(|e| format!("failed to connect MySQL session database: {e}"))?;
sqlx::raw_sql(include_str!("../../migrations/mysql/V047__sessions.sql"))
.map_err(|e| format!("failed to connect MySQL database: {e}"))?;
let schema = global_schema_for_backend(DatabaseBackend::Mysql)?;
sqlx::raw_sql(&schema)
.execute(&pool)
.await
.map_err(|e| format!("MySQL session migration failed: {e}"))?;
.map_err(|e| format!("MySQL database schema initialization failed: {e}"))?;
Pool::Mysql(pool)
}
"pgsql" | "postgres" | "postgresql" => {
@@ -183,11 +185,11 @@ impl DatabaseSessionStore {
.ok_or("DATABASE_URL required for pgsql")?;
let pool = PgPool::connect(url)
.await
.map_err(|e| format!("failed to connect PostgreSQL session database: {e}"))?;
sqlx::raw_sql(include_str!("../../migrations/pgsql/V047__sessions.sql"))
.execute(&pool)
.await
.map_err(|e| format!("PostgreSQL session migration failed: {e}"))?;
.map_err(|e| format!("failed to connect PostgreSQL database: {e}"))?;
let schema = global_schema_for_backend(DatabaseBackend::Pgsql)?;
sqlx::raw_sql(&schema).execute(&pool).await.map_err(|e| {
format!("PostgreSQL database schema initialization failed: {e}")
})?;
Pool::Pgsql(pool)
}
other => return Err(format!("unsupported database backend: {other}")),
+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.0 size limit"),
Self::ScriptTooLarge => write!(f, "Sieve script exceeds the RustBox 0.1.1 size limit"),
}
}
}