//! SQLite implementation of the EmailRepository. //! //! Manages email messages with support for: //! - IMAP synchronization tracking (message_id, imap_uid) //! - Threading via in_reply_to and thread_id //! - Read/archived status //! - Project associations //! - Snoozing and waiting-for-response tracking //! //! A trait impl cannot be split across files, so this module keeps the struct //! and the single `impl EmailRepository` block, and every method checks out a //! connection and hands it to a free function in one of the submodules below. mod crud; mod draft; mod flags; mod query; mod row; mod state; mod sync; mod thread; use std::collections::HashSet; use chrono::{DateTime, Utc}; use goingson_core::{ Email, EmailAccountId, EmailId, EmailRepository, EmailThread, NewEmail, NewEmailWithTracking, ProjectId, Result, UserId, }; use crate::Db; /// SQLite-backed implementation of [`EmailRepository`]. /// /// Manages email messages with threading support, snoozing, and /// waiting-for-response tracking. Integrates with IMAP sync via message_id. pub struct SqliteEmailRepository { db: Db, } impl SqliteEmailRepository { /// Creates a new repository instance with the given connection pool. #[tracing::instrument(skip_all)] pub fn new(db: Db) -> Self { Self { db } } } impl EmailRepository for SqliteEmailRepository { #[tracing::instrument(skip_all)] fn list_all_for_backup(&self, user_id: UserId) -> Result> { let conn = self.db.conn()?; query::list_all_for_backup(&conn, user_id) } fn list_all(&self, user_id: UserId, include_archived: bool) -> Result> { let conn = self.db.conn()?; query::list_all(&conn, user_id, include_archived) } #[tracing::instrument(skip_all)] fn list_metadata(&self, user_id: UserId, include_archived: bool) -> Result> { let conn = self.db.conn()?; query::list_metadata(&conn, user_id, include_archived) } #[tracing::instrument(skip_all)] fn list_threaded( &self, user_id: UserId, include_archived: bool, offset: Option, limit: Option, folder: Option<&str>, label: Option<&str>, ) -> Result<(Vec, i64)> { let conn = self.db.conn()?; thread::list_threaded( &conn, user_id, include_archived, offset, limit, folder, label, ) } #[tracing::instrument(skip_all)] fn list_by_project(&self, user_id: UserId, project_id: ProjectId) -> Result> { let conn = self.db.conn()?; query::list_by_project(&conn, user_id, project_id) } #[tracing::instrument(skip_all)] fn list_by_addresses(&self, user_id: UserId, addresses: &[&str]) -> Result> { let conn = self.db.conn()?; query::list_by_addresses(&conn, user_id, addresses) } #[tracing::instrument(skip_all)] fn list_unlinked(&self, user_id: UserId) -> Result> { let conn = self.db.conn()?; query::list_unlinked(&conn, user_id) } #[tracing::instrument(skip_all)] fn get_by_id(&self, id: EmailId, user_id: UserId) -> Result> { let conn = self.db.conn()?; query::get_by_id(&conn, id, user_id) } #[tracing::instrument(skip_all)] fn create(&self, user_id: UserId, email: NewEmail) -> Result { let conn = self.db.conn()?; crud::create(&conn, user_id, &email) } #[tracing::instrument(skip_all)] fn restore(&self, user_id: UserId, email: &Email) -> Result<()> { let conn = self.db.conn()?; crud::restore(&conn, user_id, email) } #[tracing::instrument(skip_all)] fn create_with_tracking(&self, user_id: UserId, email: NewEmailWithTracking) -> Result { let conn = self.db.conn()?; crud::create_with_tracking(&conn, user_id, &email) } #[tracing::instrument(skip_all)] fn create_with_tracking_batch( &self, user_id: UserId, emails: Vec, ) -> Result { let mut conn = self.db.conn()?; crud::create_with_tracking_batch(&mut conn, user_id, emails) } #[tracing::instrument(skip_all)] fn delete(&self, id: EmailId, user_id: UserId) -> Result { let conn = self.db.conn()?; crud::delete(&conn, id, user_id) } #[tracing::instrument(skip_all)] fn mark_read(&self, id: EmailId, user_id: UserId) -> Result { let conn = self.db.conn()?; flags::mark_read(&conn, id, user_id) } #[tracing::instrument(skip_all)] fn mark_unread(&self, id: EmailId, user_id: UserId) -> Result { let conn = self.db.conn()?; flags::mark_unread(&conn, id, user_id) } #[tracing::instrument(skip_all)] fn archive(&self, id: EmailId, user_id: UserId) -> Result { let conn = self.db.conn()?; flags::archive(&conn, id, user_id) } #[tracing::instrument(skip_all)] fn unarchive(&self, id: EmailId, user_id: UserId) -> Result { let conn = self.db.conn()?; flags::unarchive(&conn, id, user_id) } #[tracing::instrument(skip_all)] fn update_source_folder(&self, id: EmailId, user_id: UserId, new_folder: &str) -> Result { let conn = self.db.conn()?; flags::update_source_folder(&conn, id, user_id, new_folder) } #[tracing::instrument(skip_all)] fn set_full_body(&self, id: EmailId, user_id: UserId, body: &str) -> Result<()> { let conn = self.db.conn()?; flags::set_full_body(&conn, id, user_id, body) } #[tracing::instrument(skip_all)] fn mark_all_read(&self, user_id: UserId) -> Result { let conn = self.db.conn()?; flags::mark_all_read(&conn, user_id) } #[tracing::instrument(skip_all)] fn link_to_project( &self, id: EmailId, user_id: UserId, project_id: Option, ) -> Result { let conn = self.db.conn()?; flags::link_to_project(&conn, id, user_id, project_id) } #[tracing::instrument(skip_all)] fn count_unread(&self, user_id: UserId) -> Result { let conn = self.db.conn()?; flags::count_unread(&conn, user_id) } #[tracing::instrument(skip_all)] fn exists_by_message_id(&self, user_id: UserId, message_id: &str) -> Result { let conn = self.db.conn()?; sync::exists_by_message_id(&conn, user_id, message_id) } #[tracing::instrument(skip_all)] fn exists_by_message_ids( &self, user_id: UserId, message_ids: &[&str], ) -> Result> { let conn = self.db.conn()?; sync::exists_by_message_ids(&conn, user_id, message_ids) } #[tracing::instrument(skip_all)] fn exists_as_senders(&self, user_id: UserId, addresses: &[&str]) -> Result> { let conn = self.db.conn()?; sync::exists_as_senders(&conn, user_id, addresses) } #[tracing::instrument(skip_all)] fn snooze(&self, id: EmailId, user_id: UserId, until: DateTime) -> Result> { let conn = self.db.conn()?; state::snooze(&conn, id, user_id, until) } #[tracing::instrument(skip_all)] fn unsnooze(&self, id: EmailId, user_id: UserId) -> Result> { let conn = self.db.conn()?; state::unsnooze(&conn, id, user_id) } #[tracing::instrument(skip_all)] fn list_snoozed(&self, user_id: UserId) -> Result> { let conn = self.db.conn()?; state::list_snoozed(&conn, user_id) } #[tracing::instrument(skip_all)] fn mark_waiting( &self, id: EmailId, user_id: UserId, expected_response: Option>, ) -> Result> { let conn = self.db.conn()?; state::mark_waiting(&conn, id, user_id, expected_response) } #[tracing::instrument(skip_all)] fn clear_waiting(&self, id: EmailId, user_id: UserId) -> Result> { let conn = self.db.conn()?; state::clear_waiting(&conn, id, user_id) } #[tracing::instrument(skip_all)] fn list_waiting(&self, user_id: UserId) -> Result> { let conn = self.db.conn()?; state::list_waiting(&conn, user_id) } #[tracing::instrument(skip_all)] fn list_by_thread(&self, user_id: UserId, thread_id: &str) -> Result> { let conn = self.db.conn()?; thread::list_by_thread(&conn, user_id, thread_id) } #[tracing::instrument(skip_all)] fn list_drafts(&self, user_id: UserId) -> Result> { let conn = self.db.conn()?; draft::list_drafts(&conn, user_id) } #[tracing::instrument(skip_all)] fn save_draft( &self, id: EmailId, user_id: UserId, from: &str, to: &str, cc: Option<&str>, bcc: Option<&str>, subject: &str, body: &str, account_id: Option, in_reply_to: Option<&str>, _references: Option<&str>, thread_id: Option<&str>, ) -> Result { let conn = self.db.conn()?; draft::save_draft( &conn, id, user_id, from, to, cc, bcc, subject, body, account_id, in_reply_to, thread_id, ) } #[tracing::instrument(skip_all)] fn queue_draft( &self, id: EmailId, user_id: UserId, send_after: Option>, ) -> Result> { let conn = self.db.conn()?; draft::queue_draft(&conn, id, user_id, send_after) } #[tracing::instrument(skip_all)] fn unqueue_draft(&self, id: EmailId, user_id: UserId) -> Result> { let conn = self.db.conn()?; draft::unqueue_draft(&conn, id, user_id) } #[tracing::instrument(skip_all)] fn list_outbox(&self, user_id: UserId) -> Result> { let conn = self.db.conn()?; draft::list_outbox(&conn, user_id) } #[tracing::instrument(skip_all)] fn list_due(&self, user_id: UserId, now: DateTime) -> Result> { let conn = self.db.conn()?; draft::list_due(&conn, user_id, now) } #[tracing::instrument(skip_all)] fn record_send_failure(&self, id: EmailId, user_id: UserId, error: &str) -> Result<()> { let conn = self.db.conn()?; draft::record_send_failure(&conn, id, user_id, error) } #[tracing::instrument(skip_all)] fn get_by_message_id(&self, user_id: UserId, message_id: &str) -> Result> { let conn = self.db.conn()?; sync::get_by_message_id(&conn, user_id, message_id) } #[tracing::instrument(skip_all)] fn update_labels( &self, id: EmailId, user_id: UserId, labels: &[String], ) -> Result> { let conn = self.db.conn()?; flags::update_labels(&conn, id, user_id, labels) } #[tracing::instrument(skip_all)] fn list_folders(&self, user_id: UserId) -> Result> { let conn = self.db.conn()?; sync::list_folders(&conn, user_id) } #[tracing::instrument(skip_all)] fn list_labels(&self, user_id: UserId) -> Result> { let conn = self.db.conn()?; sync::list_labels(&conn, user_id) } }