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 chibackend
.mfilter
is aliced
;
21 import chibackend
: DynStr
;
24 // ////////////////////////////////////////////////////////////////////////// //
25 mixin(NewExceptionClass
!("FilterSyntaxException", "Exception"));
28 // ////////////////////////////////////////////////////////////////////////// //
31 Nothing
, // do nothing
32 Delete
, // delete message
33 SoftDelete
, // "soft-delete" message (mark as read, strike through, but don't remove)
34 Spam
, // mark message as spam
35 Ham
, // mark message as ham
36 Read
, // mark message as read
37 Stop
, // do not process other filters
38 Move
, // <tagname> -- move message to the specified tag
42 // called if a filter was matched
43 abstract void filterMatched ();
45 abstract DynStr
getAccount ();
47 abstract DynStr
getHeaderField (const(char)[] header
, out bool exists
);
49 abstract DynStr
getFromName ();
50 abstract DynStr
getFromMail ();
51 abstract DynStr
getToName ();
52 abstract DynStr
getToMail ();
53 abstract DynStr
getSubj (out bool exists
);
55 // returns first string from stdout
56 // it should be action name
57 abstract DynStr
exec (const(char)[] cmd
);
59 abstract void move (const(char)[] dest
);
61 // won't be called for `Nothing`, `Move`, `Exec` and `Stop`
62 abstract void performAction (Action action
);
64 abstract bool match (const(char)[] pat
, const(char)[] str, bool casesens
);
66 static Action
parseActionName (const(char)[] tok
, out bool unknown
) pure nothrow @trusted @nogc {
69 if (tok
.strEquCI("nothing") || tok
.strEquCI("noop") || tok
.strEquCI("nop")) return FilterHelper
.Action
.Nothing
;
70 if (tok
.strEquCI("delete")) return FilterHelper
.Action
.Delete
;
71 if (tok
.strEquCI("softdelete") || tok
.strEquCI("soft_delete") || tok
.strEquCI("soft-delete")) return FilterHelper
.Action
.SoftDelete
;
72 if (tok
.strEquCI("spam")) return FilterHelper
.Action
.Spam
;
73 if (tok
.strEquCI("ham")) return FilterHelper
.Action
.Ham
;
74 if (tok
.strEquCI("read")) return FilterHelper
.Action
.Read
;
75 if (tok
.strEquCI("stop")) return FilterHelper
.Action
.Stop
;
76 if (tok
.strEquCI("move")) return FilterHelper
.Action
.Move
;
77 if (tok
.strEquCI("exec")) return FilterHelper
.Action
.Exec
;
79 return FilterHelper
.Action
.Nothing
;
84 // ////////////////////////////////////////////////////////////////////////// //
86 // (frommail matchcase "abc@def" OR from-mail matchcase "gex@boo") AND from-name match "zoo" move "0rare/doo" ham stop
87 // returns `false` if no more filters should be processed ("stop" action)
88 // pass `null` as `hlp` to perform syntax check only
89 public bool executeMailFilter (const(char)[] filter
, FilterHelper hlp
) {
90 char[256] temptoken
= void;
94 // returns false if there are no more tokens
97 while (filter
.length
&& filter
[0] <= ' ') filter
= filter
[1..$];
98 if (filter
.length
== 0) return false;
100 if (ch
== '#') return false;
101 if (ch
== '"' || ch
== '\'') {
103 immutable char qch
= ch
;
104 filter
= filter
[1..$];
106 while (filter
.length
) {
108 filter
= filter
[1..$];
110 if (ch
== qch
) break;
113 if (filter
.length
== 0) throw new FilterSyntaxException("unfinished escape");
115 filter
= filter
[1..$];
116 if (ch
== 'x' || ch
== 'X' || ch
== 'u' || ch
== 'U') throw new FilterSyntaxException("charcode escapes are not supported");
118 if (pos
>= temptoken
.length
) throw new FilterSyntaxException("quoted string too long");
119 temptoken
[pos
++] = ch
;
121 tok
= temptoken
[0..pos
];
122 } else if ("()[]!".indexOf(filter
[0]) >= 0) {
125 filter
= filter
[1..$];
126 } else if (filter
[0] == '&') {
128 filter
= filter
[(filter
.length
> 1 && filter
[1] == '&' ?
2 : 1)..$];
129 } else if (filter
[0] == '|') {
131 filter
= filter
[(filter
.length
> 1 && filter
[1] == '|' ?
2 : 1)..$];
132 } else if (filter
[0] == '-' && (filter
.startsWith("-->") || filter
.startsWith("->"))) {
135 filter
= filter
[(filter
[1] == '>' ?
2 : 3)..$];
137 // just a token until a space or a special char
139 while (pos
< filter
.length
) {
141 if (ch
<= 32 ||
`"'()[]!&|`.indexOf(ch
) >= 0) break;
143 if (filter
[pos
..$].startsWith("->") || filter
[pos
..$].startsWith("-->")) break;
147 tok
= filter
[0..pos
];
148 filter
= filter
[pos
..$];
150 //{ import iv.vfs.io; writeln("TOKEN: <", tok, ">"); }
154 bool delegate (bool doskip
) parseExpr
;
157 // `tok` must be valid
170 [not] {match|matchcase} <pattern>
171 [not] exists -- because checking for empty pattern is not enough ;-)
173 bool parseExprSimple (bool doskip
) {
174 if (tok
is null) throw new FilterSyntaxException("unexpected end of expression");
180 res
= parseExpr(doskip
);
181 if (tok
!= ")") throw new FilterSyntaxException("missing closing paren");
198 Cmd cmd
= Cmd
.Header
;
199 if (tok
.strEquCI("fromname") || tok
.strEquCI("from-name") || tok
.strEquCI("from_name")) cmd
= Cmd
.FromName
;
200 else if (tok
.strEquCI("frommail") || tok
.strEquCI("from-mail") || tok
.strEquCI("from_mail")) cmd
= Cmd
.FromMail
;
201 else if (tok
.strEquCI("toname") || tok
.strEquCI("to-name") || tok
.strEquCI("to_name")) cmd
= Cmd
.ToName
;
202 else if (tok
.strEquCI("tomail") || tok
.strEquCI("to-mail") || tok
.strEquCI("to_mail")) cmd
= Cmd
.ToMail
;
203 else if (tok
.strEquCI("from")) cmd
= Cmd
.From
;
204 else if (tok
.strEquCI("to")) cmd
= Cmd
.To
;
205 else if (tok
.strEquCI("subj") || tok
.strEquCI("subject")) cmd
= Cmd
.Subj
;
206 else if (tok
.strEquCI("account")) cmd
= Cmd
.Account
;
207 else if (tok
.strEquCI("header")) cmd
= Cmd
.Header
;
208 else throw new FilterSyntaxException("unknown filter matcher \""~tok
.idup
~"\"");
211 if (cmd
== Cmd
.Header
) {
212 if (!getToken()) throw new FilterSyntaxException("unexpected end of expression");
213 if (tok
.length
== 0) throw new FilterSyntaxException("empty field name for \"header\"");
217 if (!getToken()) throw new FilterSyntaxException("\"match\", \"matchcase\" or \"exists\" expected");
219 bool inverse
= false;
220 if (tok
.strEquCI("not")) {
222 if (!getToken()) throw new FilterSyntaxException("\"match\", \"matchcase\" or \"exists\" expected");
230 Matcher mt
= Matcher
.Match
;
231 if (tok
.strEquCI("match")) mt
= Matcher
.Match
;
232 else if (tok
.strEquCI("matchcase") || tok
.strEquCI("match_case") || tok
.strEquCI("match-case")) mt
= Matcher
.MatchCase
;
233 else if (tok
.strEquCI("exists")) mt
= Matcher
.Exists
;
234 else throw new FilterSyntaxException("\"match\", \"matchcase\" or \"exists\" expected");
236 if (mt
!= Matcher
.Exists
) {
237 if (!getToken()) throw new FilterSyntaxException("pattern expected");
239 getToken(); // no args
242 if (!doskip
&& hlp
!is null) {
246 case Cmd
.FromName
: val
= hlp
.getFromName(); exists
= true; break;
247 case Cmd
.FromMail
: val
= hlp
.getFromMail(); exists
= true; break;
248 case Cmd
.ToName
: val
= hlp
.getToName(); exists
= true; break;
249 case Cmd
.ToMail
: val
= hlp
.getToMail(); exists
= true; break;
250 case Cmd
.From
: val
= hlp
.getHeaderField("From", out exists
); break;
251 case Cmd
.To
: val
= hlp
.getHeaderField("To", out exists
); break;
252 case Cmd
.Subj
: val
= hlp
.getSubj(out exists
); break;
253 case Cmd
.Account
: val
= hlp
.getAccount(); exists
= (val
.length() != 0); break;
254 case Cmd
.Header
: val
= hlp
.getHeaderField(fldname
, out exists
); break;
256 if (mt
== Matcher
.Exists
) {
259 res
= hlp
.match(tok
, val
, (mt
== Matcher
.MatchCase
));
261 if (inverse
) res
= !res
;
264 getToken(); // skip pattern
268 bool parseExpression (bool doskip
) {
269 bool res
= parseExprSimple(doskip
);
271 while (tok
!is null) {
272 //writeln("*** <", tok, ">");
274 if (tok
.strEquCI("or") || tok
== "||") cond
= '|';
275 else if (tok
.strEquCI("and") || tok
== "&&") cond
= '&';
278 final switch (cond
) {
279 case '|': if (res
) doskip
= true; break;
280 case '&': if (!res
) doskip
= true; break;
283 if (!getToken()) throw new FilterSyntaxException("expression expected");
284 immutable bool rval
= parseExprSimple(doskip
);
286 final switch (cond
) {
287 case '|': res
= (res || rval
); if (res
) doskip
= true; break;
288 case '&': res
= (res
&& rval
); if (!res
) doskip
= true; break;
295 parseExpr
= &parseExpression
;
297 if (!getToken()) return true;
298 immutable bool match
= parseExpr(false);
300 if (match
&& hlp
!is null) hlp
.filterMatched();
304 move <tagname> -- move message to the specified tag
305 delete -- delete message
306 softdelete -- "soft-delete" message (mark as read, strike through, but don't remove)
307 spam -- mark message as spam
308 ham -- mark message as ham
309 read -- mark message as read
310 exec <command> -- see below
311 stop -- do not process other filters
314 while (tok
!is null) {
315 if (tok
== "-->" || tok
.strEquCI("do") || tok
.strEquCI("perform") || tok
.strEquCI("and") || tok
== "&&") {
317 if (tok
is null) throw new FilterSyntaxException("perform what?");
320 FilterHelper
.Action act
= FilterHelper
.parseActionName(tok
, out unknown
);
321 if (unknown
) throw new FilterSyntaxException("unknown filter action \""~tok
.idup
~"\"");
322 getToken(); // skip action name
324 if (act
== FilterHelper
.Action
.Stop
) {
327 if (hlp
!is null) hlp
.performAction(act
);
331 if (act
== FilterHelper
.Action
.Nothing
) continue;
332 if (act
== FilterHelper
.Action
.Exec
) {
333 if (tok
is null) throw new FilterSyntaxException("unknown filter action \"exec\" expects one argument");
335 DynStr eres
= (hlp
!is null ? hlp
.exec(tok
).xstrip
: "nothing");
336 const(char)[] execres
= eres
.getData
;
337 //TODO: process "move" here!
338 immutable bool doStop
= (execres
.length
&& execres
[0] == '-');
340 execres
= execres
[1..$];
342 if (hlp
!is null) hlp
.performAction(act
);
344 if (execres
.startsWithCI("move")) {
345 execres
= execres
[4..$];
346 if (execres
.length
== 0 || execres
[0] > 32) throw new FilterSyntaxException("filter action \"exec\" tried to move the message to the empty tag");
347 execres
= execres
.xstrip
;
348 if (execres
.length
== 0) throw new FilterSyntaxException("filter action \"exec\" tried to move the message to the blank tag");
349 //throw new FilterSyntaxException("filter action \"exec\" cannot move messages yet");
350 tok
= execres
.dup
; // need to copy due to DynStr
351 // do not skip arguments here, "move" handler will do it for us
354 getToken(); // skip arguments
355 act
= FilterHelper
.parseActionName(execres
, out unknown
);
356 if (act
== FilterHelper
.Action
.Exec || execres
.startsWithCI("exec")) throw new FilterSyntaxException("filter action \"exec\" cannot perform recursive execs");
359 getToken(); // skip arguments
363 if (act
== FilterHelper
.Action
.Move
) {
365 if (tok
is null) throw new FilterSyntaxException("unknown filter action \"move\" expects one argument");
368 while (tok
.length
&& tok
[$-1] == '/') tok
= tok
[0..$-1].xstrip
;
369 if (tok
.length
== 0) throw new FilterSyntaxException("\"move\" expects non-empty tag name");
370 if (tok
.indexOf("//") >= 0) throw new FilterSyntaxException("\"move\" tag name cannot contain \"//\"");
371 if ((tok
[0].isalnum || tok
[0] >= 128) && tok
.indexOf(':') < 0) {
376 hlp
.move(tn
.getData
);
379 if (match
&& hlp
!is null) hlp
.move(tok
);
381 getToken(); // skip arguments
384 if (match
&& hlp
!is null) hlp
.performAction(act
);