Skip to main content

max / goingson

11.4 KB · 378 lines History Blame Raw
1 //! SQLite implementation of the EmailRepository.
2 //!
3 //! Manages email messages with support for:
4 //! - IMAP synchronization tracking (message_id, imap_uid)
5 //! - Threading via in_reply_to and thread_id
6 //! - Read/archived status
7 //! - Project associations
8 //! - Snoozing and waiting-for-response tracking
9 //!
10 //! A trait impl cannot be split across files, so this module keeps the struct
11 //! and the single `impl EmailRepository` block, and every method checks out a
12 //! connection and hands it to a free function in one of the submodules below.
13
14 mod crud;
15 mod draft;
16 mod flags;
17 mod query;
18 mod row;
19 mod state;
20 mod sync;
21 mod thread;
22
23 use std::collections::HashSet;
24
25 use chrono::{DateTime, Utc};
26 use goingson_core::{
27 Email, EmailAccountId, EmailId, EmailRepository, EmailThread, NewEmail, NewEmailWithTracking,
28 ProjectId, Result, UserId,
29 };
30
31 use crate::Db;
32
33 /// SQLite-backed implementation of [`EmailRepository`].
34 ///
35 /// Manages email messages with threading support, snoozing, and
36 /// waiting-for-response tracking. Integrates with IMAP sync via message_id.
37 pub struct SqliteEmailRepository {
38 db: Db,
39 }
40
41 impl SqliteEmailRepository {
42 /// Creates a new repository instance with the given connection pool.
43 #[tracing::instrument(skip_all)]
44 pub fn new(db: Db) -> Self {
45 Self { db }
46 }
47 }
48
49 impl EmailRepository for SqliteEmailRepository {
50 #[tracing::instrument(skip_all)]
51 fn list_all_for_backup(&self, user_id: UserId) -> Result<Vec<Email>> {
52 let conn = self.db.conn()?;
53 query::list_all_for_backup(&conn, user_id)
54 }
55
56 fn list_all(&self, user_id: UserId, include_archived: bool) -> Result<Vec<Email>> {
57 let conn = self.db.conn()?;
58 query::list_all(&conn, user_id, include_archived)
59 }
60
61 #[tracing::instrument(skip_all)]
62 fn list_metadata(&self, user_id: UserId, include_archived: bool) -> Result<Vec<Email>> {
63 let conn = self.db.conn()?;
64 query::list_metadata(&conn, user_id, include_archived)
65 }
66
67 #[tracing::instrument(skip_all)]
68 fn list_threaded(
69 &self,
70 user_id: UserId,
71 include_archived: bool,
72 offset: Option<i64>,
73 limit: Option<i64>,
74 folder: Option<&str>,
75 label: Option<&str>,
76 ) -> Result<(Vec<EmailThread>, i64)> {
77 let conn = self.db.conn()?;
78 thread::list_threaded(
79 &conn,
80 user_id,
81 include_archived,
82 offset,
83 limit,
84 folder,
85 label,
86 )
87 }
88
89 #[tracing::instrument(skip_all)]
90 fn list_by_project(&self, user_id: UserId, project_id: ProjectId) -> Result<Vec<Email>> {
91 let conn = self.db.conn()?;
92 query::list_by_project(&conn, user_id, project_id)
93 }
94
95 #[tracing::instrument(skip_all)]
96 fn list_by_addresses(&self, user_id: UserId, addresses: &[&str]) -> Result<Vec<Email>> {
97 let conn = self.db.conn()?;
98 query::list_by_addresses(&conn, user_id, addresses)
99 }
100
101 #[tracing::instrument(skip_all)]
102 fn list_unlinked(&self, user_id: UserId) -> Result<Vec<Email>> {
103 let conn = self.db.conn()?;
104 query::list_unlinked(&conn, user_id)
105 }
106
107 #[tracing::instrument(skip_all)]
108 fn get_by_id(&self, id: EmailId, user_id: UserId) -> Result<Option<Email>> {
109 let conn = self.db.conn()?;
110 query::get_by_id(&conn, id, user_id)
111 }
112
113 #[tracing::instrument(skip_all)]
114 fn create(&self, user_id: UserId, email: NewEmail) -> Result<Email> {
115 let conn = self.db.conn()?;
116 crud::create(&conn, user_id, &email)
117 }
118
119 #[tracing::instrument(skip_all)]
120 fn restore(&self, user_id: UserId, email: &Email) -> Result<()> {
121 let conn = self.db.conn()?;
122 crud::restore(&conn, user_id, email)
123 }
124
125 #[tracing::instrument(skip_all)]
126 fn create_with_tracking(&self, user_id: UserId, email: NewEmailWithTracking) -> Result<Email> {
127 let conn = self.db.conn()?;
128 crud::create_with_tracking(&conn, user_id, &email)
129 }
130
131 #[tracing::instrument(skip_all)]
132 fn create_with_tracking_batch(
133 &self,
134 user_id: UserId,
135 emails: Vec<NewEmailWithTracking>,
136 ) -> Result<usize> {
137 let mut conn = self.db.conn()?;
138 crud::create_with_tracking_batch(&mut conn, user_id, emails)
139 }
140
141 #[tracing::instrument(skip_all)]
142 fn delete(&self, id: EmailId, user_id: UserId) -> Result<bool> {
143 let conn = self.db.conn()?;
144 crud::delete(&conn, id, user_id)
145 }
146
147 #[tracing::instrument(skip_all)]
148 fn mark_read(&self, id: EmailId, user_id: UserId) -> Result<bool> {
149 let conn = self.db.conn()?;
150 flags::mark_read(&conn, id, user_id)
151 }
152
153 #[tracing::instrument(skip_all)]
154 fn mark_unread(&self, id: EmailId, user_id: UserId) -> Result<bool> {
155 let conn = self.db.conn()?;
156 flags::mark_unread(&conn, id, user_id)
157 }
158
159 #[tracing::instrument(skip_all)]
160 fn archive(&self, id: EmailId, user_id: UserId) -> Result<bool> {
161 let conn = self.db.conn()?;
162 flags::archive(&conn, id, user_id)
163 }
164
165 #[tracing::instrument(skip_all)]
166 fn unarchive(&self, id: EmailId, user_id: UserId) -> Result<bool> {
167 let conn = self.db.conn()?;
168 flags::unarchive(&conn, id, user_id)
169 }
170
171 #[tracing::instrument(skip_all)]
172 fn update_source_folder(&self, id: EmailId, user_id: UserId, new_folder: &str) -> Result<bool> {
173 let conn = self.db.conn()?;
174 flags::update_source_folder(&conn, id, user_id, new_folder)
175 }
176
177 #[tracing::instrument(skip_all)]
178 fn set_full_body(&self, id: EmailId, user_id: UserId, body: &str) -> Result<()> {
179 let conn = self.db.conn()?;
180 flags::set_full_body(&conn, id, user_id, body)
181 }
182
183 #[tracing::instrument(skip_all)]
184 fn mark_all_read(&self, user_id: UserId) -> Result<u64> {
185 let conn = self.db.conn()?;
186 flags::mark_all_read(&conn, user_id)
187 }
188
189 #[tracing::instrument(skip_all)]
190 fn link_to_project(
191 &self,
192 id: EmailId,
193 user_id: UserId,
194 project_id: Option<ProjectId>,
195 ) -> Result<bool> {
196 let conn = self.db.conn()?;
197 flags::link_to_project(&conn, id, user_id, project_id)
198 }
199
200 #[tracing::instrument(skip_all)]
201 fn count_unread(&self, user_id: UserId) -> Result<i64> {
202 let conn = self.db.conn()?;
203 flags::count_unread(&conn, user_id)
204 }
205
206 #[tracing::instrument(skip_all)]
207 fn exists_by_message_id(&self, user_id: UserId, message_id: &str) -> Result<bool> {
208 let conn = self.db.conn()?;
209 sync::exists_by_message_id(&conn, user_id, message_id)
210 }
211
212 #[tracing::instrument(skip_all)]
213 fn exists_by_message_ids(
214 &self,
215 user_id: UserId,
216 message_ids: &[&str],
217 ) -> Result<HashSet<String>> {
218 let conn = self.db.conn()?;
219 sync::exists_by_message_ids(&conn, user_id, message_ids)
220 }
221
222 #[tracing::instrument(skip_all)]
223 fn exists_as_senders(&self, user_id: UserId, addresses: &[&str]) -> Result<HashSet<String>> {
224 let conn = self.db.conn()?;
225 sync::exists_as_senders(&conn, user_id, addresses)
226 }
227
228 #[tracing::instrument(skip_all)]
229 fn snooze(&self, id: EmailId, user_id: UserId, until: DateTime<Utc>) -> Result<Option<Email>> {
230 let conn = self.db.conn()?;
231 state::snooze(&conn, id, user_id, until)
232 }
233
234 #[tracing::instrument(skip_all)]
235 fn unsnooze(&self, id: EmailId, user_id: UserId) -> Result<Option<Email>> {
236 let conn = self.db.conn()?;
237 state::unsnooze(&conn, id, user_id)
238 }
239
240 #[tracing::instrument(skip_all)]
241 fn list_snoozed(&self, user_id: UserId) -> Result<Vec<Email>> {
242 let conn = self.db.conn()?;
243 state::list_snoozed(&conn, user_id)
244 }
245
246 #[tracing::instrument(skip_all)]
247 fn mark_waiting(
248 &self,
249 id: EmailId,
250 user_id: UserId,
251 expected_response: Option<DateTime<Utc>>,
252 ) -> Result<Option<Email>> {
253 let conn = self.db.conn()?;
254 state::mark_waiting(&conn, id, user_id, expected_response)
255 }
256
257 #[tracing::instrument(skip_all)]
258 fn clear_waiting(&self, id: EmailId, user_id: UserId) -> Result<Option<Email>> {
259 let conn = self.db.conn()?;
260 state::clear_waiting(&conn, id, user_id)
261 }
262
263 #[tracing::instrument(skip_all)]
264 fn list_waiting(&self, user_id: UserId) -> Result<Vec<Email>> {
265 let conn = self.db.conn()?;
266 state::list_waiting(&conn, user_id)
267 }
268
269 #[tracing::instrument(skip_all)]
270 fn list_by_thread(&self, user_id: UserId, thread_id: &str) -> Result<Vec<Email>> {
271 let conn = self.db.conn()?;
272 thread::list_by_thread(&conn, user_id, thread_id)
273 }
274
275 #[tracing::instrument(skip_all)]
276 fn list_drafts(&self, user_id: UserId) -> Result<Vec<Email>> {
277 let conn = self.db.conn()?;
278 draft::list_drafts(&conn, user_id)
279 }
280
281 #[tracing::instrument(skip_all)]
282 fn save_draft(
283 &self,
284 id: EmailId,
285 user_id: UserId,
286 from: &str,
287 to: &str,
288 cc: Option<&str>,
289 bcc: Option<&str>,
290 subject: &str,
291 body: &str,
292 account_id: Option<EmailAccountId>,
293 in_reply_to: Option<&str>,
294 _references: Option<&str>,
295 thread_id: Option<&str>,
296 ) -> Result<Email> {
297 let conn = self.db.conn()?;
298 draft::save_draft(
299 &conn,
300 id,
301 user_id,
302 from,
303 to,
304 cc,
305 bcc,
306 subject,
307 body,
308 account_id,
309 in_reply_to,
310 thread_id,
311 )
312 }
313
314 #[tracing::instrument(skip_all)]
315 fn queue_draft(
316 &self,
317 id: EmailId,
318 user_id: UserId,
319 send_after: Option<DateTime<Utc>>,
320 ) -> Result<Option<Email>> {
321 let conn = self.db.conn()?;
322 draft::queue_draft(&conn, id, user_id, send_after)
323 }
324
325 #[tracing::instrument(skip_all)]
326 fn unqueue_draft(&self, id: EmailId, user_id: UserId) -> Result<Option<Email>> {
327 let conn = self.db.conn()?;
328 draft::unqueue_draft(&conn, id, user_id)
329 }
330
331 #[tracing::instrument(skip_all)]
332 fn list_outbox(&self, user_id: UserId) -> Result<Vec<Email>> {
333 let conn = self.db.conn()?;
334 draft::list_outbox(&conn, user_id)
335 }
336
337 #[tracing::instrument(skip_all)]
338 fn list_due(&self, user_id: UserId, now: DateTime<Utc>) -> Result<Vec<Email>> {
339 let conn = self.db.conn()?;
340 draft::list_due(&conn, user_id, now)
341 }
342
343 #[tracing::instrument(skip_all)]
344 fn record_send_failure(&self, id: EmailId, user_id: UserId, error: &str) -> Result<()> {
345 let conn = self.db.conn()?;
346 draft::record_send_failure(&conn, id, user_id, error)
347 }
348
349 #[tracing::instrument(skip_all)]
350 fn get_by_message_id(&self, user_id: UserId, message_id: &str) -> Result<Option<Email>> {
351 let conn = self.db.conn()?;
352 sync::get_by_message_id(&conn, user_id, message_id)
353 }
354
355 #[tracing::instrument(skip_all)]
356 fn update_labels(
357 &self,
358 id: EmailId,
359 user_id: UserId,
360 labels: &[String],
361 ) -> Result<Option<Email>> {
362 let conn = self.db.conn()?;
363 flags::update_labels(&conn, id, user_id, labels)
364 }
365
366 #[tracing::instrument(skip_all)]
367 fn list_folders(&self, user_id: UserId) -> Result<Vec<String>> {
368 let conn = self.db.conn()?;
369 sync::list_folders(&conn, user_id)
370 }
371
372 #[tracing::instrument(skip_all)]
373 fn list_labels(&self, user_id: UserId) -> Result<Vec<String>> {
374 let conn = self.db.conn()?;
375 sync::list_labels(&conn, user_id)
376 }
377 }
378