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

This commit is contained in:
Mateusz Gruszczyński committed 2026-10-30 17:22:17 +01:00
1 parent 0421994fe6
commit 425ea7449b
138 files changed
+5705 -1012

No files matched your search

+29 -54
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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)
}
+28 -13
View File
@@ -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
View File
@@ -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),
)
+3 -7
View File
@@ -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
View File
@@ -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
View File
@@ -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(&current.remote_images);
let preferred_body = data.preferred_body.as_deref().unwrap_or(&current.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(&current.remote_images);
let preferred_body = data
.preferred_body
.as_deref()
.unwrap_or(&current.preferred_body);
let thread_mode = data.thread_mode.as_deref().unwrap_or(&current.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(&current.default_sort);
let default_sort = data
.default_sort
.as_deref()
.unwrap_or(&current.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
View File
@@ -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
View File
@@ -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;
+12 -4
View File
@@ -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
View File
@@ -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
View File
@@ -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);
+19 -10
View File
@@ -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
View File
@@ -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)
}
+10 -7
View File
@@ -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
View File
@@ -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]
+3 -3
View File
@@ -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}"))?;