index rebuilding is 100 times faster now. no, really! ~10000 articles in ~40MB file...
[chiroptera.git] / filterengine.d
blob5510ab914358914d500a72ef3250953ee35faca0
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, either version 3 of the License, or
8 * (at your option) any later version.
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <http://www.gnu.org/licenses/>.
18 module filterengine is aliced;
19 private:
21 import iv.cmdcon;
22 import iv.encoding;
23 import iv.strex;
25 import maildb;
28 // ////////////////////////////////////////////////////////////////////////// //
29 public struct FilterData {
30 Article art; // in/out
31 string destfolder; // in/out; set to null to delete
32 Bogo bogo = Bogo.Error; // "error" means "not checked" here
36 // ////////////////////////////////////////////////////////////////////////// //
37 private abstract class FilterAction {
38 bool perform (ref FilterData fda); // return `true` to stop
40 static FilterAction parse (ref string[] tokens) {
41 if (tokens.length == 0) return null;
42 //TODO: use CTFE to build list
43 if (tokens[0] == "move") {
44 if (tokens.length < 2) throw new Exception("invalid move rule");
45 auto res = new FilterActionMove(tokens[1]);
46 tokens = tokens[2..$];
47 return res;
49 if (tokens[0] == "delete") { tokens = tokens[1..$]; return new FilterActionDelete(); }
50 if (tokens[0] == "spam") { tokens = tokens[1..$]; return new FilterActionSpam(); }
51 if (tokens[0] == "ham") { tokens = tokens[1..$]; return new FilterActionHam(); }
52 return null;
57 private class FilterActionMove : FilterAction {
58 string folder;
60 this (string afolder) {
61 if (afolder.length == 0) throw new Exception("can't move to empty folder");
62 folder = afolder;
65 override bool perform (ref FilterData fda) {
66 conwriteln("MOVE article from '", fda.art.fromname.recodeToKOI8, "' <", fda.art.frommail.recodeToKOI8, "> (", fda.art.subj.recodeToKOI8, ") -> ", folder);
67 fda.destfolder = folder;
68 return false;
73 // final action
74 private class FilterActionDelete : FilterAction {
75 override bool perform (ref FilterData fda) {
76 conwriteln("DELETE article from '", fda.art.fromname.recodeToKOI8, "' <", fda.art.frommail.recodeToKOI8, "> (", fda.art.subj.recodeToKOI8, ")");
77 fda.destfolder = null;
78 return true;
83 private class FilterActionSpam : FilterAction {
84 override bool perform (ref FilterData fda) {
85 conwriteln("SPAM article from '", fda.art.fromname.recodeToKOI8, "' <", fda.art.frommail.recodeToKOI8, "> (", fda.art.subj.recodeToKOI8, ")");
86 fda.bogo = Bogo.Spam;
87 return false;
92 private class FilterActionHam : FilterAction {
93 override bool perform (ref FilterData fda) {
94 conwriteln("HAM article from '", fda.art.fromname.recodeToKOI8, "' <", fda.art.frommail.recodeToKOI8, "> (", fda.art.subj.recodeToKOI8, ")");
95 fda.bogo = Bogo.Ham;
96 return false;
101 // ////////////////////////////////////////////////////////////////////////// //
102 private abstract class FilterCompare {
103 bool check (const(char)[] s);
105 static FilterCompare parse (ref string[] tokens) {
106 if (tokens.length == 0) return null;
107 //TODO: use CTFE to build list
108 if (tokens[0] == "matchcase") {
109 if (tokens.length < 2) throw new Exception("invalid matchcase rule");
110 if (tokens[1].length == 0) throw new Exception("matchcase with nothing");
111 auto res = new FilterCompareMatchCase(tokens[1]);
112 tokens = tokens[2..$];
113 return res;
115 return null;
119 private class FilterCompareMatchCase : FilterCompare {
120 string pat;
122 this (string apat) { pat = apat; }
124 override bool check (const(char)[] s) { return (s.indexOf(pat) >= 0); }
128 // ////////////////////////////////////////////////////////////////////////// //
129 private abstract class FilterChecker {
130 static FilterChecker parse (ref string[] tokens) {
131 if (tokens.length == 0) return null;
132 //TODO: use CTFE to build list
133 if (tokens[0] == "fromname") {
134 if (tokens.length < 2) throw new Exception("invalid fromname rule");
135 tokens = tokens[1..$];
136 auto res = new FilterCheckerFromName();
137 res.cmp = FilterCompare.parse(tokens);
138 if (res.cmp is null) throw new Exception("invalid fromname rule");
139 return res;
141 if (tokens[0] == "frommail") {
142 if (tokens.length < 2) throw new Exception("invalid frommail rule");
143 tokens = tokens[1..$];
144 auto res = new FilterCheckerFromMail();
145 res.cmp = FilterCompare.parse(tokens);
146 if (res.cmp is null) throw new Exception("invalid frommail rule");
147 return res;
149 if (tokens[0] == "header") {
150 if (tokens.length < 2) throw new Exception("invalid header rule");
151 auto res = new FilterCheckerHeaderField(tokens[1]);
152 tokens = tokens[2..$];
153 res.cmp = FilterCompare.parse(tokens);
154 if (res.cmp is null) throw new Exception("invalid frommail rule");
155 return res;
157 if (tokens[0] == "to") {
158 if (tokens.length < 2) throw new Exception("invalid to rule");
159 auto res = new FilterCheckerHeaderField("to");
160 tokens = tokens[1..$];
161 res.cmp = FilterCompare.parse(tokens);
162 if (res.cmp is null) throw new Exception("invalid frommail rule");
163 return res;
165 if (tokens[0] == "to_or_cc") {
166 if (tokens.length < 2) throw new Exception("invalid to_or_cc rule");
167 tokens = tokens[1..$];
168 auto c0 = new FilterCheckerHeaderField("to");
169 c0.cmp = FilterCompare.parse(tokens);
170 if (c0.cmp is null) throw new Exception("invalid to_or_cc rule");
171 auto c1 = new FilterCheckerHeaderField("cc");
172 c1.cmp = c0.cmp;
173 auto res = new FilterCheckerBinExpr!"or"(c0, c1);
174 return res;
176 return null;
179 static FilterChecker parseCheckers (ref string[] tokens) {
180 FilterChecker res = parse(tokens);
181 if (res is null || tokens.length == 0) return res;
182 if (tokens[0] == "|" || tokens[0] == "&") {
183 if (tokens.length < 2) throw new Exception("invalid logic op");
184 string op = tokens[0];
185 tokens = tokens[1..$];
186 FilterChecker right = parseCheckers(tokens);
187 if (right is null) throw new Exception("invalid logic op");
188 if (op == "|") res = new FilterCheckerBinExpr!"or"(res, right);
189 else if (op == "&") res = new FilterCheckerBinExpr!"and"(res, right);
190 else throw new Exception("invalid logic op");
192 return res;
195 bool check (ref FilterData fda);
199 // ////////////////////////////////////////////////////////////////////////// //
200 private class FilterCheckerBinExpr(string op) : FilterChecker if (op == "or" || op == "and") {
201 FilterChecker left, right;
203 this (FilterChecker l, FilterChecker r) { left = l; right = r; }
205 override bool check (ref FilterData fda) {
206 bool res = (left !is null ? left.check(fda) : false);
207 static if (op == "or") {
208 if (res) return true;
209 if (right is null) return false;
210 return right.check(fda);
211 } else static if (op == "and") {
212 if (!res) return false;
213 if (right is null) return false;
214 return right.check(fda);
215 } else {
216 static assert(0, "wtf?!");
222 // ////////////////////////////////////////////////////////////////////////// //
223 private abstract class FilterCheckerHeaderCompare : FilterChecker {
224 FilterCompare cmp;
226 override bool check (ref FilterData fda) {
227 if (cmp is null) return false;
228 return cmp.check(getCompareString(fda));
231 const(char)[] getCompareString (ref FilterData fda);
234 private class FilterCheckerFromName : FilterCheckerHeaderCompare {
235 override const(char)[] getCompareString (ref FilterData fda) { return fda.art.fromname; }
238 private class FilterCheckerFromMail : FilterCheckerHeaderCompare {
239 override const(char)[] getCompareString (ref FilterData fda) { return fda.art.frommail; }
242 private class FilterCheckerHeaderField : FilterCheckerHeaderCompare {
243 string fldname;
245 this (string afldname) {
246 if (afldname.length == 0) throw new Exception("no field name");
247 fldname = afldname;
250 override const(char)[] getCompareString (ref FilterData fda) {
251 auto hproc = fda.art.headersIterator;
252 while (!hproc.empty) {
253 if (hproc.fieldName.strEquCI(fldname)) return hproc.curline;
254 hproc.popFront();
256 return null;
261 // ////////////////////////////////////////////////////////////////////////// //
262 public final class Filter {
263 public:
264 string name;
265 FilterChecker checker;
266 FilterAction[] actions;
267 bool stop;
269 public:
270 this (ConString cli) {
271 string[] tokens;
272 for (;;) {
273 auto tk = ConCommand.getWord(cli);
274 if (tk is null) break;
275 tokens ~= tk.idup;
277 if (tokens.length < 1) throw new Exception("invalid filter: no name");
278 if (tokens[0].length == 0) throw new Exception("invalid filter: empty name");
279 if (tokens.length < 2) throw new Exception("invalid filter '"~tokens[0]~"': no rules");
280 //conwriteln("=======================");
281 name = tokens[0];
282 tokens = tokens[1..$];
283 //conwriteln("00tokens: ", tokens);
284 try {
285 checker = FilterChecker.parseCheckers(tokens);
286 //conwriteln("01tokens: ", tokens);
287 if (checker is null) throw new Exception("no checker");
288 while (tokens.length) {
289 auto action = FilterAction.parse(tokens);
290 //conwriteln("02tokens: ", tokens);
291 if (action is null) break;
292 actions ~= action;
293 if (tokens.length > 0 && tokens[0] == "stop") {
294 stop = true;
295 tokens = tokens[1..$];
298 if (tokens.length) {
299 //conwriteln("03tokens: ", tokens);
300 throw new Exception("extra tokens");
302 if (actions.length == 0) throw new Exception("no checker");
303 } catch (Exception e) {
304 conwriteln("**** TOKENS LEFT: ", tokens);
305 throw new Exception("filter '"~name~"' error: "~e.msg);
311 // ////////////////////////////////////////////////////////////////////////// //
312 __gshared Filter[] prefilters, postfilters;
315 shared static this () {
316 conRegFunc!((ConFuncVA cmdl) {
317 try {
318 auto flt = new Filter(cmdl.cmdline);
319 conwriteln("pre_filter: '", flt.name, "'");
320 foreach (ref ff; prefilters) {
321 if (ff.name == flt.name) { ff = flt; return; } // replace
323 prefilters ~= flt;
324 } catch (Exception e) {
325 conwriteln("FILTER ERROR: ", e.msg);
326 conwriteln(cmdl.cmdline);
328 })("pre_filter", "add pre-spamcheck filter");
330 conRegFunc!((ConFuncVA cmdl) {
331 try {
332 auto flt = new Filter(cmdl.cmdline);
333 conwriteln("filter: '", flt.name, "'");
334 foreach (ref ff; postfilters) {
335 if (ff.name == flt.name) { ff = flt; return; } // replace
337 postfilters ~= flt;
338 } catch (Exception e) {
339 conwriteln("FILTER ERROR: ", e.msg);
340 conwriteln(cmdl.cmdline);
342 })("filter", "add post-spamcheck filter");
346 // ////////////////////////////////////////////////////////////////////////// //
347 public void doFiltering(string stage) (ref FilterData fda) if (stage == "pre" || stage == "post") {
348 static if (stage == "pre") Filter[] list = prefilters;
349 else static if (stage == "post") Filter[] list = postfilters;
350 else static assert(0, "wtf?!");
351 foreach (Filter flt; list) {
352 if (flt.checker.check(fda)) {
353 conwriteln((flt.stop ? "FINAL " : ""), "FILTER '", flt.name, " HIT at article from '", fda.art.fromname.recodeToKOI8, "' <", fda.art.frommail.recodeToKOI8, "> (", fda.art.subj.recodeToKOI8, ")");
354 bool stop = false;
355 foreach (auto act; flt.actions) {
356 if (act.perform(fda)) { stop = true; break; }
358 if (flt.stop) return;
359 if (stop) {
360 conwriteln("STOP FILTER '", flt.name, " HIT at article from '", fda.art.fromname.recodeToKOI8, "' <", fda.art.frommail.recodeToKOI8, "> (", fda.art.subj.recodeToKOI8, ")");
361 return;