1 parent
0421994fe6
commit
425ea7449b
138 files changed
+5705
-1012
No files matched your search
+29
-54
@@ -1,3 +1,4 @@
|
||||
use crate::queries::sqlite::queries::db_calendar::{EVENT_SELECT_COLS, SETTINGS_SELECT_COLS};
|
||||
use rusqlite::{Connection, params};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
@@ -125,11 +126,6 @@ pub struct MeetingTemplate {
|
||||
pub created_at: String,
|
||||
}
|
||||
|
||||
const EVENT_SELECT_COLS: &str = "id, title, description, location, start_time, end_time, \
|
||||
all_day, recurrence_rule, attendees, organizer, status, source, source_uid, \
|
||||
source_message_uid, source_message_folder, meeting_url, color, reminder_minutes, \
|
||||
created_at, updated_at";
|
||||
|
||||
fn row_to_event(row: &rusqlite::Row<'_>) -> rusqlite::Result<CalendarEvent> {
|
||||
let all_day_int: i32 = row.get(6)?;
|
||||
Ok(CalendarEvent {
|
||||
@@ -191,11 +187,7 @@ pub fn list_events(
|
||||
start: &str,
|
||||
end: &str,
|
||||
) -> Result<Vec<CalendarEvent>, String> {
|
||||
let sql = format!(
|
||||
"SELECT {EVENT_SELECT_COLS} FROM calendar_events
|
||||
WHERE end_time >= ?1 AND start_time <= ?2
|
||||
ORDER BY start_time ASC"
|
||||
);
|
||||
let sql = crate::rustbox_query_db_calendar_001!();
|
||||
let mut stmt = conn
|
||||
.prepare(&sql)
|
||||
.map_err(|e| format!("Failed to prepare list_events: {e}"))?;
|
||||
@@ -211,9 +203,7 @@ pub fn list_events(
|
||||
}
|
||||
|
||||
pub fn list_all_events(conn: &Connection) -> Result<Vec<CalendarEvent>, String> {
|
||||
let sql = format!(
|
||||
"SELECT {EVENT_SELECT_COLS} FROM calendar_events ORDER BY start_time ASC, title ASC"
|
||||
);
|
||||
let sql = crate::rustbox_query_db_calendar_002!();
|
||||
let mut stmt = conn
|
||||
.prepare(&sql)
|
||||
.map_err(|e| format!("Failed to prepare list_all_events: {e}"))?;
|
||||
@@ -229,7 +219,7 @@ 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 = format!("SELECT {EVENT_SELECT_COLS} FROM calendar_events WHERE id = ?1");
|
||||
let sql = crate::rustbox_query_db_calendar_003!();
|
||||
let result = conn.query_row(&sql, params![id], row_to_event);
|
||||
|
||||
match result {
|
||||
@@ -242,16 +232,15 @@ 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("SELECT datetime('now')", [], |row| row.get(0))
|
||||
.query_row(
|
||||
crate::queries::sqlite::queries::db_calendar::Q_001,
|
||||
[],
|
||||
|row| row.get(0),
|
||||
)
|
||||
.map_err(|e| format!("Failed to get current time: {e}"))?;
|
||||
|
||||
conn.execute(
|
||||
"INSERT INTO calendar_events
|
||||
(id, title, description, location, start_time, end_time, all_day,
|
||||
recurrence_rule, attendees, organizer, status, source, source_uid,
|
||||
source_message_uid, source_message_folder, meeting_url, color,
|
||||
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)",
|
||||
crate::queries::sqlite::queries::db_calendar::Q_002,
|
||||
params![
|
||||
id,
|
||||
data.title,
|
||||
@@ -329,10 +318,7 @@ pub fn update_event(
|
||||
|
||||
sets.push("updated_at = datetime('now')".to_string());
|
||||
|
||||
let sql = format!(
|
||||
"UPDATE calendar_events SET {} WHERE id = ?{idx}",
|
||||
sets.join(", ")
|
||||
);
|
||||
let sql = crate::rustbox_query_db_calendar_004!(sets.join(", "), idx);
|
||||
values.push(Box::new(id.to_string()));
|
||||
|
||||
let params_refs: Vec<&dyn rusqlite::types::ToSql> = values.iter().map(|b| b.as_ref()).collect();
|
||||
@@ -349,7 +335,10 @@ pub fn update_event(
|
||||
|
||||
pub fn delete_event(conn: &Connection, id: &str) -> Result<bool, String> {
|
||||
let deleted = conn
|
||||
.execute("DELETE FROM calendar_events WHERE id = ?1", params![id])
|
||||
.execute(
|
||||
crate::queries::sqlite::queries::db_calendar::Q_003,
|
||||
params![id],
|
||||
)
|
||||
.map_err(|e| format!("Failed to delete event: {e}"))?;
|
||||
Ok(deleted > 0)
|
||||
}
|
||||
@@ -358,7 +347,7 @@ pub fn find_event_by_source_uid(
|
||||
conn: &Connection,
|
||||
source_uid: &str,
|
||||
) -> Result<Option<CalendarEvent>, String> {
|
||||
let sql = format!("SELECT {EVENT_SELECT_COLS} FROM calendar_events WHERE source_uid = ?1");
|
||||
let sql = crate::rustbox_query_db_calendar_005!();
|
||||
let result = conn.query_row(&sql, params![source_uid], row_to_event);
|
||||
|
||||
match result {
|
||||
@@ -368,18 +357,11 @@ pub fn find_event_by_source_uid(
|
||||
}
|
||||
}
|
||||
|
||||
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 fn get_calendar_settings(conn: &Connection) -> Result<CalendarSettings, String> {
|
||||
conn.execute(
|
||||
"INSERT OR IGNORE INTO calendar_settings (id) VALUES (1)",
|
||||
[],
|
||||
)
|
||||
.map_err(|e| format!("Failed to ensure calendar_settings row: {e}"))?;
|
||||
conn.execute(crate::queries::sqlite::queries::db_calendar::Q_004, [])
|
||||
.map_err(|e| format!("Failed to ensure calendar_settings row: {e}"))?;
|
||||
|
||||
let sql = format!("SELECT {SETTINGS_SELECT_COLS} FROM calendar_settings WHERE id = 1");
|
||||
let sql = crate::rustbox_query_db_calendar_006!();
|
||||
conn.query_row(&sql, [], row_to_settings)
|
||||
.map_err(|e| format!("Failed to get calendar settings: {e}"))
|
||||
}
|
||||
@@ -388,11 +370,8 @@ pub fn update_calendar_settings(
|
||||
conn: &Connection,
|
||||
data: &UpdateCalendarSettings,
|
||||
) -> Result<CalendarSettings, String> {
|
||||
conn.execute(
|
||||
"INSERT OR IGNORE INTO calendar_settings (id) VALUES (1)",
|
||||
[],
|
||||
)
|
||||
.map_err(|e| format!("Failed to ensure calendar_settings row: {e}"))?;
|
||||
conn.execute(crate::queries::sqlite::queries::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();
|
||||
@@ -436,10 +415,7 @@ pub fn update_calendar_settings(
|
||||
|
||||
if !sets.is_empty() {
|
||||
sets.push("updated_at = datetime('now')".to_string());
|
||||
let sql = format!(
|
||||
"UPDATE calendar_settings SET {} WHERE id = 1",
|
||||
sets.join(", ")
|
||||
);
|
||||
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())
|
||||
@@ -451,10 +427,7 @@ pub fn update_calendar_settings(
|
||||
|
||||
pub fn list_meeting_templates(conn: &Connection) -> Result<Vec<MeetingTemplate>, String> {
|
||||
let mut stmt = conn
|
||||
.prepare(
|
||||
"SELECT id, name, url_template, icon, is_default, created_at
|
||||
FROM meeting_templates ORDER BY is_default DESC, name ASC",
|
||||
)
|
||||
.prepare(crate::queries::sqlite::queries::db_calendar::Q_006)
|
||||
.map_err(|e| format!("Failed to prepare list_meeting_templates: {e}"))?;
|
||||
let rows = stmt
|
||||
.query_map([], row_to_template)
|
||||
@@ -474,21 +447,23 @@ pub fn create_meeting_template(
|
||||
icon: &str,
|
||||
) -> Result<MeetingTemplate, String> {
|
||||
conn.execute(
|
||||
"INSERT INTO meeting_templates (name, url_template, icon) VALUES (?1, ?2, ?3)",
|
||||
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.last_insert_rowid();
|
||||
let sql = "SELECT id, name, url_template, icon, is_default, created_at
|
||||
FROM meeting_templates WHERE id = ?1";
|
||||
let sql = crate::queries::sqlite::queries::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("DELETE FROM meeting_templates WHERE id = ?1", params![id])
|
||||
.execute(
|
||||
crate::queries::sqlite::queries::db_calendar::Q_009,
|
||||
params![id],
|
||||
)
|
||||
.map_err(|e| format!("Failed to delete meeting template: {e}"))?;
|
||||
Ok(deleted > 0)
|
||||
}
|
||||
|
||||
+11
-27
@@ -14,7 +14,7 @@ pub struct ContactGroup {
|
||||
|
||||
pub fn create_group(conn: &Connection, id: &str, name: &str) -> Result<(), String> {
|
||||
conn.execute(
|
||||
"INSERT INTO contact_groups (id, name) VALUES (?1, ?2)",
|
||||
crate::queries::sqlite::queries::db_contact_groups::Q_001,
|
||||
params![id, name],
|
||||
)
|
||||
.map_err(|e| format!("Failed to create contact group: {e}"))?;
|
||||
@@ -23,12 +23,7 @@ pub fn create_group(conn: &Connection, id: &str, name: &str) -> Result<(), Strin
|
||||
|
||||
pub fn list_groups(conn: &Connection) -> Result<Vec<ContactGroup>, String> {
|
||||
let mut stmt = conn
|
||||
.prepare(
|
||||
"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
|
||||
FROM contact_groups g
|
||||
ORDER BY g.name ASC",
|
||||
)
|
||||
.prepare(crate::queries::sqlite::queries::db_contact_groups::Q_002)
|
||||
.map_err(|e| format!("Failed to prepare list_groups: {e}"))?;
|
||||
|
||||
let rows = stmt
|
||||
@@ -53,7 +48,7 @@ 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(
|
||||
"UPDATE contact_groups SET name = ?1, updated_at = datetime('now') WHERE id = ?2",
|
||||
crate::queries::sqlite::queries::db_contact_groups::Q_003,
|
||||
params![name, id],
|
||||
)
|
||||
.map_err(|e| format!("Failed to update contact group: {e}"))?;
|
||||
@@ -62,14 +57,17 @@ pub fn update_group(conn: &Connection, id: &str, name: &str) -> Result<bool, Str
|
||||
|
||||
pub fn delete_group(conn: &Connection, id: &str) -> Result<bool, String> {
|
||||
let deleted = conn
|
||||
.execute("DELETE FROM contact_groups WHERE id = ?1", params![id])
|
||||
.execute(
|
||||
crate::queries::sqlite::queries::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(
|
||||
"INSERT OR IGNORE INTO contact_group_members (group_id, contact_id) VALUES (?1, ?2)",
|
||||
crate::queries::sqlite::queries::db_contact_groups::Q_005,
|
||||
params![group_id, contact_id],
|
||||
)
|
||||
.map_err(|e| format!("Failed to add group member: {e}"))?;
|
||||
@@ -79,7 +77,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(
|
||||
"DELETE FROM contact_group_members WHERE group_id = ?1 AND contact_id = ?2",
|
||||
crate::queries::sqlite::queries::db_contact_groups::Q_006,
|
||||
params![group_id, contact_id],
|
||||
)
|
||||
.map_err(|e| format!("Failed to remove group member: {e}"))?;
|
||||
@@ -88,14 +86,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(
|
||||
"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,
|
||||
c.last_contacted, c.contact_count, c.source, c.created_at, c.updated_at, c.emails_json, c.phones_json, c.addresses_json, EXISTS(SELECT 1 FROM contact_photos p WHERE p.contact_id = c.id) AS has_photo
|
||||
FROM contacts c
|
||||
JOIN contact_group_members m ON m.contact_id = c.id
|
||||
WHERE m.group_id = ?1
|
||||
ORDER BY c.name ASC",
|
||||
)
|
||||
.prepare(crate::queries::sqlite::queries::db_contact_groups::Q_007)
|
||||
.map_err(|e| format!("Failed to prepare list_group_members: {e}"))?;
|
||||
|
||||
let rows = stmt
|
||||
@@ -140,14 +131,7 @@ pub fn list_contact_groups(
|
||||
contact_id: &str,
|
||||
) -> Result<Vec<ContactGroup>, String> {
|
||||
let mut stmt = conn
|
||||
.prepare(
|
||||
"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
|
||||
FROM contact_groups g
|
||||
JOIN contact_group_members m ON m.group_id = g.id
|
||||
WHERE m.contact_id = ?1
|
||||
ORDER BY g.name ASC",
|
||||
)
|
||||
.prepare(crate::queries::sqlite::queries::db_contact_groups::Q_008)
|
||||
.map_err(|e| format!("Failed to prepare list_contact_groups: {e}"))?;
|
||||
|
||||
let rows = stmt
|
||||
|
||||
+21
-63
@@ -1,3 +1,4 @@
|
||||
use crate::queries::sqlite::queries::db_contacts::CONTACT_SELECT_COLS;
|
||||
use rusqlite::{Connection, params};
|
||||
use serde::Serialize;
|
||||
use uuid::Uuid;
|
||||
@@ -58,8 +59,6 @@ fn row_to_contact(row: &rusqlite::Row<'_>) -> rusqlite::Result<Contact> {
|
||||
})
|
||||
}
|
||||
|
||||
const CONTACT_SELECT_COLS: &str = "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, EXISTS(SELECT 1 FROM contact_photos p WHERE p.contact_id = contacts.id) AS has_photo";
|
||||
|
||||
fn escape_like(term: &str) -> String {
|
||||
term.replace('\\', "\\\\")
|
||||
.replace('%', "\\%")
|
||||
@@ -68,16 +67,7 @@ fn escape_like(term: &str) -> String {
|
||||
|
||||
pub fn upsert_contact(conn: &Connection, contact: &Contact) -> Result<(), String> {
|
||||
conn.execute(
|
||||
"INSERT INTO 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 (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18, ?19)
|
||||
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",
|
||||
crate::queries::sqlite::queries::db_contacts::Q_001,
|
||||
params![
|
||||
contact.id,
|
||||
contact.email,
|
||||
@@ -105,7 +95,7 @@ pub fn upsert_contact(conn: &Connection, contact: &Contact) -> Result<(), String
|
||||
}
|
||||
|
||||
pub fn get_contact(conn: &Connection, id: &str) -> Result<Option<Contact>, String> {
|
||||
let sql = format!("SELECT {CONTACT_SELECT_COLS} FROM contacts WHERE id = ?1");
|
||||
let sql = crate::rustbox_query_db_contacts_001!();
|
||||
let result = conn.query_row(&sql, params![id], row_to_contact);
|
||||
|
||||
match result {
|
||||
@@ -116,11 +106,7 @@ pub fn get_contact(conn: &Connection, id: &str) -> Result<Option<Contact>, Strin
|
||||
}
|
||||
|
||||
pub fn get_contact_by_email(conn: &Connection, email: &str) -> Result<Option<Contact>, String> {
|
||||
let sql = format!(
|
||||
"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))"
|
||||
);
|
||||
let sql = crate::rustbox_query_db_contacts_002!();
|
||||
let result = conn.query_row(&sql, params![email], row_to_contact);
|
||||
|
||||
match result {
|
||||
@@ -140,22 +126,13 @@ pub fn list_contacts(
|
||||
Some(q) if !q.is_empty() => {
|
||||
let escaped = escape_like(q);
|
||||
let pattern = format!("%{escaped}%");
|
||||
let sql = format!(
|
||||
"SELECT {CONTACT_SELECT_COLS} FROM contacts
|
||||
WHERE name LIKE ?1 ESCAPE '\\' OR email LIKE ?1 ESCAPE '\\' OR emails_json LIKE ?1 ESCAPE '\\'
|
||||
ORDER BY name ASC
|
||||
LIMIT ?2 OFFSET ?3"
|
||||
);
|
||||
let sql = crate::rustbox_query_db_contacts_003!();
|
||||
|
||||
drop(pattern);
|
||||
(sql, Some(escaped))
|
||||
}
|
||||
_ => {
|
||||
let sql = format!(
|
||||
"SELECT {CONTACT_SELECT_COLS} FROM contacts
|
||||
ORDER BY name ASC
|
||||
LIMIT ?1 OFFSET ?2"
|
||||
);
|
||||
let sql = crate::rustbox_query_db_contacts_004!();
|
||||
(sql, None)
|
||||
}
|
||||
};
|
||||
@@ -193,7 +170,10 @@ pub fn list_contacts(
|
||||
|
||||
pub fn delete_contact(conn: &Connection, id: &str) -> Result<bool, String> {
|
||||
let deleted = conn
|
||||
.execute("DELETE FROM contacts WHERE id = ?1", params![id])
|
||||
.execute(
|
||||
crate::queries::sqlite::queries::db_contacts::Q_002,
|
||||
params![id],
|
||||
)
|
||||
.map_err(|e| format!("Failed to delete contact: {e}"))?;
|
||||
Ok(deleted > 0)
|
||||
}
|
||||
@@ -202,12 +182,7 @@ pub fn search_contacts(conn: &Connection, query: &str, limit: u32) -> Result<Vec
|
||||
let escaped = escape_like(query);
|
||||
let pattern = format!("%{escaped}%");
|
||||
|
||||
let sql = format!(
|
||||
"SELECT {CONTACT_SELECT_COLS} FROM contacts
|
||||
WHERE name LIKE ?1 ESCAPE '\\' OR email LIKE ?1 ESCAPE '\\' OR emails_json LIKE ?1 ESCAPE '\\'
|
||||
ORDER BY name ASC
|
||||
LIMIT ?2"
|
||||
);
|
||||
let sql = crate::rustbox_query_db_contacts_005!();
|
||||
|
||||
let mut stmt = conn
|
||||
.prepare(&sql)
|
||||
@@ -231,18 +206,7 @@ pub fn search_known_addresses(
|
||||
let escaped = escape_like(query);
|
||||
let pattern = format!("%{escaped}%");
|
||||
|
||||
let sql = r#"
|
||||
SELECT email, name
|
||||
FROM known_addresses
|
||||
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
|
||||
WHERE lower(alt.value) = lower(known_addresses.email)
|
||||
)
|
||||
ORDER BY email ASC
|
||||
LIMIT ?2
|
||||
"#;
|
||||
let sql = crate::queries::sqlite::queries::db_contacts::Q_003;
|
||||
|
||||
let mut stmt = conn
|
||||
.prepare(sql)
|
||||
@@ -273,8 +237,7 @@ pub fn populate_known_addresses(
|
||||
) -> Result<(), String> {
|
||||
if !from_address.is_empty() {
|
||||
conn.execute(
|
||||
"INSERT INTO known_addresses (email, name) VALUES (?1, ?2)
|
||||
ON CONFLICT(email) DO UPDATE SET name = excluded.name WHERE excluded.name != ''",
|
||||
crate::queries::sqlite::queries::db_contacts::Q_004,
|
||||
params![from_address, from_name],
|
||||
)
|
||||
.map_err(|e| format!("Failed to insert known address (from): {e}"))?;
|
||||
@@ -300,8 +263,7 @@ fn insert_addresses_from_json(conn: &Connection, json: &str) -> Result<(), Strin
|
||||
}
|
||||
let name = entry.get("name").and_then(|v| v.as_str()).unwrap_or("");
|
||||
conn.execute(
|
||||
"INSERT INTO known_addresses (email, name) VALUES (?1, ?2)
|
||||
ON CONFLICT(email) DO UPDATE SET name = excluded.name WHERE excluded.name != ''",
|
||||
crate::queries::sqlite::queries::db_contacts::Q_005,
|
||||
params![email, name],
|
||||
)
|
||||
.map_err(|e| format!("Failed to insert known address (json): {e}"))?;
|
||||
@@ -313,11 +275,7 @@ 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(
|
||||
"UPDATE contacts
|
||||
SET contact_count = contact_count + 1,
|
||||
last_contacted = datetime('now'),
|
||||
updated_at = datetime('now')
|
||||
WHERE email = ?1",
|
||||
crate::queries::sqlite::queries::db_contacts::Q_006,
|
||||
params![email],
|
||||
)
|
||||
.map_err(|e| format!("Failed to increment contact count: {e}"))?;
|
||||
@@ -328,8 +286,7 @@ pub fn increment_contact_count(conn: &Connection, email: &str) -> Result<(), Str
|
||||
pub fn auto_add_contact(conn: &Connection, email: &str, name: &str) -> Result<(), String> {
|
||||
let id = Uuid::new_v4().to_string();
|
||||
conn.execute(
|
||||
"INSERT OR IGNORE INTO contacts (id, email, name, source)
|
||||
VALUES (?1, ?2, ?3, 'auto')",
|
||||
crate::queries::sqlite::queries::db_contacts::Q_007,
|
||||
params![id, email, name],
|
||||
)
|
||||
.map_err(|e| format!("Failed to auto-add contact: {e}"))?;
|
||||
@@ -343,9 +300,10 @@ pub fn set_contact_photo(
|
||||
data: &[u8],
|
||||
) -> Result<(), String> {
|
||||
conn.execute(
|
||||
"INSERT INTO contact_photos (contact_id, content_type, data, updated_at) VALUES (?1, ?2, ?3, datetime('now')) ON CONFLICT(contact_id) DO UPDATE SET content_type=excluded.content_type, data=excluded.data, updated_at=datetime('now')",
|
||||
crate::queries::sqlite::queries::db_contacts::Q_008,
|
||||
params![contact_id, content_type, data],
|
||||
).map_err(|e| format!("Failed to save contact photo: {e}"))?;
|
||||
)
|
||||
.map_err(|e| format!("Failed to save contact photo: {e}"))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -354,7 +312,7 @@ pub fn get_contact_photo(
|
||||
contact_id: &str,
|
||||
) -> Result<Option<(String, Vec<u8>)>, String> {
|
||||
match conn.query_row(
|
||||
"SELECT content_type, data FROM contact_photos WHERE contact_id = ?1",
|
||||
crate::queries::sqlite::queries::db_contacts::Q_009,
|
||||
params![contact_id],
|
||||
|row| Ok((row.get(0)?, row.get(1)?)),
|
||||
) {
|
||||
@@ -366,7 +324,7 @@ pub fn get_contact_photo(
|
||||
|
||||
pub fn delete_contact_photo(conn: &Connection, contact_id: &str) -> Result<bool, String> {
|
||||
conn.execute(
|
||||
"DELETE FROM contact_photos WHERE contact_id = ?1",
|
||||
crate::queries::sqlite::queries::db_contacts::Q_010,
|
||||
params![contact_id],
|
||||
)
|
||||
.map(|n| n > 0)
|
||||
|
||||
+103
-52
@@ -185,21 +185,65 @@ fn defaults() -> DisplayPreferences {
|
||||
|
||||
pub fn get_preferences(conn: &Connection) -> Result<DisplayPreferences, String> {
|
||||
let result = conn.query_row(
|
||||
"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",
|
||||
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)?,
|
||||
}),
|
||||
|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),
|
||||
@@ -231,7 +275,7 @@ pub fn update_preferences(
|
||||
data: &UpdateDisplayPreferences,
|
||||
) -> Result<DisplayPreferences, String> {
|
||||
conn.execute(
|
||||
"INSERT OR IGNORE INTO display_preferences (id) VALUES (1)",
|
||||
crate::queries::sqlite::queries::db_display_preferences::Q_002,
|
||||
[],
|
||||
)
|
||||
.map_err(|e| format!("Failed to ensure preferences row: {e}"))?;
|
||||
@@ -362,6 +406,12 @@ pub fn update_preferences(
|
||||
"yahoo",
|
||||
"hey",
|
||||
"nord",
|
||||
"github",
|
||||
"gitlab",
|
||||
"discord",
|
||||
"slack",
|
||||
"reddit",
|
||||
"notion",
|
||||
],
|
||||
)?;
|
||||
}
|
||||
@@ -386,10 +436,10 @@ pub fn update_preferences(
|
||||
if let Some(v) = data.sidebar_item_height {
|
||||
range("sidebar_item_height", v, 24, 48)?;
|
||||
}
|
||||
if let Some(v) = data.reader_max_width {
|
||||
if v != 0 {
|
||||
range("reader_max_width", v, 560, 1400)?;
|
||||
}
|
||||
if let Some(v) = data.reader_max_width
|
||||
&& v != 0
|
||||
{
|
||||
range("reader_max_width", v, 560, 1400)?;
|
||||
}
|
||||
if let Some(v) = data.composer_width {
|
||||
range("composer_width", v, 640, 1400)?;
|
||||
@@ -397,46 +447,45 @@ pub fn update_preferences(
|
||||
if let Some(v) = data.unread_font_weight {
|
||||
range("unread_font_weight", v, 500, 800)?;
|
||||
}
|
||||
if let Some(ref v) = data.accent_color {
|
||||
if !valid_hex(v) {
|
||||
return Err(format!("Invalid accent_color: {v}"));
|
||||
}
|
||||
if let Some(ref v) = data.accent_color
|
||||
&& !valid_hex(v)
|
||||
{
|
||||
return Err(format!("Invalid accent_color: {v}"));
|
||||
}
|
||||
if let Some(ref v) = data.background_color {
|
||||
if !valid_hex(v) {
|
||||
return Err(format!("Invalid background_color: {v}"));
|
||||
}
|
||||
if let Some(ref v) = data.background_color
|
||||
&& !valid_hex(v)
|
||||
{
|
||||
return Err(format!("Invalid background_color: {v}"));
|
||||
}
|
||||
if let Some(ref v) = data.panel_color {
|
||||
if !valid_hex(v) {
|
||||
return Err(format!("Invalid panel_color: {v}"));
|
||||
}
|
||||
if let Some(ref v) = data.panel_color
|
||||
&& !valid_hex(v)
|
||||
{
|
||||
return Err(format!("Invalid panel_color: {v}"));
|
||||
}
|
||||
if let Some(ref v) = data.text_color {
|
||||
if !valid_hex(v) {
|
||||
return Err(format!("Invalid text_color: {v}"));
|
||||
}
|
||||
if let Some(ref v) = data.text_color
|
||||
&& !valid_hex(v)
|
||||
{
|
||||
return Err(format!("Invalid text_color: {v}"));
|
||||
}
|
||||
if let Some(ref v) = data.selection_color {
|
||||
if !valid_hex(v) {
|
||||
return Err(format!("Invalid selection_color: {v}"));
|
||||
}
|
||||
if let Some(ref v) = data.selection_color
|
||||
&& !valid_hex(v)
|
||||
{
|
||||
return Err(format!("Invalid selection_color: {v}"));
|
||||
}
|
||||
for (name, value) in [
|
||||
("folder_background", data.folder_background.as_deref()),
|
||||
("message_background", data.message_background.as_deref()),
|
||||
("reader_background", data.reader_background.as_deref()),
|
||||
] {
|
||||
if let Some(v) = value {
|
||||
if !v.is_empty() && !valid_hex(v) {
|
||||
return Err(format!("Invalid {name}: {v}"));
|
||||
}
|
||||
if let Some(v) = value
|
||||
&& !v.is_empty()
|
||||
&& !valid_hex(v)
|
||||
{
|
||||
return Err(format!("Invalid {name}: {v}"));
|
||||
}
|
||||
}
|
||||
if let Some(ref mode) = data.animation_mode {
|
||||
if let Some(v) = mode {
|
||||
enum_value("animation_mode", v, &["rich", "medium", "subtle", "off"])?;
|
||||
}
|
||||
if let Some(Some(v)) = data.animation_mode.as_ref() {
|
||||
enum_value("animation_mode", v, &["rich", "medium", "subtle", "off"])?;
|
||||
}
|
||||
|
||||
set_text!(density, "density");
|
||||
@@ -500,11 +549,7 @@ pub fn update_preferences(
|
||||
return get_preferences(conn);
|
||||
}
|
||||
sets.push("updated_at = datetime('now')".to_string());
|
||||
let sql = format!(
|
||||
"UPDATE display_preferences SET {} WHERE id = ?{}",
|
||||
sets.join(", "),
|
||||
idx
|
||||
);
|
||||
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())
|
||||
@@ -558,6 +603,12 @@ mod tests {
|
||||
"yahoo",
|
||||
"hey",
|
||||
"nord",
|
||||
"github",
|
||||
"gitlab",
|
||||
"discord",
|
||||
"slack",
|
||||
"reddit",
|
||||
"notion",
|
||||
] {
|
||||
let conn = open_test_db();
|
||||
let update: UpdateDisplayPreferences =
|
||||
|
||||
+14
-34
@@ -72,25 +72,7 @@ pub fn upsert_draft(
|
||||
) -> Result<(), String> {
|
||||
validate_draft_id(id)?;
|
||||
conn.execute(
|
||||
"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, datetime('now'))
|
||||
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,
|
||||
updated_at = datetime('now')",
|
||||
crate::queries::sqlite::queries::db_drafts::Q_001,
|
||||
params![
|
||||
id,
|
||||
to_addresses,
|
||||
@@ -116,8 +98,7 @@ pub fn upsert_draft(
|
||||
|
||||
pub fn get_draft(conn: &Connection, id: &str) -> Result<Option<Draft>, String> {
|
||||
let result = conn.query_row(
|
||||
"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",
|
||||
crate::queries::sqlite::queries::db_drafts::Q_002,
|
||||
params![id],
|
||||
|row| {
|
||||
Ok(Draft {
|
||||
@@ -155,10 +136,7 @@ pub fn get_draft(conn: &Connection, id: &str) -> Result<Option<Draft>, String> {
|
||||
|
||||
pub fn list_drafts(conn: &Connection) -> Result<Vec<Draft>, String> {
|
||||
let mut stmt = conn
|
||||
.prepare(
|
||||
"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",
|
||||
)
|
||||
.prepare(crate::queries::sqlite::queries::db_drafts::Q_003)
|
||||
.map_err(|e| format!("Failed to prepare list_drafts: {e}"))?;
|
||||
|
||||
let rows = stmt
|
||||
@@ -205,7 +183,7 @@ pub fn set_remote_location(
|
||||
) -> Result<(), String> {
|
||||
let updated = conn
|
||||
.execute(
|
||||
"UPDATE drafts SET remote_folder = ?2, remote_uid = ?3, remote_uid_validity = ?4 WHERE id = ?1",
|
||||
crate::queries::sqlite::queries::db_drafts::Q_004,
|
||||
params![id, folder, uid, uid_validity],
|
||||
)
|
||||
.map_err(|e| format!("Failed to update remote draft location: {e}"))?;
|
||||
@@ -217,7 +195,10 @@ pub fn set_remote_location(
|
||||
|
||||
pub fn delete_draft(conn: &Connection, id: &str) -> Result<bool, String> {
|
||||
let deleted = conn
|
||||
.execute("DELETE FROM drafts WHERE id = ?1", params![id])
|
||||
.execute(
|
||||
crate::queries::sqlite::queries::db_drafts::Q_005,
|
||||
params![id],
|
||||
)
|
||||
.map_err(|e| format!("Failed to delete draft: {e}"))?;
|
||||
Ok(deleted > 0)
|
||||
}
|
||||
@@ -232,8 +213,7 @@ pub fn add_draft_attachment(
|
||||
file_path: &str,
|
||||
) -> Result<(), String> {
|
||||
conn.execute(
|
||||
"INSERT INTO draft_attachments (id, draft_id, filename, content_type, size, file_path)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
|
||||
crate::queries::sqlite::queries::db_drafts::Q_006,
|
||||
params![id, draft_id, filename, content_type, size, file_path],
|
||||
)
|
||||
.map_err(|e| format!("Failed to add draft attachment: {e}"))?;
|
||||
@@ -245,10 +225,7 @@ pub fn get_draft_attachments(
|
||||
draft_id: &str,
|
||||
) -> Result<Vec<DraftAttachment>, String> {
|
||||
let mut stmt = conn
|
||||
.prepare(
|
||||
"SELECT id, draft_id, filename, content_type, size, file_path, created_at
|
||||
FROM draft_attachments WHERE draft_id = ?1 ORDER BY created_at ASC",
|
||||
)
|
||||
.prepare(crate::queries::sqlite::queries::db_drafts::Q_007)
|
||||
.map_err(|e| format!("Failed to prepare get_draft_attachments: {e}"))?;
|
||||
|
||||
let rows = stmt
|
||||
@@ -274,7 +251,10 @@ pub fn get_draft_attachments(
|
||||
|
||||
pub fn delete_draft_attachment(conn: &Connection, id: &str) -> Result<bool, String> {
|
||||
let deleted = conn
|
||||
.execute("DELETE FROM draft_attachments WHERE id = ?1", params![id])
|
||||
.execute(
|
||||
crate::queries::sqlite::queries::db_drafts::Q_008,
|
||||
params![id],
|
||||
)
|
||||
.map_err(|e| format!("Failed to delete draft attachment: {e}"))?;
|
||||
Ok(deleted > 0)
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ pub struct UpdateFolderPreferences {
|
||||
|
||||
fn get_override(conn: &Connection, folder: &str) -> Result<Option<bool>, String> {
|
||||
conn.query_row(
|
||||
"SELECT flagged_on_top FROM folder_preferences WHERE folder_name = ?1",
|
||||
crate::queries::sqlite::queries::db_folder_preferences::Q_001,
|
||||
[folder],
|
||||
|row| row.get::<_, Option<i32>>(0),
|
||||
)
|
||||
@@ -29,7 +29,7 @@ pub fn effective_flagged_on_top(conn: &Connection, folder: &str) -> Result<bool,
|
||||
}
|
||||
let global = conn
|
||||
.query_row(
|
||||
"SELECT flagged_on_top FROM mail_preferences WHERE id = 1",
|
||||
crate::queries::sqlite::queries::db_folder_preferences::Q_002,
|
||||
[],
|
||||
|row| row.get::<_, i32>(0),
|
||||
)
|
||||
@@ -40,7 +40,7 @@ pub fn effective_flagged_on_top(conn: &Connection, folder: &str) -> Result<bool,
|
||||
pub fn get_preferences(conn: &Connection, folder: &str) -> Result<FolderPreferences, String> {
|
||||
let exists: bool = conn
|
||||
.query_row(
|
||||
"SELECT EXISTS(SELECT 1 FROM folders WHERE name = ?1)",
|
||||
crate::queries::sqlite::queries::db_folder_preferences::Q_003,
|
||||
[folder],
|
||||
|row| row.get(0),
|
||||
)
|
||||
@@ -51,7 +51,7 @@ pub fn get_preferences(conn: &Connection, folder: &str) -> Result<FolderPreferen
|
||||
let flagged_on_top = get_override(conn, folder)?;
|
||||
let effective_flagged_on_top = flagged_on_top.unwrap_or(
|
||||
conn.query_row(
|
||||
"SELECT flagged_on_top FROM mail_preferences WHERE id = 1",
|
||||
crate::queries::sqlite::queries::db_folder_preferences::Q_004,
|
||||
[],
|
||||
|row| row.get::<_, i32>(0),
|
||||
)
|
||||
@@ -71,7 +71,7 @@ pub fn update_preferences(
|
||||
) -> Result<FolderPreferences, String> {
|
||||
let exists: bool = conn
|
||||
.query_row(
|
||||
"SELECT EXISTS(SELECT 1 FROM folders WHERE name = ?1)",
|
||||
crate::queries::sqlite::queries::db_folder_preferences::Q_005,
|
||||
[folder],
|
||||
|row| row.get(0),
|
||||
)
|
||||
@@ -83,14 +83,17 @@ pub fn update_preferences(
|
||||
match data.flagged_on_top {
|
||||
Some(value) => {
|
||||
conn.execute(
|
||||
"INSERT INTO folder_preferences(folder_name, flagged_on_top, updated_at) VALUES(?1, ?2, datetime('now'))\n ON CONFLICT(folder_name) DO UPDATE SET flagged_on_top = excluded.flagged_on_top, updated_at = datetime('now')",
|
||||
crate::queries::sqlite::queries::db_folder_preferences::Q_006,
|
||||
params![folder, value as i32],
|
||||
)
|
||||
.map_err(|error| format!("Failed to update folder preferences: {error}"))?;
|
||||
}
|
||||
None => {
|
||||
conn.execute("DELETE FROM folder_preferences WHERE folder_name = ?1", [folder])
|
||||
.map_err(|error| format!("Failed to clear folder preferences: {error}"))?;
|
||||
conn.execute(
|
||||
crate::queries::sqlite::queries::db_folder_preferences::Q_007,
|
||||
[folder],
|
||||
)
|
||||
.map_err(|error| format!("Failed to clear folder preferences: {error}"))?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,14 +103,18 @@ pub fn update_preferences(
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::db::{folders, mail_preferences};
|
||||
use crate::db::pool::open_test_db;
|
||||
use crate::db::{folders, mail_preferences};
|
||||
|
||||
#[test]
|
||||
fn folder_override_inherits_and_overrides_global_value() {
|
||||
let conn = open_test_db();
|
||||
folders::insert_folder_if_new(&conn, "INBOX", Some("/"), "").unwrap();
|
||||
assert!(!get_preferences(&conn, "INBOX").unwrap().effective_flagged_on_top);
|
||||
assert!(
|
||||
!get_preferences(&conn, "INBOX")
|
||||
.unwrap()
|
||||
.effective_flagged_on_top
|
||||
);
|
||||
|
||||
mail_preferences::update_preferences(
|
||||
&conn,
|
||||
@@ -125,12 +132,18 @@ mod tests {
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert!(get_preferences(&conn, "INBOX").unwrap().effective_flagged_on_top);
|
||||
assert!(
|
||||
get_preferences(&conn, "INBOX")
|
||||
.unwrap()
|
||||
.effective_flagged_on_top
|
||||
);
|
||||
|
||||
let overridden = update_preferences(
|
||||
&conn,
|
||||
"INBOX",
|
||||
&UpdateFolderPreferences { flagged_on_top: Some(false) },
|
||||
&UpdateFolderPreferences {
|
||||
flagged_on_top: Some(false),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(overridden.flagged_on_top, Some(false));
|
||||
@@ -139,7 +152,9 @@ mod tests {
|
||||
let inherited = update_preferences(
|
||||
&conn,
|
||||
"INBOX",
|
||||
&UpdateFolderPreferences { flagged_on_top: None },
|
||||
&UpdateFolderPreferences {
|
||||
flagged_on_top: None,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(inherited.flagged_on_top, None);
|
||||
|
||||
+42
-63
@@ -28,10 +28,7 @@ pub fn upsert_folder(
|
||||
highest_modseq: u64,
|
||||
) -> Result<(), String> {
|
||||
conn.execute(
|
||||
"INSERT OR REPLACE 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, datetime('now'))",
|
||||
crate::queries::sqlite::queries::db_folders::Q_001,
|
||||
params![
|
||||
folder_name,
|
||||
delimiter,
|
||||
@@ -55,9 +52,7 @@ pub fn insert_folder_if_new(
|
||||
flags_csv: &str,
|
||||
) -> Result<(), String> {
|
||||
conn.execute(
|
||||
"INSERT OR IGNORE INTO folders
|
||||
(name, delimiter, flags, is_subscribed, updated_at)
|
||||
VALUES (?1, ?2, ?3, 1, datetime('now'))",
|
||||
crate::queries::sqlite::queries::db_folders::Q_002,
|
||||
params![folder_name, delimiter, flags_csv],
|
||||
)
|
||||
.map_err(|e| format!("Failed to insert folder: {e}"))?;
|
||||
@@ -66,12 +61,7 @@ pub fn insert_folder_if_new(
|
||||
|
||||
pub fn get_all_folders(conn: &Connection) -> Result<Vec<CachedFolder>, String> {
|
||||
let mut stmt = conn
|
||||
.prepare(
|
||||
"SELECT name, delimiter, parent, flags, is_subscribed,
|
||||
total_count, unread_count, uid_validity, highest_modseq
|
||||
FROM folders
|
||||
ORDER BY name",
|
||||
)
|
||||
.prepare(crate::queries::sqlite::queries::db_folders::Q_003)
|
||||
.map_err(|e| format!("Failed to prepare get_all_folders: {e}"))?;
|
||||
|
||||
let rows = stmt
|
||||
@@ -102,16 +92,13 @@ 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("DELETE FROM folders", [])
|
||||
.execute(crate::queries::sqlite::queries::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 = format!(
|
||||
"DELETE FROM folders WHERE name NOT IN ({})",
|
||||
placeholders.join(", ")
|
||||
);
|
||||
let sql = crate::rustbox_query_db_folders_001!(placeholders.join(", "));
|
||||
|
||||
let params: Vec<&dyn rusqlite::types::ToSql> = current_names
|
||||
.iter()
|
||||
@@ -126,7 +113,7 @@ pub fn remove_stale_folders(conn: &Connection, current_names: &[String]) -> Resu
|
||||
}
|
||||
|
||||
pub fn touch_all_folders(conn: &Connection) -> Result<(), String> {
|
||||
conn.execute("UPDATE folders SET updated_at = datetime('now')", [])
|
||||
conn.execute(crate::queries::sqlite::queries::db_folders::Q_005, [])
|
||||
.map_err(|e| format!("Failed to touch folders: {e}"))?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -138,8 +125,7 @@ pub fn update_folder_status(
|
||||
total_count: u32,
|
||||
) -> Result<(), String> {
|
||||
conn.execute(
|
||||
"UPDATE folders SET uid_validity = ?1, total_count = ?2, messages_updated_at = datetime('now')
|
||||
WHERE name = ?3",
|
||||
crate::queries::sqlite::queries::db_folders::Q_006,
|
||||
params![uid_validity, total_count, name],
|
||||
)
|
||||
.map_err(|e| format!("Failed to update folder status: {e}"))?;
|
||||
@@ -154,8 +140,7 @@ pub fn update_folder_sync_status(
|
||||
highest_modseq: u64,
|
||||
) -> Result<(), String> {
|
||||
conn.execute(
|
||||
"UPDATE folders SET uid_validity = ?1, total_count = ?2, highest_modseq = ?3
|
||||
WHERE name = ?4",
|
||||
crate::queries::sqlite::queries::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}"))?;
|
||||
@@ -165,14 +150,14 @@ pub fn update_folder_sync_status(
|
||||
pub fn refresh_unread_count(conn: &Connection, folder_name: &str) -> Result<(), String> {
|
||||
let unread: u32 = conn
|
||||
.query_row(
|
||||
"SELECT COUNT(*) FROM messages WHERE folder = ?1 AND flags NOT LIKE '%\\Seen%'",
|
||||
crate::queries::sqlite::queries::db_folders::Q_008,
|
||||
params![folder_name],
|
||||
|row| row.get(0),
|
||||
)
|
||||
.map_err(|e| format!("Failed to count unread messages: {e}"))?;
|
||||
|
||||
conn.execute(
|
||||
"UPDATE folders SET unread_count = ?1 WHERE name = ?2",
|
||||
crate::queries::sqlite::queries::db_folders::Q_009,
|
||||
params![unread, folder_name],
|
||||
)
|
||||
.map_err(|e| format!("Failed to update unread_count: {e}"))?;
|
||||
@@ -182,7 +167,7 @@ pub fn refresh_unread_count(conn: &Connection, folder_name: &str) -> Result<(),
|
||||
|
||||
pub fn adjust_unread_count(conn: &Connection, folder_name: &str, delta: i32) -> Result<(), String> {
|
||||
conn.execute(
|
||||
"UPDATE folders SET unread_count = MAX(0, CAST(unread_count AS INTEGER) + ?1) WHERE name = ?2",
|
||||
crate::queries::sqlite::queries::db_folders::Q_010,
|
||||
params![delta, folder_name],
|
||||
)
|
||||
.map_err(|e| format!("Failed to adjust unread_count: {e}"))?;
|
||||
@@ -192,10 +177,7 @@ pub fn adjust_unread_count(conn: &Connection, folder_name: &str, delta: i32) ->
|
||||
pub fn is_folder_cache_fresh(conn: &Connection, max_age_secs: u32) -> Result<bool, String> {
|
||||
let fresh: bool = conn
|
||||
.query_row(
|
||||
"SELECT EXISTS(
|
||||
SELECT 1 FROM folders
|
||||
WHERE updated_at > datetime('now', ?1)
|
||||
)",
|
||||
crate::queries::sqlite::queries::db_folders::Q_011,
|
||||
params![format!("-{max_age_secs} seconds")],
|
||||
|row| row.get(0),
|
||||
)
|
||||
@@ -210,10 +192,7 @@ pub fn is_folder_fresh(
|
||||
) -> Result<bool, String> {
|
||||
let fresh: bool = conn
|
||||
.query_row(
|
||||
"SELECT EXISTS(
|
||||
SELECT 1 FROM folders
|
||||
WHERE name = ?1 AND messages_updated_at > datetime('now', ?2)
|
||||
)",
|
||||
crate::queries::sqlite::queries::db_folders::Q_012,
|
||||
params![folder_name, format!("-{max_age_secs} seconds")],
|
||||
|row| row.get(0),
|
||||
)
|
||||
@@ -227,7 +206,7 @@ pub fn is_folder_messages_invalidated(
|
||||
) -> Result<bool, String> {
|
||||
let invalidated: bool = conn
|
||||
.query_row(
|
||||
"SELECT messages_updated_at IS NULL FROM folders WHERE name = ?1",
|
||||
crate::queries::sqlite::queries::db_folders::Q_013,
|
||||
params![folder_name],
|
||||
|row| row.get(0),
|
||||
)
|
||||
@@ -237,7 +216,7 @@ pub fn is_folder_messages_invalidated(
|
||||
|
||||
pub fn invalidate_folder_freshness(conn: &Connection, folder_name: &str) -> Result<(), String> {
|
||||
conn.execute(
|
||||
"UPDATE folders SET messages_updated_at = NULL WHERE name = ?1",
|
||||
crate::queries::sqlite::queries::db_folders::Q_014,
|
||||
params![folder_name],
|
||||
)
|
||||
.map_err(|e| format!("Failed to invalidate folder freshness: {e}"))?;
|
||||
@@ -246,10 +225,7 @@ pub fn invalidate_folder_freshness(conn: &Connection, folder_name: &str) -> Resu
|
||||
|
||||
pub fn get_folder(conn: &Connection, name: &str) -> Result<Option<CachedFolder>, String> {
|
||||
let result = conn.query_row(
|
||||
"SELECT name, delimiter, parent, flags, is_subscribed,
|
||||
total_count, unread_count, uid_validity, highest_modseq
|
||||
FROM folders
|
||||
WHERE name = ?1",
|
||||
crate::queries::sqlite::queries::db_folders::Q_015,
|
||||
params![name],
|
||||
|row| {
|
||||
let is_subscribed_int: i32 = row.get(4)?;
|
||||
@@ -277,12 +253,15 @@ pub fn get_folder(conn: &Connection, name: &str) -> Result<Option<CachedFolder>,
|
||||
|
||||
pub fn delete_folder_and_messages(conn: &Connection, folder_name: &str) -> Result<(), String> {
|
||||
conn.execute(
|
||||
"DELETE FROM messages WHERE folder = ?1",
|
||||
crate::queries::sqlite::queries::db_folders::Q_016,
|
||||
params![folder_name],
|
||||
)
|
||||
.map_err(|e| format!("Failed to delete folder messages: {e}"))?;
|
||||
conn.execute("DELETE FROM folders WHERE name = ?1", params![folder_name])
|
||||
.map_err(|e| format!("Failed to delete folder: {e}"))?;
|
||||
conn.execute(
|
||||
crate::queries::sqlite::queries::db_folders::Q_017,
|
||||
params![folder_name],
|
||||
)
|
||||
.map_err(|e| format!("Failed to delete folder: {e}"))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -294,41 +273,44 @@ 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("PRAGMA defer_foreign_keys = ON;")
|
||||
tx.execute_batch(crate::queries::sqlite::queries::db_folders::Q_018)
|
||||
.map_err(|e| format!("Failed to defer folder rename foreign keys: {e}"))?;
|
||||
|
||||
let inserted = tx.execute(
|
||||
"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, datetime('now'), messages_updated_at
|
||||
FROM folders WHERE name = ?2",
|
||||
params![new_name, old_name],
|
||||
).map_err(|e| format!("Failed to create renamed folder cache entry: {e}"))?;
|
||||
let inserted = tx
|
||||
.execute(
|
||||
crate::queries::sqlite::queries::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(
|
||||
"UPDATE messages SET folder = ?1 WHERE folder = ?2",
|
||||
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(
|
||||
"UPDATE message_tags SET message_folder = ?1 WHERE message_folder = ?2",
|
||||
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(
|
||||
"UPDATE calendar_events SET source_message_folder = ?1 WHERE source_message_folder = ?2",
|
||||
crate::queries::sqlite::queries::db_folders::Q_022,
|
||||
params![new_name, old_name],
|
||||
)
|
||||
.ok();
|
||||
tx.execute(
|
||||
"UPDATE folders SET parent = ?1 WHERE parent = ?2",
|
||||
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("DELETE FROM folders WHERE name = ?1", params![old_name])
|
||||
.map_err(|e| format!("Failed to remove old folder cache entry: {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.commit()
|
||||
.map_err(|e| format!("Failed to commit folder rename: {e}"))?;
|
||||
Ok(())
|
||||
@@ -339,7 +321,7 @@ pub fn find_folder_by_attribute(
|
||||
attribute: &str,
|
||||
) -> Result<Option<String>, String> {
|
||||
conn.query_row(
|
||||
"SELECT name FROM folders WHERE flags LIKE '%' || ?1 || '%' LIMIT 1",
|
||||
crate::queries::sqlite::queries::db_folders::Q_025,
|
||||
[attribute],
|
||||
|row| row.get(0),
|
||||
)
|
||||
@@ -460,17 +442,14 @@ 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(
|
||||
"UPDATE folders SET messages_updated_at = '2000-01-01 00:00:00' WHERE name = 'Sent'",
|
||||
[],
|
||||
)
|
||||
.unwrap();
|
||||
conn.execute(crate::queries::sqlite::queries::db_folders::Q_026, [])
|
||||
.unwrap();
|
||||
|
||||
update_folder_sync_status(&conn, "Sent", 1, 5, 42).unwrap();
|
||||
|
||||
let timestamp: String = conn
|
||||
.query_row(
|
||||
"SELECT messages_updated_at FROM folders WHERE name = 'Sent'",
|
||||
crate::queries::sqlite::queries::db_folders::Q_027,
|
||||
[],
|
||||
|row| row.get(0),
|
||||
)
|
||||
|
||||
@@ -9,7 +9,7 @@ impl HeaderSync {
|
||||
pub(crate) fn capture(conn: &Connection) -> Result<Self, String> {
|
||||
let generation = conn
|
||||
.query_row(
|
||||
"SELECT value FROM user_meta WHERE key = 'header_sync_generation'",
|
||||
crate::queries::sqlite::queries::db_header_sync::Q_001,
|
||||
[],
|
||||
|row| row.get::<_, String>(0),
|
||||
)
|
||||
@@ -32,11 +32,7 @@ impl HeaderSync {
|
||||
}
|
||||
|
||||
pub(crate) fn invalidate(conn: &Transaction<'_>) -> Result<(), String> {
|
||||
conn.execute(
|
||||
"INSERT INTO user_meta (key, value) VALUES ('header_sync_generation', '1')
|
||||
ON CONFLICT(key) DO UPDATE SET value = CAST(value AS INTEGER) + 1",
|
||||
[],
|
||||
)
|
||||
.map_err(|e| e.to_string())?;
|
||||
conn.execute(crate::queries::sqlite::queries::db_header_sync::Q_002, [])
|
||||
.map_err(|e| e.to_string())?;
|
||||
Ok(())
|
||||
}
|
||||
+193
-61
@@ -1,4 +1,5 @@
|
||||
use rusqlite::{params, Connection};
|
||||
use crate::queries::sqlite::queries::db_identities::IDENTITY_SELECT_COLS;
|
||||
use rusqlite::{Connection, params};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
@@ -26,22 +27,37 @@ pub struct Identity {
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct CreateIdentity {
|
||||
#[serde(default)] pub display_name: String,
|
||||
#[serde(default)]
|
||||
pub display_name: String,
|
||||
pub email: String,
|
||||
#[serde(default)] pub signature_html: String,
|
||||
#[serde(default)] pub is_default: bool,
|
||||
#[serde(default)] pub reply_to: String,
|
||||
#[serde(default)] pub bcc: String,
|
||||
#[serde(default)] pub organization: String,
|
||||
#[serde(default)] pub signature_text: String,
|
||||
#[serde(default = "default_true")] pub signature_new: bool,
|
||||
#[serde(default = "default_true")] pub signature_reply: bool,
|
||||
#[serde(default = "default_true")] pub signature_forward: bool,
|
||||
#[serde(default = "default_signature_position")] pub signature_position: String,
|
||||
#[serde(default)] pub pgp_fingerprint: Option<String>,
|
||||
#[serde(default)] pub pgp_sign_default: bool,
|
||||
#[serde(default)] pub pgp_encrypt_default: bool,
|
||||
#[serde(default)] pub pgp_inline_default: bool,
|
||||
#[serde(default)]
|
||||
pub signature_html: String,
|
||||
#[serde(default)]
|
||||
pub is_default: bool,
|
||||
#[serde(default)]
|
||||
pub reply_to: String,
|
||||
#[serde(default)]
|
||||
pub bcc: String,
|
||||
#[serde(default)]
|
||||
pub organization: String,
|
||||
#[serde(default)]
|
||||
pub signature_text: String,
|
||||
#[serde(default = "default_true")]
|
||||
pub signature_new: bool,
|
||||
#[serde(default = "default_true")]
|
||||
pub signature_reply: bool,
|
||||
#[serde(default = "default_true")]
|
||||
pub signature_forward: bool,
|
||||
#[serde(default = "default_signature_position")]
|
||||
pub signature_position: String,
|
||||
#[serde(default)]
|
||||
pub pgp_fingerprint: Option<String>,
|
||||
#[serde(default)]
|
||||
pub pgp_sign_default: bool,
|
||||
#[serde(default)]
|
||||
pub pgp_encrypt_default: bool,
|
||||
#[serde(default)]
|
||||
pub pgp_inline_default: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
@@ -64,11 +80,19 @@ pub struct UpdateIdentity {
|
||||
pub pgp_inline_default: Option<bool>,
|
||||
}
|
||||
|
||||
fn default_true() -> bool { true }
|
||||
fn default_signature_position() -> String { "above".to_string() }
|
||||
fn default_true() -> bool {
|
||||
true
|
||||
}
|
||||
fn default_signature_position() -> String {
|
||||
"above".to_string()
|
||||
}
|
||||
|
||||
fn validate_signature_position(value: &str) -> Result<(), String> {
|
||||
if matches!(value, "above" | "below") { Ok(()) } else { Err(format!("Invalid signature_position: {value}")) }
|
||||
if matches!(value, "above" | "below") {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(format!("Invalid signature_position: {value}"))
|
||||
}
|
||||
}
|
||||
|
||||
fn row_to_identity(row: &rusqlite::Row<'_>) -> rusqlite::Result<Identity> {
|
||||
@@ -95,17 +119,20 @@ fn row_to_identity(row: &rusqlite::Row<'_>) -> rusqlite::Result<Identity> {
|
||||
})
|
||||
}
|
||||
|
||||
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 fn list_identities(conn: &Connection) -> Result<Vec<Identity>, String> {
|
||||
let sql = format!("SELECT {IDENTITY_SELECT_COLS} FROM identities ORDER BY is_default DESC, email ASC");
|
||||
let mut stmt = conn.prepare(&sql).map_err(|e| format!("Failed to prepare list_identities: {e}"))?;
|
||||
let rows = stmt.query_map([], row_to_identity).map_err(|e| format!("Failed to query identities: {e}"))?;
|
||||
rows.map(|row| row.map_err(|e| format!("Failed to read identity row: {e}"))).collect()
|
||||
let sql = crate::rustbox_query_db_identities_001!();
|
||||
let mut stmt = conn
|
||||
.prepare(&sql)
|
||||
.map_err(|e| format!("Failed to prepare list_identities: {e}"))?;
|
||||
let rows = stmt
|
||||
.query_map([], row_to_identity)
|
||||
.map_err(|e| format!("Failed to query identities: {e}"))?;
|
||||
rows.map(|row| row.map_err(|e| format!("Failed to read identity row: {e}")))
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn get_identity(conn: &Connection, id: i64) -> Result<Option<Identity>, String> {
|
||||
let sql = format!("SELECT {IDENTITY_SELECT_COLS} FROM identities WHERE id = ?1");
|
||||
let sql = crate::rustbox_query_db_identities_002!();
|
||||
match conn.query_row(&sql, params![id], row_to_identity) {
|
||||
Ok(identity) => Ok(Some(identity)),
|
||||
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
|
||||
@@ -115,7 +142,7 @@ pub fn get_identity(conn: &Connection, id: i64) -> Result<Option<Identity>, Stri
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn get_default_identity(conn: &Connection) -> Result<Option<Identity>, String> {
|
||||
let sql = format!("SELECT {IDENTITY_SELECT_COLS} FROM identities WHERE is_default = 1 LIMIT 1");
|
||||
let sql = crate::rustbox_query_db_identities_003!();
|
||||
match conn.query_row(&sql, [], row_to_identity) {
|
||||
Ok(identity) => Ok(Some(identity)),
|
||||
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
|
||||
@@ -125,49 +152,127 @@ pub fn get_default_identity(conn: &Connection) -> Result<Option<Identity>, Strin
|
||||
|
||||
pub fn create_identity(conn: &Connection, data: &CreateIdentity) -> Result<Identity, String> {
|
||||
validate_signature_position(&data.signature_position)?;
|
||||
if data.is_default { clear_default(conn)?; }
|
||||
if data.is_default {
|
||||
clear_default(conn)?;
|
||||
}
|
||||
conn.execute(
|
||||
"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)",
|
||||
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}"))?;
|
||||
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();
|
||||
get_identity(conn, id)?.ok_or_else(|| "Failed to read back created identity".to_string())
|
||||
}
|
||||
|
||||
pub fn update_identity(conn: &Connection, id: i64, data: &UpdateIdentity) -> Result<Option<Identity>, String> {
|
||||
if get_identity(conn, id)?.is_none() { return Ok(None); }
|
||||
if data.is_default == Some(true) { clear_default(conn)?; }
|
||||
if let Some(value) = data.signature_position.as_deref() { validate_signature_position(value)?; }
|
||||
pub fn update_identity(
|
||||
conn: &Connection,
|
||||
id: i64,
|
||||
data: &UpdateIdentity,
|
||||
) -> Result<Option<Identity>, String> {
|
||||
if get_identity(conn, id)?.is_none() {
|
||||
return Ok(None);
|
||||
}
|
||||
if data.is_default == Some(true) {
|
||||
clear_default(conn)?;
|
||||
}
|
||||
if let Some(value) = data.signature_position.as_deref() {
|
||||
validate_signature_position(value)?;
|
||||
}
|
||||
let mut sets = Vec::new();
|
||||
let mut values: Vec<Box<dyn rusqlite::types::ToSql>> = 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())); idx += 1; } }; }
|
||||
macro_rules! boolean { ($field:ident,$column:literal) => { if let Some(value)=data.$field { sets.push(format!(concat!($column," = ?{}"),idx)); values.push(Box::new(value as i32)); idx += 1; } }; }
|
||||
text!(display_name,"display_name"); text!(email,"email"); text!(signature_html,"signature_html"); boolean!(is_default,"is_default");
|
||||
text!(reply_to,"reply_to"); text!(bcc,"bcc"); text!(organization,"organization"); text!(signature_text,"signature_text");
|
||||
boolean!(signature_new,"signature_new"); boolean!(signature_reply,"signature_reply"); boolean!(signature_forward,"signature_forward"); 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())); idx += 1; }
|
||||
boolean!(pgp_sign_default,"pgp_sign_default"); boolean!(pgp_encrypt_default,"pgp_encrypt_default"); boolean!(pgp_inline_default,"pgp_inline_default");
|
||||
if sets.is_empty() { return get_identity(conn,id); }
|
||||
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()));
|
||||
idx += 1;
|
||||
}
|
||||
};
|
||||
}
|
||||
macro_rules! boolean {
|
||||
($field:ident,$column:literal) => {
|
||||
if let Some(value) = data.$field {
|
||||
sets.push(format!(concat!($column, " = ?{}"), idx));
|
||||
values.push(Box::new(value as i32));
|
||||
idx += 1;
|
||||
}
|
||||
};
|
||||
}
|
||||
text!(display_name, "display_name");
|
||||
text!(email, "email");
|
||||
text!(signature_html, "signature_html");
|
||||
boolean!(is_default, "is_default");
|
||||
text!(reply_to, "reply_to");
|
||||
text!(bcc, "bcc");
|
||||
text!(organization, "organization");
|
||||
text!(signature_text, "signature_text");
|
||||
boolean!(signature_new, "signature_new");
|
||||
boolean!(signature_reply, "signature_reply");
|
||||
boolean!(signature_forward, "signature_forward");
|
||||
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()));
|
||||
idx += 1;
|
||||
}
|
||||
boolean!(pgp_sign_default, "pgp_sign_default");
|
||||
boolean!(pgp_encrypt_default, "pgp_encrypt_default");
|
||||
boolean!(pgp_inline_default, "pgp_inline_default");
|
||||
if sets.is_empty() {
|
||||
return get_identity(conn, id);
|
||||
}
|
||||
sets.push("updated_at = datetime('now')".to_string());
|
||||
let sql = format!("UPDATE identities SET {} WHERE id = ?{}", sets.join(", "), idx);
|
||||
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()).map_err(|e| format!("Failed to update identity: {e}"))?;
|
||||
get_identity(conn,id)
|
||||
let refs: Vec<&dyn rusqlite::types::ToSql> =
|
||||
values.iter().map(|value| value.as_ref()).collect();
|
||||
conn.execute(&sql, refs.as_slice())
|
||||
.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("DELETE FROM identities WHERE id = ?1", params![id]).map_err(|e| format!("Failed to delete identity: {e}"))? > 0)
|
||||
Ok(conn
|
||||
.execute(
|
||||
crate::queries::sqlite::queries::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("SELECT COUNT(*) FROM identities", [], |row| row.get(0)).map_err(|e| format!("Failed to count identities: {e}"))?;
|
||||
let count: i64 = conn
|
||||
.query_row(
|
||||
crate::queries::sqlite::queries::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("UPDATE identities SET is_default = 0, updated_at = datetime('now') WHERE is_default = 1", []).map_err(|e| format!("Failed to clear default identity: {e}"))?;
|
||||
conn.execute(crate::queries::sqlite::queries::db_identities::Q_004, [])
|
||||
.map_err(|e| format!("Failed to clear default identity: {e}"))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -177,28 +282,55 @@ mod tests {
|
||||
use crate::db::pool::open_test_db;
|
||||
|
||||
fn sample(email: &str, is_default: bool) -> CreateIdentity {
|
||||
CreateIdentity { display_name:"Alice".into(), email:email.into(), signature_html:"<p>Best</p>".into(), is_default, reply_to:String::new(), bcc:String::new(), organization:String::new(), signature_text:"Best".into(), signature_new:true, signature_reply:true, signature_forward:true, signature_position:"above".into(), pgp_fingerprint:None, pgp_sign_default:false, pgp_encrypt_default:false, pgp_inline_default:false }
|
||||
CreateIdentity {
|
||||
display_name: "Alice".into(),
|
||||
email: email.into(),
|
||||
signature_html: "<p>Best</p>".into(),
|
||||
is_default,
|
||||
reply_to: String::new(),
|
||||
bcc: String::new(),
|
||||
organization: String::new(),
|
||||
signature_text: "Best".into(),
|
||||
signature_new: true,
|
||||
signature_reply: true,
|
||||
signature_forward: true,
|
||||
signature_position: "above".into(),
|
||||
pgp_fingerprint: None,
|
||||
pgp_sign_default: false,
|
||||
pgp_encrypt_default: false,
|
||||
pgp_inline_default: false,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_and_extended_options_roundtrip() {
|
||||
let conn=open_test_db();
|
||||
let mut value=sample("alice@example.com", true);
|
||||
value.reply_to="reply@example.com".into(); value.bcc="archive@example.com".into(); value.organization="Example".into(); value.signature_position="below".into();
|
||||
let identity=create_identity(&conn,&value).unwrap();
|
||||
assert_eq!(identity.reply_to,"reply@example.com"); assert_eq!(identity.bcc,"archive@example.com"); assert_eq!(identity.organization,"Example"); assert_eq!(identity.signature_position,"below");
|
||||
let conn = open_test_db();
|
||||
let mut value = sample("alice@example.com", true);
|
||||
value.reply_to = "reply@example.com".into();
|
||||
value.bcc = "archive@example.com".into();
|
||||
value.organization = "Example".into();
|
||||
value.signature_position = "below".into();
|
||||
let identity = create_identity(&conn, &value).unwrap();
|
||||
assert_eq!(identity.reply_to, "reply@example.com");
|
||||
assert_eq!(identity.bcc, "archive@example.com");
|
||||
assert_eq!(identity.organization, "Example");
|
||||
assert_eq!(identity.signature_position, "below");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn only_one_default() {
|
||||
let conn=open_test_db();
|
||||
let first=create_identity(&conn,&sample("a@example.com",true)).unwrap();
|
||||
let second=create_identity(&conn,&sample("b@example.com",true)).unwrap();
|
||||
assert!(second.is_default); assert!(!get_identity(&conn,first.id).unwrap().unwrap().is_default);
|
||||
let conn = open_test_db();
|
||||
let first = create_identity(&conn, &sample("a@example.com", true)).unwrap();
|
||||
let second = create_identity(&conn, &sample("b@example.com", true)).unwrap();
|
||||
assert!(second.is_default);
|
||||
assert!(!get_identity(&conn, first.id).unwrap().unwrap().is_default);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_invalid_signature_position() {
|
||||
let conn=open_test_db(); let mut value=sample("a@example.com",false); value.signature_position="side".into(); assert!(create_identity(&conn,&value).is_err());
|
||||
let conn = open_test_db();
|
||||
let mut value = sample("a@example.com", false);
|
||||
value.signature_position = "side".into();
|
||||
assert!(create_identity(&conn, &value).is_err());
|
||||
}
|
||||
}
|
||||
+70
-36
@@ -32,7 +32,7 @@ pub struct UpdateMailPreferences {
|
||||
|
||||
pub fn get_preferences(conn: &Connection) -> Result<MailPreferences, String> {
|
||||
conn.query_row(
|
||||
"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",
|
||||
crate::queries::sqlite::queries::db_mail_preferences::Q_001,
|
||||
[],
|
||||
|row| {
|
||||
Ok(MailPreferences {
|
||||
@@ -57,58 +57,92 @@ pub fn update_preferences(
|
||||
conn: &Connection,
|
||||
data: &UpdateMailPreferences,
|
||||
) -> Result<MailPreferences, String> {
|
||||
conn.execute("INSERT OR IGNORE INTO mail_preferences (id) VALUES (1)", [])
|
||||
.map_err(|error| format!("Failed to initialize mail preferences: {error}"))?;
|
||||
conn.execute(
|
||||
crate::queries::sqlite::queries::db_mail_preferences::Q_002,
|
||||
[],
|
||||
)
|
||||
.map_err(|error| format!("Failed to initialize mail preferences: {error}"))?;
|
||||
|
||||
if let Some(value) = data.messages_per_page {
|
||||
if !(20..=200).contains(&value) {
|
||||
return Err("messages_per_page must be between 20 and 200".to_string());
|
||||
}
|
||||
if let Some(value) = data.messages_per_page
|
||||
&& !(20..=200).contains(&value)
|
||||
{
|
||||
return Err("messages_per_page must be between 20 and 200".to_string());
|
||||
}
|
||||
if let Some(value) = data.mark_read_delay_ms {
|
||||
if value > 30_000 {
|
||||
return Err("mark_read_delay_ms must be between 0 and 30000".to_string());
|
||||
}
|
||||
if let Some(value) = data.mark_read_delay_ms
|
||||
&& value > 30_000
|
||||
{
|
||||
return Err("mark_read_delay_ms must be between 0 and 30000".to_string());
|
||||
}
|
||||
if let Some(ref value) = data.remote_images {
|
||||
if !matches!(value.as_str(), "never" | "contacts" | "ask" | "always") {
|
||||
return Err("remote_images must be never, contacts, ask or always".to_string());
|
||||
}
|
||||
if let Some(ref value) = data.remote_images
|
||||
&& !matches!(value.as_str(), "never" | "contacts" | "ask" | "always")
|
||||
{
|
||||
return Err("remote_images must be never, contacts, ask or always".to_string());
|
||||
}
|
||||
if let Some(ref value) = data.preferred_body {
|
||||
if !matches!(value.as_str(), "auto" | "html" | "text") {
|
||||
return Err("preferred_body must be auto, html or text".to_string());
|
||||
}
|
||||
if let Some(ref value) = data.preferred_body
|
||||
&& !matches!(value.as_str(), "auto" | "html" | "text")
|
||||
{
|
||||
return Err("preferred_body must be auto, html or text".to_string());
|
||||
}
|
||||
if let Some(ref value) = data.thread_mode {
|
||||
if !matches!(value.as_str(), "threaded" | "flat") {
|
||||
return Err("thread_mode must be threaded or flat".to_string());
|
||||
}
|
||||
if let Some(ref value) = data.thread_mode
|
||||
&& !matches!(value.as_str(), "threaded" | "flat")
|
||||
{
|
||||
return Err("thread_mode must be threaded or flat".to_string());
|
||||
}
|
||||
if let Some(ref value) = data.default_sort {
|
||||
if !matches!(
|
||||
if let Some(ref value) = data.default_sort
|
||||
&& !matches!(
|
||||
value.as_str(),
|
||||
"date_desc" | "date_asc" | "sender_asc" | "sender_desc" | "subject_asc" | "subject_desc" | "size_asc" | "size_desc"
|
||||
) {
|
||||
return Err("unsupported default_sort".to_string());
|
||||
}
|
||||
"date_desc"
|
||||
| "date_asc"
|
||||
| "sender_asc"
|
||||
| "sender_desc"
|
||||
| "subject_asc"
|
||||
| "subject_desc"
|
||||
| "size_asc"
|
||||
| "size_desc"
|
||||
)
|
||||
{
|
||||
return Err("unsupported default_sort".to_string());
|
||||
}
|
||||
|
||||
let current = get_preferences(conn)?;
|
||||
let messages_per_page = data.messages_per_page.unwrap_or(current.messages_per_page);
|
||||
let mark_read_delay_ms = data.mark_read_delay_ms.unwrap_or(current.mark_read_delay_ms);
|
||||
let remote_images = data.remote_images.as_deref().unwrap_or(¤t.remote_images);
|
||||
let preferred_body = data.preferred_body.as_deref().unwrap_or(¤t.preferred_body);
|
||||
let mark_read_delay_ms = data
|
||||
.mark_read_delay_ms
|
||||
.unwrap_or(current.mark_read_delay_ms);
|
||||
let remote_images = data
|
||||
.remote_images
|
||||
.as_deref()
|
||||
.unwrap_or(¤t.remote_images);
|
||||
let preferred_body = data
|
||||
.preferred_body
|
||||
.as_deref()
|
||||
.unwrap_or(¤t.preferred_body);
|
||||
let thread_mode = data.thread_mode.as_deref().unwrap_or(¤t.thread_mode);
|
||||
let collapse_quotes = data.collapse_quotes.unwrap_or(current.collapse_quotes);
|
||||
let show_snippets = data.show_snippets.unwrap_or(current.show_snippets);
|
||||
let default_sort = data.default_sort.as_deref().unwrap_or(¤t.default_sort);
|
||||
let default_sort = data
|
||||
.default_sort
|
||||
.as_deref()
|
||||
.unwrap_or(¤t.default_sort);
|
||||
let flagged_on_top = data.flagged_on_top.unwrap_or(current.flagged_on_top);
|
||||
let sync_imap_keywords = data.sync_imap_keywords.unwrap_or(current.sync_imap_keywords);
|
||||
let sync_imap_keywords = data
|
||||
.sync_imap_keywords
|
||||
.unwrap_or(current.sync_imap_keywords);
|
||||
|
||||
conn.execute(
|
||||
"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 = datetime('now') WHERE id = 1",
|
||||
rusqlite::params![messages_per_page, mark_read_delay_ms, remote_images, preferred_body, thread_mode, collapse_quotes as i32, show_snippets as i32, default_sort, flagged_on_top as i32, sync_imap_keywords as i32],
|
||||
crate::queries::sqlite::queries::db_mail_preferences::Q_003,
|
||||
rusqlite::params![
|
||||
messages_per_page,
|
||||
mark_read_delay_ms,
|
||||
remote_images,
|
||||
preferred_body,
|
||||
thread_mode,
|
||||
collapse_quotes as i32,
|
||||
show_snippets as i32,
|
||||
default_sort,
|
||||
flagged_on_top as i32,
|
||||
sync_imap_keywords as i32
|
||||
],
|
||||
)
|
||||
.map_err(|error| format!("Failed to update mail preferences: {error}"))?;
|
||||
|
||||
|
||||
+38
-78
@@ -1,3 +1,4 @@
|
||||
use crate::queries::sqlite::queries::db_messages::MSG_SELECT_COLS;
|
||||
use rusqlite::{Connection, params};
|
||||
use serde::Serialize;
|
||||
|
||||
@@ -35,7 +36,7 @@ pub struct CachedMessage {
|
||||
}
|
||||
|
||||
fn row_to_cached_message(row: &rusqlite::Row<'_>) -> rusqlite::Result<CachedMessage> {
|
||||
let has_attachments_int: i32 = row.get(12)?;
|
||||
let has_attachments_int: i32 = row.get(13)?;
|
||||
Ok(CachedMessage {
|
||||
uid: row.get(0)?,
|
||||
folder: row.get(1)?,
|
||||
@@ -49,17 +50,13 @@ fn row_to_cached_message(row: &rusqlite::Row<'_>) -> rusqlite::Result<CachedMess
|
||||
cc_addresses: row.get(9)?,
|
||||
date: row.get(10)?,
|
||||
flags: row.get(11)?,
|
||||
size: row.get(13)?,
|
||||
size: row.get(12)?,
|
||||
has_attachments: has_attachments_int != 0,
|
||||
snippet: row.get(14)?,
|
||||
reaction: row.get(15)?,
|
||||
})
|
||||
}
|
||||
|
||||
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, has_attachments, size, snippet, reaction";
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn upsert_message(
|
||||
conn: &Connection,
|
||||
@@ -82,11 +79,7 @@ pub fn upsert_message(
|
||||
) -> Result<(), String> {
|
||||
let date_epoch = parse_date_to_epoch(date);
|
||||
conn.execute(
|
||||
"INSERT OR REPLACE 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)",
|
||||
crate::queries::sqlite::queries::db_messages::Q_001,
|
||||
params![
|
||||
uid,
|
||||
folder,
|
||||
@@ -123,14 +116,17 @@ pub fn remove_after_mutation(conn: &Connection, folder: &str, uid: u32) -> Resul
|
||||
|
||||
pub fn delete_folder_messages(conn: &Connection, folder: &str) -> Result<usize, String> {
|
||||
let deleted = conn
|
||||
.execute("DELETE FROM messages WHERE folder = ?1", params![folder])
|
||||
.execute(
|
||||
crate::queries::sqlite::queries::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(
|
||||
"SELECT COALESCE(MAX(uid), 0) FROM messages WHERE folder = ?1",
|
||||
crate::queries::sqlite::queries::db_messages::Q_003,
|
||||
params![folder],
|
||||
|row| row.get(0),
|
||||
)
|
||||
@@ -139,11 +135,7 @@ pub fn max_uid(conn: &Connection, folder: &str) -> Result<u32, String> {
|
||||
|
||||
pub fn list_index_source_messages(conn: &Connection) -> Result<Vec<IndexSourceMessage>, String> {
|
||||
let mut stmt = conn
|
||||
.prepare(
|
||||
"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",
|
||||
)
|
||||
.prepare(crate::queries::sqlite::queries::db_messages::Q_004)
|
||||
.map_err(|e| format!("Failed to prepare index rebuild query: {e}"))?;
|
||||
|
||||
let rows = stmt
|
||||
@@ -261,13 +253,7 @@ pub fn get_messages(
|
||||
per_page: u32,
|
||||
) -> Result<Vec<CachedMessage>, String> {
|
||||
let offset = page * per_page;
|
||||
let sql = format!(
|
||||
"SELECT {MSG_SELECT_COLS}
|
||||
FROM messages
|
||||
WHERE folder = ?1
|
||||
ORDER BY date_epoch DESC
|
||||
LIMIT ?2 OFFSET ?3"
|
||||
);
|
||||
let sql = crate::rustbox_query_db_messages_001!();
|
||||
|
||||
let mut stmt = conn
|
||||
.prepare(&sql)
|
||||
@@ -318,13 +304,7 @@ pub fn get_messages_sorted(
|
||||
};
|
||||
|
||||
let offset = page * per_page;
|
||||
let sql = format!(
|
||||
"SELECT {MSG_SELECT_COLS}
|
||||
FROM messages
|
||||
WHERE folder = ?1
|
||||
ORDER BY {order_by}
|
||||
LIMIT ?2 OFFSET ?3"
|
||||
);
|
||||
let sql = crate::rustbox_query_db_messages_002!(&order_by);
|
||||
let mut stmt = conn
|
||||
.prepare(&sql)
|
||||
.map_err(|e| format!("Failed to prepare sorted get_messages: {e}"))?;
|
||||
@@ -337,7 +317,7 @@ pub fn get_messages_sorted(
|
||||
|
||||
pub fn count_messages(conn: &Connection, folder: &str) -> Result<u32, String> {
|
||||
conn.query_row(
|
||||
"SELECT COUNT(*) FROM messages WHERE folder = ?1",
|
||||
crate::queries::sqlite::queries::db_messages::Q_005,
|
||||
params![folder],
|
||||
|row| row.get(0),
|
||||
)
|
||||
@@ -346,7 +326,11 @@ pub fn count_messages(conn: &Connection, folder: &str) -> Result<u32, String> {
|
||||
|
||||
pub fn count_all_messages(conn: &Connection) -> Result<usize, String> {
|
||||
let count: i64 = conn
|
||||
.query_row("SELECT COUNT(*) FROM messages", [], |row| row.get(0))
|
||||
.query_row(
|
||||
crate::queries::sqlite::queries::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())
|
||||
}
|
||||
@@ -358,7 +342,7 @@ pub fn update_message_flags(
|
||||
flags_csv: &str,
|
||||
) -> Result<(), String> {
|
||||
conn.execute(
|
||||
"UPDATE messages SET flags = ?1 WHERE folder = ?2 AND uid = ?3",
|
||||
crate::queries::sqlite::queries::db_messages::Q_007,
|
||||
params![flags_csv, folder, uid],
|
||||
)
|
||||
.map_err(|e| format!("Failed to update message flags: {e}"))?;
|
||||
@@ -377,10 +361,7 @@ pub fn cache_message_body(
|
||||
email_theme: Option<i32>,
|
||||
) -> Result<(), String> {
|
||||
conn.execute(
|
||||
"UPDATE messages
|
||||
SET body_html = ?1, body_text = ?2, body_cached = 1,
|
||||
attachments_json = ?3, raw_headers = ?4, email_theme = ?5
|
||||
WHERE folder = ?6 AND uid = ?7",
|
||||
crate::queries::sqlite::queries::db_messages::Q_008,
|
||||
params![
|
||||
html,
|
||||
text,
|
||||
@@ -402,7 +383,7 @@ pub fn update_email_theme(
|
||||
email_theme: i32,
|
||||
) -> Result<(), String> {
|
||||
conn.execute(
|
||||
"UPDATE messages SET email_theme = ?1 WHERE folder = ?2 AND uid = ?3",
|
||||
crate::queries::sqlite::queries::db_messages::Q_009,
|
||||
params![email_theme, folder, uid],
|
||||
)
|
||||
.map_err(|e| format!("Failed to update email theme: {e}"))?;
|
||||
@@ -423,9 +404,7 @@ pub fn get_cached_body(
|
||||
uid: u32,
|
||||
) -> Result<Option<CachedBody>, String> {
|
||||
let result = conn.query_row(
|
||||
"SELECT body_cached, body_html, body_text, attachments_json, raw_headers, email_theme
|
||||
FROM messages
|
||||
WHERE folder = ?1 AND uid = ?2",
|
||||
crate::queries::sqlite::queries::db_messages::Q_010,
|
||||
params![folder, uid],
|
||||
|row| {
|
||||
let cached: i32 = row.get(0)?;
|
||||
@@ -455,13 +434,9 @@ pub fn get_single_message(
|
||||
folder: &str,
|
||||
uid: u32,
|
||||
) -> Result<Option<CachedMessage>, String> {
|
||||
let sql = format!(
|
||||
"SELECT {MSG_SELECT_COLS}
|
||||
FROM messages
|
||||
WHERE folder = ?1 AND uid = ?2"
|
||||
);
|
||||
let sql = crate::rustbox_query_db_messages_003!();
|
||||
|
||||
let result = conn.query_row(&sql, params![folder, uid], |row| row_to_cached_message(row));
|
||||
let result = conn.query_row(&sql, params![folder, uid], row_to_cached_message);
|
||||
|
||||
match result {
|
||||
Ok(msg) => Ok(Some(msg)),
|
||||
@@ -472,7 +447,7 @@ pub fn get_single_message(
|
||||
|
||||
pub fn delete_message(conn: &Connection, folder: &str, uid: u32) -> Result<(), String> {
|
||||
conn.execute(
|
||||
"DELETE FROM messages WHERE folder = ?1 AND uid = ?2",
|
||||
crate::queries::sqlite::queries::db_messages::Q_011,
|
||||
params![folder, uid],
|
||||
)
|
||||
.map_err(|e| format!("Failed to delete message: {e}"))?;
|
||||
@@ -481,12 +456,7 @@ pub fn delete_message(conn: &Connection, folder: &str, uid: u32) -> Result<(), S
|
||||
|
||||
pub fn get_unindexed_messages(conn: &Connection, limit: u32) -> Result<Vec<(String, u32)>, String> {
|
||||
let mut stmt = conn
|
||||
.prepare(
|
||||
"SELECT folder, uid FROM messages
|
||||
WHERE body_cached = 0
|
||||
ORDER BY date_epoch DESC
|
||||
LIMIT ?1",
|
||||
)
|
||||
.prepare(crate::queries::sqlite::queries::db_messages::Q_012)
|
||||
.map_err(|e| format!("Failed to prepare get_unindexed_messages: {e}"))?;
|
||||
|
||||
let rows = stmt
|
||||
@@ -507,7 +477,7 @@ pub fn get_all_uids_and_flags(
|
||||
folder: &str,
|
||||
) -> Result<Vec<(u32, String)>, String> {
|
||||
let mut stmt = conn
|
||||
.prepare("SELECT uid, flags FROM messages WHERE folder = ?1")
|
||||
.prepare(crate::queries::sqlite::queries::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| {
|
||||
@@ -528,14 +498,7 @@ pub fn get_thread_messages(
|
||||
) -> Result<Vec<CachedMessage>, String> {
|
||||
let escaped_target = target_message_id.replace("%", "\\%").replace("_", "\\_");
|
||||
let like_pattern = format!("%{}%", escaped_target);
|
||||
let sql = format!(
|
||||
"SELECT {MSG_SELECT_COLS}
|
||||
FROM messages
|
||||
WHERE message_id = ?1
|
||||
OR in_reply_to = ?1
|
||||
OR references_header LIKE ?2 ESCAPE '\\'
|
||||
ORDER BY date_epoch ASC"
|
||||
);
|
||||
let sql = crate::rustbox_query_db_messages_004!();
|
||||
|
||||
let mut stmt = conn
|
||||
.prepare(&sql)
|
||||
@@ -732,14 +695,7 @@ pub fn search_messages_sqlite(
|
||||
format!("WHERE {}", conditions.join(" AND "))
|
||||
};
|
||||
|
||||
let sql = format!(
|
||||
"SELECT {MSG_SELECT_COLS}
|
||||
FROM messages
|
||||
{where_clause}
|
||||
ORDER BY date_epoch DESC
|
||||
LIMIT ?{}",
|
||||
param_values.len() + 1
|
||||
);
|
||||
let sql = crate::rustbox_query_db_messages_005!(&where_clause, param_values.len() + 1);
|
||||
|
||||
param_values.push(Box::new(limit as i64));
|
||||
|
||||
@@ -763,6 +719,15 @@ pub fn search_messages_sqlite(
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -1314,8 +1279,3 @@ mod tests {
|
||||
assert_eq!(thread[0].uid, 1);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cached_storage_bytes(conn: &Connection) -> Result<Option<u64>, String> {
|
||||
conn.query_row("SELECT SUM(size) FROM messages", [], |row| row.get(0))
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
+2
-2
@@ -3,16 +3,16 @@ pub mod contact_groups;
|
||||
pub mod contacts;
|
||||
pub mod display_preferences;
|
||||
pub mod drafts;
|
||||
pub mod folders;
|
||||
pub mod folder_preferences;
|
||||
pub mod folders;
|
||||
pub(crate) mod header_sync;
|
||||
pub mod identities;
|
||||
pub mod mail_preferences;
|
||||
pub mod messages;
|
||||
pub mod notification_preferences;
|
||||
pub mod pgp;
|
||||
pub mod response_templates;
|
||||
pub mod pool;
|
||||
pub mod response_templates;
|
||||
pub mod special_folders;
|
||||
pub mod tags;
|
||||
pub mod trusted_senders;
|
||||
|
||||
@@ -24,7 +24,7 @@ pub struct UpdateNotificationPreferences {
|
||||
|
||||
pub fn get_preferences(conn: &Connection) -> Result<NotificationPreferences, String> {
|
||||
let result = conn.query_row(
|
||||
"SELECT enabled, sound, folders, dnd_enabled, dnd_start, dnd_end, updated_at FROM notification_preferences WHERE id = 1",
|
||||
crate::queries::sqlite::queries::db_notification_preferences::Q_001,
|
||||
[],
|
||||
|row| {
|
||||
let enabled_int: i32 = row.get(0)?;
|
||||
@@ -34,7 +34,15 @@ pub fn get_preferences(conn: &Connection) -> Result<NotificationPreferences, Str
|
||||
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))
|
||||
Ok((
|
||||
enabled_int,
|
||||
sound_int,
|
||||
folders_json,
|
||||
dnd_enabled_int,
|
||||
dnd_start,
|
||||
dnd_end,
|
||||
updated_at,
|
||||
))
|
||||
},
|
||||
);
|
||||
|
||||
@@ -78,7 +86,7 @@ pub fn update_preferences(
|
||||
data: &UpdateNotificationPreferences,
|
||||
) -> Result<NotificationPreferences, String> {
|
||||
conn.execute(
|
||||
"INSERT OR IGNORE INTO notification_preferences (id) VALUES (1)",
|
||||
crate::queries::sqlite::queries::db_notification_preferences::Q_002,
|
||||
[],
|
||||
)
|
||||
.map_err(|e| format!("Failed to ensure preferences row: {e}"))?;
|
||||
@@ -132,7 +140,7 @@ pub fn update_preferences(
|
||||
|
||||
sets.push("updated_at = datetime('now')".to_string());
|
||||
let set_clause = sets.join(", ");
|
||||
let sql = format!("UPDATE notification_preferences SET {set_clause} WHERE id = ?{idx}");
|
||||
let sql = crate::rustbox_query_db_notification_preferences_001!(&set_clause, idx);
|
||||
values.push(Box::new(1_i32));
|
||||
|
||||
let params_refs: Vec<&dyn rusqlite::types::ToSql> = values.iter().map(|v| v.as_ref()).collect();
|
||||
|
||||
+48
-32
@@ -1,4 +1,5 @@
|
||||
use rusqlite::{params, Connection};
|
||||
use crate::queries::sqlite::queries::db_pgp::KEY_COLS;
|
||||
use rusqlite::{Connection, params};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
@@ -48,22 +49,23 @@ fn row_to_key(row: &rusqlite::Row<'_>) -> rusqlite::Result<PgpKeyInternal> {
|
||||
})
|
||||
}
|
||||
|
||||
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 fn list_keys(conn: &Connection) -> Result<Vec<PgpKeyRecord>, String> {
|
||||
let mut stmt = conn
|
||||
.prepare(&format!("SELECT {KEY_COLS} FROM pgp_keys ORDER BY has_secret DESC, lower(email), fingerprint"))
|
||||
.prepare(&crate::rustbox_query_db_pgp_001!())
|
||||
.map_err(|e| format!("Failed to prepare PGP key list: {e}"))?;
|
||||
let rows = stmt
|
||||
.query_map([], row_to_key)
|
||||
.map_err(|e| format!("Failed to list PGP keys: {e}"))?;
|
||||
rows.map(|row| row.map(|v| v.record).map_err(|e| format!("Failed to read PGP key: {e}")))
|
||||
.collect()
|
||||
rows.map(|row| {
|
||||
row.map(|v| v.record)
|
||||
.map_err(|e| format!("Failed to read PGP key: {e}"))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn get_key(conn: &Connection, fingerprint: &str) -> Result<Option<PgpKeyInternal>, String> {
|
||||
let normalized = normalize_fingerprint(fingerprint);
|
||||
let sql = format!("SELECT {KEY_COLS} FROM pgp_keys WHERE replace(upper(fingerprint),' ','')=?1");
|
||||
let sql = crate::rustbox_query_db_pgp_002!();
|
||||
match conn.query_row(&sql, params![normalized], row_to_key) {
|
||||
Ok(v) => Ok(Some(v)),
|
||||
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
|
||||
@@ -73,13 +75,16 @@ pub fn get_key(conn: &Connection, fingerprint: &str) -> Result<Option<PgpKeyInte
|
||||
|
||||
pub fn find_keys_by_email(conn: &Connection, email: &str) -> Result<Vec<PgpKeyRecord>, String> {
|
||||
let mut stmt = conn
|
||||
.prepare(&format!("SELECT {KEY_COLS} FROM pgp_keys WHERE lower(email)=lower(?1) ORDER BY CASE trust WHEN 'verified' THEN 0 WHEN 'trusted' THEN 1 WHEN 'unknown' THEN 2 ELSE 3 END, has_secret DESC"))
|
||||
.prepare(&crate::rustbox_query_db_pgp_003!())
|
||||
.map_err(|e| format!("Failed to prepare PGP email lookup: {e}"))?;
|
||||
let rows = stmt
|
||||
.query_map(params![email.trim()], row_to_key)
|
||||
.map_err(|e| format!("Failed to query PGP keys by email: {e}"))?;
|
||||
rows.map(|row| row.map(|v| v.record).map_err(|e| format!("Failed to read PGP key: {e}")))
|
||||
.collect()
|
||||
rows.map(|row| {
|
||||
row.map(|v| v.record)
|
||||
.map_err(|e| format!("Failed to read PGP key: {e}"))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn upsert_key(
|
||||
@@ -88,7 +93,7 @@ pub fn upsert_key(
|
||||
secret_key_path: Option<&str>,
|
||||
) -> Result<(), String> {
|
||||
conn.execute(
|
||||
"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=datetime('now')",
|
||||
crate::queries::sqlite::queries::db_pgp::Q_001,
|
||||
params![
|
||||
record.fingerprint,
|
||||
record.key_id,
|
||||
@@ -108,7 +113,7 @@ pub fn upsert_key(
|
||||
pub fn delete_key(conn: &Connection, fingerprint: &str) -> Result<bool, String> {
|
||||
Ok(conn
|
||||
.execute(
|
||||
"DELETE FROM pgp_keys WHERE replace(upper(fingerprint),' ','')=?1",
|
||||
crate::queries::sqlite::queries::db_pgp::Q_002,
|
||||
params![normalize_fingerprint(fingerprint)],
|
||||
)
|
||||
.map_err(|e| format!("Failed to delete PGP key: {e}"))?
|
||||
@@ -121,7 +126,7 @@ pub fn set_trust(conn: &Connection, fingerprint: &str, trust: &str) -> Result<bo
|
||||
}
|
||||
Ok(conn
|
||||
.execute(
|
||||
"UPDATE pgp_keys SET trust=?2,updated_at=datetime('now') WHERE replace(upper(fingerprint),' ','')=?1",
|
||||
crate::queries::sqlite::queries::db_pgp::Q_003,
|
||||
params![normalize_fingerprint(fingerprint), trust],
|
||||
)
|
||||
.map_err(|e| format!("Failed to update PGP trust: {e}"))?
|
||||
@@ -129,35 +134,45 @@ pub fn set_trust(conn: &Connection, fingerprint: &str, trust: &str) -> Result<bo
|
||||
}
|
||||
|
||||
pub fn get_preferences(conn: &Connection) -> Result<PgpPreferences, String> {
|
||||
conn.query_row(
|
||||
"SELECT wkd_enabled,hkp_enabled,require_trusted_encryption FROM pgp_preferences WHERE id=1",
|
||||
[],
|
||||
|row| {
|
||||
Ok(PgpPreferences {
|
||||
wkd_enabled: row.get::<_, i32>(0)? != 0,
|
||||
hkp_enabled: row.get::<_, i32>(1)? != 0,
|
||||
require_trusted_encryption: row.get::<_, i32>(2)? != 0,
|
||||
})
|
||||
},
|
||||
)
|
||||
conn.query_row(crate::queries::sqlite::queries::db_pgp::Q_004, [], |row| {
|
||||
Ok(PgpPreferences {
|
||||
wkd_enabled: row.get::<_, i32>(0)? != 0,
|
||||
hkp_enabled: row.get::<_, i32>(1)? != 0,
|
||||
require_trusted_encryption: row.get::<_, i32>(2)? != 0,
|
||||
})
|
||||
})
|
||||
.map_err(|e| format!("Failed to read PGP preferences: {e}"))
|
||||
}
|
||||
|
||||
pub fn update_preferences(conn: &Connection, prefs: &PgpPreferences) -> Result<PgpPreferences, String> {
|
||||
pub fn update_preferences(
|
||||
conn: &Connection,
|
||||
prefs: &PgpPreferences,
|
||||
) -> Result<PgpPreferences, String> {
|
||||
conn.execute(
|
||||
"UPDATE pgp_preferences SET wkd_enabled=?1,hkp_enabled=?2,require_trusted_encryption=?3,updated_at=datetime('now') WHERE id=1",
|
||||
params![prefs.wkd_enabled as i32, prefs.hkp_enabled as i32, prefs.require_trusted_encryption as i32],
|
||||
crate::queries::sqlite::queries::db_pgp::Q_005,
|
||||
params![
|
||||
prefs.wkd_enabled as i32,
|
||||
prefs.hkp_enabled as i32,
|
||||
prefs.require_trusted_encryption as i32
|
||||
],
|
||||
)
|
||||
.map_err(|e| format!("Failed to update PGP preferences: {e}"))?;
|
||||
get_preferences(conn)
|
||||
}
|
||||
|
||||
pub fn normalize_fingerprint(value: &str) -> String {
|
||||
value.chars().filter(|c| !c.is_ascii_whitespace()).flat_map(char::to_uppercase).collect()
|
||||
value
|
||||
.chars()
|
||||
.filter(|c| !c.is_ascii_whitespace())
|
||||
.flat_map(char::to_uppercase)
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn find_key_internal_by_email(conn: &Connection, email: &str) -> Result<Option<PgpKeyInternal>, String> {
|
||||
let sql = format!("SELECT {KEY_COLS} FROM pgp_keys WHERE lower(email)=lower(?1) ORDER BY CASE trust WHEN 'verified' THEN 0 WHEN 'trusted' THEN 1 WHEN 'unknown' THEN 2 ELSE 3 END, has_secret DESC LIMIT 1");
|
||||
pub fn find_key_internal_by_email(
|
||||
conn: &Connection,
|
||||
email: &str,
|
||||
) -> Result<Option<PgpKeyInternal>, String> {
|
||||
let sql = crate::rustbox_query_db_pgp_004!();
|
||||
match conn.query_row(&sql, params![email.trim()], row_to_key) {
|
||||
Ok(value) => Ok(Some(value)),
|
||||
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
|
||||
@@ -167,10 +182,11 @@ pub fn find_key_internal_by_email(conn: &Connection, email: &str) -> Result<Opti
|
||||
|
||||
pub fn list_secret_keys(conn: &Connection) -> Result<Vec<PgpKeyInternal>, String> {
|
||||
let mut stmt = conn
|
||||
.prepare(&format!("SELECT {KEY_COLS} FROM pgp_keys WHERE has_secret=1 AND secret_key_path IS NOT NULL ORDER BY lower(email), fingerprint"))
|
||||
.prepare(&crate::rustbox_query_db_pgp_005!())
|
||||
.map_err(|e| format!("Failed to prepare PGP secret key list: {e}"))?;
|
||||
let rows = stmt
|
||||
.query_map([], row_to_key)
|
||||
.map_err(|e| format!("Failed to list PGP secret keys: {e}"))?;
|
||||
rows.map(|row| row.map_err(|e| format!("Failed to read PGP secret key: {e}"))).collect()
|
||||
rows.map(|row| row.map_err(|e| format!("Failed to read PGP secret key: {e}")))
|
||||
.collect()
|
||||
}
|
||||
+79
-71
@@ -32,198 +32,204 @@ impl UserDb {
|
||||
}
|
||||
|
||||
const MIGRATIONS: &[(u32, &str)] = &[
|
||||
(1, include_str!("../../migrations/V001__initial_schema.sql")),
|
||||
(
|
||||
1,
|
||||
include_str!("../../migrations/sqlite/V001__initial_schema.sql"),
|
||||
),
|
||||
(
|
||||
2,
|
||||
include_str!("../../migrations/V002__folders_and_messages.sql"),
|
||||
include_str!("../../migrations/sqlite/V002__folders_and_messages.sql"),
|
||||
),
|
||||
(
|
||||
3,
|
||||
include_str!("../../migrations/sqlite/V003__add_date_epoch.sql"),
|
||||
),
|
||||
(3, include_str!("../../migrations/V003__add_date_epoch.sql")),
|
||||
(
|
||||
4,
|
||||
include_str!("../../migrations/V004__folder_messages_updated_at.sql"),
|
||||
include_str!("../../migrations/sqlite/V004__folder_messages_updated_at.sql"),
|
||||
),
|
||||
(
|
||||
5,
|
||||
include_str!("../../migrations/V005__drafts_and_attachments.sql"),
|
||||
include_str!("../../migrations/sqlite/V005__drafts_and_attachments.sql"),
|
||||
),
|
||||
(
|
||||
6,
|
||||
include_str!("../../migrations/V006__cache_attachments_and_headers.sql"),
|
||||
include_str!("../../migrations/sqlite/V006__cache_attachments_and_headers.sql"),
|
||||
),
|
||||
(
|
||||
7,
|
||||
include_str!("../../migrations/sqlite/V007__contacts.sql"),
|
||||
),
|
||||
(
|
||||
8,
|
||||
include_str!("../../migrations/sqlite/V008__identities.sql"),
|
||||
),
|
||||
(7, include_str!("../../migrations/V007__contacts.sql")),
|
||||
(8, include_str!("../../migrations/V008__identities.sql")),
|
||||
(
|
||||
9,
|
||||
include_str!("../../migrations/V009__notification_preferences.sql"),
|
||||
include_str!("../../migrations/sqlite/V009__notification_preferences.sql"),
|
||||
),
|
||||
(
|
||||
10,
|
||||
include_str!("../../migrations/V010__contact_groups.sql"),
|
||||
include_str!("../../migrations/sqlite/V010__contact_groups.sql"),
|
||||
),
|
||||
(
|
||||
11,
|
||||
include_str!("../../migrations/V011__message_reaction.sql"),
|
||||
include_str!("../../migrations/sqlite/V011__message_reaction.sql"),
|
||||
),
|
||||
(12, include_str!("../../migrations/V012__tags.sql")),
|
||||
(12, include_str!("../../migrations/sqlite/V012__tags.sql")),
|
||||
(
|
||||
13,
|
||||
include_str!("../../migrations/V013__display_preferences.sql"),
|
||||
include_str!("../../migrations/sqlite/V013__display_preferences.sql"),
|
||||
),
|
||||
(
|
||||
14,
|
||||
include_str!("../../migrations/V014__compose_format.sql"),
|
||||
include_str!("../../migrations/sqlite/V014__compose_format.sql"),
|
||||
),
|
||||
(
|
||||
15,
|
||||
include_str!("../../migrations/sqlite/V015__deep_index.sql"),
|
||||
),
|
||||
(
|
||||
16,
|
||||
include_str!("../../migrations/sqlite/V016__calendar.sql"),
|
||||
),
|
||||
(15, include_str!("../../migrations/V015__deep_index.sql")),
|
||||
(16, include_str!("../../migrations/V016__calendar.sql")),
|
||||
(
|
||||
17,
|
||||
include_str!("../../migrations/V017__add_email_theme.sql"),
|
||||
include_str!("../../migrations/sqlite/V017__add_email_theme.sql"),
|
||||
),
|
||||
(
|
||||
18,
|
||||
include_str!("../../migrations/V018__animation_mode.sql"),
|
||||
include_str!("../../migrations/sqlite/V018__animation_mode.sql"),
|
||||
),
|
||||
(
|
||||
19,
|
||||
include_str!("../../migrations/V019__known_addresses.sql"),
|
||||
include_str!("../../migrations/sqlite/V019__known_addresses.sql"),
|
||||
),
|
||||
(
|
||||
20,
|
||||
include_str!("../../migrations/V020__appearance_personalization.sql"),
|
||||
include_str!("../../migrations/sqlite/V020__appearance_personalization.sql"),
|
||||
),
|
||||
(
|
||||
21,
|
||||
include_str!("../../migrations/V021__special_folders.sql"),
|
||||
include_str!("../../migrations/sqlite/V021__special_folders.sql"),
|
||||
),
|
||||
(
|
||||
22,
|
||||
include_str!("../../migrations/V022__identity_options.sql"),
|
||||
include_str!("../../migrations/sqlite/V022__identity_options.sql"),
|
||||
),
|
||||
(
|
||||
23,
|
||||
include_str!("../../migrations/V023__saved_searches.sql"),
|
||||
include_str!("../../migrations/sqlite/V023__saved_searches.sql"),
|
||||
),
|
||||
(
|
||||
24,
|
||||
include_str!("../../migrations/V024__notification_dnd.sql"),
|
||||
include_str!("../../migrations/sqlite/V024__notification_dnd.sql"),
|
||||
),
|
||||
(
|
||||
25,
|
||||
include_str!("../../migrations/V025__custom_theme_colors.sql"),
|
||||
include_str!("../../migrations/sqlite/V025__custom_theme_colors.sql"),
|
||||
),
|
||||
(
|
||||
26,
|
||||
include_str!("../../migrations/V026__mail_preferences.sql"),
|
||||
include_str!("../../migrations/sqlite/V026__mail_preferences.sql"),
|
||||
),
|
||||
(
|
||||
27,
|
||||
include_str!("../../migrations/V027__contact_profile.sql"),
|
||||
include_str!("../../migrations/sqlite/V027__contact_profile.sql"),
|
||||
),
|
||||
(
|
||||
28,
|
||||
include_str!("../../migrations/V028__contact_multi_values.sql"),
|
||||
include_str!("../../migrations/sqlite/V028__contact_multi_values.sql"),
|
||||
),
|
||||
(
|
||||
29,
|
||||
include_str!("../../migrations/V029__draft_send_options.sql"),
|
||||
include_str!("../../migrations/sqlite/V029__draft_send_options.sql"),
|
||||
),
|
||||
(
|
||||
30,
|
||||
include_str!("../../migrations/V030__folder_list_preferences.sql"),
|
||||
include_str!("../../migrations/sqlite/V030__folder_list_preferences.sql"),
|
||||
),
|
||||
(
|
||||
31,
|
||||
include_str!("../../migrations/V031__compose_preferences.sql"),
|
||||
include_str!("../../migrations/sqlite/V031__compose_preferences.sql"),
|
||||
),
|
||||
(
|
||||
32,
|
||||
include_str!("../../migrations/V032__advanced_appearance.sql"),
|
||||
include_str!("../../migrations/sqlite/V032__advanced_appearance.sql"),
|
||||
),
|
||||
(
|
||||
33,
|
||||
include_str!("../../migrations/V033__draft_compose_format.sql"),
|
||||
include_str!("../../migrations/sqlite/V033__draft_compose_format.sql"),
|
||||
),
|
||||
(
|
||||
34,
|
||||
include_str!("../../migrations/V034__imap_keyword_sync.sql"),
|
||||
include_str!("../../migrations/sqlite/V034__imap_keyword_sync.sql"),
|
||||
),
|
||||
(
|
||||
35,
|
||||
include_str!("../../migrations/V035__compose_behavior_preferences.sql"),
|
||||
include_str!("../../migrations/sqlite/V035__compose_behavior_preferences.sql"),
|
||||
),
|
||||
(
|
||||
36,
|
||||
include_str!("../../migrations/V036__remote_draft_tracking.sql"),
|
||||
include_str!("../../migrations/sqlite/V036__remote_draft_tracking.sql"),
|
||||
),
|
||||
(
|
||||
37,
|
||||
include_str!("../../migrations/V037__openpgp_keyring.sql"),
|
||||
include_str!("../../migrations/sqlite/V037__openpgp_keyring.sql"),
|
||||
),
|
||||
(
|
||||
38,
|
||||
include_str!("../../migrations/V038__identity_openpgp.sql"),
|
||||
include_str!("../../migrations/sqlite/V038__identity_openpgp.sql"),
|
||||
),
|
||||
(
|
||||
39,
|
||||
include_str!("../../migrations/V039__draft_openpgp_options.sql"),
|
||||
include_str!("../../migrations/sqlite/V039__draft_openpgp_options.sql"),
|
||||
),
|
||||
(
|
||||
40,
|
||||
include_str!("../../migrations/V040__draft_openpgp_attach_key.sql"),
|
||||
include_str!("../../migrations/sqlite/V040__draft_openpgp_attach_key.sql"),
|
||||
),
|
||||
(
|
||||
41,
|
||||
include_str!("../../migrations/V041__draft_openpgp_recipient_keys.sql"),
|
||||
include_str!("../../migrations/sqlite/V041__draft_openpgp_recipient_keys.sql"),
|
||||
),
|
||||
(
|
||||
42,
|
||||
include_str!("../../migrations/V042__trusted_senders.sql"),
|
||||
include_str!("../../migrations/sqlite/V042__trusted_senders.sql"),
|
||||
),
|
||||
(
|
||||
43,
|
||||
include_str!("../../migrations/V043__response_templates.sql"),
|
||||
include_str!("../../migrations/sqlite/V043__response_templates.sql"),
|
||||
),
|
||||
(
|
||||
44,
|
||||
include_str!("../../migrations/V044__interface_skin.sql"),
|
||||
include_str!("../../migrations/sqlite/V044__interface_skin.sql"),
|
||||
),
|
||||
(
|
||||
45,
|
||||
include_str!("../../migrations/V045__response_template_format.sql"),
|
||||
include_str!("../../migrations/sqlite/V045__response_template_format.sql"),
|
||||
),
|
||||
(
|
||||
46,
|
||||
include_str!("../../migrations/V046__preset_scale_unlock.sql"),
|
||||
include_str!("../../migrations/sqlite/V046__preset_scale_unlock.sql"),
|
||||
),
|
||||
];
|
||||
|
||||
fn run_migrations(conn: &Connection) -> Result<(), String> {
|
||||
let mut current: u32 = conn
|
||||
.query_row("PRAGMA user_version", [], |row| row.get(0))
|
||||
let current: u32 = conn
|
||||
.query_row(crate::queries::sqlite::queries::db_pool::Q_001, [], |row| {
|
||||
row.get(0)
|
||||
})
|
||||
.map_err(|e| format!("Failed to read user_version: {e}"))?;
|
||||
|
||||
let refinery_max: Option<u32> = conn
|
||||
.query_row(
|
||||
"SELECT MAX(version) FROM refinery_schema_history",
|
||||
[],
|
||||
|row| row.get(0),
|
||||
)
|
||||
.unwrap_or(None);
|
||||
|
||||
if let Some(v) = refinery_max
|
||||
&& v > current
|
||||
{
|
||||
current = v;
|
||||
conn.execute_batch(&format!("PRAGMA user_version = {current};"))
|
||||
.map_err(|e| format!("Failed to sync user_version from refinery: {e}"))?;
|
||||
}
|
||||
|
||||
for &(version, sql) in MIGRATIONS {
|
||||
if version > current {
|
||||
conn.execute_batch(sql)
|
||||
.map_err(|e| format!("Migration V{version:03} failed: {e}"))?;
|
||||
conn.execute_batch(&format!("PRAGMA user_version = {version};"))
|
||||
.map_err(|e| format!("Failed to set user_version to {version}: {e}"))?;
|
||||
conn.execute_batch(&crate::queries::sqlite::queries::db_pool::set_user_version(
|
||||
version,
|
||||
))
|
||||
.map_err(|e| format!("Failed to set user_version to {version}: {e}"))?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -237,10 +243,10 @@ pub fn open_user_db(data_dir: &str, user_hash: &str) -> Result<Connection, Strin
|
||||
let db_path = dir.join("db.sqlite");
|
||||
let conn = Connection::open(&db_path).map_err(|e| format!("Failed to open SQLite: {e}"))?;
|
||||
|
||||
conn.execute_batch("PRAGMA journal_mode=WAL;")
|
||||
conn.execute_batch(crate::queries::sqlite::queries::db_pool::Q_002)
|
||||
.map_err(|e| format!("Failed to set WAL mode: {e}"))?;
|
||||
|
||||
conn.execute_batch("PRAGMA foreign_keys=ON;")
|
||||
conn.execute_batch(crate::queries::sqlite::queries::db_pool::Q_003)
|
||||
.map_err(|e| format!("Failed to enable foreign keys: {e}"))?;
|
||||
|
||||
run_migrations(&conn)?;
|
||||
@@ -252,7 +258,7 @@ pub fn open_user_db(data_dir: &str, user_hash: &str) -> Result<Connection, Strin
|
||||
pub fn open_test_db() -> Connection {
|
||||
let conn = Connection::open_in_memory().expect("Failed to open in-memory SQLite");
|
||||
|
||||
conn.execute_batch("PRAGMA foreign_keys=ON;")
|
||||
conn.execute_batch(crate::queries::sqlite::queries::db_pool::Q_004)
|
||||
.expect("Failed to enable foreign keys");
|
||||
|
||||
run_migrations(&conn).expect("Test migrations failed");
|
||||
@@ -269,7 +275,7 @@ mod tests {
|
||||
let conn = open_test_db();
|
||||
|
||||
let tables: Vec<String> = conn
|
||||
.prepare("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name")
|
||||
.prepare(crate::queries::sqlite::queries::db_pool::Q_005)
|
||||
.unwrap()
|
||||
.query_map([], |row| row.get(0))
|
||||
.unwrap()
|
||||
@@ -294,7 +300,9 @@ mod tests {
|
||||
assert!(db_file.exists());
|
||||
|
||||
let fk: i32 = conn
|
||||
.query_row("PRAGMA foreign_keys", [], |row| row.get(0))
|
||||
.query_row(crate::queries::sqlite::queries::db_pool::Q_006, [], |row| {
|
||||
row.get(0)
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(fk, 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("SELECT id,name,body,format,created_at,updated_at FROM response_templates ORDER BY name COLLATE NOCASE")
|
||||
.prepare(crate::queries::sqlite::queries::db_response_templates::Q_001)
|
||||
.map_err(|e| format!("Failed to prepare response templates: {e}"))?;
|
||||
let rows = stmt
|
||||
.query_map([], from_row)
|
||||
@@ -35,7 +35,7 @@ pub fn list(conn: &Connection) -> Result<Vec<ResponseTemplate>, String> {
|
||||
|
||||
pub fn get(conn: &Connection, id: i64) -> Result<Option<ResponseTemplate>, String> {
|
||||
match conn.query_row(
|
||||
"SELECT id,name,body,format,created_at,updated_at FROM response_templates WHERE id=?1",
|
||||
crate::queries::sqlite::queries::db_response_templates::Q_002,
|
||||
params![id],
|
||||
from_row,
|
||||
) {
|
||||
@@ -67,7 +67,7 @@ pub fn create(
|
||||
_ => return Err("Invalid response template format".to_string()),
|
||||
};
|
||||
conn.execute(
|
||||
"INSERT INTO response_templates(name,body,format) VALUES (?1,?2,?3)",
|
||||
crate::queries::sqlite::queries::db_response_templates::Q_003,
|
||||
params![name, body, format],
|
||||
)
|
||||
.map_err(|e| {
|
||||
@@ -103,12 +103,18 @@ pub fn update(
|
||||
"text" | "" => "text",
|
||||
_ => return Err("Invalid response template format".to_string()),
|
||||
};
|
||||
let changed = conn.execute(
|
||||
"UPDATE response_templates SET name=?1,body=?2,format=?3,updated_at=datetime('now') WHERE id=?4",
|
||||
params![name, body, format, id],
|
||||
).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 update response template: {e}") })?;
|
||||
let changed = conn
|
||||
.execute(
|
||||
crate::queries::sqlite::queries::db_response_templates::Q_004,
|
||||
params![name, body, format, id],
|
||||
)
|
||||
.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 update response template: {e}")
|
||||
}
|
||||
})?;
|
||||
if changed == 0 {
|
||||
return Ok(None);
|
||||
}
|
||||
@@ -117,7 +123,10 @@ pub fn update(
|
||||
|
||||
pub fn delete(conn: &Connection, id: i64) -> Result<bool, String> {
|
||||
Ok(conn
|
||||
.execute("DELETE FROM response_templates WHERE id=?1", params![id])
|
||||
.execute(
|
||||
crate::queries::sqlite::queries::db_response_templates::Q_005,
|
||||
params![id],
|
||||
)
|
||||
.map_err(|e| format!("Failed to delete response template: {e}"))?
|
||||
> 0)
|
||||
}
|
||||
|
||||
+26
-12
@@ -1,4 +1,4 @@
|
||||
use rusqlite::{params, Connection};
|
||||
use rusqlite::{Connection, params};
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
@@ -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("SELECT id,name,query_string,created_at,updated_at FROM saved_searches ORDER BY name COLLATE NOCASE")
|
||||
.prepare(crate::queries::sqlite::queries::db_saved_searches::Q_001)
|
||||
.map_err(|e| format!("Failed to prepare saved searches: {e}"))?;
|
||||
let rows = stmt
|
||||
.query_map([], row_to_saved)
|
||||
@@ -38,7 +38,7 @@ pub fn create(conn: &Connection, name: &str, query_string: &str) -> Result<Saved
|
||||
return Err("Saved search name and query are required".to_string());
|
||||
}
|
||||
conn.execute(
|
||||
"INSERT INTO saved_searches(name,query_string) VALUES (?1,?2)",
|
||||
crate::queries::sqlite::queries::db_saved_searches::Q_002,
|
||||
params![name, query_string],
|
||||
)
|
||||
.map_err(|e| format!("Failed to save search: {e}"))?;
|
||||
@@ -46,23 +46,32 @@ pub fn create(conn: &Connection, name: &str, query_string: &str) -> Result<Saved
|
||||
get(conn, id)?.ok_or_else(|| "Failed to read saved search".to_string())
|
||||
}
|
||||
|
||||
pub fn update(conn: &Connection, id: i64, name: &str, query_string: &str) -> Result<Option<SavedSearch>, String> {
|
||||
pub fn update(
|
||||
conn: &Connection,
|
||||
id: i64,
|
||||
name: &str,
|
||||
query_string: &str,
|
||||
) -> Result<Option<SavedSearch>, String> {
|
||||
let name = name.trim();
|
||||
let query_string = query_string.trim();
|
||||
if name.is_empty() || query_string.is_empty() {
|
||||
return Err("Saved search name and query are required".to_string());
|
||||
}
|
||||
let changed = conn.execute(
|
||||
"UPDATE saved_searches SET name=?1,query_string=?2,updated_at=datetime('now') WHERE id=?3",
|
||||
params![name, query_string, id],
|
||||
).map_err(|e| format!("Failed to update saved search: {e}"))?;
|
||||
if changed == 0 { return Ok(None); }
|
||||
let changed = conn
|
||||
.execute(
|
||||
crate::queries::sqlite::queries::db_saved_searches::Q_003,
|
||||
params![name, query_string, id],
|
||||
)
|
||||
.map_err(|e| format!("Failed to update saved search: {e}"))?;
|
||||
if changed == 0 {
|
||||
return Ok(None);
|
||||
}
|
||||
get(conn, id)
|
||||
}
|
||||
|
||||
pub fn get(conn: &Connection, id: i64) -> Result<Option<SavedSearch>, String> {
|
||||
match conn.query_row(
|
||||
"SELECT id,name,query_string,created_at,updated_at FROM saved_searches WHERE id=?1",
|
||||
crate::queries::sqlite::queries::db_saved_searches::Q_004,
|
||||
params![id],
|
||||
row_to_saved,
|
||||
) {
|
||||
@@ -73,6 +82,11 @@ pub fn get(conn: &Connection, id: i64) -> Result<Option<SavedSearch>, String> {
|
||||
}
|
||||
|
||||
pub fn delete(conn: &Connection, id: i64) -> Result<bool, String> {
|
||||
Ok(conn.execute("DELETE FROM saved_searches WHERE id=?1", params![id])
|
||||
.map_err(|e| format!("Failed to delete saved search: {e}"))? > 0)
|
||||
Ok(conn
|
||||
.execute(
|
||||
crate::queries::sqlite::queries::db_saved_searches::Q_005,
|
||||
params![id],
|
||||
)
|
||||
.map_err(|e| format!("Failed to delete saved search: {e}"))?
|
||||
> 0)
|
||||
}
|
||||
@@ -21,7 +21,7 @@ pub fn get_role(conn: &Connection, role: &str) -> Result<Option<String>, String>
|
||||
return Err(format!("Invalid special folder role: {role}"));
|
||||
}
|
||||
conn.query_row(
|
||||
"SELECT folder_name FROM special_folders WHERE role = ?1",
|
||||
crate::queries::sqlite::queries::db_special_folders::Q_001,
|
||||
[role],
|
||||
|row| row.get(0),
|
||||
)
|
||||
@@ -47,7 +47,7 @@ pub fn set_role(conn: &Connection, role: &str, folder_name: Option<&str>) -> Res
|
||||
Some(folder) => {
|
||||
let exists: bool = conn
|
||||
.query_row(
|
||||
"SELECT EXISTS(SELECT 1 FROM folders WHERE name = ?1)",
|
||||
crate::queries::sqlite::queries::db_special_folders::Q_002,
|
||||
[folder],
|
||||
|row| row.get(0),
|
||||
)
|
||||
@@ -56,14 +56,17 @@ pub fn set_role(conn: &Connection, role: &str, folder_name: Option<&str>) -> Res
|
||||
return Err(format!("Folder does not exist: {folder}"));
|
||||
}
|
||||
conn.execute(
|
||||
"INSERT INTO special_folders(role, folder_name, updated_at) VALUES(?1, ?2, datetime('now'))\n ON CONFLICT(role) DO UPDATE SET folder_name = excluded.folder_name, updated_at = datetime('now')",
|
||||
crate::queries::sqlite::queries::db_special_folders::Q_003,
|
||||
params![role, folder],
|
||||
)
|
||||
.map_err(|e| format!("Failed to set special folder {role}: {e}"))?;
|
||||
}
|
||||
None => {
|
||||
conn.execute("DELETE FROM special_folders WHERE role = ?1", [role])
|
||||
.map_err(|e| format!("Failed to clear special folder {role}: {e}"))?;
|
||||
conn.execute(
|
||||
crate::queries::sqlite::queries::db_special_folders::Q_004,
|
||||
[role],
|
||||
)
|
||||
.map_err(|e| format!("Failed to clear special folder {role}: {e}"))?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
@@ -84,7 +87,7 @@ pub fn rename_folder_reference(
|
||||
new_name: &str,
|
||||
) -> Result<(), String> {
|
||||
conn.execute(
|
||||
"UPDATE special_folders SET folder_name = ?1, updated_at = datetime('now') WHERE folder_name = ?2",
|
||||
crate::queries::sqlite::queries::db_special_folders::Q_005,
|
||||
params![new_name, old_name],
|
||||
)
|
||||
.map_err(|e| format!("Failed to update special folder reference: {e}"))?;
|
||||
@@ -93,7 +96,7 @@ pub fn rename_folder_reference(
|
||||
|
||||
pub fn clear_folder_reference(conn: &Connection, folder_name: &str) -> Result<(), String> {
|
||||
conn.execute(
|
||||
"DELETE FROM special_folders WHERE folder_name = ?1",
|
||||
crate::queries::sqlite::queries::db_special_folders::Q_006,
|
||||
[folder_name],
|
||||
)
|
||||
.map_err(|e| format!("Failed to clear special folder reference: {e}"))?;
|
||||
|
||||
+16
-46
@@ -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(
|
||||
"INSERT INTO tags (id, name, color, imap_keyword) VALUES (?1, ?2, ?3, ?4)",
|
||||
crate::queries::sqlite::queries::db_tags::Q_001,
|
||||
params![id, name, color, imap_keyword],
|
||||
)
|
||||
.map_err(|e| format!("Failed to create tag: {e}"))?;
|
||||
@@ -42,12 +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(
|
||||
"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
|
||||
FROM tags t
|
||||
ORDER BY t.name ASC",
|
||||
)
|
||||
.prepare(crate::queries::sqlite::queries::db_tags::Q_002)
|
||||
.map_err(|e| format!("Failed to prepare list_tags: {e}"))?;
|
||||
|
||||
let rows = stmt
|
||||
@@ -81,7 +76,7 @@ pub fn get_message_refs_by_tag(
|
||||
tag_id: &str,
|
||||
) -> Result<Vec<(u32, String)>, String> {
|
||||
let mut stmt = conn
|
||||
.prepare("SELECT message_uid, message_folder FROM message_tags WHERE tag_id = ?1")
|
||||
.prepare(crate::queries::sqlite::queries::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)?)))
|
||||
@@ -140,10 +135,8 @@ pub fn reconcile_imap_keywords(
|
||||
add_tag_to_message(conn, &tag.id, uid, folder)?;
|
||||
added += 1;
|
||||
}
|
||||
(false, true) => {
|
||||
if remove_tag_from_message(conn, &tag.id, uid, folder)? {
|
||||
removed += 1;
|
||||
}
|
||||
(false, true) if remove_tag_from_message(conn, &tag.id, uid, folder)? => {
|
||||
removed += 1;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
@@ -154,7 +147,7 @@ pub fn reconcile_imap_keywords(
|
||||
pub fn update_tag(conn: &Connection, id: &str, name: &str, color: &str) -> Result<bool, String> {
|
||||
let updated = conn
|
||||
.execute(
|
||||
"UPDATE tags SET name = ?1, color = ?2, updated_at = datetime('now') WHERE id = ?3",
|
||||
crate::queries::sqlite::queries::db_tags::Q_004,
|
||||
params![name, color, id],
|
||||
)
|
||||
.map_err(|e| format!("Failed to update tag: {e}"))?;
|
||||
@@ -163,7 +156,7 @@ pub fn update_tag(conn: &Connection, id: &str, name: &str, color: &str) -> Resul
|
||||
|
||||
pub fn delete_tag(conn: &Connection, id: &str) -> Result<bool, String> {
|
||||
let deleted = conn
|
||||
.execute("DELETE FROM tags WHERE id = ?1", params![id])
|
||||
.execute(crate::queries::sqlite::queries::db_tags::Q_005, params![id])
|
||||
.map_err(|e| format!("Failed to delete tag: {e}"))?;
|
||||
Ok(deleted > 0)
|
||||
}
|
||||
@@ -175,7 +168,7 @@ pub fn add_tag_to_message(
|
||||
folder: &str,
|
||||
) -> Result<(), String> {
|
||||
conn.execute(
|
||||
"INSERT OR IGNORE INTO message_tags (tag_id, message_uid, message_folder) VALUES (?1, ?2, ?3)",
|
||||
crate::queries::sqlite::queries::db_tags::Q_006,
|
||||
params![tag_id, uid, folder],
|
||||
)
|
||||
.map_err(|e| format!("Failed to add tag to message: {e}"))?;
|
||||
@@ -190,7 +183,7 @@ pub fn remove_tag_from_message(
|
||||
) -> Result<bool, String> {
|
||||
let deleted = conn
|
||||
.execute(
|
||||
"DELETE FROM message_tags WHERE tag_id = ?1 AND message_uid = ?2 AND message_folder = ?3",
|
||||
crate::queries::sqlite::queries::db_tags::Q_007,
|
||||
params![tag_id, uid, folder],
|
||||
)
|
||||
.map_err(|e| format!("Failed to remove tag from message: {e}"))?;
|
||||
@@ -203,13 +196,7 @@ pub fn get_message_tags(
|
||||
folder: &str,
|
||||
) -> Result<Vec<MessageTag>, String> {
|
||||
let mut stmt = conn
|
||||
.prepare(
|
||||
"SELECT t.id, t.name, t.color
|
||||
FROM tags t
|
||||
JOIN message_tags mt ON mt.tag_id = t.id
|
||||
WHERE mt.message_uid = ?1 AND mt.message_folder = ?2
|
||||
ORDER BY t.name ASC",
|
||||
)
|
||||
.prepare(crate::queries::sqlite::queries::db_tags::Q_008)
|
||||
.map_err(|e| format!("Failed to prepare get_message_tags: {e}"))?;
|
||||
|
||||
let rows = stmt
|
||||
@@ -249,14 +236,7 @@ pub fn get_tags_for_messages(
|
||||
})
|
||||
.collect();
|
||||
|
||||
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 sql = crate::rustbox_query_db_tags_001!(conditions.join(" OR "));
|
||||
|
||||
let mut stmt = conn
|
||||
.prepare(&sql)
|
||||
@@ -299,16 +279,7 @@ pub fn get_messages_by_tag(
|
||||
) -> Result<Vec<CachedMessage>, String> {
|
||||
let offset = page * per_page;
|
||||
let mut stmt = conn
|
||||
.prepare(
|
||||
"SELECT m.uid, m.folder, m.message_id, m.in_reply_to, m.references_header,
|
||||
m.subject, m.from_address, m.from_name, m.to_addresses, m.cc_addresses,
|
||||
m.date, m.flags, m.size, m.has_attachments, m.snippet, m.reaction
|
||||
FROM messages m
|
||||
JOIN message_tags mt ON mt.message_uid = m.uid AND mt.message_folder = m.folder
|
||||
WHERE mt.tag_id = ?1
|
||||
ORDER BY m.date_epoch DESC
|
||||
LIMIT ?2 OFFSET ?3",
|
||||
)
|
||||
.prepare(crate::queries::sqlite::queries::db_tags::Q_009)
|
||||
.map_err(|e| format!("Failed to prepare get_messages_by_tag: {e}"))?;
|
||||
|
||||
let rows = stmt
|
||||
@@ -344,7 +315,7 @@ pub fn get_messages_by_tag(
|
||||
|
||||
pub fn count_messages_by_tag(conn: &Connection, tag_id: &str) -> Result<u32, String> {
|
||||
conn.query_row(
|
||||
"SELECT COUNT(*) FROM message_tags WHERE tag_id = ?1",
|
||||
crate::queries::sqlite::queries::db_tags::Q_010,
|
||||
params![tag_id],
|
||||
|row| row.get(0),
|
||||
)
|
||||
@@ -358,13 +329,12 @@ mod tests {
|
||||
|
||||
fn insert_test_message(conn: &Connection, folder: &str, uid: u32) {
|
||||
conn.execute(
|
||||
"INSERT OR IGNORE INTO folders (name) VALUES (?1)",
|
||||
crate::queries::sqlite::queries::db_tags::Q_011,
|
||||
params![folder],
|
||||
)
|
||||
.unwrap();
|
||||
conn.execute(
|
||||
"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, '')",
|
||||
crate::queries::sqlite::queries::db_tags::Q_012,
|
||||
params![uid, folder],
|
||||
)
|
||||
.unwrap();
|
||||
@@ -462,7 +432,7 @@ mod tests {
|
||||
let map = get_tags_for_messages(&conn, &refs).unwrap();
|
||||
|
||||
assert_eq!(map.get(&(1, "INBOX".to_string())).unwrap().len(), 1);
|
||||
assert!(map.get(&(2, "INBOX".to_string())).is_none());
|
||||
assert!(!map.contains_key(&(2, "INBOX".to_string())));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -2,7 +2,7 @@ use rusqlite::Connection;
|
||||
|
||||
pub fn list(conn: &Connection) -> Result<Vec<String>, String> {
|
||||
let mut stmt = conn
|
||||
.prepare("SELECT email FROM trusted_senders ORDER BY lower(email)")
|
||||
.prepare(crate::queries::sqlite::queries::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))
|
||||
@@ -13,7 +13,7 @@ pub fn list(conn: &Connection) -> Result<Vec<String>, String> {
|
||||
|
||||
pub fn add(conn: &Connection, email: &str) -> Result<(), String> {
|
||||
conn.execute(
|
||||
"INSERT OR IGNORE INTO trusted_senders(email) VALUES(?1)",
|
||||
crate::queries::sqlite::queries::db_trusted_senders::Q_002,
|
||||
[email],
|
||||
)
|
||||
.map_err(|e| format!("Failed to trust sender: {e}"))?;
|
||||
@@ -22,7 +22,7 @@ pub fn add(conn: &Connection, email: &str) -> Result<(), String> {
|
||||
|
||||
pub fn remove(conn: &Connection, email: &str) -> Result<(), String> {
|
||||
conn.execute(
|
||||
"DELETE FROM trusted_senders WHERE email = ?1 COLLATE NOCASE",
|
||||
crate::queries::sqlite::queries::db_trusted_senders::Q_003,
|
||||
[email],
|
||||
)
|
||||
.map_err(|e| format!("Failed to remove trusted sender: {e}"))?;
|
||||
|
||||
Reference in new issue
Block a user