1 package PMG
::CLI
::pmgqm
;
12 use POSIX
qw(strftime);
21 use PVE
::JSONSchema
qw(get_standard_option);
23 use PMG
::RESTEnvironment
;
29 use PMG
::ClusterConfig
;
30 use PMG
::API2
::Quarantine
;
32 use base
qw(PVE::CLIHandler);
34 sub setup_environment
{
35 PMG
::RESTEnvironment-
>setup_default_cli_env();
39 my ($data, $ref) = @_;
41 my @lines = split ('\n', $ref->{header
});
42 my $head = new Mail
::Header
(\
@lines);
46 $item->{id
} = sprintf("C%dR%dT%d", $ref->{cid
}, $ref->{rid
}, $ref->{ticketid
});
48 $item->{subject
} = PMG
::Utils
::rfc1522_to_html
(
49 PVE
::Tools
::trim
($head->get('subject')) || 'No Subject');
51 my $from = PMG
::Utils
::rfc1522_to_html
(PVE
::Tools
::trim
($head->get('from') // $ref->{sender
}));
52 my $sender = PMG
::Utils
::rfc1522_to_html
(PVE
::Tools
::trim
($head->get('sender')));
55 $item->{sender
} = $sender;
56 $item->{from
} = sprintf ("%s on behalf of %s", $sender, $from);
58 $item->{from
} = $from;
61 $item->{envelope_sender
} = $ref->{sender
};
62 $item->{pmail
} = encode_entities
(PMG
::Utils
::try_decode_utf8
($ref->{pmail
}));
63 $item->{receiver
} = $ref->{receiver
} || $ref->{pmail
};
65 $item->{date
} = strftime
("%F", localtime($ref->{time}));
66 $item->{time} = strftime
("%H:%M:%S", localtime($ref->{time}));
68 $item->{bytes
} = $ref->{bytes
};
69 $item->{spamlevel
} = $ref->{spamlevel
};
70 $item->{spaminfo
} = $ref->{info
};
71 $item->{file
} = $ref->{file
};
73 my $basehref = "$data->{protocol_fqdn_port}/quarantine";
74 if ($data->{authmode
} ne 'ldap') {
75 my $ticket = uri_escape
($data->{ticket
});
76 $item->{href
} = "$basehref?ticket=$ticket&cselect=$item->{id}&date=$item->{date}";
78 $item->{href
} = "$basehref?cselect=$item->{id}&date=$item->{date}";
84 __PACKAGE__-
>register_method ({
88 description
=> "Print quarantine status (mails per user) for specified time span.",
90 additionalProperties
=> 0,
93 description
=> "Select time span.",
95 enum
=> ['today', 'yesterday', 'week'],
101 returns
=> { type
=> 'null'},
105 my $cinfo = PMG
::ClusterConfig-
>new();
106 my $role = $cinfo->{local}->{type
} // '-';
108 if (!(($role eq '-') || ($role eq 'master'))) {
109 warn "local node is not master\n";
113 my $cfg = PMG
::Config-
>new();
115 my $timespan = $param->{timespan
} // 'today';
117 my ($start, $end) = PMG
::Utils
::lookup_timespan
($timespan);
119 my $hostname = PVE
::INotify
::nodename
();
121 my $fqdn = $cfg->get('spamquar', 'hostname') //
122 PVE
::Tools
::get_fqdn
($hostname);
125 my $dbh = PMG
::DBTools
::open_ruledb
();
127 my $domains = PVE
::INotify
::read_file
('domains');
128 my $domainregex = PMG
::Utils
::domain_regex
([keys %$domains]);
130 my $sth = $dbh->prepare(
131 "SELECT pmail, AVG(spamlevel) as spamlevel, count(*) FROM CMailStore, CMSReceivers " .
132 "WHERE time >= $start AND time < $end AND " .
133 "QType = 'S' AND CID = CMailStore_CID AND RID = CMailStore_RID " .
134 "AND Status = 'N' " .
140 print "Count Spamlevel Mail\n";
142 while (my $ref = $sth->fetchrow_hashref()) {
144 my $extern = ($domainregex && $ref->{pmail
} !~ $domainregex);
145 my $hint = $extern ?
" (external address)" : "";
146 printf ("%-5d %10.2f %s$hint\n", $ref->{count
}, $ref->{spamlevel
}, $ref->{pmail
});
154 __PACKAGE__-
>register_method ({
158 description
=> "Generate and send spam report emails.",
160 additionalProperties
=> 0,
162 receiver
=> get_standard_option
('pmg-email-address', {
163 description
=> "Generate report for a single email address. If not specified, generate reports for all users.",
167 description
=> "Select time span.",
169 enum
=> ['today', 'yesterday', 'week'],
174 description
=> "Spam report style. Default value is read from spam quarantine configuration.",
176 enum
=> ['short', 'verbose', 'custom'],
179 redirect
=> get_standard_option
('pmg-email-address', {
180 description
=> "Redirect spam report email to this address.",
184 description
=> "Debug mode. Print raw email to stdout instead of sending them.",
191 returns
=> { type
=> 'null'},
195 my $cinfo = PMG
::ClusterConfig-
>new();
196 my $role = $cinfo->{local}->{type
} // '-';
198 if (!(($role eq '-') || ($role eq 'master'))) {
199 warn "local node is not master - not sending spam report\n";
203 my $cfg = PMG
::Config-
>new();
205 my $reportstyle = $param->{style
} // $cfg->get('spamquar', 'reportstyle');
207 # overwrite report style none when:
208 # - explicit receiver specified
209 # - when debug flag enabled
210 if ($reportstyle eq 'none') {
211 $reportstyle = 'verbose' if $param->{debug
} || defined($param->{receiver
});
214 return if $reportstyle eq 'none'; # do nothing
216 my $timespan = $param->{timespan
} // 'today';
218 my ($start, $end) = PMG
::Utils
::lookup_timespan
($timespan);
220 my $hostname = PVE
::INotify
::nodename
();
222 my $fqdn = $cfg->get('spamquar', 'hostname') //
223 PVE
::Tools
::get_fqdn
($hostname);
225 my $port = $cfg->get('spamquar', 'port') // 8006;
227 my $protocol = $cfg->get('spamquar', 'protocol') // 'https';
229 my $protocol_fqdn_port = "$protocol://$fqdn";
230 if (($protocol eq 'https' && $port != 443) ||
231 ($protocol eq 'http' && $port != 80)) {
232 $protocol_fqdn_port .= ":$port";
235 my $authmode = $cfg->get ('spamquar', 'authmode') // 'ticket';
238 protocol
=> $protocol,
241 hostname
=> $hostname,
242 date
=> strftime
("%F", localtime($end - 1)),
243 timespan
=> $timespan,
245 protocol_fqdn_port
=> $protocol_fqdn_port,
246 authmode
=> $authmode,
249 my $mailfrom = $cfg->get ('spamquar', 'mailfrom') //
250 "Proxmox Mail Gateway <postmaster>";
252 my $dbh = PMG
::DBTools
::open_ruledb
();
254 my $target = $param->{receiver
};
255 my $redirect = $param->{redirect
};
257 if (defined($redirect) && !defined($target)) {
258 die "can't redirect mails for all users\n";
261 my $domains = PVE
::INotify
::read_file
('domains');
262 my $domainregex = PMG
::Utils
::domain_regex
([keys %$domains]);
264 my $template = "spamreport-${reportstyle}.tt";
266 foreach my $path (@$PMG::Config
::tt_include_path
) {
267 if (-f
"$path/$template") { $found = 1; last; }
270 warn "unable to find template '$template' - using default\n";
271 $template = "spamreport-verbose.tt";
274 my $sth = $dbh->prepare(
275 "SELECT * FROM CMailStore, CMSReceivers " .
276 "WHERE time >= $start AND time < $end AND " .
277 ($target ?
"pmail = ? AND " : '') .
278 "QType = 'S' AND CID = CMailStore_CID AND RID = CMailStore_RID " .
279 "AND Status = 'N' " .
280 "ORDER BY pmail, time, receiver");
283 $sth->execute(encode
('UTF-8', $target));
292 my $tt = PMG
::Config
::get_template_toolkit
();
296 my $extern = ($domainregex && $creceiver !~ $domainregex);
298 $data->{mailcount
} = $mailcount;
299 my $sendto = $redirect ?
$redirect : $creceiver;
300 PMG
::Utils
::finalize_report
($tt, $template, $data, $mailfrom, $sendto, $param->{debug
});
304 while (my $ref = $sth->fetchrow_hashref()) {
305 my $decoded_pmail = PMG
::Utils
::try_decode_utf8
($ref->{pmail
});
306 if ($creceiver ne $decoded_pmail) {
308 $finalize->() if $data;
310 $data = clone
($global_data);
312 $creceiver = $decoded_pmail;
315 $data->{pmail
} = encode_entities
($decoded_pmail);
316 $data->{pmail_raw
} = $ref->{pmail
};
317 $data->{managehref
} = "$protocol_fqdn_port/quarantine";
318 if ($data->{authmode
} ne 'ldap') {
319 $data->{ticket
} = PMG
::Ticket
::assemble_quarantine_ticket
($data->{pmail_raw
});
320 my $esc_ticket = uri_escape
($data->{ticket
});
321 $data->{managehref
} .= "?ticket=${esc_ticket}";
326 push @{$data->{items
}}, get_item_data
($data, $ref);
333 $finalize->() if $data;
335 if (defined($target) && !$mailcount) {
336 print STDERR
"no mails for '$target'\n";
342 sub find_stale_files
{
343 my ($path, $lifetime, $purge) = @_;
345 return if ! -d
$path;
347 my (undef, undef, undef, $mday, $mon, $year) = localtime(time());
348 my $daystart = timelocal
(0, 0, 0, $mday, $mon, $year);
349 my $expire = $daystart - $lifetime*86400;
352 my $name = $File::Find
::name
;
353 return if $name !~ m
|^($path/.*)$|;
354 $name = $1; # untaint
355 my $stat = stat($name);
357 return if $stat->mtime >= $expire;
360 print "removed: $name\n";
367 find
({ wanted
=> $wanted, no_chdir
=> 1 }, $path);
370 sub test_quarantine_files
{
371 my ($spamlifetime, $viruslifetime, $purge) = @_;
373 print STDERR
"searching for stale files\n" if !$purge;
375 my $spooldir = $PMG::MailQueue
::spooldir
;
377 find_stale_files
("$spooldir/spam", $spamlifetime, $purge);
378 foreach my $dir (<"/var/spool/pmg/cluster/*/spam">) {
379 next if $dir !~ m
|^(/var/spool/pmg/cluster
/\d+/spam)$|;
381 find_stale_files
($dir, $spamlifetime, $purge);
384 find_stale_files
("$spooldir/virus", $viruslifetime, $purge);
385 foreach my $dir (<"/var/spool/pmg/cluster/*/virus">) {
386 next if $dir !~ m
|^(/var/spool/pmg/cluster
/\d+/virus
)$|;
388 find_stale_files
($dir, $viruslifetime, $purge);
392 __PACKAGE__-
>register_method ({
396 description
=> "Cleanup Quarantine database. Remove entries older than configured quarantine lifetime.",
398 additionalProperties
=> 0,
401 description
=> "Only search for quarantine files older than configured quarantine lifetime. Just print found files, but do not remove them.",
408 returns
=> { type
=> 'null'},
412 my $cfg = PMG
::Config-
>new();
414 my $spamlifetime = $cfg->get('spamquar', 'lifetime');
415 my $viruslifetime = $cfg->get ('virusquar', 'lifetime');
417 my $purge = !$param->{check
};
420 print STDERR
"purging database\n";
422 my $dbh = PMG
::DBTools
::open_ruledb
();
424 if (my $count = PMG
::DBTools
::purge_quarantine_database
($dbh, 'S', $spamlifetime)) {
425 print STDERR
"removed $count spam quarantine files\n";
428 if (my $count = PMG
::DBTools
::purge_quarantine_database
($dbh, 'V', $viruslifetime)) {
429 print STDERR
"removed $count virus quarantine files\n";
432 if (my $count = PMG
::DBTools
::purge_quarantine_database
($dbh, 'A', $spamlifetime)) {
433 print STDERR
"removed $count attachment quarantine files\n";
437 test_quarantine_files
($spamlifetime, $viruslifetime, $purge);
444 'purge' => [ __PACKAGE__
, 'purge', []],
445 'send' => [ __PACKAGE__
, 'send', []],
446 'status' => [ __PACKAGE__
, 'status', []],