Skip to main content

pidge_client/graph/
people.rs

1//! GET /me/people: Outlook's ranked "people I interact with" list, used to
2//! resolve a display name or partial address to a full e-mail address.
3
4use serde::Deserialize;
5
6use crate::error::ClientError;
7
8/// A person from `/me/people`, with the first scored e-mail address Graph
9/// returned (entries without any scored address are skipped entirely).
10#[derive(Debug, Clone, PartialEq, Eq)]
11pub struct Person {
12    pub display_name: String,
13    pub address: String,
14}
15
16#[derive(Deserialize)]
17struct GraphPeopleList {
18    value: Vec<GraphPerson>,
19}
20
21#[derive(Deserialize)]
22struct GraphPerson {
23    #[serde(rename = "displayName", default)]
24    display_name: Option<String>,
25    #[serde(rename = "scoredEmailAddresses", default)]
26    scored_email_addresses: Vec<GraphScoredEmailAddress>,
27}
28
29#[derive(Deserialize)]
30struct GraphScoredEmailAddress {
31    address: String,
32}
33
34/// GET /me/people?$top={top}&$select=displayName,scoredEmailAddresses
35pub async fn list_people(
36    http: &reqwest::Client,
37    base_url: &str,
38    access_token: &str,
39    top: usize,
40) -> Result<Vec<Person>, ClientError> {
41    let url = format!("{base_url}/me/people");
42    let resp = super::send_with_retry(http.get(&url).bearer_auth(access_token).query(&[
43        ("$top", top.to_string()),
44        ("$select", "displayName,scoredEmailAddresses".to_string()),
45    ]))
46    .await?;
47    let status = resp.status();
48    if !status.is_success() {
49        let text = resp.text().await.unwrap_or_default();
50        return Err(ClientError::Graph {
51            status: status.as_u16(),
52            message: text,
53        });
54    }
55    let list: GraphPeopleList = resp.json().await?;
56    Ok(list
57        .value
58        .into_iter()
59        .filter_map(|p| {
60            let address = p.scored_email_addresses.into_iter().next()?.address;
61            Some(Person {
62                display_name: super::mail::clean(p.display_name),
63                address,
64            })
65        })
66        .collect())
67}
68
69#[cfg(test)]
70mod tests {
71    use super::*;
72    use wiremock::matchers::{method, path, query_param};
73    use wiremock::{Mock, MockServer, ResponseTemplate};
74
75    #[tokio::test]
76    async fn list_people_maps_one_person() {
77        let server = MockServer::start().await;
78        Mock::given(method("GET"))
79            .and(path("/me/people"))
80            .and(query_param("$top", "10"))
81            .and(query_param("$select", "displayName,scoredEmailAddresses"))
82            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
83                "value": [
84                    {
85                        "displayName": "Anna",
86                        "scoredEmailAddresses": [{"address": "anna@example.com"}]
87                    }
88                ]
89            })))
90            .mount(&server)
91            .await;
92        let http = reqwest::Client::new();
93        let people = list_people(&http, &server.uri(), "AT", 10).await.unwrap();
94        assert_eq!(people.len(), 1);
95        assert_eq!(people[0].display_name, "Anna");
96        assert_eq!(people[0].address, "anna@example.com");
97    }
98
99    #[tokio::test]
100    async fn list_people_skips_entries_without_a_scored_address() {
101        let server = MockServer::start().await;
102        Mock::given(method("GET"))
103            .and(path("/me/people"))
104            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
105                "value": [
106                    { "displayName": "No Address", "scoredEmailAddresses": [] },
107                    {
108                        "displayName": "Anna",
109                        "scoredEmailAddresses": [
110                            {"address": "anna@example.com"},
111                            {"address": "anna.alt@example.com"}
112                        ]
113                    }
114                ]
115            })))
116            .mount(&server)
117            .await;
118        let http = reqwest::Client::new();
119        let people = list_people(&http, &server.uri(), "AT", 10).await.unwrap();
120        assert_eq!(people.len(), 1);
121        assert_eq!(people[0].address, "anna@example.com");
122    }
123}