1use chrono::{Datelike, Local, TimeZone};
15
16use crate::message::{self, Envelope};
17
18#[derive(Debug, Clone)]
21pub struct Matcher {
22 raw: String,
23 re: Option<regex_lite::Regex>,
24}
25
26impl Matcher {
27 pub fn new(raw: &str) -> Matcher {
28 Matcher {
29 raw: raw.to_string(),
30 re: regex_lite::Regex::new(&format!("(?i){raw}")).ok(),
31 }
32 }
33
34 pub fn raw(&self) -> &str {
36 &self.raw
37 }
38
39 pub fn is_match(&self, text: &str) -> bool {
40 match &self.re {
41 Some(re) => re.is_match(text),
42 None => text.to_lowercase().contains(&self.raw.to_lowercase()),
43 }
44 }
45
46 pub fn find_ranges(&self, text: &str) -> Vec<(usize, usize)> {
50 if let Some(re) = &self.re {
51 return re.find_iter(text).map(|m| (m.start(), m.end())).collect();
52 }
53 let hay = text.to_ascii_lowercase();
54 let needle = self.raw.to_ascii_lowercase();
55 if needle.is_empty() {
56 return Vec::new();
57 }
58 let mut out = Vec::new();
59 let mut from = 0;
60 while let Some(pos) = hay[from..].find(&needle) {
61 let start = from + pos;
62 out.push((start, start + needle.len()));
63 from = start + needle.len();
64 }
65 out
66 }
67}
68
69impl PartialEq for Matcher {
70 fn eq(&self, other: &Self) -> bool {
71 self.raw == other.raw
72 }
73}
74impl Eq for Matcher {}
75
76#[derive(Debug, Clone, PartialEq, Eq)]
77pub enum Pattern {
78 All(Vec<Pattern>),
79 Any(Vec<Pattern>),
80 Not(Box<Pattern>),
81 From(Matcher),
82 Subject(Matcher),
83 Body(Matcher),
84 To(Matcher),
85 Cc(Matcher),
86 Recipient(Matcher),
88 Sender(Matcher),
90 Header(Matcher),
92 MessageId(Matcher),
94 References(Matcher),
96 Whole(Matcher),
98 Label(Matcher),
100 FromOrTo(Matcher),
102 Read,
104 Old,
106 Replied,
108 Subscribed,
110 Thread(Box<Pattern>),
112 Parent(Box<Pattern>),
114 Child(Box<Pattern>),
116 Collapsed,
118 Unreferenced,
120 Date {
122 min: Option<i64>,
123 max: Option<i64>,
124 },
125 Received {
127 min: Option<i64>,
128 max: Option<i64>,
129 },
130 Number {
133 min: Option<Bound>,
134 max: Option<Bound>,
135 },
136 Size {
138 min: Option<u64>,
139 max: Option<u64>,
140 },
141 Duplicate,
143 New,
144 Flagged,
145 Deleted,
146 Unread,
147 Tagged,
148 ToMe,
150 FromMe,
152 ToList,
154 Default(Matcher),
156}
157
158#[derive(Debug, Clone, Copy, PartialEq, Eq)]
161pub enum Bound {
162 Num(usize),
163 Current,
164 Last,
165}
166
167impl Bound {
168 fn resolve(self, pos: &Position) -> usize {
169 match self {
170 Bound::Num(n) => n,
171 Bound::Current => pos.current,
172 Bound::Last => pos.last,
173 }
174 }
175}
176
177#[derive(Default, Clone, Copy)]
183pub struct Me<'a> {
184 pub addresses: &'a [String],
186 pub alternates: &'a [Matcher],
188}
189
190impl<'a> Me<'a> {
191 pub fn new(addresses: &'a [String], alternates: &'a [Matcher]) -> Me<'a> {
192 Me {
193 addresses,
194 alternates,
195 }
196 }
197
198 pub fn addresses(addresses: &'a [String]) -> Me<'a> {
201 Me {
202 addresses,
203 alternates: &[],
204 }
205 }
206
207 pub fn is_me(&self, addr: &str) -> bool {
208 let bare = addr.trim().to_lowercase();
209 self.addresses.contains(&bare) || self.alternates.iter().any(|m| m.is_match(&bare))
210 }
211
212 pub fn any<'b>(&self, addresses: impl IntoIterator<Item = &'b String>) -> bool {
214 addresses.into_iter().any(|a| self.is_me(a))
215 }
216
217 pub fn wrote(&self, from_header: &str) -> bool {
219 crate::compose::addresses(from_header)
220 .iter()
221 .any(|a| self.is_me(a))
222 }
223
224 pub fn is_empty(&self) -> bool {
225 self.addresses.is_empty() && self.alternates.is_empty()
226 }
227}
228
229#[derive(Default, Clone, Copy)]
233pub struct Scope<'a> {
234 pub me: Me<'a>,
235 pub lists: &'a [Matcher],
237 pub subscribed: &'a [Matcher],
239 pub position: Position,
240 pub thread: Option<ThreadView<'a>>,
244}
245
246#[derive(Clone, Copy)]
248pub struct ThreadView<'a> {
249 pub members: &'a [usize],
250 pub parent: Option<usize>,
251 pub children: &'a [usize],
252 pub collapsed: bool,
253 pub envs: &'a dyn EnvSource,
254}
255
256pub trait EnvSource {
259 fn envelope(&self, i: usize) -> Option<&Envelope>;
260}
261
262impl EnvSource for [Envelope] {
263 fn envelope(&self, i: usize) -> Option<&Envelope> {
264 self.get(i)
265 }
266}
267
268impl EnvSource for Vec<Envelope> {
269 fn envelope(&self, i: usize) -> Option<&Envelope> {
270 self.get(i)
271 }
272}
273
274impl<'a> Scope<'a> {
275 pub fn me(me: &'a [String]) -> Scope<'a> {
277 Scope {
278 me: Me::addresses(me),
279 ..Default::default()
280 }
281 }
282
283 pub fn any_list(&self, addresses: &[String]) -> bool {
285 addresses
286 .iter()
287 .any(|a| self.lists.iter().any(|m| m.is_match(a)))
288 }
289}
290
291#[derive(Debug, Clone, Copy, Default)]
296pub struct Position {
297 pub number: usize,
299 pub current: usize,
301 pub last: usize,
303 pub duplicate: bool,
304}
305
306enum Tok {
309 Not,
310 Or,
311 LParen,
312 RParen,
313 Op(char),
314 Word(String),
315}
316
317fn lex(input: &str) -> Vec<Tok> {
318 let mut out = Vec::new();
319 let mut chars = input.chars().peekable();
320 while let Some(&c) = chars.peek() {
321 match c {
322 c if c.is_whitespace() => {
323 chars.next();
324 }
325 '!' => {
326 chars.next();
327 out.push(Tok::Not);
328 }
329 '|' => {
330 chars.next();
331 out.push(Tok::Or);
332 }
333 '(' => {
334 chars.next();
335 out.push(Tok::LParen);
336 }
337 ')' => {
338 chars.next();
339 out.push(Tok::RParen);
340 }
341 '~' => {
342 chars.next();
343 if let Some(op) = chars.next() {
344 out.push(Tok::Op(op));
345 if op == '(' {
348 out.push(Tok::LParen);
349 }
350 }
351 }
352 '"' | '\'' => {
353 let quote = c;
354 chars.next();
355 let mut word = String::new();
356 for c in chars.by_ref() {
357 if c == quote {
358 break;
359 }
360 word.push(c);
361 }
362 out.push(Tok::Word(word));
363 }
364 _ => {
365 let mut word = String::new();
366 while let Some(&c) = chars.peek() {
367 if c.is_whitespace() || matches!(c, '(' | ')' | '|') {
368 break;
369 }
370 word.push(c);
371 chars.next();
372 }
373 out.push(Tok::Word(word));
374 }
375 }
376 }
377 out
378}
379
380pub fn parse(input: &str) -> Result<Vec<Pattern>, String> {
382 parse_at(input, Local::now().timestamp())
383}
384
385fn parse_at(input: &str, now: i64) -> Result<Vec<Pattern>, String> {
387 let mut toks = lex(input).into_iter().peekable();
388 let top = parse_or(&mut toks, now)?;
389 if toks.next().is_some() {
390 return Err("unbalanced )".into());
391 }
392 Ok(match top {
393 Pattern::All(terms) => terms,
394 one => vec![one],
395 })
396}
397
398type Toks = std::iter::Peekable<std::vec::IntoIter<Tok>>;
399
400fn parse_or(toks: &mut Toks, now: i64) -> Result<Pattern, String> {
401 let mut terms = vec![parse_and(toks, now)?];
402 while matches!(toks.peek(), Some(Tok::Or)) {
403 toks.next();
404 terms.push(parse_and(toks, now)?);
405 }
406 Ok(if terms.len() == 1 {
407 terms.pop().expect("one term")
408 } else {
409 Pattern::Any(terms)
410 })
411}
412
413fn parse_and(toks: &mut Toks, now: i64) -> Result<Pattern, String> {
414 let mut terms = Vec::new();
415 while !matches!(toks.peek(), None | Some(Tok::Or) | Some(Tok::RParen)) {
416 terms.push(parse_unary(toks, now)?);
417 }
418 Ok(if terms.len() == 1 {
419 terms.pop().expect("one term")
420 } else {
421 Pattern::All(terms)
422 })
423}
424
425fn parse_unary(toks: &mut Toks, now: i64) -> Result<Pattern, String> {
426 match toks.next() {
427 Some(Tok::Not) => Ok(Pattern::Not(Box::new(parse_unary(toks, now)?))),
428 Some(Tok::LParen) => {
429 let inner = parse_or(toks, now)?;
430 match toks.next() {
431 Some(Tok::RParen) => Ok(inner),
432 _ => Err("missing )".into()),
433 }
434 }
435 Some(Tok::Op(op @ ('(' | '<' | '>'))) => {
436 match toks.next() {
437 Some(Tok::LParen) => {}
438 _ => return Err(format!("~{op} needs a (pattern)")),
439 }
440 let inner = Box::new(parse_or(toks, now)?);
441 match toks.next() {
442 Some(Tok::RParen) => {}
443 _ => return Err("missing )".into()),
444 }
445 Ok(match op {
446 '(' => Pattern::Thread(inner),
447 '<' => Pattern::Parent(inner),
448 _ => Pattern::Child(inner),
449 })
450 }
451 Some(Tok::Op(op)) => {
452 let mut arg = || match toks.next() {
453 Some(Tok::Word(w)) => Ok(w),
454 _ => Err(format!("~{op} needs an argument")),
455 };
456 Ok(match op {
457 'f' => Pattern::From(Matcher::new(&arg()?)),
458 's' => Pattern::Subject(Matcher::new(&arg()?)),
459 'b' => Pattern::Body(Matcher::new(&arg()?)),
460 't' => Pattern::To(Matcher::new(&arg()?)),
461 'c' => Pattern::Cc(Matcher::new(&arg()?)),
462 'C' => Pattern::Recipient(Matcher::new(&arg()?)),
463 'e' => Pattern::Sender(Matcher::new(&arg()?)),
464 'h' => Pattern::Header(Matcher::new(&arg()?)),
465 'i' => Pattern::MessageId(Matcher::new(&arg()?)),
466 'x' => Pattern::References(Matcher::new(&arg()?)),
467 'B' => Pattern::Whole(Matcher::new(&arg()?)),
468 'y' => Pattern::Label(Matcher::new(&arg()?)),
469 'L' => Pattern::FromOrTo(Matcher::new(&arg()?)),
470 'd' => date_term(&arg()?, now)?,
471 'r' => match date_term(&arg()?, now)? {
472 Pattern::Date { min, max } => Pattern::Received { min, max },
473 other => other,
474 },
475 'm' => number_term(&arg()?)?,
476 'z' => size_term(&arg()?)?,
477 '=' => Pattern::Duplicate,
478 'N' => Pattern::New,
479 'F' => Pattern::Flagged,
480 'D' => Pattern::Deleted,
481 'U' => Pattern::Unread,
482 'T' => Pattern::Tagged,
483 'A' => Pattern::All(Vec::new()),
484 'p' => Pattern::ToMe,
485 'P' => Pattern::FromMe,
486 'l' => Pattern::ToList,
487 'R' => Pattern::Read,
488 'O' => Pattern::Old,
489 'Q' => Pattern::Replied,
490 'u' => Pattern::Subscribed,
491 'v' => Pattern::Collapsed,
492 '$' => Pattern::Unreferenced,
493 'S' | 'E' | 'g' | 'G' | 'V' | 'k' | 'X' | 'n' | 'H' => {
499 return Err(format!("~{op} is not supported"));
500 }
501 other => return Err(format!("unknown pattern ~{other}")),
502 })
503 }
504 Some(Tok::Word(w)) => Ok(Pattern::Default(Matcher::new(&w))),
505 _ => Err("empty pattern group".into()),
506 }
507}
508
509fn date_term(spec: &str, now: i64) -> Result<Pattern, String> {
515 if let Some(rest) = spec.strip_prefix('<') {
516 return Ok(Pattern::Date {
517 min: Some(now - offset_secs(rest)?),
518 max: None,
519 });
520 }
521 if let Some(rest) = spec.strip_prefix('>') {
522 return Ok(Pattern::Date {
523 min: None,
524 max: Some(now - offset_secs(rest)?),
525 });
526 }
527 if let Some(rest) = spec.strip_prefix('=') {
528 let then = now - offset_secs(rest)?;
529 let date = Local
530 .timestamp_opt(then, 0)
531 .earliest()
532 .ok_or("bad offset")?
533 .date_naive();
534 let (min, max) = day_bounds_of(date)?;
535 return Ok(Pattern::Date {
536 min: Some(min),
537 max: Some(max),
538 });
539 }
540 match spec.split_once('-') {
541 Some((min, max)) => {
542 let min = if min.is_empty() {
543 None
544 } else {
545 Some(day_bounds(min, now)?.0)
546 };
547 let max = if max.is_empty() {
548 None
549 } else {
550 Some(day_bounds(max, now)?.1)
551 };
552 Ok(Pattern::Date { min, max })
553 }
554 None => {
555 let (min, max) = day_bounds(spec, now)?;
556 Ok(Pattern::Date {
557 min: Some(min),
558 max: Some(max),
559 })
560 }
561 }
562}
563
564fn offset_secs(s: &str) -> Result<i64, String> {
567 let err = || format!("bad date offset {s:?} (want e.g. 1w, 2d, 3H)");
568 let unit = s.chars().last().ok_or_else(err)?;
569 let n: i64 = s[..s.len() - unit.len_utf8()].parse().map_err(|_| err())?;
570 let secs = match unit {
571 'y' => 365 * 86400,
572 'm' => 30 * 86400,
573 'w' => 7 * 86400,
574 'd' => 86400,
575 'H' => 3600,
576 'M' => 60,
577 _ => return Err(err()),
578 };
579 Ok(n * secs)
580}
581
582fn day_bounds(s: &str, now: i64) -> Result<(i64, i64), String> {
585 let err = || format!("bad date {s:?} (want DD/MM/YYYY)");
586 let today = Local
587 .timestamp_opt(now, 0)
588 .earliest()
589 .ok_or_else(err)?
590 .date_naive();
591 let mut parts = s.split('/');
592 let day: u32 = parts.next().unwrap_or("").parse().map_err(|_| err())?;
593 let month: u32 = match parts.next() {
594 Some(m) => m.parse().map_err(|_| err())?,
595 None => today.month(),
596 };
597 let year: i32 = match parts.next() {
598 Some(y) => {
599 let y: i32 = y.parse().map_err(|_| err())?;
600 if y < 100 { 2000 + y } else { y }
601 }
602 None => today.year(),
603 };
604 if parts.next().is_some() {
605 return Err(err());
606 }
607 let date = chrono::NaiveDate::from_ymd_opt(year, month, day).ok_or_else(err)?;
608 day_bounds_of(date)
609}
610
611fn day_bounds_of(date: chrono::NaiveDate) -> Result<(i64, i64), String> {
612 let midnight = date.and_hms_opt(0, 0, 0).ok_or("bad date")?;
613 let start = Local
614 .from_local_datetime(&midnight)
615 .earliest()
616 .ok_or("bad date")?
617 .timestamp();
618 Ok((start, start + 86400))
619}
620
621fn number_term(spec: &str) -> Result<Pattern, String> {
626 let end = |text: &str| -> Result<Bound, String> {
627 match text {
628 "." => Ok(Bound::Current),
629 "$" => Ok(Bound::Last),
630 n => n
631 .parse()
632 .map(Bound::Num)
633 .map_err(|_| format!("~m wants a number, . or $, got {n:?}")),
634 }
635 };
636 match spec.split_once('-') {
637 Some((min, max)) => Ok(Pattern::Number {
638 min: (!min.is_empty()).then(|| end(min)).transpose()?,
639 max: (!max.is_empty()).then(|| end(max)).transpose()?,
640 }),
641 None => {
642 let one = end(spec)?;
643 Ok(Pattern::Number {
644 min: Some(one),
645 max: Some(one),
646 })
647 }
648 }
649}
650
651fn size_term(spec: &str) -> Result<Pattern, String> {
654 if let Some(rest) = spec.strip_prefix('>') {
655 return Ok(Pattern::Size {
656 min: Some(bytes(rest)?),
657 max: None,
658 });
659 }
660 if let Some(rest) = spec.strip_prefix('<') {
661 return Ok(Pattern::Size {
662 min: None,
663 max: Some(bytes(rest)?),
664 });
665 }
666 match spec.split_once('-') {
667 Some((min, max)) => Ok(Pattern::Size {
668 min: (!min.is_empty()).then(|| bytes(min)).transpose()?,
669 max: (!max.is_empty()).then(|| bytes(max)).transpose()?,
670 }),
671 None => {
672 let exact = bytes(spec)?;
673 Ok(Pattern::Size {
674 min: Some(exact),
675 max: Some(exact),
676 })
677 }
678 }
679}
680
681fn bytes(spec: &str) -> Result<u64, String> {
682 let spec = spec.trim();
683 let err = || format!("~z wants a size like 100K, got {spec:?}");
684 let (digits, scale) = match spec.chars().last().map(|c| c.to_ascii_uppercase()) {
685 Some('K') => (&spec[..spec.len() - 1], 1024),
686 Some('M') => (&spec[..spec.len() - 1], 1024 * 1024),
687 Some('G') => (&spec[..spec.len() - 1], 1024 * 1024 * 1024),
688 _ => (spec, 1),
689 };
690 digits
691 .trim()
692 .parse::<u64>()
693 .map(|n| n * scale)
694 .map_err(|_| err())
695}
696
697pub type BodyOracle<'a> = &'a dyn Fn(&Envelope, &Matcher) -> Option<bool>;
702
703struct Ctx<'a> {
705 env: &'a Envelope,
706 scope: Scope<'a>,
707 body: Option<String>,
708 sender: Option<String>,
709 headers: Option<String>,
710 received: Option<i64>,
711 oracle: Option<BodyOracle<'a>>,
712}
713
714pub fn matches(patterns: &[Pattern], env: &Envelope, me: &[String]) -> bool {
717 matches_in(patterns, env, Scope::me(me), None)
718}
719
720pub fn matches_via(
723 patterns: &[Pattern],
724 env: &Envelope,
725 me: &[String],
726 oracle: Option<BodyOracle>,
727) -> bool {
728 matches_in(patterns, env, Scope::me(me), oracle)
729}
730
731pub fn matches_in<'a>(
734 patterns: &[Pattern],
735 env: &'a Envelope,
736 scope: Scope<'a>,
737 oracle: Option<BodyOracle<'a>>,
738) -> bool {
739 let mut ctx = Ctx {
740 env,
741 scope,
742 body: None,
743 sender: None,
744 headers: None,
745 received: None,
746 oracle,
747 };
748 patterns.iter().all(|p| eval(p, &mut ctx))
749}
750
751pub fn body_terms(patterns: &[Pattern]) -> Vec<String> {
753 let mut out = Vec::new();
754 fn walk(p: &Pattern, out: &mut Vec<String>) {
755 match p {
756 Pattern::All(terms) | Pattern::Any(terms) => {
757 terms.iter().for_each(|t| walk(t, out));
758 }
759 Pattern::Not(term)
760 | Pattern::Thread(term)
761 | Pattern::Parent(term)
762 | Pattern::Child(term) => {
763 walk(term, out);
764 }
765 Pattern::Body(m) if !out.contains(&m.raw) => out.push(m.raw.clone()),
766 _ => {}
767 }
768 }
769 patterns.iter().for_each(|p| walk(p, &mut out));
770 out
771}
772
773fn eval_peer(inner: &Pattern, view: ThreadView, i: usize, ctx: &Ctx) -> bool {
778 let Some(env) = view.envs.envelope(i) else {
779 return false;
780 };
781 let mut peer = Ctx {
782 env,
783 scope: Scope {
784 me: ctx.scope.me,
785 lists: ctx.scope.lists,
786 subscribed: ctx.scope.subscribed,
787 position: Position::default(),
788 thread: None,
789 },
790 body: None,
791 sender: None,
792 headers: None,
793 received: None,
794 oracle: ctx.oracle,
795 };
796 eval(inner, &mut peer)
797}
798
799fn eval(p: &Pattern, ctx: &mut Ctx) -> bool {
800 let env = ctx.env;
801 match p {
802 Pattern::All(terms) => terms.iter().all(|t| eval(t, ctx)),
803 Pattern::Any(terms) => terms.iter().any(|t| eval(t, ctx)),
804 Pattern::Not(term) => !eval(term, ctx),
805 Pattern::From(m) => m.is_match(&env.from) || m.is_match(&env.from_full),
806 Pattern::Subject(m) => m.is_match(&env.subject),
807 Pattern::Default(m) => {
808 m.is_match(&env.subject) || m.is_match(&env.from) || m.is_match(&env.from_full)
809 }
810 Pattern::To(m) => env.to.iter().any(|a| m.is_match(a)),
811 Pattern::Cc(m) => env.cc.iter().any(|a| m.is_match(a)),
812 Pattern::Recipient(m) => env.to.iter().chain(&env.cc).any(|a| m.is_match(a)),
813 Pattern::Sender(m) => {
814 let sender = ctx.sender.get_or_insert_with(|| {
815 message::first_header(&env.file.path, "Sender").unwrap_or_default()
816 });
817 !sender.is_empty() && m.is_match(sender)
818 }
819 Pattern::Header(m) => {
820 let headers = ctx
821 .headers
822 .get_or_insert_with(|| message::header_text(&env.file.path).unwrap_or_default());
823 m.is_match(headers)
824 }
825 Pattern::MessageId(m) => env.msg_id.as_deref().is_some_and(|id| m.is_match(id)),
826 Pattern::References(m) => env.references.iter().any(|id| m.is_match(id)),
827 Pattern::Whole(m) => {
828 if m.is_match(&env.from_full)
829 || m.is_match(&env.subject)
830 || env.to.iter().chain(&env.cc).any(|a| m.is_match(a))
831 {
832 return true;
833 }
834 let headers = ctx
835 .headers
836 .get_or_insert_with(|| message::header_text(&env.file.path).unwrap_or_default());
837 if m.is_match(headers) {
838 return true;
839 }
840 let body = ctx
841 .body
842 .get_or_insert_with(|| message::body_text(&env.file.path).unwrap_or_default());
843 m.is_match(body)
844 }
845 Pattern::Label(m) => env.label.as_deref().is_some_and(|l| m.is_match(l)),
846 Pattern::FromOrTo(m) => {
847 m.is_match(&env.from)
848 || m.is_match(&env.from_full)
849 || env.to.iter().chain(&env.cc).any(|a| m.is_match(a))
850 }
851 Pattern::Read => env.file.flags.seen,
852 Pattern::Old => !env.file.is_new && !env.file.flags.seen,
853 Pattern::Replied => env.file.flags.answered,
854 Pattern::Subscribed => env
855 .to
856 .iter()
857 .chain(&env.cc)
858 .any(|a| ctx.scope.subscribed.iter().any(|m| m.is_match(a))),
859 Pattern::Date { min, max } => {
860 min.is_none_or(|min| env.date >= min) && max.is_none_or(|max| env.date < max)
861 }
862 Pattern::Received { min, max } => {
863 let at = *ctx.received.get_or_insert_with(|| {
866 std::fs::metadata(&env.file.path)
867 .and_then(|m| m.modified())
868 .ok()
869 .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
870 .map(|d| d.as_secs() as i64)
871 .unwrap_or(env.date)
872 });
873 min.is_none_or(|min| at >= min) && max.is_none_or(|max| at < max)
874 }
875 Pattern::Number { min, max } => {
876 let n = ctx.scope.position.number;
877 n > 0
878 && min.is_none_or(|b| n >= b.resolve(&ctx.scope.position))
879 && max.is_none_or(|b| n <= b.resolve(&ctx.scope.position))
880 }
881 Pattern::Size { min, max } => {
882 let size = env.file.size;
883 min.is_none_or(|min| size >= min) && max.is_none_or(|max| size <= max)
884 }
885 Pattern::Duplicate => ctx.scope.position.duplicate,
886 Pattern::Thread(inner) => match ctx.scope.thread {
887 Some(t) => t.members.iter().any(|&i| eval_peer(inner, t, i, ctx)),
888 None => eval(inner, ctx),
889 },
890 Pattern::Parent(inner) => match ctx.scope.thread {
891 Some(t) => t.parent.is_some_and(|i| eval_peer(inner, t, i, ctx)),
892 None => false,
893 },
894 Pattern::Child(inner) => match ctx.scope.thread {
895 Some(t) => t.children.iter().any(|&i| eval_peer(inner, t, i, ctx)),
896 None => false,
897 },
898 Pattern::Collapsed => ctx.scope.thread.is_some_and(|t| t.collapsed),
899 Pattern::Unreferenced => ctx
900 .scope
901 .thread
902 .is_some_and(|t| t.parent.is_none() && t.children.is_empty()),
903 Pattern::New => env.file.is_new,
904 Pattern::Flagged => env.file.flags.flagged,
905 Pattern::Deleted => env.file.flags.deleted,
906 Pattern::Unread => !env.file.flags.seen,
907 Pattern::Tagged => env.tagged,
908 Pattern::ToMe => ctx.scope.me.any(env.to.iter().chain(&env.cc)),
909 Pattern::FromMe => ctx.scope.me.wrote(&env.from_full),
910 Pattern::ToList => env
911 .to
912 .iter()
913 .chain(&env.cc)
914 .any(|a| ctx.scope.lists.iter().any(|m| m.is_match(a))),
915 Pattern::Body(m) => {
916 if let Some(answer) = ctx.oracle.and_then(|oracle| oracle(env, m)) {
917 return answer;
918 }
919 let body = ctx
920 .body
921 .get_or_insert_with(|| message::body_text(&env.file.path).unwrap_or_default());
922 m.is_match(body)
923 }
924 }
925}
926
927#[cfg(test)]
928mod tests {
929 use super::*;
930 use crate::maildir::{Flags, MailFile};
931
932 fn env(from: &str, subject: &str, is_new: bool, flags: Flags) -> Envelope {
933 Envelope {
934 file: MailFile {
935 path: "/nonexistent".into(),
936 is_new,
937 flags,
938 size: 0,
939 },
940 from: crate::message::short_from(from),
941 from_full: from.into(),
942 subject: subject.into(),
943 date: 0,
944 msg_id: None,
945 references: vec![],
946 tagged: false,
947 to: vec![],
948 cc: vec![],
949 lines: Some(0),
950 list: None,
951 label: None,
952 broken: false,
953 }
954 }
955
956 fn ok(input: &str) -> Vec<Pattern> {
957 parse(input).unwrap_or_else(|e| panic!("parse {input:?}: {e}"))
958 }
959
960 #[test]
961 fn parse_mixed_terms() {
962 assert_eq!(
963 ok("~f jane ~N lunch"),
964 vec![
965 Pattern::From(Matcher::new("jane")),
966 Pattern::New,
967 Pattern::Default(Matcher::new("lunch")),
968 ]
969 );
970 assert_eq!(ok(""), vec![]);
971 assert_eq!(
973 ok("~s \"pizza friday\""),
974 vec![Pattern::Subject(Matcher::new("pizza friday"))]
975 );
976 }
977
978 #[test]
979 fn patterns_v3_parse() {
980 assert_eq!(
981 ok("~i msg1"),
982 vec![Pattern::MessageId(Matcher::new("msg1"))]
983 );
984 assert_eq!(
985 ok("~x parent"),
986 vec![Pattern::References(Matcher::new("parent"))]
987 );
988 assert_eq!(
989 ok("~h x-spam"),
990 vec![Pattern::Header(Matcher::new("x-spam"))]
991 );
992 assert_eq!(ok("~="), vec![Pattern::Duplicate]);
993 assert_eq!(
994 ok("~m 10-20"),
995 vec![Pattern::Number {
996 min: Some(Bound::Num(10)),
997 max: Some(Bound::Num(20)),
998 }]
999 );
1000 assert_eq!(
1001 ok("~m .-$"),
1002 vec![Pattern::Number {
1003 min: Some(Bound::Current),
1004 max: Some(Bound::Last),
1005 }]
1006 );
1007 assert_eq!(
1008 ok("~m 7"),
1009 vec![Pattern::Number {
1010 min: Some(Bound::Num(7)),
1011 max: Some(Bound::Num(7)),
1012 }]
1013 );
1014 assert_eq!(
1015 ok("~z >100K"),
1016 vec![Pattern::Size {
1017 min: Some(102400),
1018 max: None,
1019 }]
1020 );
1021 assert_eq!(
1022 ok("~z 1K-2M"),
1023 vec![Pattern::Size {
1024 min: Some(1024),
1025 max: Some(2 * 1024 * 1024),
1026 }]
1027 );
1028 assert!(matches!(
1030 ok("~r <1w").as_slice(),
1031 [Pattern::Received { .. }]
1032 ));
1033 assert!(parse("~m nonsense").is_err());
1034 assert!(parse("~z 10X").is_err());
1035 }
1036
1037 #[test]
1038 fn patterns_v3_match() {
1039 let mut e = env("jane@x", "lunch", false, Flags::default());
1040 e.msg_id = Some("msg1@example.com".into());
1041 e.references = vec!["parent@example.com".into()];
1042 e.file.size = 150 * 1024;
1043 assert!(matches(&ok("~i msg1"), &e, &[]));
1044 assert!(!matches(&ok("~i other"), &e, &[]));
1045 assert!(matches(&ok("~x parent"), &e, &[]));
1046 assert!(matches(&ok("~z >100K"), &e, &[]));
1047 assert!(!matches(&ok("~z >1M"), &e, &[]));
1048 assert!(matches(&ok("~z 100K-200K"), &e, &[]));
1049 assert!(!matches(&ok("~m 1"), &e, &[]));
1051 assert!(!matches(&ok("~="), &e, &[]));
1052 let pos = Position {
1053 number: 3,
1054 current: 5,
1055 last: 9,
1056 duplicate: true,
1057 };
1058 let scope = Scope {
1059 position: pos,
1060 ..Default::default()
1061 };
1062 let at = |p: &str| matches_in(&ok(p), &e, scope, None);
1063 assert!(at("~m 3"));
1064 assert!(at("~m 1-3"));
1065 assert!(at("~m -3"));
1066 assert!(!at("~m 4-"));
1067 assert!(at("~m 1-."));
1068 assert!(!at("~m .-$"));
1069 assert!(at("~="));
1070 assert!(!matches_in(&ok("~m 3"), &e, Scope::default(), None));
1071 e.to = vec!["dev@lists.example.com".into()];
1073 assert!(!matches(&ok("~l"), &e, &[]));
1074 let lists = [Matcher::new("@lists\\.example\\.com")];
1075 let scope = Scope {
1076 lists: &lists,
1077 ..Default::default()
1078 };
1079 assert!(matches_in(&ok("~l"), &e, scope, None));
1080 assert!(scope.any_list(&["dev@lists.example.com".to_string()]));
1081 }
1082
1083 #[test]
1084 fn thread_terms_look_at_the_neighbours() {
1085 let envs = vec![
1089 env("jane@example.com", "root", false, Flags::default()),
1090 env("bob@example.com", "reply", false, Flags::default()),
1091 env("jane@example.com", "again", false, Flags::default()),
1092 ];
1093 let members = [0usize, 1, 2];
1094 let children_of_root = [1usize];
1095 let children_of_reply = [2usize];
1096 let view = |i: usize| ThreadView {
1097 members: &members,
1098 parent: match i {
1099 0 => None,
1100 1 => Some(0),
1101 _ => Some(1),
1102 },
1103 children: match i {
1104 0 => &children_of_root[..],
1105 1 => &children_of_reply[..],
1106 _ => &[],
1107 },
1108 collapsed: i == 0,
1109 envs: &envs,
1110 };
1111 let at = |pat: &str, i: usize| {
1112 let scope = Scope {
1113 thread: Some(view(i)),
1114 ..Default::default()
1115 };
1116 matches_in(&ok(pat), &envs[i], scope, None)
1117 };
1118 assert!(at("~(~f bob)", 0));
1120 assert!(at("~(~f bob)", 2));
1121 assert!(!at("~(~f alice)", 1));
1122 assert!(at("~<(~f jane)", 1));
1124 assert!(!at("~<(~f jane)", 2));
1125 assert!(!at("~<(~A)", 0));
1126 assert!(at("~>(~f bob)", 0));
1127 assert!(!at("~>(~f jane)", 0), "a grandchild is not a child");
1128 assert!(at("~v", 0));
1130 assert!(!at("~v", 1));
1131 assert!(!at("~$", 0), "it has children");
1132 assert!(at("!~$ ~(~s again)", 2));
1133 let alone = |pat: &str, i: usize| matches_in(&ok(pat), &envs[i], Scope::default(), None);
1136 assert!(alone("~(~f bob)", 1));
1137 assert!(!alone("~(~f bob)", 0));
1138 assert!(!alone("~<(~A)", 1));
1139 assert!(!alone("~$", 0));
1140 assert!(parse("~(").is_err());
1142 assert!(parse("~<~f x").is_err());
1143 assert!(parse("~(~f x").is_err());
1144 }
1145
1146 #[test]
1147 fn parse_reports_errors() {
1148 assert!(parse("~S").is_err()); assert!(parse("~g").is_err());
1150 assert!(parse("~f").is_err());
1151 assert!(parse("~x").is_err()); assert!(parse("(~N").is_err());
1153 assert!(parse("~N)").is_err());
1154 assert!(parse("~d nonsense").is_err());
1155 }
1156
1157 #[test]
1158 fn matches_is_case_insensitive_and_anded() {
1159 let e = env("Jane Doe", "Lunch on Friday", true, Flags::default());
1160 assert!(matches(&ok("~f JANE"), &e, &[]));
1161 assert!(matches(&ok("~f jane ~s lunch"), &e, &[]));
1162 assert!(!matches(&ok("~f jane ~s dinner"), &e, &[]));
1163 assert!(matches(&ok("friday"), &e, &[]));
1164 assert!(matches(&ok("doe"), &e, &[])); assert!(matches(&ok("~N"), &e, &[]));
1166 assert!(!matches(&ok("~F"), &e, &[]));
1167 assert!(matches(&[], &e, &[])); }
1169
1170 #[test]
1171 fn from_matches_the_whole_header() {
1172 let e = env(
1175 "Jane Doe <jane@example.com>",
1176 "Lunch",
1177 false,
1178 Flags::default(),
1179 );
1180 assert!(matches(&ok("~f jane@example.com"), &e, &[]));
1181 assert!(matches(&ok("~f example"), &e, &[]));
1182 assert!(matches(&ok("~f \"jane doe\""), &e, &[]));
1183 assert!(matches(&ok("example.com"), &e, &[]));
1184 assert!(!matches(&ok("~f petr@example.com"), &e, &[]));
1185 let mut old = env(
1188 "Jane Doe <jane@example.com>",
1189 "Lunch",
1190 false,
1191 Flags::default(),
1192 );
1193 old.from_full = String::new();
1194 assert!(matches(&ok("~f doe"), &old, &[]));
1195 assert!(!matches(&ok("~f jane@example.com"), &old, &[]));
1196 }
1197
1198 #[test]
1199 fn not_or_and_grouping() {
1200 let e = env("Jane", "Lunch", true, Flags::default());
1201 assert!(matches(&ok("!~F"), &e, &[]));
1202 assert!(!matches(&ok("!~N"), &e, &[]));
1203 assert!(matches(&ok("~f jane | ~f petr"), &e, &[]));
1204 assert!(matches(&ok("~f petr | ~f jane"), &e, &[]));
1205 assert!(!matches(&ok("~f petr | ~f alice"), &e, &[]));
1206 assert!(matches(&ok("~f petr ~s x | ~f jane ~s lunch"), &e, &[]));
1208 assert!(!matches(&ok("~f petr (~s x | ~s lunch)"), &e, &[]));
1209 assert!(matches(&ok("~f jane (~s x | ~s lunch)"), &e, &[]));
1210 assert!(matches(&ok("!(~f petr | ~f alice)"), &e, &[]));
1211 }
1212
1213 #[test]
1214 fn body_terms_collect_and_the_oracle_answers() {
1215 let pats = ok("~b invoice ~s x !(~b a | ~b invoice)");
1216 assert_eq!(body_terms(&pats), vec!["invoice", "a"]);
1217 let e = env("Jane", "report", false, Flags::default());
1219 let pats = ok("~b invoice");
1220 assert!(!matches(&pats, &e, &[]));
1221 let yes = |_: &Envelope, m: &Matcher| Some(m.raw() == "invoice");
1222 assert!(matches_via(&pats, &e, &[], Some(&yes)));
1223 let dunno = |_: &Envelope, _: &Matcher| None;
1225 assert!(!matches_via(&pats, &e, &[], Some(&dunno)));
1226 }
1227
1228 #[test]
1229 fn string_arguments_are_regexes() {
1230 let e = env("Jane", "Re: budget", false, Flags::default());
1231 assert!(matches(&ok("~s ^re:"), &e, &[]));
1232 assert!(!matches(&ok("~s ^budget"), &e, &[]));
1233 assert!(matches(&ok("~s bud.et"), &e, &[]));
1234 assert!(matches(&ok("~s \"re:.*budget\""), &e, &[]));
1235 let e2 = env("Jane", "cost [draft]", false, Flags::default());
1237 assert!(matches(&ok("~s \"[draft\""), &e2, &[]));
1238 }
1239
1240 #[test]
1241 fn recipients_and_to_me() {
1242 let mut e = env("Jane", "s", false, Flags::default());
1243 e.to = vec!["team@example.com".into(), "me@example.com".into()];
1244 e.cc = vec!["boss@example.com".into()];
1245 assert!(matches(&ok("~t team@"), &e, &[]));
1246 assert!(!matches(&ok("~t boss@"), &e, &[]));
1247 assert!(matches(&ok("~c boss@"), &e, &[]));
1248 assert!(matches(&ok("~C boss@"), &e, &[]));
1249 assert!(matches(&ok("~C team@"), &e, &[]));
1250 let me = vec!["me@example.com".to_string()];
1251 assert!(matches(&ok("~p"), &e, &me));
1252 assert!(!matches(&ok("~p"), &e, &["other@example.com".to_string()]));
1253 assert!(!matches(&ok("~p"), &e, &[]));
1254 }
1255
1256 #[test]
1257 fn alternates_widen_who_counts_as_me() {
1258 let mut e = env("Jane Doe <jane@example.com>", "s", false, Flags::default());
1259 e.to = vec!["j.doe@old.example.com".into()];
1260 assert!(!matches(&ok("~p"), &e, &["me@example.com".to_string()]));
1262 let alternates = vec![Matcher::new("@old\\.example\\.com$")];
1263 let scope = Scope {
1264 me: Me::new(&[], &alternates),
1265 ..Default::default()
1266 };
1267 assert!(matches_in(&ok("~p"), &e, scope, None));
1268 assert!(!matches_in(&ok("~P"), &e, scope, None));
1270 let mine = vec!["jane@example.com".to_string()];
1271 let scope = Scope {
1272 me: Me::addresses(&mine),
1273 ..Default::default()
1274 };
1275 assert!(matches_in(&ok("~P"), &e, scope, None));
1276 assert!(!matches_in(&ok("~p"), &e, scope, None));
1277 }
1278
1279 #[test]
1280 fn sender_header_is_read_from_disk() {
1281 let dir = tempfile::tempdir().unwrap();
1282 let path = dir.path().join("msg");
1283 std::fs::write(
1284 &path,
1285 "From: jane@example.com\nSender: list-bot@example.com\nSubject: s\n\nbody\n",
1286 )
1287 .unwrap();
1288 let mut e = env("jane", "s", false, Flags::default());
1289 e.file.path = path;
1290 assert!(matches(&ok("~e list-bot"), &e, &[]));
1291 assert!(!matches(&ok("~e jane"), &e, &[]));
1292 assert!(!matches(
1294 &ok("~e .*"),
1295 &env("j", "s", false, Flags::default()),
1296 &[]
1297 ));
1298 }
1299
1300 #[test]
1301 fn date_offsets_and_ranges() {
1302 let now = 1_800_000_000i64; let mut e = env("Jane", "s", false, Flags::default());
1304 e.date = now - 3 * 86400; let m = |input: &str, e: &Envelope| matches(&parse_at(input, now).unwrap(), e, &[]);
1306 assert!(m("~d <1w", &e));
1307 assert!(!m("~d <2d", &e));
1308 assert!(m("~d >2d", &e));
1309 assert!(!m("~d >1w", &e));
1310 assert!(m("~d =3d", &e));
1311 assert!(!m("~d =2d", &e));
1312 assert!(m("!~d <2d", &e));
1313 let day = chrono::Local.timestamp_opt(e.date, 0).unwrap();
1315 let spec = day.format("%d/%m/%Y").to_string();
1316 assert!(m(&format!("~d {spec}"), &e));
1317 assert!(m(&format!("~d {spec}-"), &e));
1318 assert!(m(&format!("~d -{spec}"), &e));
1319 let before = day - chrono::Duration::days(2);
1320 assert!(!m(&format!("~d -{}", before.format("%d/%m/%Y")), &e));
1321 assert!(m(&format!("~d {}-", before.format("%d/%m/%Y")), &e));
1322 assert!(parse_at("~d 1/1/26-31/12/26", now).is_ok());
1324 assert!(parse_at("~d 15", now).is_ok());
1325 }
1326
1327 #[test]
1328 fn tagged_pattern_reads_the_runtime_mark() {
1329 let mut e = env("Jane", "s", false, Flags::default());
1330 assert!(!matches(&ok("~T"), &e, &[]));
1331 e.tagged = true;
1332 assert!(matches(&ok("~T"), &e, &[]));
1333 }
1334}