6 use POSIX
":sys_wait_h";
7 use POSIX
qw(:signal_h getuid);
17 use PMG
::Utils
qw(postgres_admin_cmd);
19 our $default_db_name = "Proxmox_ruledb";
21 # FIXME: drop Host column with PMG 7.0
22 sub cgreylist_merge_sql
{
25 my $network = $with_mask ?
'network(set_masklen(?, ?))' : '?';
28 'INSERT INTO CGREYLIST (IPNet,Host,Sender,Receiver,Instance,RCTime,' .
29 'ExTime,Delay,Blocked,Passed,MTime,CID) ' .
30 "VALUES ($network, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) " .
31 'ON CONFLICT (IPNet,Sender,Receiver) DO UPDATE SET ' .
32 'Host = CASE WHEN CGREYLIST.MTime >= excluded.MTime THEN CGREYLIST.Host ELSE excluded.Host END,' .
33 'CID = GREATEST(CGREYLIST.CID, excluded.CID), RCTime = LEAST(CGREYLIST.RCTime, excluded.RCTime),' .
34 'ExTime = GREATEST(CGREYLIST.ExTime, excluded.ExTime),' .
35 'Delay = GREATEST(CGREYLIST.Delay, excluded.Delay),' .
36 'Blocked = GREATEST(CGREYLIST.Blocked, excluded.Blocked),' .
37 'Passed = GREATEST(CGREYLIST.Passed, excluded.Passed)';
43 my ($database, $host, $port) = @_;
47 $database //= $default_db_name;
51 # Note: pmgtunnel uses UDP sockets inside directory '/run/pmgtunnel',
52 # and the cluster 'cid' as port number. You can connect to the
53 # socket with: host => /run/pmgtunnel, port => $cid
55 my $dsn = "dbi:Pg:dbname=$database;host=$host;port=$port;";
58 # only low level alarm interface works for DBI->connect
59 my $mask = POSIX
::SigSet-
>new(SIGALRM
);
60 my $action = POSIX
::SigAction-
>new(sub { die "connect timeout\n" }, $mask);
61 my $oldaction = POSIX
::SigAction-
>new();
62 sigaction
(SIGALRM
, $action, $oldaction);
68 $rdb = DBI-
>connect($dsn, 'root', undef,
69 { PrintError
=> 0, RaiseError
=> 1 });
73 sigaction
(SIGALRM
, $oldaction); # restore original handler
79 my $dsn = "DBI:Pg:dbname=$database;host=/var/run/postgresql;port=$port";
81 my $dbh = DBI-
>connect($dsn, $> == 0 ?
'root' : 'www-data', undef,
82 { PrintError
=> 0, RaiseError
=> 1 });
91 postgres_admin_cmd
('dropdb', undef, $dbname);
96 my $database_list = {};
101 my ($name, $owner) = map { PVE
::Tools
::trim
($_) } split(/\|/, $line);
102 return if !$name || !$owner;
104 $database_list->{$name} = { owner
=> $owner };
107 postgres_admin_cmd
('psql', { outfunc
=> $parser }, '--list', '--quiet', '--tuples-only');
109 return $database_list;
112 my $cgreylist_ctablecmd = <<__EOD;
113 CREATE TABLE CGreylist
114 (IPNet VARCHAR(49) NOT NULL,
115 Host INTEGER NOT NULL,
116 Sender VARCHAR(255) NOT NULL,
117 Receiver VARCHAR(255) NOT NULL,
118 Instance VARCHAR(255),
119 RCTime INTEGER NOT NULL,
120 ExTime INTEGER NOT NULL,
121 Delay INTEGER NOT NULL DEFAULT 0,
122 Blocked INTEGER NOT NULL,
123 Passed INTEGER NOT NULL,
124 CID INTEGER NOT NULL,
125 MTime INTEGER NOT NULL,
126 PRIMARY KEY (IPNet, Sender, Receiver));
128 CREATE INDEX CGreylist_Instance_Sender_Index ON CGreylist (Instance, Sender);
130 CREATE INDEX CGreylist_ExTime_Index ON CGreylist (ExTime);
132 CREATE INDEX CGreylist_MTime_Index ON CGreylist (MTime);
135 my $clusterinfo_ctablecmd = <<__EOD;
136 CREATE TABLE ClusterInfo
137 (CID INTEGER NOT NULL,
138 Name VARCHAR NOT NULL,
141 PRIMARY KEY (CID, Name))
144 my $local_stat_ctablecmd = <<__EOD;
145 CREATE TABLE LocalStat
146 (Time INTEGER NOT NULL,
147 RBLCount INTEGER DEFAULT 0 NOT NULL,
148 PregreetCount INTEGER DEFAULT 0 NOT NULL,
149 CID INTEGER NOT NULL,
150 MTime INTEGER NOT NULL,
151 PRIMARY KEY (Time, CID));
153 CREATE INDEX LocalStat_MTime_Index ON LocalStat (MTime);
157 my $daily_stat_ctablecmd = <<__EOD;
158 CREATE TABLE DailyStat
159 (Time INTEGER NOT NULL UNIQUE,
160 CountIn INTEGER NOT NULL,
161 CountOut INTEGER NOT NULL,
162 BytesIn REAL NOT NULL,
163 BytesOut REAL NOT NULL,
164 VirusIn INTEGER NOT NULL,
165 VirusOut INTEGER NOT NULL,
166 SpamIn INTEGER NOT NULL,
167 SpamOut INTEGER NOT NULL,
168 BouncesIn INTEGER NOT NULL,
169 BouncesOut INTEGER NOT NULL,
170 GreylistCount INTEGER NOT NULL,
171 SPFCount INTEGER NOT NULL,
172 PTimeSum REAL NOT NULL,
173 MTime INTEGER NOT NULL,
174 RBLCount INTEGER DEFAULT 0 NOT NULL,
177 CREATE INDEX DailyStat_MTime_Index ON DailyStat (MTime);
181 my $domain_stat_ctablecmd = <<__EOD;
182 CREATE TABLE DomainStat
183 (Time INTEGER NOT NULL,
184 Domain VARCHAR(255) NOT NULL,
185 CountIn INTEGER NOT NULL,
186 CountOut INTEGER NOT NULL,
187 BytesIn REAL NOT NULL,
188 BytesOut REAL NOT NULL,
189 VirusIn INTEGER NOT NULL,
190 VirusOut INTEGER NOT NULL,
191 SpamIn INTEGER NOT NULL,
192 SpamOut INTEGER NOT NULL,
193 BouncesIn INTEGER NOT NULL,
194 BouncesOut INTEGER NOT NULL,
195 PTimeSum REAL NOT NULL,
196 MTime INTEGER NOT NULL,
197 PRIMARY KEY (Time, Domain));
199 CREATE INDEX DomainStat_MTime_Index ON DomainStat (MTime);
202 my $statinfo_ctablecmd = <<__EOD;
203 CREATE TABLE StatInfo
204 (Name VARCHAR(255) NOT NULL UNIQUE,
210 my $virusinfo_stat_ctablecmd = <<__EOD;
211 CREATE TABLE VirusInfo
212 (Time INTEGER NOT NULL,
213 Name VARCHAR NOT NULL,
214 Count INTEGER NOT NULL,
215 MTime INTEGER NOT NULL,
216 PRIMARY KEY (Time, Name));
218 CREATE INDEX VirusInfo_MTime_Index ON VirusInfo (MTime);
224 # V - Virus quarantine
225 # S - Spam quarantine
226 # D - Delayed Mails - not implemented
227 # A - Held for Audit - not implemented
232 my $cmailstore_ctablecmd = <<__EOD;
233 CREATE TABLE CMailStore
234 (CID INTEGER DEFAULT 0 NOT NULL,
235 RID INTEGER NOT NULL,
237 Time INTEGER NOT NULL,
238 QType "char" NOT NULL,
239 Bytes INTEGER NOT NULL,
240 Spamlevel INTEGER NOT NULL,
242 Sender VARCHAR(255) NOT NULL,
243 Header VARCHAR NOT NULL,
244 File VARCHAR(255) NOT NULL,
245 PRIMARY KEY (CID, RID));
246 CREATE INDEX CMailStore_Time_Index ON CMailStore (Time);
248 CREATE TABLE CMSReceivers
249 (CMailStore_CID INTEGER NOT NULL,
250 CMailStore_RID INTEGER NOT NULL,
251 PMail VARCHAR(255) NOT NULL,
252 Receiver VARCHAR(255),
253 TicketID INTEGER NOT NULL,
254 Status "char" NOT NULL,
255 MTime INTEGER NOT NULL);
257 CREATE INDEX CMailStore_ID_Index ON CMSReceivers (CMailStore_CID, CMailStore_RID);
259 CREATE INDEX CMSReceivers_MTime_Index ON CMSReceivers (MTime);
263 my $cstatistic_ctablecmd = <<__EOD;
264 CREATE TABLE CStatistic
265 (CID INTEGER DEFAULT 0 NOT NULL,
266 RID INTEGER NOT NULL,
268 Time INTEGER NOT NULL,
269 Bytes INTEGER NOT NULL,
270 Direction Boolean NOT NULL,
271 Spamlevel INTEGER NOT NULL,
272 VirusInfo VARCHAR(255) NULL,
273 PTime INTEGER NOT NULL,
274 Sender VARCHAR(255) NOT NULL,
275 PRIMARY KEY (CID, RID));
277 CREATE INDEX CStatistic_Time_Index ON CStatistic (Time);
279 CREATE TABLE CReceivers
280 (CStatistic_CID INTEGER NOT NULL,
281 CStatistic_RID INTEGER NOT NULL,
282 Receiver VARCHAR(255) NOT NULL,
283 Blocked Boolean NOT NULL);
285 CREATE INDEX CStatistic_ID_Index ON CReceivers (CStatistic_CID, CStatistic_RID);
288 # user preferences (black an whitelists, ...)
289 # Name: perference name ('BL' -> blacklist, 'WL' -> whitelist)
290 # Data: arbitrary data
291 my $userprefs_ctablecmd = <<__EOD;
292 CREATE TABLE UserPrefs
296 MTime INTEGER NOT NULL,
297 PRIMARY KEY (PMail, Name));
299 CREATE INDEX UserPrefs_MTime_Index ON UserPrefs (MTime);
303 sub cond_create_dbtable
{
304 my ($dbh, $name, $ctablecmd) = @_;
309 my $cmd = "SELECT tablename FROM pg_tables " .
310 "WHERE tablename = lower ('$name')";
312 my $sth = $dbh->prepare($cmd);
316 if (!(my $ref = $sth->fetchrow_hashref())) {
317 $dbh->do ($ctablecmd);
330 sub database_column_exists
{
331 my ($dbh, $table, $column) = @_;
333 my $sth = $dbh->prepare(
334 "SELECT column_name FROM information_schema.columns " .
335 "WHERE table_name = ? and column_name = ?");
336 $sth->execute(lc($table), lc($column));
337 my $res = $sth->fetchrow_hashref();
338 return defined($res);
357 $dbname = $default_db_name if !$dbname;
359 my $silent_opts = { outfunc
=> sub {}, errfunc
=> sub {} };
360 # make sure we have user 'root'
361 eval { postgres_admin_cmd
('createuser', $silent_opts, '-D', 'root'); };
362 # also create 'www-data' (and give it read-only access below)
363 eval { postgres_admin_cmd
('createuser', $silent_opts, '-I', '-D', 'www-data'); };
365 # use sql_ascii to avoid any character set conversions, and be compatible with
366 # older postgres versions (update from 8.1 must be possible)
368 $createdb->($dbname);
370 my $dbh = open_ruledb
($dbname);
372 # make sure 'www-data' can read all tables
373 $dbh->do("ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO \"www-data\"");
377 CREATE TABLE Attribut
378 (Object_ID INTEGER NOT NULL,
379 Name VARCHAR(20) NOT NULL,
381 PRIMARY KEY (Object_ID, Name));
383 CREATE INDEX Attribut_Object_ID_Index ON Attribut(Object_ID);
387 ObjectType INTEGER NOT NULL,
388 Objectgroup_ID INTEGER NOT NULL,
392 CREATE TABLE Objectgroup
394 Name VARCHAR(255) NOT NULL,
395 Info VARCHAR(255) NULL,
396 Class VARCHAR(10) NOT NULL,
401 Name VARCHAR(255) NULL,
402 Priority INTEGER NOT NULL,
403 Active INTEGER NOT NULL DEFAULT 0,
404 Direction INTEGER NOT NULL DEFAULT 2,
405 Count INTEGER NOT NULL DEFAULT 0,
408 CREATE TABLE RuleGroup
409 (Objectgroup_ID INTEGER NOT NULL,
410 Rule_ID INTEGER NOT NULL,
411 Grouptype INTEGER NOT NULL,
412 PRIMARY KEY (Objectgroup_ID, Rule_ID, Grouptype));
414 $cgreylist_ctablecmd;
416 $clusterinfo_ctablecmd;
418 $local_stat_ctablecmd;
420 $daily_stat_ctablecmd;
422 $domain_stat_ctablecmd;
426 $cmailstore_ctablecmd;
428 $cstatistic_ctablecmd;
430 $userprefs_ctablecmd;
432 $virusinfo_stat_ctablecmd;
439 sub cond_create_action_quarantine
{
442 my $dbh = $ruledb->{dbh
};
445 my $sth = $dbh->prepare(
446 "SELECT * FROM Objectgroup, Object " .
447 "WHERE Object.ObjectType = ? AND Objectgroup.Class = ? " .
448 "AND Object.objectgroup_id = Objectgroup.id");
450 my $otype = PMG
::RuleDB
::Quarantine
::otype
();
451 if ($sth->execute($otype, 'action') <= 0) {
452 my $obj = PMG
::RuleDB
::Quarantine-
>new ();
453 my $txt = decode_entities
(PMG
::RuleDB
::Quarantine-
>otype_text);
454 my $quarantine = $ruledb->create_group_with_obj
455 ($obj, $txt, 'Move to quarantine.');
460 sub cond_create_std_actions
{
463 cond_create_action_quarantine
($ruledb);
465 #cond_create_action_report_spam($ruledb);
472 my $dbh = $ruledb->{dbh
};
474 # make sure we do not use slow sequential scans when upgraing
475 # database (before analyze can gather statistics)
476 $dbh->do("set enable_seqscan = false");
479 'LocalStat', $local_stat_ctablecmd,
480 'DailyStat', $daily_stat_ctablecmd,
481 'DomainStat', $domain_stat_ctablecmd,
482 'StatInfo', $statinfo_ctablecmd,
483 'CMailStore', $cmailstore_ctablecmd,
484 'UserPrefs', $userprefs_ctablecmd,
485 'CGreylist', $cgreylist_ctablecmd,
486 'CStatistic', $cstatistic_ctablecmd,
487 'ClusterInfo', $clusterinfo_ctablecmd,
488 'VirusInfo', $virusinfo_stat_ctablecmd,
491 foreach my $table (keys %$tables) {
492 cond_create_dbtable
($dbh, $table, $tables->{$table});
495 cond_create_std_actions
($ruledb);
497 # upgrade tables here if necessary
498 if (!database_column_exists
($dbh, 'LocalStat', 'PregreetCount')) {
499 $dbh->do("ALTER TABLE LocalStat ADD COLUMN " .
500 "PregreetCount INTEGER DEFAULT 0 NOT NULL");
503 eval { $dbh->do("ALTER TABLE LocalStat DROP CONSTRAINT localstat_time_key"); };
507 # add missing TicketID to CMSReceivers
508 if (!database_column_exists
($dbh, 'CMSReceivers', 'TicketID')) {
511 $dbh->do("CREATE SEQUENCE cmsreceivers_ticketid_seq");
512 $dbh->do("ALTER TABLE CMSReceivers ADD COLUMN " .
513 "TicketID INTEGER NOT NULL " .
514 "DEFAULT nextval('cmsreceivers_ticketid_seq')");
515 $dbh->do("ALTER TABLE CMSReceivers ALTER COLUMN " .
516 "TicketID DROP DEFAULT");
517 $dbh->do("DROP SEQUENCE cmsreceivers_ticketid_seq");
526 # update obsolete content type names
528 $dbh->do("UPDATE Object " .
529 "SET value = 'content-type:application/java-vm' ".
530 "WHERE objecttype = 3003 " .
531 "AND value = 'content-type:application/x-java-vm';");
534 # FIXME: drop Host column with PMG 7.0
535 # increase column size of cgreylist.ipnet for ipv6 support and transfer data
537 my $sth = $dbh->prepare("SELECT character_maximum_length ".
538 "FROM information_schema.columns ".
539 "WHERE table_name = 'cgreylist' AND column_name = 'ipnet'");
541 my $res = $sth->fetchrow_hashref();
542 if ($res->{character_maximum_length
} == 16) {
544 $dbh->do("ALTER TABLE CGreylist ALTER COLUMN " .
545 "IPNet TYPE varchar(49)");
547 $dbh->do("UPDATE CGreylist cg1 SET IPNet = IPNet || '.0/24' ".
548 "WHERE position('/' in IPNet) = 0 AND ".
549 "NOT EXISTS (SELECT 1 FROM CGreylist cg2 WHERE ".
550 "cg2.IPNet = cg1.IPNet || '.0/24' AND ".
551 "cg1.Receiver = cg2.Receiver AND cg1.Sender = cg2.Sender)");
553 #ignore errors here - legacy rows will eventually expire
562 foreach my $table (keys %$tables) {
563 eval { $dbh->do("ANALYZE $table"); };
571 my ($ruledb, $reset, $testmode) = @_;
573 my $dbh = $ruledb->{dbh
};
576 # Greylist Objectgroup
577 my $greylistgroup = PMG
::RuleDB
::Group-
>new
578 ("GreyExclusion", "-", "greylist");
579 $ruledb->save_group ($greylistgroup);
582 # we do not touch greylist objects
583 my $glids = "SELECT object.ID FROM Object, Objectgroup WHERE " .
584 "objectgroup_id = objectgroup.id and class = 'greylist'";
586 $dbh->do ("DELETE FROM Rule; " .
587 "DELETE FROM RuleGroup; " .
588 "DELETE FROM Attribut WHERE Object_ID NOT IN ($glids); " .
589 "DELETE FROM Object WHERE ID NOT IN ($glids); " .
590 "DELETE FROM Objectgroup WHERE class != 'greylist';");
596 my $obj = PMG
::RuleDB
::EMail-
>new ('nomail@fromthisdomain.com');
597 my $blacklist = $ruledb->create_group_with_obj(
598 $obj, 'Blacklist', 'Global blacklist');
601 $obj = PMG
::RuleDB
::EMail-
>new('mail@fromthisdomain.com');
602 my $whitelist = $ruledb->create_group_with_obj(
603 $obj, 'Whitelist', 'Global whitelist');
608 $obj = PMG
::RuleDB
::TimeFrame-
>new(8*60, 16*60);
609 my $working_hours =$ruledb->create_group_with_obj($obj, 'Office Hours' ,
610 'Usual office hours');
615 $obj = PMG
::RuleDB
::ContentTypeFilter-
>new('image/.*');
616 my $img_content = $ruledb->create_group_with_obj(
617 $obj, 'Images', 'All kinds of graphic files');
620 $obj = PMG
::RuleDB
::ContentTypeFilter-
>new('audio/.*');
621 my $mm_content = $ruledb->create_group_with_obj(
622 $obj, 'Multimedia', 'Audio and Video');
624 $obj = PMG
::RuleDB
::ContentTypeFilter-
>new('video/.*');
625 $ruledb->group_add_object($mm_content, $obj);
628 $obj = PMG
::RuleDB
::ContentTypeFilter-
>new('application/vnd\.ms-excel');
629 my $office_content = $ruledb->create_group_with_obj(
630 $obj, 'Office Files', 'Common Office Files');
632 $obj = PMG
::RuleDB
::ContentTypeFilter-
>new(
633 'application/vnd\.ms-powerpoint');
635 $ruledb->group_add_object($office_content, $obj);
637 $obj = PMG
::RuleDB
::ContentTypeFilter-
>new('application/msword');
638 $ruledb->group_add_object ($office_content, $obj);
640 $obj = PMG
::RuleDB
::ContentTypeFilter-
>new(
641 'application/vnd\.openxmlformats-officedocument\..*');
642 $ruledb->group_add_object($office_content, $obj);
644 $obj = PMG
::RuleDB
::ContentTypeFilter-
>new(
645 'application/vnd\.oasis\.opendocument\..*');
646 $ruledb->group_add_object($office_content, $obj);
648 $obj = PMG
::RuleDB
::ContentTypeFilter-
>new(
649 'application/vnd\.stardivision\..*');
650 $ruledb->group_add_object($office_content, $obj);
652 $obj = PMG
::RuleDB
::ContentTypeFilter-
>new(
653 'application/vnd\.sun\.xml\..*');
654 $ruledb->group_add_object($office_content, $obj);
657 $obj = PMG
::RuleDB
::ContentTypeFilter-
>new(
658 'application/x-ms-dos-executable');
659 my $exe_content = $ruledb->create_group_with_obj(
660 $obj, 'Dangerous Content', 'executable files and partial messages');
662 $obj = PMG
::RuleDB
::ContentTypeFilter-
>new('application/x-java');
663 $ruledb->group_add_object($exe_content, $obj);
664 $obj = PMG
::RuleDB
::ContentTypeFilter-
>new('application/javascript');
665 $ruledb->group_add_object($exe_content, $obj);
666 $obj = PMG
::RuleDB
::ContentTypeFilter-
>new('application/x-executable');
667 $ruledb->group_add_object($exe_content, $obj);
668 $obj = PMG
::RuleDB
::ContentTypeFilter-
>new('application/x-ms-dos-executable');
669 $ruledb->group_add_object($exe_content, $obj);
670 $obj = PMG
::RuleDB
::ContentTypeFilter-
>new('message/partial');
671 $ruledb->group_add_object($exe_content, $obj);
672 $obj = PMG
::RuleDB
::MatchFilename-
>new('.*\.(vbs|pif|lnk|shs|shb)');
673 $ruledb->group_add_object($exe_content, $obj);
674 $obj = PMG
::RuleDB
::MatchFilename-
>new('.*\.\{.+\}');
675 $ruledb->group_add_object($exe_content, $obj);
678 $obj = PMG
::RuleDB
::Virus-
>new();
679 my $virus = $ruledb->create_group_with_obj(
680 $obj, 'Virus', 'Matches virus infected mail');
685 $obj = PMG
::RuleDB
::Spam-
>new(3);
686 my $spam3 = $ruledb->create_group_with_obj(
687 $obj, 'Spam (Level 3)', 'Matches possible spam mail');
689 $obj = PMG
::RuleDB
::Spam-
>new(5);
690 my $spam5 = $ruledb->create_group_with_obj(
691 $obj, 'Spam (Level 5)', 'Matches possible spam mail');
693 $obj = PMG
::RuleDB
::Spam-
>new(10);
694 my $spam10 = $ruledb->create_group_with_obj(
695 $obj, 'Spam (Level 10)', 'Matches possible spam mail');
700 $obj = PMG
::RuleDB
::ModField-
>new('X-SPAM-LEVEL', '__SPAM_INFO__');
701 my $mod_spam_level = $ruledb->create_group_with_obj(
702 $obj, 'Modify Spam Level',
703 'Mark mail as spam by adding a header tag.');
706 $obj = PMG
::RuleDB
::ModField-
>new('subject', 'SPAM: __SUBJECT__');
707 my $mod_spam_subject = $ruledb->create_group_with_obj(
708 $obj, 'Modify Spam Subject',
709 'Mark mail as spam by modifying the subject.');
711 # Remove matching attachments
712 $obj = PMG
::RuleDB
::Remove-
>new(0);
713 my $remove = $ruledb->create_group_with_obj(
714 $obj, 'Remove attachments', 'Remove matching attachments');
716 # Remove all attachments
717 $obj = PMG
::RuleDB
::Remove-
>new(1);
718 my $remove_all = $ruledb->create_group_with_obj(
719 $obj, 'Remove all attachments', 'Remove all attachments');
722 $obj = PMG
::RuleDB
::Accept-
>new();
723 my $accept = $ruledb->create_group_with_obj(
724 $obj, 'Accept', 'Accept mail for Delivery');
727 $obj = PMG
::RuleDB
::Block-
>new ();
728 my $block = $ruledb->create_group_with_obj($obj, 'Block', 'Block mail');
731 $obj = PMG
::RuleDB
::Quarantine-
>new();
732 my $quarantine = $ruledb->create_group_with_obj(
733 $obj, 'Quarantine', 'Move mail to quarantine');
736 $obj = PMG
::RuleDB
::Notify-
>new('__ADMIN__');
737 my $notify_admin = $ruledb->create_group_with_obj(
738 $obj, 'Notify Admin', 'Send notification');
741 $obj = PMG
::RuleDB
::Notify-
>new('__SENDER__');
742 my $notify_sender = $ruledb->create_group_with_obj(
743 $obj, 'Notify Sender', 'Send notification');
746 $obj = PMG
::RuleDB
::Disclaimer-
>new ();
747 my $add_discl = $ruledb->create_group_with_obj(
748 $obj, 'Disclaimer', 'Add Disclaimer');
750 # Move to attachment quarantine
751 $obj = PMG
::RuleDB
::Remove-
>new(0, undef, undef, 1);
752 my $attach_quar = $ruledb->create_group_with_obj(
753 $obj, 'Attachment Quarantine (remove matching)', 'Remove matching attachments and move the whole mail to the attachment quarantine.');
755 # Remove all attachments
756 $obj = PMG
::RuleDB
::Remove-
>new(1, undef, undef, 1);
757 my $attach_quar_all = $ruledb->create_group_with_obj(
758 $obj, 'Attachment Quarantine (remove all)', 'Remove all attachments and move the whole mail to the attachment quarantine.');
760 # Attach original mail
761 #$obj = Proxmox::RuleDB::Attach->new ();
762 #my $attach_orig = $ruledb->create_group_with_obj ($obj, 'Attach Original Mail',
763 # 'Attach Original Mail');
765 ####################### RULES ##################################
767 ## Block Dangerous Files
768 my $rule = PMG
::RuleDB
::Rule-
>new ('Block Dangerous Files', 93, 1, 0);
769 $ruledb->save_rule ($rule);
771 $ruledb->rule_add_what_group ($rule, $exe_content);
772 $ruledb->rule_add_action ($rule, $remove);
775 $rule = PMG
::RuleDB
::Rule-
>new ('Block Viruses', 96, 1, 0);
776 $ruledb->save_rule ($rule);
778 $ruledb->rule_add_what_group ($rule, $virus);
779 $ruledb->rule_add_action ($rule, $notify_admin);
782 $ruledb->rule_add_action ($rule, $block);
784 $ruledb->rule_add_action ($rule, $quarantine);
788 $rule = PMG
::RuleDB
::Rule-
>new ('Virus Alert', 96, 1, 1);
789 $ruledb->save_rule ($rule);
791 $ruledb->rule_add_what_group ($rule, $virus);
792 $ruledb->rule_add_action ($rule, $notify_sender);
793 $ruledb->rule_add_action ($rule, $notify_admin);
794 $ruledb->rule_add_action ($rule, $block);
797 $rule = PMG
::RuleDB
::Rule-
>new ('Blacklist', 98, 1, 0);
798 $ruledb->save_rule ($rule);
800 $ruledb->rule_add_from_group ($rule, $blacklist);
801 $ruledb->rule_add_action ($rule, $block);
805 $rule = PMG
::RuleDB
::Rule-
>new ('Modify Header', 90, 1, 0);
806 $ruledb->save_rule ($rule);
807 $ruledb->rule_add_action ($rule, $mod_spam_level);
811 $rule = PMG
::RuleDB
::Rule-
>new ('Whitelist', 85, 1, 0);
812 $ruledb->save_rule ($rule);
814 $ruledb->rule_add_from_group ($rule, $whitelist);
815 $ruledb->rule_add_action ($rule, $accept);
818 $rule = PMG
::RuleDB
::Rule-
>new ('Mark Spam', 80, 1, 0);
819 $ruledb->save_rule ($rule);
821 $ruledb->rule_add_what_group ($rule, $spam10);
822 $ruledb->rule_add_action ($rule, $mod_spam_level);
823 $ruledb->rule_add_action ($rule, $mod_spam_subject);
825 # Quarantine/Mark Spam (Level 3)
826 $rule = PMG
::RuleDB
::Rule-
>new ('Quarantine/Mark Spam (Level 3)', 80, 1, 0);
827 $ruledb->save_rule ($rule);
829 $ruledb->rule_add_what_group ($rule, $spam3);
830 $ruledb->rule_add_action ($rule, $mod_spam_subject);
831 $ruledb->rule_add_action ($rule, $quarantine);
832 #$ruledb->rule_add_action ($rule, $count_spam);
835 # Quarantine/Mark Spam (Level 5)
836 $rule = PMG
::RuleDB
::Rule-
>new ('Quarantine/Mark Spam (Level 5)', 81, 0, 0);
837 $ruledb->save_rule ($rule);
839 $ruledb->rule_add_what_group ($rule, $spam5);
840 $ruledb->rule_add_action ($rule, $mod_spam_subject);
841 $ruledb->rule_add_action ($rule, $quarantine);
843 ## Block Spam Level 10
844 $rule = PMG
::RuleDB
::Rule-
>new ('Block Spam (Level 10)', 82, 0, 0);
845 $ruledb->save_rule ($rule);
847 $ruledb->rule_add_what_group ($rule, $spam10);
848 $ruledb->rule_add_action ($rule, $block);
850 ## Block Outgoing Spam
851 $rule = PMG
::RuleDB
::Rule-
>new ('Block outgoing Spam', 70, 0, 1);
852 $ruledb->save_rule ($rule);
854 $ruledb->rule_add_what_group ($rule, $spam3);
855 $ruledb->rule_add_action ($rule, $notify_admin);
856 $ruledb->rule_add_action ($rule, $notify_sender);
857 $ruledb->rule_add_action ($rule, $block);
860 $rule = PMG
::RuleDB
::Rule-
>new ('Add Disclaimer', 60, 0, 1);
861 $ruledb->save_rule ($rule);
862 $ruledb->rule_add_action ($rule, $add_discl);
864 # Block Multimedia Files
865 $rule = PMG
::RuleDB
::Rule-
>new ('Block Multimedia Files', 87, 0, 2);
866 $ruledb->save_rule ($rule);
868 $ruledb->rule_add_what_group ($rule, $mm_content);
869 $ruledb->rule_add_action ($rule, $remove);
871 # Quarantine Office Files
872 $rule = PMG
::RuleDB
::Rule-
>new ('Quarantine Office Files', 89, 0, 0);
873 $ruledb->save_rule ($rule);
875 $ruledb->rule_add_what_group ($rule, $office_content);
876 $ruledb->rule_add_action ($rule, $attach_quar);
878 #$ruledb->rule_add_from_group ($rule, $anybody);
879 #$ruledb->rule_add_from_group ($rule, $trusted);
880 #$ruledb->rule_add_to_group ($rule, $anybody);
881 #$ruledb->rule_add_what_group ($rule, $ct_filter);
882 #$ruledb->rule_add_action ($rule, $add_discl);
883 #$ruledb->rule_add_action ($rule, $remove);
884 #$ruledb->rule_add_action ($rule, $bcc);
885 #$ruledb->rule_add_action ($rule, $storeq);
886 #$ruledb->rule_add_action ($rule, $accept);
888 cond_create_std_actions
($ruledb);
893 sub get_remote_time
{
896 my $sth = $rdb->prepare("SELECT EXTRACT (EPOCH FROM TIMESTAMP (0) WITH TIME ZONE 'now') as ctime;");
898 my $ctinfo = $sth->fetchrow_hashref();
901 return $ctinfo ?
$ctinfo->{ctime
} : 0;
905 my ($lcid, $database) = @_;
907 die "got unexpected cid for new master" if !$lcid;
912 $dbh = open_ruledb
($database);
916 print STDERR
"update quarantine database\n";
917 $dbh->do ("UPDATE CMailStore SET CID = $lcid WHERE CID = 0;" .
918 "UPDATE CMSReceivers SET CMailStore_CID = $lcid WHERE CMailStore_CID = 0;");
920 print STDERR
"update statistic database\n";
921 $dbh->do ("UPDATE CStatistic SET CID = $lcid WHERE CID = 0;" .
922 "UPDATE CReceivers SET CStatistic_CID = $lcid WHERE CStatistic_CID = 0;");
924 print STDERR
"update greylist database\n";
925 $dbh->do ("UPDATE CGreylist SET CID = $lcid WHERE CID = 0;");
927 print STDERR
"update localstat database\n";
928 $dbh->do ("UPDATE LocalStat SET CID = $lcid WHERE CID = 0;");
935 $dbh->rollback if $err;
942 sub purge_statistic_database
{
943 my ($dbh, $statlifetime) = @_;
945 return if $statlifetime <= 0;
947 my (undef, undef, undef, $mday, $mon, $year) = localtime(time());
948 my $end = timelocal
(0, 0, 0, $mday, $mon, $year);
949 my $start = $end - $statlifetime*86400;
951 # delete statistics older than $start
958 my $sth = $dbh->prepare("DELETE FROM CStatistic WHERE time < $start");
964 $sth = $dbh->prepare(
965 "DELETE FROM CReceivers WHERE NOT EXISTS " .
966 "(SELECT * FROM CStatistic WHERE CID = CStatistic_CID AND RID = CStatistic_RID)");
980 sub purge_quarantine_database
{
981 my ($dbh, $qtype, $lifetime) = @_;
983 my $spooldir = $PMG::MailQueue
::spooldir
;
985 my (undef, undef, undef, $mday, $mon, $year) = localtime(time());
986 my $end = timelocal
(0, 0, 0, $mday, $mon, $year);
987 my $start = $end - $lifetime*86400;
989 my $sth = $dbh->prepare(
990 "SELECT file FROM CMailStore WHERE time < $start AND QType = '$qtype'");
996 while (my $ref = $sth->fetchrow_hashref()) {
997 my $filename = "$spooldir/$ref->{file}";
998 $count++ if unlink($filename);
1004 "DELETE FROM CMailStore WHERE time < $start AND QType = '$qtype';" .
1005 "DELETE FROM CMSReceivers WHERE NOT EXISTS " .
1006 "(SELECT * FROM CMailStore WHERE CID = CMailStore_CID AND RID = CMailStore_RID)");
1011 sub get_quarantine_count
{
1012 my ($dbh, $qtype) = @_;
1014 # Note;: We try to estimate used disk space - each mail
1015 # is stored in an extra file ...
1019 my $sth = $dbh->prepare(
1020 "SELECT count(ID) as count, sum (ceil((Bytes+$bs-1)/$bs)*$bs) / (1024*1024) as mbytes, " .
1021 "avg(Bytes) as avgbytes, avg(Spamlevel) as avgspam " .
1022 "FROM CMailStore WHERE QType = ?");
1024 $sth->execute($qtype);
1026 my $ref = $sth->fetchrow_hashref();
1030 foreach my $k (qw(count mbytes avgbytes avgspam)) {
1038 my ($ldb, $rdb, $table) = @_;
1040 $table = lc($table);
1042 my $sth = $ldb->column_info(undef, undef, $table, undef);
1043 my $attrs = $sth->fetchall_arrayref({});
1046 foreach my $ref (@$attrs) {
1047 push @col_arr, $ref->{COLUMN_NAME
};
1052 my $cols = join(', ', @col_arr);
1053 $cols || die "unable to fetch column definitions of table '$table' : ERROR";
1055 $rdb->do("COPY $table ($cols) TO STDOUT");
1060 $ldb->do("COPY $table ($cols) FROM stdin");
1062 while ($rdb->pg_getcopydata($data) >= 0) {
1063 $ldb->pg_putcopydata($data);
1066 $ldb->pg_putcopyend();
1069 $ldb->pg_putcopyend();
1074 sub copy_selected_data
{
1075 my ($dbh, $select_sth, $table, $attrs, $callback) = @_;
1079 my $insert_sth = $dbh->prepare(
1080 "INSERT INTO ${table}(" . join(',', @$attrs) . ') ' .
1081 'VALUES (' . join(',', ('?') x
scalar(@$attrs)) . ')');
1083 while (my $ref = $select_sth->fetchrow_hashref()) {
1084 $callback->($ref) if $callback;
1086 $insert_sth->execute(map { $ref->{$_} } @$attrs);
1092 sub update_master_clusterinfo
{
1093 my ($clientcid) = @_;
1095 my $dbh = open_ruledb
();
1097 $dbh->do("DELETE FROM ClusterInfo WHERE CID = $clientcid");
1099 my @mt = ('CMSReceivers', 'CGreylist', 'UserPrefs', 'DomainStat', 'DailyStat', 'LocalStat', 'VirusInfo');
1101 foreach my $table (@mt) {
1102 $dbh->do ("INSERT INTO ClusterInfo (cid, name, ivalue) select $clientcid, 'lastmt_$table', " .
1103 "EXTRACT(EPOCH FROM now())");
1107 sub update_client_clusterinfo
{
1108 my ($mastercid) = @_;
1110 my $dbh = open_ruledb
();
1112 $dbh->do ("DELETE FROM StatInfo"); # not needed at node
1114 $dbh->do ("DELETE FROM ClusterInfo WHERE CID = $mastercid");
1116 $dbh->do ("INSERT INTO ClusterInfo (cid, name, ivalue) select $mastercid, 'lastid_CMailStore', " .
1117 "COALESCE (max (rid), -1) FROM CMailStore WHERE cid = $mastercid");
1119 $dbh->do ("INSERT INTO ClusterInfo (cid, name, ivalue) select $mastercid, 'lastid_CStatistic', " .
1120 "COALESCE (max (rid), -1) FROM CStatistic WHERE cid = $mastercid");
1122 my @mt = ('CMSReceivers', 'CGreylist', 'UserPrefs', 'DomainStat', 'DailyStat', 'LocalStat', 'VirusInfo');
1124 foreach my $table (@mt) {
1125 $dbh->do ("INSERT INTO ClusterInfo (cid, name, ivalue) select $mastercid, 'lastmt_$table', " .
1126 "COALESCE (max (mtime), 0) FROM $table");
1130 sub create_clusterinfo_default
{
1131 my ($dbh, $rcid, $name, $ivalue, $svalue) = @_;
1133 my $sth = $dbh->prepare("SELECT * FROM ClusterInfo WHERE CID = ? AND Name = ?");
1134 $sth->execute($rcid, $name);
1135 if (!$sth->fetchrow_hashref()) {
1136 $dbh->do("INSERT INTO ClusterInfo (CID, Name, IValue, SValue) " .
1137 "VALUES (?, ?, ?, ?)", undef,
1138 $rcid, $name, $ivalue, $svalue);
1143 sub read_int_clusterinfo
{
1144 my ($dbh, $rcid, $name) = @_;
1146 my $sth = $dbh->prepare(
1147 "SELECT ivalue as value FROM ClusterInfo " .
1148 "WHERE cid = ? AND NAME = ?");
1149 $sth->execute($rcid, $name);
1150 my $cinfo = $sth->fetchrow_hashref();
1153 return $cinfo->{value
};
1156 sub write_maxint_clusterinfo
{
1157 my ($dbh, $rcid, $name, $value) = @_;
1159 $dbh->do("UPDATE ClusterInfo SET ivalue = GREATEST(ivalue, ?) " .
1160 "WHERE cid = ? AND name = ?", undef,
1161 $value, $rcid, $name);
1167 my $ni = $cinfo->{master
};
1169 die "no master defined - unable to sync data from master\n" if !$ni;
1171 my $master_ip = $ni->{ip
};
1172 my $master_cid = $ni->{cid
};
1173 my $master_name = $ni->{name
};
1175 my $fn = "/tmp/masterdb$$.tar";
1178 my $dbname = $default_db_name;
1181 print STDERR
"copying master database from '${master_ip}'\n";
1183 open (my $fh, ">", $fn) || die "open '$fn' failed - $!\n";
1185 my $cmd = ['/usr/bin/ssh', '-o', 'BatchMode=yes',
1186 '-o', "HostKeyAlias=${master_name}", $master_ip,
1187 'pg_dump', $dbname, '-F', 'c' ];
1189 PVE
::Tools
::run_command
($cmd, output
=> '>&' . fileno($fh));
1195 print STDERR
"copying master database finished (got $size bytes)\n";
1197 print STDERR
"delete local database\n";
1199 postgres_admin_cmd
('dropdb', undef, $dbname , '--if-exists');
1201 print STDERR
"create new local database\n";
1203 $createdb->($dbname);
1205 print STDERR
"insert received data into local database\n";
1211 if ($line =~ m/restoring data for table \"(.+)\"/) {
1212 print STDERR
"restoring table $1\n";
1213 } elsif (!$mess && ($line =~ m/creating (INDEX|CONSTRAINT)/)) {
1214 $mess = "creating indexes";
1215 print STDERR
"$mess\n";
1222 errmsg
=> "pg_restore failed"
1225 postgres_admin_cmd
('pg_restore', $opts, '-d', $dbname, '-v', $fn);
1227 print STDERR
"run analyze to speed up database queries\n";
1229 postgres_admin_cmd
('psql', { input
=> 'analyze;' }, $dbname);
1231 update_client_clusterinfo
($master_cid);
1241 sub cluster_sync_status
{
1248 foreach my $ni (values %{$cinfo->{ids
}}) {
1249 next if $cinfo->{local}->{cid
} == $ni->{cid
}; # skip local CID
1250 $minmtime->{$ni->{cid
}} = 0;
1254 $dbh = open_ruledb
();
1256 my $sth = $dbh->prepare(
1257 "SELECT cid, MIN (ivalue) as minmtime FROM ClusterInfo " .
1258 "WHERE name = 'lastsync' AND ivalue > 0 " .
1263 while (my $info = $sth->fetchrow_hashref()) {
1264 foreach my $ni (values %{$cinfo->{ids
}}) {
1265 next if $cinfo->{local}->{cid
} == $ni->{cid
}; # skip local CID
1266 if ($ni->{cid
} == $info->{cid
}) { # node exists
1267 $minmtime->{$ni->{cid
}} = $info->{minmtime
};
1276 $dbh->disconnect() if $dbh;
1278 syslog
('err', $err) if $err;
1283 sub load_mail_data
{
1284 my ($dbh, $cid, $rid, $ticketid) = @_;
1286 my $sth = $dbh->prepare(
1287 "SELECT * FROM CMailStore, CMSReceivers WHERE " .
1288 "CID = ? AND RID = ? AND TicketID = ? AND " .
1289 "CID = CMailStore_CID AND RID = CMailStore_RID");
1290 $sth->execute($cid, $rid, $ticketid);
1292 my $res = $sth->fetchrow_hashref();
1296 die "no such mail (C${cid}R${rid}T${ticketid})\n" if !defined($res);
1304 # Note: we pass $ruledb when modifying SMTP whitelist
1305 if (defined($ruledb)) {
1307 my $rulecache = PMG
::RuleCache-
>new($ruledb);
1308 PMG
::Config
::rewrite_postfix_whitelist
($rulecache);
1311 warn "problems updating SMTP whitelist - $err";
1315 PMG
::Utils
::reload_smtp_filter
();