Skip to main content

max / goingson

3.8 KB · 117 lines History Blame Raw
1 //! What the IMAP sync loop reads to decide what to fetch: existence checks by
2 //! message-id and sender, the message-id lookup, and the folder/label sets.
3
4 use std::collections::HashSet;
5
6 use goingson_core::{CoreError, Email, Result, UserId};
7 use rusqlite::{Connection, params, params_from_iter};
8
9 use crate::utils::{bind_placeholders, query_all, query_opt};
10
11 use super::row::{EMAIL_SELECT_COLUMNS, EmailRow};
12
13 /// Whether an email with this message-id is already stored.
14 pub(super) fn exists_by_message_id(
15 conn: &Connection,
16 user_id: UserId,
17 message_id: &str,
18 ) -> Result<bool> {
19 let count: i64 = conn
20 .query_row(
21 "SELECT COUNT(*) FROM emails WHERE user_id = ? AND message_id = ?",
22 params![user_id.to_string(), message_id],
23 |row| row.get(0),
24 )
25 .map_err(CoreError::database)?;
26 Ok(count > 0)
27 }
28
29 /// Which of the given message-ids are already stored.
30 pub(super) fn exists_by_message_ids(
31 conn: &Connection,
32 user_id: UserId,
33 message_ids: &[&str],
34 ) -> Result<HashSet<String>> {
35 if message_ids.is_empty() {
36 return Ok(HashSet::new());
37 }
38
39 let placeholders = bind_placeholders(message_ids.len());
40 let query = format!(
41 "SELECT message_id FROM emails WHERE user_id = ? AND message_id IN ({placeholders})"
42 );
43
44 // user_id binds first, then one placeholder per message id.
45 let mut binds: Vec<String> = vec![user_id.to_string()];
46 binds.extend(message_ids.iter().map(|m| (*m).to_string()));
47
48 let rows: Vec<String> = query_all(conn, &query, params_from_iter(binds.iter()), |row| {
49 row.get(0)
50 })?;
51
52 Ok(rows.into_iter().collect())
53 }
54
55 /// Which of the given addresses have ever sent the user an email.
56 pub(super) fn exists_as_senders(
57 conn: &Connection,
58 user_id: UserId,
59 addresses: &[&str],
60 ) -> Result<HashSet<String>> {
61 if addresses.is_empty() {
62 return Ok(HashSet::new());
63 }
64
65 let placeholders = bind_placeholders(addresses.len());
66 let query = format!(
67 "SELECT DISTINCT LOWER(from_address) FROM emails WHERE user_id = ? AND LOWER(from_address) IN ({placeholders})"
68 );
69
70 let mut binds: Vec<String> = Vec::with_capacity(addresses.len() + 1);
71 binds.push(user_id.to_string());
72 binds.extend(addresses.iter().map(|a| a.to_lowercase()));
73
74 let rows: Vec<String> = query_all(conn, &query, params_from_iter(binds), |row| row.get(0))?;
75
76 Ok(rows.into_iter().collect())
77 }
78
79 /// One email by its RFC message-id.
80 pub(super) fn get_by_message_id(
81 conn: &Connection,
82 user_id: UserId,
83 message_id: &str,
84 ) -> Result<Option<Email>> {
85 let query = format!(
86 "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 = ?"
87 );
88 let row = query_opt(
89 conn,
90 &query,
91 params![user_id.to_string(), user_id.to_string(), message_id],
92 EmailRow::from_row,
93 )?;
94 row.map(Email::try_from).transpose()
95 }
96
97 /// Distinct IMAP source folders seen across the user's non-draft emails.
98 pub(super) fn list_folders(conn: &Connection, user_id: UserId) -> Result<Vec<String>> {
99 query_all(
100 conn,
101 "SELECT DISTINCT source_folder FROM emails WHERE user_id = ? AND source_folder IS NOT NULL AND is_draft = 0 ORDER BY source_folder ASC",
102 params![user_id.to_string()],
103 |row| row.get(0),
104 )
105 }
106
107 /// Distinct labels across the user's non-draft emails.
108 pub(super) fn list_labels(conn: &Connection, user_id: UserId) -> Result<Vec<String>> {
109 // Extract all unique labels across all emails via JSON parsing
110 query_all(
111 conn,
112 "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",
113 params![user_id.to_string()],
114 |row| row.get(0),
115 )
116 }
117