Skip to main content

max / goingson

3.5 KB · 110 lines History Blame Raw
1 //! Converting an email into a task or a calendar event.
2 //!
3 //! One command per conversion, so fetching the email, resolving the sender's
4 //! contact, assembling the payload and creating are one round trip. The
5 //! derivation rules live in `goingson_core::email_convert`, where they are
6 //! unit-tested.
7
8 use std::sync::Arc;
9
10 use chrono::Utc;
11 use goingson_core::{EmailId, EmailSource, Validate, event_from_email, task_from_email};
12 use tauri::State;
13 use tracing::{instrument, warn};
14
15 use super::{ApiError, OptionNotFound};
16 use crate::commands::{EventResponse, TaskResponse};
17 use crate::state::{AppState, DESKTOP_USER_ID};
18
19 /// Resolve the sender's contact, if one exists.
20 ///
21 /// Best-effort: an unparseable `From` or a lookup failure means no contact
22 /// link, never a failed conversion. Mirrors the frontend's old swallowed
23 /// `findByEmail` call.
24 fn sender_contact_id(state: &Arc<AppState>, from: &str) -> Option<goingson_core::ContactId> {
25 let address = goingson_core::email_compose::extract_email_address(from);
26 if address.is_empty() {
27 return None;
28 }
29 match state.contacts.find_by_email(DESKTOP_USER_ID, address) {
30 Ok(contact) => contact.map(|c| c.id),
31 Err(e) => {
32 warn!("failed to resolve sender contact for {address}: {e}");
33 None
34 }
35 }
36 }
37
38 /// Creates a task from an email, linking the sender's contact when known.
39 ///
40 /// # Errors
41 ///
42 /// Returns `NOT_FOUND` if the email doesn't exist.
43 /// Returns `VALIDATION_ERROR` if the email's subject is empty, since the
44 /// subject becomes the task description.
45 /// Returns `DATABASE_ERROR` if the insert fails.
46 #[tauri::command]
47 #[instrument(skip_all)]
48 pub async fn create_task_from_email(
49 state: State<'_, Arc<AppState>>,
50 id: EmailId,
51 ) -> Result<TaskResponse, ApiError> {
52 let email = state
53 .emails
54 .get_by_id(id, DESKTOP_USER_ID)?
55 .or_not_found("email", id)?;
56
57 let contact_id = sender_contact_id(&state, &email.from);
58 let source = EmailSource {
59 id: email.id,
60 subject: &email.subject,
61 from: &email.from,
62 body: &email.body,
63 project_id: email.project_id,
64 };
65
66 let new_task = task_from_email(&source, contact_id, Utc::now());
67 new_task.validate()?;
68
69 let task = state.tasks.create(DESKTOP_USER_ID, new_task)?;
70 Ok(TaskResponse::from(task))
71 }
72
73 /// Creates a calendar event from an email, linking the sender's contact when
74 /// known. The event runs an hour from the next whole hour, since an email
75 /// carries no time of its own.
76 ///
77 /// # Errors
78 ///
79 /// Returns `NOT_FOUND` if the email doesn't exist.
80 /// Returns `VALIDATION_ERROR` if the email's subject is empty, since the
81 /// subject becomes the event title.
82 /// Returns `DATABASE_ERROR` if the insert fails.
83 #[tauri::command]
84 #[instrument(skip_all)]
85 pub async fn create_event_from_email(
86 state: State<'_, Arc<AppState>>,
87 id: EmailId,
88 ) -> Result<EventResponse, ApiError> {
89 let email = state
90 .emails
91 .get_by_id(id, DESKTOP_USER_ID)?
92 .or_not_found("email", id)?;
93
94 let contact_id = sender_contact_id(&state, &email.from);
95 let source = EmailSource {
96 id: email.id,
97 subject: &email.subject,
98 from: &email.from,
99 body: &email.body,
100 project_id: email.project_id,
101 };
102
103 let mut new_event = event_from_email(&source, contact_id, Utc::now());
104 new_event.user_id = Some(DESKTOP_USER_ID);
105 new_event.validate()?;
106
107 let event = state.events.create(DESKTOP_USER_ID, new_event)?;
108 Ok(EventResponse::from(event))
109 }
110