mail_query/ast.rs
1//! Abstract syntax tree for parsed Gmail-style email queries.
2//!
3//! All public enums are `#[non_exhaustive]` so the crate can add new
4//! variants (for new Gmail operators) without breaking downstream
5//! pattern-matching. Callers must include a `_ => ...` arm.
6
7use chrono::NaiveDate;
8
9/// Root AST node for a parsed query.
10#[derive(Debug, Clone, PartialEq, Eq)]
11#[non_exhaustive]
12#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
13pub enum QueryNode {
14 /// A bare term. Subject to the backend's tokenizer/stemmer.
15 Text(String),
16
17 /// `+word` — exact-match form, the backend should disable stemming
18 /// for this term. New in v0.1.0; mirrors Gmail's `+word` semantics.
19 Exact(String),
20
21 /// `"quoted phrase"` — multi-word phrase.
22 Phrase(String),
23
24 /// `field:value` — e.g. `from:alice`, `subject:invoice`.
25 Field { field: QueryField, value: String },
26
27 /// `is:unread`, `has:attachment`, etc. See [`FilterKind`].
28 Filter(FilterKind),
29
30 /// `label:work`, `category:promotions` (categories normalise to
31 /// canonical `CATEGORY_*` labels).
32 Label(String),
33
34 /// `after:2024-01-01`, `older_than:5d`, `date:today`, etc. The
35 /// `Relative` variant deliberately is *not* resolved to a concrete
36 /// date at parse time — backends call
37 /// [`ParserOptions::now_provider`][crate::ParserOptions] to evaluate
38 /// it. See the README for the rationale.
39 DateRange { bound: DateBound, date: DateValue },
40
41 /// `size:>5M`, `larger:200K`, etc.
42 Size { op: SizeOp, bytes: u64 },
43
44 /// `foo AROUND 3 bar` — word proximity.
45 Near {
46 left: String,
47 right: String,
48 distance: u32,
49 },
50
51 /// Conjunction. `parse` builds left-associative trees.
52 And(Box<QueryNode>, Box<QueryNode>),
53
54 /// Disjunction. Left-associative.
55 Or(Box<QueryNode>, Box<QueryNode>),
56
57 /// `-foo` or `NOT foo`.
58 Not(Box<QueryNode>),
59}
60
61/// Built-in `field:` names. New Gmail field operators will land as
62/// additional variants here.
63#[derive(Debug, Clone, Copy, PartialEq, Eq)]
64#[non_exhaustive]
65#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
66#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
67pub enum QueryField {
68 From,
69 To,
70 Cc,
71 Bcc,
72 Subject,
73 Body,
74 Filename,
75 List,
76 DeliveredTo,
77 Rfc822MsgId,
78}
79
80/// `is:` and `has:` filter values.
81///
82/// The closed set covers Gmail-documented operators. Operators that
83/// Gmail adds over time, color-star variants beyond the common set, and
84/// caller-specific filters (e.g. application-defined `is:owed-reply`)
85/// land in [`FilterKind::Custom`].
86#[derive(Debug, Clone, PartialEq, Eq)]
87#[non_exhaustive]
88#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
89#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
90pub enum FilterKind {
91 // `is:` family
92 Unread,
93 Read,
94 Starred,
95 Draft,
96 Sent,
97 Trash,
98 Spam,
99 Answered,
100 Inbox,
101 Archived,
102 /// `in:anywhere` / `in:all` — search every folder, including spam and trash.
103 Anywhere,
104
105 // `has:` family
106 HasAttachment,
107 HasCalendar,
108 HasUserLabels,
109 NoUserLabels,
110 HasDrive,
111 HasDocument,
112 HasSpreadsheet,
113 HasPresentation,
114 HasYoutube,
115 HasInlineImage,
116 HasLink,
117 HasLinkHeavy,
118 NoLinks,
119
120 /// Escape hatch for filters not in the closed set. The carried
121 /// string is the operator value as parsed (lowercased, hyphenated
122 /// canonical form). Examples:
123 /// - Gmail's `has:reaction` → `Custom("reaction")`
124 /// - Color-star variants → `Custom("yellow-star")` etc., when the
125 /// caller has registered them via
126 /// [`ParserOptions::custom_filters`][crate::ParserOptions].
127 /// - Application-defined filters: `Custom("owed-reply")`,
128 /// `Custom("reply-later")`, etc.
129 Custom(String),
130}
131
132/// Date bound for [`QueryNode::DateRange`].
133#[derive(Debug, Clone, Copy, PartialEq, Eq)]
134#[non_exhaustive]
135#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
136#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
137pub enum DateBound {
138 /// `after:`, `newer:`, `newer_than:`.
139 After,
140 /// `before:`, `older:`, `older_than:`.
141 Before,
142 /// `date:`.
143 Exact,
144}
145
146/// Date value for [`QueryNode::DateRange`].
147///
148/// The parser does *not* resolve `Relative` against a concrete `now` —
149/// that's deliberate. A query parsed today and serialised back via
150/// [`Display`][std::fmt::Display] must mean the same thing tomorrow.
151/// Backends resolve `Relative` against
152/// [`ParserOptions::now_provider`][crate::ParserOptions] when building
153/// an executable query.
154#[derive(Debug, Clone, PartialEq, Eq)]
155#[non_exhaustive]
156#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
157pub enum DateValue {
158 Specific(NaiveDate),
159 Today,
160 Yesterday,
161 ThisWeek,
162 ThisMonth,
163 /// `older_than:5d`, `newer_than:2w`, etc. — a duration relative to
164 /// "now". Resolution happens at query-execution time.
165 Relative {
166 amount: u32,
167 unit: RelativeUnit,
168 },
169}
170
171/// Time unit for [`DateValue::Relative`].
172#[derive(Debug, Clone, Copy, PartialEq, Eq)]
173#[non_exhaustive]
174#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
175#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
176pub enum RelativeUnit {
177 Day,
178 Week,
179 Month,
180 Year,
181}
182
183/// Comparison operator for [`QueryNode::Size`].
184#[derive(Debug, Clone, Copy, PartialEq, Eq)]
185#[non_exhaustive]
186#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
187#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
188pub enum SizeOp {
189 LessThan,
190 LessThanOrEqual,
191 Equal,
192 GreaterThan,
193 GreaterThanOrEqual,
194}