Skip to main content

max / goingson

7.3 KB · 183 lines History Blame Raw
1 //! Email writes that create or remove rows: plain create, backup restore, the
2 //! two IMAP-tracking inserts, and delete.
3
4 use chrono::Utc;
5 use goingson_core::{
6 CoreError, DbValue, Email, EmailId, NewEmail, NewEmailWithTracking, Result, UserId,
7 };
8 use rusqlite::{Connection, params};
9
10 use crate::utils::{execute, format_datetime, format_datetime_opt};
11
12 use super::query;
13
14 /// Insert a hand-composed email and return it as stored.
15 pub(super) fn create(conn: &Connection, user_id: UserId, email: &NewEmail) -> Result<Email> {
16 let id = EmailId::new();
17 let received_at = format_datetime(&email.received_at.unwrap_or_else(Utc::now));
18 execute(
19 conn,
20 "INSERT INTO emails (id, user_id, project_id, from_address, to_address, subject, body, is_read, received_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
21 params![
22 id.to_string(),
23 user_id.to_string(),
24 email.project_id.as_ref().map(ToString::to_string),
25 &email.from_address,
26 &email.to_address,
27 &email.subject,
28 &email.body,
29 i32::from(email.is_read),
30 &received_at
31 ],
32 )?;
33 query::get_by_id(conn, id, user_id)?
34 .ok_or_else(|| CoreError::internal("Failed to retrieve created email"))
35 }
36
37 /// Re-insert an email from a backup, preserving its original id.
38 pub(super) fn restore(conn: &Connection, user_id: UserId, email: &Email) -> Result<()> {
39 // Durable content fields are round-tripped; account-linked and
40 // sync-transient state (email_account_id, imap_uid, source_folder,
41 // attachment_meta, draft_account_id) is intentionally omitted, those
42 // FK into email_accounts (not part of a backup) or are re-derived on the
43 // next IMAP sync. Preserving the original id + message_id makes a second
44 // restore a no-op.
45 let labels_json = serde_json::to_string(&email.labels).unwrap_or_else(|_| "[]".to_string());
46 execute(
47 conn,
48 "INSERT OR IGNORE INTO emails (id, user_id, project_id, from_address, to_address, subject, body, body_format, html_body, is_read, is_archived, received_at, message_id, in_reply_to, thread_id, is_outgoing, labels, is_draft, cc_address, bcc_address, snoozed_until, waiting_for_response, waiting_since, expected_response_date) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
49 params![
50 email.id.to_string(),
51 user_id.to_string(),
52 email.project_id.map(|p| p.to_string()),
53 &email.from,
54 &email.to,
55 &email.subject,
56 &email.body,
57 email.body_format.db_value(),
58 &email.html_body,
59 i32::from(email.is_read),
60 i32::from(email.is_archived),
61 format_datetime(&email.received_at),
62 &email.message_id,
63 &email.in_reply_to,
64 &email.thread_id,
65 i32::from(email.is_outgoing),
66 &labels_json,
67 i32::from(email.is_draft),
68 &email.cc_address,
69 &email.bcc_address,
70 format_datetime_opt(email.snoozed_until),
71 i32::from(email.waiting_for_response),
72 format_datetime_opt(email.waiting_since),
73 format_datetime_opt(email.expected_response_date)
74 ],
75 )?;
76 Ok(())
77 }
78
79 /// Insert one synced message, keyed by a deterministic id from its message-id.
80 pub(super) fn create_with_tracking(
81 conn: &Connection,
82 user_id: UserId,
83 email: &NewEmailWithTracking,
84 ) -> Result<Email> {
85 let id = goingson_core::deterministic_email_id(email.message_id.as_deref());
86 let received_at = format_datetime(&email.received_at.unwrap_or_else(Utc::now));
87 execute(
88 conn,
89 "INSERT INTO emails (id, user_id, project_id, from_address, to_address, subject, body, body_format, html_body, is_read, is_archived, received_at, message_id, in_reply_to, thread_id, email_account_id, is_outgoing, imap_uid, source_folder, attachment_meta, body_truncated, jmap_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
90 params![
91 id.to_string(),
92 user_id.to_string(),
93 email.project_id.as_ref().map(ToString::to_string),
94 &email.from_address,
95 &email.to_address,
96 &email.subject,
97 &email.body,
98 email.body_format.db_value(),
99 &email.html_body,
100 i32::from(email.is_read),
101 i32::from(email.is_archived),
102 &received_at,
103 &email.message_id,
104 &email.in_reply_to,
105 &email.thread_id,
106 email.email_account_id.as_ref().map(ToString::to_string),
107 i32::from(email.is_outgoing),
108 email.imap_uid,
109 &email.source_folder,
110 &email.attachment_meta,
111 i32::from(email.body_truncated),
112 &email.jmap_id
113 ],
114 )?;
115 query::get_by_id(conn, id, user_id)?
116 .ok_or_else(|| CoreError::internal("Failed to retrieve created email"))
117 }
118
119 /// Insert a batch of synced messages in one transaction, skipping duplicates.
120 pub(super) fn create_with_tracking_batch(
121 conn: &mut Connection,
122 user_id: UserId,
123 emails: Vec<NewEmailWithTracking>,
124 ) -> Result<usize> {
125 if emails.is_empty() {
126 return Ok(0);
127 }
128
129 let mut count = 0usize;
130 let uid = user_id.to_string();
131
132 let tx = conn.transaction().map_err(CoreError::database)?;
133
134 for email in emails {
135 let id = goingson_core::deterministic_email_id(email.message_id.as_deref());
136 let received_at = format_datetime(&email.received_at.unwrap_or_else(Utc::now));
137 let result = execute(
138 &tx,
139 "INSERT OR IGNORE INTO emails (id, user_id, project_id, from_address, to_address, subject, body, body_format, html_body, is_read, is_archived, received_at, message_id, in_reply_to, thread_id, email_account_id, is_outgoing, imap_uid, source_folder, attachment_meta, body_truncated, jmap_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
140 params![
141 id.to_string(),
142 &uid,
143 email.project_id.map(|p| p.to_string()),
144 &email.from_address,
145 &email.to_address,
146 &email.subject,
147 &email.body,
148 email.body_format.db_value(),
149 &email.html_body,
150 i32::from(email.is_read),
151 i32::from(email.is_archived),
152 &received_at,
153 &email.message_id,
154 &email.in_reply_to,
155 &email.thread_id,
156 email.email_account_id.map(|a| a.to_string()),
157 i32::from(email.is_outgoing),
158 email.imap_uid,
159 &email.source_folder,
160 &email.attachment_meta,
161 i32::from(email.body_truncated),
162 &email.jmap_id
163 ],
164 )?;
165 if result > 0 {
166 count += 1;
167 }
168 }
169
170 tx.commit().map_err(CoreError::database)?;
171 Ok(count)
172 }
173
174 /// Delete one email. Returns whether a row was removed.
175 pub(super) fn delete(conn: &Connection, id: EmailId, user_id: UserId) -> Result<bool> {
176 let result = execute(
177 conn,
178 "DELETE FROM emails WHERE id = ? AND user_id = ?",
179 params![id.to_string(), user_id.to_string()],
180 )?;
181 Ok(result > 0)
182 }
183