Skip to main content

max / goingson

7.5 KB · 178 lines History Blame Raw
1 //! Row layer for the email repository: the column lists every query selects,
2 //! the raw `EmailRow` those queries deserialize into, and its conversion into
3 //! the domain [`Email`].
4
5 use goingson_core::{BodyFormat, CoreError, Email, ParseableEnum};
6
7 use crate::utils::{parse_datetime, parse_uuid, parse_uuid_opt};
8
9 /// Column list for SELECT queries - avoids duplication across methods.
10 pub(super) const EMAIL_SELECT_COLUMNS: &str = r"e.id, e.project_id, p.name as project_name, e.from_address, e.to_address,
11 e.subject, e.body, e.body_format, e.html_body, e.is_read, e.is_archived, e.received_at, e.message_id,
12 e.in_reply_to, e.thread_id, e.email_account_id, e.is_outgoing, e.imap_uid, e.source_folder,
13 e.attachment_meta, e.labels, e.is_draft, e.cc_address, e.bcc_address, e.draft_account_id,
14 e.snoozed_until, e.waiting_for_response, e.waiting_since, e.expected_response_date,
15 e.body_truncated, e.jmap_id,
16 e.queued_at, e.send_after, e.send_attempts, e.send_error";
17
18 /// Same shape as [`EMAIL_SELECT_COLUMNS`] but with the two heavy body columns
19 /// blanked, for flat list views that never render a body (Perf S3). `body_truncated`
20 /// is forced to 1 so the reader knows to re-fetch the full body on open.
21 pub(super) const EMAIL_LIST_COLUMNS: &str = r"e.id, e.project_id, p.name as project_name, e.from_address, e.to_address,
22 e.subject, '' AS body, e.body_format, NULL AS html_body, e.is_read, e.is_archived, e.received_at, e.message_id,
23 e.in_reply_to, e.thread_id, e.email_account_id, e.is_outgoing, e.imap_uid, e.source_folder,
24 e.attachment_meta, e.labels, e.is_draft, e.cc_address, e.bcc_address, e.draft_account_id,
25 e.snoozed_until, e.waiting_for_response, e.waiting_since, e.expected_response_date,
26 1 AS body_truncated, e.jmap_id,
27 e.queued_at, e.send_after, e.send_attempts, e.send_error";
28
29 /// Upper bound on a flat metadata list so it can never materialize an unbounded
30 /// number of rows (the list UI paginates via `list_threaded`; this flat path is a
31 /// safety-capped fallback).
32 pub(super) const EMAIL_LIST_CAP: i64 = 1000;
33
34 #[derive(Debug, Clone)]
35 pub(super) struct EmailRow {
36 pub id: String,
37 pub project_id: Option<String>,
38 pub project_name: Option<String>,
39 pub from_address: String,
40 pub to_address: String,
41 pub subject: String,
42 pub body: String,
43 pub body_format: String,
44 pub html_body: Option<String>,
45 pub is_read: i32,
46 pub is_archived: i32,
47 pub received_at: String,
48 pub message_id: Option<String>,
49 pub in_reply_to: Option<String>,
50 pub thread_id: Option<String>,
51 pub email_account_id: Option<String>,
52 pub is_outgoing: i32,
53 pub imap_uid: Option<i64>,
54 pub source_folder: Option<String>,
55 pub attachment_meta: Option<String>,
56 pub labels: String,
57 pub is_draft: i32,
58 pub cc_address: Option<String>,
59 pub bcc_address: Option<String>,
60 pub draft_account_id: Option<String>,
61 pub snoozed_until: Option<String>,
62 pub waiting_for_response: i32,
63 pub waiting_since: Option<String>,
64 pub expected_response_date: Option<String>,
65 pub body_truncated: i32,
66 pub jmap_id: Option<String>,
67 pub queued_at: Option<String>,
68 pub send_after: Option<String>,
69 pub send_attempts: i32,
70 pub send_error: Option<String>,
71 }
72
73 impl EmailRow {
74 pub(super) fn from_row(row: &rusqlite::Row<'_>) -> rusqlite::Result<Self> {
75 Ok(Self {
76 id: row.get("id")?,
77 project_id: row.get("project_id")?,
78 project_name: row.get("project_name")?,
79 from_address: row.get("from_address")?,
80 to_address: row.get("to_address")?,
81 subject: row.get("subject")?,
82 body: row.get("body")?,
83 body_format: row.get("body_format")?,
84 html_body: row.get("html_body")?,
85 is_read: row.get("is_read")?,
86 is_archived: row.get("is_archived")?,
87 received_at: row.get("received_at")?,
88 message_id: row.get("message_id")?,
89 in_reply_to: row.get("in_reply_to")?,
90 thread_id: row.get("thread_id")?,
91 email_account_id: row.get("email_account_id")?,
92 is_outgoing: row.get("is_outgoing")?,
93 imap_uid: row.get("imap_uid")?,
94 source_folder: row.get("source_folder")?,
95 attachment_meta: row.get("attachment_meta")?,
96 labels: row.get("labels")?,
97 is_draft: row.get("is_draft")?,
98 cc_address: row.get("cc_address")?,
99 bcc_address: row.get("bcc_address")?,
100 draft_account_id: row.get("draft_account_id")?,
101 snoozed_until: row.get("snoozed_until")?,
102 waiting_for_response: row.get("waiting_for_response")?,
103 waiting_since: row.get("waiting_since")?,
104 expected_response_date: row.get("expected_response_date")?,
105 body_truncated: row.get("body_truncated")?,
106 jmap_id: row.get("jmap_id")?,
107 queued_at: row.get("queued_at")?,
108 send_after: row.get("send_after")?,
109 send_attempts: row.get("send_attempts")?,
110 send_error: row.get("send_error")?,
111 })
112 }
113 }
114
115 impl TryFrom<EmailRow> for Email {
116 type Error = CoreError;
117
118 fn try_from(row: EmailRow) -> std::result::Result<Self, Self::Error> {
119 Ok(Email {
120 id: parse_uuid(&row.id)?.into(),
121 project_id: parse_uuid_opt(row.project_id.as_deref())?.map(Into::into),
122 project_name: row.project_name,
123 from: row.from_address,
124 to: row.to_address,
125 subject: row.subject,
126 body: row.body,
127 body_format: BodyFormat::from_str_or_default(&row.body_format),
128 html_body: row.html_body,
129 body_truncated: row.body_truncated != 0,
130 jmap_id: row.jmap_id,
131 queued_at: row
132 .queued_at
133 .as_ref()
134 .map(|s| parse_datetime(s))
135 .transpose()?,
136 send_after: row
137 .send_after
138 .as_ref()
139 .map(|s| parse_datetime(s))
140 .transpose()?,
141 send_attempts: row.send_attempts,
142 send_error: row.send_error,
143 is_read: row.is_read != 0,
144 is_archived: row.is_archived != 0,
145 received_at: parse_datetime(&row.received_at)?,
146 message_id: row.message_id,
147 in_reply_to: row.in_reply_to,
148 thread_id: row.thread_id,
149 email_account_id: parse_uuid_opt(row.email_account_id.as_deref())?.map(Into::into),
150 is_outgoing: row.is_outgoing != 0,
151 imap_uid: row.imap_uid,
152 source_folder: row.source_folder,
153 attachment_meta: row.attachment_meta,
154 labels: serde_json::from_str(&row.labels).unwrap_or_default(),
155 is_draft: row.is_draft != 0,
156 cc_address: row.cc_address,
157 bcc_address: row.bcc_address,
158 draft_account_id: parse_uuid_opt(row.draft_account_id.as_deref())?.map(Into::into),
159 snoozed_until: row
160 .snoozed_until
161 .as_ref()
162 .map(|s| parse_datetime(s))
163 .transpose()?,
164 waiting_for_response: row.waiting_for_response != 0,
165 waiting_since: row
166 .waiting_since
167 .as_ref()
168 .map(|s| parse_datetime(s))
169 .transpose()?,
170 expected_response_date: row
171 .expected_response_date
172 .as_ref()
173 .map(|s| parse_datetime(s))
174 .transpose()?,
175 })
176 }
177 }
178