mailrs_sieve_core/ast.rs
1//! RFC 5228 §3-4 AST.
2
3/// One Sieve command — identifier + args + optional nested block.
4#[derive(Debug, Clone, PartialEq, Eq)]
5pub struct Command {
6 /// Command identifier (e.g. `"if"`, `"keep"`, `"fileinto"`).
7 pub name: String,
8 /// Positional + tagged + test arguments, in source order.
9 pub args: Vec<Argument>,
10 /// Block body (`{ … }`) — empty for non-block commands.
11 pub block: Vec<Command>,
12}
13
14/// Argument to a command or a nested test.
15#[derive(Debug, Clone, PartialEq, Eq)]
16pub enum Argument {
17 /// `:identifier` tag.
18 Tag(String),
19 /// Numeric literal (with K/M/G already applied).
20 Number(u64),
21 /// String literal — quoted or multi-line.
22 String(String),
23 /// Bracketed string list `[ "a", "b" ]`.
24 StringList(Vec<String>),
25 /// Nested test (`header :is "Subject" "spam"`,
26 /// `allof(...)`, `anyof(...)`, `not test`, …).
27 Test(Test),
28 /// Nested test list (`allof(t1, t2)`, `anyof(t1, t2)`).
29 TestList(Vec<Test>),
30}
31
32/// One test expression. The match-types + address-parts get
33/// extracted into the dedicated fields so the evaluator doesn't
34/// scan the arg list every time.
35#[derive(Debug, Clone, PartialEq, Eq)]
36pub struct Test {
37 /// Test identifier (`"header"`, `"address"`, `"size"`, …).
38 pub name: String,
39 /// Tag arguments (`:is`, `:domain`, `:localpart`, …) in
40 /// source order.
41 pub tags: Vec<String>,
42 /// Positional args (numeric / string / string-list).
43 pub args: Vec<Argument>,
44 /// Nested tests (for `allof`, `anyof`, `not`).
45 pub children: Vec<Test>,
46}
47
48/// Standardised match-type for the evaluator.
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50pub enum MatchType {
51 /// `:is` — exact case-insensitive match.
52 Is,
53 /// `:contains` — substring case-insensitive match.
54 Contains,
55 /// `:matches` — wildcard match (`*` / `?`).
56 Matches,
57}
58
59impl MatchType {
60 /// Pick the first match-type tag from a `tags` slice, defaulting to `:is`.
61 pub fn from_tags(tags: &[String]) -> Self {
62 for t in tags {
63 match t.as_str() {
64 "is" => return Self::Is,
65 "contains" => return Self::Contains,
66 "matches" => return Self::Matches,
67 _ => {}
68 }
69 }
70 Self::Is
71 }
72}
73
74/// One action the evaluator emits.
75#[derive(Debug, Clone, PartialEq, Eq)]
76pub enum Action {
77 /// Keep the message in the default mailbox (the implicit
78 /// default when no action fires). The `flags` vector carries
79 /// any IMAP flags set by RFC 5232 `imap4flags` extension
80 /// (empty when the extension is not used).
81 Keep {
82 /// IMAP flags to attach to the kept copy (RFC 5232).
83 flags: Vec<String>,
84 },
85 /// Discard the message — drop without notice.
86 Discard,
87 /// File into a named mailbox (RFC 5228 `fileinto` ext) with
88 /// optional IMAP flags (RFC 5232 `imap4flags` ext — empty
89 /// when the extension is not used).
90 FileInto {
91 /// Destination mailbox name.
92 mailbox: String,
93 /// IMAP flags to attach to the filed copy (RFC 5232).
94 flags: Vec<String>,
95 },
96 /// Forward / redirect to another address.
97 Redirect(String),
98 /// Reject with the given reason string.
99 Reject(String),
100 /// RFC 5230 `vacation` — generate an automatic reply. The
101 /// stateful parts (dedup window, recipient detection, reply
102 /// message build) are the caller's job; this engine only
103 /// surfaces the parsed action.
104 Vacation(VacationAction),
105}
106
107/// RFC 5230 `vacation` action — everything the caller needs to
108/// generate the auto-reply.
109///
110/// Fields with `Option` default to "use the server-defined
111/// value" (per RFC 5230 §4.1–4.5). `addresses` and `mime` default
112/// to `Vec::new()` / `false`.
113#[derive(Debug, Clone, PartialEq, Eq)]
114pub struct VacationAction {
115 /// The auto-reply body (positional argument — required).
116 pub reason: String,
117 /// `:days <n>` or `:seconds <n>` — the dedup window.
118 /// `None` = server default (RFC 5230 §4.1 hints at 7 days).
119 pub period: Option<VacationPeriod>,
120 /// `:subject <s>` — overrides the default `Auto:`-prefixed
121 /// Subject (RFC 5230 §4.2).
122 pub subject: Option<String>,
123 /// `:from <addr>` — overrides the default `From:` of the auto-
124 /// reply (RFC 5230 §4.3); must be a path the mailbox owner can
125 /// legitimately use.
126 pub from: Option<String>,
127 /// `:addresses [<a>, <b>, …]` — alternate recipient addresses
128 /// the user may be addressed at; the auto-reply is only sent
129 /// if one of these matches the incoming envelope-to (RFC 5230
130 /// §4.4).
131 pub addresses: Vec<String>,
132 /// `:mime` — when true the `reason` is a full RFC 2822 MIME
133 /// entity; otherwise it's plain text (RFC 5230 §4.5).
134 pub mime: bool,
135 /// `:handle <h>` — handle for the dedup index (RFC 5230 §4.6);
136 /// distinct reasons sharing one handle are deduplicated as one.
137 pub handle: Option<String>,
138}
139
140/// The `:days` / `:seconds` window on a `vacation` action.
141#[derive(Debug, Clone, Copy, PartialEq, Eq)]
142pub enum VacationPeriod {
143 /// `:days <n>` — RFC 5230 §4.1.
144 Days(u64),
145 /// `:seconds <n>` — RFC 6131 extension (sub-day windows).
146 Seconds(u64),
147}
148
149/// RFC 5228 §5.4 envelope context — caller-supplied state the
150/// `envelope` test inspects.
151///
152/// `from` is the SMTP MAIL FROM (RFC 5321 reverse-path).
153/// `to` is the list of RCPT TO (one envelope test may match any).
154/// `auth` is the RFC 5228 §5.4 SASL-authenticated identity.
155#[derive(Debug, Clone, Default, PartialEq, Eq)]
156pub struct Envelope {
157 /// SMTP MAIL FROM (RFC 5321 reverse-path).
158 pub from: Option<String>,
159 /// SMTP RCPT TO list (RFC 5321 forward-path); the envelope
160 /// test on `"to"` matches if ANY recipient matches.
161 pub to: Vec<String>,
162 /// RFC 5228 §5.4 SASL `:auth` identity.
163 pub auth: Option<String>,
164}