Skip to main content

max / goingson

3.4 KB · 93 lines History Blame Raw
1 //! Contact side-effects of sending: create implicit contacts for recipients
2 //! that don't already have one. Best-effort — never fails the send.
3
4 use super::*;
5
6 /// Create implicit contacts for recipients that don't have an existing contact.
7 /// Errors are logged and swallowed — never fails the send.
8 pub(super) async fn create_implicit_contacts(
9 state: &std::sync::Arc<AppState>,
10 to: &str,
11 cc: Option<&str>,
12 bcc: Option<&str>,
13 sender_email: &str,
14 ) -> Vec<crate::commands::ContactResponse> {
15 use goingson_db_sqlite::utils::is_valid_email;
16 use goingson_core::{NewContact, NewContactEmail};
17 use std::collections::HashSet;
18
19 // Collect all unique recipient addresses
20 let mut addresses = HashSet::new();
21 for field in [Some(to), cc, bcc].into_iter().flatten() {
22 for addr in field.split(',').map(str::trim).filter(|a| !a.is_empty()) {
23 let lower = addr.to_lowercase();
24 if lower != sender_email.to_lowercase() && is_valid_email(addr) {
25 addresses.insert((addr.to_string(), lower));
26 }
27 }
28 }
29
30 let mut new_contacts = Vec::new();
31
32 for (addr, _lower) in addresses {
33 // Check if a contact already exists for this address
34 match state.contacts.find_by_email(DESKTOP_USER_ID, &addr).await {
35 Ok(Some(_)) => continue, // already exists
36 Ok(None) => {} // proceed to create
37 Err(e) => {
38 tracing::warn!("Failed to check contact for {}: {}", addr, e);
39 continue;
40 }
41 }
42
43 // Derive display name from the local part of the email address
44 let display_name = addr.split('@').next().unwrap_or(&addr)
45 .replace(['.', '_'], " ")
46 .split_whitespace()
47 .map(|w| {
48 let mut c = w.chars();
49 match c.next() {
50 None => String::new(),
51 Some(f) => f.to_uppercase().to_string() + c.as_str(),
52 }
53 })
54 .collect::<Vec<_>>()
55 .join(" ");
56
57 let new_contact = NewContact {
58 display_name,
59 nickname: None,
60 company: None,
61 title: None,
62 notes: String::new(),
63 tags: vec![],
64 birthday: None,
65 timezone: None,
66 is_implicit: true,
67 };
68
69 match state.contacts.create(DESKTOP_USER_ID, new_contact).await {
70 Ok(contact) => {
71 let email_entry = NewContactEmail {
72 address: addr.clone(),
73 label: String::new(),
74 is_primary: true,
75 };
76 if let Err(e) = state.contacts.add_email(contact.id, DESKTOP_USER_ID, email_entry).await {
77 tracing::warn!("Failed to add email to implicit contact: {}", e);
78 }
79 // Re-fetch to get hydrated contact with email sub-collection
80 match state.contacts.get_by_id(contact.id, DESKTOP_USER_ID).await {
81 Ok(Some(c)) => new_contacts.push(crate::commands::ContactResponse::from(c)),
82 _ => new_contacts.push(crate::commands::ContactResponse::from(contact)),
83 }
84 }
85 Err(e) => {
86 tracing::warn!("Failed to create implicit contact for {}: {}", addr, e);
87 }
88 }
89 }
90
91 new_contacts
92 }
93