//! What the IMAP sync loop reads to decide what to fetch: existence checks by //! message-id and sender, the message-id lookup, and the folder/label sets. use std::collections::HashSet; use goingson_core::{CoreError, Email, Result, UserId}; use rusqlite::{Connection, params, params_from_iter}; use crate::utils::{bind_placeholders, query_all, query_opt}; use super::row::{EMAIL_SELECT_COLUMNS, EmailRow}; /// Whether an email with this message-id is already stored. pub(super) fn exists_by_message_id( conn: &Connection, user_id: UserId, message_id: &str, ) -> Result { let count: i64 = conn .query_row( "SELECT COUNT(*) FROM emails WHERE user_id = ? AND message_id = ?", params![user_id.to_string(), message_id], |row| row.get(0), ) .map_err(CoreError::database)?; Ok(count > 0) } /// Which of the given message-ids are already stored. pub(super) fn exists_by_message_ids( conn: &Connection, user_id: UserId, message_ids: &[&str], ) -> Result> { if message_ids.is_empty() { return Ok(HashSet::new()); } let placeholders = bind_placeholders(message_ids.len()); let query = format!( "SELECT message_id FROM emails WHERE user_id = ? AND message_id IN ({placeholders})" ); // user_id binds first, then one placeholder per message id. let mut binds: Vec = vec![user_id.to_string()]; binds.extend(message_ids.iter().map(|m| (*m).to_string())); let rows: Vec = query_all(conn, &query, params_from_iter(binds.iter()), |row| { row.get(0) })?; Ok(rows.into_iter().collect()) } /// Which of the given addresses have ever sent the user an email. pub(super) fn exists_as_senders( conn: &Connection, user_id: UserId, addresses: &[&str], ) -> Result> { if addresses.is_empty() { return Ok(HashSet::new()); } let placeholders = bind_placeholders(addresses.len()); let query = format!( "SELECT DISTINCT LOWER(from_address) FROM emails WHERE user_id = ? AND LOWER(from_address) IN ({placeholders})" ); let mut binds: Vec = Vec::with_capacity(addresses.len() + 1); binds.push(user_id.to_string()); binds.extend(addresses.iter().map(|a| a.to_lowercase())); let rows: Vec = query_all(conn, &query, params_from_iter(binds), |row| row.get(0))?; Ok(rows.into_iter().collect()) } /// One email by its RFC message-id. pub(super) fn get_by_message_id( conn: &Connection, user_id: UserId, message_id: &str, ) -> Result> { let query = format!( "SELECT {EMAIL_SELECT_COLUMNS} FROM emails e LEFT JOIN projects p ON e.project_id = p.id AND p.user_id = ? WHERE e.user_id = ? AND e.message_id = ?" ); let row = query_opt( conn, &query, params![user_id.to_string(), user_id.to_string(), message_id], EmailRow::from_row, )?; row.map(Email::try_from).transpose() } /// Distinct IMAP source folders seen across the user's non-draft emails. pub(super) fn list_folders(conn: &Connection, user_id: UserId) -> Result> { query_all( conn, "SELECT DISTINCT source_folder FROM emails WHERE user_id = ? AND source_folder IS NOT NULL AND is_draft = 0 ORDER BY source_folder ASC", params![user_id.to_string()], |row| row.get(0), ) } /// Distinct labels across the user's non-draft emails. pub(super) fn list_labels(conn: &Connection, user_id: UserId) -> Result> { // Extract all unique labels across all emails via JSON parsing query_all( conn, "SELECT DISTINCT j.value FROM emails e, json_each(e.labels) j WHERE e.user_id = ? AND e.is_draft = 0 ORDER BY j.value ASC", params![user_id.to_string()], |row| row.get(0), ) }