2025-06-26 16:23:53 -07:00
|
|
|
use super::ContactResolverBackend;
|
|
|
|
|
use dbus::arg::{RefArg, Variant};
|
2025-08-01 12:26:17 -07:00
|
|
|
use dbus::blocking::Connection;
|
2025-06-26 16:23:53 -07:00
|
|
|
use once_cell::sync::OnceCell;
|
|
|
|
|
use std::collections::HashMap;
|
2025-06-26 18:23:15 -07:00
|
|
|
use std::sync::Mutex;
|
2025-08-01 12:26:17 -07:00
|
|
|
use std::time::Duration;
|
2025-06-26 16:23:53 -07:00
|
|
|
|
2025-06-26 18:23:15 -07:00
|
|
|
#[derive(Clone)]
|
2025-06-26 16:23:53 -07:00
|
|
|
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<String> = OnceCell::new();
|
|
|
|
|
|
2025-06-26 18:23:15 -07:00
|
|
|
/// 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<Self> {
|
|
|
|
|
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<Mutex<AddressBookHandle>> = OnceCell::new();
|
|
|
|
|
|
2025-06-26 20:44:24 -07:00
|
|
|
/// Check whether a given well-known name currently has an owner on the bus.
|
|
|
|
|
fn name_has_owner(conn: &Connection, name: &str) -> bool {
|
2025-08-01 12:26:17 -07:00
|
|
|
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(),));
|
2025-06-26 20:44:24 -07:00
|
|
|
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<std::sync::MutexGuard<'static, AddressBookHandle>> {
|
|
|
|
|
// Initialize cell if necessary.
|
|
|
|
|
let cell = ADDRESS_BOOK_HANDLE
|
2025-06-26 18:23:15 -07:00
|
|
|
.get_or_try_init(|| AddressBookHandle::new().map(Mutex::new))
|
2025-06-26 20:44:24 -07:00
|
|
|
.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);
|
|
|
|
|
}
|
2025-06-26 18:23:15 -07:00
|
|
|
}
|
|
|
|
|
|
2025-06-26 16:23:53 -07:00
|
|
|
/// 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, dbus::Error> {
|
|
|
|
|
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<String> {
|
|
|
|
|
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,): (
|
2025-08-01 12:26:17 -07:00
|
|
|
HashMap<dbus::Path<'static>, HashMap<String, HashMap<String, Variant<Box<dyn RefArg>>>>>,
|
2025-06-26 16:23:53 -07:00
|
|
|
) = 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()?;
|
2025-06-27 00:48:20 -07:00
|
|
|
if uid == "system-address-book" {
|
|
|
|
|
// Decoy.
|
|
|
|
|
return None;
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-26 16:23:53 -07:00
|
|
|
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.
|
2025-08-01 12:26:17 -07:00
|
|
|
fn open_address_book(conn: &Connection, source_uid: &str) -> anyhow::Result<(String, String)> {
|
2025-06-26 16:23:53 -07:00
|
|
|
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))
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-26 20:44:24 -07:00
|
|
|
/// 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>) {
|
2025-08-01 12:26:17 -07:00
|
|
|
let _: Result<(), _> =
|
|
|
|
|
proxy.method_call("org.gnome.evolution.dataserver.AddressBook", "Open", ());
|
2025-06-26 20:44:24 -07:00
|
|
|
}
|
|
|
|
|
|
2025-06-26 16:23:53 -07:00
|
|
|
impl ContactResolverBackend for EDSContactResolverBackend {
|
|
|
|
|
type ContactID = String;
|
|
|
|
|
|
|
|
|
|
fn resolve_contact_id(&self, address: &str) -> Option<Self::ContactID> {
|
2025-06-26 20:44:24 -07:00
|
|
|
let handle = match obtain_handle() {
|
2025-06-26 18:23:15 -07:00
|
|
|
Some(h) => h,
|
|
|
|
|
None => return None,
|
2025-06-26 16:23:53 -07:00
|
|
|
};
|
|
|
|
|
|
2025-06-26 18:23:15 -07:00
|
|
|
let address_book_proxy = handle.connection.with_proxy(
|
|
|
|
|
&handle.bus_name,
|
|
|
|
|
&handle.object_path,
|
|
|
|
|
Duration::from_secs(60),
|
|
|
|
|
);
|
2025-06-26 16:23:53 -07:00
|
|
|
|
2025-06-26 20:44:24 -07:00
|
|
|
ensure_address_book_open(&address_book_proxy);
|
|
|
|
|
|
2025-06-26 16:23:53 -07:00
|
|
|
let filter = if address.contains('@') {
|
|
|
|
|
format!("(is \"email\" \"{}\")", address)
|
|
|
|
|
} else {
|
2025-06-27 00:48:20 -07:00
|
|
|
let mut filters: Vec<String> = Vec::new();
|
|
|
|
|
filters.push(format!("(is \"phone\" \"{}\")", address));
|
|
|
|
|
|
2025-06-26 18:23:15 -07:00
|
|
|
let normalized_address = address
|
2025-06-26 18:37:23 -07:00
|
|
|
.chars()
|
|
|
|
|
.filter(|c| c.is_numeric())
|
|
|
|
|
.collect::<String>();
|
|
|
|
|
|
2025-06-27 00:48:20 -07:00
|
|
|
filters.push(format!("(is \"phone\" \"{}\")", normalized_address));
|
|
|
|
|
|
2025-06-26 18:37:23 -07:00
|
|
|
let local_address = address
|
2025-06-26 18:23:15 -07:00
|
|
|
.replace('+', "")
|
2025-06-26 16:23:53 -07:00
|
|
|
.chars()
|
|
|
|
|
.skip_while(|c| c.is_numeric() || *c == '(' || *c == ')')
|
2025-06-26 18:23:15 -07:00
|
|
|
.collect::<String>()
|
|
|
|
|
.chars()
|
2025-06-26 16:23:53 -07:00
|
|
|
.filter(|c| c.is_numeric())
|
|
|
|
|
.collect::<String>();
|
2025-06-26 18:37:23 -07:00
|
|
|
|
2025-06-27 00:48:20 -07:00
|
|
|
if !local_address.is_empty() {
|
|
|
|
|
filters.push(format!("(is \"phone\" \"{}\")", local_address));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
format!("(or {})", filters.join(" "))
|
2025-06-26 16:23:53 -07:00
|
|
|
};
|
|
|
|
|
|
2025-06-26 18:23:15 -07:00
|
|
|
log::trace!(
|
|
|
|
|
"EDS resolver: GetContactListUids filter: {}, address: {}",
|
|
|
|
|
filter,
|
|
|
|
|
address
|
|
|
|
|
);
|
|
|
|
|
|
2025-06-26 16:23:53 -07:00
|
|
|
let uids_result: Result<(Vec<String>,), _> = 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<String> {
|
2025-06-26 20:44:24 -07:00
|
|
|
let handle = match obtain_handle() {
|
2025-06-26 18:23:15 -07:00
|
|
|
Some(h) => h,
|
|
|
|
|
None => return None,
|
2025-06-26 16:23:53 -07:00
|
|
|
};
|
|
|
|
|
|
2025-06-26 18:23:15 -07:00
|
|
|
let address_book_proxy = handle.connection.with_proxy(
|
|
|
|
|
&handle.bus_name,
|
|
|
|
|
&handle.object_path,
|
|
|
|
|
Duration::from_secs(60),
|
|
|
|
|
);
|
2025-06-26 16:23:53 -07:00
|
|
|
|
2025-06-26 20:44:24 -07:00
|
|
|
ensure_address_book_open(&address_book_proxy);
|
|
|
|
|
|
2025-06-26 16:23:53 -07:00
|
|
|
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
|
|
|
|
|
}
|
2025-08-01 12:26:17 -07:00
|
|
|
}
|