]>
Commit | Line | Data |
---|---|---|
c881fe35 DM |
1 | package PMG::RuleCache; |
2 | ||
3 | use strict; | |
4 | use warnings; | |
5 | use DBI; | |
c881fe35 DM |
6 | |
7 | use PVE::SafeSyslog; | |
8 | ||
9 | use PMG::Utils; | |
10 | use PMG::RuleDB; | |
11 | use Digest::SHA; | |
12 | ||
13 | my $ocache_size = 1023; | |
14 | ||
15 | sub new { | |
16 | my ($type, $ruledb) = @_; | |
17 | ||
18 | my $self; | |
19 | ||
20 | $self->{ruledb} = $ruledb; | |
21 | $self->{ocache} = (); | |
22 | ||
23 | bless $self, $type; | |
24 | ||
25 | my $rules = (); | |
26 | ||
27 | my $dbh = $ruledb->{dbh}; | |
28 | ||
29 | my $sha1 = Digest::SHA->new; | |
30 | ||
64ba4c0d DC |
31 | my $type_map = { |
32 | 0 => "from", | |
33 | 1 => "to", | |
34 | 2 => "when", | |
35 | 3 => "what", | |
36 | 4 => "action", | |
37 | }; | |
38 | ||
c881fe35 DM |
39 | eval { |
40 | $dbh->begin_work; | |
41 | ||
42 | # read a consistent snapshot | |
43 | $dbh->do("SET TRANSACTION ISOLATION LEVEL SERIALIZABLE"); | |
44 | ||
45 | my $sth = $dbh->prepare( | |
46 | "SELECT ID, Name, Priority, Active, Direction FROM Rule " . | |
47 | "where Active > 0 " . | |
f6b69037 | 48 | "ORDER BY Priority DESC, ID DESC"); |
c881fe35 DM |
49 | |
50 | $sth->execute(); | |
51 | ||
52 | while (my $ref = $sth->fetchrow_hashref()) { | |
53 | my $ruleid = $ref->{id}; | |
54 | my $rule = PMG::RuleDB::Rule->new( | |
55 | $ref->{name}, $ref->{priority}, $ref->{active}, | |
56 | $ref->{direction}); | |
57 | ||
58 | $rule->{id} = $ruleid; | |
59 | push @$rules, $rule; | |
60 | ||
c9bb8609 DM |
61 | $sha1->add(join(',', $ref->{id}, $ref->{name}, $ref->{priority}, $ref->{active}, |
62 | $ref->{direction}) . "|"); | |
c881fe35 | 63 | |
64ba4c0d DC |
64 | $self->{"$ruleid:from"} = { groups => [] }; |
65 | $self->{"$ruleid:to"} = { groups => [] }; | |
66 | $self->{"$ruleid:when"} = { groups => [] }; | |
67 | $self->{"$ruleid:what"} = { groups => [] }; | |
68 | $self->{"$ruleid:action"} = { groups => [] }; | |
c881fe35 | 69 | |
4ffb26d2 DC |
70 | my $attribute_sth = $dbh->prepare("SELECT * FROM Rule_Attributes WHERE Rule_ID = ? ORDER BY Name"); |
71 | $attribute_sth->execute($ruleid); | |
72 | ||
73 | my $rule_attributes = []; | |
74 | while (my $ref = $attribute_sth->fetchrow_hashref()) { | |
75 | if ($ref->{name} =~ m/^(from|to|when|what)-(and|invert)$/) { | |
76 | my $type = $1; | |
77 | my $prop = $2; | |
78 | my $value = $ref->{value}; | |
79 | $self->{"${ruleid}:${type}"}->{$prop} = $value; | |
80 | ||
81 | $sha1->add("${ruleid}:${type}-${prop}=${value}|"); | |
82 | } | |
83 | } | |
84 | ||
c881fe35 DM |
85 | my $sth1 = $dbh->prepare( |
86 | "SELECT Objectgroup_ID, Grouptype FROM RuleGroup " . | |
87 | "where RuleGroup.Rule_ID = '$ruleid' " . | |
88 | "ORDER BY Grouptype, Objectgroup_ID"); | |
89 | ||
90 | $sth1->execute(); | |
91 | while (my $ref1 = $sth1->fetchrow_hashref()) { | |
92 | my $gtype = $ref1->{grouptype}; | |
93 | my $groupid = $ref1->{objectgroup_id}; | |
64ba4c0d | 94 | my $objects = []; |
c881fe35 DM |
95 | |
96 | my $sth2 = $dbh->prepare( | |
97 | "SELECT ID FROM Object where Objectgroup_ID = '$groupid' " . | |
98 | "ORDER BY ID"); | |
99 | $sth2->execute(); | |
100 | while (my $ref2 = $sth2->fetchrow_hashref()) { | |
101 | my $objid = $ref2->{'id'}; | |
102 | my $obj = $self->_get_object($objid); | |
103 | ||
104 | $sha1->add (join (',', $objid, $gtype, $groupid) . "|"); | |
105 | $sha1->add ($obj->{digest}, "|"); | |
106 | ||
64ba4c0d DC |
107 | push @$objects, $obj; |
108 | ||
109 | if ($gtype == 3) { # what | |
5e809f47 DC |
110 | if ($obj->otype == PMG::RuleDB::ArchiveFilter->otype || |
111 | $obj->otype == PMG::RuleDB::MatchArchiveFilename->otype) | |
112 | { | |
c881fe35 DM |
113 | if ($rule->{direction} == 0) { |
114 | $self->{archivefilter_in} = 1; | |
115 | } elsif ($rule->{direction} == 1) { | |
116 | $self->{archivefilter_out} = 1; | |
117 | } else { | |
118 | $self->{archivefilter_in} = 1; | |
119 | $self->{archivefilter_out} = 1; | |
120 | } | |
121 | } | |
122 | } elsif ($gtype == 4) { # action | |
c881fe35 DM |
123 | $self->{"$ruleid:final"} = 1 if $obj->final(); |
124 | } | |
125 | } | |
126 | $sth2->finish(); | |
64ba4c0d DC |
127 | |
128 | my $group = { | |
129 | objects => $objects, | |
130 | }; | |
131 | ||
4ffb26d2 DC |
132 | my $objectgroup_sth = $dbh->prepare("SELECT * FROM Objectgroup_Attributes WHERE Objectgroup_ID = ?"); |
133 | $objectgroup_sth->execute($groupid); | |
134 | ||
135 | while (my $ref = $objectgroup_sth->fetchrow_hashref()) { | |
136 | $group->{and} = $ref->{value} if $ref->{name} eq 'and'; | |
137 | $group->{invert} = $ref->{value} if $ref->{name} eq 'invert'; | |
138 | } | |
139 | $sha1->add (join(',', $groupid, $group->{and} // 0, $group->{invert} // 0), "|"); | |
140 | ||
64ba4c0d DC |
141 | my $type = $type_map->{$gtype}; |
142 | push $self->{"$ruleid:$type"}->{groups}->@*, $group; | |
c881fe35 DM |
143 | } |
144 | ||
145 | $sth1->finish(); | |
c881fe35 DM |
146 | } |
147 | ||
148 | # Cache Greylist Exclusion | |
149 | $sth = $dbh->prepare( | |
150 | "SELECT object.id FROM object, objectgroup " . | |
151 | "WHERE class = 'greylist' AND " . | |
152 | "objectgroup.id = object.objectgroup_id " . | |
153 | "ORDER BY object.id"); | |
154 | ||
155 | $sth->execute(); | |
156 | my $grey_excl_sender = (); | |
157 | my $grey_excl_receiver = (); | |
158 | while (my $ref2 = $sth->fetchrow_hashref()) { | |
159 | my $obj = $self->_get_object ($ref2->{'id'}); | |
160 | ||
161 | if ($obj->receivertest()) { | |
162 | push @$grey_excl_receiver, $obj; | |
163 | } else { | |
164 | push @$grey_excl_sender, $obj; | |
165 | } | |
c9bb8609 | 166 | $sha1->add ($ref2->{'id'}, "|"); |
c881fe35 DM |
167 | $sha1->add ($obj->{digest}, "|"); |
168 | } | |
169 | ||
170 | $self->{"greylist:sender"} = $grey_excl_sender; | |
171 | $self->{"greylist:receiver"} = $grey_excl_receiver; | |
172 | ||
173 | $sth->finish(); | |
174 | }; | |
175 | my $err = $@; | |
176 | ||
177 | $dbh->rollback; # end transaction | |
178 | ||
b902c0b8 | 179 | syslog ('err', "unable to load rulecache : $err") if $err; |
c881fe35 DM |
180 | |
181 | $self->{rules} = $rules; | |
182 | ||
183 | $self->{digest} = $sha1->hexdigest; | |
184 | ||
185 | return $self; | |
186 | } | |
187 | ||
188 | sub final { | |
189 | my ($self, $ruleid) = @_; | |
190 | ||
9ef3f143 | 191 | defined($ruleid) || die "undefined rule id: ERROR"; |
c881fe35 DM |
192 | |
193 | return $self->{"$ruleid:final"}; | |
194 | } | |
195 | ||
196 | sub rules { | |
197 | my ($self) = @_; | |
198 | ||
199 | $self->{rules}; | |
200 | } | |
201 | ||
202 | sub _get_object { | |
203 | my ($self, $objid) = @_; | |
204 | ||
205 | my $cid = $objid % $ocache_size; | |
206 | ||
207 | my $obj = $self->{ocache}[$cid]; | |
208 | ||
209 | if (!defined ($obj) || $obj->{id} != $objid) { | |
210 | $obj = $self->{ruledb}->load_object($objid); | |
211 | $self->{ocache}[$cid] = $obj; | |
212 | } | |
213 | ||
9ef3f143 | 214 | $obj || die "unable to get object $objid: ERROR"; |
c881fe35 DM |
215 | |
216 | return $obj; | |
217 | } | |
218 | ||
219 | sub get_actions { | |
220 | my ($self, $ruleid) = @_; | |
221 | ||
9ef3f143 | 222 | defined($ruleid) || die "undefined rule id: ERROR"; |
c881fe35 | 223 | |
64ba4c0d DC |
224 | my $actions = $self->{"$ruleid:action"}; |
225 | ||
226 | return undef if scalar($actions->{groups}->@*) == 0; | |
227 | ||
228 | my $res = []; | |
229 | for my $action ($actions->{groups}->@*) { | |
230 | push $res->@*, $action->{objects}->@*; | |
231 | } | |
232 | return $res; | |
c881fe35 DM |
233 | } |
234 | ||
235 | sub greylist_match { | |
236 | my ($self, $addr, $ip) = @_; | |
237 | ||
238 | my $grey = $self->{"greylist:sender"}; | |
239 | ||
240 | foreach my $obj (@$grey) { | |
241 | if ($obj->who_match ($addr, $ip)) { | |
242 | return 1; | |
243 | } | |
244 | } | |
245 | ||
246 | return 0; | |
247 | } | |
248 | ||
249 | sub greylist_match_receiver { | |
250 | my ($self, $addr) = @_; | |
251 | ||
252 | my $grey = $self->{"greylist:receiver"}; | |
253 | ||
254 | foreach my $obj (@$grey) { | |
255 | if ($obj->who_match($addr)) { | |
256 | return 1; | |
257 | } | |
258 | } | |
259 | ||
260 | return 0; | |
261 | } | |
262 | ||
263 | sub from_match { | |
264 | my ($self, $ruleid, $addr, $ip, $ldap) = @_; | |
265 | ||
266 | my $from = $self->{"$ruleid:from"}; | |
267 | ||
64ba4c0d | 268 | return 1 if scalar($from->{groups}->@*) == 0; |
c881fe35 | 269 | |
61954d8d | 270 | # postfix prefixes ipv6 addresses with IPv6: |
a34b95db | 271 | if (defined($ip) && $ip =~ /^IPv6:(.*)/) { |
61954d8d DC |
272 | $ip = $1; |
273 | } | |
274 | ||
eaf6270f DC |
275 | return match_list_with_mode($from->{groups}, $from->{and}, $from->{invert}, sub { |
276 | my ($group) = @_; | |
277 | my $list = $group->{objects}; | |
278 | return match_list_with_mode($list, $group->{and}, $group->{invert}, sub { | |
279 | my ($obj) = @_; | |
280 | return $obj->who_match($addr, $ip, $ldap); | |
281 | }); | |
282 | }); | |
c881fe35 DM |
283 | } |
284 | ||
285 | sub to_match { | |
286 | my ($self, $ruleid, $addr, $ldap) = @_; | |
287 | ||
288 | my $to = $self->{"$ruleid:to"}; | |
289 | ||
64ba4c0d | 290 | return 1 if scalar($to->{groups}->@*) == 0; |
c881fe35 | 291 | |
eaf6270f DC |
292 | return match_list_with_mode($to->{groups}, $to->{and}, $to->{invert}, sub { |
293 | my ($group) = @_; | |
294 | my $list = $group->{objects}; | |
295 | return match_list_with_mode($list, $group->{and}, $group->{invert}, sub { | |
296 | my ($obj) = @_; | |
297 | return $obj->who_match($addr, undef, $ldap); | |
298 | }); | |
299 | }); | |
c881fe35 DM |
300 | } |
301 | ||
302 | sub when_match { | |
303 | my ($self, $ruleid, $time) = @_; | |
304 | ||
305 | my $when = $self->{"$ruleid:when"}; | |
306 | ||
64ba4c0d | 307 | return 1 if scalar($when->{groups}->@*) == 0; |
c881fe35 | 308 | |
eaf6270f DC |
309 | return match_list_with_mode($when->{groups}, $when->{and}, $when->{invert}, sub { |
310 | my ($group) = @_; | |
311 | my $list = $group->{objects}; | |
312 | return match_list_with_mode($list, $group->{and}, $group->{invert}, sub { | |
313 | my ($obj) = @_; | |
314 | return $obj->when_match($time); | |
315 | }); | |
316 | }); | |
c881fe35 DM |
317 | } |
318 | ||
319 | sub what_match { | |
320 | my ($self, $ruleid, $queue, $element, $msginfo, $dbh) = @_; | |
321 | ||
322 | my $what = $self->{"$ruleid:what"}; | |
323 | ||
90ce7abb DC |
324 | my $marks; |
325 | my $spaminfo; | |
c881fe35 | 326 | |
64ba4c0d | 327 | if (scalar($what->{groups}->@*) == 0) { |
c881fe35 DM |
328 | # match all targets |
329 | foreach my $target (@{$msginfo->{targets}}) { | |
90ce7abb | 330 | $marks->{$target} = []; |
c881fe35 | 331 | } |
90ce7abb | 332 | return ($marks, $spaminfo); |
c881fe35 DM |
333 | } |
334 | ||
fe7eae4e DC |
335 | my $what_matches = {}; |
336 | ||
64ba4c0d | 337 | for my $group ($what->{groups}->@*) { |
fe7eae4e DC |
338 | my $group_matches = {}; |
339 | my $and = $group->{and}; | |
340 | my $invert = $group->{invert}; | |
64ba4c0d DC |
341 | for my $obj ($group->{objects}->@*) { |
342 | if (!$obj->can('what_match_targets')) { | |
fe7eae4e DC |
343 | my $match = $obj->what_match($queue, $element, $msginfo, $dbh); |
344 | for my $target ($msginfo->{targets}->@*) { | |
345 | if (defined($match)) { | |
346 | push $group_matches->{$target}->@*, $match; | |
347 | } else { | |
348 | push $group_matches->{$target}->@*, undef; | |
90ce7abb | 349 | } |
64ba4c0d | 350 | } |
90ce7abb | 351 | } else { |
fe7eae4e DC |
352 | my $target_info = $obj->what_match_targets($queue, $element, $msginfo, $dbh); |
353 | for my $target ($msginfo->{targets}->@*) { | |
354 | my $match = $target_info->{$target}; | |
355 | if (defined($match)) { | |
356 | push $group_matches->{$target}->@*, $match->{marks}; | |
90ce7abb | 357 | # only save spaminfo once |
fe7eae4e DC |
358 | $spaminfo = $match->{spaminfo} if !defined($spaminfo); |
359 | } else { | |
360 | push $group_matches->{$target}->@*, undef; | |
64ba4c0d | 361 | } |
c881fe35 DM |
362 | } |
363 | } | |
364 | } | |
fe7eae4e DC |
365 | |
366 | for my $target (keys $group_matches->%*) { | |
367 | my $matches = group_match_and_invert($group_matches->{$target}, $and, $invert, $msginfo); | |
368 | push $what_matches->{$target}->@*, $matches; | |
369 | } | |
370 | } | |
371 | ||
372 | for my $target (keys $what_matches->%*) { | |
373 | my $target_marks = what_match_and_invert($what_matches->{$target}, $what->{and}, $what->{invert}); | |
374 | $marks->{$target} = $target_marks; | |
c881fe35 DM |
375 | } |
376 | ||
90ce7abb | 377 | return ($marks, $spaminfo); |
c881fe35 DM |
378 | } |
379 | ||
fe7eae4e DC |
380 | # combines matches of groups |
381 | # this is only binary, and if it matches, 'or' combines the marks | |
382 | # so that all found marks are included | |
383 | # | |
384 | # this way we can create rules like: | |
385 | # | |
386 | # --- | |
387 | # What is and combined: | |
388 | # group1: match filename .*\.pdf | |
389 | # group2: spamlevel >= 3 | |
390 | # ACTION: remove attachments | |
391 | # --- | |
392 | # which would remove attachments for all *.pdf filenames where | |
393 | # the spamlevel is >= 3 | |
394 | sub what_match_and_invert($$$) { | |
395 | my ($matches, $and, $invert) = @_; | |
396 | ||
397 | my $match_result = match_list_with_mode($matches, $and, $invert, sub { | |
398 | my ($match) = @_; | |
399 | return defined($match); | |
400 | }); | |
401 | ||
402 | if ($match_result) { | |
403 | my $res = []; | |
404 | for my $match ($matches->@*) { | |
405 | push $res->@*, $match->@* if defined($match); | |
406 | } | |
407 | return $res; | |
408 | } else { | |
409 | return undef; | |
410 | } | |
411 | } | |
412 | ||
413 | # combines group matches according to and/invert | |
414 | # since we want match groups per mime part, we must | |
415 | # look at the marks and possibly invert them | |
416 | sub group_match_and_invert($$$$) { | |
417 | my ($group_matches, $and, $invert, $msginfo) = @_; | |
418 | ||
419 | my $encountered_parts = 0; | |
420 | if ($and) { | |
421 | my $set = {}; | |
422 | my $count = scalar($group_matches->@*); | |
423 | for my $match ($group_matches->@*) { | |
424 | if (!defined($match)) { | |
425 | $set = {}; | |
426 | last; | |
427 | } | |
428 | ||
429 | if (scalar($match->@*) > 0) { | |
430 | $encountered_parts = 1; | |
431 | $set->{$_}++ for $match->@*; | |
432 | } else { | |
433 | $set->{$_}++ for (1..$msginfo->{max_aid}); | |
434 | } | |
435 | } | |
436 | ||
437 | $group_matches = undef; | |
438 | for my $key (keys $set->%*) { | |
439 | if ($set->{$key} == $count) { | |
440 | push $group_matches->@*, $key; | |
441 | } | |
442 | } | |
443 | if (defined($group_matches) && scalar($group_matches->@*) == $count && !$encountered_parts) { | |
444 | $group_matches = []; | |
445 | } | |
446 | } else { | |
447 | my $set = {}; | |
448 | for my $match ($group_matches->@*) { | |
449 | next if !defined($match); | |
450 | if (scalar($match->@*) == 0) { | |
451 | $set->{$_} = 1 for (1..$msginfo->{max_aid}); | |
452 | } else { | |
453 | $encountered_parts = 1; | |
454 | $set->{$_} = 1 for $match->@*; | |
455 | } | |
456 | } | |
457 | ||
458 | my $count = scalar(keys $set->%*); | |
459 | if ($count == $msginfo->{max_aid} && !$encountered_parts) { | |
460 | $group_matches = []; | |
461 | } elsif ($count == 0) { | |
462 | $group_matches = undef; | |
463 | } else { | |
464 | $group_matches = [keys $set->%*]; | |
465 | } | |
466 | } | |
467 | ||
468 | if ($invert) { | |
469 | $group_matches = invert_mark_list($group_matches, $msginfo->{max_aid}); | |
470 | } | |
471 | ||
472 | return $group_matches; | |
473 | } | |
474 | ||
eaf6270f DC |
475 | # calls sub with each element of $list, and and/ors/inverts the result |
476 | sub match_list_with_mode($$$$) { | |
477 | my ($list, $and, $invert, $sub) = @_; | |
478 | ||
479 | $and //= 0; | |
480 | $invert //= 0; | |
481 | ||
482 | for my $el ($list->@*) { | |
483 | my $res = $sub->($el); | |
484 | if (!$and) { | |
485 | return !$invert if $res; | |
486 | } else { | |
487 | return $invert if !$res; | |
488 | } | |
489 | } | |
490 | ||
491 | return $and != $invert; | |
492 | } | |
493 | ||
fe7eae4e DC |
494 | # inverts a list of marks with the remaining ones of the mail |
495 | # examples: | |
496 | # mail has [1,2,3,4,5] | |
497 | # | |
498 | # undef => [1,2,3,4,5] | |
499 | # [1,2] => [3,4,5] | |
500 | # [1,2,3,4,5] => undef | |
501 | # [] => undef // [] means the whole mail matched | |
502 | sub invert_mark_list($$) { | |
503 | my ($list, $max_aid) = @_; | |
504 | ||
505 | if (defined($list)) { | |
506 | my $length = scalar($list->@*); | |
507 | if ($length == 0 || $length == ($max_aid - 1)) { | |
508 | return undef; | |
509 | } | |
510 | } | |
511 | ||
512 | $list //= []; | |
513 | ||
514 | my $set = {}; | |
515 | $set->{$_} = 1 for $list->@*; | |
516 | ||
517 | my $new_list = []; | |
518 | for (my $i = 1; $i <= $max_aid; $i++) { | |
519 | if (!$set->{$i}) { | |
520 | push $new_list->@*, $i; | |
521 | } | |
522 | } | |
523 | ||
524 | return $new_list; | |
525 | } | |
526 | ||
c881fe35 | 527 | 1; |