]> git.proxmox.com Git - pmg-api.git/blame - src/PMG/RuleCache.pm
dkim: add QID in warnings
[pmg-api.git] / src / PMG / RuleCache.pm
CommitLineData
c881fe35
DM
1package PMG::RuleCache;
2
3use strict;
4use warnings;
5use DBI;
c881fe35
DM
6
7use PVE::SafeSyslog;
8
9use PMG::Utils;
10use PMG::RuleDB;
11use Digest::SHA;
12
13my $ocache_size = 1023;
14
15sub 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
188sub 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
196sub rules {
197 my ($self) = @_;
198
199 $self->{rules};
200}
201
202sub _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
219sub 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
235sub 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
249sub 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
263sub 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
285sub 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
302sub 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
319sub 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
394sub 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
416sub 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
476sub 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
502sub 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 5271;