1use crate::ast::{DateBound, DateValue, FilterKind, QueryField, QueryNode, RelativeUnit, SizeOp};
20use crate::error::ParseError;
21use crate::options::ParserOptions;
22
23#[derive(Debug, Clone, PartialEq)]
26enum Token {
27 Word(String),
28 Phrase(String),
29 Colon,
30 Minus,
31 LParen,
32 RParen,
33 LBrace,
34 RBrace,
35 And,
36 Or,
37 Not,
38 Around,
39}
40
41fn tokenize(input: &str) -> Result<Vec<Token>, ParseError> {
44 let mut tokens = Vec::new();
45 let mut chars = input.chars().peekable();
46
47 while let Some(&ch) = chars.peek() {
48 match ch {
49 ' ' | '\t' | '\n' | '\r' => {
50 chars.next();
51 }
52 '(' => {
53 chars.next();
54 tokens.push(Token::LParen);
55 }
56 ')' => {
57 chars.next();
58 tokens.push(Token::RParen);
59 }
60 '{' => {
61 chars.next();
62 tokens.push(Token::LBrace);
63 }
64 '}' => {
65 chars.next();
66 tokens.push(Token::RBrace);
67 }
68 ':' => {
69 chars.next();
70 tokens.push(Token::Colon);
71 }
72 '-' => {
73 chars.next();
74 tokens.push(Token::Minus);
75 }
76 '"' => {
77 chars.next();
78 let mut s = String::new();
79 loop {
80 match chars.next() {
81 Some('\\') => match chars.next() {
82 Some('"') => s.push('"'),
84 Some(c) => {
85 s.push('\\');
86 s.push(c);
87 }
88 None => break,
89 },
90 Some('"') => break,
91 Some(c) => s.push(c),
92 None => break,
93 }
94 }
95 tokens.push(Token::Phrase(s));
96 }
97 _ => {
98 let mut word = String::new();
99 while let Some(&c) = chars.peek() {
100 if c.is_whitespace()
101 || c == '('
102 || c == ')'
103 || c == '{'
104 || c == '}'
105 || c == ':'
106 || c == '"'
107 {
108 break;
109 }
110 word.push(c);
111 chars.next();
112 }
113 if word.eq_ignore_ascii_case("AND") {
114 tokens.push(Token::And);
115 } else if word.eq_ignore_ascii_case("OR") {
116 tokens.push(Token::Or);
117 } else if word.eq_ignore_ascii_case("NOT") {
118 tokens.push(Token::Not);
119 } else if word.eq_ignore_ascii_case("AROUND") {
120 tokens.push(Token::Around);
121 } else {
122 tokens.push(Token::Word(word));
123 }
124 }
125 }
126 }
127
128 Ok(tokens)
129}
130
131struct Parser<'a> {
134 tokens: Vec<Token>,
135 pos: usize,
136 options: &'a ParserOptions,
137}
138
139impl<'a> Parser<'a> {
140 fn new(tokens: Vec<Token>, options: &'a ParserOptions) -> Self {
141 Self {
142 tokens,
143 pos: 0,
144 options,
145 }
146 }
147
148 fn peek(&self) -> Option<&Token> {
149 self.tokens.get(self.pos)
150 }
151
152 fn next(&mut self) -> Option<Token> {
153 let tok = self.tokens.get(self.pos).cloned();
154 if tok.is_some() {
155 self.pos += 1;
156 }
157 tok
158 }
159
160 fn at_end(&self) -> bool {
161 self.pos >= self.tokens.len()
162 }
163
164 fn parse_expression(&mut self) -> Result<QueryNode, ParseError> {
168 self.parse_or()
169 }
170
171 fn parse_or(&mut self) -> Result<QueryNode, ParseError> {
173 let mut left = self.parse_and()?;
174
175 while matches!(self.peek(), Some(Token::Or)) {
176 self.next();
177 let right = self.parse_and()?;
178 left = QueryNode::Or(Box::new(left), Box::new(right));
179 }
180
181 Ok(left)
182 }
183
184 fn parse_and(&mut self) -> Result<QueryNode, ParseError> {
187 let mut left = self.parse_near()?;
188
189 while !self.at_end() {
190 if matches!(self.peek(), Some(Token::RParen | Token::RBrace | Token::Or)) {
191 break;
192 }
193 if matches!(self.peek(), Some(Token::And)) {
194 self.next();
195 }
196 if self.at_end()
197 || matches!(self.peek(), Some(Token::RParen | Token::RBrace | Token::Or))
198 {
199 break;
200 }
201 let right = self.parse_near()?;
202 left = QueryNode::And(Box::new(left), Box::new(right));
203 }
204
205 Ok(left)
206 }
207
208 fn parse_near(&mut self) -> Result<QueryNode, ParseError> {
209 let mut left = self.parse_unary()?;
210
211 while matches!(self.peek(), Some(Token::Around)) {
212 self.next();
213 let distance = match self.next() {
214 Some(Token::Word(w)) => w
215 .parse::<u32>()
216 .map_err(|_| ParseError::UnexpectedToken(w.to_string()))?,
217 Some(tok) => return Err(ParseError::UnexpectedToken(format!("{:?}", tok))),
218 None => return Err(ParseError::UnexpectedEnd),
219 };
220 let right = self.parse_unary()?;
221 let left_term = near_term(&left)?;
222 let right_term = near_term(&right)?;
223 left = QueryNode::Near {
224 left: left_term,
225 right: right_term,
226 distance,
227 };
228 }
229
230 Ok(left)
231 }
232
233 fn parse_unary(&mut self) -> Result<QueryNode, ParseError> {
234 match self.peek() {
235 Some(Token::Minus) => {
236 self.next();
237 let node = self.parse_atom()?;
238 Ok(QueryNode::Not(Box::new(node)))
239 }
240 Some(Token::Not) => {
241 self.next();
242 let node = self.parse_atom()?;
243 Ok(QueryNode::Not(Box::new(node)))
244 }
245 _ => self.parse_atom(),
246 }
247 }
248
249 fn parse_atom(&mut self) -> Result<QueryNode, ParseError> {
250 match self.peek() {
251 Some(Token::LParen) => {
252 self.next();
253 let node = self.parse_expression()?;
254 match self.next() {
255 Some(Token::RParen) => Ok(node),
256 _ => Err(ParseError::UnmatchedParen),
257 }
258 }
259 Some(Token::LBrace) => self.parse_brace_group(),
260 Some(Token::Phrase(s)) => {
261 let s = s.clone();
262 self.next();
263 phrase_node(s)
264 }
265 Some(Token::Word(_)) => {
266 if self.pos + 1 < self.tokens.len()
268 && matches!(self.tokens[self.pos + 1], Token::Colon)
269 {
270 return self.parse_field_value();
271 }
272 let word = match self.next() {
273 Some(Token::Word(w)) => w,
274 _ => unreachable!(),
275 };
276 if let Some(rest) = word.strip_prefix('+') {
278 if !rest.is_empty() {
279 return Ok(QueryNode::Exact(rest.to_string()));
280 }
281 }
282 Ok(QueryNode::Text(word))
283 }
284 Some(tok) => Err(ParseError::UnexpectedToken(format!("{:?}", tok))),
285 None => Err(ParseError::UnexpectedEnd),
286 }
287 }
288
289 fn parse_brace_group(&mut self) -> Result<QueryNode, ParseError> {
295 self.next();
296 let mut node: Option<QueryNode> = None;
297
298 while !self.at_end() {
299 if matches!(self.peek(), Some(Token::RBrace)) {
300 self.next();
301 return node.ok_or(ParseError::UnexpectedEnd);
302 }
303 if matches!(self.peek(), Some(Token::And | Token::Or)) {
304 self.next();
305 continue;
306 }
307
308 let part = self.parse_unary()?;
309 node = Some(match node {
310 Some(left) => QueryNode::Or(Box::new(left), Box::new(part)),
311 None => part,
312 });
313 }
314
315 Err(ParseError::UnmatchedBrace)
316 }
317
318 fn parse_field_value(&mut self) -> Result<QueryNode, ParseError> {
319 let field_name = match self.next() {
320 Some(Token::Word(w)) => w,
321 _ => return Err(ParseError::UnexpectedEnd),
322 };
323
324 match self.next() {
325 Some(Token::Colon) => {}
326 _ => return Err(ParseError::ExpectedValue),
327 }
328
329 if matches!(self.peek(), Some(Token::LParen)) {
330 return self.parse_field_group(&field_name);
331 }
332
333 let value = match self.next() {
334 Some(Token::Word(w)) => w,
335 Some(Token::Phrase(p)) => p,
336 _ => return Err(ParseError::ExpectedValue),
337 };
338
339 self.build_field_value(&field_name, normalize_value(value))
340 }
341
342 fn parse_field_group(&mut self, field_name: &str) -> Result<QueryNode, ParseError> {
343 self.next();
344 let mut node: Option<QueryNode> = None;
345 let mut use_or = false;
346
347 while !self.at_end() {
348 match self.peek() {
349 Some(Token::RParen) => {
350 self.next();
351 return node.ok_or(ParseError::ExpectedValue);
352 }
353 Some(Token::And) => {
354 self.next();
355 use_or = false;
356 }
357 Some(Token::Or) => {
358 self.next();
359 use_or = true;
360 }
361 Some(Token::Minus | Token::Not) => {
362 self.next();
363 let value = self.next_field_group_value(field_name)?;
364 node = Some(combine_group_node(
365 node,
366 QueryNode::Not(Box::new(value)),
367 use_or,
368 ));
369 use_or = false;
370 }
371 _ => {
372 let value = self.next_field_group_value(field_name)?;
373 node = Some(combine_group_node(node, value, use_or));
374 use_or = false;
375 }
376 }
377 }
378
379 Err(ParseError::UnmatchedParen)
380 }
381
382 fn next_field_group_value(&mut self, field_name: &str) -> Result<QueryNode, ParseError> {
383 let value = match self.next() {
384 Some(Token::Word(w)) => normalize_value(w),
385 Some(Token::Phrase(p)) => normalize_value(p),
386 Some(tok) => return Err(ParseError::UnexpectedToken(format!("{:?}", tok))),
387 None => return Err(ParseError::UnexpectedEnd),
388 };
389 self.build_field_value(field_name, value)
390 }
391
392 fn build_field_value(&self, field_name: &str, value: String) -> Result<QueryNode, ParseError> {
393 match field_name.to_lowercase().as_str() {
394 "from" => Ok(QueryNode::Field {
395 field: QueryField::From,
396 value,
397 }),
398 "to" => Ok(QueryNode::Field {
399 field: QueryField::To,
400 value,
401 }),
402 "cc" => Ok(QueryNode::Field {
403 field: QueryField::Cc,
404 value,
405 }),
406 "bcc" => Ok(QueryNode::Field {
407 field: QueryField::Bcc,
408 value,
409 }),
410 "subject" => Ok(QueryNode::Field {
411 field: QueryField::Subject,
412 value,
413 }),
414 "body" => Ok(QueryNode::Field {
415 field: QueryField::Body,
416 value,
417 }),
418 "filename" => Ok(QueryNode::Field {
419 field: QueryField::Filename,
420 value,
421 }),
422 "list" => Ok(QueryNode::Field {
423 field: QueryField::List,
424 value,
425 }),
426 "deliveredto" => Ok(QueryNode::Field {
427 field: QueryField::DeliveredTo,
428 value,
429 }),
430 "rfc822msgid" => Ok(QueryNode::Field {
431 field: QueryField::Rfc822MsgId,
432 value,
433 }),
434 "label" => Ok(QueryNode::Label(value)),
435 "category" => category_label(&value)
436 .map(|label| QueryNode::Label(label.to_string()))
437 .ok_or_else(|| ParseError::UnknownFilter(value.to_lowercase())),
438 "is" => self.build_is_filter(value),
439 "in" => self.build_in_filter(value),
440 "has" => self.build_has_filter(value),
441 "size" => {
442 let (op, bytes) = parse_size_value(&value)?;
443 Ok(QueryNode::Size { op, bytes })
444 }
445 "larger" => {
446 let bytes = parse_size_bytes(&value)?;
447 Ok(QueryNode::Size {
448 op: SizeOp::GreaterThan,
449 bytes,
450 })
451 }
452 "smaller" => {
453 let bytes = parse_size_bytes(&value)?;
454 Ok(QueryNode::Size {
455 op: SizeOp::LessThan,
456 bytes,
457 })
458 }
459 "after" => Ok(QueryNode::DateRange {
460 bound: DateBound::After,
461 date: parse_date_value(&value)?,
462 }),
463 "before" => Ok(QueryNode::DateRange {
464 bound: DateBound::Before,
465 date: parse_date_value(&value)?,
466 }),
467 "date" => Ok(QueryNode::DateRange {
468 bound: DateBound::Exact,
469 date: parse_date_value(&value)?,
470 }),
471 "older" | "older_than" => Ok(QueryNode::DateRange {
472 bound: DateBound::Before,
473 date: parse_relative_duration(&value)?,
474 }),
475 "newer" | "newer_than" => Ok(QueryNode::DateRange {
476 bound: DateBound::After,
477 date: parse_relative_duration(&value)?,
478 }),
479 other => Err(ParseError::UnknownFilter(other.to_string())),
480 }
481 }
482
483 fn build_is_filter(&self, value: String) -> Result<QueryNode, ParseError> {
484 match value.to_lowercase().as_str() {
485 "unread" => Ok(QueryNode::Filter(FilterKind::Unread)),
486 "read" => Ok(QueryNode::Filter(FilterKind::Read)),
487 "starred" => Ok(QueryNode::Filter(FilterKind::Starred)),
488 "important" => Ok(QueryNode::Label("IMPORTANT".to_string())),
489 "muted" => Ok(QueryNode::Label("MUTED".to_string())),
490 "draft" | "drafts" => Ok(QueryNode::Filter(FilterKind::Draft)),
491 "sent" => Ok(QueryNode::Filter(FilterKind::Sent)),
492 "trash" | "deleted" => Ok(QueryNode::Filter(FilterKind::Trash)),
493 "spam" | "junk" => Ok(QueryNode::Filter(FilterKind::Spam)),
494 "answered" | "replied" => Ok(QueryNode::Filter(FilterKind::Answered)),
495 "inbox" => Ok(QueryNode::Filter(FilterKind::Inbox)),
496 "archived" | "archive" => Ok(QueryNode::Filter(FilterKind::Archived)),
497 other => self.resolve_custom_filter(other),
498 }
499 }
500
501 fn build_in_filter(&self, value: String) -> Result<QueryNode, ParseError> {
502 match value.to_lowercase().as_str() {
503 "inbox" => Ok(QueryNode::Filter(FilterKind::Inbox)),
504 "anywhere" | "all" | "allmail" | "all_mail" => {
505 Ok(QueryNode::Filter(FilterKind::Anywhere))
506 }
507 "draft" | "drafts" => Ok(QueryNode::Filter(FilterKind::Draft)),
508 "sent" => Ok(QueryNode::Filter(FilterKind::Sent)),
509 "trash" | "deleted" => Ok(QueryNode::Filter(FilterKind::Trash)),
510 "spam" | "junk" => Ok(QueryNode::Filter(FilterKind::Spam)),
511 "archived" | "archive" => Ok(QueryNode::Filter(FilterKind::Archived)),
512 "snoozed" => Ok(QueryNode::Label("SNOOZED".to_string())),
513 other => self.resolve_custom_filter(other),
514 }
515 }
516
517 fn build_has_filter(&self, value: String) -> Result<QueryNode, ParseError> {
518 match value.to_lowercase().as_str() {
519 "attachment" | "attachments" => Ok(QueryNode::Filter(FilterKind::HasAttachment)),
520 "calendar" | "invite" | "invites" => Ok(QueryNode::Filter(FilterKind::HasCalendar)),
521 "userlabels" => Ok(QueryNode::Filter(FilterKind::HasUserLabels)),
522 "nouserlabels" => Ok(QueryNode::Filter(FilterKind::NoUserLabels)),
523 "drive" => Ok(QueryNode::Filter(FilterKind::HasDrive)),
524 "document" => Ok(QueryNode::Filter(FilterKind::HasDocument)),
525 "spreadsheet" => Ok(QueryNode::Filter(FilterKind::HasSpreadsheet)),
526 "presentation" => Ok(QueryNode::Filter(FilterKind::HasPresentation)),
527 "youtube" => Ok(QueryNode::Filter(FilterKind::HasYoutube)),
528 "inline" | "image" | "inline-image" | "inline-images" => {
529 Ok(QueryNode::Filter(FilterKind::HasInlineImage))
530 }
531 "link" | "links" => Ok(QueryNode::Filter(FilterKind::HasLink)),
532 "link-heavy" | "links-heavy" | "linkheavy" => {
533 Ok(QueryNode::Filter(FilterKind::HasLinkHeavy))
534 }
535 "link-none" | "no-link" | "no-links" | "linkfree" | "link-free" => {
536 Ok(QueryNode::Filter(FilterKind::NoLinks))
537 }
538 "yellow-star" | "orange-star" | "red-star" | "purple-star" | "blue-star"
539 | "green-star" | "red-bang" | "orange-guillemet" | "yellow-bang" | "green-check"
540 | "blue-info" | "purple-question" => Ok(QueryNode::Filter(FilterKind::Starred)),
541 other => self.resolve_custom_filter(other),
542 }
543 }
544
545 fn resolve_custom_filter(&self, value: &str) -> Result<QueryNode, ParseError> {
550 let canonical = canonical_filter_name(value);
551 if self.options.has_custom_filter(&canonical) {
552 return Ok(QueryNode::Filter(FilterKind::Custom(canonical)));
553 }
554 Err(ParseError::UnknownFilter(value.to_string()))
555 }
556}
557
558fn combine_group_node(left: Option<QueryNode>, right: QueryNode, use_or: bool) -> QueryNode {
559 match left {
560 Some(left) if use_or => QueryNode::Or(Box::new(left), Box::new(right)),
561 Some(left) => QueryNode::And(Box::new(left), Box::new(right)),
562 None => right,
563 }
564}
565
566fn normalize_value(value: String) -> String {
569 value.strip_prefix('+').unwrap_or(&value).to_string()
570}
571
572pub(crate) fn canonical_filter_name(value: &str) -> String {
575 value.to_lowercase().replace('_', "-")
576}
577
578fn near_term(node: &QueryNode) -> Result<String, ParseError> {
579 match node {
580 QueryNode::Text(value) | QueryNode::Phrase(value) | QueryNode::Exact(value) => {
581 Ok(value.clone())
582 }
583 other => Err(ParseError::UnexpectedToken(format!("{:?}", other))),
584 }
585}
586
587fn phrase_node(value: String) -> Result<QueryNode, ParseError> {
588 let parts = value.split_whitespace().collect::<Vec<_>>();
589 if parts.len() == 4 && parts[1].eq_ignore_ascii_case("AROUND") {
590 if let Ok(distance) = parts[2].parse::<u32>() {
591 return Ok(QueryNode::Near {
592 left: parts[0].to_string(),
593 right: parts[3].to_string(),
594 distance,
595 });
596 }
597 }
598 Ok(QueryNode::Phrase(value))
599}
600
601fn category_label(value: &str) -> Option<&'static str> {
602 match value.to_lowercase().as_str() {
603 "primary" | "personal" => Some("CATEGORY_PERSONAL"),
604 "social" => Some("CATEGORY_SOCIAL"),
605 "promotions" => Some("CATEGORY_PROMOTIONS"),
606 "updates" => Some("CATEGORY_UPDATES"),
607 "forums" => Some("CATEGORY_FORUMS"),
608 "reservations" => Some("CATEGORY_RESERVATIONS"),
609 "purchases" => Some("CATEGORY_PURCHASES"),
610 _ => None,
611 }
612}
613
614fn parse_date_value(s: &str) -> Result<DateValue, ParseError> {
615 use chrono::NaiveDate;
616 match s.to_lowercase().as_str() {
617 "today" => Ok(DateValue::Today),
618 "yesterday" => Ok(DateValue::Yesterday),
619 "this-week" => Ok(DateValue::ThisWeek),
620 "this-month" => Ok(DateValue::ThisMonth),
621 _ => {
622 for format in ["%Y-%m-%d", "%Y/%m/%d", "%m/%d/%Y"] {
623 if let Ok(date) = NaiveDate::parse_from_str(s, format) {
624 return Ok(DateValue::Specific(date));
625 }
626 }
627 Err(ParseError::InvalidDate(s.to_string()))
628 }
629 }
630}
631
632fn parse_relative_duration(s: &str) -> Result<DateValue, ParseError> {
638 let input = s.trim().to_lowercase();
639 if input.len() < 2 {
640 return Err(ParseError::InvalidDate(s.to_string()));
641 }
642
643 let (amount_str, unit_str) = input.split_at(input.len() - 1);
644 let amount = amount_str
645 .parse::<u32>()
646 .map_err(|_| ParseError::InvalidDate(s.to_string()))?;
647 let unit = match unit_str {
648 "d" => RelativeUnit::Day,
649 "w" => RelativeUnit::Week,
650 "m" => RelativeUnit::Month,
651 "y" => RelativeUnit::Year,
652 _ => return Err(ParseError::InvalidDate(s.to_string())),
653 };
654 Ok(DateValue::Relative { amount, unit })
655}
656
657fn parse_size_value(s: &str) -> Result<(SizeOp, u64), ParseError> {
658 let input = s.trim().to_lowercase();
659 if input.is_empty() {
660 return Err(ParseError::InvalidSize(s.to_string()));
661 }
662
663 let (op, rest) = if let Some(rest) = input.strip_prefix(">=") {
664 (SizeOp::GreaterThanOrEqual, rest)
665 } else if let Some(rest) = input.strip_prefix("<=") {
666 (SizeOp::LessThanOrEqual, rest)
667 } else if let Some(rest) = input.strip_prefix('>') {
668 (SizeOp::GreaterThan, rest)
669 } else if let Some(rest) = input.strip_prefix('<') {
670 (SizeOp::LessThan, rest)
671 } else if let Some(rest) = input.strip_prefix('=') {
672 (SizeOp::Equal, rest)
673 } else {
674 (SizeOp::Equal, input.as_str())
675 };
676
677 let number_end = rest
678 .find(|ch: char| !ch.is_ascii_digit() && ch != '.')
679 .unwrap_or(rest.len());
680 let (number_part, unit_part) = rest.split_at(number_end);
681 if number_part.is_empty() {
682 return Err(ParseError::InvalidSize(s.to_string()));
683 }
684
685 let value = number_part
686 .parse::<f64>()
687 .map_err(|_| ParseError::InvalidSize(s.to_string()))?;
688 let multiplier = match unit_part {
689 "" | "b" => 1_f64,
690 "k" | "kb" => 1024_f64,
691 "m" | "mb" => 1024_f64 * 1024_f64,
692 "g" | "gb" => 1024_f64 * 1024_f64 * 1024_f64,
693 other => return Err(ParseError::InvalidSize(other.to_string())),
694 };
695
696 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
697 let bytes = (value * multiplier).round() as u64;
698 Ok((op, bytes))
699}
700
701fn parse_size_bytes(s: &str) -> Result<u64, ParseError> {
702 parse_size_value(s).map(|(_, bytes)| bytes)
703}
704
705pub fn parse(input: &str) -> Result<QueryNode, ParseError> {
711 parse_with(input, &ParserOptions::default())
712}
713
714pub fn parse_with(input: &str, options: &ParserOptions) -> Result<QueryNode, ParseError> {
719 let input = input.trim();
720 if input.is_empty() {
721 return Err(ParseError::UnexpectedEnd);
722 }
723 let tokens = tokenize(input)?;
724 if tokens.is_empty() {
725 return Err(ParseError::UnexpectedEnd);
726 }
727 let mut parser = Parser::new(tokens, options);
728 let node = parser.parse_expression()?;
729 if !parser.at_end() {
730 match parser.peek() {
731 Some(Token::RParen) => return Err(ParseError::UnmatchedParen),
732 Some(Token::RBrace) => return Err(ParseError::UnmatchedBrace),
733 _ => {}
734 }
735 }
736 Ok(node)
737}