use super::ContactResolverBackend; use dbus::arg::{RefArg, Variant}; use dbus::blocking::Connection; use once_cell::sync::OnceCell; use std::collections::HashMap; use std::sync::Mutex; use std::time::Duration; #[derive(Clone)] pub struct EDSContactResolverBackend; // Cache the UID of the default local address book so we do not have to scan // all sources over and over again. Discovering the address book requires a // D-Bus round-trip that we would rather avoid on every lookup. static ADDRESS_BOOK_SOURCE_UID: OnceCell = OnceCell::new(); /// Holds a D-Bus connection and the identifiers needed to create an address-book proxy. struct AddressBookHandle { connection: Connection, object_path: String, bus_name: String, } impl AddressBookHandle { fn new() -> anyhow::Result { let connection = new_session_connection()?; let source_uid = ensure_address_book_uid(&connection)?; let (object_path, bus_name) = open_address_book(&connection, &source_uid)?; Ok(Self { connection, object_path, bus_name, }) } } /// Obtain the global address-book handle, initialising it on the first call. static ADDRESS_BOOK_HANDLE: OnceCell> = OnceCell::new(); /// Check whether a given well-known name currently has an owner on the bus. fn name_has_owner(conn: &Connection, name: &str) -> bool { let proxy = conn.with_proxy( "org.freedesktop.DBus", "/org/freedesktop/DBus", Duration::from_secs(2), ); let result: Result<(bool,), _> = proxy.method_call("org.freedesktop.DBus", "NameHasOwner", (name.to_string(),)); result.map(|(b,)| b).unwrap_or(false) } /// Returns a fresh handle, ensuring the cached one is still valid. If the backend owning the /// address-book disappeared, the cache is cleared and we try to create a new handle. fn obtain_handle() -> Option> { // Initialize cell if necessary. let cell = ADDRESS_BOOK_HANDLE .get_or_try_init(|| AddressBookHandle::new().map(Mutex::new)) .ok()?; // Validate existing handle. { let mut guard = cell.lock().ok()?; if !name_has_owner(&guard.connection, &guard.bus_name) { // Try to refresh the handle in-place. match AddressBookHandle::new() { Ok(new_h) => { *guard = new_h; } Err(e) => { log::debug!("EDS resolver: failed to refresh address book handle: {}", e); // keep the stale handle but report failure return None; } } } // Return guard after ensuring validity. return Some(guard); } } /// Helper that returns a blocking D-Bus session connection. Creating the /// connection is cheap (<1 ms) but we still keep it around because the /// underlying socket is re-used by the dbus crate. fn new_session_connection() -> Result { Connection::new_session() } /// Scan Evolution-Data-Server sources to find a suitable address-book source /// UID. The implementation mirrors what `gdbus introspect` reveals for the /// EDS interfaces. We search all `org.gnome.evolution.dataserver.Source` /// objects and pick the first one that advertises the `[Address Book]` section /// with a `BackendName=` entry in its INI-style `Data` property. fn ensure_address_book_uid(conn: &Connection) -> anyhow::Result { if let Some(uid) = ADDRESS_BOOK_SOURCE_UID.get() { return Ok(uid.clone()); } let source_manager_proxy = conn.with_proxy( "org.gnome.evolution.dataserver.Sources5", "/org/gnome/evolution/dataserver/SourceManager", Duration::from_secs(5), ); // The GetManagedObjects reply is the usual ObjectManager map. let (managed_objects,): ( HashMap, HashMap>>>>, ) = source_manager_proxy.method_call( "org.freedesktop.DBus.ObjectManager", "GetManagedObjects", (), )?; let uid = managed_objects .values() .filter_map(|ifaces| ifaces.get("org.gnome.evolution.dataserver.Source")) .filter_map(|props| { let uid = props.get("UID")?.as_str()?; if uid == "system-address-book" { // Decoy. return None; } let data = props.get("Data")?.as_str()?; if data_contains_address_book_backend(data) { Some(uid.to_owned()) } else { None } }) .next() .ok_or_else(|| anyhow::anyhow!("No address book source found"))?; // Remember for future look-ups. log::debug!("EDS resolver: found address book source UID: {}", uid); let _ = ADDRESS_BOOK_SOURCE_UID.set(uid.clone()); Ok(uid) } fn data_contains_address_book_backend(data: &str) -> bool { let mut in_address_book_section = false; for line in data.lines() { let trimmed = line.trim(); if trimmed.starts_with('[') && trimmed.ends_with(']') { in_address_book_section = trimmed == "[Address Book]"; continue; } if in_address_book_section && trimmed.starts_with("BackendName=") { return true; } } false } /// Open the Evolution address book referenced by `source_uid` and return the /// pair `(object_path, bus_name)` that identifies the newly created D-Bus /// proxy. fn open_address_book(conn: &Connection, source_uid: &str) -> anyhow::Result<(String, String)> { let factory_proxy = conn.with_proxy( "org.gnome.evolution.dataserver.AddressBook10", "/org/gnome/evolution/dataserver/AddressBookFactory", Duration::from_secs(60), ); let (object_path, bus_name): (String, String) = factory_proxy.method_call( "org.gnome.evolution.dataserver.AddressBookFactory", "OpenAddressBook", (source_uid.to_owned(),), )?; Ok((object_path, bus_name)) } /// Ensure that the backend for the given address-book proxy is opened. /// Evolution-Data-Server returns "Backend is not opened yet" until someone /// calls the `Open` method once per process. We ignore any error here /// because the backend might already be open. fn ensure_address_book_open(proxy: &dbus::blocking::Proxy<&Connection>) { let _: Result<(), _> = proxy.method_call("org.gnome.evolution.dataserver.AddressBook", "Open", ()); } impl ContactResolverBackend for EDSContactResolverBackend { type ContactID = String; fn resolve_contact_id(&self, address: &str) -> Option { let handle = match obtain_handle() { Some(h) => h, None => return None, }; let address_book_proxy = handle.connection.with_proxy( &handle.bus_name, &handle.object_path, Duration::from_secs(60), ); ensure_address_book_open(&address_book_proxy); let filter = if address.contains('@') { format!("(is \"email\" \"{}\")", address) } else { let mut filters: Vec = Vec::new(); filters.push(format!("(is \"phone\" \"{}\")", address)); let normalized_address = address .chars() .filter(|c| c.is_numeric()) .collect::(); filters.push(format!("(is \"phone\" \"{}\")", normalized_address)); let local_address = address .replace('+', "") .chars() .skip_while(|c| c.is_numeric() || *c == '(' || *c == ')') .collect::() .chars() .filter(|c| c.is_numeric()) .collect::(); if !local_address.is_empty() { filters.push(format!("(is \"phone\" \"{}\")", local_address)); } format!("(or {})", filters.join(" ")) }; log::trace!( "EDS resolver: GetContactListUids filter: {}, address: {}", filter, address ); let uids_result: Result<(Vec,), _> = address_book_proxy.method_call( "org.gnome.evolution.dataserver.AddressBook", "GetContactListUids", (filter,), ); let (uids,) = match uids_result { Ok(v) => v, Err(e) => { log::debug!("EDS resolver: GetContactListUids failed: {}", e); return None; } }; uids.into_iter().next() } fn get_contact_display_name(&self, contact_id: &Self::ContactID) -> Option { let handle = match obtain_handle() { Some(h) => h, None => return None, }; let address_book_proxy = handle.connection.with_proxy( &handle.bus_name, &handle.object_path, Duration::from_secs(60), ); ensure_address_book_open(&address_book_proxy); let vcard_result: Result<(String,), _> = address_book_proxy.method_call( "org.gnome.evolution.dataserver.AddressBook", "GetContact", (contact_id.clone(),), ); let (vcard,) = match vcard_result { Ok(v) => v, Err(e) => { log::debug!("EDS resolver: GetContact failed: {}", e); return None; } }; for line in vcard.lines() { if let Some(rest) = line.strip_prefix("FN:") { return Some(rest.to_string()); } } None } } impl Default for EDSContactResolverBackend { fn default() -> Self { Self } }