1 parent
ecc201ef50
commit
698e3c4a1b
124 files changed
+4593
-2721
No files matched your search
+9
-15
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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)]
|
||||
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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)]
|
||||
|
||||
@@ -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
@@ -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
File diff suppressed because it is too large.
Load diff
@@ -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
@@ -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
@@ -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
@@ -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]
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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)?;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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
@@ -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,
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
);
|
||||
|
||||
|
||||
@@ -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
@@ -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"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user