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

This commit is contained in:
Mateusz Gruszczyński committed 2026-11-04 14:59:03 +01:00
1 parent 6e74b33f03
commit 3b9110c8c8
44 files changed
+3057 -128

No files matched your search

+1
View File
@@ -16,6 +16,7 @@ pub mod pool;
pub mod response_templates;
pub mod search_outbox;
pub mod settings;
pub mod snooze;
pub mod special_folders;
pub mod tags;
pub mod trusted_senders;
+29 -14
View File
@@ -21,20 +21,33 @@ const SQLITE_SCHEMA: &str = include_str!("../../migrations/sqlite/schema.sql");
const MYSQL_SCHEMA: &str = include_str!("../../migrations/mysql/schema.sql");
const PGSQL_SCHEMA: &str = include_str!("../../migrations/pgsql/schema.sql");
const SQLITE_MIGRATIONS: &[(u32, &str)] = &[(
51,
include_str!(
"../../migrations/sqlite/V051__adaptive_email_colors_and_message_loading_mode.sql"
const SQLITE_MIGRATIONS: &[(u32, &str)] = &[
(
51,
include_str!(
"../../migrations/sqlite/V051__adaptive_email_colors_and_message_loading_mode.sql"
),
),
)];
const MYSQL_MIGRATIONS: &[(u32, &str)] = &[(
51,
include_str!("../../migrations/mysql/V051__adaptive_email_colors_and_message_loading_mode.sql"),
)];
const PGSQL_MIGRATIONS: &[(u32, &str)] = &[(
51,
include_str!("../../migrations/pgsql/V051__adaptive_email_colors_and_message_loading_mode.sql"),
)];
(52, include_str!("../../migrations/sqlite/V052__snooze.sql")),
];
const MYSQL_MIGRATIONS: &[(u32, &str)] = &[
(
51,
include_str!(
"../../migrations/mysql/V051__adaptive_email_colors_and_message_loading_mode.sql"
),
),
(52, include_str!("../../migrations/mysql/V052__snooze.sql")),
];
const PGSQL_MIGRATIONS: &[(u32, &str)] = &[
(
51,
include_str!(
"../../migrations/pgsql/V051__adaptive_email_colors_and_message_loading_mode.sql"
),
),
(52, include_str!("../../migrations/pgsql/V052__snooze.sql")),
];
const TENANT_TABLES: &[&str] = &[
"blob_delete_outbox",
@@ -61,6 +74,7 @@ const TENANT_TABLES: &[&str] = &[
"response_templates",
"saved_searches",
"search_outbox",
"snoozed_messages",
"special_folders",
"tags",
"trusted_senders",
@@ -87,6 +101,7 @@ const IMPORT_TABLES: &[&str] = &[
"messages",
"tags",
"message_tags",
"snoozed_messages",
"notification_preferences",
"pgp_keys",
"pgp_preferences",
@@ -2648,7 +2663,7 @@ mod tests {
|row| row.get(0),
)
.unwrap();
assert_eq!(version, 51);
assert_eq!(version, 52);
let adaptive: i64 = conn
.query_row(
+191
View File
@@ -0,0 +1,191 @@
//! Snoozing a message hides it from its folder listing until a chosen wake
//! time. This is purely local cache state: nothing is moved on the real IMAP
//! server, so snoozing never risks the actual mailbox. The client is
//! responsible for excluding a still-sleeping message from the folder it
//! renders and for showing the "Snoozed" view built from [`list_snoozed`].
use std::time::{SystemTime, UNIX_EPOCH};
use crate::db::pool::{Connection, params};
pub fn now_epoch() -> i64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs()
.min(i64::MAX as u64) as i64
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct SnoozedMessage {
pub id: String,
pub message_uid: u32,
pub message_folder: String,
pub wake_at: i64,
pub subject: String,
pub from_name: String,
pub from_address: String,
pub snippet: String,
}
/// Snoozes `(folder, uid)` until `wake_at` (epoch seconds), replacing any
/// existing snooze for the same message. Returns the new snooze id.
pub fn snooze_message(
conn: &Connection,
folder: &str,
uid: u32,
wake_at: i64,
) -> Result<String, String> {
if folder.trim().is_empty() {
return Err("folder must not be empty".to_string());
}
if wake_at <= now_epoch() {
return Err("snooze wake time must be in the future".to_string());
}
conn.execute(
crate::dbq!(db_snoozed_messages::Q_002),
params![uid, folder],
)
.map_err(|e| format!("failed to clear existing snooze: {e}"))?;
let id = uuid::Uuid::new_v4().to_string();
conn.execute(
crate::dbq!(db_snoozed_messages::Q_001),
params![&id, uid, folder, wake_at],
)
.map_err(|e| format!("failed to snooze message: {e}"))?;
Ok(id)
}
/// Removes any snooze on `(folder, uid)`. Returns whether a row was removed.
pub fn unsnooze_message(conn: &Connection, folder: &str, uid: u32) -> Result<bool, String> {
let changed = conn
.execute(
crate::dbq!(db_snoozed_messages::Q_002),
params![uid, folder],
)
.map_err(|e| format!("failed to unsnooze message: {e}"))?;
Ok(changed > 0)
}
/// Every snoozed message (woken or not), joined with the cached message
/// metadata needed to render the "Snoozed" view, ordered by wake time.
pub fn list_snoozed(conn: &Connection) -> Result<Vec<SnoozedMessage>, String> {
let mut stmt = conn
.prepare(crate::dbq!(db_snoozed_messages::Q_003))
.map_err(|e| format!("failed to prepare snoozed-messages query: {e}"))?;
let rows = stmt
.query_map([], |row| {
Ok(SnoozedMessage {
id: row.get(0)?,
message_uid: row.get::<_, i64>(1)?.max(0) as u32,
message_folder: row.get(2)?,
wake_at: row.get(3)?,
subject: row.get::<_, Option<String>>(4)?.unwrap_or_default(),
from_name: row.get::<_, Option<String>>(5)?.unwrap_or_default(),
from_address: row.get::<_, Option<String>>(6)?.unwrap_or_default(),
snippet: row.get::<_, Option<String>>(7)?.unwrap_or_default(),
})
})
.map_err(|e| format!("failed to query snoozed messages: {e}"))?;
rows.map(|row| row.map_err(|e| format!("failed to read snoozed message row: {e}")))
.collect()
}
/// The UIDs in `folder` that are still asleep as of `now` (i.e. due to be
/// hidden from that folder's listing right now). Cheap enough for the client
/// to call once per folder open; no IMAP or background work is involved.
pub fn list_sleeping_uids_in_folder(
conn: &Connection,
folder: &str,
now: i64,
) -> Result<Vec<u32>, String> {
let mut stmt = conn
.prepare(crate::dbq!(db_snoozed_messages::Q_004))
.map_err(|e| format!("failed to prepare sleeping-uids query: {e}"))?;
let rows = stmt
.query_map(params![folder, now], |row| {
Ok(row.get::<_, i64>(0)?.max(0) as u32)
})
.map_err(|e| format!("failed to query sleeping uids: {e}"))?;
rows.map(|row| row.map_err(|e| format!("failed to read sleeping uid row: {e}")))
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::db::pool::open_test_db;
fn insert_cached_message(conn: &Connection, folder: &str, uid: u32, subject: &str) {
conn.execute(
"INSERT OR IGNORE INTO folders (name) VALUES (?1)",
params![folder],
)
.unwrap();
conn.execute(
"INSERT INTO messages (uid, folder, subject, date, size) VALUES (?1, ?2, ?3, '', 0)",
params![uid, folder, subject],
)
.unwrap();
}
#[test]
fn snoozing_rejects_a_wake_time_in_the_past() {
let conn = open_test_db();
let error = snooze_message(&conn, "INBOX", 1, now_epoch() - 10).unwrap_err();
assert!(error.contains("future"));
}
#[test]
fn re_snoozing_the_same_message_replaces_the_wake_time() {
let conn = open_test_db();
let future = now_epoch() + 3600;
let later = now_epoch() + 7200;
snooze_message(&conn, "INBOX", 42, future).unwrap();
snooze_message(&conn, "INBOX", 42, later).unwrap();
let snoozed = list_snoozed(&conn).unwrap();
assert_eq!(snoozed.len(), 1);
assert_eq!(snoozed[0].wake_at, later);
}
#[test]
fn unsnoozing_removes_the_entry() {
let conn = open_test_db();
snooze_message(&conn, "INBOX", 7, now_epoch() + 60).unwrap();
assert!(unsnooze_message(&conn, "INBOX", 7).unwrap());
assert!(list_snoozed(&conn).unwrap().is_empty());
assert!(!unsnooze_message(&conn, "INBOX", 7).unwrap());
}
#[test]
fn list_snoozed_carries_cached_message_metadata() {
let conn = open_test_db();
insert_cached_message(&conn, "INBOX", 11, "Quarterly report");
snooze_message(&conn, "INBOX", 11, now_epoch() + 60).unwrap();
let snoozed = list_snoozed(&conn).unwrap();
assert_eq!(snoozed.len(), 1);
assert_eq!(snoozed[0].subject, "Quarterly report");
}
#[test]
fn sleeping_uids_exclude_already_woken_snoozes() {
let conn = open_test_db();
let now = now_epoch();
snooze_message(&conn, "INBOX", 1, now + 60).unwrap();
snooze_message(&conn, "INBOX", 2, now + 120).unwrap();
let sleeping = list_sleeping_uids_in_folder(&conn, "INBOX", now).unwrap();
assert_eq!(sleeping.len(), 2);
let sleeping_later = list_sleeping_uids_in_folder(&conn, "INBOX", now + 90).unwrap();
assert_eq!(sleeping_later, vec![2]);
}
#[test]
fn sleeping_uids_are_scoped_to_their_folder() {
let conn = open_test_db();
let now = now_epoch();
snooze_message(&conn, "INBOX", 1, now + 60).unwrap();
snooze_message(&conn, "Archive", 2, now + 60).unwrap();
let sleeping = list_sleeping_uids_in_folder(&conn, "INBOX", now).unwrap();
assert_eq!(sleeping, vec![1]);
}
}