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