Skip to main content

max / goingson

3.5 KB · 100 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::{AppState, DESKTOP_USER_ID};
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_core::{NewContact, NewContactEmail};
16 use goingson_db_sqlite::utils::is_valid_email;
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
45 .split('@')
46 .next()
47 .unwrap_or(&addr)
48 .replace(['.', '_'], " ")
49 .split_whitespace()
50 .map(|w| {
51 let mut c = w.chars();
52 match c.next() {
53 None => String::new(),
54 Some(f) => f.to_uppercase().to_string() + c.as_str(),
55 }
56 })
57 .collect::<Vec<_>>()
58 .join(" ");
59
60 let new_contact = NewContact {
61 display_name,
62 nickname: None,
63 company: None,
64 title: None,
65 notes: String::new(),
66 tags: vec![],
67 birthday: None,
68 timezone: None,
69 is_implicit: true,
70 };
71
72 match state.contacts.create(DESKTOP_USER_ID, new_contact).await {
73 Ok(contact) => {
74 let email_entry = NewContactEmail {
75 address: addr.clone(),
76 label: String::new(),
77 is_primary: true,
78 };
79 if let Err(e) = state
80 .contacts
81 .add_email(contact.id, DESKTOP_USER_ID, email_entry)
82 .await
83 {
84 tracing::warn!("Failed to add email to implicit contact: {}", e);
85 }
86 // Re-fetch to get hydrated contact with email sub-collection
87 match state.contacts.get_by_id(contact.id, DESKTOP_USER_ID).await {
88 Ok(Some(c)) => new_contacts.push(crate::commands::ContactResponse::from(c)),
89 _ => new_contacts.push(crate::commands::ContactResponse::from(contact)),
90 }
91 }
92 Err(e) => {
93 tracing::warn!("Failed to create implicit contact for {}: {}", addr, e);
94 }
95 }
96 }
97
98 new_contacts
99 }
100