]> git.proxmox.com Git - pmg-api.git/blame - bin/pmg-smtp-filter
new helper analyze_virus()
[pmg-api.git] / bin / pmg-smtp-filter
CommitLineData
6f4aeba1
DM
1#!/usr/bin/perl -w
2
3use strict;
4use Carp;
5use Getopt::Long;
6use Time::HiRes qw (gettimeofday tv_interval);
7use Proxmox::SafeSyslog;
8use POSIX qw(:sys_wait_h errno_h signal_h);
9
10use MIME::Parser;
11use File::Path;
12use Net::Server::PreFork;
13use Net::Server::SIG qw(register_sig check_sigs);
14use Fcntl ':flock';
15use File::Basename;
16
17use Proxmox::proxcfg;
18use Proxmox::Utils;
19use Proxmox::RuleDB;
20use Proxmox::ModGroup;
21use Proxmox::Config::System;
22use Proxmox::SMTP;
23use Proxmox::MailQueue;
24use Proxmox::AtomicFile;
25use Proxmox::LDAPCache;
26use Xdgmime;
27use Proxmox::Unpack;
28
29use Mail::SpamAssassin;
30use Mail::SpamAssassin::NetSet;
31use Proxmox::Commtouch;
32
33use vars qw(@ISA);
34@ISA = qw(Net::Server::PreFork);
35
36
37
38my $opt_commandline = [$0, @ARGV];
39my $opt_max_dequeue = 1;
40my $opt_dequeue_time = 60*2;
41
42my $opt_ext_port = 10024;
43my $opt_int_port = 10023;
44my $opt_inject_port = 10025;
45
46my $opt_testmode;
47my $opt_untrusted;
48my $opt_pidfile;
49my $opt_database;
50
51initlog ('proxprox', 'mail');
52
53if (!GetOptions ('testmode=s' => \$opt_testmode,
54 'pidfile=s' => \$opt_pidfile,
55 'untrusted' => \$opt_untrusted,
56 'database=s' => \$opt_database)) {
57 die "usage error\n";
58 exit (-1);
59}
60
61$opt_pidfile = "/var/run/proxprox.pid" if !$opt_pidfile;
62
63my $max_servers = 1;
64my $min_servers = 1;
65my $min_spare_servers = 0;
66my $max_spare_servers = 0;
67my $max_requests = 1;
68
69if (!$opt_testmode) {
70
71 my $system_cfg = Proxmox::Config::System->new ();
72 my $demo = $system_cfg->get ('administration', 'demo');
73
74 if ($demo) {
75 syslog ('info', 'demo mode detected - not starting server');
76 exit (0);
77 }
78
79 $max_servers = $system_cfg->get ('mail', 'max_filters') + 2;
80 $min_servers = 2;
81 $min_spare_servers = 1;
82 $max_spare_servers = 4;
83 $max_requests = 20;
84}
85
86$opt_max_dequeue = 0 if $opt_testmode;
87
88my $daemonize = 1;
89if (defined ($ENV{BOUND_SOCKETS})) {
90 $daemonize = undef;
91}
92
93my $server_attr = {
94 port => [ $opt_int_port, $opt_ext_port ],
95 host => '127.0.0.1',
96 min_servers => $min_servers,
97 max_servers => $max_servers,
98 min_spare_servers => $min_spare_servers,
99 max_spare_servers => $max_spare_servers,
100 max_requests => $max_requests,
101 serialize => 'flock',
102 max_dequeue => $opt_max_dequeue,
103 check_for_dequeue => $opt_dequeue_time,
104 log_level => 3,
105 pid_file => $opt_pidfile,
106 no_close_by_child => 1,
107 no_client_stdout => 1,
108 commandline => $opt_commandline,
109};
110
111$server_attr->{setsid} = $daemonize if !$opt_testmode;
112
113my $database;
114
115if (defined($opt_database)) {
116 $database = $opt_database;
117} else {
118 $database = $opt_testmode ? "Proxmox_testdb" : "Proxmox_ruledb";
119}
120
121$SIG{'__WARN__'} = sub {
122 my $err = $@;
123 my $t = $_[0];
124 chomp $t;
125 syslog('warning', "WARNING: %s", $t);
126 $@ = $err;
127};
128
129sub get_prox_vars {
130 my ($self, $queue, $entity, $msginfo, $rule, $targets, $spaminfo) = @_;
131
132 $spaminfo = {
133 sa_score => $queue->{sa_score},
134 sa_hits => $queue->{sa_hits},
135 sa_data => $queue->{sa_data},
136 sa_max => $queue->{sa_max}
137 } if !$spaminfo;
138
139 my $vars = {
140 'SUBJECT' => $entity->head->get ('subject', 0) || 'No Subject',
141 'RULE' => $rule->{name},
142 'RULE_INFO' => $msginfo->{rule_info},
143 'SENDER' => $msginfo->{sender},
144 'SENDER_IP' => $msginfo->{xforward}->{addr},
145 'TARGETS' => join (', ', @$targets),
146 'RECEIVERS' => join (', ', @{$msginfo->{targets}}),
147 'SPAMLEVEL' => $spaminfo->{sa_score},
148 'SPAMSTARS' => '*' x (($spaminfo->{sa_score} || 0) > 100 ? 100 : $spaminfo->{sa_score} || 0),
149 'ADMIN' => $self->{system_cfg}->get ('administration', 'email'),
150 'HOST' => $msginfo->{hostname},
151 'DOMAIN' => $msginfo->{domain},
152 'FQDN' => $msginfo->{fqdn},
153 'MSGID' => $queue->{msgid},
154 'VERSION' => Proxmox::proxcfg::package() . "/" .Proxmox::proxcfg::version() . "/" . Proxmox::proxcfg::repoid(),
155 };
156
157 $vars->{__spaminfo} = $spaminfo;
158
159 if ($opt_testmode) {
160 if ($queue->{vinfo_avira} || $queue->{vinfo_clam} || $queue->{vinfo_commtouch}) {
161 $vars->{'VIRUS_INFO'} = "Virus Info:";
162 $vars->{'VIRUS_INFO'} .= " clam: $queue->{vinfo_clam}" if $queue->{vinfo_clam};
163 $vars->{'VIRUS_INFO'} .= " avira: $queue->{vinfo_avira}" if $queue->{vinfo_avira};
164 $vars->{'VIRUS_INFO'} .= " commtouch: $queue->{vinfo_commtouch}" if $queue->{vinfo_commtouch};
165 } else {
166 $vars->{'VIRUS_INFO'} = '';
167 }
168 } else {
169 if ($queue->{vinfo}) {
170 $vars->{'VIRUS_INFO'} = "Virus Info: $queue->{vinfo}\n";
171 } else {
172 $vars->{'VIRUS_INFO'} = '';
173 }
174 }
175
176 $vars->{'SPAM_HITS'} = $spaminfo->{sa_hits};
177
178 $vars->{'SPAM_INFO'} = '';
179 my $sscores = $spaminfo->{sa_data};
180
181 if (defined ($sscores) && @$sscores != -1) {
182 my $sa_text;
183 if ($opt_testmode) {
184 $sa_text = "Spam detection results: 100\n";
185 } else {
186 $sa_text = "Spam detection results: $spaminfo->{sa_score}\n";
187 }
188
189 foreach my $s (@$sscores) {
190 if ($opt_testmode) {
191 $sa_text .= sprintf ("%-22s %6s %s\n", $s->{rule},
192 1, $s->{desc} || '-');
193 } else {
194 $sa_text .= sprintf ("%-22s %6s %s\n", $s->{rule},
195 $s->{score}, $s->{desc} || '-');
196 }
197 }
198 $vars->{'SPAM_INFO'} = $sa_text;
199 }
200
201 if ($opt_testmode) {
202 delete ($vars->{'ADMIN'});
203 #delete ($vars->{'SPAM_INFO'});
204 }
205
206 return $vars;
207}
208
209sub apply_rules {
210 my ($self, $queue, $msginfo, $entity, $ldap) = @_;
211
212 my $final;
213 my %rule_targets;
214 my %rule_actions;
215 my %rule_marks;
216 my $matching_rules = [];
217
218 my $rulecache = $self->{rulecache};
219 my $rules = $rulecache->rules ();
220 my $dbh = $self->{ruledb}->{dbh};
221
222 # first, we remove all conditional written 'X-' header attributes
223 foreach my $rule (@$rules) {
224 next if !$rule->{active};
225 next if ($rule->{direction} == 0) && $msginfo->{trusted};
226 next if ($rule->{direction} == 1) && !$msginfo->{trusted};
227
228 my $actions = $rulecache->get_actions ($rule->{id});
229 if ($actions) {
230 foreach my $action (@$actions) {
231 if ($action->isa ("Proxmox::RuleDB::ModField")) {
232 my $fname = $action->{field};
233 next if $fname !~ m/^X-/i;
234 $entity->head->delete($fname);
235 }
236 }
237 }
238 }
239
240 foreach my $rule (@$rules) {
241 next if !$rule->{active};
242 next if ($rule->{direction} == 0) && $msginfo->{trusted};
243 next if ($rule->{direction} == 1) && !$msginfo->{trusted};
244
245 # match from, when and what classes (not target dependent)
246 if (!($rulecache->from_match ($rule->{id}, $msginfo->{sender}, $msginfo->{xforward}->{addr}, $ldap) &&
247 $rulecache->when_match ($rule->{id}, time))) {
248 next;
249 }
250
251 $rule_marks{$rule->{id}} =
252 $rulecache->what_match ($rule->{id}, $queue, $entity, $msginfo, $dbh);
253
254 $rule_actions{$rule->{id}} = $rulecache->get_actions ($rule->{id});
255 my $fin = $rulecache->final ($rule->{id});
256
257 # match targets
258 foreach my $target (@{$msginfo->{targets}}) {
259 next if $final->{$target};
260 next if !defined ($rule_marks{$rule->{id}});
261 next if !defined ($rule_marks{$rule->{id}}->{$target});
262 next if !defined ($rule_marks{$rule->{id}}->{$target}->{marks});
263 next if !$rulecache->to_match ($rule->{id}, $target, $ldap);
264
265 $final->{$target} = $fin;
266
267 push @{$rule_targets{$rule->{id}}}, $target;
268 }
269 }
270
271 # Compute rule_info (summary about matching rule)
272 # this can be used for debugging
273 my $rule_info = "";
274 foreach my $rule (@$rules) {
275 next if !$rule_targets{$rule->{id}};
276
277 push @$matching_rules, $rule->{id};
278
279 $rule_info .= "Rule: $rule->{name}\n";
280
281 foreach my $target (@{$rule_targets{$rule->{id}}}) {
282 $rule_info .= " Receiver: $target\n";
283 }
284 foreach my $action (@{$rule_actions{$rule->{id}}}) {
285 $rule_info .= " Action: " . $action->short_desc () . "\n";
286 }
287
288 }
289 $msginfo->{rule_info} = $rule_info;
290
291 if ($msginfo->{testmode}) {
292 my $vars = $self->get_prox_vars ($queue, $entity, $msginfo, undef, [], undef);
293 my $out = "__RULE_INFO__";
294 $out = Proxmox::Utils::subst_values ($out, $vars);
295 my $fh = $msginfo->{test_fh};
296 print $fh $out;
297 }
298
299 # apply actions
300
301 my $mod_group = Proxmox::ModGroup->new ($entity, $msginfo->{targets});
302
303 foreach my $rule (@$rules) {
304 my $targets = $rule_targets{$rule->{id}};
305 next if !$targets;
306
307 my $spaminfo;
308 foreach my $t (@$targets) {
309 if ($rule_marks{$rule->{id}}->{$t} && $rule_marks{$rule->{id}}->{$t}->{spaminfo}) {
310 $spaminfo = $rule_marks{$rule->{id}}->{$t}->{spaminfo};
311 # we assume spam info is the same for all matching targets
312 last;
313 }
314 }
315
316 my $vars = $self->get_prox_vars ($queue, $entity, $msginfo, $rule,
317 $rule_targets{$rule->{id}}, $spaminfo);
318
319
320 my @sorted_actions =
321 sort {$a->priority <=> $b->priority} @{$rule_actions{$rule->{id}}};
322
323 foreach my $action (@sorted_actions) {
324 $action->execute ($queue, $self->{ruledb}, $mod_group,
325 $rule_targets{$rule->{id}},
326 $msginfo, $vars, $rule_marks{$rule->{id}}->{marks}, $ldap);
327 last if $action->final;
328 }
329 }
330
331 # we deliver all mail not matched by any rule
332 # (default action = accept)
333 my $unmatched;
334 foreach my $target (@{$msginfo->{targets}}) {
335 next if $final->{$target};
336
337 push @$unmatched, $target;
338 }
339
340 if ($unmatched) {
341 my $accept = Proxmox::RuleDB::Accept->new ();
342 $accept->execute ($queue, $self->{ruledb}, $mod_group, $unmatched,
343 $msginfo, undef, undef, undef);
344 }
345
346 return $matching_rules;
347}
348
349# reload ruledb and system config
350sub load_config {
351 my $self = shift;
352 my $prop = $self->{server};
353
354 if ($self->{ruledb}) {
355 $self->log (0, "reloading configuration $database");
356 $self->{ruledb}->close ();
357 }
358
359 $self->{system_cfg} = Proxmox::Config::System->new ();
360 $self->{cinfo} = $self->{system_cfg}->clusterinfo ();
361
362 eval {
363 my $dbh = Proxmox::Utils::open_ruledb ($database);
364 $self->{ruledb} = Proxmox::RuleDB->new ($dbh);
365
366 # load rulecache
367 $self->{rulecache} = Proxmox::RuleDB::RuleCache->new ($self->{ruledb});
368 };
369
370 my $err = $@;
371
372 if ($err) {
373 sleep (10); # reduce restart rate when postgres is down
374 die $err;
375 }
376
377 # create LDAP object
378 $self->{ldap} = Proxmox::LDAPSet->new_from_system_cfg ($self->{system_cfg}, 1);
379
380 $self->{reload_config} = 0;
381}
382
383sub log {
384 my ($self, $level, $msg, @therest) = @_;
385 my $prop = $self->{server};
386
387 return if $level =~ /^\d+$/ && $level > $prop->{log_level};
388
389 $level = $Proxmox::SafeSyslog::syslog_map->{$level} || $level;
390 if (@therest) {
391 syslog($level, $msg, @therest);
392 } else {
393 syslog ($level, $msg);
394 }
395}
396
397sub pre_loop_hook {
398 my $self = shift;
399 my $prop = $self->{server};
400
401 $prop->{log_level} = 3;
402
403 $self->log (0, "Filter daemon (re)started (max. $max_servers processes)");
404
405 eval { Proxmox::MailQueue::cleanup_active(); };
406 $self->log (0, "Cleanup failures: $@") if $@;
407
408 my $sig_set = POSIX::SigSet->new;
409 $sig_set->addset (&POSIX::SIGHUP);
410 $sig_set->addset (&POSIX::SIGCHLD);
411 my $old_sig_set = POSIX::SigSet->new();
412
413 sigprocmask (SIG_UNBLOCK, $sig_set, $old_sig_set);
414
415 my ($backup_umask) = umask;
416
417 my $system_cfg = Proxmox::Config::System->new ();
418
419 # Note: you ned to restart the daemon when you change 'rbl_checks'
420 $system_cfg->get ('spam', 'rbl_checks');
421
422 $self->{sa} = Mail::SpamAssassin->new ({
423 debug => 0,
424 local_tests_only => $opt_testmode ||
425 !$system_cfg->get('spam', 'rbl_checks'),
426 home_dir_for_helpers => '/root',
427 userstate_dir => '/root/.spamassassin',
428 dont_copy_prefs => 1,
429 stop_at_threshold => 0,
430 });
431
432 $self->{sa}->compile_now;
433
434 alarm (0); # SA forgets to clear alarm in some cases
435 umask ($backup_umask);
436 initlog ('proxprox', 'mail');
437
438 $SIG{'USR1'} = sub {
439 # reloading server configuration
440 if (defined $prop->{children}) {
441 foreach my $pid (keys %{$prop->{children}}) {
442 kill (10, $pid); # SIGUSR1 childs
443 }
444 }
445 }
446}
447
448sub child_init_hook {
449 my $self = shift;
450
451 $0 = 'proxprox child';
452
453 # $self->log (3, "init child");
454
455 eval {
456 $self->load_config ();
457 };
458
459 if ($@) {
460 $self->log (0, $@);
461 $self->child_finish_hook;
462 exit;
463 }
464
465 $SIG{'USR1'} = sub {
466 $self->{reload_config} = 1;
467 }
468}
469
470sub child_finish_hook {
471 my $self = shift;
472
473 # $self->log (3, "finish child");
474 $self->{ruledb}->close () if $self->{ruledb};
475}
476
477sub run_dequeue {
478 my $self = shift;
479
480 # do database maintainance here
481
482 $self->log (2, "starting database maintainance");
483
484 my ($csec, $usec) = gettimeofday ();
485
486 my $system_cfg = Proxmox::Config::System->new ();
487 my $cinfo = $system_cfg->clusterinfo();
488
489 my $dbh;
490
491 eval {
492 $dbh = Proxmox::Utils::open_ruledb ($database);
493 };
494
495 my $err = $@;
496
497 if ($err) {
498 $self->log (0, msgquote ("ERROR: $err"));
499 return;
500 }
501
502 eval {
503 Proxmox::Statistic::update_stats ($dbh, $cinfo);
504 };
505
506 $err = $@;
507
508 my ($csec_end, $usec_end) = gettimeofday ();
509 my $ptime = int (($csec_end-$csec)*1000 + ($usec_end - $usec)/1000);
510
511 if ($err) {
512 $self->log (0, msgquote ($err));
513 } else {
514 $self->log (2, "end database maintainance ($ptime ms)");
515 }
516
517 $dbh->disconnect() if $dbh;
518}
519
520
521sub unpack_entity {
522 my ($self, $unpack, $entity, $msginfo, $queue) = @_;
523
524 my $magic;
525 my $path;
526
527 if (($magic = $entity->{PMX_magic_ct}) &&
528 ($path = $entity->{PMX_decoded_path})) {
529
530 my $filename = basename ($path);
531
532 if (Proxmox::Unpack::is_archive ($magic)) {
533 $self->log (3, "$queue->{logid}: found archive '$filename' ($magic)");
534
535 my $start = [gettimeofday];
536
537 $unpack->{mime} = {};
538
539 eval {
540 $unpack->unpack_archive ($path, $magic);
541 };
542
543 $self->log (3, "$queue->{logid}: unpack failed - $@") if $@;
544
545 $entity->{PMX_content_types} = $unpack->{mime};
546
547 if ($opt_testmode) {
548 my $types = join (", ", sort keys (%{$entity->{PMX_content_types}}));
549 my $fh = $msginfo->{test_fh};
550 $filename =~ s/\d+/X/g if $filename =~ m/^msg-\d+-\d+.msg/;
551 print $fh "Types:$filename: $types\n" if $types;
552 }
553
554 my $elapsed = int(tv_interval ($start) * 1000);
555
556 $self->log (3, "$queue->{logid}: unpack archive '$filename' done ($elapsed ms)");
557 }
558 }
559
560 foreach my $part ($entity->parts) {
561 $self->unpack_entity ($unpack, $part, $msginfo, $queue);
562 }
563
564}
565
566sub handle_smtp {
567 my ($self, $smtp) = @_;
568
569 my ($csec, $usec) = gettimeofday ();
570
571 my $queue;
572 my $msginfo = {};
573 my $system_cfg = $self->{system_cfg};
574 my $ldap = $self->{ldap};
575 my $cinfo = $self->{cinfo};
576 my $lcid = $cinfo->{local}->{cid};
577
578 $msginfo->{test_fh} = Proxmox::AtomicFile->new ("testresult.out", "w")
579 if $opt_testmode;
580
581 $msginfo->{trusted} = $self->{trusted};
582
583
584# PHASE 1 - save incoming mail (already done)
585# on error: exit
586
587 $queue = $smtp->{queue};
588 $queue->{sa} = $self->{sa};
589
590 $queue->{bulk_score} = $system_cfg->get ('spam', 'bulk_score');
591
592 my $lic = $system_cfg->{license};
593 $queue->{lic_valid} = ($lic->valid && !$lic->expired) ? 1 : 0;
594
595 my $matching_rules;
596
597 eval {
598 $msginfo->{testmode} = $opt_testmode;
599 $msginfo->{sender} = $smtp->{from};
600 $msginfo->{xforward} = $smtp->{xforward};
601 $msginfo->{targets} = $smtp->{to};
602
603 $msginfo->{hostname} = $system_cfg->get ('dns', 'hostname');
604 $msginfo->{domain} = $system_cfg->get ('dns', 'domain');
605 $msginfo->{fqdn} = "$msginfo->{hostname}.$msginfo->{domain}";
606 $msginfo->{lcid} = $lcid;
607
608 # $msginfo->{targets} is case sensitive,
609 # but pmail is always lower case!
610
611 foreach my $t (@{$msginfo->{targets}}) {
612 my $res;
613 if ($ldap && ($res = $ldap->account_info ($t))) {
614 $msginfo->{pmail}->{$t} = $res->{pmail};
615 } else {
616 $msginfo->{pmail}->{$t} = lc ($t);
617 }
618 }
619
620# PHASE 2 - parse mail
621# on error: exit
622
623 my $maxfiles = $system_cfg->get ('clamav', 'archivemaxfiles') || 0;
624
625 my $entity = $queue->parse_mail ($maxfiles);
626
627 $self->log (3, "$queue->{logid}: new mail message-id=%s", $queue->{msgid});
628
629# PHASE 3 - run external content analyzers
630# (SPAM analyzer is run on demand later)
631# on error: log error messages
632
633
634 # test for virus first
635 $queue->{vinfo} = Proxmox::Utils::analyze_virus ($queue, $queue->{dataname},
636 $system_cfg, $opt_testmode);
637
638 # commtouch scanner
639 # only run with valid license
640 # do not run when we already detected a virus
641 # do not run for outbound mail (requires separate ctasd)
642
643 $queue->{ptime_commtouch} = 0;
644
645 if ($opt_testmode ||
646 ($queue->{lic_valid} && !$queue->{vinfo} && !$msginfo->{trusted})) {
647 my ($ct_refid, $ct_spam, $ct_vod, $ct_virus);
648 my ($csec, $usec) = gettimeofday ();
649 eval {
650
651 ($ct_refid, $ct_spam, $ct_vod, $ct_virus) =
652 Proxmox::Commtouch::ctasd_classify($queue->{dataname}, $queue->{from});
653 };
654 my $err = $@;
655
656 my ($csec_end, $usec_end) = gettimeofday ();
657 $queue->{ptime_commtouch} =
658 int (($csec_end-$csec)*1000 + ($usec_end - $usec)/1000);
659
660 if ($err) {
661 syslog ('err', "ctasd error: %s", $err);
662 $queue->{errors} = 1;
663 } else {
664 if ($ct_virus) {
665 syslog ('info', "$queue->{logid}: virus detected: $ct_virus (commtouch)");
666 $queue->{vinfo_commtouch} = $ct_virus;
667 $queue->{vinfo} = "$ct_virus (commtouch)";
668 }
669
670 if ($ct_spam && $ct_spam eq 'Confirmed') {
671 syslog ('info', "$queue->{logid}: known spam mail (confirmed by commtouch)");
672 }
673
674 $queue->{commtouch_spam} = $ct_spam;
675 $queue->{commtouch_vod} = $ct_vod;
676
677 if ($opt_testmode) {
678 $queue->{commtouch_refid} = "FAKEK_COMTOUCH_REFID(testmode)";
679 } else {
680 $queue->{commtouch_refid} = $ct_refid;
681 }
682 }
683 }
684
685 # always add this headers to incoming mails
686 # to enable user to report false negatives
687 if (!$msginfo->{trusted}) {
688 if (!$queue->{lic_valid}) {
689 $entity->head->replace('X-Proxmox-License', 'Invalid');
690 }
691
692 if ($queue->{vinfo}) {
693 $entity->head->replace('X-Proxmox-VInfo', $queue->{vinfo});
694 }
695
696 if ($queue->{commtouch_refid}) {
697 $entity->head->replace('X-Proxmox-CTCH-Refid', $queue->{commtouch_refid});
698 }
699 }
700
701 # we unpack after virus scanning, because this is more secure.
702 # This way virus scanners gets the whole mail files and are able
703 # to detect phishing signature for example - which would not work
704 # if we decompose first and only analyze the decomposed attachments.
705 # Disadvantage is that we need to unpack more than
706 # once (bad performance).
707
708 # should we scan content types inside archives
709
710
711 my $rulecache = $self->{rulecache};
712
713 my $scan_archives = 0;
714
715 if (($rulecache->{archivefilter_in} && !$msginfo->{trusted}) ||
716 ($rulecache->{archivefilter_out} && $msginfo->{trusted})) {
717 $scan_archives = 1;
718 }
719
720 if ($scan_archives && !$queue->{vinfo}) {
721
722 # unpack all archives - determine contained content types
723
724 my $decdir = $queue->{dumpdir} . "/__decoded_archives";
725 mkdir $decdir;
726
727 my $start = [gettimeofday];
728
729 my $unpack;
730 eval {
731
732 # limits: We use clamav limit for maxfiles, and scan
733 # only 4 levels, timeout of 30 seconds
734
735 $unpack = Proxmox::Unpack->new (tmpdir => $decdir,
736 timeout => 30,
737 ctonly => 1, # only detect CTs
738 maxrec => -4,
739 maxfiles => $maxfiles);
740
741 $self->unpack_entity ($unpack, $entity, $msginfo, $queue);
742 };
743
744 my $err = $@;
745
746 $unpack->cleanup() if $unpack;
747
748 my $elapsed = int(tv_interval ($start) * 1000);
749
750 if ($err) {
751 $self->log (3, "$queue->{logid}: unpack archive failed ($elapsed ms) - $err");
752 }
753 }
754
755# PHASE 4 - apply rules
756# on error: exit (cleanup process should do the rest)
757 $msginfo->{maxspamsize} = $system_cfg->get ('spam', 'maxspamsize');
758 if ($msginfo->{maxspamsize} <= 1024*64) {
759 $msginfo->{maxspamsize} = 1024*64;
760 }
761
762 if ($msginfo->{trusted}) {
763 my $hide = $system_cfg->get ('mail', 'hide_received');
764 $entity->head->delete("Received") if $hide;
765 }
766
767 $matching_rules = $self->apply_rules ($queue, $msginfo, $entity, $ldap);
768 };
769
770 my $err = $@;
771
772 $self->{errors} = $queue->{errors};
773
774 # restart on error
775 $self->{errors} = 1 if $err;
776
777 $queue->close ();
778
779 die $err if $err;
780
781 my ($csec_end, $usec_end) = gettimeofday ();
782 my $time_total =
783 int (($csec_end-$csec)*1000 + ($usec_end - $usec)/1000);
784
785# PHASE 5 - log statistics
786# on error: log error messages
787
788 eval {
789 my $dbh = $self->{ruledb}->{dbh};
790 my $where = "";
791 foreach my $rid (@$matching_rules) {
792 $where .= $where ? " OR ID = $rid" : "ID = $rid";
793 }
794 if ($where) {
795 $dbh->do ("UPDATE Rule " .
796 "SET Count = Count + 1 " .
797 "WHERE $where");
798 }
799
800 my $insert_cmds = "SELECT nextval ('cstatistic_id_seq');INSERT INTO CStatistic " .
801 "(CID, RID, ID, Time, Bytes, Direction, Spamlevel, VirusInfo, PTime, Sender) VALUES (" .
802 "$lcid, currval ('cstatistic_id_seq'), currval ('cstatistic_id_seq'),";
803
804 $insert_cmds .= $queue->{rtime} . ',';
805 $insert_cmds .= $queue->{bytes} . ',';
806 $insert_cmds .= $dbh->quote ($msginfo->{trusted} ? 0 : 1) . ',';
807 $insert_cmds .= ($queue->{sa_score} || 0) . ',';
808 $insert_cmds .= $dbh->quote ($queue->{vinfo}) . ',';
809 $insert_cmds .= $time_total . ',';
810 $insert_cmds .= $dbh->quote ($msginfo->{sender}) . ');';
811
812 foreach my $r (@{$msginfo->{targets}}) {
813 my $tmp = $dbh->quote ($r);
814 my $blocked = $queue->{status}->{$r} eq 'blocked' ? 1 : 0;
815 $insert_cmds .= "INSERT INTO CReceivers (CStatistic_CID, CStatistic_RID, Receiver, Blocked) " .
816 "VALUES ($lcid, currval ('cstatistic_id_seq'), $tmp, '$blocked'); ";
817 }
818
819 $dbh->do ($insert_cmds);
820 };
821
822 # save $err (because log clears $@)
823 $err = $@;
824
825 $time_total = $time_total/1000;
826
827 my $ptspam = ($queue->{ptime_spam} || 0)/1000;
828 my $ptcommtouch = ($queue->{ptime_commtouch} || 0)/1000;
829 my $ptavira = ($queue->{ptime_avira} || 0)/1000;
830 my $ptclam = ($queue->{ptime_clam} || 0)/1000;
831
832 $self->log (3, "$queue->{logid}: processing time: ${time_total} seconds ($ptcommtouch, $ptspam, $ptclam, $ptavira)");
833
834 $msginfo->{test_fh}->close if $opt_testmode;
835
836 die $err if ($err);
837}
838
839
840sub process_request {
841 my $self = shift;
842 my $prop = $self->{server};
843 my $sock = $prop->{client};
844
845 eval {
846
847 # make sure the ldap cache is up to date
848 $self->{ldap}->update (1);
849
850 $self->load_config() if $self->{reload_config};
851
852 $self->{trusted} = 0;
853 if ($prop->{sockport} == $opt_int_port && !$opt_untrusted) {
854 $self->{trusted} = 1;
855 }
856
857 my $smtp = Proxmox::SMTP->new ($sock);
858
859 my $maxcount = $max_requests - $prop->{requests};
860
861 my $count = $smtp->loop (\&handle_smtp, $self, $maxcount);
862 if ($count > 1) {
863 $prop->{requests} += $count - 1;
864 }
865 };
866
867 my $err = $@;
868
869 $self->log (0, $err) if $err;
870
871 kill (15, $prop->{ppid}) if $opt_testmode;
872
873 my $mem = Proxmox::Utils::get_mem_usage();
874
875 if ($opt_testmode) {
876 $self->log (0, "memory usage: $mem->{size} bytes");
877 } else {
878 if ($self->{errors}) {
879 $self->log (0, "fast exit because of errors (free $mem->{size} bytes)");
880 $self->done (1);
881 } elsif ($mem->{size} > (300*1024*1024)) {
882 $self->log (0, "fast exit to reduce server load (free $mem->{size} bytes)");
883 $self->done (1);
884 }
885 }
886
887 $self->done (1) if $err;
888}
889
890# test sig_hup with: for ((;;)) ;do kill -HUP `cat /var/run/proxprox.pid`; done;
891# wrapper to avoid multiple calls to sig_hup
892sub sig_hup {
893 my $self = shift;
894 my $prop = $self->{server};
895
896 return if defined ($prop->{_HUP}); # do not call twice
897
898 $self->SUPER::sig_hup();
899}
900
901sub restart_close_hook {
902 my $self = shift;
903
904 my $sig_set = POSIX::SigSet->new;
905 $sig_set->addset (&POSIX::SIGHUP);
906 $sig_set->addset (&POSIX::SIGCHLD); # to avoid zombies
907 my $old_sig_set = POSIX::SigSet->new();
908
909 sigprocmask (SIG_BLOCK, $sig_set, $old_sig_set);
910}
911
912sub pre_server_close_hook {
913 my $self = shift;
914 my $prop = $self->{server};
915
916 if (defined $prop->{_HUP}) {
917 undef $prop->{pid_file_unlink};
918 }
919
920 if (defined $prop->{children}) {
921 foreach my $pid (keys %{$prop->{children}}) {
922 kill (1, $pid); # HUP childs
923 }
924 }
925
926 # nicely shutdown childs (give them max 30 seconds to shut down)
927 my $previous_alarm = alarm (30);
928 eval {
929 local $SIG{ALRM} = sub { die "Timed Out!\n" };
930
931 my $pid;
932 1 while ((($pid = waitpid (-1, 0)) > 0) || ($! == EINTR));
933 };
934 alarm ($previous_alarm);
935}
936
937# initialize mime system before fork
938xdg_mime_get_mime_type_for_file ($0);
939
940my $server = bless {
941 server => $server_attr,
942};
943
944if (!$opt_testmode) {
945 $server->run ();
946} else {
947 if (fork) {
948 $server->run ();
949 } else {
950 sleep (1);
951 my $sender ='sender@proxtest.com';
952 my $targets = ['target1@proxtest.com',
953 'target2@proxtest.com',
954 'target3@proxtest.com'];
955
956 my $smtp = Net::SMTP->new ('127.0.0.1', Port => 10023);
957
958 die "unable to connect $!" if !$smtp;
959
960 # syslog ('info', "connected to " . $smtp->domain);
961
962 $smtp->mail ($sender);
963 $smtp->to (@$targets);
964
965 $smtp->data();
966
967 open (TMP, $opt_testmode) ||
968 croak ("unable to upen file '$opt_testmode' - $! :ERROR");
969 while (<TMP>) {
970 $smtp->datasend ($_);
971 }
972 close TMP;
973
974 $smtp->datasend ("\n");
975 $smtp->dataend ();
976
977 $smtp->quit;
978 }
979}
980
981exit (0);
982
983__END__
984
985=head1 NAME
986
987proxprox - the Proxmox mail filter
988
989=head1 SYNOPSIS
990
991proxprox [-u] [-t testfile]
992
993=head1 DESCRIPTION
994
995Documentation is available at www.proxmox.com
996
997
998