Skip to main content

hey_sdk/services/
search.rs

1//! Searching mail.
2//!
3//! HEY has no JSON search endpoint — `/search` and `/advanced_search` render HTML — so
4//! results are read off the advanced search page. Only the refine options are JSON, through
5//! [`Search::get_advanced_filters`].
6
7use crate::error::Error;
8use crate::generated::types::AdvancedSearchResult;
9
10pub use crate::generated::services::search::*;
11
12/// An advanced search. `query` is the free-text part; the rest map onto the `refine[...]`
13/// parameters the advanced search form submits. An empty refinement is left off the wire.
14#[derive(Debug, Clone, Default, PartialEq)]
15pub struct SearchParams {
16    /// The words to search for.
17    pub query: String,
18    /// The 1-based results page. Zero and one both ask for the first.
19    pub page: u32,
20    /// Words that must all appear.
21    pub required: String,
22    /// Words of which at least one must appear.
23    pub any: String,
24    /// Words that must not appear.
25    pub none: String,
26    /// Text that must appear verbatim.
27    pub exact_phrase: String,
28    /// Narrows by sender.
29    pub from: String,
30    /// Narrows by recipient.
31    pub to: String,
32    /// Narrows by subject line.
33    pub subject: String,
34    /// `last_7_days`, `last_30_days`, `last_90_days` or a four-digit year.
35    pub date: String,
36    /// Narrows to a box: `imbox`, `feed`, `papertrail` or `trash`.
37    pub r#in: String,
38    /// Narrows to a folder name.
39    pub label: String,
40    /// Narrows by attachment kind, or `any`.
41    pub attachment: String,
42}
43
44/// One page of matches and the number of the page after it.
45///
46/// Search numbers its pages rather than cursoring them, so the next page is read by passing
47/// that number back as [`SearchParams::page`]. It is `None` on the last page: HEY only sends
48/// the `Link` header while there is more to read, which is how a caller walking the results
49/// is told to stop asking rather than having to ask for a page that turns out empty.
50#[derive(Debug, Clone, Default, PartialEq)]
51#[non_exhaustive]
52pub struct SearchResults {
53    /// The matches, grouped by topic.
54    pub result: AdvancedSearchResult,
55    /// The number of the page after this one, while there is one.
56    pub next_page: Option<u32>,
57}
58
59impl Search<'_> {
60    /// Runs an advanced search and answers the matching threads, grouped by topic as the
61    /// search page shows them: the topic, your posting of it, and the entries that matched
62    /// as summaries — read a message with
63    /// [`Messages::get`](crate::services::messages::Messages::get).
64    pub async fn search(&self, params: &SearchParams) -> Result<AdvancedSearchResult, Error> {
65        Ok(self.search_page(params).await?.result)
66    }
67
68    /// Runs the same search as [`Search::search`] and also answers which page comes next.
69    pub async fn search_page(&self, params: &SearchParams) -> Result<SearchResults, Error> {
70        let page = self.advanced(&refinements(params)).await?;
71        let next_page = page.next_page().and_then(|page| page.parse().ok());
72        Ok(SearchResults {
73            result: page.into_inner(),
74            next_page,
75        })
76    }
77}
78
79fn refinements(params: &SearchParams) -> AdvancedSearchParams {
80    let mut advanced = AdvancedSearchParams {
81        q: optional(&params.query),
82        page: None,
83        refine_from: optional(&params.from),
84        refine_to: optional(&params.to),
85        refine_subject: optional(&params.subject),
86        refine_exact_phrase: optional(&params.exact_phrase),
87        refine_required: optional(&params.required),
88        refine_any: optional(&params.any),
89        refine_none: optional(&params.none),
90        refine_date: optional(&params.date),
91        refine_in: optional(&params.r#in),
92        refine_label: optional(&params.label),
93        refine_attachment: optional(&params.attachment),
94    };
95    if params.page > 1 {
96        advanced.page = Some(params.page.to_string());
97    }
98    advanced
99}
100
101fn optional(value: &str) -> Option<String> {
102    if value.is_empty() {
103        None
104    } else {
105        Some(value.to_string())
106    }
107}