Started working on contact resolution
This commit is contained in:
229
kordophoned/src/daemon/contact_resolver/eds.rs
Normal file
229
kordophoned/src/daemon/contact_resolver/eds.rs
Normal file
@@ -0,0 +1,229 @@
|
||||
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;
|
||||
|
||||
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();
|
||||
|
||||
/// 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> {
|
||||
// Only email addresses are supported for now. We fall back to NONE on
|
||||
// any error to keep the resolver infallible for callers.
|
||||
let conn = match new_session_connection() {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
log::debug!("EDS resolver: failed to open session D-Bus: {}", e);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
let source_uid = match ensure_address_book_uid(&conn) {
|
||||
Ok(u) => u,
|
||||
Err(e) => {
|
||||
log::debug!("EDS resolver: could not determine address-book UID: {}", e);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
let (object_path, bus_name) = match open_address_book(&conn, &source_uid) {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
log::debug!("EDS resolver: failed to open address book: {}", e);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
let address_book_proxy = conn.with_proxy(bus_name, object_path, Duration::from_secs(60));
|
||||
|
||||
let filter = if address.contains('@') {
|
||||
format!("(is \"email\" \"{}\")", address)
|
||||
} else {
|
||||
// Remove country code, if present
|
||||
let address = address.replace("+", "")
|
||||
.chars()
|
||||
.skip_while(|c| c.is_numeric() || *c == '(' || *c == ')')
|
||||
.collect::<String>();
|
||||
|
||||
// Remove any remaining non-numeric characters
|
||||
let address = address.chars()
|
||||
.filter(|c| c.is_numeric())
|
||||
.collect::<String>();
|
||||
|
||||
format!("(is \"phone\" \"{}\")", 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 conn = match new_session_connection() {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
log::debug!("EDS resolver: failed to open session D-Bus: {}", e);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
let source_uid = match ensure_address_book_uid(&conn) {
|
||||
Ok(u) => u,
|
||||
Err(e) => {
|
||||
log::debug!("EDS resolver: could not determine address-book UID: {}", e);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
let (object_path, bus_name) = match open_address_book(&conn, &source_uid) {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
log::debug!("EDS resolver: failed to open address book: {}", e);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
let address_book_proxy = conn.with_proxy(bus_name, 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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user