Skip to main content

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}