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
;
22 // ////////////////////////////////////////////////////////////////////////// //
23 mixin(NewExceptionClass
!("FilterSyntaxException", "Exception"));
26 // ////////////////////////////////////////////////////////////////////////// //
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
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 {
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
;
73 return FilterHelper
.Action
.Nothing
;
78 // ////////////////////////////////////////////////////////////////////////// //
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;
87 // returns false if there are no more tokens
90 while (filter
.length
&& filter
[0] <= ' ') filter
= filter
[1..$];
91 if (filter
.length
== 0) return false;
93 if (ch
== '#') return false;
94 if (ch
== '"' || ch
== '\'') {
96 immutable char qch
= ch
;
97 filter
= filter
[1..$];
99 while (filter
.length
) {
101 filter
= filter
[1..$];
103 if (ch
== qch
) break;
106 if (filter
.length
== 0) throw new FilterSyntaxException("unfinished escape");
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
)) {
118 while (pos
< filter
.length
&& (isalnum(filter
[pos
]) || filter
[pos
] == '-' || filter
[pos
] == '_')) ++pos
;
119 tok
= filter
[0..pos
];
120 filter
= filter
[pos
..$];
123 if (filter
.startsWith("->")) {
125 filter
= filter
[2..$];
126 } else if (filter
.startsWith("-->")) {
128 filter
= filter
[3..$];
131 filter
= filter
[1..$];
134 //writeln("TOKEN: <", tok, ">");
138 bool delegate (bool doskip
) parseExpr
;
141 // `tok` must be valid
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");
164 res
= parseExpr(doskip
);
165 if (tok
!= ")") throw new FilterSyntaxException("missing closing paren");
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\"");
199 if (!getToken()) throw new FilterSyntaxException("\"match\", \"matchcase\" or \"exists\" expected");
201 bool inverse
= false;
202 if (tok
.strEquCI("not")) {
204 if (!getToken()) throw new FilterSyntaxException("\"match\", \"matchcase\" or \"exists\" expected");
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");
221 getToken(); // no args
224 if (!doskip
&& hlp
!is null) {
225 const(char)[] val
= null;
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);
239 res
= hlp
.match(tok
, val
, (mt
== Matcher
.MatchCase
));
241 if (inverse
) res
= !res
;
244 getToken(); // skip pattern
248 bool parseExpression (bool doskip
) {
249 bool res
= parseExprSimple(doskip
);
251 while (tok
!is null) {
252 //writeln("*** <", tok, ">");
254 if (tok
.strEquCI("or") || tok
== "|" || tok
== "||") cond
= '|';
255 else if (tok
.strEquCI("and") || tok
== "&" || tok
== "&&") cond
= '&';
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
);
266 final switch (cond
) {
267 case '|': if (res
) doskip
= true; break;
268 case '&': if (!res
) doskip
= true; break;
275 parseExpr
= &parseExpression
;
277 if (!getToken()) return true;
278 immutable bool match
= parseExpr(false);
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
292 while (tok
!is null) {
293 if (tok
== "-->" || tok
.strEquCI("do") || tok
.strEquCI("perform")) {
295 if (tok
is null) throw new FilterSyntaxException("perform what?");
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
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");
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
320 if (match
&& hlp
!is null) hlp
.performAction(act
);