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
;
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..$];
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(); }
57 private class FilterActionMove
: FilterAction
{
60 this (string afolder
) {
61 if (afolder
.length
== 0) throw new Exception("can't move to empty folder");
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
;
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;
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
, ")");
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
, ")");
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..$];
119 private class FilterCompareMatchCase
: FilterCompare
{
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");
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");
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");
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");
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");
173 auto res
= new FilterCheckerBinExpr
!"or"(c0
, c1
);
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");
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
);
216 static assert(0, "wtf?!");
222 // ////////////////////////////////////////////////////////////////////////// //
223 private abstract class FilterCheckerHeaderCompare
: FilterChecker
{
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
{
245 this (string afldname
) {
246 if (afldname
.length
== 0) throw new Exception("no field name");
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
;
261 // ////////////////////////////////////////////////////////////////////////// //
262 public final class Filter
{
265 FilterChecker checker
;
266 FilterAction
[] actions
;
270 this (ConString
cli) {
273 auto tk
= ConCommand
.getWord(cli);
274 if (tk
is null) break;
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("=======================");
282 tokens
= tokens
[1..$];
283 //conwriteln("00tokens: ", tokens);
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;
293 if (tokens
.length
> 0 && tokens
[0] == "stop") {
295 tokens
= tokens
[1..$];
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
) {
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
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
) {
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
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
, ")");
355 foreach (auto act
; flt
.actions
) {
356 if (act
.perform(fda
)) { stop
= true; break; }
358 if (flt
.stop
) return;
360 conwriteln("STOP FILTER '", flt
.name
, " HIT at article from '", fda
.art
.fromname
.recodeToKOI8
, "' <", fda
.art
.frommail
.recodeToKOI8
, "> (", fda
.art
.subj
.recodeToKOI8
, ")");