//! Converting an email into a task or a calendar event. //! //! One command per conversion, so fetching the email, resolving the sender's //! contact, assembling the payload and creating are one round trip. The //! derivation rules live in `goingson_core::email_convert`, where they are //! unit-tested. use std::sync::Arc; use chrono::Utc; use goingson_core::{EmailId, EmailSource, Validate, event_from_email, task_from_email}; use tauri::State; use tracing::{instrument, warn}; use super::{ApiError, OptionNotFound}; use crate::commands::{EventResponse, TaskResponse}; use crate::state::{AppState, DESKTOP_USER_ID}; /// Resolve the sender's contact, if one exists. /// /// Best-effort: an unparseable `From` or a lookup failure means no contact /// link, never a failed conversion. Mirrors the frontend's old swallowed /// `findByEmail` call. fn sender_contact_id(state: &Arc, from: &str) -> Option { let address = goingson_core::email_compose::extract_email_address(from); if address.is_empty() { return None; } match state.contacts.find_by_email(DESKTOP_USER_ID, address) { Ok(contact) => contact.map(|c| c.id), Err(e) => { warn!("failed to resolve sender contact for {address}: {e}"); None } } } /// Creates a task from an email, linking the sender's contact when known. /// /// # Errors /// /// Returns `NOT_FOUND` if the email doesn't exist. /// Returns `VALIDATION_ERROR` if the email's subject is empty, since the /// subject becomes the task description. /// Returns `DATABASE_ERROR` if the insert fails. #[tauri::command] #[instrument(skip_all)] pub async fn create_task_from_email( state: State<'_, Arc>, id: EmailId, ) -> Result { let email = state .emails .get_by_id(id, DESKTOP_USER_ID)? .or_not_found("email", id)?; let contact_id = sender_contact_id(&state, &email.from); let source = EmailSource { id: email.id, subject: &email.subject, from: &email.from, body: &email.body, project_id: email.project_id, }; let new_task = task_from_email(&source, contact_id, Utc::now()); new_task.validate()?; let task = state.tasks.create(DESKTOP_USER_ID, new_task)?; Ok(TaskResponse::from(task)) } /// Creates a calendar event from an email, linking the sender's contact when /// known. The event runs an hour from the next whole hour, since an email /// carries no time of its own. /// /// # Errors /// /// Returns `NOT_FOUND` if the email doesn't exist. /// Returns `VALIDATION_ERROR` if the email's subject is empty, since the /// subject becomes the event title. /// Returns `DATABASE_ERROR` if the insert fails. #[tauri::command] #[instrument(skip_all)] pub async fn create_event_from_email( state: State<'_, Arc>, id: EmailId, ) -> Result { let email = state .emails .get_by_id(id, DESKTOP_USER_ID)? .or_not_found("email", id)?; let contact_id = sender_contact_id(&state, &email.from); let source = EmailSource { id: email.id, subject: &email.subject, from: &email.from, body: &email.body, project_id: email.project_id, }; let mut new_event = event_from_email(&source, contact_id, Utc::now()); new_event.user_id = Some(DESKTOP_USER_ID); new_event.validate()?; let event = state.events.create(DESKTOP_USER_ID, new_event)?; Ok(EventResponse::from(event)) }