]> git.proxmox.com Git - pmg-api.git/blame - PMG/DBTools.pm
upgradedb: call reload_ruledb()
[pmg-api.git] / PMG / DBTools.pm
CommitLineData
a355f100
DM
1package PMG::DBTools;
2
3use strict;
4use warnings;
5
6use POSIX ":sys_wait_h";
7use POSIX ':signal_h';
8use DBI;
8fb6f404 9use Time::Local;
a355f100 10
f3464b71 11use PVE::SafeSyslog;
a355f100
DM
12use PVE::Tools;
13
f3464b71 14use PMG::Utils;
0a580593 15use PMG::RuleDB;
1af3c560 16use PMG::MailQueue;
0a580593 17
987ee804
DM
18our $default_db_name = "Proxmox_ruledb";
19
2e049252
DM
20our $cgreylist_merge_sql =
21 'INSERT INTO CGREYLIST (IPNet,Host,Sender,Receiver,Instance,RCTime,' .
22 'ExTime,Delay,Blocked,Passed,MTime,CID) ' .
23 'VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ' .
24 'ON CONFLICT (IPNet,Sender,Receiver) DO UPDATE SET ' .
0c6e9b56
DM
25 'Host = CASE WHEN CGREYLIST.MTime >= excluded.MTime THEN CGREYLIST.Host ELSE excluded.Host END,' .
26 'CID = GREATEST(CGREYLIST.CID, excluded.CID), RCTime = LEAST(CGREYLIST.RCTime, excluded.RCTime),' .
27 'ExTime = GREATEST(CGREYLIST.ExTime, excluded.ExTime),' .
28 'Delay = GREATEST(CGREYLIST.Delay, excluded.Delay),' .
29 'Blocked = GREATEST(CGREYLIST.Blocked, excluded.Blocked),' .
30 'Passed = GREATEST(CGREYLIST.Passed, excluded.Passed)';
2e049252 31
a355f100
DM
32sub open_ruledb {
33 my ($database, $host, $port) = @_;
34
5475e159 35 $port //= 5432;
a355f100 36
5475e159 37 $database //= $default_db_name;
a355f100
DM
38
39 if ($host) {
40
155b0da9
DM
41 # Note: pmgtunnel uses UDP sockets inside directory '/var/run/pmgtunnel',
42 # and the cluster 'cid' as port number. You can connect to the
43 # socket with: host => /var/run/pmgtunnel, port => $cid
44
a355f100
DM
45 my $dsn = "dbi:Pg:dbname=$database;host=$host;port=$port;";
46
47 my $timeout = 5;
48 # only low level alarm interface works for DBI->connect
49 my $mask = POSIX::SigSet->new(SIGALRM);
50 my $action = POSIX::SigAction->new(sub { die "connect timeout\n" }, $mask);
51 my $oldaction = POSIX::SigAction->new();
52 sigaction(SIGALRM, $action, $oldaction);
53
54 my $rdb;
55
56 eval {
57 alarm($timeout);
5475e159 58 $rdb = DBI->connect($dsn, 'root', undef,
a355f100
DM
59 { PrintError => 0, RaiseError => 1 });
60 alarm(0);
61 };
62 alarm(0);
63 sigaction(SIGALRM, $oldaction); # restore original handler
f1bf78ff 64
a355f100
DM
65 die $@ if $@;
66
67 return $rdb;
68 } else {
155b0da9 69 my $dsn = "DBI:Pg:dbname=$database;host=/var/run/postgresql;port=$port";
a355f100 70
5475e159 71 my $dbh = DBI->connect($dsn, $> == 0 ? 'root' : 'www-data', undef,
a355f100
DM
72 { PrintError => 0, RaiseError => 1 });
73
74 return $dbh;
75 }
76}
77
97cab76a
DM
78sub postgres_admin_cmd {
79 my ($cmd, $options, @params) = @_;
80
81 $cmd = ref($cmd) ? $cmd : [ $cmd ];
82 my $uid = getpwnam('postgres') || die "getpwnam postgres failed\n";
83
84 local $> = $uid;
85 $! && die "setuid postgres ($uid) failed - $!\n";
86
87 PVE::Tools::run_command([@$cmd, '-U', 'postgres', @params], %$options);
88}
89
a355f100
DM
90sub delete_ruledb {
91 my ($dbname) = @_;
92
97cab76a 93 postgres_admin_cmd('dropdb', undef, $dbname);
a355f100
DM
94}
95
f1bf78ff
DM
96sub database_list {
97
98 my $database_list = {};
99
100 my $parser = sub {
101 my $line = shift;
102
103 my ($name, $owner) = map { PVE::Tools::trim($_) } split(/\|/, $line);
104 return if !$name || !$owner;
105
106 $database_list->{$name} = { owner => $owner };
107 };
108
97cab76a 109 postgres_admin_cmd('psql', { outfunc => $parser }, '--list', '--quiet', '--tuples-only');
f1bf78ff
DM
110
111 return $database_list;
112}
113
f1bf78ff
DM
114my $cgreylist_ctablecmd = <<__EOD;
115 CREATE TABLE CGreylist
116 (IPNet VARCHAR(16) NOT NULL,
117 Host INTEGER NOT NULL,
118 Sender VARCHAR(255) NOT NULL,
119 Receiver VARCHAR(255) NOT NULL,
120 Instance VARCHAR(255),
121 RCTime INTEGER NOT NULL,
122 ExTime INTEGER NOT NULL,
123 Delay INTEGER NOT NULL DEFAULT 0,
124 Blocked INTEGER NOT NULL,
125 Passed INTEGER NOT NULL,
126 CID INTEGER NOT NULL,
127 MTime INTEGER NOT NULL,
128 PRIMARY KEY (IPNet, Sender, Receiver));
129
130 CREATE INDEX CGreylist_Instance_Sender_Index ON CGreylist (Instance, Sender);
131
132 CREATE INDEX CGreylist_ExTime_Index ON CGreylist (ExTime);
133
134 CREATE INDEX CGreylist_MTime_Index ON CGreylist (MTime);
135__EOD
136
137my $clusterinfo_ctablecmd = <<__EOD;
138 CREATE TABLE ClusterInfo
139 (CID INTEGER NOT NULL,
140 Name VARCHAR NOT NULL,
141 IValue INTEGER,
142 SValue VARCHAR,
143 PRIMARY KEY (CID, Name))
144__EOD
145
146my $daily_stat_ctablecmd = <<__EOD;
147 CREATE TABLE DailyStat
148 (Time INTEGER NOT NULL UNIQUE,
149 CountIn INTEGER NOT NULL,
150 CountOut INTEGER NOT NULL,
151 BytesIn REAL NOT NULL,
152 BytesOut REAL NOT NULL,
153 VirusIn INTEGER NOT NULL,
154 VirusOut INTEGER NOT NULL,
155 SpamIn INTEGER NOT NULL,
156 SpamOut INTEGER NOT NULL,
157 BouncesIn INTEGER NOT NULL,
158 BouncesOut INTEGER NOT NULL,
159 GreylistCount INTEGER NOT NULL,
160 SPFCount INTEGER NOT NULL,
161 PTimeSum REAL NOT NULL,
162 MTime INTEGER NOT NULL,
163 RBLCount INTEGER DEFAULT 0 NOT NULL,
164 PRIMARY KEY (Time));
165
166 CREATE INDEX DailyStat_MTime_Index ON DailyStat (MTime);
167
168__EOD
169
170my $domain_stat_ctablecmd = <<__EOD;
171 CREATE TABLE DomainStat
172 (Time INTEGER NOT NULL,
173 Domain VARCHAR(255) NOT NULL,
174 CountIn INTEGER NOT NULL,
175 CountOut INTEGER NOT NULL,
176 BytesIn REAL NOT NULL,
177 BytesOut REAL NOT NULL,
178 VirusIn INTEGER NOT NULL,
179 VirusOut INTEGER NOT NULL,
180 SpamIn INTEGER NOT NULL,
181 SpamOut INTEGER NOT NULL,
182 BouncesIn INTEGER NOT NULL,
183 BouncesOut INTEGER NOT NULL,
184 PTimeSum REAL NOT NULL,
185 MTime INTEGER NOT NULL,
186 PRIMARY KEY (Time, Domain));
187
188 CREATE INDEX DomainStat_MTime_Index ON DomainStat (MTime);
189__EOD
190
191my $statinfo_ctablecmd = <<__EOD;
192 CREATE TABLE StatInfo
193 (Name VARCHAR(255) NOT NULL UNIQUE,
194 IValue INTEGER,
195 SValue VARCHAR(255),
196 PRIMARY KEY (Name))
197__EOD
198
199my $virusinfo_stat_ctablecmd = <<__EOD;
200 CREATE TABLE VirusInfo
201 (Time INTEGER NOT NULL,
202 Name VARCHAR NOT NULL,
203 Count INTEGER NOT NULL,
204 MTime INTEGER NOT NULL,
205 PRIMARY KEY (Time, Name));
206
207 CREATE INDEX VirusInfo_MTime_Index ON VirusInfo (MTime);
208
209__EOD
210
211# mail storage stable
212# QTypes
213# V - Virus quarantine
214# S - Spam quarantine
215# D - Delayed Mails - not implemented
216# A - Held for Audit - not implemented
217# Status
218# N - new
219# D - deleted
220
221my $cmailstore_ctablecmd = <<__EOD;
222 CREATE TABLE CMailStore
223 (CID INTEGER DEFAULT 0 NOT NULL,
224 RID INTEGER NOT NULL,
225 ID SERIAL UNIQUE,
226 Time INTEGER NOT NULL,
227 QType "char" NOT NULL,
228 Bytes INTEGER NOT NULL,
229 Spamlevel INTEGER NOT NULL,
230 Info VARCHAR NULL,
231 Sender VARCHAR(255) NOT NULL,
232 Header VARCHAR NOT NULL,
233 File VARCHAR(255) NOT NULL,
234 PRIMARY KEY (CID, RID));
235 CREATE INDEX CMailStore_Time_Index ON CMailStore (Time);
236
237 CREATE TABLE CMSReceivers
238 (CMailStore_CID INTEGER NOT NULL,
239 CMailStore_RID INTEGER NOT NULL,
240 PMail VARCHAR(255) NOT NULL,
241 Receiver VARCHAR(255),
f1bf78ff
DM
242 Status "char" NOT NULL,
243 MTime INTEGER NOT NULL);
244
245 CREATE INDEX CMailStore_ID_Index ON CMSReceivers (CMailStore_CID, CMailStore_RID);
246
247 CREATE INDEX CMSReceivers_MTime_Index ON CMSReceivers (MTime);
248
249__EOD
250
251my $cstatistic_ctablecmd = <<__EOD;
252 CREATE TABLE CStatistic
253 (CID INTEGER DEFAULT 0 NOT NULL,
254 RID INTEGER NOT NULL,
255 ID SERIAL UNIQUE,
256 Time INTEGER NOT NULL,
257 Bytes INTEGER NOT NULL,
258 Direction Boolean NOT NULL,
259 Spamlevel INTEGER NOT NULL,
260 VirusInfo VARCHAR(255) NULL,
261 PTime INTEGER NOT NULL,
262 Sender VARCHAR(255) NOT NULL,
263 PRIMARY KEY (CID, RID));
264
265 CREATE INDEX CStatistic_Time_Index ON CStatistic (Time);
266
267 CREATE TABLE CReceivers
268 (CStatistic_CID INTEGER NOT NULL,
269 CStatistic_RID INTEGER NOT NULL,
270 Receiver VARCHAR(255) NOT NULL,
271 Blocked Boolean NOT NULL);
272
273 CREATE INDEX CStatistic_ID_Index ON CReceivers (CStatistic_CID, CStatistic_RID);
274__EOD
275
276# user preferences (black an whitelists, ...)
277# Name: perference name ('BL' -> blacklist, 'WL' -> whitelist)
278# Data: arbitrary data
279my $userprefs_ctablecmd = <<__EOD;
280 CREATE TABLE UserPrefs
281 (PMail VARCHAR,
282 Name VARCHAR(255),
283 Data VARCHAR,
284 MTime INTEGER NOT NULL,
285 PRIMARY KEY (PMail, Name));
286
287 CREATE INDEX UserPrefs_MTime_Index ON UserPrefs (MTime);
288
289__EOD
758c7b6b 290
0a580593
DM
291sub cond_create_dbtable {
292 my ($dbh, $name, $ctablecmd) = @_;
293
294 eval {
295 $dbh->begin_work;
296
297 my $cmd = "SELECT tablename FROM pg_tables " .
298 "WHERE tablename = lower ('$name')";
299
300 my $sth = $dbh->prepare ($cmd);
758c7b6b 301
0a580593
DM
302 $sth->execute();
303
304 if (!(my $ref = $sth->fetchrow_hashref())) {
305 $dbh->do ($ctablecmd);
306 }
758c7b6b 307
0a580593
DM
308 $sth->finish();
309
310 $dbh->commit;
311 };
312 if (my $err = $@) {
313 $dbh->rollback;
9ef3f143 314 die $err;
0a580593
DM
315 }
316}
f1bf78ff
DM
317
318sub create_ruledb {
319 my ($dbname) = @_;
320
987ee804 321 $dbname = $default_db_name if !$dbname;
f1bf78ff 322
97cab76a
DM
323 my $silent_opts = { outfunc => sub {}, errfunc => sub {} };
324 # make sure we have user 'root'
325 eval { postgres_admin_cmd('createuser', $silent_opts, '-D', 'root'); };
5475e159
DM
326 # also create 'www-data' (and give it read-only access below)
327 eval { postgres_admin_cmd('createuser', $silent_opts, '-I', '-D', 'www-data'); };
97cab76a 328
f1bf78ff
DM
329 # use sql_ascii to avoid any character set conversions, and be compatible with
330 # older postgres versions (update from 8.1 must be possible)
f1bf78ff 331
97cab76a
DM
332 postgres_admin_cmd('createdb', undef, '-E', 'sql_ascii', '-T', 'template0',
333 '--lc-collate=C', '--lc-ctype=C', $dbname);
f1bf78ff
DM
334
335 my $dbh = open_ruledb($dbname);
336
5475e159
DM
337 # make sure 'www-data' can read all tables
338 $dbh->do("ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO \"www-data\"");
339
f1bf78ff
DM
340 $dbh->do (
341<<EOD
342 CREATE TABLE Attribut
343 (Object_ID INTEGER NOT NULL,
344 Name VARCHAR(20) NOT NULL,
345 Value BYTEA NULL,
346 PRIMARY KEY (Object_ID, Name));
347
348 CREATE INDEX Attribut_Object_ID_Index ON Attribut(Object_ID);
349
350 CREATE TABLE Object
351 (ID SERIAL UNIQUE,
352 ObjectType INTEGER NOT NULL,
353 Objectgroup_ID INTEGER NOT NULL,
354 Value BYTEA NULL,
355 PRIMARY KEY (ID));
356
357 CREATE TABLE Objectgroup
358 (ID SERIAL UNIQUE,
359 Name VARCHAR(255) NOT NULL,
360 Info VARCHAR(255) NULL,
361 Class VARCHAR(10) NOT NULL,
362 PRIMARY KEY (ID));
363
364 CREATE TABLE Rule
365 (ID SERIAL UNIQUE,
366 Name VARCHAR(255) NULL,
367 Priority INTEGER NOT NULL,
368 Active INTEGER NOT NULL DEFAULT 0,
369 Direction INTEGER NOT NULL DEFAULT 2,
370 Count INTEGER NOT NULL DEFAULT 0,
371 PRIMARY KEY (ID));
372
373 CREATE TABLE RuleGroup
374 (Objectgroup_ID INTEGER NOT NULL,
375 Rule_ID INTEGER NOT NULL,
376 Grouptype INTEGER NOT NULL,
377 PRIMARY KEY (Objectgroup_ID, Rule_ID, Grouptype));
378
379 $cgreylist_ctablecmd;
380
381 $clusterinfo_ctablecmd;
382
383 $daily_stat_ctablecmd;
384
385 $domain_stat_ctablecmd;
386
387 $statinfo_ctablecmd;
388
389 $cmailstore_ctablecmd;
390
391 $cstatistic_ctablecmd;
392
393 $userprefs_ctablecmd;
394
395 $virusinfo_stat_ctablecmd;
396EOD
397 );
398
399 return $dbh;
400}
401
0a580593
DM
402sub cond_create_action_quarantine {
403 my ($ruledb) = @_;
404
405 my $dbh = $ruledb->{dbh};
406
407 eval {
408 my $sth = $dbh->prepare(
409 "SELECT * FROM Objectgroup, Object " .
410 "WHERE Object.ObjectType = ? AND Objectgroup.Class = ? " .
411 "AND Object.objectgroup_id = Objectgroup.id");
412
413 my $otype = PMG::RuleDB::Quarantine::otype();
414 if ($sth->execute($otype, 'action') <= 0) {
415 my $obj = PMG::RuleDB::Quarantine->new ();
416 my $txt = decode_entities(PMG::RuleDB::Quarantine->otype_text);
417 my $quarantine = $ruledb->create_group_with_obj
9578dcd7 418 ($obj, $txt, 'Move to quarantine.');
0a580593
DM
419 }
420 };
421}
422
423sub cond_create_std_actions {
424 my ($ruledb) = @_;
425
426 cond_create_action_quarantine($ruledb);
427
428 #cond_create_action_report_spam($ruledb);
429}
430
431
432sub upgradedb {
433 my ($ruledb) = @_;
434
435 my $dbh = $ruledb->{dbh};
436
758c7b6b 437 # make sure we do not use slow sequential scans when upgraing
0a580593
DM
438 # database (before analyze can gather statistics)
439 $dbh->do("set enable_seqscan = false");
440
97cab76a
DM
441 my $tables = {
442 'DailyStat'=> $daily_stat_ctablecmd,
443 'DomainStat', $domain_stat_ctablecmd,
444 'StatInfo', $statinfo_ctablecmd,
445 'CMailStore', $cmailstore_ctablecmd,
446 'UserPrefs', $userprefs_ctablecmd,
447 'CGreylist', $cgreylist_ctablecmd,
448 'CStatistic', $cstatistic_ctablecmd,
449 'ClusterInfo', $clusterinfo_ctablecmd,
450 'VirusInfo', $virusinfo_stat_ctablecmd,
451 };
452
453 foreach my $table (keys %$tables) {
454 cond_create_dbtable($dbh, $table, $tables->{$tables});
455 }
0a580593 456
e7c865af 457 cond_create_std_actions($ruledb);
0a580593 458
dc295201 459 # upgrade tables here if necessary
0a580593
DM
460
461 # update obsolete content type names
462 eval {
e7c865af
DM
463 $dbh->do("UPDATE Object " .
464 "SET value = 'content-type:application/java-vm' ".
465 "WHERE objecttype = 3003 " .
466 "AND value = 'content-type:application/x-java-vm';");
0a580593
DM
467 };
468
97cab76a
DM
469 foreach my $table (keys %$tables) {
470 eval { $dbh->do("ANALYZE $table"); };
471 warn $@ if $@;
472 }
79bc9b56
DM
473
474 reload_ruledb();
0a580593
DM
475}
476
477sub init_ruledb {
478 my ($ruledb, $reset, $testmode) = @_;
479
480 my $dbh = $ruledb->{dbh};
481
482 if (!$reset) {
483 # Greylist Objectgroup
484 my $greylistgroup = PMG::RuleDB::Group->new
485 ("GreyExclusion", "-", "greylist");
486 $ruledb->save_group ($greylistgroup);
487
488 } else {
489 # we do not touch greylist objects
490 my $glids = "SELECT object.ID FROM Object, Objectgroup WHERE " .
491 "objectgroup_id = objectgroup.id and class = 'greylist'";
492
493 $dbh->do ("DELETE FROM Rule; " .
494 "DELETE FROM RuleGroup; " .
495 "DELETE FROM Attribut WHERE Object_ID NOT IN ($glids); " .
496 "DELETE FROM Object WHERE ID NOT IN ($glids); " .
497 "DELETE FROM Objectgroup WHERE class != 'greylist';");
498 }
499
500 # WHO Objects
501
502 # Blacklist
503 my $obj = PMG::RuleDB::EMail->new ('nomail@fromthisdomain.com');
504 my $blacklist = $ruledb->create_group_with_obj(
505 $obj, 'Blacklist', 'Global blacklist');
758c7b6b 506
0a580593
DM
507 # Whitelist
508 $obj = PMG::RuleDB::EMail->new('mail@fromthisdomain.com');
509 my $whitelist = $ruledb->create_group_with_obj(
510 $obj, 'Whitelist', 'Global whitelist');
511
512 # WHEN Objects
513
514 # Working hours
a29b9649 515 $obj = PMG::RuleDB::TimeFrame->new(8*60, 16*60);
0a580593
DM
516 my $working_hours =$ruledb->create_group_with_obj($obj, 'Office Hours' ,
517 'Usual office hours');
518
519 # WHAT Objects
520
0a580593 521 # Images
a29b9649
DM
522 $obj = PMG::RuleDB::ContentTypeFilter->new('image/.*');
523 my $img_content = $ruledb->create_group_with_obj(
524 $obj, 'Images', 'All kinds of graphic files');
525
0a580593 526 # Multimedia
a29b9649
DM
527 $obj = PMG::RuleDB::ContentTypeFilter->new('audio/.*');
528 my $mm_content = $ruledb->create_group_with_obj(
529 $obj, 'Multimedia', 'Audio and Video');
0a580593 530
a29b9649
DM
531 $obj = PMG::RuleDB::ContentTypeFilter->new('video/.*');
532 $ruledb->group_add_object($mm_content, $obj);
0a580593
DM
533
534 # Office Files
a29b9649
DM
535 $obj = PMG::RuleDB::ContentTypeFilter->new('application/vnd\.ms-excel');
536 my $office_content = $ruledb->create_group_with_obj(
537 $obj, 'Office Files', 'Common Office Files');
758c7b6b 538
a29b9649
DM
539 $obj = PMG::RuleDB::ContentTypeFilter->new(
540 'application/vnd\.ms-powerpoint');
758c7b6b 541
a29b9649 542 $ruledb->group_add_object($office_content, $obj);
758c7b6b 543
a29b9649 544 $obj = PMG::RuleDB::ContentTypeFilter->new('application/msword');
0a580593 545 $ruledb->group_add_object ($office_content, $obj);
758c7b6b 546
a29b9649
DM
547 $obj = PMG::RuleDB::ContentTypeFilter->new(
548 'application/vnd\.openxmlformats-officedocument\..*');
549 $ruledb->group_add_object($office_content, $obj);
758c7b6b 550
a29b9649
DM
551 $obj = PMG::RuleDB::ContentTypeFilter->new(
552 'application/vnd\.oasis\.opendocument\..*');
553 $ruledb->group_add_object($office_content, $obj);
0a580593 554
a29b9649
DM
555 $obj = PMG::RuleDB::ContentTypeFilter->new(
556 'application/vnd\.stardivision\..*');
557 $ruledb->group_add_object($office_content, $obj);
758c7b6b 558
a29b9649
DM
559 $obj = PMG::RuleDB::ContentTypeFilter->new(
560 'application/vnd\.sun\.xml\..*');
561 $ruledb->group_add_object($office_content, $obj);
758c7b6b 562
0a580593 563 # Dangerous Content
a29b9649
DM
564 $obj = PMG::RuleDB::ContentTypeFilter->new(
565 'application/x-ms-dos-executable');
566 my $exe_content = $ruledb->create_group_with_obj(
567 $obj, 'Dangerous Content', 'executable files and partial messages');
758c7b6b 568
a29b9649
DM
569 $obj = PMG::RuleDB::ContentTypeFilter->new('application/x-java');
570 $ruledb->group_add_object($exe_content, $obj);
571 $obj = PMG::RuleDB::ContentTypeFilter->new('application/javascript');
572 $ruledb->group_add_object($exe_content, $obj);
573 $obj = PMG::RuleDB::ContentTypeFilter->new('application/x-executable');
574 $ruledb->group_add_object($exe_content, $obj);
79ef2452
DM
575 $obj = PMG::RuleDB::ContentTypeFilter->new('application/x-ms-dos-executable');
576 $ruledb->group_add_object($exe_content, $obj);
a29b9649
DM
577 $obj = PMG::RuleDB::ContentTypeFilter->new('message/partial');
578 $ruledb->group_add_object($exe_content, $obj);
579 $obj = PMG::RuleDB::MatchFilename->new('.*\.(vbs|pif|lnk|shs|shb)');
580 $ruledb->group_add_object($exe_content, $obj);
be6c5e4a 581 $obj = PMG::RuleDB::MatchFilename->new('.*\.\{.+\}');
a29b9649 582 $ruledb->group_add_object($exe_content, $obj);
0a580593
DM
583
584 # Virus
758c7b6b
DM
585 $obj = PMG::RuleDB::Virus->new();
586 my $virus = $ruledb->create_group_with_obj(
587 $obj, 'Virus', 'Matches virus infected mail');
588
0a580593
DM
589 # WHAT Objects
590
591 # Spam
758c7b6b
DM
592 $obj = PMG::RuleDB::Spam->new(3);
593 my $spam3 = $ruledb->create_group_with_obj(
594 $obj, 'Spam (Level 3)', 'Matches possible spam mail');
e7c865af 595
758c7b6b
DM
596 $obj = PMG::RuleDB::Spam->new(5);
597 my $spam5 = $ruledb->create_group_with_obj(
598 $obj, 'Spam (Level 5)', 'Matches possible spam mail');
e7c865af 599
758c7b6b
DM
600 $obj = PMG::RuleDB::Spam->new(10);
601 my $spam10 = $ruledb->create_group_with_obj(
602 $obj, 'Spam (Level 10)', 'Matches possible spam mail');
0a580593
DM
603
604 # ACTIONS
605
758c7b6b
DM
606 # Mark Spam
607 $obj = PMG::RuleDB::ModField->new('X-SPAM-LEVEL', '__SPAM_INFO__');
608 my $mod_spam_level = $ruledb->create_group_with_obj(
e7c865af 609 $obj, 'Modify Spam Level',
758c7b6b 610 'Mark mail as spam by adding a header tag.');
0a580593
DM
611
612 # Mark Spam
758c7b6b
DM
613 $obj = PMG::RuleDB::ModField->new('subject', 'SPAM: __SUBJECT__');
614 my $mod_spam_subject = $ruledb->create_group_with_obj(
e7c865af 615 $obj, 'Modify Spam Subject',
758c7b6b 616 'Mark mail as spam by modifying the subject.');
e7c865af 617
0a580593 618 # Remove matching attachments
758c7b6b
DM
619 $obj = PMG::RuleDB::Remove->new(0);
620 my $remove = $ruledb->create_group_with_obj(
621 $obj, 'Remove attachments', 'Remove matching attachments');
e7c865af 622
0a580593 623 # Remove all attachments
758c7b6b
DM
624 $obj = PMG::RuleDB::Remove->new(1);
625 my $remove_all = $ruledb->create_group_with_obj(
626 $obj, 'Remove all attachments', 'Remove all attachments');
0a580593
DM
627
628 # Accept
758c7b6b
DM
629 $obj = PMG::RuleDB::Accept->new();
630 my $accept = $ruledb->create_group_with_obj(
631 $obj, 'Accept', 'Accept mail for Delivery');
0a580593
DM
632
633 # Block
758c7b6b
DM
634 $obj = PMG::RuleDB::Block->new ();
635 my $block = $ruledb->create_group_with_obj($obj, 'Block', 'Block mail');
0a580593
DM
636
637 # Quarantine
758c7b6b
DM
638 $obj = PMG::RuleDB::Quarantine->new();
639 my $quarantine = $ruledb->create_group_with_obj(
640 $obj, 'Quarantine', 'Move mail to quarantine');
0a580593
DM
641
642 # Notify Admin
758c7b6b
DM
643 $obj = PMG::RuleDB::Notify->new('__ADMIN__');
644 my $notify_admin = $ruledb->create_group_with_obj(
645 $obj, 'Notify Admin', 'Send notification');
0a580593
DM
646
647 # Notify Sender
758c7b6b
DM
648 $obj = PMG::RuleDB::Notify->new('__SENDER__');
649 my $notify_sender = $ruledb->create_group_with_obj(
650 $obj, 'Notify Sender', 'Send notification');
0a580593
DM
651
652 # Add Disclaimer
758c7b6b
DM
653 $obj = PMG::RuleDB::Disclaimer->new ();
654 my $add_discl = $ruledb->create_group_with_obj(
655 $obj, 'Disclaimer', 'Add Disclaimer');
0a580593
DM
656
657 # Attach original mail
658 #$obj = Proxmox::RuleDB::Attach->new ();
758c7b6b 659 #my $attach_orig = $ruledb->create_group_with_obj ($obj, 'Attach Original Mail',
0a580593
DM
660 # 'Attach Original Mail');
661
662 ####################### RULES ##################################
663
664 ## Block Dangerous Files
a29b9649 665 my $rule = PMG::RuleDB::Rule->new ('Block Dangerous Files', 93, 1, 0);
0a580593
DM
666 $ruledb->save_rule ($rule);
667
668 $ruledb->rule_add_what_group ($rule, $exe_content);
669 $ruledb->rule_add_action ($rule, $remove);
670
671 ## Block Viruses
a29b9649 672 $rule = PMG::RuleDB::Rule->new ('Block Viruses', 96, 1, 0);
0a580593
DM
673 $ruledb->save_rule ($rule);
674
675 $ruledb->rule_add_what_group ($rule, $virus);
676 $ruledb->rule_add_action ($rule, $notify_admin);
758c7b6b 677
0a580593
DM
678 if ($testmode) {
679 $ruledb->rule_add_action ($rule, $block);
680 } else {
681 $ruledb->rule_add_action ($rule, $quarantine);
682 }
683
684 ## Virus Alert
a29b9649 685 $rule = PMG::RuleDB::Rule->new ('Virus Alert', 96, 1, 1);
0a580593
DM
686 $ruledb->save_rule ($rule);
687
688 $ruledb->rule_add_what_group ($rule, $virus);
689 $ruledb->rule_add_action ($rule, $notify_sender);
690 $ruledb->rule_add_action ($rule, $notify_admin);
691 $ruledb->rule_add_action ($rule, $block);
758c7b6b 692
0a580593 693 ## Blacklist
a29b9649 694 $rule = PMG::RuleDB::Rule->new ('Blacklist', 98, 1, 0);
0a580593
DM
695 $ruledb->save_rule ($rule);
696
697 $ruledb->rule_add_from_group ($rule, $blacklist);
698 $ruledb->rule_add_action ($rule, $block);
699
700 ## Modify header
701 if (!$testmode) {
a29b9649 702 $rule = PMG::RuleDB::Rule->new ('Modify Header', 90, 1, 0);
0a580593
DM
703 $ruledb->save_rule ($rule);
704 $ruledb->rule_add_action ($rule, $mod_spam_level);
705 }
706
707 ## Whitelist
a29b9649 708 $rule = PMG::RuleDB::Rule->new ('Whitelist', 85, 1, 0);
0a580593
DM
709 $ruledb->save_rule ($rule);
710
711 $ruledb->rule_add_from_group ($rule, $whitelist);
712 $ruledb->rule_add_action ($rule, $accept);
713
714 if ($testmode) {
a29b9649 715 $rule = PMG::RuleDB::Rule->new ('Mark Spam', 80, 1, 0);
0a580593
DM
716 $ruledb->save_rule ($rule);
717
718 $ruledb->rule_add_what_group ($rule, $spam10);
719 $ruledb->rule_add_action ($rule, $mod_spam_level);
720 $ruledb->rule_add_action ($rule, $mod_spam_subject);
721 } else {
722 # Quarantine/Mark Spam (Level 3)
a29b9649 723 $rule = PMG::RuleDB::Rule->new ('Quarantine/Mark Spam (Level 3)', 80, 1, 0);
0a580593
DM
724 $ruledb->save_rule ($rule);
725
726 $ruledb->rule_add_what_group ($rule, $spam3);
727 $ruledb->rule_add_action ($rule, $mod_spam_subject);
728 $ruledb->rule_add_action ($rule, $quarantine);
729 #$ruledb->rule_add_action ($rule, $count_spam);
730 }
731
732 # Quarantine/Mark Spam (Level 5)
a29b9649 733 $rule = PMG::RuleDB::Rule->new ('Quarantine/Mark Spam (Level 5)', 79, 0, 0);
0a580593
DM
734 $ruledb->save_rule ($rule);
735
736 $ruledb->rule_add_what_group ($rule, $spam5);
737 $ruledb->rule_add_action ($rule, $mod_spam_subject);
738 $ruledb->rule_add_action ($rule, $quarantine);
739
740 ## Block Spam Level 10
a29b9649 741 $rule = PMG::RuleDB::Rule->new ('Block Spam (Level 10)', 78, 0, 0);
0a580593
DM
742 $ruledb->save_rule ($rule);
743
744 $ruledb->rule_add_what_group ($rule, $spam10);
745 $ruledb->rule_add_action ($rule, $block);
746
747 ## Block Outgoing Spam
a29b9649 748 $rule = PMG::RuleDB::Rule->new ('Block outgoing Spam', 70, 0, 1);
0a580593
DM
749 $ruledb->save_rule ($rule);
750
751 $ruledb->rule_add_what_group ($rule, $spam3);
752 $ruledb->rule_add_action ($rule, $notify_admin);
753 $ruledb->rule_add_action ($rule, $notify_sender);
754 $ruledb->rule_add_action ($rule, $block);
755
756 ## Add disclaimer
a29b9649 757 $rule = PMG::RuleDB::Rule->new ('Add Disclaimer', 60, 0, 1);
0a580593
DM
758 $ruledb->save_rule ($rule);
759 $ruledb->rule_add_action ($rule, $add_discl);
760
761 # Block Multimedia Files
a29b9649 762 $rule = PMG::RuleDB::Rule->new ('Block Multimedia Files', 87, 0, 2);
0a580593
DM
763 $ruledb->save_rule ($rule);
764
765 $ruledb->rule_add_what_group ($rule, $mm_content);
766 $ruledb->rule_add_action ($rule, $remove);
767
768 #$ruledb->rule_add_from_group ($rule, $anybody);
769 #$ruledb->rule_add_from_group ($rule, $trusted);
770 #$ruledb->rule_add_to_group ($rule, $anybody);
771 #$ruledb->rule_add_what_group ($rule, $ct_filter);
772 #$ruledb->rule_add_action ($rule, $add_discl);
773 #$ruledb->rule_add_action ($rule, $remove);
774 #$ruledb->rule_add_action ($rule, $bcc);
775 #$ruledb->rule_add_action ($rule, $storeq);
776 #$ruledb->rule_add_action ($rule, $accept);
777
778 cond_create_std_actions ($ruledb);
8c002247
DM
779
780 reload_ruledb();
0a580593
DM
781}
782
8f4e102b
DM
783sub get_remote_time {
784 my ($rdb) = @_;
785
786 my $sth = $rdb->prepare("SELECT EXTRACT (EPOCH FROM TIMESTAMP (0) WITH TIME ZONE 'now') as ctime;");
787 $sth->execute();
788 my $ctinfo = $sth->fetchrow_hashref();
789 $sth->finish ();
790
791 return $ctinfo ? $ctinfo->{ctime} : 0;
792}
793
cfdf6608
DM
794sub init_masterdb {
795 my ($lcid, $database) = @_;
796
797 die "got unexpected cid for new master" if !$lcid;
798
799 my $dbh;
800
801 eval {
802 $dbh = open_ruledb($database);
803
804 $dbh->begin_work;
805
806 print STDERR "update quarantine database\n";
807 $dbh->do ("UPDATE CMailStore SET CID = $lcid WHERE CID = 0;" .
808 "UPDATE CMSReceivers SET CMailStore_CID = $lcid WHERE CMailStore_CID = 0;");
809
810 print STDERR "update statistic database\n";
811 $dbh->do ("UPDATE CStatistic SET CID = $lcid WHERE CID = 0;" .
812 "UPDATE CReceivers SET CStatistic_CID = $lcid WHERE CStatistic_CID = 0;");
813
814 print STDERR "update greylist database\n";
815 $dbh->do ("UPDATE CGreylist SET CID = $lcid WHERE CID = 0;");
816
817 $dbh->commit;
818 };
819 my $err = $@;
820
821 if ($dbh) {
822 $dbh->rollback if $err;
823 $dbh->disconnect();
824 }
825
826 die $err if $err;
827}
828
8fb6f404
DM
829sub purge_statistic_database {
830 my ($dbh, $statlifetime) = @_;
831
832 return if $statlifetime <= 0;
833
834 my (undef, undef, undef, $mday, $mon, $year) = localtime(time());
835 my $end = timelocal(0, 0, 0, $mday, $mon, $year);
836 my $start = $end - $statlifetime*86400;
837
838 # delete statistics older than $start
839
840 my $rows = 0;
841
842 eval {
843 $dbh->begin_work;
844
845 my $sth = $dbh->prepare("DELETE FROM CStatistic WHERE time < $start");
846 $sth->execute;
847 $rows = $sth->rows;
848 $sth->finish;
849
850 if ($rows > 0) {
851 $sth = $dbh->prepare(
852 "DELETE FROM CReceivers WHERE NOT EXISTS " .
853 "(SELECT * FROM CStatistic WHERE CID = CStatistic_CID AND RID = CStatistic_RID)");
854
855 $sth->execute;
856 }
857 $dbh->commit;
858 };
859 if (my $err = $@) {
860 $dbh->rollback;
861 die $err;
862 }
863
864 return $rows;
865}
866
1af3c560
DM
867sub purge_quarantine_database {
868 my ($dbh, $qtype, $lifetime) = @_;
869
870 my $spooldir = $PMG::MailQueue::spooldir;
871
872 my (undef, undef, undef, $mday, $mon, $year) = localtime(time());
873 my $end = timelocal(0, 0, 0, $mday, $mon, $year);
874 my $start = $end - $lifetime*86400;
875
876 my $sth = $dbh->prepare(
877 "SELECT file FROM CMailStore WHERE time < $start AND QType = '$qtype'");
878
879 $sth->execute();
880
b8b8c31b
DM
881 my $count = 0;
882
1af3c560
DM
883 while (my $ref = $sth->fetchrow_hashref()) {
884 my $filename = "$spooldir/$ref->{file}";
b8b8c31b 885 $count++ if unlink($filename);
1af3c560
DM
886 }
887
888 $sth->finish();
889
890 $dbh->do(
891 "DELETE FROM CMailStore WHERE time < $start AND QType = '$qtype';" .
892 "DELETE FROM CMSReceivers WHERE NOT EXISTS " .
893 "(SELECT * FROM CMailStore WHERE CID = CMailStore_CID AND RID = CMailStore_RID)");
b8b8c31b
DM
894
895 return $count;
1af3c560
DM
896}
897
da686811
DM
898sub copy_table {
899 my ($ldb, $rdb, $table) = @_;
900
901 $table = lc($table);
902
903 my $sth = $ldb->column_info(undef, undef, $table, undef);
904 my $attrs = $sth->fetchall_arrayref({});
905
906 my @col_arr;
907 foreach my $ref (@$attrs) {
908 push @col_arr, $ref->{COLUMN_NAME};
909 }
910
911 $sth->finish();
912
913 my $cols = join(', ', @col_arr);
914 $cols || die "unable to fetch column definitions of table '$table' : ERROR";
915
916 $rdb->do("COPY $table ($cols) TO STDOUT");
917
918 my $data = '';
919
920 eval {
921 $ldb->do("COPY $table ($cols) FROM stdin");
922
923 while ($rdb->pg_getcopydata($data) >= 0) {
924 $ldb->pg_putcopydata($data);
925 }
926
927 $ldb->pg_putcopyend();
928 };
929 if (my $err = $@) {
930 $ldb->pg_putcopyend();
931 die $err;
932 }
933}
934
6cf5b4aa
DM
935sub copy_selected_data {
936 my ($dbh, $select_sth, $table, $attrs, $callback) = @_;
937
938 my $count = 0;
939
940 my $insert_sth = $dbh->prepare(
c2e5905e 941 "INSERT INTO ${table}(" . join(',', @$attrs) . ') ' .
6cf5b4aa
DM
942 'VALUES (' . join(',', ('?') x scalar(@$attrs)) . ')');
943
944 while (my $ref = $select_sth->fetchrow_hashref()) {
945 $callback->($ref) if $callback;
946 $count++;
947 $insert_sth->execute(map { $ref->{$_} } @$attrs);
948 }
949
950 return $count;
951}
952
987ee804
DM
953sub update_master_clusterinfo {
954 my ($clientcid) = @_;
955
956 my $dbh = open_ruledb();
957
958 $dbh->do("DELETE FROM ClusterInfo WHERE CID = $clientcid");
959
960 my @mt = ('CMSReceivers', 'CGreylist', 'UserPrefs', 'DomainStat', 'DailyStat', 'VirusInfo');
961
962 foreach my $table (@mt) {
963 $dbh->do ("INSERT INTO ClusterInfo (cid, name, ivalue) select $clientcid, 'lastmt_$table', " .
964 "EXTRACT(EPOCH FROM now())");
965 }
966}
967
968sub update_client_clusterinfo {
969 my ($mastercid) = @_;
970
971 my $dbh = open_ruledb();
972
973 $dbh->do ("DELETE FROM StatInfo"); # not needed at node
974
975 $dbh->do ("DELETE FROM ClusterInfo WHERE CID = $mastercid");
976
977 $dbh->do ("INSERT INTO ClusterInfo (cid, name, ivalue) select $mastercid, 'lastid_CMailStore', " .
978 "COALESCE (max (rid), -1) FROM CMailStore WHERE cid = $mastercid");
979
980 $dbh->do ("INSERT INTO ClusterInfo (cid, name, ivalue) select $mastercid, 'lastid_CStatistic', " .
981 "COALESCE (max (rid), -1) FROM CStatistic WHERE cid = $mastercid");
982
983 my @mt = ('CMSReceivers', 'CGreylist', 'UserPrefs', 'DomainStat', 'DailyStat', 'VirusInfo');
984
985 foreach my $table (@mt) {
986 $dbh->do ("INSERT INTO ClusterInfo (cid, name, ivalue) select $mastercid, 'lastmt_$table', " .
987 "COALESCE (max (mtime), 0) FROM $table");
988 }
989}
990
7b313034
DM
991sub create_clusterinfo_default {
992 my ($dbh, $rcid, $name, $ivalue, $svalue) = @_;
993
994 my $sth = $dbh->prepare("SELECT * FROM ClusterInfo WHERE CID = ? AND Name = ?");
995 $sth->execute($rcid, $name);
996 if (!$sth->fetchrow_hashref()) {
997 $dbh->do("INSERT INTO ClusterInfo (CID, Name, IValue, SValue) " .
998 "VALUES (?, ?, ?, ?)", undef,
999 $rcid, $name, $ivalue, $svalue);
1000 }
1001 $sth->finish();
1002}
1003
1004sub read_int_clusterinfo {
1005 my ($dbh, $rcid, $name) = @_;
1006
1007 my $sth = $dbh->prepare(
1008 "SELECT ivalue as value FROM ClusterInfo " .
1009 "WHERE cid = ? AND NAME = ?");
1010 $sth->execute($rcid, $name);
1011 my $cinfo = $sth->fetchrow_hashref();
1012 $sth->finish();
1013
1014 return $cinfo->{value};
1015}
1016
1017sub write_maxint_clusterinfo {
1018 my ($dbh, $rcid, $name, $value) = @_;
1019
2e049252 1020 $dbh->do("UPDATE ClusterInfo SET ivalue = GREATEST(ivalue, ?) " .
7b313034
DM
1021 "WHERE cid = ? AND name = ?", undef,
1022 $value, $rcid, $name);
1023}
1024
987ee804
DM
1025sub init_nodedb {
1026 my ($cinfo) = @_;
1027
1028 my $ni = $cinfo->{master};
1029
1030 die "no master defined - unable to sync data from master\n" if !$ni;
1031
1032 my $master_ip = $ni->{ip};
1033 my $master_cid = $ni->{cid};
22c16ff6 1034 my $master_name = $ni->{name};
987ee804
DM
1035
1036 my $fn = "/tmp/masterdb$$.tar";
1037 unlink $fn;
1038
1039 my $dbname = $default_db_name;
1040
1041 eval {
1042 print STDERR "copying master database from '${master_ip}'\n";
1043
97cab76a
DM
1044 open (my $fh, ">", $fn) || die "open '$fn' failed - $!\n";
1045
1046 postgres_admin_cmd(
1047 ['/usr/bin/ssh', '-o', 'BatchMode=yes',
1048 '-o', "HostKeyAlias=${master_name}",
1049 $master_ip, 'pg_dump'],
1050 { output => '>&' . fileno($fh) },
1051 $dbname, '-F', 'c');
22c16ff6 1052
97cab76a 1053 close($fh);
987ee804
DM
1054
1055 my $size = -s $fn;
1056
1057 print STDERR "copying master database finished (got $size bytes)\n";
1058
1059 print STDERR "delete local database\n";
1060
97cab76a 1061 postgres_admin_cmd('dropdb', undef, $dbname , '--if-exists');
987ee804
DM
1062
1063 print STDERR "create new local database\n";
1064
97cab76a 1065 postgres_admin_cmd('createdb', undef, $dbname);
987ee804
DM
1066
1067 print STDERR "insert received data into local database\n";
1068
1069 my $mess;
1070 my $parser = sub {
1071 my $line = shift;
1072
1073 if ($line =~ m/restoring data for table \"(.+)\"/) {
1074 print STDERR "restoring table $1\n";
1075 } elsif (!$mess && ($line =~ m/creating (INDEX|CONSTRAINT)/)) {
1076 $mess = "creating indexes";
1077 print STDERR "$mess\n";
1078 }
1079 };
1080
97cab76a
DM
1081 my $opts = {
1082 outfunc => $parser,
1083 errfunc => $parser,
1084 errmsg => "pg_restore failed"
1085 };
1086
1087 postgres_admin_cmd('pg_restore', $opts, '-d', $dbname, '-v', $fn);
987ee804
DM
1088
1089 print STDERR "run analyze to speed up database queries\n";
1090
97cab76a 1091 postgres_admin_cmd('psql', { input => 'analyze;' }, $dbname);
987ee804
DM
1092
1093 update_client_clusterinfo($master_cid);
1094 };
1095
1096 my $err = $@;
1097
1098 unlink $fn;
1099
1100 die $err if $err;
1101}
1102
f3464b71
DM
1103sub cluster_sync_status {
1104 my ($cinfo) = @_;
1105
1106 my $dbh;
1107
1108 my $minmtime;
1109
1110 foreach my $ni (values %{$cinfo->{ids}}) {
1111 next if $cinfo->{local}->{cid} == $ni->{cid}; # skip local CID
1112 $minmtime->{$ni->{cid}} = 0;
1113 }
1114
1115 eval {
1116 $dbh = open_ruledb();
1117
1118 my $sth = $dbh->prepare(
1119 "SELECT cid, MIN (ivalue) as minmtime FROM ClusterInfo " .
1120 "WHERE name = 'lastsync' AND ivalue > 0 " .
1121 "GROUP BY cid");
1122
1123 $sth->execute();
1124
1125 while (my $info = $sth->fetchrow_hashref()) {
1126 foreach my $ni (values %{$cinfo->{ids}}) {
1127 next if $cinfo->{local}->{cid} == $ni->{cid}; # skip local CID
1128 if ($ni->{cid} == $info->{cid}) { # node exists
1129 $minmtime->{$ni->{cid}} = $info->{minmtime};
1130 }
1131 }
1132 }
1133
2ab22e49 1134 $sth->finish();
f3464b71
DM
1135 };
1136 my $err = $@;
1137
1138 $dbh->disconnect() if $dbh;
1139
b902c0b8 1140 syslog('err', $err) if $err;
f3464b71
DM
1141
1142 return $minmtime;
1143}
1144
2ab22e49
DM
1145sub load_mail_data {
1146 my ($dbh, $cid, $rid) = @_;
1147
1148 my $sth = $dbh->prepare(
1149 "SELECT * FROM CMailStore, CMSReceivers WHERE " .
1150 "CID = $cid and RID = $rid AND " .
1151 "CID = CMailStore_CID AND RID = CMailStore_RID");
1152 $sth->execute ();
1153
1154 my $res = $sth->fetchrow_hashref();
1155
1156 $sth->finish();
1157
1158 die "no such mail (C${cid}R${rid})\n" if !defined($res);
1159
1160 return $res;
1161}
1162
cbef3ff8
DM
1163sub reload_ruledb {
1164 my $pid_file = '/var/run/pmg-smtp-filter.pid';
1165 my $pid = PVE::Tools::file_read_firstline($pid_file);
1166
1167 return 0 if !$pid;
1168
1169 return kill (10, $pid); # send SIGUSR1
1170}
1171
a355f100 11721;