moved new SQLite backend support modules to the top level
[chiroptera.git] / chiroptera / mfilter.d
blobb3574ef4e7900b49b2ad2d0468cb4a749526a756
1 /* E-Mail Client
2 * coded by Ketmar // Invisible Vector <ketmar@ketmar.no-ip.org>
3 * Understanding is not required. Only obedience.
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, version 3 of the License ONLY.
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
14 * You should have received a copy of the GNU General Public License
15 * along with this program. If not, see <http://www.gnu.org/licenses/>.
17 module chiroptera.mfilter is aliced;
19 import iv.strex;
22 // ////////////////////////////////////////////////////////////////////////// //
23 mixin(NewExceptionClass!("FilterSyntaxException", "Exception"));
26 // ////////////////////////////////////////////////////////////////////////// //
27 class FilterHelper {
28 enum Action {
29 Nothing, // do nothing
30 Delete, // delete message
31 SoftDelete, // "soft-delete" message (mark as read, strike through, but don't remove)
32 Spam, // mark message as spam
33 Ham, // mark message as ham
34 Read, // mark message as read
35 Stop, // do not process other filters
36 Move, // <tagname> -- move message to the specified tag
37 Exec, // <command>
40 // return `null` if there is no such header
41 abstract const(char)[] getHeaderField (const(char)[] header);
43 // return `null` if there is no such field
44 abstract const(char)[] getFromName ();
45 abstract const(char)[] getFromMail ();
46 abstract const(char)[] getToName ();
47 abstract const(char)[] getToMail ();
49 // returns first string from stdout
50 // it should be action name
51 abstract string exec (const(char)[] cmd);
53 abstract void move (const(char)[] dest);
55 // won't be called for `Nothing`, `Move`, `Exec` and `Stop`
56 abstract void performAction (Action action);
58 abstract bool match (const(char)[] pat, const(char)[] str, bool casesens);
60 static Action parseActionName (const(char)[] tok, out bool unknown) pure nothrow @trusted @nogc {
61 unknown = false;
62 tok = tok.xstrip;
63 if (tok.strEquCI("nothing") || tok.strEquCI("noop") || tok.strEquCI("nop")) return FilterHelper.Action.Nothing;
64 if (tok.strEquCI("delete")) return FilterHelper.Action.Delete;
65 if (tok.strEquCI("softdelete") || tok.strEquCI("soft_delete") || tok.strEquCI("soft-delete")) return FilterHelper.Action.SoftDelete;
66 if (tok.strEquCI("spam")) return FilterHelper.Action.Spam;
67 if (tok.strEquCI("ham")) return FilterHelper.Action.Ham;
68 if (tok.strEquCI("read")) return FilterHelper.Action.Read;
69 if (tok.strEquCI("stop")) return FilterHelper.Action.Stop;
70 if (tok.strEquCI("move")) return FilterHelper.Action.Move;
71 if (tok.strEquCI("exec")) return FilterHelper.Action.Exec;
72 unknown = true;
73 return FilterHelper.Action.Nothing;
78 // ////////////////////////////////////////////////////////////////////////// //
79 // filter "xx"
80 // (frommail matchcase "abc@def" OR from-mail matchcase "gex@boo") AND from-name match "zoo" move "0rare/doo" ham stop
81 // returns `false` if no more filters should be processed ("stop" action)
82 // pass `null` as `hlp` to perform syntax check only
83 public bool executeMailFilter (const(char)[] filter, FilterHelper hlp) {
84 char[256] temptoken = void;
85 const(char)[] tok;
87 // returns false if there are no more tokens
88 bool getToken () {
89 tok = null;
90 while (filter.length && filter[0] <= ' ') filter = filter[1..$];
91 if (filter.length == 0) return false;
92 char ch = filter[0];
93 if (ch == '#') return false;
94 if (ch == '"' || ch == '\'') {
95 // quoted string
96 immutable char qch = ch;
97 filter = filter[1..$];
98 usize pos = 0;
99 while (filter.length) {
100 ch = filter[0];
101 filter = filter[1..$];
102 if (ch != '\\') {
103 if (ch == qch) break;
104 } else {
105 // escape
106 if (filter.length == 0) throw new FilterSyntaxException("unfinished escape");
107 ch = filter[0];
108 filter = filter[1..$];
109 if (ch == 'x' || ch == 'X' || ch == 'u' || ch == 'U') throw new FilterSyntaxException("charcode escapes are not supported");
111 if (pos >= temptoken.length) throw new FilterSyntaxException("quoted string too long");
112 temptoken[pos++] = ch;
114 tok = temptoken[0..pos];
115 } else if (isalnum(ch)) {
116 // identifier
117 usize pos = 1;
118 while (pos < filter.length && (isalnum(filter[pos]) || filter[pos] == '-' || filter[pos] == '_')) ++pos;
119 tok = filter[0..pos];
120 filter = filter[pos..$];
121 } else {
122 // delimiter
123 if (filter.startsWith("->")) {
124 tok = "-->";
125 filter = filter[2..$];
126 } else if (filter.startsWith("-->")) {
127 tok = "-->";
128 filter = filter[3..$];
129 } else {
130 tok = filter[0..1];
131 filter = filter[1..$];
134 //writeln("TOKEN: <", tok, ">");
135 return true;
138 bool delegate (bool doskip) parseExpr;
140 // parse expression
141 // `tok` must be valid
143 <simpleexpr> is:
144 from-name <checker>
145 from-mail <checker>
146 from <checker>
147 to-name <checker>
148 to-mail <checker>
149 to <checker>
150 subj <checker>
151 header <checker>
153 <checker> is:
154 [not] {match|matchcase} <pattern>
155 [not] exists -- because checking for empty pattern is not enough ;-)
157 bool parseExprSimple (bool doskip) {
158 if (tok is null) throw new FilterSyntaxException("unexpected end of expression");
159 bool res = true;
161 // subexpression?
162 if (tok == "(") {
163 getToken();
164 res = parseExpr(doskip);
165 if (tok != ")") throw new FilterSyntaxException("missing closing paren");
166 getToken();
167 return res;
170 // simple matcher
171 enum Cmd {
172 FromName,
173 FromMail,
174 From,
175 ToName,
176 ToMail,
178 Subj,
179 Header,
181 Cmd cmd = Cmd.Header;
182 if (tok.strEquCI("fromname") || tok.strEquCI("from-name") || tok.strEquCI("from_name")) cmd = Cmd.FromName;
183 else if (tok.strEquCI("frommail") || tok.strEquCI("from-mail") || tok.strEquCI("from_mail")) cmd = Cmd.FromMail;
184 else if (tok.strEquCI("toname") || tok.strEquCI("to-name") || tok.strEquCI("to_name")) cmd = Cmd.ToName;
185 else if (tok.strEquCI("tomail") || tok.strEquCI("to-mail") || tok.strEquCI("to_mail")) cmd = Cmd.ToMail;
186 else if (tok.strEquCI("from")) cmd = Cmd.From;
187 else if (tok.strEquCI("to")) cmd = Cmd.To;
188 else if (tok.strEquCI("subj") || tok.strEquCI("subject")) cmd = Cmd.Subj;
189 else if (tok.strEquCI("header")) cmd = Cmd.Header;
190 else throw new FilterSyntaxException("unknown filter matcher \""~tok.idup~"\"");
192 const(char)[] fldname = null;
193 if (cmd == Cmd.Header) {
194 if (!getToken()) throw new FilterSyntaxException("unexpected end of expression");
195 if (tok.length == 0) throw new FilterSyntaxException("empty field name for \"header\"");
196 fldname = tok;
199 if (!getToken()) throw new FilterSyntaxException("\"match\", \"matchcase\" or \"exists\" expected");
201 bool inverse = false;
202 if (tok.strEquCI("not")) {
203 inverse = true;
204 if (!getToken()) throw new FilterSyntaxException("\"match\", \"matchcase\" or \"exists\" expected");
207 enum Matcher {
208 Match,
209 MatchCase,
210 Exists,
212 Matcher mt = Matcher.Match;
213 if (tok.strEquCI("match")) mt = Matcher.Match;
214 else if (tok.strEquCI("matchcase") || tok.strEquCI("match_case") || tok.strEquCI("match-case")) mt = Matcher.MatchCase;
215 else if (tok.strEquCI("exists")) mt = Matcher.Exists;
216 else throw new FilterSyntaxException("\"match\", \"matchcase\" or \"exists\" expected");
218 if (mt != Matcher.Exists) {
219 if (!getToken()) throw new FilterSyntaxException("pattern expected");
220 } else {
221 getToken(); // no args
224 if (!doskip && hlp !is null) {
225 const(char)[] val = null;
226 final switch (cmd) {
227 case Cmd.FromName: val = hlp.getFromName(); break;
228 case Cmd.FromMail: val = hlp.getFromMail(); break;
229 case Cmd.ToName: val = hlp.getToName(); break;
230 case Cmd.ToMail: val = hlp.getToMail(); break;
231 case Cmd.From: val = hlp.getHeaderField("From"); break;
232 case Cmd.To: val = hlp.getHeaderField("To"); break;
233 case Cmd.Subj: val = hlp.getHeaderField("Subject"); break;
234 case Cmd.Header: val = hlp.getHeaderField(fldname); break;
236 if (mt == Matcher.Exists) {
237 res = (val !is null);
238 } else {
239 res = hlp.match(tok, val, (mt == Matcher.MatchCase));
241 if (inverse) res = !res;
244 getToken(); // skip pattern
245 return res;
248 bool parseExpression (bool doskip) {
249 bool res = parseExprSimple(doskip);
250 //writeln("!!!");
251 while (tok !is null) {
252 //writeln("*** <", tok, ">");
253 char cond = 0;
254 if (tok.strEquCI("or") || tok == "|" || tok == "||") cond = '|';
255 else if (tok.strEquCI("and") || tok == "&" || tok == "&&") cond = '&';
256 else break;
257 if (!doskip) {
258 final switch (cond) {
259 case '|': if (res) doskip = true; break;
260 case '&': if (!res) doskip = true; break;
263 if (!getToken()) throw new FilterSyntaxException("expression expected");
264 immutable bool rval = parseExprSimple(doskip);
265 if (!doskip) {
266 final switch (cond) {
267 case '|': if (res) doskip = true; break;
268 case '&': if (!res) doskip = true; break;
272 return res;
275 parseExpr = &parseExpression;
277 if (!getToken()) return true;
278 immutable bool match = parseExpr(false);
280 // actions
282 move <tagname> -- move message to the specified tag
283 delete -- delete message
284 softdelete -- "soft-delete" message (mark as read, strike through, but don't remove)
285 spam -- mark message as spam
286 ham -- mark message as ham
287 read -- mark message as read
288 exec <command> -- see below
289 stop -- do not process other filters
291 bool res = true;
292 while (tok !is null) {
293 if (tok == "-->" || tok.strEquCI("do") || tok.strEquCI("perform")) {
294 getToken();
295 if (tok is null) throw new FilterSyntaxException("perform what?");
297 bool unknown;
298 FilterHelper.Action act = FilterHelper.parseActionName(tok, out unknown);
299 if (unknown) throw new FilterSyntaxException("unknown filter action \""~tok.idup~"\"");
300 getToken(); // skip action name
301 doaction:
302 if (act == FilterHelper.Action.Stop) { if (match) res = false; continue; }
303 if (act == FilterHelper.Action.Nothing) continue;
304 if (act == FilterHelper.Action.Exec) {
305 if (tok is null) throw new FilterSyntaxException("unknown filter action \"exec\" expects one argument");
306 string execres = (match && hlp !is null ? hlp.exec(tok).xstrip : null);
307 getToken(); // skip arguments
308 //TODO: process "move" here!
309 act = FilterHelper.parseActionName(execres, out unknown);
310 if (act == FilterHelper.Action.Exec || execres.startsWithCI("exec")) throw new FilterSyntaxException("filter action \"exec\" cannot perform recursive execs");
311 if (act == FilterHelper.Action.Move || execres.startsWithCI("move")) throw new FilterSyntaxException("filter action \"exec\" cannot move messages yet");
312 goto doaction;
314 if (act == FilterHelper.Action.Move) {
315 if (tok is null) throw new FilterSyntaxException("unknown filter action \"move\" expects one argument");
316 if (match && hlp !is null) hlp.move(tok);
317 getToken(); // skip arguments
318 continue;
320 if (match && hlp !is null) hlp.performAction(act);
322 return res;