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 sub cgreylist_merge_sql
{
24 my $network = $with_mask ?
'network(set_masklen(?, ?))' : '?';
27 'INSERT INTO CGREYLIST (IPNet,Sender,Receiver,Instance,RCTime,' .
28 'ExTime,Delay,Blocked,Passed,MTime,CID) ' .
29 "VALUES ($network, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) " .
30 'ON CONFLICT (IPNet,Sender,Receiver) DO UPDATE SET ' .
31 'CID = GREATEST(CGREYLIST.CID, excluded.CID), RCTime = LEAST(CGREYLIST.RCTime, excluded.RCTime),' .
32 'ExTime = GREATEST(CGREYLIST.ExTime, excluded.ExTime),' .
33 'Delay = GREATEST(CGREYLIST.Delay, excluded.Delay),' .
34 'Blocked = GREATEST(CGREYLIST.Blocked, excluded.Blocked),' .
35 'Passed = GREATEST(CGREYLIST.Passed, excluded.Passed)';
41 my ($database, $host, $port) = @_;
45 $database //= $default_db_name;
49 # Note: pmgtunnel uses UDP sockets inside directory '/run/pmgtunnel',
50 # and the cluster 'cid' as port number. You can connect to the
51 # socket with: host => /run/pmgtunnel, port => $cid
53 my $dsn = "dbi:Pg:dbname=$database;host=$host;port=$port;";
56 # only low level alarm interface works for DBI->connect
57 my $mask = POSIX
::SigSet-
>new(SIGALRM
);
58 my $action = POSIX
::SigAction-
>new(sub { die "connect timeout\n" }, $mask);
59 my $oldaction = POSIX
::SigAction-
>new();
60 sigaction
(SIGALRM
, $action, $oldaction);
66 $rdb = DBI-
>connect($dsn, 'root', undef, { PrintError
=> 0, RaiseError
=> 1 });
70 sigaction
(SIGALRM
, $oldaction); # restore original handler
76 my $dsn = "DBI:Pg:dbname=$database;host=/var/run/postgresql;port=$port";
77 my $user = $> == 0 ?
'root' : 'www-data';
78 my $dbh = DBI-
>connect($dsn, $user, undef, { PrintError
=> 0, RaiseError
=> 1 });
87 postgres_admin_cmd
('dropdb', undef, $dbname);
92 my $database_list = {};
97 my ($name, $owner) = map { PVE
::Tools
::trim
($_) } split(/\|/, $line);
98 return if !$name || !$owner;
100 $database_list->{$name} = { owner
=> $owner };
103 postgres_admin_cmd
('psql', { outfunc
=> $parser }, '--list', '--quiet', '--tuples-only');
105 return $database_list;
108 my $cgreylist_ctablecmd = <<__EOD;
109 CREATE TABLE CGreylist
110 (IPNet VARCHAR(49) NOT NULL,
111 Sender VARCHAR(255) NOT NULL,
112 Receiver VARCHAR(255) NOT NULL,
113 Instance VARCHAR(255),
114 RCTime INTEGER NOT NULL,
115 ExTime INTEGER NOT NULL,
116 Delay INTEGER NOT NULL DEFAULT 0,
117 Blocked INTEGER NOT NULL,
118 Passed INTEGER NOT NULL,
119 CID INTEGER NOT NULL,
120 MTime INTEGER NOT NULL,
121 PRIMARY KEY (IPNet, Sender, Receiver));
123 CREATE INDEX CGreylist_Instance_Sender_Index ON CGreylist (Instance, Sender);
125 CREATE INDEX CGreylist_ExTime_Index ON CGreylist (ExTime);
127 CREATE INDEX CGreylist_MTime_Index ON CGreylist (MTime);
130 my $clusterinfo_ctablecmd = <<__EOD;
131 CREATE TABLE ClusterInfo
132 (CID INTEGER NOT NULL,
133 Name VARCHAR NOT NULL,
136 PRIMARY KEY (CID, Name))
139 my $local_stat_ctablecmd = <<__EOD;
140 CREATE TABLE LocalStat
141 (Time INTEGER NOT NULL,
142 RBLCount INTEGER DEFAULT 0 NOT NULL,
143 PregreetCount INTEGER DEFAULT 0 NOT NULL,
144 CID INTEGER NOT NULL,
145 MTime INTEGER NOT NULL,
146 PRIMARY KEY (Time, CID));
148 CREATE INDEX LocalStat_MTime_Index ON LocalStat (MTime);
152 my $daily_stat_ctablecmd = <<__EOD;
153 CREATE TABLE DailyStat
154 (Time INTEGER NOT NULL UNIQUE,
155 CountIn INTEGER NOT NULL,
156 CountOut INTEGER NOT NULL,
157 BytesIn REAL NOT NULL,
158 BytesOut REAL NOT NULL,
159 VirusIn INTEGER NOT NULL,
160 VirusOut INTEGER NOT NULL,
161 SpamIn INTEGER NOT NULL,
162 SpamOut INTEGER NOT NULL,
163 BouncesIn INTEGER NOT NULL,
164 BouncesOut INTEGER NOT NULL,
165 GreylistCount INTEGER NOT NULL,
166 SPFCount INTEGER NOT NULL,
167 PTimeSum REAL NOT NULL,
168 MTime INTEGER NOT NULL,
169 RBLCount INTEGER DEFAULT 0 NOT NULL,
172 CREATE INDEX DailyStat_MTime_Index ON DailyStat (MTime);
176 my $domain_stat_ctablecmd = <<__EOD;
177 CREATE TABLE DomainStat
178 (Time INTEGER NOT NULL,
179 Domain VARCHAR(255) NOT NULL,
180 CountIn INTEGER NOT NULL,
181 CountOut INTEGER NOT NULL,
182 BytesIn REAL NOT NULL,
183 BytesOut REAL NOT NULL,
184 VirusIn INTEGER NOT NULL,
185 VirusOut INTEGER NOT NULL,
186 SpamIn INTEGER NOT NULL,
187 SpamOut INTEGER NOT NULL,
188 BouncesIn INTEGER NOT NULL,
189 BouncesOut INTEGER NOT NULL,
190 PTimeSum REAL NOT NULL,
191 MTime INTEGER NOT NULL,
192 PRIMARY KEY (Time, Domain));
194 CREATE INDEX DomainStat_MTime_Index ON DomainStat (MTime);
197 my $statinfo_ctablecmd = <<__EOD;
198 CREATE TABLE StatInfo
199 (Name VARCHAR(255) NOT NULL UNIQUE,
205 my $virusinfo_stat_ctablecmd = <<__EOD;
206 CREATE TABLE VirusInfo
207 (Time INTEGER NOT NULL,
208 Name VARCHAR NOT NULL,
209 Count INTEGER NOT NULL,
210 MTime INTEGER NOT NULL,
211 PRIMARY KEY (Time, Name));
213 CREATE INDEX VirusInfo_MTime_Index ON VirusInfo (MTime);
219 # V - Virus quarantine
220 # S - Spam quarantine
221 # D - Delayed Mails - not implemented
222 # A - Held for Audit - not implemented
227 my $cmailstore_ctablecmd = <<__EOD;
228 CREATE TABLE CMailStore
229 (CID INTEGER DEFAULT 0 NOT NULL,
230 RID INTEGER NOT NULL,
232 Time INTEGER NOT NULL,
233 QType "char" NOT NULL,
234 Bytes INTEGER NOT NULL,
235 Spamlevel INTEGER NOT NULL,
237 Sender VARCHAR(255) NOT NULL,
238 Header VARCHAR NOT NULL,
239 File VARCHAR(255) NOT NULL,
240 PRIMARY KEY (CID, RID));
241 CREATE INDEX CMailStore_Time_Index ON CMailStore (Time);
243 CREATE TABLE CMSReceivers
244 (CMailStore_CID INTEGER NOT NULL,
245 CMailStore_RID INTEGER NOT NULL,
246 PMail VARCHAR(255) NOT NULL,
247 Receiver VARCHAR(255),
248 TicketID INTEGER NOT NULL,
249 Status "char" NOT NULL,
250 MTime INTEGER NOT NULL);
252 CREATE INDEX CMailStore_ID_Index ON CMSReceivers (CMailStore_CID, CMailStore_RID);
254 CREATE INDEX CMSReceivers_MTime_Index ON CMSReceivers (MTime);
258 my $cstatistic_ctablecmd = <<__EOD;
259 CREATE TABLE CStatistic
260 (CID INTEGER DEFAULT 0 NOT NULL,
261 RID INTEGER NOT NULL,
263 Time INTEGER NOT NULL,
264 Bytes INTEGER NOT NULL,
265 Direction Boolean NOT NULL,
266 Spamlevel INTEGER NOT NULL,
267 VirusInfo VARCHAR(255) NULL,
268 PTime INTEGER NOT NULL,
269 Sender VARCHAR(255) NOT NULL,
270 PRIMARY KEY (CID, RID));
272 CREATE INDEX CStatistic_Time_Index ON CStatistic (Time);
274 CREATE TABLE CReceivers
275 (CStatistic_CID INTEGER NOT NULL,
276 CStatistic_RID INTEGER NOT NULL,
277 Receiver VARCHAR(255) NOT NULL,
278 Blocked Boolean NOT NULL);
280 CREATE INDEX CStatistic_ID_Index ON CReceivers (CStatistic_CID, CStatistic_RID);
283 # user preferences (black an whitelists, ...)
284 # Name: preference name ('BL' -> blacklist, 'WL' -> whitelist)
285 # Data: arbitrary data
286 my $userprefs_ctablecmd = <<__EOD;
287 CREATE TABLE UserPrefs
291 MTime INTEGER NOT NULL,
292 PRIMARY KEY (PMail, Name));
294 CREATE INDEX UserPrefs_MTime_Index ON UserPrefs (MTime);
298 my $rule_attributes_cmd = <<__EOD;
299 CREATE TABLE Rule_Attributes (
300 Rule_ID INTEGER NOT NULL REFERENCES Rule (ID) ON DELETE CASCADE,
301 Name VARCHAR(20) NOT NULL,
303 PRIMARY KEY (Rule_ID, Name)
306 CREATE INDEX Rule_Attributes_Rule_ID_Index ON Rule_Attributes(Rule_ID);
310 my $object_group_attributes_cmd = <<__EOD;
311 CREATE TABLE Objectgroup_Attributes (
312 Objectgroup_ID INTEGER NOT NULL REFERENCES Objectgroup (ID) ON DELETE CASCADE,
313 Name VARCHAR(20) NOT NULL,
315 PRIMARY KEY (Objectgroup_ID, Name)
318 CREATE INDEX Objectgroup_Attributes_Objectgroup_ID_Index ON Objectgroup_Attributes(Objectgroup_ID);
322 sub cond_create_dbtable
{
323 my ($dbh, $name, $ctablecmd) = @_;
328 my $cmd = "SELECT tablename FROM pg_tables " .
329 "WHERE tablename = lower ('$name')";
331 my $sth = $dbh->prepare($cmd);
335 if (!(my $ref = $sth->fetchrow_hashref())) {
336 $dbh->do ($ctablecmd);
349 sub database_column_exists
{
350 my ($dbh, $table, $column) = @_;
352 my $sth = $dbh->prepare(
353 "SELECT column_name FROM information_schema.columns " .
354 "WHERE table_name = ? and column_name = ?");
355 $sth->execute(lc($table), lc($column));
356 my $res = $sth->fetchrow_hashref();
357 return defined($res);
372 # allow root and www-data to access the public SCHEMA like pre prostgres15
373 # this is not a security issue, since the db is not externally reachable anyway and no
374 # other users should exist
375 my $grantcmd = "GRANT CREATE ON SCHEMA public To \"root\";"
376 ."GRANT USAGE ON SCHEMA public To \"root\";"
377 ."GRANT CREATE ON SCHEMA public To \"www-data\";"
378 ."GRANT USAGE ON SCHEMA public To \"www-data\";";
380 postgres_admin_cmd
('psql', { input
=> $grantcmd }, '-d', $dbname);
386 $dbname = $default_db_name if !$dbname;
388 my $silent_opts = { outfunc
=> sub {}, errfunc
=> sub {} };
389 # make sure we have user 'root'
390 eval { postgres_admin_cmd
('createuser', $silent_opts, '-D', 'root'); };
391 # also create 'www-data' (and give it read-only access below)
392 eval { postgres_admin_cmd
('createuser', $silent_opts, '-I', '-D', 'www-data'); };
394 # use sql_ascii to avoid any character set conversions, and be compatible with
395 # older postgres versions (update from 8.1 must be possible)
397 $createdb->($dbname);
399 my $dbh = open_ruledb
($dbname);
401 # make sure 'www-data' can read all tables
402 $dbh->do("ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO \"www-data\"");
406 CREATE TABLE Attribut (
407 Object_ID INTEGER NOT NULL,
408 Name VARCHAR(20) NOT NULL,
410 PRIMARY KEY (Object_ID, Name)
413 CREATE INDEX Attribut_Object_ID_Index ON Attribut(Object_ID);
415 CREATE TABLE Object (
417 ObjectType INTEGER NOT NULL,
418 Objectgroup_ID INTEGER NOT NULL,
423 CREATE TABLE Objectgroup
425 Name VARCHAR(255) NOT NULL,
426 Info VARCHAR(255) NULL,
427 Class VARCHAR(10) NOT NULL,
432 Name VARCHAR(255) NULL,
433 Priority INTEGER NOT NULL,
434 Active INTEGER NOT NULL DEFAULT 0,
435 Direction INTEGER NOT NULL DEFAULT 2,
436 Count INTEGER NOT NULL DEFAULT 0,
440 CREATE TABLE RuleGroup (
441 Objectgroup_ID INTEGER NOT NULL,
442 Rule_ID INTEGER NOT NULL,
443 Grouptype INTEGER NOT NULL,
444 PRIMARY KEY (Objectgroup_ID, Rule_ID, Grouptype)
447 $cgreylist_ctablecmd;
449 $clusterinfo_ctablecmd;
451 $local_stat_ctablecmd;
453 $daily_stat_ctablecmd;
455 $domain_stat_ctablecmd;
459 $cmailstore_ctablecmd;
461 $cstatistic_ctablecmd;
463 $userprefs_ctablecmd;
465 $virusinfo_stat_ctablecmd;
467 $rule_attributes_cmd;
469 $object_group_attributes_cmd;
476 sub cond_create_action_quarantine
{
479 my $dbh = $ruledb->{dbh
};
482 my $sth = $dbh->prepare(
483 "SELECT * FROM Objectgroup, Object WHERE Object.ObjectType = ? AND Objectgroup.Class = ?"
484 ." AND Object.objectgroup_id = Objectgroup.id"
487 my $otype = PMG
::RuleDB
::Quarantine
::otype
();
488 if ($sth->execute($otype, 'action') <= 0) {
489 my $obj = PMG
::RuleDB
::Quarantine-
>new ();
490 my $txt = decode_entities
(PMG
::RuleDB
::Quarantine-
>otype_text);
491 my $quarantine = $ruledb->create_group_with_obj($obj, $txt, 'Move to quarantine.');
496 sub cond_create_std_actions
{
499 cond_create_action_quarantine
($ruledb);
501 #cond_create_action_report_spam($ruledb);
508 my $dbh = $ruledb->{dbh
};
510 # make sure we do not use slow sequential scans when upgraing
511 # database (before analyze can gather statistics)
512 $dbh->do("set enable_seqscan = false");
515 'LocalStat', $local_stat_ctablecmd,
516 'DailyStat', $daily_stat_ctablecmd,
517 'DomainStat', $domain_stat_ctablecmd,
518 'StatInfo', $statinfo_ctablecmd,
519 'CMailStore', $cmailstore_ctablecmd,
520 'UserPrefs', $userprefs_ctablecmd,
521 'CGreylist', $cgreylist_ctablecmd,
522 'CStatistic', $cstatistic_ctablecmd,
523 'ClusterInfo', $clusterinfo_ctablecmd,
524 'VirusInfo', $virusinfo_stat_ctablecmd,
525 'Rule_Attributes', $rule_attributes_cmd,
526 'Objectgroup_Attributes', $object_group_attributes_cmd,
529 foreach my $table (keys %$tables) {
530 cond_create_dbtable
($dbh, $table, $tables->{$table});
533 cond_create_std_actions
($ruledb);
535 # upgrade tables here if necessary
536 if (!database_column_exists
($dbh, 'LocalStat', 'PregreetCount')) {
537 $dbh->do("ALTER TABLE LocalStat ADD COLUMN PregreetCount INTEGER DEFAULT 0 NOT NULL");
540 eval { $dbh->do("ALTER TABLE LocalStat DROP CONSTRAINT localstat_time_key"); };
543 # add missing TicketID to CMSReceivers
544 if (!database_column_exists
($dbh, 'CMSReceivers', 'TicketID')) {
547 $dbh->do("CREATE SEQUENCE cmsreceivers_ticketid_seq");
549 "ALTER TABLE CMSReceivers ADD COLUMN TicketID INTEGER NOT NULL"
550 ." DEFAULT nextval('cmsreceivers_ticketid_seq')"
552 $dbh->do("ALTER TABLE CMSReceivers ALTER COLUMN TicketID DROP DEFAULT");
553 $dbh->do("DROP SEQUENCE cmsreceivers_ticketid_seq");
562 # update obsolete content type names
565 "UPDATE Object SET value = 'content-type:application/java-vm' WHERE objecttype = 3003"
566 ." AND value = 'content-type:application/x-java-vm';"
570 # increase column size of cgreylist.ipnet for ipv6 support and transfer data
572 my $sth = $dbh->prepare(
573 "SELECT character_maximum_length FROM information_schema.columns"
574 ." WHERE table_name = 'cgreylist' AND column_name = 'ipnet'"
577 my $res = $sth->fetchrow_hashref();
578 if ($res->{character_maximum_length
} == 16) {
580 $dbh->do("ALTER TABLE CGreylist ALTER COLUMN IPNet TYPE varchar(49)");
583 "UPDATE CGreylist cg1 SET IPNet = IPNet || '.0/24' ".
584 "WHERE position('/' in IPNet) = 0 AND ".
585 "NOT EXISTS (SELECT 1 FROM CGreylist cg2 WHERE ".
586 "cg2.IPNet = cg1.IPNet || '.0/24' AND ".
587 "cg1.Receiver = cg2.Receiver AND cg1.Sender = cg2.Sender)"
590 #ignore errors here - legacy rows will eventually expire
599 # drop greylist Host column with PMG 7.0
600 if (database_column_exists
($dbh, 'CGreylist', 'Host')) {
603 $dbh->do("ALTER TABLE CGreylist DROP COLUMN Host");
612 foreach my $table (keys %$tables) {
613 eval { $dbh->do("ANALYZE $table"); };
621 my ($ruledb, $reset, $testmode) = @_;
623 my $dbh = $ruledb->{dbh
};
626 # Greylist Objectgroup
627 my $greylistgroup = PMG
::RuleDB
::Group-
>new("GreyExclusion", "-", "greylist");
628 $ruledb->save_group ($greylistgroup);
631 # we do not touch greylist objects
632 my $glids = "SELECT object.ID FROM Object, Objectgroup WHERE " .
633 "objectgroup_id = objectgroup.id and class = 'greylist'";
637 ." DELETE FROM RuleGroup;"
638 ." DELETE FROM Attribut WHERE Object_ID NOT IN ($glids);"
639 ." DELETE FROM Object WHERE ID NOT IN ($glids);"
640 ." DELETE FROM Objectgroup WHERE class != 'greylist';"
647 my $obj = PMG
::RuleDB
::EMail-
>new ('nomail@fromthisdomain.com');
648 my $blacklist = $ruledb->create_group_with_obj(
649 $obj, 'Blacklist', 'Global blacklist');
652 $obj = PMG
::RuleDB
::EMail-
>new('mail@fromthisdomain.com');
653 my $whitelist = $ruledb->create_group_with_obj($obj, 'Whitelist', 'Global whitelist');
658 $obj = PMG
::RuleDB
::TimeFrame-
>new(8*60, 16*60);
659 my $working_hours =$ruledb->create_group_with_obj($obj, 'Office Hours', 'Usual office hours');
664 $obj = PMG
::RuleDB
::ContentTypeFilter-
>new('image/.*');
665 my $img_content = $ruledb->create_group_with_obj($obj, 'Images', 'All kinds of graphic files');
668 $obj = PMG
::RuleDB
::ContentTypeFilter-
>new('audio/.*');
669 my $mm_content = $ruledb->create_group_with_obj($obj, 'Multimedia', 'Audio and Video');
671 $obj = PMG
::RuleDB
::ContentTypeFilter-
>new('video/.*');
672 $ruledb->group_add_object($mm_content, $obj);
675 $obj = PMG
::RuleDB
::ContentTypeFilter-
>new('application/vnd\.ms-excel');
676 my $office_content = $ruledb->create_group_with_obj($obj, 'Office Files', 'Common Office Files');
678 $obj = PMG
::RuleDB
::ContentTypeFilter-
>new('application/vnd\.ms-powerpoint');
680 $ruledb->group_add_object($office_content, $obj);
682 $obj = PMG
::RuleDB
::ContentTypeFilter-
>new('application/msword');
683 $ruledb->group_add_object ($office_content, $obj);
685 $obj = PMG
::RuleDB
::ContentTypeFilter-
>new(
686 'application/vnd\.openxmlformats-officedocument\..*');
687 $ruledb->group_add_object($office_content, $obj);
689 $obj = PMG
::RuleDB
::ContentTypeFilter-
>new('application/vnd\.oasis\.opendocument\..*');
690 $ruledb->group_add_object($office_content, $obj);
692 $obj = PMG
::RuleDB
::ContentTypeFilter-
>new('application/vnd\.stardivision\..*');
693 $ruledb->group_add_object($office_content, $obj);
695 $obj = PMG
::RuleDB
::ContentTypeFilter-
>new('application/vnd\.sun\.xml\..*');
696 $ruledb->group_add_object($office_content, $obj);
699 $obj = PMG
::RuleDB
::ContentTypeFilter-
>new('application/x-ms-dos-executable');
700 my $exe_content = $ruledb->create_group_with_obj(
701 $obj, 'Dangerous Content', 'executable files and partial messages');
703 $obj = PMG
::RuleDB
::ContentTypeFilter-
>new('application/x-java');
704 $ruledb->group_add_object($exe_content, $obj);
705 $obj = PMG
::RuleDB
::ContentTypeFilter-
>new('application/javascript');
706 $ruledb->group_add_object($exe_content, $obj);
707 $obj = PMG
::RuleDB
::ContentTypeFilter-
>new('application/x-executable');
708 $ruledb->group_add_object($exe_content, $obj);
709 $obj = PMG
::RuleDB
::ContentTypeFilter-
>new('message/partial');
710 $ruledb->group_add_object($exe_content, $obj);
711 $obj = PMG
::RuleDB
::MatchFilename-
>new('.*\.(vbs|pif|lnk|shs|shb)');
712 $ruledb->group_add_object($exe_content, $obj);
713 $obj = PMG
::RuleDB
::MatchFilename-
>new('.*\.\{.+\}');
714 $ruledb->group_add_object($exe_content, $obj);
717 $obj = PMG
::RuleDB
::Virus-
>new();
718 my $virus = $ruledb->create_group_with_obj($obj, 'Virus', 'Matches virus infected mail');
723 $obj = PMG
::RuleDB
::Spam-
>new(3);
724 my $spam3 = $ruledb->create_group_with_obj($obj, 'Spam (Level 3)', 'Matches possible spam mail');
726 $obj = PMG
::RuleDB
::Spam-
>new(5);
727 my $spam5 = $ruledb->create_group_with_obj($obj, 'Spam (Level 5)', 'Matches possible spam mail');
729 $obj = PMG
::RuleDB
::Spam-
>new(10);
730 my $spam10 = $ruledb->create_group_with_obj($obj, 'Spam (Level 10)', 'Matches possible spam mail');
735 $obj = PMG
::RuleDB
::ModField-
>new('X-SPAM-LEVEL', '__SPAM_INFO__');
736 my $mod_spam_level = $ruledb->create_group_with_obj(
737 $obj, 'Modify Spam Level', 'Mark mail as spam by adding a header tag.');
740 $obj = PMG
::RuleDB
::ModField-
>new('subject', 'SPAM: __SUBJECT__');
741 my $mod_spam_subject = $ruledb->create_group_with_obj(
742 $obj, 'Modify Spam Subject',
743 'Mark mail as spam by modifying the subject.'
746 # Remove matching attachments
747 $obj = PMG
::RuleDB
::Remove-
>new(0);
748 my $remove = $ruledb->create_group_with_obj(
749 $obj, 'Remove attachments', 'Remove matching attachments');
751 # Remove all attachments
752 $obj = PMG
::RuleDB
::Remove-
>new(1);
753 my $remove_all = $ruledb->create_group_with_obj(
754 $obj, 'Remove all attachments', 'Remove all attachments');
757 $obj = PMG
::RuleDB
::Accept-
>new();
758 my $accept = $ruledb->create_group_with_obj($obj, 'Accept', 'Accept mail for Delivery');
761 $obj = PMG
::RuleDB
::Block-
>new ();
762 my $block = $ruledb->create_group_with_obj($obj, 'Block', 'Block mail');
765 $obj = PMG
::RuleDB
::Quarantine-
>new();
766 my $quarantine = $ruledb->create_group_with_obj($obj, 'Quarantine', 'Move mail to quarantine');
769 $obj = PMG
::RuleDB
::Notify-
>new('__ADMIN__');
770 my $notify_admin = $ruledb->create_group_with_obj($obj, 'Notify Admin', 'Send notification');
773 $obj = PMG
::RuleDB
::Notify-
>new('__SENDER__');
774 my $notify_sender = $ruledb->create_group_with_obj($obj, 'Notify Sender', 'Send notification');
777 $obj = PMG
::RuleDB
::Disclaimer-
>new ();
778 my $add_discl = $ruledb->create_group_with_obj($obj, 'Disclaimer', 'Add Disclaimer');
780 # Move to attachment quarantine
781 $obj = PMG
::RuleDB
::Remove-
>new(0, undef, undef, 1);
782 my $attach_quar = $ruledb->create_group_with_obj(
783 $obj, 'Attachment Quarantine (remove matching)', 'Remove matching attachments and move the whole mail to the attachment quarantine.');
785 # Remove all attachments
786 $obj = PMG
::RuleDB
::Remove-
>new(1, undef, undef, 1);
787 my $attach_quar_all = $ruledb->create_group_with_obj(
788 $obj, 'Attachment Quarantine (remove all)', 'Remove all attachments and move the whole mail to the attachment quarantine.');
790 # Attach original mail
791 #$obj = Proxmox::RuleDB::Attach->new ();
792 #my $attach_orig = $ruledb->create_group_with_obj ($obj, 'Attach Original Mail',
793 # 'Attach Original Mail');
795 ####################### RULES ##################################
797 ## Block Dangerous Files
798 my $rule = PMG
::RuleDB
::Rule-
>new ('Block Dangerous Files', 93, 1, 0);
799 $ruledb->save_rule ($rule);
801 $ruledb->rule_add_what_group ($rule, $exe_content);
802 $ruledb->rule_add_action ($rule, $remove);
805 $rule = PMG
::RuleDB
::Rule-
>new ('Block Viruses', 96, 1, 0);
806 $ruledb->save_rule ($rule);
808 $ruledb->rule_add_what_group ($rule, $virus);
809 $ruledb->rule_add_action ($rule, $notify_admin);
812 $ruledb->rule_add_action ($rule, $block);
814 $ruledb->rule_add_action ($rule, $quarantine);
818 $rule = PMG
::RuleDB
::Rule-
>new ('Virus Alert', 96, 1, 1);
819 $ruledb->save_rule ($rule);
821 $ruledb->rule_add_what_group ($rule, $virus);
822 $ruledb->rule_add_action ($rule, $notify_sender);
823 $ruledb->rule_add_action ($rule, $notify_admin);
824 $ruledb->rule_add_action ($rule, $block);
827 $rule = PMG
::RuleDB
::Rule-
>new ('Blacklist', 98, 1, 0);
828 $ruledb->save_rule ($rule);
830 $ruledb->rule_add_from_group ($rule, $blacklist);
831 $ruledb->rule_add_action ($rule, $block);
835 $rule = PMG
::RuleDB
::Rule-
>new ('Modify Header', 90, 1, 0);
836 $ruledb->save_rule ($rule);
837 $ruledb->rule_add_action ($rule, $mod_spam_level);
841 $rule = PMG
::RuleDB
::Rule-
>new ('Whitelist', 85, 1, 0);
842 $ruledb->save_rule ($rule);
844 $ruledb->rule_add_from_group ($rule, $whitelist);
845 $ruledb->rule_add_action ($rule, $accept);
848 $rule = PMG
::RuleDB
::Rule-
>new ('Mark Spam', 80, 1, 0);
849 $ruledb->save_rule ($rule);
851 $ruledb->rule_add_what_group ($rule, $spam10);
852 $ruledb->rule_add_action ($rule, $mod_spam_level);
853 $ruledb->rule_add_action ($rule, $mod_spam_subject);
855 # Quarantine/Mark Spam (Level 3)
856 $rule = PMG
::RuleDB
::Rule-
>new ('Quarantine/Mark Spam (Level 3)', 80, 1, 0);
857 $ruledb->save_rule ($rule);
859 $ruledb->rule_add_what_group ($rule, $spam3);
860 $ruledb->rule_add_action ($rule, $mod_spam_subject);
861 $ruledb->rule_add_action ($rule, $quarantine);
862 #$ruledb->rule_add_action ($rule, $count_spam);
865 # Quarantine/Mark Spam (Level 5)
866 $rule = PMG
::RuleDB
::Rule-
>new ('Quarantine/Mark Spam (Level 5)', 81, 0, 0);
867 $ruledb->save_rule ($rule);
869 $ruledb->rule_add_what_group ($rule, $spam5);
870 $ruledb->rule_add_action ($rule, $mod_spam_subject);
871 $ruledb->rule_add_action ($rule, $quarantine);
873 ## Block Spam Level 10
874 $rule = PMG
::RuleDB
::Rule-
>new ('Block Spam (Level 10)', 82, 0, 0);
875 $ruledb->save_rule ($rule);
877 $ruledb->rule_add_what_group ($rule, $spam10);
878 $ruledb->rule_add_action ($rule, $block);
880 ## Block Outgoing Spam
881 $rule = PMG
::RuleDB
::Rule-
>new ('Block outgoing Spam', 70, 0, 1);
882 $ruledb->save_rule ($rule);
884 $ruledb->rule_add_what_group ($rule, $spam3);
885 $ruledb->rule_add_action ($rule, $notify_admin);
886 $ruledb->rule_add_action ($rule, $notify_sender);
887 $ruledb->rule_add_action ($rule, $block);
890 $rule = PMG
::RuleDB
::Rule-
>new ('Add Disclaimer', 60, 0, 1);
891 $ruledb->save_rule ($rule);
892 $ruledb->rule_add_action ($rule, $add_discl);
894 # Block Multimedia Files
895 $rule = PMG
::RuleDB
::Rule-
>new ('Block Multimedia Files', 87, 0, 2);
896 $ruledb->save_rule ($rule);
898 $ruledb->rule_add_what_group ($rule, $mm_content);
899 $ruledb->rule_add_action ($rule, $remove);
901 # Quarantine Office Files
902 $rule = PMG
::RuleDB
::Rule-
>new ('Quarantine Office Files', 89, 0, 0);
903 $ruledb->save_rule ($rule);
905 $ruledb->rule_add_what_group ($rule, $office_content);
906 $ruledb->rule_add_action ($rule, $attach_quar);
908 #$ruledb->rule_add_from_group ($rule, $anybody);
909 #$ruledb->rule_add_from_group ($rule, $trusted);
910 #$ruledb->rule_add_to_group ($rule, $anybody);
911 #$ruledb->rule_add_what_group ($rule, $ct_filter);
912 #$ruledb->rule_add_action ($rule, $add_discl);
913 #$ruledb->rule_add_action ($rule, $remove);
914 #$ruledb->rule_add_action ($rule, $bcc);
915 #$ruledb->rule_add_action ($rule, $storeq);
916 #$ruledb->rule_add_action ($rule, $accept);
918 cond_create_std_actions
($ruledb);
923 sub get_remote_time
{
926 my $sth = $rdb->prepare("SELECT EXTRACT (EPOCH FROM TIMESTAMP (0) WITH TIME ZONE 'now')::INTEGER as ctime;");
928 my $ctinfo = $sth->fetchrow_hashref();
931 return $ctinfo ?
$ctinfo->{ctime
} : 0;
935 my ($lcid, $database) = @_;
937 die "got unexpected cid for new master" if !$lcid;
942 $dbh = open_ruledb
($database);
946 print STDERR
"update quarantine database\n";
947 $dbh->do("UPDATE CMailStore SET CID = $lcid WHERE CID = 0;" .
948 "UPDATE CMSReceivers SET CMailStore_CID = $lcid WHERE CMailStore_CID = 0;");
950 print STDERR
"update statistic database\n";
951 $dbh->do("UPDATE CStatistic SET CID = $lcid WHERE CID = 0;" .
952 "UPDATE CReceivers SET CStatistic_CID = $lcid WHERE CStatistic_CID = 0;");
954 print STDERR
"update greylist database\n";
955 $dbh->do("UPDATE CGreylist SET CID = $lcid WHERE CID = 0;");
957 print STDERR
"update localstat database\n";
958 $dbh->do("UPDATE LocalStat SET CID = $lcid WHERE CID = 0;");
965 $dbh->rollback if $err;
972 sub purge_statistic_database
{
973 my ($dbh, $statlifetime) = @_;
975 return if $statlifetime <= 0;
977 my (undef, undef, undef, $mday, $mon, $year) = localtime(time());
978 my $end = timelocal
(0, 0, 0, $mday, $mon, $year);
979 my $start = $end - $statlifetime*86400;
981 # delete statistics older than $start
988 my $sth = $dbh->prepare("DELETE FROM CStatistic WHERE time < $start");
994 $sth = $dbh->prepare(
995 "DELETE FROM CReceivers WHERE NOT EXISTS " .
996 "(SELECT * FROM CStatistic WHERE CID = CStatistic_CID AND RID = CStatistic_RID)");
1010 sub purge_quarantine_database
{
1011 my ($dbh, $qtype, $lifetime) = @_;
1013 my $spooldir = $PMG::MailQueue
::spooldir
;
1015 my (undef, undef, undef, $mday, $mon, $year) = localtime(time());
1016 my $end = timelocal
(0, 0, 0, $mday, $mon, $year);
1017 my $start = $end - $lifetime*86400;
1019 my $sth = $dbh->prepare(
1020 "SELECT file FROM CMailStore WHERE time < $start AND QType = '$qtype'");
1026 while (my $ref = $sth->fetchrow_hashref()) {
1027 my $filename = "$spooldir/$ref->{file}";
1028 $count++ if unlink($filename);
1034 "DELETE FROM CMailStore WHERE time < $start AND QType = '$qtype';" .
1035 "DELETE FROM CMSReceivers WHERE NOT EXISTS " .
1036 "(SELECT * FROM CMailStore WHERE CID = CMailStore_CID AND RID = CMailStore_RID)");
1041 sub get_quarantine_count
{
1042 my ($dbh, $qtype) = @_;
1044 # Note;: We try to estimate used disk space - each mail
1045 # is stored in an extra file ...
1049 my $sth = $dbh->prepare(
1050 "SELECT count(ID) as count, sum (ceil((Bytes+$bs-1)/$bs)*$bs) / (1024*1024) as mbytes, " .
1051 "avg(Bytes) as avgbytes, avg(Spamlevel) as avgspam " .
1052 "FROM CMailStore WHERE QType = ?");
1054 $sth->execute($qtype);
1056 my $ref = $sth->fetchrow_hashref();
1060 foreach my $k (qw(count mbytes avgbytes avgspam)) {
1068 my ($ldb, $rdb, $table) = @_;
1070 $table = lc($table);
1072 my $sth = $ldb->column_info(undef, undef, $table, undef);
1073 my $attrs = $sth->fetchall_arrayref({});
1076 foreach my $ref (@$attrs) {
1077 push @col_arr, $ref->{COLUMN_NAME
};
1082 my $cols = join(', ', @col_arr);
1083 $cols || die "unable to fetch column definitions of table '$table' : ERROR";
1085 $rdb->do("COPY $table ($cols) TO STDOUT");
1090 $ldb->do("COPY $table ($cols) FROM stdin");
1092 while ($rdb->pg_getcopydata($data) >= 0) {
1093 $ldb->pg_putcopydata($data);
1096 $ldb->pg_putcopyend();
1099 $ldb->pg_putcopyend();
1104 sub copy_selected_data
{
1105 my ($dbh, $select_sth, $table, $attrs, $callback) = @_;
1109 my $insert_sth = $dbh->prepare(
1110 "INSERT INTO ${table}(" . join(',', @$attrs) . ') ' .
1111 'VALUES (' . join(',', ('?') x
scalar(@$attrs)) . ')');
1113 while (my $ref = $select_sth->fetchrow_hashref()) {
1114 $callback->($ref) if $callback;
1116 $insert_sth->execute(map { $ref->{$_} } @$attrs);
1122 sub update_master_clusterinfo
{
1123 my ($clientcid) = @_;
1125 my $dbh = open_ruledb
();
1127 $dbh->do("DELETE FROM ClusterInfo WHERE CID = $clientcid");
1129 my @mt = ('CMSReceivers', 'CGreylist', 'UserPrefs', 'DomainStat', 'DailyStat', 'LocalStat', 'VirusInfo');
1131 foreach my $table (@mt) {
1132 $dbh->do ("INSERT INTO ClusterInfo (cid, name, ivalue) select $clientcid, 'lastmt_$table', " .
1133 "EXTRACT(EPOCH FROM now())::INTEGER");
1136 my @lastid_tables = ('CStatistic', 'CMailStore');
1138 for my $table (@lastid_tables) {
1139 $dbh->do("INSERT INTO ClusterInfo (cid, name, ivalue) " .
1140 "SELECT $clientcid, 'lastid_$table', COALESCE (max (rid), -1) FROM $table " .
1141 "WHERE cid = $clientcid");
1145 sub update_client_clusterinfo
{
1146 my ($mastercid) = @_;
1148 my $dbh = open_ruledb
();
1150 $dbh->do("DELETE FROM StatInfo"); # not needed at node
1152 $dbh->do("DELETE FROM ClusterInfo WHERE CID = $mastercid");
1154 $dbh->do("INSERT INTO ClusterInfo (cid, name, ivalue) select $mastercid, 'lastid_CMailStore', " .
1155 "COALESCE (max (rid), -1) FROM CMailStore WHERE cid = $mastercid");
1157 $dbh->do("INSERT INTO ClusterInfo (cid, name, ivalue) select $mastercid, 'lastid_CStatistic', " .
1158 "COALESCE (max (rid), -1) FROM CStatistic WHERE cid = $mastercid");
1160 my @mt = ('CMSReceivers', 'CGreylist', 'UserPrefs', 'DomainStat', 'DailyStat', 'LocalStat', 'VirusInfo');
1162 foreach my $table (@mt) {
1163 $dbh->do ("INSERT INTO ClusterInfo (cid, name, ivalue) select $mastercid, 'lastmt_$table', " .
1164 "COALESCE (max (mtime), 0) FROM $table");
1168 sub create_clusterinfo_default
{
1169 my ($dbh, $rcid, $name, $ivalue, $svalue) = @_;
1171 my $sth = $dbh->prepare("SELECT * FROM ClusterInfo WHERE CID = ? AND Name = ?");
1172 $sth->execute($rcid, $name);
1173 if (!$sth->fetchrow_hashref()) {
1174 $dbh->do("INSERT INTO ClusterInfo (CID, Name, IValue, SValue) VALUES (?, ?, ?, ?)", undef,
1175 $rcid, $name, $ivalue, $svalue);
1180 sub read_int_clusterinfo
{
1181 my ($dbh, $rcid, $name) = @_;
1183 my $sth = $dbh->prepare(
1184 "SELECT ivalue as value FROM ClusterInfo WHERE cid = ? AND NAME = ?");
1185 $sth->execute($rcid, $name);
1186 my $cinfo = $sth->fetchrow_hashref();
1189 return $cinfo->{value
};
1192 sub write_maxint_clusterinfo
{
1193 my ($dbh, $rcid, $name, $value) = @_;
1196 "UPDATE ClusterInfo SET ivalue = GREATEST(ivalue, ?) WHERE cid = ? AND name = ?", undef,
1197 $value, $rcid, $name
1204 my $ni = $cinfo->{master
};
1206 die "no master defined - unable to sync data from master\n" if !$ni;
1208 my $master_ip = $ni->{ip
};
1209 my $master_cid = $ni->{cid
};
1210 my $master_name = $ni->{name
};
1212 my $fn = "/tmp/masterdb$$.tar";
1215 my $dbname = $default_db_name;
1218 print STDERR
"copying master database from '${master_ip}'\n";
1220 open (my $fh, ">", $fn) || die "open '$fn' failed - $!\n";
1223 '/usr/bin/ssh', '-o', 'BatchMode=yes', '-o', "HostKeyAlias=${master_name}", $master_ip,
1224 '--', 'pg_dump', $dbname, '-F', 'c'
1226 PVE
::Tools
::run_command
($cmd, output
=> '>&' . fileno($fh));
1232 print STDERR
"copying master database finished (got $size bytes)\n";
1234 print STDERR
"delete local database\n";
1235 postgres_admin_cmd
('dropdb', undef, $dbname , '--if-exists');
1237 print STDERR
"create new local database\n";
1238 $createdb->($dbname);
1240 print STDERR
"insert received data into local database\n";
1246 if ($line =~ m/restoring data for table \"(.+)\"/) {
1247 print STDERR
"restoring table $1\n";
1248 } elsif (!$mess && ($line =~ m/creating (INDEX|CONSTRAINT)/)) {
1249 $mess = "creating indexes";
1250 print STDERR
"$mess\n";
1257 errmsg
=> "pg_restore failed"
1259 postgres_admin_cmd
('pg_restore', $opts, '-d', $dbname, '-v', $fn);
1261 print STDERR
"run analyze to speed up database queries\n";
1262 postgres_admin_cmd
('psql', { input
=> 'analyze;' }, $dbname);
1264 update_client_clusterinfo
($master_cid);
1271 sub cluster_sync_status
{
1276 foreach my $ni (values %{$cinfo->{ids
}}) {
1277 next if $cinfo->{local}->{cid
} == $ni->{cid
}; # skip local CID
1278 $minmtime->{$ni->{cid
}} = 0;
1283 $dbh = open_ruledb
();
1285 my $sth = $dbh->prepare(
1286 "SELECT cid, MIN (ivalue) as minmtime FROM ClusterInfo WHERE name = 'lastsync' AND"
1287 ." ivalue > 0 GROUP BY cid"
1292 while (my $info = $sth->fetchrow_hashref()) {
1293 foreach my $ni (values %{$cinfo->{ids
}}) {
1294 next if $cinfo->{local}->{cid
} == $ni->{cid
}; # skip local CID
1295 if ($ni->{cid
} == $info->{cid
}) { # node exists
1296 $minmtime->{$ni->{cid
}} = $info->{minmtime
};
1305 $dbh->disconnect() if $dbh;
1307 syslog
('err', $err) if $err;
1312 sub load_mail_data
{
1313 my ($dbh, $cid, $rid, $ticketid) = @_;
1315 my $sth = $dbh->prepare(
1316 "SELECT * FROM CMailStore, CMSReceivers WHERE CID = ? AND RID = ? AND TicketID = ? AND"
1317 ." CID = CMailStore_CID AND RID = CMailStore_RID"
1319 $sth->execute($cid, $rid, $ticketid);
1321 my $res = $sth->fetchrow_hashref();
1325 die "no such mail (C${cid}R${rid}T${ticketid})\n" if !defined($res);
1333 # Note: we pass $ruledb when modifying SMTP whitelist
1334 if (defined($ruledb)) {
1336 my $rulecache = PMG
::RuleCache-
>new($ruledb);
1337 PMG
::Config
::rewrite_postfix_whitelist
($rulecache);
1339 warn "problems updating SMTP whitelist - $@" if $@;
1342 PMG
::Utils
::reload_smtp_filter
();