Private
Public Access
1
0
Files
Kordophone/kordophoned/src/daemon/contact_resolver/eds.rs

244 lines
7.7 KiB
Rust

use super::ContactResolverBackend;
use dbus::blocking::Connection;
use dbus::arg::{RefArg, Variant};
use once_cell::sync::OnceCell;
use std::collections::HashMap;
use std::time::Duration;
use std::sync::Mutex;
use std::thread;
#[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<String> = 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<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();
fn get_address_book_handle() -> Option<&'static Mutex<AddressBookHandle>> {
ADDRESS_BOOK_HANDLE
.get_or_try_init(|| AddressBookHandle::new().map(Mutex::new))
.map_err(|e| {
log::debug!(
"EDS resolver: failed to initialise address book handle: {}",
e
);
})
.ok()
}
/// 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,): (
HashMap<
dbus::Path<'static>,
HashMap<String, HashMap<String, Variant<Box<dyn RefArg>>>>,
>,
) = 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()?;
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))
}
impl ContactResolverBackend for EDSContactResolverBackend {
type ContactID = String;
fn resolve_contact_id(&self, address: &str) -> Option<Self::ContactID> {
let handle_mutex = match get_address_book_handle() {
Some(h) => h,
None => return None,
};
let handle = handle_mutex.lock().unwrap();
let address_book_proxy = handle.connection.with_proxy(
&handle.bus_name,
&handle.object_path,
Duration::from_secs(60),
);
let filter = if address.contains('@') {
format!("(is \"email\" \"{}\")", address)
} else {
let normalized_address = address
.replace('+', "")
.chars()
.skip_while(|c| c.is_numeric() || *c == '(' || *c == ')')
.collect::<String>()
.chars()
.filter(|c| c.is_numeric())
.collect::<String>();
format!(
"(or (is \"phone\" \"{}\") (is \"phone\" \"{}\") )",
address, normalized_address
)
};
log::trace!(
"EDS resolver: GetContactListUids filter: {}, address: {}",
filter,
address
);
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> {
let handle_mutex = match get_address_book_handle() {
Some(h) => h,
None => return None,
};
let handle = handle_mutex.lock().unwrap();
let address_book_proxy = handle.connection.with_proxy(
&handle.bus_name,
&handle.object_path,
Duration::from_secs(60),
);
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
}
}