Skip to main content

rusmes_core/sieve/
parser.rs

1//! Sieve script parser (RFC 5228)
2
3/// Sieve script value
4#[derive(Debug, Clone, PartialEq)]
5pub enum SieveValue {
6    String(String),
7    StringList(Vec<String>),
8    Number(i64),
9    Tag(String),
10}
11
12/// Sieve test condition
13#[derive(Debug, Clone, PartialEq)]
14pub enum SieveTest {
15    /// true test
16    True,
17    /// false test
18    False,
19    /// header test: header ["comparator"] ["match-type"] <header-names: string-list> <key-list: string-list>
20    Header {
21        comparator: Option<String>,
22        match_type: String,
23        headers: Vec<String>,
24        keys: Vec<String>,
25    },
26    /// address test: address \["comparator"\] \["match-type"\] `<header-list>` `<key-list>`
27    Address {
28        comparator: Option<String>,
29        match_type: String,
30        headers: Vec<String>,
31        keys: Vec<String>,
32    },
33    /// envelope test
34    Envelope {
35        comparator: Option<String>,
36        match_type: String,
37        parts: Vec<String>,
38        keys: Vec<String>,
39    },
40    /// exists test: exists <header-names: string-list>
41    Exists(Vec<String>),
42    /// size test: size <":over" / ":under"> <limit: number>
43    Size { over: bool, limit: i64 },
44    /// allof test: allof <tests: test-list>
45    AllOf(Vec<SieveTest>),
46    /// anyof test: anyof <tests: test-list>
47    AnyOf(Vec<SieveTest>),
48    /// not test: not `<test>`
49    Not(Box<SieveTest>),
50}
51
52/// Sieve command/action
53#[derive(Debug, Clone, PartialEq)]
54pub enum SieveCommand {
55    /// keep - keep message in inbox
56    Keep,
57    /// fileinto - file message into mailbox
58    Fileinto(String),
59    /// redirect - forward to address
60    Redirect(String),
61    /// discard - silently discard message
62    Discard,
63    /// stop - stop processing
64    Stop,
65    /// if/elsif/else control structure
66    If {
67        test: SieveTest,
68        then_commands: Vec<SieveCommand>,
69        elsif_branches: Vec<(SieveTest, Vec<SieveCommand>)>,
70        else_commands: Option<Vec<SieveCommand>>,
71    },
72    /// require - declare required extensions
73    Require(Vec<String>),
74    /// set - set variable (RFC 5229)
75    Set { name: String, value: String },
76    /// vacation - auto-reply (RFC 5230)
77    Vacation {
78        days: Option<i64>,
79        subject: Option<String>,
80        from: Option<String>,
81        addresses: Vec<String>,
82        message: String,
83    },
84}
85
86/// Parsed Sieve script
87#[derive(Debug, Clone)]
88pub struct SieveScript {
89    /// Script commands
90    pub commands: Vec<SieveCommand>,
91    /// Required extensions
92    pub requires: Vec<String>,
93}
94
95impl SieveScript {
96    /// Create an empty Sieve script
97    pub fn new() -> Self {
98        Self {
99            commands: Vec::new(),
100            requires: Vec::new(),
101        }
102    }
103
104    /// Parse a Sieve script from text
105    pub fn parse(script: &str) -> Result<Self, String> {
106        let mut parser = Parser::new(script);
107        parser.parse()
108    }
109
110    /// Add a command
111    pub fn add_command(&mut self, command: SieveCommand) {
112        if let SieveCommand::Require(exts) = &command {
113            self.requires.extend(exts.clone());
114        }
115        self.commands.push(command);
116    }
117
118    /// Validate script
119    pub fn validate(&self) -> Result<(), String> {
120        // Check for unknown extensions
121        let known_extensions = ["fileinto", "envelope", "variables", "vacation"];
122        for ext in &self.requires {
123            if !known_extensions.contains(&ext.as_str()) {
124                return Err(format!("Unknown extension: {}", ext));
125            }
126        }
127        Ok(())
128    }
129}
130
131impl Default for SieveScript {
132    fn default() -> Self {
133        Self::new()
134    }
135}
136
137/// Simple Sieve script parser
138struct Parser {
139    input: String,
140    pos: usize,
141}
142
143impl Parser {
144    fn new(input: &str) -> Self {
145        Self {
146            input: input.to_string(),
147            pos: 0,
148        }
149    }
150
151    fn parse(&mut self) -> Result<SieveScript, String> {
152        let mut script = SieveScript::new();
153
154        self.skip_whitespace();
155        while self.pos < self.input.len() {
156            let cmd = self.parse_command()?;
157            script.add_command(cmd);
158            self.skip_whitespace();
159        }
160
161        Ok(script)
162    }
163
164    fn parse_command(&mut self) -> Result<SieveCommand, String> {
165        self.skip_whitespace();
166        let word = self.parse_word()?;
167
168        match word.as_str() {
169            "require" => self.parse_require(),
170            "if" => self.parse_if(),
171            "keep" => {
172                self.expect(";")?;
173                Ok(SieveCommand::Keep)
174            }
175            "discard" => {
176                self.expect(";")?;
177                Ok(SieveCommand::Discard)
178            }
179            "stop" => {
180                self.expect(";")?;
181                Ok(SieveCommand::Stop)
182            }
183            "fileinto" => self.parse_fileinto(),
184            "redirect" => self.parse_redirect(),
185            "set" => self.parse_set(),
186            "vacation" => self.parse_vacation(),
187            _ => Err(format!("Unknown command: {}", word)),
188        }
189    }
190
191    fn parse_require(&mut self) -> Result<SieveCommand, String> {
192        self.skip_whitespace();
193        let extensions = if self.peek_char() == Some('"') {
194            vec![self.parse_string()?]
195        } else if self.peek_char() == Some('[') {
196            self.parse_string_list()?
197        } else {
198            return Err("Expected string or string list after 'require'".to_string());
199        };
200        self.expect(";")?;
201        Ok(SieveCommand::Require(extensions))
202    }
203
204    fn parse_if(&mut self) -> Result<SieveCommand, String> {
205        self.skip_whitespace();
206        let test = self.parse_test()?;
207        self.skip_whitespace();
208        let then_commands = self.parse_block()?;
209
210        let mut elsif_branches = Vec::new();
211        let mut else_commands = None;
212
213        loop {
214            self.skip_whitespace();
215            if self.peek_word() == Some("elsif".to_string()) {
216                self.parse_word()?; // consume "elsif"
217                self.skip_whitespace();
218                let elsif_test = self.parse_test()?;
219                self.skip_whitespace();
220                let elsif_commands = self.parse_block()?;
221                elsif_branches.push((elsif_test, elsif_commands));
222            } else if self.peek_word() == Some("else".to_string()) {
223                self.parse_word()?; // consume "else"
224                self.skip_whitespace();
225                else_commands = Some(self.parse_block()?);
226                break;
227            } else {
228                break;
229            }
230        }
231
232        Ok(SieveCommand::If {
233            test,
234            then_commands,
235            elsif_branches,
236            else_commands,
237        })
238    }
239
240    fn parse_fileinto(&mut self) -> Result<SieveCommand, String> {
241        self.skip_whitespace();
242        let mailbox = self.parse_string()?;
243        self.expect(";")?;
244        Ok(SieveCommand::Fileinto(mailbox))
245    }
246
247    fn parse_redirect(&mut self) -> Result<SieveCommand, String> {
248        self.skip_whitespace();
249        let address = self.parse_string()?;
250        self.expect(";")?;
251        Ok(SieveCommand::Redirect(address))
252    }
253
254    fn parse_set(&mut self) -> Result<SieveCommand, String> {
255        self.skip_whitespace();
256        let name = self.parse_string()?;
257        self.skip_whitespace();
258        let value = self.parse_string()?;
259        self.expect(";")?;
260        Ok(SieveCommand::Set { name, value })
261    }
262
263    fn parse_vacation(&mut self) -> Result<SieveCommand, String> {
264        self.skip_whitespace();
265
266        let mut days = None;
267        let mut subject = None;
268        let mut from = None;
269        let mut addresses = Vec::new();
270
271        // Parse optional tags
272        while self.peek_char() == Some(':') {
273            let tag = self.parse_tag()?;
274            self.skip_whitespace();
275
276            match tag.as_str() {
277                ":days" => {
278                    days = Some(self.parse_number()?);
279                }
280                ":subject" => {
281                    subject = Some(self.parse_string()?);
282                }
283                ":from" => {
284                    from = Some(self.parse_string()?);
285                }
286                ":addresses" => {
287                    addresses = self.parse_string_list()?;
288                }
289                _ => return Err(format!("Unknown vacation tag: {}", tag)),
290            }
291            self.skip_whitespace();
292        }
293
294        let message = self.parse_string()?;
295        self.expect(";")?;
296
297        Ok(SieveCommand::Vacation {
298            days,
299            subject,
300            from,
301            addresses,
302            message,
303        })
304    }
305
306    fn parse_test(&mut self) -> Result<SieveTest, String> {
307        self.skip_whitespace();
308        let word = self.parse_word()?;
309
310        match word.as_str() {
311            "true" => Ok(SieveTest::True),
312            "false" => Ok(SieveTest::False),
313            "header" => self.parse_header_test(),
314            "address" => self.parse_address_test(),
315            "envelope" => self.parse_envelope_test(),
316            "exists" => self.parse_exists_test(),
317            "size" => self.parse_size_test(),
318            "allof" => self.parse_allof_test(),
319            "anyof" => self.parse_anyof_test(),
320            "not" => self.parse_not_test(),
321            _ => Err(format!("Unknown test: {}", word)),
322        }
323    }
324
325    fn parse_header_test(&mut self) -> Result<SieveTest, String> {
326        self.skip_whitespace();
327
328        let mut comparator = None;
329        let mut match_type = "is".to_string();
330
331        // Parse optional tags
332        while self.peek_char() == Some(':') {
333            let tag = self.parse_tag()?;
334            self.skip_whitespace();
335
336            match tag.as_str() {
337                ":comparator" => {
338                    comparator = Some(self.parse_string()?);
339                    self.skip_whitespace();
340                }
341                ":is" | ":contains" | ":matches" => {
342                    match_type = tag[1..].to_string();
343                }
344                _ => return Err(format!("Unknown header test tag: {}", tag)),
345            }
346        }
347
348        let headers = self.parse_string_or_list()?;
349        self.skip_whitespace();
350        let keys = self.parse_string_or_list()?;
351
352        Ok(SieveTest::Header {
353            comparator,
354            match_type,
355            headers,
356            keys,
357        })
358    }
359
360    fn parse_address_test(&mut self) -> Result<SieveTest, String> {
361        self.skip_whitespace();
362
363        let mut comparator = None;
364        let mut match_type = "is".to_string();
365
366        while self.peek_char() == Some(':') {
367            let tag = self.parse_tag()?;
368            self.skip_whitespace();
369
370            match tag.as_str() {
371                ":comparator" => {
372                    comparator = Some(self.parse_string()?);
373                    self.skip_whitespace();
374                }
375                ":is" | ":contains" | ":matches" => {
376                    match_type = tag[1..].to_string();
377                }
378                _ => {}
379            }
380        }
381
382        let headers = self.parse_string_or_list()?;
383        self.skip_whitespace();
384        let keys = self.parse_string_or_list()?;
385
386        Ok(SieveTest::Address {
387            comparator,
388            match_type,
389            headers,
390            keys,
391        })
392    }
393
394    fn parse_envelope_test(&mut self) -> Result<SieveTest, String> {
395        self.skip_whitespace();
396
397        let mut comparator = None;
398        let mut match_type = "is".to_string();
399
400        while self.peek_char() == Some(':') {
401            let tag = self.parse_tag()?;
402            self.skip_whitespace();
403
404            match tag.as_str() {
405                ":comparator" => {
406                    comparator = Some(self.parse_string()?);
407                    self.skip_whitespace();
408                }
409                ":is" | ":contains" | ":matches" => {
410                    match_type = tag[1..].to_string();
411                }
412                _ => {}
413            }
414        }
415
416        let parts = self.parse_string_or_list()?;
417        self.skip_whitespace();
418        let keys = self.parse_string_or_list()?;
419
420        Ok(SieveTest::Envelope {
421            comparator,
422            match_type,
423            parts,
424            keys,
425        })
426    }
427
428    fn parse_exists_test(&mut self) -> Result<SieveTest, String> {
429        self.skip_whitespace();
430        let headers = self.parse_string_or_list()?;
431        Ok(SieveTest::Exists(headers))
432    }
433
434    fn parse_size_test(&mut self) -> Result<SieveTest, String> {
435        self.skip_whitespace();
436        let tag = self.parse_tag()?;
437        let over = match tag.as_str() {
438            ":over" => true,
439            ":under" => false,
440            _ => return Err(format!("Expected :over or :under, got {}", tag)),
441        };
442        self.skip_whitespace();
443        let limit = self.parse_number()?;
444        Ok(SieveTest::Size { over, limit })
445    }
446
447    fn parse_allof_test(&mut self) -> Result<SieveTest, String> {
448        self.skip_whitespace();
449        self.expect("(")?;
450        let mut tests = Vec::new();
451        loop {
452            self.skip_whitespace();
453            if self.peek_char() == Some(')') {
454                break;
455            }
456            tests.push(self.parse_test()?);
457            self.skip_whitespace();
458            if self.peek_char() == Some(',') {
459                self.advance();
460            }
461        }
462        self.expect(")")?;
463        Ok(SieveTest::AllOf(tests))
464    }
465
466    fn parse_anyof_test(&mut self) -> Result<SieveTest, String> {
467        self.skip_whitespace();
468        self.expect("(")?;
469        let mut tests = Vec::new();
470        loop {
471            self.skip_whitespace();
472            if self.peek_char() == Some(')') {
473                break;
474            }
475            tests.push(self.parse_test()?);
476            self.skip_whitespace();
477            if self.peek_char() == Some(',') {
478                self.advance();
479            }
480        }
481        self.expect(")")?;
482        Ok(SieveTest::AnyOf(tests))
483    }
484
485    fn parse_not_test(&mut self) -> Result<SieveTest, String> {
486        self.skip_whitespace();
487        let test = self.parse_test()?;
488        Ok(SieveTest::Not(Box::new(test)))
489    }
490
491    fn parse_block(&mut self) -> Result<Vec<SieveCommand>, String> {
492        self.expect("{")?;
493        let mut commands = Vec::new();
494        loop {
495            self.skip_whitespace();
496            if self.peek_char() == Some('}') {
497                break;
498            }
499            commands.push(self.parse_command()?);
500        }
501        self.expect("}")?;
502        Ok(commands)
503    }
504
505    fn parse_string_or_list(&mut self) -> Result<Vec<String>, String> {
506        self.skip_whitespace();
507        if self.peek_char() == Some('"') {
508            Ok(vec![self.parse_string()?])
509        } else if self.peek_char() == Some('[') {
510            self.parse_string_list()
511        } else {
512            Err("Expected string or string list".to_string())
513        }
514    }
515
516    fn parse_string_list(&mut self) -> Result<Vec<String>, String> {
517        self.expect("[")?;
518        let mut strings = Vec::new();
519        loop {
520            self.skip_whitespace();
521            if self.peek_char() == Some(']') {
522                break;
523            }
524            strings.push(self.parse_string()?);
525            self.skip_whitespace();
526            if self.peek_char() == Some(',') {
527                self.advance();
528            }
529        }
530        self.expect("]")?;
531        Ok(strings)
532    }
533
534    fn parse_string(&mut self) -> Result<String, String> {
535        self.skip_whitespace();
536        if self.peek_char() != Some('"') {
537            return Err("Expected string".to_string());
538        }
539        self.advance(); // consume '"'
540
541        let mut result = String::new();
542        let mut escaped = false;
543
544        while self.pos < self.input.len() {
545            let ch = match self.current_char() {
546                Some(c) => c,
547                None => break,
548            };
549            self.advance();
550
551            if escaped {
552                result.push(ch);
553                escaped = false;
554            } else if ch == '\\' {
555                escaped = true;
556            } else if ch == '"' {
557                return Ok(result);
558            } else {
559                result.push(ch);
560            }
561        }
562
563        Err("Unterminated string".to_string())
564    }
565
566    fn parse_number(&mut self) -> Result<i64, String> {
567        self.skip_whitespace();
568        let mut num_str = String::new();
569
570        while self.pos < self.input.len() {
571            let ch = match self.current_char() {
572                Some(c) => c,
573                None => break,
574            };
575            if ch.is_ascii_digit() {
576                num_str.push(ch);
577                self.advance();
578            } else {
579                break;
580            }
581        }
582
583        if num_str.is_empty() {
584            return Err("Expected number".to_string());
585        }
586
587        // Handle K/M/G suffixes
588        if let Some(ch) = self.current_char() {
589            if ch == 'K' || ch == 'M' || ch == 'G' {
590                self.advance();
591                let multiplier = match ch {
592                    'K' => 1024,
593                    'M' => 1024 * 1024,
594                    'G' => 1024 * 1024 * 1024,
595                    _ => 1,
596                };
597                let base: i64 = num_str
598                    .parse()
599                    .map_err(|e| format!("Invalid number: {}", e))?;
600                return Ok(base * multiplier);
601            }
602        }
603
604        num_str
605            .parse()
606            .map_err(|e| format!("Invalid number: {}", e))
607    }
608
609    fn parse_tag(&mut self) -> Result<String, String> {
610        self.skip_whitespace();
611        if self.current_char() != Some(':') {
612            return Err("Expected tag starting with ':'".to_string());
613        }
614        self.advance(); // consume ':'
615
616        let mut tag = String::from(":");
617        while self.pos < self.input.len() {
618            let ch = match self.current_char() {
619                Some(c) => c,
620                None => break,
621            };
622            if ch.is_alphanumeric() || ch == '_' || ch == '-' {
623                tag.push(ch);
624                self.advance();
625            } else {
626                break;
627            }
628        }
629
630        if tag.len() == 1 {
631            return Err("Empty tag".to_string());
632        }
633
634        Ok(tag)
635    }
636
637    fn parse_word(&mut self) -> Result<String, String> {
638        self.skip_whitespace();
639        let mut word = String::new();
640
641        while self.pos < self.input.len() {
642            let ch = match self.current_char() {
643                Some(c) => c,
644                None => break,
645            };
646            if ch.is_alphanumeric() || ch == '_' || ch == '-' {
647                word.push(ch);
648                self.advance();
649            } else {
650                break;
651            }
652        }
653
654        if word.is_empty() {
655            return Err("Expected word".to_string());
656        }
657
658        Ok(word)
659    }
660
661    fn peek_word(&mut self) -> Option<String> {
662        let saved_pos = self.pos;
663        let result = self.parse_word().ok();
664        self.pos = saved_pos;
665        result
666    }
667
668    fn expect(&mut self, s: &str) -> Result<(), String> {
669        self.skip_whitespace();
670        for expected_ch in s.chars() {
671            if self.current_char() != Some(expected_ch) {
672                return Err(format!(
673                    "Expected '{}', got '{:?}'",
674                    expected_ch,
675                    self.current_char()
676                ));
677            }
678            self.advance();
679        }
680        Ok(())
681    }
682
683    fn skip_whitespace(&mut self) {
684        while self.pos < self.input.len() {
685            let ch = match self.input.chars().nth(self.pos) {
686                Some(c) => c,
687                None => break,
688            };
689            if ch.is_whitespace() {
690                self.pos += 1;
691            } else if ch == '#' {
692                // Skip comment
693                while self.pos < self.input.len() {
694                    let c = match self.input.chars().nth(self.pos) {
695                        Some(c) => c,
696                        None => break,
697                    };
698                    self.pos += 1;
699                    if c == '\n' {
700                        break;
701                    }
702                }
703            } else {
704                break;
705            }
706        }
707    }
708
709    fn current_char(&self) -> Option<char> {
710        if self.pos < self.input.len() {
711            self.input.chars().nth(self.pos)
712        } else {
713            None
714        }
715    }
716
717    fn peek_char(&self) -> Option<char> {
718        self.current_char()
719    }
720
721    fn advance(&mut self) {
722        if self.pos < self.input.len() {
723            self.pos += 1;
724        }
725    }
726}
727
728#[cfg(test)]
729mod tests {
730    use super::*;
731
732    #[test]
733    fn test_parse_simple_keep() {
734        let script = "keep;";
735        let parsed = SieveScript::parse(script).unwrap();
736        assert_eq!(parsed.commands.len(), 1);
737        assert_eq!(parsed.commands[0], SieveCommand::Keep);
738    }
739
740    #[test]
741    fn test_parse_fileinto() {
742        let script = r#"fileinto "INBOX.Spam";"#;
743        let parsed = SieveScript::parse(script).unwrap();
744        assert_eq!(parsed.commands.len(), 1);
745        assert_eq!(
746            parsed.commands[0],
747            SieveCommand::Fileinto("INBOX.Spam".to_string())
748        );
749    }
750
751    #[test]
752    fn test_parse_redirect() {
753        let script = r#"redirect "user@example.com";"#;
754        let parsed = SieveScript::parse(script).unwrap();
755        assert_eq!(parsed.commands.len(), 1);
756        assert_eq!(
757            parsed.commands[0],
758            SieveCommand::Redirect("user@example.com".to_string())
759        );
760    }
761
762    #[test]
763    fn test_parse_discard() {
764        let script = "discard;";
765        let parsed = SieveScript::parse(script).unwrap();
766        assert_eq!(parsed.commands.len(), 1);
767        assert_eq!(parsed.commands[0], SieveCommand::Discard);
768    }
769
770    #[test]
771    fn test_parse_require() {
772        let script = r#"require "fileinto";"#;
773        let parsed = SieveScript::parse(script).unwrap();
774        assert_eq!(parsed.requires, vec!["fileinto"]);
775    }
776
777    #[test]
778    fn test_parse_if_header() {
779        let script = r#"
780            if header :contains "Subject" "spam" {
781                discard;
782            }
783        "#;
784        let parsed = SieveScript::parse(script).unwrap();
785        assert_eq!(parsed.commands.len(), 1);
786    }
787
788    #[test]
789    fn test_parse_if_else() {
790        let script = r#"
791            if false {
792                discard;
793            } else {
794                keep;
795            }
796        "#;
797        let parsed = SieveScript::parse(script).unwrap();
798        assert_eq!(parsed.commands.len(), 1);
799    }
800
801    #[test]
802    fn test_parse_size_test() {
803        let script = r#"
804            if size :over 100K {
805                discard;
806            }
807        "#;
808        let parsed = SieveScript::parse(script).unwrap();
809        assert_eq!(parsed.commands.len(), 1);
810    }
811
812    #[test]
813    fn test_parse_exists_test() {
814        let script = r#"
815            if exists "X-Spam-Flag" {
816                fileinto "Spam";
817            }
818        "#;
819        let parsed = SieveScript::parse(script).unwrap();
820        assert_eq!(parsed.commands.len(), 1);
821    }
822
823    #[test]
824    fn test_parse_allof() {
825        let script = r#"
826            if allof(true, true) {
827                keep;
828            }
829        "#;
830        let parsed = SieveScript::parse(script).unwrap();
831        assert_eq!(parsed.commands.len(), 1);
832    }
833
834    #[test]
835    fn test_parse_comment() {
836        let script = r#"
837            # This is a comment
838            keep; # Another comment
839        "#;
840        let parsed = SieveScript::parse(script).unwrap();
841        assert_eq!(parsed.commands.len(), 1);
842    }
843}