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(¶ms.query),
82 page: None,
83 refine_from: optional(¶ms.from),
84 refine_to: optional(¶ms.to),
85 refine_subject: optional(¶ms.subject),
86 refine_exact_phrase: optional(¶ms.exact_phrase),
87 refine_required: optional(¶ms.required),
88 refine_any: optional(¶ms.any),
89 refine_none: optional(¶ms.none),
90 refine_date: optional(¶ms.date),
91 refine_in: optional(¶ms.r#in),
92 refine_label: optional(¶ms.label),
93 refine_attachment: optional(¶ms.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}