1#!/usr/local/bin/perl -T -w
2
3############################
4package postfwd3::basic;
5
6use warnings;
7use strict;
8use IO::Socket qw(SOCK_STREAM);
9use Sys::Syslog qw(:DEFAULT setlogsock);
10# export
11use Exporter qw(import);
12our @EXPORT = qw(
13	%postfwd_settings %postfwd_patterns
14	&uniq &init_log &log_info &log_note
15	&log_warn &log_err &log_crit
16);
17our @EXPORT_OK = qw(
18	%postfwd_commands
19	&wantsdebug &hash_to_list
20	&hash_to_str &str_to_hash
21	&check_inet &check_unix &ts
22	&load_hash &save_hash
23	$TIMEHIRES $STORABLE $NETADDR $DIGESTMD5
24);
25our($TIMEHIRES); our($STORABLE); our($NETADDR); our($NETCIDR); our($DIGESTMD5);
26BEGIN {
27	# use Time::HiRes if available
28	eval { require Time::HiRes };
29	$TIMEHIRES = ($@) ? 0 : (Time::HiRes->VERSION || 'available');
30	# use Storable if available
31	eval { require Storable };
32	$STORABLE = ($@) ? 0 : (Storable->VERSION || 'available');
33	eval { require NetAddr::IP };
34	$NETADDR = ($@) ? 0 : (NetAddr::IP->VERSION || 'available');
35	eval { require Net::CIDR::Lite };
36	$NETCIDR = ($@) ? 0 : (Net::CIDR::Lite->VERSION || 'available');
37	eval { require Digest::MD5 };
38	$DIGESTMD5 = ($@) ? 0 : (Digest::MD5->VERSION || 'available');
39	# use Storable if available
40	Storable->import( qw(nstore retrieve) ) if $STORABLE;
41	# This check prevents an error on "CentOS release 6"
42	eval { require Net::Server::Multiplex };
43	die "\nERROR: Required perl module 'Net::Server::Multiplex' not found!\n\n" if ($@);
44};
45
46
47# basics
48our $NAME	= "postfwd3";
49our $VERSION	= "2.03";
50our $DEFAULT	= 'DUNNO';
51
52# change this, to match your POD requirements
53# we need pod2text for the -m switch (manual)
54$ENV{PATH}                      = "/bin:/usr/bin:/usr/local/bin";
55$ENV{ENV}                       = "";
56our($cmd_manual)                = "pod2text";
57our($cmd_pager)                 = "more";
58
59my $sepreq	= '///';
60my $seplst	= ':::';
61my $seplim	= '~~~';
62my $nounixsock	= ($^O eq 'solaris');
63my $terminating = 0;
64
65# program settings
66our %postfwd_settings = (
67	base => {
68		user             => 'nobody',
69		group            => 'nobody',
70		log_level        => 2,
71		log_file         => 'Sys::Syslog',
72		syslog_ident     => "$NAME",
73		umask		 => "0177",
74		no_client_stdout => 1,
75	},
76	master => {
77		pid_file         => "/var/tmp/$NAME-master.pid",
78		watchdog	 => 60,
79		failures	 => 7,
80		respawn		 => 4,
81		daemons		 => [ 'cache', 'server' ],
82	},
83	cache => {
84		commandline	 => " ".$NAME."::cache",
85		syslog_ident     => "$NAME/cache",
86		host		 => (($nounixsock) ? "127.0.0.1" : ""),
87		port             => (($nounixsock) ? "10043" : "/var/tmp/$NAME-cache.socket"),
88		proto		 => (($nounixsock) ? "tcp" : "unix"),
89		check		 => (($nounixsock) ? \&check_inet : \&check_unix),
90		umask		 => "0177",
91	},
92	server => {
93		commandline	 => " ".$NAME."::policy",
94		syslog_ident     => "$NAME/policy",
95		host             => '127.0.0.1',
96		port             => '10045',
97		proto            => "tcp",
98		check		 => \&check_inet,
99		umask		 => "0111",
100		# child control
101		#check_for_dead         => 30,
102		#check_for_waiting      => 10,
103		min_spare_servers       => 5,
104		min_servers             => 10,
105		max_spare_servers       => 50,
106		max_servers             => 100,
107		max_requests            => 200, # lowered
108		child_communication     => 1, # children report data to parent for summary in PreFork mode
109		leave_children_open_on_hup => 1,  # children should finish their work
110	},
111	syslog => {
112		nolog		=> 0,
113		noidlestats	=> 0,
114		norulestats	=> 0,
115		name		=> $NAME,
116		facility	=> 'mail',
117		options		=> 'pid',
118		# allow "umlaute" ;)
119		#unsafe_charset	=> qr/[^\x20-\x7E,\x80-\xFE]/,
120		unsafe_charset	=> qr/[^\x20-\x7E]/,
121		unsafe_version  => ( (not(defined $Sys::Syslog::VERSION) or $Sys::Syslog::VERSION lt '0.15')
122				or (not(defined $Net::Server::VERSION) or $Net::Server::VERSION lt '0.94') ),
123		perfmon   	=> 0,
124		stdout		=> 0,
125		stdin		=> 0,
126	},
127	timeout => {
128		rule		=> 40,
129		cache		=> 3,
130		server		=> 3,
131		config		=> 4,
132	},
133	request => {
134		ttl		=> 600,
135		cleanup		=> 600,
136		no_sender	=> 0,
137		rdomain_only	=> 0,
138		no_size		=> 0,
139		nolog		=> 0,
140		noparent	=> 0,
141		autocacheid	=> 0,
142		usemd5		=> 1,
143	},
144	dns => {
145		disable		=> 0,
146		nolog		=> 0,
147		noparent	=> 1,
148		anylog		=> 0,
149		async_txt	=> 0,
150		timeout		=> 14,
151		max_timeout	=> 10,
152		max_interval	=> 1200,
153		ttl		=> 3600,
154		cleanup		=> 600,
155		mask		=> '^127\.',
156		max_ns_lookups	=> 100,
157		max_mx_lookups	=> 100,
158		ipv6_dnsbl	=> 0,
159	},
160	rate => {
161		cleanup		=> 600,
162		noparent	=> 0,
163		store		=> undef,
164	},
165	group => {
166		ttl		=> 3600, # default ttl=1h
167		maxitems	=> 999999, # '-1' for unlimited
168		cleanup		=> 600,
169		noparent	=> 0,
170		aggregate_addrs	=> 0,
171		store		=> undef,
172	},
173	scores => {
174		"5.0"		=> "554 5.7.1 ".$NAME." score exceeded",
175	},
176	debug     => {
177		#all		=> 0,
178		#verbose	=> 0,
179		#cache		=> 0,
180		#rates		=> 0,
181		#config		=> 0,
182		#cache		=> 0,
183		#getcache	=> 0,
184		#setcache	=> 0,
185		#dns		=> 0,
186		#getdns		=> 0,
187		#setdns		=> 0,
188	},
189	dumper => {
190		Indent		 => 2,
191		Purity		 => 1,
192		Quotekeys	 => 0,
193		Sortkeys	 => 1,
194		Terse		 => 1,
195	},
196	name		=> $NAME,
197	version		=> $VERSION,
198	default		=> $DEFAULT,
199	# choose 'Multiplex' or 'PreFork'
200	personality	=> 'Multiplex',
201	autopersonality	=> 1,
202	daemon		=> 1,
203	chroot		=> undef,
204	manual		=> $cmd_manual,
205	pager		=> $cmd_pager,
206	sepreq		=> $sepreq,
207	seplst		=> $seplst,
208	seplim		=> $seplim,
209	summary		=> 600,
210	instant		=> 0,
211	verbose		=> 0,
212	test		=> 0,
213	keep_rates	=> 0,
214	keep_groups	=> 0,
215	aggregate_addrs	=> 0,
216	cidr_method	=> 'postfwd',
217	max_command_recursion => 64,
218        timeformat	=> ( ($TIMEHIRES) ? '%.2f' : '%d' ),
219);
220
221# daemon commands
222our %postfwd_commands = (
223	ping          => 'PING',
224	pong          => 'PONG',
225	dumpstats     => 'DS',
226	dumpcache     => 'DC',
227	delcache      => 'RC',
228	delrate       => 'RR',
229	countcache    => 'CN',
230	matchcache    => 'MT',
231	setcacheitem  => 'SC',
232	getcacheitem  => 'GC',
233	getcacheval   => 'GV',
234	setrateitem   => 'SR',
235	groupadd      => 'GA',
236	groupdel      => 'GD',
237	groupget      => 'GG',
238);
239
240# precompiled patterns
241our %postfwd_patterns = (
242	ping          => $postfwd_commands{ping},
243	pong          => $postfwd_commands{pong},
244	keyval	      => qr/^([^=]+)=(.*)$/,
245	cntval        => qr/^([^=]+)=(\d+)$/,
246	command       => qr/^CMD\s*=/i,
247	dumpstats     => qr/^CMD\s*=\s*$postfwd_commands{dumpstats}\s*;\s*$/i,
248	dumpcache     => qr/^CMD\s*=\s*$postfwd_commands{dumpcache}\s*;\s*$/i,
249	delcache      => qr/^CMD\s*=\s*$postfwd_commands{delcache}\s+(.*?)$/i,
250	delrate       => qr/^CMD\s*=\s*$postfwd_commands{delrate}\s+(.*?)$/i,
251	countcache    => qr/^CMD\s*=\s*$postfwd_commands{countcache}\s*;\s*TYPE\s*=\s*(.*?)\s*$/i,
252	matchcache    => qr/^CMD\s*=\s*$postfwd_commands{matchcache}\s*;\s*TYPE\s*=\s*(.*?)\s*$/i,
253	setcacheitem  => qr/^CMD\s*=\s*$postfwd_commands{setcacheitem}\s*;\s*TYPE\s*=\s*([^;]+)\s*;\s*ITEM\s*=\s*(.*?)\s*$sepreq\s*(.*?)\s*$/i,
254	getcacheitem  => qr/^CMD\s*=\s*$postfwd_commands{getcacheitem}\s*;\s*TYPE\s*=\s*([^;]+)\s*;\s*ITEM\s*=\s*(.*?)\s*$/i,
255	getcacheval   => qr/^CMD\s*=\s*$postfwd_commands{getcacheval}\s*;\s*TYPE\s*=\s*([^;]+)\s*;\s*ITEM\s*=\s*(.*?)\s*$sepreq\s*KEY\s*=\s*(.*?)\s*$/i,
256	setrateitem   => qr/^CMD\s*=\s*$postfwd_commands{setrateitem}\s*;\s*TYPE\s*=\s*([^;]+)\s*;\s*ITEM\s*=\s*(.*?)\s*$sepreq\s*(.*?)\s*$/i,
257	groupadd      => qr/^CMD\s*=\s*$postfwd_commands{groupadd}\s*;\s*TYPE\s*=\s*([^;]+)\s*;\s*ITEM\s*=\s*(.*?)\s*$seplim\s*(.*?)\s*$/i,
258	groupdel      => qr/^CMD\s*=\s*$postfwd_commands{groupdel}\s*;\s*TYPE\s*=\s*([^;]+)\s*;\s*ITEM\s*=\s*(.*?)\s*$/i,
259	groupget      => qr/^CMD\s*=\s*$postfwd_commands{groupget}\s*;\s*TYPE\s*=\s*(.*?)\s*$/i,
260);
261
262
263## SUBS
264
265# prints formatted timestamp
266sub ts { return sprintf ($postfwd_settings{timeformat}, $_[0]) };
267
268# takes a list and returns a unified list, keeping given order
269sub uniq {
270	undef my %uniq;
271	return grep(!$uniq{$_}++, @_);
272};
273
274# tests debug levels
275sub wantsdebug {
276	return unless defined $postfwd_settings{debug};
277	foreach (@_) { return 1 if defined $postfwd_settings{debug}{$_} };
278};
279
280# hash -> scalar
281sub hash_to_str {
282	my $request = shift; my $result  = '';
283	map { $result .= $postfwd_settings{sepreq}."$_=".((ref $request->{$_} eq 'ARRAY') ? (join $postfwd_settings{seplst}, @{$request->{$_}}) : ($request->{$_} || '')) } (keys %{$request});
284	return $result;
285};
286
287# scalar -> hash
288sub str_to_hash {
289	my $request = shift; my %result  = ();
290	foreach (split $postfwd_settings{sepreq}, $$request) {
291		next unless m/$postfwd_patterns{keyval}/;
292		my @items = split $postfwd_settings{seplst}, $2;
293		($#items) ? @{$result{$1}} = @items : $result{$1} = $2;
294	}; return %result;
295};
296
297# displays hash structure
298sub hash_to_list {
299	my ($pre, $request) = @_; my @output = ();
300	# get longest key
301	my $minkey = '-'.(length((sort {length($b) <=> length($a)} (keys %{$request}))[0] || '') + 1);
302	while ( my($s, $v) = each %{$request} ) {
303		my $r = ref $v;
304		if ($r eq 'HASH') {
305			push @output, (%{$v})
306				? hash_to_list ( sprintf ("%s -> %".$minkey."s", $pre, '%'.$s), \%{$v} )
307				: sprintf ("%s -> %".$minkey."s -> %s", $pre, '%'.$s, 'undef');
308		} elsif ($r eq 'ARRAY') {
309			push @output, sprintf ("%s -> %".$minkey."s -> %s", $pre, '@'.$s, ((@{$v}) ? "'".(join ",", @{$v})."'" : 'undef'));
310		} elsif ($r eq 'CODE') {
311			push @output, sprintf ("%s -> %".$minkey."s -> %s", $pre, '&'.$s, ((defined $v) ? "'".$v."'" : 'undef'));
312		} else {
313			push @output, sprintf ("%s -> %".$minkey."s -> %s", $pre, '$'.$s, ((defined $v) ? "'".$v."'" : 'undef'));
314		};
315	};
316	@output = sort { my ($c, $d) = ($a, $b);
317		$c =~ tr/$/1/; $c =~ tr/&/2/; $c =~ tr/@/3/; $c =~ tr/%/4/;
318		$d =~ tr/$/1/; $d =~ tr/&/2/; $d =~ tr/@/3/; $d =~ tr/%/4/;
319		return $c cmp $d; } @output;
320	return @output;
321};
322
323# Sys::Syslog < 0.15
324sub mylogs_old {
325	my $prio = shift @_;
326	my $msg  = shift @_;
327	eval { local $SIG{'__DIE__'}; syslog ($prio,$msg,@_) };
328};
329
330# Sys::Syslog >= 0.15
331sub mylogs_new {
332	my $prio = shift @_;
333	my $msg  = shift @_;
334	syslog ($prio,$msg,@_);
335};
336
337# Syslog to stdout
338sub mylogs_stdout {
339	my $prio = shift @_;
340	my $msg  = shift @_;
341	$msg =~ s/\%/%%/g; $msg =~ /^(.*)$/;
342	printf STDERR "[".$postfwd_settings{name}."][$$][LOG $prio]: $1\n", @_;
343};
344
345# send log message
346sub mylogs {
347	my $prio = shift @_;
348	my $msg  = shift @_;
349	return if $postfwd_settings{syslog}{nolog};
350	# escape unsafe characters
351	$msg =~ s/$postfwd_settings{syslog}{unsafe_charset}/?/g;
352	$msg =~ s/\%/%%/g;
353	&{$postfwd_settings{syslog}{logger}} ($prio,$msg,@_);
354};
355
356# short versions
357sub log_info { mylogs ('info', @_) };
358sub log_note { mylogs ('notice', @_) };
359sub log_warn { mylogs ('warning', @_) };
360sub log_err  { mylogs ('err', @_) };
361sub log_crit { mylogs ('crit', @_) };
362
363# init logging
364sub init_log {
365	my($logname) = @_;
366	$postfwd_settings{syslog}{name} = $logname if $logname;
367	# Some module versions contain non-digits (example: Sys::Syslog::VERSION 0.33_01)
368	my $sver = $Sys::Syslog::VERSION;
369	my $nver = $Net::Server::VERSION;
370	$sver =~ s/[^0-9\.]//g if defined $sver;
371	$nver =~ s/[^0-9\.]//g if defined $nver;
372	$postfwd_settings{syslog}{unsafe_version} = ( (not((defined $sver) or (defined $nver))) or ($sver lt '0.15') or ($nver lt '0.94') );
373	if ($postfwd_settings{syslog}{stdout}) {
374		$postfwd_settings{syslog}{logger}   = \&mylogs_stdout;
375		$postfwd_settings{syslog}{socktype} = 'console';
376		$postfwd_settings{syslog}{options} = 'cons,pid';
377		$postfwd_settings{base}{log_file} = undef;
378	} else {
379		$postfwd_settings{syslog}{socktype} = ( $postfwd_settings{syslog}{socktype} || (($postfwd_settings{syslog}{unsafe_version}) ? (($nounixsock) ? 'inet' : 'unix') : 'native') );
380		$postfwd_settings{syslog}{logger}   = ($postfwd_settings{syslog}{unsafe_version}) ? \&mylogs_old : \&mylogs_new;
381	};
382	setlogsock $postfwd_settings{syslog}{socktype};
383	openlog $postfwd_settings{syslog}{name}, $postfwd_settings{syslog}{options}, $postfwd_settings{syslog}{facility};
384	log_info ("set up syslogging Sys::Syslog".((defined $Sys::Syslog::VERSION) ? " version $Sys::Syslog::VERSION" : '').' ['.($postfwd_settings{syslog}{unsafe_version} || 0).']' ) if wantsdebug (qw[ all verbose ]);
385};
386
387# check: INET
388sub check_inet {
389	my ($type,$send) = @_;
390	if ( my $socket = new IO::Socket::INET (
391		PeerAddr => $postfwd_settings{$type}{host},
392		PeerPort => $postfwd_settings{$type}{port},
393		Proto    => 'tcp',
394		Timeout  => $postfwd_settings{timeout}{$type},
395		Type     => SOCK_STREAM ) ) {
396		$socket->print("$send\n");
397		$send = $socket->getline();
398		$socket->close();
399		chomp($send) if $send;
400	} else {
401		warn("can not open socket to $postfwd_settings{$type}{host}:$postfwd_settings{$type}{port}: '$!' '$@'\n") unless $terminating;
402		undef $send;
403	};
404	return $send || '';
405};
406
407# check: UNIX
408sub check_unix {
409	my ($type,$send) = @_;
410	if ( my $socket = new IO::Socket::UNIX (
411		Peer     => $postfwd_settings{$type}{port},
412		Timeout  => $postfwd_settings{timeout}{$type},
413		Type     => SOCK_STREAM ) ) {
414		$socket->print("$send\n");
415		$send = $socket->getline();
416		$socket->close();
417		chomp($send) if $send;
418	} else {
419		warn("can not open socket to $postfwd_settings{$type}{host}:$postfwd_settings{$type}{port}: '$!' '$@'\n") unless $terminating;
420		undef $send;
421	};
422	return $send || '';
423};
424
425# saves hash to disk
426sub save_hash {
427    my($ref,$nam,$fil) = @_;
428    return unless ($STORABLE and $fil and defined $ref and scalar keys %{$ref});
429    eval {
430	local $SIG{__DIE__} = sub { warn("can not store $nam cache to $fil: '$!' '$@'") };
431	nstore ($ref, $fil);
432    };
433    log_info ("Saved ".(scalar %{$ref})." $nam items to $fil") unless $@;
434};
435
436# loads hash from disk
437sub load_hash {
438    my($ref,$nam,$fil) = @_;
439    return unless ($STORABLE and $fil and (-f $fil));
440    eval {
441	local $SIG{__DIE__} = sub { log_note ("can not load $nam cache from $fil: '$!' '$@'") };
442	%{$ref} = %{retrieve($fil)};
443    };
444    log_info ("Fetched ".(scalar %{$ref})." $nam items from $fil") if ( not($@) and $ref );
445};
446
4471; # EOF postfwd3::basic
448
449
450############################
451package postfwd3::cache;
452
453
454## MODULES
455use warnings;
456use strict;
457use base 'Net::Server::Multiplex';
458import postfwd3::basic qw(:DEFAULT &wantsdebug &hash_to_list &hash_to_str &ts &load_hash &save_hash $TIMEHIRES $NETADDR $DIGESTMD5);
459use vars qw( %Cache %Groups %Cleanup %Count %Interval %Top $Reload_Conf $Summary $StartTime );
460BEGIN {
461	# use Time::HiRes if available
462	Time::HiRes->import( qw(time) ) if $TIMEHIRES;
463};
464
465
466## SUBS
467
468# prepare stats
469sub list_stats {
470	my @output = (); my $line = ''; my $now = time();
471	my $uptime  = $now - $StartTime;
472	return @output unless $uptime and (%Count or %Cache);
473	return @output if $postfwd_settings{syslog}{noidlestats}
474		and ((($Interval{request_set} || 0) + ($Interval{request_get} || 0)) <= 0)
475		and ((($Interval{rate_set} || 0) + ($Interval{rate_get} || 0)) <= 0)
476		and ((($Interval{dns_set} || 0) + ($Interval{dns_get} || 0)) <= 0);
477	push ( @output, sprintf (
478		"[STATS] %s::cache %s: %d queries since %d days, %02d:%02d:%02d hours",
479		$postfwd_settings{name},
480		$postfwd_settings{version},
481		($Count{cache_queries} || 0),
482		($uptime / 60 / 60 / 24),
483		(($uptime / 60 / 60) % 24),
484		(($uptime / 60) % 60),
485		($uptime % 60)
486	) );
487	my $lastreq = (($now - $Summary) > 0) ? (($Interval{request_set} || 0) + ($Interval{request_get} || 0)) / ($now - $Summary) * 60 : 0;
488	$Top{request} = $lastreq if ($lastreq > ($Top{request} || 0)); $Top{request} ||= 0;
489	my $lastdns = (($now - $Summary) > 0) ? (($Interval{dns_set} || 0) + ($Interval{dns_get} || 0)) / ($now - $Summary) * 60 : 0;
490	$Top{dns} = $lastdns if ($lastdns > ($Top{dns} || 0)); $Top{dns} ||= 0;
491	push ( @output, sprintf (
492		"[STATS] Requests: %.1f/min last, %.1f/min overall, %.1f/min top",
493		$lastreq,
494		(($Count{request_set} || 0) + ($Count{request_get} || 0)) / $uptime * 60,
495		$Top{request}
496	) );
497	push ( @output, sprintf (
498		"[STATS] Dnsstats: %.1f/min last, %.1f/min overall, %.1f/min top",
499		$lastdns,
500		(($Count{dns_set} || 0) + ($Count{dns_get} || 0)) / $uptime * 60,
501		$Top{dns}
502	) ) unless ($postfwd_settings{dns}{disable} or $postfwd_settings{dns}{noparent});
503	push ( @output, sprintf (
504		"[STATS] Hitrates: %.1f%% requests, %.1f%% dns, %.1f%% rates",
505		($Count{request_get}) ? ($Count{request_hits} || 0) / $Count{request_get} * 100 : 0,
506		($Count{dns_get}) ? ($Count{dns_hits} || 0) / $Count{dns_get} * 100 : 0,
507		($Count{rate_get}) ? ($Count{rate_hits} || 0) / $Count{rate_get} * 100 : 0
508	) );
509	push ( @output, "[STATS] Contents: ".
510		join ', ', map { $_ = "$_=".(scalar keys %{$Cache{$_}}) } (reverse sort keys %Cache)
511	);
512
513	my $grpcnt = (scalar keys %Groups || 0);
514	if ( $grpcnt ) {
515		my $grpitems = 0;
516		map { $grpitems += (scalar keys %{$Groups{$_}} || 0) } (sort keys %Groups);
517		push ( @output, "[STATS] $grpcnt Groups with $grpitems item".(($grpitems > 1) ? 's' : '')." overall" );
518	};
519
520	if (wantsdebug (qw[ all stats devel parent_cache ])) {
521		push ( @output, "[STATS] Counters: ".
522			join ', ', map { $_ = "$_=".$Count{$_} } (reverse sort keys %Count) );
523		push ( @output, "[STATS] Interval: ".
524			join ', ', map { $_ = "$_=".$Interval{$_} } (reverse sort keys %Interval) );
525	};
526	map { $Interval{$_} = 0 } (keys %Interval);
527	$Summary = $now;
528	return @output;
529};
530
531# return cache contents
532sub dump_cache {
533	my @result = ();
534	foreach (sort keys %Cache) {
535		push @result, hash_to_list ('%'.$_."_cache", $Cache{$_}) if %{$Cache{$_}};
536	};
537	push @result, hash_to_list ('%Group_cache', \%Groups) if %Groups;
538	return @result;
539};
540
541# get a whole cache item
542sub get_cache {
543	my ($self,$now,$type,$item) = @_;
544	my @answer = ();
545	return '<undef>' unless ( defined $Cache{$type}{$item}{'until'} and ($now <= $Cache{$type}{$item}{'until'}[0]));
546	$Count{$type."_hits"}++;
547	map { push @answer, "$_=".(join $postfwd_settings{seplst}, @{$Cache{$type}{$item}{$_}}) } (keys %{$Cache{$type}{$item}});
548	return (join $postfwd_settings{sepreq}, @answer);
549};
550
551# set item to cache
552sub set_cache {
553	my ($self,$type,$item,$vals) = @_;
554	my @answer = ();
555	undef $Cache{$type}{$item};
556	foreach my $arg (split ($postfwd_settings{sepreq}, $vals)) {
557		map {	push @{$Cache{$type}{$item}{$1}}, $_;
558			push @answer, "$type->$item->$1=$_";
559			@{$Cache{$type}{$item}{$1}} = uniq(@{$Cache{$type}{$item}{$1}});
560		} (split $postfwd_settings{seplst}, $2) if ($arg =~ m/$postfwd_patterns{keyval}/);
561	};
562	@answer = '<undef>' unless @answer;
563	return (join '; ', @answer);
564};
565
566# add item to group
567sub add_group {
568	my ($self,$now,$groupname,$groupitem,$groupttl) = @_;
569	my $answer = 'no';
570	unless ( (defined $Groups{$groupname}{$groupitem} and $Groups{$groupname}{$groupitem} > $now)
571	    or (scalar keys %{$Groups{$groupname}} >= $postfwd_settings{group}{maxitems})
572	    or ($postfwd_settings{group}{maxitems} < 0)
573	) {
574		$Groups{$groupname}{$groupitem} = $groupttl;
575		$Count{"group_hits"}++;
576		$answer = 'ok';
577	};
578	return $answer;
579};
580
581# remove item from group
582sub del_group {
583	my ($self,$now,$groupname,$groupitem) = @_;
584	my $answer = 'no';
585	if (defined $Groups{$groupname}{$groupitem}) {
586		delete $Groups{$groupname}{$groupitem};
587		$Count{"group_hits"}++;
588		$answer = 'ok';
589	};
590	return $answer;
591};
592
593# get items from group
594sub get_group {
595	my ($self,$now,$groupname) = @_;
596	my @answer = ();
597	if (defined $Groups{$groupname} and scalar keys %{$Groups{$groupname}}) {
598		map { push @answer, $_  if $Groups{$groupname}{$_} > $now } (keys %{$Groups{$groupname}});
599		$Count{"group_hits"}++;
600	};
601	return (join $postfwd_settings{sepreq}, @answer);
602};
603
604# set rate to cache
605sub set_rate {
606	my ($self,$now,$type,$item,$vals) = @_;
607	my $rindex = ''; my %entry = (); my $rcount = undef;
608	($item, $rindex) = split $postfwd_settings{seplim}, $item;
609	push @{$Cache{$type}{$item}{'list'}}, $rindex;
610	@{$Cache{$type}{$item}{'list'}} = uniq(@{$Cache{$type}{$item}{'list'}});
611	foreach my $arg (split ($postfwd_settings{sepreq}, $vals)) {
612		map {
613			#push @{$entry{$1}}, $_;
614			#@{$entry{$1}} = uniq(@{$entry{$1}});
615			$entry{$1} = $_;
616		} (split $postfwd_settings{seplst}, $2) if ($arg =~ m/$postfwd_patterns{keyval}/);
617	};
618	unless (defined $Cache{$type}{$item}{$rindex} and defined $Cache{$type}{$item}{$rindex}{'until'}) {
619		%{$Cache{$type}{$item}{$rindex}} = %entry;
620	#} elsif ($now > $Cache{$type}{$item}{$rindex}{'until'}[0]) {
621	} elsif ($now > $Cache{$type}{$item}{$rindex}{'until'}) {
622		%{$Cache{$type}{$item}{$rindex}} = %entry;
623	} else {
624		#$rcount = $Cache{$type}{$item}{$rindex}{'count'}[0] + ($entry{'count'}[0] || 0);
625		#$Cache{$type}{$item}{$rindex}{'count'}[0] = $rcount;
626		$rcount = $Cache{$type}{$item}{$rindex}{'count'} + ($entry{'count'} || 0);
627		$Cache{$type}{$item}{$rindex}{'count'} = $rcount;
628	};
629	#return $rcount || $Cache{$type}{$item}{$rindex}{'count'}[0] || '<undef>';
630	return $rcount || $Cache{$type}{$item}{$rindex}{'count'} || '<undef>';
631};
632
633# clean up cache
634sub cleanup_cache {
635	my($type,$now) = @_;
636	my $start = $Cleanup{$type} = time();
637	log_info ("[CLEANUP] checking $type cache...") if wantsdebug (qw[ all cleanup parentcleanup ]);
638	return unless defined $Cache{$type} and my $count = scalar keys %{$Cache{$type}};
639	CLEANUP: foreach my $checkitem (keys %{$Cache{$type}}) {
640		next CLEANUP unless (defined $Cache{$type}{$checkitem});
641		unless ( defined $Cache{$type}{$checkitem}{'list'} ) {
642			# remove incomplete objects
643			unless ( defined $Cache{$type}{$checkitem}{'until'} and defined $Cache{$type}{$checkitem}{ttl} ) {
644				if ( wantsdebug (qw[ all cleanup parentcleanup devel ]) ) {
645					log_info ("[CLEANUP] deleting incomplete $type cache item '$checkitem'");
646					map { log_info ("[CLEANUP]  $_") } ( hash_to_list($Cache{$type}{$checkitem}) );
647				};
648				delete $Cache{$type}{$checkitem};
649			# remove timed out objects
650			} elsif ( $now > $Cache{$type}{$checkitem}{'until'}[0] ) {
651				log_info ("[CLEANUP] removing $type cache item '$checkitem' after ttl ".$Cache{$type}{$checkitem}{ttl}[0]."s")
652					if wantsdebug (qw[ all cleanup parentcleanup ]);
653				delete $Cache{$type}{$checkitem};
654			};
655		} else {
656			my @i = ();
657			foreach my $crate (@{$Cache{$type}{$checkitem}{'list'}}) {
658				unless ( defined $Cache{$type}{$checkitem}{$crate}{'until'} and defined $Cache{$type}{$checkitem}{$crate}{ttl} ) {
659					if ( wantsdebug (qw[ all cleanup parentcleanup devel ]) ) {
660						log_info ("[CLEANUP] deleting incomplete $type cache item '$checkitem'->'$crate'");
661						map { log_info ("[CLEANUP]  $_") } ( hash_to_list($Cache{$type}{$checkitem}{$crate}) );
662					};
663					delete $Cache{$type}{$checkitem}{$crate};
664				#} elsif ( $now > $Cache{$type}{$checkitem}{$crate}{'until'}[0] ) {
665				} elsif ( $now > $Cache{$type}{$checkitem}{$crate}{'until'} ) {
666					#log_info ("[CLEANUP] removing $type cache item '$checkitem'->'$crate' after ttl ".$Cache{$type}{$checkitem}{$crate}{ttl}[0]."s")
667					log_info ("[CLEANUP] removing $type cache item '$checkitem'->'$crate' after ttl ".$Cache{$type}{$checkitem}{$crate}{ttl}."s")
668						if wantsdebug (qw[ all cleanup parentcleanup ]);
669					delete $Cache{$type}{$checkitem}{$crate};
670				} else {
671					push @i, $crate;
672				};
673			};
674			unless ($i[0]) {
675				log_info ("[CLEANUP] removing $type cache complete item '$checkitem'")
676					if wantsdebug (qw[ all cleanup parentcleanup ]);
677				delete $Cache{$type}{$checkitem};
678			} else {
679				log_info ("[CLEANUP] new $type cache limits for item '$checkitem': ".(join ', ', @i))
680					if wantsdebug (qw[ all cleanup parentcleanup ]);
681				@{$Cache{$type}{$checkitem}{'list'}} = @i;
682			};
683		};
684	};
685	my $end = time();
686	log_info ("[CLEANUP] cleaning $type cache needed ".ts($end - $start)." seconds for "
687		.($count - scalar keys %{$Cache{$type}})." out of ".$count
688		." cached items after cleanup time ".$postfwd_settings{$type}{cleanup}."s")
689		if ( wantsdebug (qw[ all verbose cleanup parentcleanup ]) or (($end - $start) >= 1) );
690};
691
692# clean up group cache
693sub cleanup_groups {
694    my($now) = $_[0]; return unless $now;
695    my $start = $Cleanup{group} = time();
696    map { log_info("[CLEANUP] PRE: Group: '$_' -> ".(scalar keys %{$Groups{$_}})." items") } (sort keys %Groups) if wantsdebug (qw[ all cleanup parentcleanup groups ]);
697    foreach my $groupname (keys %Groups) {
698	foreach my $groupitem (keys %{$Groups{$groupname}}) {
699		unless ( $Groups{$groupname}{$groupitem} ) {
700			log_info ("[CLEANUP] deleting incomplete group-cache item '$groupitem' from group '$groupname'")
701				if wantsdebug (qw[ all cleanup parentcleanup devel groups ]);
702			delete $Groups{$groupname}{$groupitem};
703		} elsif ($now > $Groups{$groupname}{$groupitem}) {
704			log_info ("[CLEANUP] removing expired group-cache item '$groupitem' from group '$groupname'")
705				if wantsdebug (qw[ all cleanup parentcleanup groups ]);
706			delete $Groups{$groupname}{$groupitem};
707		};
708	};
709	unless ( %{$Groups{$groupname}} ) {
710		log_info ("[CLEANUP] removing empty group '$groupname'")
711			if wantsdebug (qw[ all cleanup parentcleanup groups ]);
712		delete $Groups{$groupname};
713	};
714    };
715    map { log_info("[CLEANUP] POST: Group: '$_' -> ".(scalar keys %{$Groups{$_}})." items") } (sort keys %Groups) if wantsdebug (qw[ all cleanup parentcleanup groups ]);
716    my $end = time();
717    log_info ("[CLEANUP] cleaning group cache needed ".ts($end - $start)." seconds")
718	if ( wantsdebug (qw[ all verbose cleanup parentcleanup ]) or (($end - $start) >= 1) );
719};
720
721# saves rate limits to disk
722sub save_rates {
723    cleanup_cache ('rate', time());
724    save_hash ($Cache{rate}, 'rate', $postfwd_settings{rate}{store});
725};
726
727# loads rate limits from disk
728sub load_rates {
729    my %lr = ();
730    load_hash (\%lr, 'rate', $postfwd_settings{rate}{store});
731    if (scalar keys %lr) {
732	%{$Cache{rate}} = %lr;
733    	cleanup_cache ('rate', time());
734    };
735};
736
737# saves group cache to disk
738sub save_groups {
739    cleanup_groups (time());
740    save_hash (\%Groups, 'group', $postfwd_settings{group}{store});
741};
742
743# loads group cache from disk
744sub load_groups {
745    load_hash (\%Groups, 'group', $postfwd_settings{group}{store});
746    cleanup_groups (time()) if scalar keys %Groups;
747};
748
749sub set_personality {};
750
751
752## Net::Server::Multiplex methods
753
754# ignore syslog failures
755sub handle_syslog_error {};
756
757# set $Reload_Conf marker on HUP signal
758sub sig_hup {
759	log_note ("catched HUP signal - clearing request cache on next request");
760	$Reload_Conf = 1;
761};
762
763# cache start
764sub pre_loop_hook() {
765	my $self = shift;
766	# change cache name
767	$0 = $self->{server}->{commandline} = " ".$postfwd_settings{name}.'::cache';
768	$postfwd_settings{name} .= "/cache";
769	load_rates();
770	load_groups();
771	$StartTime = $Summary = $Cleanup{request} = $Cleanup{rate} = $Cleanup{dns} = $Cleanup{group} = time();
772	log_info ("ready for input");
773};
774
775# cache end
776sub post_child_cleanup_hook {
777	save_rates();
778	save_groups();
779};
780
781# cache process request
782sub mux_input {
783	my ($self, $mux, $client, $mydata) = @_;
784	my $action = '<undef>';
785	my $now = time();
786	while ( $$mydata =~ s/^([^\r\n]*)\r?\n// ) {
787		# check request line
788		next unless defined $1;
789		my $request = $1;
790		log_info ("request: '$request'") if wantsdebug (qw[ all ]);
791		if ($Reload_Conf) {
792			undef $Reload_Conf; my $s = ''; delete $Cache{request};
793			unless ($postfwd_settings{keep_rates}) { delete $Cache{rate}; $s = ' and rate' };
794			unless ($postfwd_settings{keep_groups}) { %Groups = (); $s = ' and group' };
795			log_info ("request".(($s) ? "$s" : '')." cache cleared") if wantsdebug (qw[ all verbose ]);
796		};
797		if ($request eq $postfwd_patterns{ping}) {
798			$action = $postfwd_patterns{pong};
799		} elsif ($request =~ m/$postfwd_patterns{groupget}/) {
800			my ($type) = ($1);
801			log_info ("[GETGROUP] request: '$request'") if wantsdebug (qw[ all cache setcache groups ]);
802			cleanup_groups ($now) if (($now - $Cleanup{group}) > ($postfwd_settings{group}{cleanup} || 300));
803			$Count{cache_queries}++; $Interval{cache_queries}++;
804			$Count{"group_get"}++; $Interval{"group_get"}++;
805			$action = $self->get_group($now,$type);
806			log_info ("[GETGROUP] answer: '$action'") if wantsdebug (qw[ all cache getcache groups ]);
807		} elsif ($request =~ m/$postfwd_patterns{getcacheitem}/) {
808			my ($type, $item) = ($1, $2);
809			log_info ("[GETCACHEITEM] request: '$request'") if wantsdebug (qw[ all cache getcache ]);
810			cleanup_cache ($type,$now) if (($now - $Cleanup{$type}) > ($postfwd_settings{$type}{cleanup} || 300));
811			$Count{cache_queries}++; $Interval{cache_queries}++;
812			$Count{$type."_get"}++; $Interval{$type."_get"}++;
813			$action = $self->get_cache($now,$type,$item);
814			log_info ("[GETCACHEITEM] answer: '$action'") if wantsdebug (qw[ all cache getcache ]);
815		} elsif ($request =~ m/$postfwd_patterns{setcacheitem}/) {
816			my ($type, $item, $vals) = ($1, $2, $3);
817			log_info ("[SETCACHEITEM] request: '$request'") if wantsdebug (qw[ all cache setcache ]);
818			$Count{cache_queries}++; $Interval{cache_queries}++;
819			$Count{$type."_set"}++; $Interval{$type."_set"}++;
820			$action = $self->set_cache($type,$item,$vals);
821			log_info ("[SETCACHEITEM] answer: '$action'") if wantsdebug (qw[ all cache setcache ]);
822		} elsif ($request =~ m/$postfwd_patterns{setrateitem}/) {
823			my ($type, $item, $vals) = ($1, $2, $3);
824			log_info ("[SETRATEITEM] request: '$request'") if wantsdebug (qw[ all cache setcache ]);
825			$Count{cache_queries}++; $Interval{cache_queries}++;
826			$Count{$type."_set"}++; $Interval{$type."_set"}++;
827			$action = $self->set_rate($now,$type,$item,$vals);
828			log_info ("[SETRATEITEM] answer: '$action'") if wantsdebug (qw[ all cache setcache ]);
829		} elsif ($request =~ m/$postfwd_patterns{groupadd}/) {
830			my ($type, $item, $vals) = ($1, $2, $3);
831			log_info ("[ADDGROUP] request: '$request'") if wantsdebug (qw[ all cache setcache groups ]);
832			$Count{cache_queries}++; $Interval{cache_queries}++;
833			$Count{"group_add"}++; $Interval{"group_add"}++;
834			$action = $self->add_group($now,$type,$item,$vals);
835			log_info ("[ADDGROUP] answer: '$action'") if wantsdebug (qw[ all cache setcache groups ]);
836		} elsif ($request =~ m/$postfwd_patterns{groupdel}/) {
837			my ($type, $item) = ($1, $2);
838			log_info ("[DELGROUP] request: '$request'") if wantsdebug (qw[ all cache setcache groups ]);
839			$Count{cache_queries}++; $Interval{cache_queries}++;
840			$Count{"group_del"}++; $Interval{"group_del"}++;
841			$action = $self->del_group($now,$type,$item);
842			log_info ("[DELGROUP] answer: '$action'") if wantsdebug (qw[ all cache setcache groups ]);
843		} elsif ($request =~ m/$postfwd_patterns{dumpstats}/) {
844			$action = join $postfwd_settings{sepreq}.$postfwd_settings{seplst}, list_stats();
845		} elsif ($request =~ m/$postfwd_patterns{dumpcache}/) {
846			$action = join $postfwd_settings{sepreq}.$postfwd_settings{seplst}, dump_cache();
847		} elsif ($request =~ m/$postfwd_patterns{delcache}/) {
848			my $del = $1; $del =~ s/^[%]?//;
849			if (defined $Cache{'request'}{$del}) {
850				delete $Cache{'request'}{$del};
851				log_info ("[DELCACHEITEM] request cache item '$del' removed");
852				$action = "request cache item '$del' removed";
853			} else {
854				log_info ("[DELCACHEITEM] request cache removal of '$del' failed: item not found");
855				$action = "request cache removal of '$del' failed: item not found";
856			};
857		} elsif ($request =~ m/$postfwd_patterns{delrate}/) {
858			my $del = $1; $del =~ s/^[%]?//;
859			if (defined $Cache{'rate'}{$del}) {
860				delete $Cache{'rate'}{$del};
861				log_info ("[DELRATEITEM] rate cache item '$del' removed");
862				$action = "rate cache item '$del' removed";
863			} else {
864				log_info ("[DELRATEITEM] rate cache removal of '$del' failed: item not found");
865				$action = "rate cache removal of '$del' failed: item not found";
866			};
867		} else {
868			log_note ("warning: ignoring unknown command '".substr($request,0,512)."'");
869		};
870		print $client "$action\n";
871		log_info ("answer: '$action'") if wantsdebug (qw[ all ]);
872	};
873};
874
8751; # EOF postfwd3::cache
876
877
878############################
879package postfwd3::server;
880
881use warnings;
882use strict;
883use IO::Socket qw(SOCK_STREAM);
884use Net::DNS;
885use Net::Server::PreFork;
886use Net::Server::Multiplex;
887
888import postfwd3::basic qw(:DEFAULT %postfwd_commands &check_inet &check_unix &wantsdebug &hash_to_str &str_to_hash &hash_to_list &ts &load_hash &save_hash $TIMEHIRES $NETADDR $DIGESTMD5);
889# export these functions for '-C' switch
890use Exporter qw(import);
891our @EXPORT_OK = qw(
892	&read_config &show_config &process_input &get_plugins
893);
894our @ISA = ();
895BEGIN {
896	# use Time::HiRes if available
897	Time::HiRes->import( qw(time) ) if $TIMEHIRES;
898	# use NetAddr::IP if available
899	NetAddr::IP->import( qw(Compact) ) if $NETADDR;
900	# use Digest::MD5 if available
901	Digest::MD5->import( qw(md5_hex md5_base64) ) if $DIGESTMD5;
902};
903
904
905
906# these items have to be compared as...
907# scoring
908my $COMP_SCORES               = "score";
909my $COMP_NS_NAME              = "sender_ns_names";
910my $COMP_NS_ADDR              = "sender_ns_addrs";
911my $COMP_MX_NAME              = "sender_mx_names";
912my $COMP_MX_ADDR              = "sender_mx_addrs";
913my $COMP_HELO_ADDR            = "helo_address";
914# networks in CIDR notation (a.b.c.d/nn)
915my $COMP_NETWORK_CIDRS        = "(client_address|sender_(ns|mx)_addrs|helo_address)";
916# RBL checks
917my $COMP_DNSBL_TEXT           = "dnsbltext";
918my $COMP_RBL_CNT              = "rblcount";
919my $COMP_RHSBL_CNT            = "rhsblcount";
920my $COMP_RBL_KEY              = "rbl";
921my $COMP_RHSBL_KEY            = "rhsbl";
922my $COMP_RHSBL_KEY_CLIENT     = "rhsbl_client";
923my $COMP_RHSBL_KEY_SENDER     = "rhsbl_sender";
924my $COMP_RHSBL_KEY_RCLIENT    = "rhsbl_reverse_client";
925my $COMP_RHSBL_KEY_HELO       = "rhsbl_helo";
926my %DNSBLITEMS = (
927	rbl => {
928		cnt	=> "rblcount",
929	},
930	rhsbl => {
931		cnt	=> "rhsblcount",
932	},
933	rhsbl_client => {
934		cnt	=> "rhsblcount",
935	},
936	rhsbl_sender => {
937		cnt	=> "rhsblcount",
938	},
939	rhsbl_reverse_client => {
940		cnt	=> "rhsblcount",
941	},
942	rhsbl_helo => {
943		cnt	=> "rhsblcount",
944	},
945);
946# dns key value matching
947my %DNS_REPNAMES = (
948	"NS"	=> "nsdname",
949	"MX"	=> "exchange",
950	"A"	=> "address",
951	"TXT"	=> "char_str_list",
952	"CNAME"	=> "cname",
953);
954
955# file items
956our($COMP_CONF_FILE)            = 'cfile|file';
957our($COMP_CONF_TABLE)           = 'ctable|table';
958our($COMP_LIVE_FILE)            = 'lfile';
959our($COMP_LIVE_TABLE)           = 'ltable';
960our($COMP_TABLES)               = qr/^($COMP_CONF_TABLE|$COMP_LIVE_TABLE)$/i;
961our($COMP_CONF_FILE_TABLE)      = qr/^($COMP_CONF_FILE|$COMP_CONF_TABLE):(.+)$/i;
962our($COMP_LIVE_FILE_TABLE)      = qr/^($COMP_LIVE_FILE|$COMP_LIVE_TABLE):(.+)$/i;
963our($COMP_ADDRESS_ITEM)		= qr/^[;=<>~]+([0-9a-fA-F_:\/\.\[\]\-]+)$/;
964# date checks
965my $COMP_DATE                 = "date";
966my $COMP_TIME                 = "time";
967my $COMP_DAYS                 = "days";
968my $COMP_MONTHS               = "months";
969# always true
970my $COMP_ACTION               = "action";
971my $COMP_ACTION_MATCH         = qr/^(\w[\-\w]+)\s*\(\s*(.*?)\s*\)$/;
972my $COMP_ID                   = "id";
973my $COMP_CACHE                = "cache";
974# rule hits
975my $COMP_HITS                 = "request_hits";
976# item match counter
977my $COMP_MATCHES              = "matches";
978# separator
979my $COMP_SEPARATOR            = "[=\~\<\>]=|[\<\>]|[=\!][=\~\<\>]|=";
980# macros
981my $COMP_ACL                  = "[\&][\&]";
982# negation
983my $COMP_NEG                  = "[\!][\!]";
984my $COMP_DENEG                = qr/^$COMP_NEG\s*\(?\s*(.+?)\s*\)?$/;
985# variables
986my $COMP_VAR                  = "[\$][\$]";
987my $COMP_DEVAR1               = qr/(.*)$COMP_VAR\s*(\w+)(.*)/;
988my $COMP_DEVAR2               = qr/(.*)$COMP_VAR\s*\((\w+)\)(.*)/;
989# groups
990my $COMP_GROUP                = "[\%][\%]";
991# date calculations
992my $COMP_DATECALC             = "($COMP_DATE|$COMP_TIME|$COMP_DAYS|$COMP_MONTHS)";
993# these items allow whitespace-or-comma-separated values
994my $COMP_CSV                  = "($COMP_NETWORK_CIDRS|$COMP_RBL_KEY|$COMP_RHSBL_KEY|$COMP_RHSBL_KEY_CLIENT|$COMP_RHSBL_KEY_HELO|$COMP_RHSBL_KEY_SENDER|$COMP_RHSBL_KEY_RCLIENT|$COMP_DATECALC)";
995# dont treat these as lists
996my $COMP_SINGLE               = "($COMP_ID|$COMP_CACHE|$COMP_SCORES|$COMP_RBL_CNT|$COMP_RHSBL_CNT)";
997
998# date tools
999my %months = (
1000	"Jan" =>  0, "jan" =>  0, "JAN" =>  0,
1001	"Feb" =>  1, "feb" =>  1, "FEB" =>  1,
1002	"Mar" =>  2, "mar" =>  2, "MAR" =>  2,
1003	"Apr" =>  3, "apr" =>  3, "APR" =>  3,
1004	"May" =>  4, "may" =>  4, "MAY" =>  4,
1005	"Jun" =>  5, "jun" =>  5, "JUN" =>  5,
1006	"Jul" =>  6, "jul" =>  6, "JUL" =>  6,
1007	"Aug" =>  7, "aug" =>  7, "AUG" =>  7,
1008	"Sep" =>  8, "sep" =>  8, "SEP" =>  8,
1009	"Oct" =>  9, "oct" =>  9, "OCT" =>  9,
1010	"Nov" => 10, "nov" => 10, "NOV" => 10,
1011	"Dec" => 11, "dec" => 11, "DEC" => 11,
1012);
1013my %weekdays = (
1014	"Sun" => 0, "sun" => 0, "SUN" => 0,
1015	"Mon" => 1, "mon" => 1, "MON" => 1,
1016	"Tue" => 2, "tue" => 2, "TUE" => 2,
1017	"Wed" => 3, "wed" => 3, "WED" => 3,
1018	"Thu" => 4, "thu" => 4, "THU" => 4,
1019	"Fri" => 5, "fri" => 5, "FRI" => 5,
1020	"Sat" => 6, "sat" => 6, "SAT" => 6,
1021);
1022
1023use vars qw(
1024	@Rules @DNSBL_Text @Rate_Items
1025	%Rule_by_ID %Matches %ACLs %Timeouts %Hits %Count
1026	%postfwd_items %postfwd_compare %postfwd_actions
1027	%postfwd_items_plugin %postfwd_compare_plugin %postfwd_actions_plugin
1028	%Request_Cache %AutoCacheID %Config_Cache %DNS_Cache %Rate_Cache %Group_Cache
1029	$Cleanup_Requests $Cleanup_RBLs $Cleanup_Rates $Cleanup_Timeouts $Cleanup_Groups
1030	%Cache %Cleanup $StartTime $Summary
1031);
1032
1033
1034## SUBS
1035
1036# cache query
1037sub cache_query { return ( &{$postfwd_settings{cache}{check}}('cache',@_) || '<undef>' ) };
1038
1039# get ip and mask
1040sub cidr_parse {
1041	return unless defined $_[0];
1042	return unless $_[0] =~ m/^(\d+)\.(\d+)\.(\d+)\.(\d+)\/(\d+)$/;
1043	return unless ($1 < 256 and $2 < 256 and $3 < 256 and $4 < 256 and $5 <= 32 and $5 >= 0);
1044	my $net = ($1<<24)+($2<<16)+($3<<8)+$4;
1045	my $mask = ~((1<<(32-$5))-1);
1046	return ($net & $mask, $mask);
1047};
1048
1049# compare address to network
1050sub cidr_match {
1051	my ($net, $mask, $addr) = @_;
1052	return unless defined $net and defined $addr;
1053	$addr =  ($1<<24)+($2<<16)+($3<<8)+$4 if ($addr =~ /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/);
1054	return ($addr & $mask) == $net;
1055};
1056
1057# sets an action for a score
1058sub modify_score {
1059	my($myscore,$myaction) = @_;
1060	log_info ( ((defined $postfwd_settings{scores}{$myscore}) ? "redefined" : "setting new")
1061		." score $myscore with action=\"$myaction\"") if wantsdebug (qw[ all thisrequest verbose ]);
1062	$postfwd_settings{scores}{$myscore} = $myaction;
1063};
1064
1065# returns content of !!() negation
1066sub deneg_item {
1067	my($val) = (defined $_[0]) ? $_[0] : '';
1068	return ( ($val =~ /$COMP_DENEG/) ? $1 : '' );
1069};
1070
1071# resolves $$() variables
1072sub devar_item {
1073	my($cmp,$val,$myitem,$request) = @_;
1074	return '' unless $val and $myitem;
1075	my($pre,$post,$var,$myresult) = '';
1076	while ( ($val =~ /$COMP_DEVAR1/g) or ($val =~ /$COMP_DEVAR2/g) ) {
1077		($pre,$var,$post) = ($1,$2,$3);
1078		if ($var eq $COMP_DNSBL_TEXT) {
1079			$myresult=$val=$pre.(join "; ", uniq(@DNSBL_Text)).$post;
1080		} elsif (defined $request->{$var}) {
1081			$myresult=$val=$pre.$request->{$var}.$post;
1082		};
1083		log_info ("substitute :  \"$myitem\"  \"$cmp\"  \"$val\"") if wantsdebug (qw[ all thisrequest devar ]);
1084	};
1085	return $myresult;
1086};
1087
1088# resolves %%() groups
1089sub degroup_item {
1090	my($now, $groupname) = @_;
1091	my(@myresult) = ();
1092	$groupname =~ s/^$COMP_GROUP//;
1093	return () unless $groupname;
1094	if ($postfwd_settings{group}{noparent}) {
1095		if (defined $Group_Cache{$groupname}) {
1096			foreach my $item (keys %{$Group_Cache{$groupname}}) {
1097				push @myresult, $item if $Group_Cache{$groupname}{$item} >= $now;
1098			};
1099		};
1100	} else  {
1101		my $cmd = "CMD=".$postfwd_commands{groupget}.";TYPE=$groupname";
1102		my $res = cache_query ($cmd);
1103		log_info ("get parent group '".$cmd."' -> '".($res || '<undef>')."'") if wantsdebug (qw[ all thisrequest groups ]);
1104		unless ($res eq '<undef>') {
1105			push @myresult, split $postfwd_settings{sepreq}, $res;
1106			@myresult = uniq(@myresult);
1107		};
1108	};
1109	log_info ("group :  \"$groupname\"  \"".((@myresult) ? (join ',', @myresult) : '<empty>')."\"") if wantsdebug (qw[ all thisrequest groups ]);
1110	return @myresult;
1111};
1112
1113# clean up RBL cache
1114sub cleanup_dns_cache {
1115	my($now) = $_[0]; return unless $now;
1116	foreach my $checkitem (keys %DNS_Cache) {
1117		# remove inclomplete objects (dns timeouts)
1118		unless ( defined($DNS_Cache{$checkitem}{'until'}) and defined($DNS_Cache{$checkitem}{ttl}) ) {
1119			log_info ("[CLEANUP] deleting incomplete dns-cache item '$checkitem'")
1120				if wantsdebug (qw[ all cleanup childcleanup devel ]);
1121			delete $DNS_Cache{$checkitem};
1122		# remove timed out objects
1123		} elsif ( $now > $DNS_Cache{$checkitem}{'until'} ) {
1124			log_info ("[CLEANUP] removing dns-cache item '$checkitem' after ttl ".$DNS_Cache{$checkitem}{ttl}."s")
1125				if wantsdebug (qw[ all cleanup childcleanup ]);
1126			delete $DNS_Cache{$checkitem};
1127		};
1128	};
1129};
1130
1131# clean up request cache
1132sub cleanup_request_cache {
1133	my($now) = $_[0]; return unless $now;
1134	foreach my $checkitem (keys %Request_Cache) {
1135		unless ( defined($Request_Cache{$checkitem}{'until'}) and defined($Request_Cache{$checkitem}{ttl}) ) {
1136			log_info ("[CLEANUP] deleting incomplete request-cache item '$checkitem'")
1137				if wantsdebug (qw[ all cleanup childcleanup devel ]);
1138			delete $Request_Cache{$checkitem};
1139		} elsif ( $now > $Request_Cache{$checkitem}{'until'} ) {
1140			log_info ("[CLEANUP] removing request-cache item '$checkitem' after ttl ".$Request_Cache{$checkitem}{ttl}."s")
1141				if wantsdebug (qw[ all cleanup childcleanup ]);
1142			delete $Request_Cache{$checkitem};
1143		};
1144	};
1145};
1146
1147# clean up group cache
1148sub cleanup_group_cache {
1149	my($now) = $_[0]; return unless $now;
1150	foreach my $groupname (keys %Group_Cache) {
1151		foreach my $groupitem (keys %{$Group_Cache{$groupname}}) {
1152			unless ( $Group_Cache{$groupname}{$groupitem} ) {
1153				log_info ("[CLEANUP] deleting incomplete group-cache item '$groupitem' from group '$groupname'")
1154					if wantsdebug (qw[ all cleanup childcleanup devel groups ]);
1155				delete $Group_Cache{$groupname}{$groupitem};
1156			} elsif ($now > $Group_Cache{$groupname}{$groupitem}) {
1157				log_info ("[CLEANUP] removing expired group-cache item '$groupitem' from group '$groupname'")
1158					if wantsdebug (qw[ all cleanup childcleanup groups ]);
1159				delete $Group_Cache{$groupname}{$groupitem};
1160			};
1161		};
1162		unless ( scalar keys %{$Group_Cache{$groupname}} ) {
1163			log_info ("[CLEANUP] removing empty group '$groupname'")
1164				if wantsdebug (qw[ all cleanup childcleanup groups ]);
1165			delete $Group_Cache{$groupname};
1166		};
1167	};
1168};
1169
1170# clean up rate cache
1171sub cleanup_rate_cache {
1172	my($now) = $_[0]; return unless $now;
1173	foreach my $checkitem (keys %Rate_Cache) {
1174		unless (defined $Rate_Cache{$checkitem}{'list'}) {
1175			log_info ("[CLEANUP] deleting incomplete rate-cache item '$checkitem'")
1176				if wantsdebug (qw[ all cleanup childcleanup devel ]);
1177			delete $Rate_Cache{$checkitem};
1178		} else {
1179			my @i = ();
1180			foreach my $crate (@{$Rate_Cache{$checkitem}{'list'}}) {
1181				if ( not(defined $Rate_Cache{$checkitem}{$crate}{'until'}) or not(defined $Rate_Cache{$checkitem}{$crate}{'ttl'}) ) {
1182					log_info ("[CLEANUP] deleting incomplete rate-cache item '$checkitem'->'$crate'")
1183						if wantsdebug (qw[ all cleanup childcleanup devel ]);
1184					delete $Rate_Cache{$checkitem}{$crate};
1185				} elsif ( $now > $Rate_Cache{$checkitem}{$crate}{'until'} ) {
1186					log_info ("[CLEANUP] removing rate-cache item '$checkitem'->'$crate' after ttl ".$Rate_Cache{$checkitem}{$crate}{ttl}."s")
1187						if wantsdebug (qw[ all cleanup childcleanup ]);
1188					delete $Rate_Cache{$checkitem}{$crate};
1189				} else {
1190					push @i, $crate;
1191				};
1192			};
1193			unless ($i[0]) {
1194				log_info ("[CLEANUP] removing complete rate-cache item '$checkitem'")
1195					if wantsdebug (qw[ all cleanup childcleanup ]);
1196				delete $Rate_Cache{$checkitem};
1197			} else {
1198				log_info ("[CLEANUP] new limits for rate-cache item '$checkitem': ".(join ', ', @i))
1199					if wantsdebug (qw[ all cleanup childcleanup ]);
1200				@{$Rate_Cache{$checkitem}{'list'}} = @i;
1201			};
1202		};
1203	};
1204};
1205
1206# saves rate limits to disk
1207sub save_rates {
1208    cleanup_rate_cache (time());
1209    save_hash (\%Rate_Cache, 'rate', $postfwd_settings{rate}{store});
1210};
1211
1212# loads rate limits from disk
1213sub load_rates {
1214    load_hash (\%Rate_Cache, 'rate', $postfwd_settings{rate}{store});
1215    cleanup_rate_cache (time()) if scalar keys %Rate_Cache;
1216};
1217
1218# saves group cache to disk
1219sub save_groups {
1220    cleanup_group_cache (time());
1221    save_hash (\%Group_Cache, 'group', $postfwd_settings{group}{store});
1222};
1223
1224# loads group cache from disk
1225sub load_groups {
1226    load_hash (\%Group_Cache, 'group', $postfwd_settings{group}{store});
1227    cleanup_group_cache (time()) if scalar keys %Group_Cache;
1228};
1229
1230# return cache contents
1231sub dump_cache {
1232	my @result = ();
1233	my %cache = ();
1234	$cache{Request_Cache} = \%Request_Cache;
1235	$cache{Rate_Cache}    = \%Rate_Cache;
1236	$cache{DNS_Cache}     = \%DNS_Cache;
1237	$cache{Group_Cache}     = \%Group_Cache;
1238	foreach (keys %cache) {
1239		push @result, hash_to_list ('%'.$_, $cache{$_}) if %{$cache{$_}};
1240	}; return @result;
1241};
1242
1243# aggregate ip address items with NetAddr::IP->Compact()
1244sub aggregate_addr {
1245	my($file,$num,$index,$item,@addrs) = @_;
1246	my @keep = my @comp = ();
1247	foreach my $addr (@addrs) {
1248		# disable aggregation for dynamic elements (lfile, negation, ...)
1249		if ( $addr =~ m@$COMP_ADDRESS_ITEM@ ) {
1250			($1) ? push @comp, $1 : push @keep, $addr;
1251		} else {
1252			push @keep, $addr;
1253		};
1254	};
1255	if ( @comp ) {
1256		@comp = Compact( map { $_ = NetAddr::IP->new($_) } @comp );
1257		# the compare operator can be safely overwritten for cidr
1258		# as all of them lead to the same action
1259		map { $_ = "=;$_" } @comp;
1260		push @keep, @comp;
1261		log_note ("notice: Rule $index ($file line $num): aggregated ".$item."-networks : ".(scalar @addrs)." -> ".(scalar @keep)) if wantsdebug (qw[ all thisrequest config aggr cidr ]);
1262	};
1263	return @keep;
1264};
1265
1266# preparses configuration line for ACL syntax
1267sub acl_parser {
1268	my($file,$num,$myline) = @_;
1269	if ( $myline =~ /^\s*($COMP_ACL[\-\w]+)\s*{\s*(.*?)\s*;?\s*}[\s;]*$/ ) {
1270		$ACLs{$1} = $2; $myline = "";
1271	} else {
1272		while ( $myline =~ /($COMP_ACL[\-\w]+)/) {
1273			my($acl)  = $1;
1274			if ( $acl and defined $ACLs{$acl} ) {
1275				$myline =~ s/\s*$acl\s*/$ACLs{$acl}/g;
1276			} else {
1277				log_warn ("file $file, ignoring line $num: undefined macro '$acl'");
1278				return "";
1279			};
1280		};
1281	};
1282	return $myline;
1283};
1284
1285# prepares pcre item
1286sub prepare_pcre {
1287	my($item) = shift; undef my $neg;
1288	# temporarily remove negation
1289	$item = $neg if ($neg = deneg_item($item));
1290	# allow // regex
1291	$item =~ s/^\/?(.*?)\/?$/$1/;
1292	# re-enable negation
1293	$item = "!!($item)" if $neg;
1294	return $item;
1295};
1296
1297# prepares file item
1298sub prepare_file {
1299	my($forced_reload,$type,$cmp,$file) = @_; my(@result) = (); undef my $fh;
1300	my($is_table) = ($type =~ /^$COMP_TABLES$/);
1301	unless (-e $file) {
1302		log_warn ("error: $type:$file not found - will be ignored");
1303		return @result;
1304	};
1305	if ( not($forced_reload) and (defined $Config_Cache{$file}{lastread}) and ($Config_Cache{$file}{lastread} > (stat $file)[9]) ) {
1306		log_info ("$type:$file unchanged - using cached content (mtime: "
1307				.(stat $file)[9].", cache: $Config_Cache{$file}{lastread})")
1308				if wantsdebug (qw[ all thisrequest config ]);
1309		return  @{$Config_Cache{$file}{content}};
1310	};
1311	unless (open ($fh, '<', $file)) {
1312		log_warn ("error: could not open $type:$file - $! - will be ignored");
1313		return @result;
1314	};
1315	log_info ("reading $type:$file") if wantsdebug (qw[ all thisrequest config ]);
1316	while (<$fh>) {
1317		chomp;
1318		s/#.*//g;
1319		next if /^\s*$/;
1320		s/\s+[^\s]+$// if $is_table;
1321		s/^\s+//; s/\s+$//;
1322		push @result, prepare_item($forced_reload, $cmp, $_);
1323	}; close ($fh);
1324        # update Config_Cache
1325        $Config_Cache{$file}{lastread}   = time();
1326        @{$Config_Cache{$file}{content}} = @result;
1327	log_info ("read ".($#result + 1)." items from $type:$file") if wantsdebug (qw[ all thisrequest config ]);
1328	return @result;
1329};
1330
1331# prepares ruleset item
1332sub prepare_item {
1333	my($forced_reload,$cmp,$item) = @_; my(@result) = (); undef my $type;
1334	if ($item =~ /$COMP_CONF_FILE_TABLE/) {
1335		return prepare_file ($forced_reload, $1, $cmp, $2);
1336	} elsif ($cmp eq '=~' or $cmp eq '!~') {
1337		return $cmp.";".prepare_pcre($item);
1338	} else {
1339		return $cmp.";".$item;
1340	};
1341};
1342
1343# compatibility for old "rate"-syntax
1344sub check_for_old_syntax {
1345  my($myindex,$myfile,$mynum,$mykey,$myvalue) = @_;
1346  if ($mykey =~ /^action$/) {
1347    if ($myvalue =~ /^(\w[\-\w]+)\s*\(\s*(.*?)\s*\)$/) {
1348	my($mycmd,$myarg) = ($1, $2);
1349	if ($mycmd =~ /^(rate|size|rcpt)(5321)?$/i) {
1350	  if ($myarg =~ /^\$\$(.*)$/) {
1351	    $myarg = $1;
1352	    $myvalue = "$mycmd($myarg)";
1353	    log_note ( "notice: Rule $myindex ($myfile line $mynum): "
1354	    ."removing obsolete '\$\$' for $mycmd limit index. See man page for new syntax." ) if wantsdebug (qw[ all thisrequest config verbose ]);
1355	  };
1356	  push @Rate_Items, (split '/', $myarg)[0];
1357	};
1358    };
1359  };
1360  return $myvalue;
1361};
1362
1363# parses configuration line
1364sub parse_config_line {
1365	my($forced_reload, $myfile, $mynum, $myindex, $myline) = @_;
1366	my(%myrule) = ();
1367	my($mykey, $myvalue, $mycomp, $prevalert);
1368	eval {
1369	    local $SIG{'__DIE__'};
1370	    local $SIG{'ALRM'}  = sub { $myline =~ s/[ \t][ \t]*/ /g; log_warn ("timeout after ".$postfwd_settings{timeout}{config}."s at parsing Rule $myindex ($myfile line $mynum): \"$myline\""); %myrule = (); die };
1371	    $prevalert = alarm($postfwd_settings{timeout}{config}) if $postfwd_settings{timeout}{config};
1372	    if ( $myline = acl_parser ($myfile, $mynum, $myline) ) {
1373		unless ( $myline =~ /^\s*[^=\s]+\s*$COMP_SEPARATOR\s*([^;\s]+\s*)+(;\s*[^=\s]+\s*$COMP_SEPARATOR\s*([^;\s]+\s*)+)*[;\s]*$/ ) {
1374			log_warn ("ignoring invalid $myfile line ".$mynum.": \"".$myline."\"");
1375		} else {
1376			# separate items
1377			foreach (split ";", $myline) {
1378				# remove whitespaces around
1379				s/^\s*(.*?)\s*($COMP_SEPARATOR)\s*(.*?)\s*$/$1$2$3/;
1380				( ($mycomp = $2) =~ /^([\<\>\~])=$/ ) and $mycomp = "=$1";
1381				($mykey, $myvalue) = split /$COMP_SEPARATOR/, $_, 2;
1382				if ($mykey =~ /^$COMP_SINGLE$/) {
1383					log_note ( "notice: Rule $myindex ($myfile line $mynum):"
1384						." overriding $mykey=\"".$myrule{$mykey}."\""
1385						." with $mykey=\"$myvalue\""
1386						) if (defined $myrule{$mykey});
1387					$myvalue = check_for_old_syntax($myindex,$myfile,$mynum,$mykey,$myvalue);
1388					$myrule{$mykey} = $myvalue;
1389				} elsif ($mykey =~ /^$COMP_CSV$/) {
1390					map { push @{$myrule{$mykey}}, prepare_item ($forced_reload, $mycomp, $_) } ( split /\s*,\s*/, $myvalue );
1391				} elsif ($mykey =~ /^$COMP_ACTION$/) {
1392					push @{$myrule{$mykey}}, $myvalue;
1393				} else {
1394					push @{$myrule{$mykey}}, prepare_item ($forced_reload, $mycomp, $myvalue);
1395				};
1396			};
1397			unless (exists($myrule{$COMP_ACTION})) {
1398				log_warn ("Rule ".$myindex." ($myfile line ".$mynum."): contains no action and will be ignored");
1399				return (%myrule = ());
1400			};
1401			unless (exists($myrule{$COMP_ID})) {
1402				$myrule{$COMP_ID} = "R-".$myindex;
1403				log_note ("notice: Rule $myindex ($myfile line $mynum): contains no rule identifier - will use \"$myrule{id}\"") if wantsdebug (qw[ all thisrequest config verbose ]);
1404			};
1405			map { @{$myrule{$_}} = aggregate_addr($myfile,$mynum,$myindex,$_,@{$myrule{$_}}) if $_ =~ /$COMP_NETWORK_CIDRS/ } (keys %myrule) if ($NETADDR and $postfwd_settings{aggregate_addrs});
1406			log_info ("loaded: Rule $myindex ($myfile line $mynum): id->\"$myrule{id}\" action->\"".(join ' || ', @{$myrule{action}})."\"") if wantsdebug (qw[ all thisrequest config verbose ]);
1407		};
1408	    };
1409	    alarm($prevalert) if $postfwd_settings{timeout}{config};
1410	};
1411	return %myrule;
1412};
1413
1414# parses configuration file
1415sub read_config_file {
1416	my($forced_reload, $myindex, $myfile) = @_;
1417	my(%myrule, @myruleset, @lines) = ();
1418	my($mybuffer) = ""; undef my $fh;
1419
1420	unless (-e $myfile) {
1421		log_warn ("error: file ".$myfile." not found - file will be ignored");
1422	} else {
1423		unless (open ($fh, '<', $myfile)) {
1424			log_warn ("error: could not open ".$myfile." - $! - file will be ignored");
1425		} else {
1426			log_info ("reading file $myfile") if wantsdebug (qw[ all thisrequest config verbose ]);
1427			while (<$fh>) {
1428				chomp;
1429				s/(\"|#.*)//g;
1430				next if /^\s*$/;
1431				if ( /(.*)\\\s*$/ or /(.*\{)\s*$/ ) { $mybuffer = $mybuffer.$1; next; };
1432				$mybuffer .= $_;
1433				if ( $lines[0] and $mybuffer =~ /^(\}|\s+\S)/ ) {
1434					my $last = pop(@lines); $last .= ';' unless $last =~ /;\s*$/;
1435					$mybuffer = $last.$mybuffer;
1436				};
1437				push @lines, $mybuffer;
1438				$mybuffer = "";
1439			};
1440			map {
1441				log_info ("parsing line: '$_'") if wantsdebug (qw[ all thisrequest config ]);
1442				%myrule = parse_config_line ($forced_reload, $myfile, $., ($#myruleset+$myindex+1), $mybuffer.$_);
1443				push ( @myruleset, { %myrule } ) if (%myrule);
1444				$mybuffer = "";
1445			} @lines;
1446			close ($fh);
1447			log_info ("loaded: Rules $myindex - ".($myindex + $#myruleset)." from file \"$myfile\"") if wantsdebug (qw[ all thisrequest config verbose ]);
1448		};
1449	};
1450	return @myruleset;
1451};
1452
1453# reads all configuration items
1454sub read_config {
1455	my($forced_reload) = shift;
1456	my(%myrule, @myruleset) = ();
1457	my($mytype,$myitem);
1458
1459	# init, cleanup cache and config vars
1460	@Rules = (); %Rule_by_ID = %Request_Cache = (); @Rate_Items = ();
1461	%Rate_Cache = () unless $postfwd_settings{keep_rates};
1462	%Group_Cache = () unless $postfwd_settings{keep_groups};
1463
1464	# parse configurations
1465	for my $config (@{$postfwd_settings{Configs}}) {
1466		($mytype,$myitem) = split $postfwd_settings{sepreq}, $config;
1467		if ($mytype eq "r" or $mytype eq "rule") {
1468			%myrule = parse_config_line ($forced_reload, 'RULE', 0, ($#Rules + 1), $myitem);
1469			push ( @Rules, { %myrule } ) if (%myrule);
1470		} elsif ($mytype eq "f" or $mytype eq "file") {
1471			if ( not($forced_reload) and defined $Config_Cache{$myitem}{lastread} and ($Config_Cache{$myitem}{lastread} > (stat $myitem)[9]) ) {
1472				log_info ("file \"$myitem\" unchanged - using cached ruleset (mtime: ".(stat $myitem)[9].",
1473					cache: $Config_Cache{$myitem}{lastread})"
1474					) if wantsdebug (qw[ all thisrequest config verbose ]);
1475				push ( @Rules, @{$Config_Cache{$myitem}{ruleset}} ) if $Config_Cache{$myitem}{ruleset};
1476			} else {
1477				@myruleset = read_config_file ($forced_reload, ($#Rules + 1), $myitem);
1478				if (@myruleset) {
1479					@Rules = ( @Rules, @myruleset ) if @myruleset;
1480					$Config_Cache{$myitem}{lastread} = time();
1481					@{$Config_Cache{$myitem}{ruleset}} = @myruleset;
1482				};
1483			};
1484		};
1485	};
1486	if ($#Rules < 0) {
1487		log_warn("critical: no rules found - i feel useless (have you set -f or -r?)");
1488	} else {
1489		# update Rule by ID hash
1490		map { $Rule_by_ID{$Rules[$_]{$COMP_ID}} = $_ } (0 .. $#Rules);
1491		if ($postfwd_settings{request}{autocacheid}) {
1492			# update AutoCacheID hash
1493			my @myautocacheid = ();
1494			map { $Rule_by_ID{$Rules[$_]{$COMP_ID}} = $_; push @myautocacheid, keys %{$Rules[$_]} } (0 .. $#Rules);
1495			map { $AutoCacheID{lc($_)} = 1 } (grep !/^($COMP_ID|$COMP_ACTION)$/, uniq(@myautocacheid));
1496			if (%AutoCacheID) {
1497				log_info ("Setting AutoCacheID to '".(join ',', (sort keys %AutoCacheID))."'") if wantsdebug(qw[ all verbose thisrequest config cache ]);
1498			} else {
1499				log_info ("No items found in ruleset. Disabling AutoCacheID") if wantsdebug(qw[ all verbose thisrequest config cache ]);
1500			};
1501		};
1502		if ( @Rate_Items ) {
1503			@Rate_Items = uniq(@Rate_Items);
1504			log_info ("rate items: ".(join ', ', @Rate_Items)) if wantsdebug (qw[ all thisrequest verbose rates ]);
1505			# disable request cache with ratelimits
1506			$postfwd_settings{request}{ttl} = 0;
1507			log_note ("disabling request cache due to ratelimits in configuration");
1508		};
1509	};
1510};
1511
1512# displays configuration
1513sub show_config {
1514	if (wantsdebug (qw[ all verbose ])) {
1515		print STDOUT "=" x 75, "\n";
1516		printf STDOUT "Rule count: %s\n", ($#Rules + 1);
1517		print STDOUT "=" x 75, "\n";
1518	};
1519	for my $index (0 .. $#Rules) {
1520		next unless exists $Rules[$index];
1521		printf STDOUT "Rule %3d: id->\"%s\"; action->\"%s\"", $index, $Rules[$index]{$COMP_ID}, join(' || ', @{$Rules[$index]{$COMP_ACTION}});
1522		my $line = (wantsdebug (qw[ all verbose ])) ? "\n\t  " : "";
1523		for my $mykey ( reverse sort keys %{$Rules[$index]} ) {
1524			unless (($mykey eq $COMP_ACTION) or ($mykey eq $COMP_ID)) {
1525				$line .= "; " unless wantsdebug (qw[ all verbose ]);
1526				$line .= ($mykey =~ /^$COMP_SINGLE$/)
1527					? $mykey."->\"".$Rules[$index]{$mykey}."\""
1528					: $mykey."->\"".(join ', ', @{$Rules[$index]{$mykey}})."\"";
1529				$line .= " ; " if wantsdebug (qw[ all verbose ]);
1530			};
1531		};
1532		$line =~ s/\s*\;\s*$// if wantsdebug (qw[ all verbose ]);
1533		printf STDOUT "%s\n", $line;
1534		print STDOUT "-" x 75, "\n" if wantsdebug (qw[ all verbose ]);
1535	};
1536};
1537
1538
1539## sub DNS
1540
1541# checks for rbl timeouts
1542sub rbl_timeout {
1543    my($myrbl) = shift;
1544    return ( ($postfwd_settings{dns}{max_timeout} > 0) and (defined $Timeouts{$myrbl}) and ($Timeouts{$myrbl} > $postfwd_settings{dns}{max_timeout}) );
1545};
1546
1547# reads DNS answers
1548sub rbl_read_dns {
1549    my($myresult)		= shift;
1550    my($now)			= time();
1551    my($que,$ttl,$res,$typ)	= undef;
1552    my(@addrs,@texts)		= ();
1553
1554    if ( defined $myresult ) {
1555	# read question, for dns cache id
1556	foreach ($myresult->question) {
1557		$typ = ($_->qtype || ''); $que = ($_->qname || '');
1558		map { log_info ("[GETDNS00] type=$typ, query=$que, $_") } (hash_to_list ('%packet', $myresult))
1559			if wantsdebug (qw[ all thisrequest dns getdns getdnspacket ]);
1560		next unless ($typ and $que);
1561		log_info ("[GETDNS01] type=$typ, query=$que") if wantsdebug (qw[ all thisrequest dns getdns ]);
1562		unless ( (defined $DNS_Cache{$que})
1563			and (($typ eq 'A') or ($typ eq 'TXT')) ) {
1564			log_note ("[DNSBL] ignoring unknown query '$que', type '$typ'");
1565			next;
1566		};
1567
1568		# parse answers
1569		foreach ($myresult->answer) {
1570			log_info ("[GETDNS02] type=$typ, query=$que, restype='".$_->type."'") if wantsdebug (qw[ all thisrequest dns getdns ]);
1571			if ($_->type eq 'A') {
1572				push @addrs, $_->address if $_->address;
1573				$ttl = $_->ttl;
1574				log_info ("[GETDNSA1] type=$typ, query=$que, ttl=$ttl, answer='".($_->address || '')."'") if wantsdebug (qw[ all thisrequest dns getdns ]);
1575			} elsif ($_->type eq 'TXT') {
1576				$res = (join(" ", $_->char_str_list()) || '');
1577				# escape commas for set() action
1578				$res =~ s/,/ /g;
1579				push @texts, $res;
1580				$ttl = $_->ttl;
1581				log_info ("[GETDNST1] type=$typ, query=$que, ttl=$ttl, answer='$res'") if wantsdebug (qw[ all thisrequest dns getdns ]);
1582			} elsif (wantsdebug (qw[ all thisrequest dns getdns ])) {
1583				log_info ("[GETDNS??] received answer type=".$typ." for query $que");
1584			};
1585		};
1586
1587		# save result in cache
1588		if ($typ eq 'A') {
1589			$ttl = ( $DNS_Cache{$que}{ttl} > ($ttl||=0) ) ? $DNS_Cache{$que}{ttl} : $ttl;
1590			@{$DNS_Cache{$que}{A}}	  = @addrs;
1591			$DNS_Cache{$que}{ttl}	  = $ttl;
1592			$DNS_Cache{$que}{delay}   = ($now - $DNS_Cache{$que}{delay});
1593			$DNS_Cache{$que}{'log'}	  = 1;
1594			$DNS_Cache{$que}{'until'} = $now + $DNS_Cache{$que}{ttl};
1595			log_info ("[GETDNSA2] type=$typ, query=$que, cache='".(hash_to_str($DNS_Cache{$que}))."'") if wantsdebug (qw[ all thisrequest dns getdns ]);
1596		#} elsif ($typ eq 'TXT') {
1597		} else {
1598			$res = (join(" ", @texts) || '');
1599			$ttl = ( $DNS_Cache{$que}{ttl} > ($ttl||=0) ) ? $DNS_Cache{$que}{ttl} : $ttl;
1600			$DNS_Cache{$que}{TXT} = $res;
1601			$DNS_Cache{$que}{ttl}  = $ttl unless $DNS_Cache{$que}{ttl};
1602			log_info ("[GETDNST2] type=$typ, query=$que, cache='".(hash_to_str($DNS_Cache{$que}))."'") if wantsdebug (qw[ all thisrequest dns getdns ]);
1603		};
1604	};
1605	return $que if (@addrs || $res);
1606    } else {
1607	log_note ("[DNSBL] dns timeout");
1608    };
1609};
1610
1611# fires DNS queries
1612sub rbl_prepare_lookups {
1613    my($mytype, $myval, @myrbls) = @_;
1614    my($myresult) = undef;
1615    my($cmp,$rblitem,$myquery);
1616    my(@lookups) = ();
1617
1618    # skip these
1619    return @lookups if not(defined $myval) or ($myval eq '') or ($myval eq "unknown") or ($myval =~ /:/);
1620
1621    # removes duplicate lookups, but keeps the specified order
1622    @myrbls = uniq(@myrbls);
1623
1624    RBLQUERY: foreach (@myrbls) {
1625
1626	# separate rbl-name and answer
1627	($cmp,$rblitem) = split ";", $_;
1628	next RBLQUERY unless $rblitem;
1629	my($myrbl, $myrblans, $myrbltime) = split /\//, $rblitem;
1630	next RBLQUERY unless $myrbl;
1631	next RBLQUERY if rbl_timeout($myrbl);
1632	$myrblans = $postfwd_settings{dns}{mask} unless $myrblans;
1633	$myrbltime = $postfwd_settings{dns}{ttl} unless $myrbltime;
1634
1635	# create query string
1636	$myquery = $myval.".".$myrbl;
1637	my $mypat = qr/$myrblans/;
1638
1639	# query our cache
1640	if ( exists($DNS_Cache{$myquery}) and exists($DNS_Cache{$myquery}{A}) ) {
1641		ANSWER1: foreach (@{$DNS_Cache{$myquery}{A}}) { last ANSWER1 if $myresult = ( $_ =~ /$mypat/ ) };
1642		log_info ("[DNSBL] cached $mytype: $myrbl $myval ($myquery) - answer: \'".(join ", ", @{$DNS_Cache{$myquery}{A}})."\'")
1643			if ( wantsdebug (qw[ all thisrequest ]) or ($myresult and wantsdebug (qw[ verbose ])) );
1644
1645	# query parent cache
1646	} elsif (   not($postfwd_settings{dns}{noparent})
1647		and not((my $pans = cache_query ("CMD=".$postfwd_commands{getcacheitem}.";TYPE=dns;ITEM=$myquery")) eq '<undef>') ) {
1648		%{$DNS_Cache{$myquery}} = str_to_hash(\$pans); delete $DNS_Cache{$myquery}{'log'} if $DNS_Cache{$myquery}{'log'};
1649		if ($DNS_Cache{$myquery}{A}) {
1650			ref $DNS_Cache{$myquery}{A} eq 'ARRAY' or $DNS_Cache{$myquery}{A} = [ $DNS_Cache{$myquery}{A} ];
1651			ANSWER2: foreach (@{$DNS_Cache{$myquery}{A}}) { last ANSWER2 if $myresult = ( $_ =~ /$mypat/ ) };
1652			log_info ("[DNSBL] parent cached $mytype: $myrbl $myval ($myquery) - answer: \'".(join ", ", @{$DNS_Cache{$myquery}{A}})."\'")
1653				if ( wantsdebug (qw[ all thisrequest ]) or ($myresult and wantsdebug (qw[ verbose ])) );
1654		};
1655
1656	# not found -> prepare dns query
1657	} else {
1658		$DNS_Cache{$myquery} = {
1659			type		=> $mytype,
1660			name		=> $myrbl,
1661			value		=> $myval,
1662			ttl		=> $myrbltime,
1663			delay		=> time(),
1664		};
1665		log_info("[DNSBL] query $mytype:  $myrbl $myval ($myquery)") if wantsdebug (qw[ all thisrequest ]);
1666		push @lookups, $myquery;
1667	};
1668    };
1669    # return necessary lookups
1670    return @lookups;
1671};
1672
1673# checks RBL items
1674sub rbl_check {
1675    my($mytype,$myrbl,$myval) = @_;
1676    my($myanswer,$myrblans,$myrbltime,$myresult,$mystart,$myend);
1677    my($m1,$m2,$myrbltype,$m4,$myrbltxt,$myquery);
1678    my($now) = time();
1679
1680    # skip these
1681    return $myresult if not(defined $myval) or ($myval eq '') or ($myval eq "unknown") or ($myval =~ /:/);
1682
1683    # separate rbl-name and answer
1684    ($myrbl, $myrblans, $myrbltime) = split '/', $myrbl;
1685    $myrblans = $postfwd_settings{dns}{mask} unless $myrblans;
1686    $myrbltime = $postfwd_settings{dns}{ttl} unless $myrbltime;
1687
1688    # create query string
1689    $myquery = $myval.".".$myrbl;
1690
1691    # query our cache
1692    return $myresult unless ( $myresult = (defined $DNS_Cache{$myquery} and not(defined $DNS_Cache{$myquery}{'timed'})) );
1693    if (not($postfwd_settings{dns}{noparent}) and defined $DNS_Cache{$myquery}{'log'}) {
1694	my $pdns = "CMD=".$postfwd_commands{setcacheitem}.";TYPE=dns;ITEM=$myquery".hash_to_str($DNS_Cache{$myquery});
1695	cache_query ($pdns);
1696    };
1697    if ( $myresult  = ($#{$DNS_Cache{$myquery}{A}} >= 0) ) {
1698	my $mypat = qr/$myrblans/;
1699	ANSWER: foreach (@{$DNS_Cache{$myquery}{A}}) {
1700		last ANSWER if ( $myresult = ( ($_) and ($_ =~ m/$mypat/)) );
1701	};
1702	push @DNSBL_Text, $DNS_Cache{$myquery}{type}.':'.$DNS_Cache{$myquery}{name}.':<'.($DNS_Cache{$myquery}{TXT} || '').'>'
1703		if $myresult and defined $DNS_Cache{$myquery}{type} and defined $DNS_Cache{$myquery}{name};
1704	if ( wantsdebug (qw[ all thisrequest verbose ]) or $postfwd_settings{dns}{anylog}
1705		or ($myresult and not($postfwd_settings{dns}{nolog}) and defined $DNS_Cache{$myquery}{'log'}) ) {
1706		log_info ("[DNSBL] ".( ($mytype eq $COMP_RBL_KEY) ? join('.', reverse(split(/\./,$myval))) : $myval )." listed on "
1707			.lc(($DNS_Cache{$myquery}{type} || $mytype)).":$myrbl (answer: ".(join ", ", @{$DNS_Cache{$myquery}{A}})
1708			.", time: ".ts($DNS_Cache{$myquery}{delay})."s, ttl: ".$DNS_Cache{$myquery}{ttl}."s, '".($DNS_Cache{$myquery}{TXT} || '')."')");
1709		delete $DNS_Cache{$myquery}{'log'} if defined $DNS_Cache{$myquery}{'log'};
1710	};
1711    };
1712    return $myresult;
1713};
1714
1715# dns resolver wrapper
1716sub dns_query {
1717    my (@queries) = @_; undef my @result;
1718    eval {
1719        local $SIG{__DIE__} = sub { log_note ("[DNS] ERROR: \"$!\", DETAIL: \"@_\""); return if $^S; };
1720        @result = dns_query_net_dns(@queries);
1721    };
1722    return @result;
1723};
1724
1725# resolves dns queries using Net::DNS
1726sub dns_query_net_dns {
1727    my (@queries) = @_; undef my @result; undef my $pans;
1728    my %ownsock  = (); my @ownready = (); undef my $bgsock;
1729    my $ownsel   = IO::Select->new();
1730    my $dns = Net::DNS::Resolver->new(
1731	tcp_timeout => $postfwd_settings{dns}{timeout},
1732	udp_timeout => $postfwd_settings{dns}{timeout},
1733	persistent_tcp => 0, persistent_udp => 0,
1734	retrans => 0, retry => 1, dnsrch => 0, defnames => 0,
1735    );
1736    my $now = time();
1737    # prepare queries
1738    foreach (@queries) {
1739	my ($item, $type) = split ','; $type ||= 'A';
1740	# query child cache
1741	if ( (defined $DNS_Cache{$item}{$type}) and (defined $DNS_Cache{$item}{'until'}) and ($DNS_Cache{$item}{'until'} >= $now) ) {
1742	    $DNS_Cache{$item}{$type} = [ $DNS_Cache{$item}{$type} ] unless (ref $DNS_Cache{$item}{$type} eq 'ARRAY');
1743	    log_info ("[DNS] dnsccache: item=$item, type=$type -> ".(join ',', @{$DNS_Cache{$item}{$type}})." (ttl: ".($DNS_Cache{$item}{ttl} || 0).")")
1744		if ($postfwd_settings{dns}{anylog} or wantsdebug (qw[ all thisrequest dns getdns ]));
1745	    push @result, @{$DNS_Cache{$item}{$type}};
1746	# query parent cache
1747	} elsif (   not($postfwd_settings{dns}{noparent})
1748	    and not(($pans = cache_query ("CMD=".$postfwd_commands{getcacheitem}.";TYPE=dns;ITEM=$item")) eq '<undef>')
1749	    and (%{$DNS_Cache{$item}} = str_to_hash(\$pans))
1750	    and (defined $DNS_Cache{$item}{$type}) and (defined $DNS_Cache{$item}{'until'}) and ($DNS_Cache{$item}{'until'} >= $now) ) {
1751	    $DNS_Cache{$item}{$type} = [ $DNS_Cache{$item}{$type} ] unless (ref $DNS_Cache{$item}{$type} eq 'ARRAY');
1752	    log_info ("[DNS] dnspcache: item=$item, type=$type -> ".(join ',', @{$DNS_Cache{$item}{$type}})." (ttl: ".($DNS_Cache{$item}{ttl} || 0).")")
1753		if ($postfwd_settings{dns}{anylog} or wantsdebug (qw[ all thisrequest dns getdns ]));
1754	    push @result, @{$DNS_Cache{$item}{$type}};
1755	# send queries
1756	} else {
1757	    log_info ("[DNS] dnsquery: item=$item, type=$type")
1758		if ($postfwd_settings{dns}{anylog} or wantsdebug (qw[ all thisrequest dns getdns ]));
1759	    $DNS_Cache{$item}{delay} = $now;
1760	    $bgsock = $dns->bgsend ($item, $type);
1761	    $ownsel->add($bgsock);
1762	    $ownsock{$bgsock} = $item.','.$type;
1763	};
1764    };
1765    # retrieve answers
1766    while ((scalar keys %ownsock) and (@ownready = $ownsel->can_read($postfwd_settings{dns}{timeout}))) {
1767	foreach my $sock (@ownready) {
1768	    if (defined $ownsock{$sock}) {
1769		my $packet = $dns->bgread($sock);
1770		my ($item, $type) = split ',', $ownsock{$sock};
1771		my $rname = $DNS_REPNAMES{$type};
1772		my @rrs = (grep { $_->type eq $type } $packet->answer);
1773		$now = time(); my $ttl = 0; my @ans = ();
1774		if (@rrs) {
1775		    # sort MX records by preference
1776		    @rrs = sort { $a->preference <=> $b->preference } @rrs if ($type eq 'MX');
1777		    foreach my $rr (@rrs) {
1778			$ttl = $rr->ttl if ($rr->ttl > $ttl);
1779			log_info ("[DNS] dnsanswer: item=$item, type=$type -> $rname=".$rr->$rname." (ttl: $ttl)")
1780			    if ($postfwd_settings{dns}{anylog} or wantsdebug (qw[ all thisrequest dns setdns ]));
1781			push @ans, $rr->$rname;
1782		    };
1783		    push @result, @ans;
1784		};
1785		# add to dns cache
1786		$ttl ||= $postfwd_settings{dns}{ttl};
1787		@{$DNS_Cache{$item}{$type}} = @ans;
1788		$DNS_Cache{$item}{ttl} = $ttl;
1789		$DNS_Cache{$item}{'until'} = $now + $ttl;
1790		$DNS_Cache{$item}{delay} = ($DNS_Cache{delay}) ? $now - $DNS_Cache{delay} : 0;
1791		cache_query ( "CMD=".$postfwd_commands{setcacheitem}.";TYPE=dns;ITEM=$item".hash_to_str($DNS_Cache{$item}) )
1792		    unless ($postfwd_settings{dns}{noparent});
1793		$DNS_Cache{$item}{'log'} = 1;
1794		log_info ("[DNS] dnsanswers: item=$item, type=$type -> $rname=".((@{$DNS_Cache{$item}{$type}}) ? join ',', @{$DNS_Cache{$item}{$type}} : '')." (delay: ".ts($DNS_Cache{$item}{delay}).", ttl: $ttl)")
1795		    if ($postfwd_settings{dns}{anylog} or wantsdebug (qw[ all thisrequest verbose dns setdns ]));
1796		delete $ownsock{$sock};
1797	    } else {
1798		$ownsel->remove($sock);
1799		$sock = undef;
1800	    };
1801	};
1802    };
1803    # show timeouts
1804    map { log_note ("dnsquery: timeout for $_ after ".$postfwd_settings{dns}{timeout}." seconds") } (values %ownsock);
1805    return @result;
1806};
1807
1808
1809## SUB plugins
1810
1811#
1812# these subroutines integrate additional attributes to
1813# a request before the ruleset is evaluated
1814# call: %result = postfwd_items{foo}(%request)
1815# save: $result{$_}
1816#
1817%postfwd_items = (
1818	"__builtin__" => sub {
1819		my($request) = shift;
1820		# postfwd version
1821		$request->{version} = $postfwd_settings{name}." ".$postfwd_settings{version};
1822		# postfwd server interface
1823		$request->{postfwd_interface} = $postfwd_settings{server}{interface};
1824		# postfwd server port
1825		$request->{postfwd_port} = $postfwd_settings{server}{port};
1826		# sender info
1827		$request->{sender} =~ /(.*)@([^@]*)$/;
1828		( $request->{sender_localpart}, $request->{sender_domain} ) = ( $1, $2 );
1829		# recipient info
1830		$request->{recipient} =~ /(.*)@([^@]*)$/;
1831		( $request->{recipient_localpart}, $request->{recipient_domain} ) = ( $1, $2 );
1832		# reverted ip address (for lookups)
1833		if ( $request->{client_address} =~ /\./ ) {
1834			# IPv4
1835			$request->{reverse_address} = join(".", reverse(split(/\./,$request->{client_address})));
1836		} elsif ( $request->{client_address} =~ /:/ ) {
1837			# IPv6
1838			$request->{reverse_address} = join('.', (reverse split '', (join '', (map { $_ = sprintf "%04s", $_ } (split /:/, $request->{client_address})))))
1839				if ($postfwd_settings{dns}{ipv6_dnsbl});
1840		};
1841	},
1842	"sender_dns" => sub {
1843		my($request) = @_;
1844		map { $request->{$_} = $request->{sender_domain} } ($COMP_NS_NAME, $COMP_NS_ADDR, $COMP_MX_NAME, $COMP_MX_ADDR);
1845		$request->{$COMP_HELO_ADDR} = $request->{helo_name};
1846	},
1847);
1848# returns additional request information
1849# for all postfwd_items
1850sub postfwd_items {
1851    my($request) = shift;
1852    foreach (sort keys %postfwd_items) {
1853	log_info ("[PLUGIN] executing postfwd-item ".$_)
1854		if wantsdebug (qw[ all thisrequest ]);
1855	&{$postfwd_items{$_}}($request) if (defined $postfwd_items{$_});
1856    };
1857    map { $request->{$_} = '' unless (defined $request->{$_}); log_info ("[PLUGIN]  ATTR: $_=$request->{$_}") if wantsdebug (qw[ all thisrequest ]) } (keys %{$request});
1858};
1859#
1860# compare item subroutines
1861# must take compare_item_foo ( $COMPARE_TYPE, $RULEITEM, $REQUESTITEM, $REQUEST );
1862#
1863%postfwd_compare = (
1864	# old version: cidr only for v4, v6 as regex
1865	"cidr_postfwd" => sub {
1866		my($cmp,$val,$myitem,$request) = @_;
1867		my($myresult) = ($val and $myitem);
1868		log_info ("type cidr_postfwd :  \"$myitem\"  \"$cmp\"  \"$val\"") if wantsdebug (qw[ all thisrequest ]);
1869		if ($myresult) {
1870			# always true
1871			$myresult = ($val eq '0.0.0.0/0');
1872			unless ($myresult) {
1873				# v4 addresses only
1874				$myresult = ($myitem =~ /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/);
1875				if ($myresult) {
1876					$val .= '/32' unless ($val =~ /\/\d{1,2}$/);
1877					$myresult = cidr_match((cidr_parse($val)),$myitem);
1878				} else {
1879					log_info ("Non IPv4 address. Using type default") if wantsdebug (qw[ all thisrequest ]);
1880					return &{$postfwd_compare{default}}($cmp,$val,$myitem,$request);
1881				};
1882			};
1883		};
1884		$myresult = not($myresult) if ($cmp eq '!=');
1885		return $myresult;
1886	},
1887	# new version: based on NetAddr::IP
1888	"cidr_netaddr" => sub {
1889		my($cmp,$val,$myitem,$request) = @_;
1890		my($myresult) = ($val and $myitem);
1891		log_info ("type cidr_netaddr :  \"$myitem\"  \"$cmp\"  \"$val\"") if wantsdebug (qw[ all thisrequest ]);
1892		if ($myresult) {
1893			$myresult = 0;
1894			# compare with NetAddr::IP->within(), unfortunately code dies on unknown value
1895			eval { local $SIG{__DIE__} = sub{ log_warn("NetAddr::IP error comparing '$val' to '$myitem'") }; $myresult = NetAddr::IP->new($myitem)->within(NetAddr::IP->new($val)) };
1896		};
1897		$myresult = not($myresult) if ($cmp eq '!=');
1898		return $myresult;
1899	},
1900	# new version: based on Net::CIDR::Lite
1901	"cidr_netcidr" => sub {
1902		my($cmp,$val,$myitem,$request) = @_;
1903		my($myresult) = ($val and $myitem);
1904		log_info ("type cidr_netcidr :  \"$myitem\"  \"$cmp\"  \"$val\"") if wantsdebug (qw[ all thisrequest ]);
1905		if ($myresult) {
1906			$myresult = 0;
1907			# compare with Net::CIDR::Lite->find(), unfortunately code dies on unknown value
1908			eval { local $SIG{__DIE__} = sub{ log_warn("Net::CIDR::Lite error comparing '$val' to '$myitem'") }; $myresult = Net::CIDR::Lite->new($val)->find($myitem) };
1909		};
1910		$myresult = not($myresult) if ($cmp eq '!=');
1911		return $myresult;
1912	},
1913	"numeric" => sub {
1914		my($cmp,$val,$myitem,$request) = @_;
1915		my($myresult) = undef;
1916		log_info ("type numeric :  \"$myitem\"  \"$cmp\"  \"$val\"") if wantsdebug (qw[ all thisrequest ]);
1917		$myitem ||= "0"; $val ||= "0";
1918                if ($cmp eq '==') {
1919                        $myresult = ($myitem == $val);
1920                } elsif ($cmp eq '=<') {
1921                        $myresult = ($myitem <= $val);
1922                } elsif ($cmp eq '=>') {
1923                        $myresult = ($myitem >= $val);
1924                } elsif ($cmp eq '<') {
1925                        $myresult = ($myitem < $val);
1926                } elsif ($cmp eq '>') {
1927                        $myresult = ($myitem > $val);
1928                } elsif ($cmp eq '!=') {
1929                        $myresult = not($myitem == $val);
1930                } elsif ($cmp eq '!<') {
1931                        $myresult = not($myitem <= $val);
1932                } elsif ($cmp eq '!>') {
1933                        $myresult = not($myitem >= $val);
1934		} else {
1935			$myresult = ($myitem >= $val);
1936		};
1937		return $myresult;
1938	},
1939	$COMP_RBL_KEY => sub {
1940		my($cmp,$val,$myitem,$request) = @_;
1941		my($myresult) = not($postfwd_settings{dns}{disabled});
1942		log_info ("type rbl :  \"$myitem\"  \"$cmp\"  \"$val\"") if wantsdebug (qw[ all thisrequest ]);
1943		$myresult = ( rbl_check ($COMP_RBL_KEY, $val, $myitem) ) if $myresult;
1944		$myresult = not($myresult) if ($cmp eq '!=');
1945		return $myresult;
1946	},
1947	$COMP_RHSBL_KEY => sub {
1948		my($cmp,$val,$myitem,$request) = @_;
1949		my($myresult) = not($postfwd_settings{dns}{disabled});
1950		log_info ("type rhsbl :  \"$myitem\"  \"$cmp\"  \"$val\"") if wantsdebug (qw[ all thisrequest ]);
1951		$myresult = ( rbl_check ($COMP_RHSBL_KEY, $val, $myitem) ) if $myresult;
1952		$myresult = not($myresult) if ($cmp eq '!=');
1953		return $myresult;
1954	},
1955	$COMP_MONTHS => sub {
1956		my($cmp,$val,$myitem,$request) = @_;
1957		my($myresult) = undef;
1958		my($imon) = (split (',', $myitem))[4]; $imon ||= 0;
1959		my($rmin,$rmax) = split (/\s*-\s*/, $val);
1960		$rmin = ($rmin) ? (($rmin =~ /^\d$/) ? $rmin : $months{$rmin}) : $imon;
1961		$rmax = ($rmax) ? (($rmax =~ /^\d$/) ? $rmax : $months{$rmax}) : (($val =~ /-/) ? $imon : $rmin);
1962		log_info ("type months :  \"$imon\"  \"$cmp\"  \"$rmin\"-\"$rmax\"")
1963			if wantsdebug (qw[ all thisrequest ]);
1964		$myresult = (($rmin <= $imon) and ($rmax >= $imon));
1965		$myresult = not($myresult) if ($cmp eq '!=');
1966		return $myresult;
1967	},
1968	$COMP_DAYS => sub {
1969		my($cmp,$val,$myitem,$request) = @_;
1970		my($myresult) = undef;
1971		my($iday) = (split (',', $myitem))[6]; $iday ||= 0;
1972		my($rmin,$rmax) = split (/\s*-\s*/, $val);
1973		$rmin = ($rmin) ? (($rmin =~ /^\d$/) ? $rmin : $weekdays{$rmin}) : $iday;
1974		$rmax = ($rmax) ? (($rmax =~ /^\d$/) ? $rmax : $weekdays{$rmax}) : (($val =~ /-/) ? $iday : $rmin);
1975		log_info ("type days :  \"$iday\"  \"$cmp\"  \"$rmin\"-\"$rmax\"")
1976			if wantsdebug (qw[ all thisrequest ]);
1977		$myresult = (($rmin <= $iday) and ($rmax >= $iday));
1978		$myresult = not($myresult) if ($cmp eq '!=');
1979		return $myresult;
1980	},
1981	$COMP_DATE => sub {
1982		my($cmp,$val,$myitem,$request) = @_;
1983		my($myresult) = undef;
1984		my($isec,$imin,$ihour,$iday,$imon,$iyear) = split (',', $myitem);
1985		my($rmin,$rmax) = split (/\s*-\s*/, $val);
1986		my($idat) = ($iyear + 1900) . ((($imon+1) < 10) ? '0'.($imon+1) : ($imon+1)) . (($iday < 10) ? '0'.$iday : $iday);
1987		$rmin = ($rmin) ? join ('', reverse split ('\.', $rmin)) : $idat;
1988		$rmax = ($rmax) ? join ('', reverse split ('\.', $rmax)) : (($val =~ /-/) ? $idat : $rmin);
1989		log_info ("type date :  \"$idat\"  \"$cmp\"  \"$rmin\"-\"$rmax\"")
1990			if wantsdebug (qw[ all thisrequest ]);
1991		$myresult = (($rmin <= $idat) and ($rmax >= $idat));
1992		$myresult = not($myresult) if ($cmp eq '!=');
1993		return $myresult;
1994	},
1995	$COMP_TIME => sub {
1996		my($cmp,$val,$myitem,$request) = @_;
1997		my($myresult) = undef;
1998		my($isec,$imin,$ihour,$iday,$imon,$iyear) = split (',', $myitem);
1999		my($rmin,$rmax) = split (/\s*-\s*/, $val);
2000		my($idat) = (($ihour < 10) ? '0'.$ihour : $ihour) . (($imin < 10) ? '0'.$imin : $imin) . (($isec < 10) ? '0'.$isec : $isec);
2001		$rmin = ($rmin) ? join ('', split ('\:', $rmin)) : $idat;
2002		$rmax = ($rmax) ? join ('', split ('\:', $rmax)) : (($val =~ /-/) ? $idat : $rmin);
2003		log_info ("type time :  \"$idat\"  \"$cmp\"  \"$rmin\"-\"$rmax\"")
2004			if wantsdebug (qw[ all thisrequest ]);
2005		$myresult = (($rmin <= $idat) and ($rmax >= $idat));
2006		$myresult = not($myresult) if ($cmp eq '!=');
2007		return $myresult;
2008	},
2009	$COMP_HELO_ADDR => sub {
2010		my($cmp,$val,$myitem,$request) = @_;
2011		my($myresult) = undef;
2012		return $myresult if $postfwd_settings{dns}{disabled};
2013		return $myresult unless $myitem =~ /\./;
2014		if ( my @answers = dns_query ("$myitem,A") ) {
2015			log_info ("type $COMP_HELO_ADDR : \"".(join ',', @answers)."\"  \"$cmp\"  \"$val\"") if wantsdebug (qw[ all thisrequest ]);
2016			map { $myresult = ( &{$postfwd_compare{cidr}}(($cmp,$val,$_,$request)) ); return $myresult if $myresult } @answers;
2017		};
2018		return $myresult;
2019	},
2020	$COMP_NS_NAME => sub {
2021		my($cmp,$val,$myitem,$request) = @_;
2022		my($myresult) = undef;
2023		return $myresult if $postfwd_settings{dns}{disabled};
2024		return $myresult unless $myitem =~ /\./;
2025		if ( my @answers = dns_query ("$myitem,NS") ) {
2026			log_info ("type $COMP_NS_NAME : \"".(join ',', @answers)."\"  \"$cmp\"  \"$val\"") if wantsdebug (qw[ all thisrequest ]);
2027			map { $myresult = ( &{$postfwd_compare{default}}(($cmp,$val,$_,$request)) ); return $myresult if $myresult } @answers;
2028		} else {
2029			$myresult = ( &{$postfwd_compare{default}}(($cmp,$val,'',$request)) );
2030		};
2031		return $myresult;
2032	},
2033	$COMP_MX_NAME => sub {
2034		my($cmp,$val,$myitem,$request) = @_;
2035		my($myresult) = undef;
2036		return $myresult if $postfwd_settings{dns}{disabled};
2037		return $myresult unless $myitem =~ /\./;
2038		if ( my @answers = dns_query ("$myitem,MX") ) {
2039			log_info ("type $COMP_MX_NAME : \"".(join ',', @answers)."\"  \"$cmp\"  \"$val\"") if wantsdebug (qw[ all thisrequest ]);
2040			map { $myresult = ( &{$postfwd_compare{default}}(($cmp,$val,$_,$request)) ); return $myresult if $myresult } @answers;
2041		} else {
2042			$myresult = ( &{$postfwd_compare{default}}(($cmp,$val,'',$request)) );
2043		};
2044		return $myresult;
2045	},
2046	$COMP_NS_ADDR => sub {
2047		my($cmp,$val,$myitem,$request) = @_;
2048		my($myresult) = undef;
2049		return $myresult if $postfwd_settings{dns}{disabled};
2050		return $myresult unless $myitem =~ /\./;
2051		if ( my @answers = dns_query ("$myitem,NS") ) {
2052			splice (@answers, $postfwd_settings{dns}{max_ns_lookups}) if $postfwd_settings{dns}{max_ns_lookups} and $#answers > $postfwd_settings{dns}{max_ns_lookups};
2053			if ( @answers = dns_query (@answers) ) {
2054				log_info ("type $COMP_NS_ADDR : \"".(join ',', @answers)."\"  \"$cmp\"  \"$val\"") if wantsdebug (qw[ all thisrequest ]);
2055				map { $myresult = ( &{$postfwd_compare{cidr}}(($cmp,$val,$_,$request)) ); return $myresult if $myresult } @answers;
2056			};
2057		};
2058		return $myresult;
2059	},
2060	$COMP_MX_ADDR => sub {
2061		my($cmp,$val,$myitem,$request) = @_;
2062		my($myresult) = undef;
2063		return $myresult if $postfwd_settings{dns}{disabled};
2064		return $myresult unless $myitem =~ /\./;
2065		if ( my @answers = dns_query ("$myitem,MX") ) {
2066			splice (@answers, $postfwd_settings{dns}{max_mx_lookups}) if $postfwd_settings{dns}{max_mx_lookups} and $#answers > $postfwd_settings{dns}{max_mx_lookups};
2067			if ( @answers = dns_query (@answers) ) {
2068				log_info ("type $COMP_MX_ADDR : \"".(join ',', @answers)."\"  \"$cmp\"  \"$val\"") if wantsdebug (qw[ all thisrequest ]);
2069				map { $myresult = ( &{$postfwd_compare{cidr}}(($cmp,$val,$_,$request)) ); return $myresult if $myresult } @answers;
2070			};
2071		};
2072		return $myresult;
2073	},
2074	"default" => sub {
2075		my($cmp,$val,$myitem,$request) = @_;
2076		my($var,$myresult) = undef;
2077		log_info ("type default :  \"$myitem\"  \"$cmp\"  \"$val\"") if wantsdebug (qw[ all thisrequest ]);
2078		# backward compatibility
2079		$cmp = '==' if ( ($var) and ($cmp eq '=') );
2080		if ($cmp eq '==') {
2081			$myresult = ( lc($myitem) eq lc($val) ) if $myitem;
2082		} elsif ($cmp eq '!=') {
2083			$myresult = not( lc($myitem) eq lc($val) ) if $myitem;
2084		} elsif ($cmp eq '=<') {
2085			$myresult = (($myitem || 0) <= $val);
2086		} elsif ($cmp eq '!<') {
2087			$myresult = not(($myitem || 0) <= $val);
2088		} elsif ($cmp eq '=>') {
2089			$myresult = (($myitem || 0) >= $val);
2090		} elsif ($cmp eq '!>') {
2091			$myresult = not(($myitem || 0) >= $val);
2092		} elsif ($cmp eq '<') {
2093			$myresult = (($myitem || 0) < $val);
2094		} elsif ($cmp eq '>') {
2095			$myresult = (($myitem || 0) > $val);
2096		} elsif ($cmp eq '=~') {
2097			$myresult = ($myitem =~ /$val/i);
2098		} elsif ($cmp eq '!~') {
2099			$myresult = ($myitem !~ /$val/i);
2100		} else {
2101			# allow // regex
2102			$val =~ s/^\/?(.*?)\/?$/$1/;
2103			$myresult = $myitem =~ /$val/i;
2104		};
2105		return $myresult;
2106	},
2107	"cidr"			=> sub { return &{$postfwd_compare{cidr_postfwd}}(@_); },
2108	"client_address"	=> sub { return &{$postfwd_compare{cidr}}(@_); },
2109	"encryption_keysize"	=> sub { return &{$postfwd_compare{numeric}}(@_); },
2110	"size"			=> sub { return &{$postfwd_compare{numeric}}(@_); },
2111	"recipient_count"	=> sub { return &{$postfwd_compare{numeric}}(@_); },
2112	"request_score"		=> sub { return &{$postfwd_compare{numeric}}(@_); },
2113	$COMP_RHSBL_KEY_CLIENT	=> sub { return &{$postfwd_compare{$COMP_RHSBL_KEY}}(@_); },
2114	$COMP_RHSBL_KEY_SENDER	=> sub { return &{$postfwd_compare{$COMP_RHSBL_KEY}}(@_); },
2115	$COMP_RHSBL_KEY_HELO	=> sub { return &{$postfwd_compare{$COMP_RHSBL_KEY}}(@_); },
2116	$COMP_RHSBL_KEY_RCLIENT	=> sub { return &{$postfwd_compare{$COMP_RHSBL_KEY}}(@_); },
2117);
2118#
2119# these subroutines define postfwd actions
2120#
2121%postfwd_actions = (
2122	# example action foo()
2123	# "foo" => sub {
2124	#	my($index,$now,$mycmd,$myarg,$myline,%request) = @_;
2125	#	my($myaction) = $postfwd_settings{default}; my($stop) = 0;
2126	#	...
2127	#	return ($stop,$index,$myaction,$myline);
2128	# },
2129	# jump() command
2130	"jump"	=> sub {
2131		my($index,$now,$mycmd,$myarg,$myline,$request) = @_;
2132		my($myaction) = $postfwd_settings{default}; my($stop) = 0;
2133		if (defined $Rule_by_ID{$myarg}) {
2134			my($ruleno) = $Rule_by_ID{$myarg};
2135			log_info ("[RULES] ".$myline
2136				.", jump to rule $ruleno (id $myarg)")
2137				if wantsdebug (qw[ all thisrequest verbose ]);
2138			$index = $ruleno - 1;
2139		} else {
2140			log_warn ("[RULES] ".$myline." - error: jump failed, can not find rule-id ".$myarg." - ignoring");
2141		};
2142		return ($stop,$index,$myaction,$myline);
2143	},
2144	# set() command
2145	"set"	=> sub {
2146		my($index,$now,$mycmd,$myarg,$myline,$request) = @_;
2147		my($myaction) = $postfwd_settings{default}; my($stop) = 0;
2148		foreach ( split (",", $myarg) ) {
2149			if ( /^\s*([^=]+?)\s*([\.\-\*\/\+=]=|=[\.\-\*\/\+=]|=)\s*(.*?)\s*$/ ) {
2150				my($r_var, $mod, $r_val) = ($1, $2, $3);
2151				my($m_val) = (defined $request->{$r_var}) ? $request->{$r_var} : 0;
2152				# saves some ifs
2153				if (($mod eq '=') or ($mod eq '==')) {
2154					$m_val = $r_val;
2155				} elsif ( ($mod eq '.=') or ($mod eq '=.') ) {
2156					$m_val .= $r_val;
2157				} elsif ( (($mod eq '+=') or ($mod eq '=+')) and (($m_val=~/^\-?\d+(\.\d+)?$/) and ($r_val=~/^\-?\d+(\.\d+)?$/)) ) {
2158					$m_val += $r_val;
2159				} elsif ( (($mod eq '-=') or ($mod eq '=-')) and (($m_val=~/^\-?\d+(\.\d+)?$/) and ($r_val=~/^\-?\d+(\.\d+)?$/)) ) {
2160					$m_val -= $r_val;
2161				} elsif ( (($mod eq '*=') or ($mod eq '=*')) and (($m_val=~/^\-?\d+(\.\d+)?$/) and ($r_val=~/^\-?\d+(\.\d+)?$/)) ) {
2162					$m_val *= $r_val;
2163				} elsif ( (($mod eq '/=') or ($mod eq '=/')) and (($m_val=~/^\-?\d+(\.\d+)?$/) and ($r_val=~/^\-?\d+(\.\d+)?$/)) ) {
2164					$m_val /= (($r_val == 0) ? 1 : $r_val);
2165				} else {
2166					$m_val = $r_val;
2167				};
2168				$m_val = $1.($2 || '').($3 || '') if ( $m_val =~ /^(\-?\d+)([\.,]\d+)?(.*)$/ );
2169				(defined $request->{$r_var})
2170					? log_info ("notice", "[RULES] ".$myline.", redefining existing ".$r_var."=".$request->{$r_var}." with ".$r_var."=".$m_val)
2171					: log_info ("[RULES] ".$myline.", defining ".$r_var."=".$m_val)
2172					if wantsdebug (qw[ all thisrequest verbose ]);
2173				$request->{$r_var} = $m_val;
2174			} else {
2175				log_warn ("[RULES] ".$myline.", ignoring unknown set() attribute ".$_);
2176			};
2177		};
2178		return ($stop,$index,$myaction,$myline);
2179	},
2180	# score() command
2181	"score"	=> sub {
2182		my($index,$now,$mycmd,$myarg,$myline,$request) = @_;
2183		my($myaction) = $postfwd_settings{default}; my($stop) = 0;
2184		my($score) = (defined $request->{request_score}) ? $request->{request_score} : 0;
2185		if ($myarg =~/^([\+\-\*\/\=]?)(\d+)([\.,](\d+))?$/) {
2186			my($mod, $val) = ($1, $2 + ((defined $4) ? "0.$4" : 0));
2187			if ($mod eq '-') {
2188				$score -= $val;
2189			} elsif ($mod eq '*') {
2190				$score *= $val;
2191			} elsif ($mod eq '/') {
2192				$score /= $val unless ($val == 0);
2193			} elsif ($mod eq '=') {
2194				$score = $val;
2195			} else {
2196				$score += $val;
2197			};
2198			$score = $1.((defined $2) ? $2 : '.0') if ( $score =~ /^(\-?\d+)([\.,]\d\d?)?/ );
2199			log_info ("[SCORE] ".$myline.", modifying score about ".$myarg." points to ". $score)
2200				if wantsdebug (qw[ all thisrequest verbose ]);
2201			$request->{score} = $request->{request_score} = $score;
2202		} elsif ($myarg) {
2203			log_warn ("[RULES] ".$myline.", invalid value for score \"$myarg\" - ignoring");
2204		};
2205		MAXSCORE: foreach my $max_score (reverse sort keys %{$postfwd_settings{scores}}) {
2206			if ( ($score >= $max_score) and ($postfwd_settings{scores}{$max_score}) ) {
2207				$myaction=$postfwd_settings{scores}{$max_score};
2208				$myline .= ", score=".$score."/".$max_score;
2209				$stop = $score; last MAXSCORE;
2210			};
2211		};
2212		return ($stop,$index,$myaction,$myline);
2213	},
2214	# mail() command
2215	"mail"	=> sub {
2216		my($index,$now,$mycmd,$myarg,$myline,$request) = @_;
2217		my($myaction) = $postfwd_settings{default}; my($stop) = 0;
2218		my($mserver,$mhelo,$mfrom,$mto,$msubject,$mbody) = split "/", $myarg, 6;
2219		($mserver, my $mport) = split ":", $mserver; my $res = "";
2220		my @talk = (
2221			"HELO $mhelo",
2222			"MAIL FROM: $mfrom",
2223			"RCPT TO: $mto",
2224			"DATA",
2225			"Subject: $msubject\r\n$mbody\r\n.",
2226			"QUIT",
2227		);
2228		if ( my $socket = IO::Socket::INET->new(
2229			PeerAddr => $mserver,
2230			PeerPort => ($mport ||= 25),
2231			Proto    => 'tcp',
2232			Timeout  => 30,
2233			Type     => SOCK_STREAM,
2234		) ) {
2235			SMTP: foreach (@talk) {
2236				print $socket "$_\r\n"; $res = <$socket>; chomp($res);
2237				last SMTP unless $res =~ /^[23][0-9][0-9] /;
2238			};
2239			close($socket);
2240			log_info ("[MAIL] ".$myline.", mail server=<$mserver:$mport>, from=<$mfrom>, to=<$mto>, subject=<$msubject>, status=<$res>");
2241		} else {
2242			log_info ("[MAIL] ".$myline.", could not open socket to $mserver:$mport: '$!'");
2243		};
2244		return ($stop,$index,$myaction,$myline);
2245	},
2246	# sendmail()
2247	"sendmail" => sub {
2248		my($index,$now,$mycmd,$myarg,$myline,$request) = @_;
2249		my($myaction) = $postfwd_settings{default}; my($stop) = 0;
2250		my($mcmd,$mfrom,$mto,$msubject,$mbody) = split '::', $myarg, 5;
2251		my($msg) = "From: $mfrom\nTo: $mto\nSubject: $msubject\n\n$mbody\n";
2252		my($sm) = undef;
2253		if ( (-x $mcmd) and open ($sm, '|-', "$mcmd -i -f $mfrom $mto") ) {
2254			if ( print $sm "$msg" ) {
2255				log_info ("[SENDMAIL] ".$myline.", $mcmd from=<$mfrom>, to=<$mto>, subject=<$msubject>");
2256			} else {
2257				log_note ("[SENDMAIL] ".$myline.", could not print to $mcmd pipe: '$!'");
2258			};
2259			close($sm);
2260		} else {
2261			log_note ("[SENDMAIL] ".$myline.", could not open pipe to $mcmd: '$!'");
2262		};
2263		return ($stop,$index,$myaction,$myline);
2264	},
2265	# rate() command
2266	"rate"	=> sub {
2267		my($index,$now,$mycmd,$myarg,$myline,$request) = @_;
2268		my($myaction) = $postfwd_settings{default}; my($stop) = 0;
2269		my($ratetype,$ratecount,$ratetime,$ratecmd) = split "/", $myarg, 4;
2270		my($rcount) = ( ($mycmd =~ /^size/) ? $request->{size} : (($mycmd =~ /^rcpt/) ? $request->{recipient_count} : 1 ) );
2271		if ($ratetype and $ratecount and $ratetime and $ratecmd and $rcount) {
2272		  my $crate = $Rules[$index]{$COMP_ID}.'+'.$ratecount.'_'.$ratetime;
2273		  if ( defined $request->{$ratetype} ) {
2274                        my $r = $request->{$ratetype};
2275                        unless ($mycmd =~ /5321$/) {
2276                                $r = lc($r);
2277                        } else {
2278                                $r = ($r =~ /^([^@]+)@(\S+)$/) ? $1.'@'.lc($2) : lc($r);
2279                        };
2280                        $ratetype .= "=".$r;
2281			push @{$Rate_Cache{$ratetype}{'list'}}, $crate;
2282			@{$Rate_Cache{$ratetype}{'list'}} = uniq(@{$Rate_Cache{$ratetype}{'list'}});
2283			my $rate_exists = ( defined $Rate_Cache{$ratetype}{$crate} );
2284			$Rate_Cache{$ratetype}{$crate}{'type'}     = $mycmd;
2285			$Rate_Cache{$ratetype}{$crate}{'maxcount'} = $ratecount;
2286			# child rate cache
2287                        if ($postfwd_settings{rate}{noparent}) {
2288				# create rate
2289				unless ( $rate_exists ) {
2290					$Rate_Cache{$ratetype}{$crate}{'ttl'}	= $ratetime;
2291					$Rate_Cache{$ratetype}{$crate}{'until'}	= $now + $ratetime;
2292					$Rate_Cache{$ratetype}{$crate}{'count'}	= $rcount;
2293                                	log_info ("created rate limit object '$ratetype'->'".$crate."' with value '$rcount'") if wantsdebug (qw[ all thisrequest rates ]);
2294				} else {
2295					# renew rate
2296					if ( $now > $Rate_Cache{$ratetype}{$crate}{'until'} ) {
2297						$Rate_Cache{$ratetype}{$crate}{'ttl'}	= $ratetime;
2298						$Rate_Cache{$ratetype}{$crate}{'until'}	= $now + $ratetime;
2299						$Rate_Cache{$ratetype}{$crate}{'count'}	= $rcount;
2300	                                	log_info ("renewed rate limit object '$ratetype'->'".$crate."' with value '$rcount'") if wantsdebug (qw[ all thisrequest rates ]);
2301					# increase rate
2302					} else {
2303						$Rate_Cache{$ratetype}{$crate}{'count'} += $rcount || 0;
2304	                                	log_info ("updated rate limit object '$ratetype'->'".$crate."' with value '$rcount' to '".$Rate_Cache{$ratetype}{$crate}{'count'}."'") if wantsdebug (qw[ all thisrequest rates ]);
2305					};
2306				};
2307			# parent rate cache
2308                        } else {
2309				$Rate_Cache{$ratetype}{$crate}{'ttl'}	= $ratetime;
2310				$Rate_Cache{$ratetype}{$crate}{'until'}	= $now + $ratetime;
2311				$Rate_Cache{$ratetype}{$crate}{'count'}	= $rcount;
2312                                my $prate = "CMD=".$postfwd_commands{setrateitem}.";TYPE=rate;ITEM=$ratetype".$postfwd_settings{seplim}.$crate.hash_to_str($Rate_Cache{$ratetype}{$crate});
2313                                $prate = cache_query ($prate);
2314				$Rate_Cache{$ratetype}{$crate}{count} = $prate if ($prate =~ /^\d+$/);
2315                               	log_info ("updated parent rate limit object '$ratetype'->'".$crate."' with value '$rcount' to '".$Rate_Cache{$ratetype}{$crate}{'count'}."'") if wantsdebug (qw[ all thisrequest rates ]);
2316                        };
2317			# rate exceeded
2318			$stop = ( $Rate_Cache{$ratetype}{$crate}{count} > $Rate_Cache{$ratetype}{$crate}{maxcount} );
2319			if ($stop) {
2320				$myaction=$ratecmd;
2321				$request->{'ratecount'} = $Rate_Cache{$ratetype}{$crate}{count} || 0;
2322				$myline .= ", rate=".$Rate_Cache{$ratetype}{$crate}{type}."/".$Rate_Cache{$ratetype}{$crate}{count}."/".$Rate_Cache{$ratetype}{$crate}{maxcount};
2323			};
2324		  } else {
2325			log_note ("[RULES] ".$myline.", ignoring empty index for ".$mycmd." limit '".$ratetype."'") if wantsdebug (qw[ all thisrequest rates ]);
2326		  };
2327		} else {
2328			log_note ("[RULES] ".$myline.(($rcount) ? ", ignoring unknown ".$mycmd."() attribute \'".$myarg."\'" : ", ignoring empty counter"));
2329		};
2330		return ($stop,$index,$myaction,$myline);
2331	},
2332	# size() command
2333	"size"	=> sub { return &{$postfwd_actions{rate}}(@_); },
2334	# rcpt() command
2335	"rcpt"	=> sub { return &{$postfwd_actions{rate}}(@_); },
2336	# rate() command, according to rfc5321 case-sensivity
2337	"rate5321"      => sub { return &{$postfwd_actions{rate}}(@_); },
2338	# rcpt() command, according to rfc5321 case-sensivity
2339	"rcpt5321"      => sub { return &{$postfwd_actions{rate}}(@_); },
2340	# size() command, according to rfc5321 case-sensivity
2341	"size5321"      => sub { return &{$postfwd_actions{rate}}(@_); },
2342	# groupadd() command
2343	"groupadd"	=> sub {
2344		my($index,$now,$mycmd,$myarg,$myline,$request) = @_;
2345		my($myaction) = $postfwd_settings{default};
2346		my($groupname,$groupitem,$groupttl,$stop) = split "/", $myarg, 4; $stop ||= 0;
2347		$groupname =~ s/^$COMP_GROUP//; $groupitem =~ s/^$COMP_VAR//;
2348		if ( $groupname and $groupitem and $request->{$groupitem} ) {
2349			$groupttl ||= $postfwd_settings{group}{ttl};
2350			log_info ("[RULES] ".$myline.", adding item '".$request->{$groupitem}."' to group '".$groupname."' with ttl=".$groupttl."s") if wantsdebug (qw[ all thisrequest groups ]);
2351			if ($postfwd_settings{group}{noparent}) {
2352				$Group_Cache{$groupname}{$request->{$groupitem}} = $now + $groupttl unless (
2353					(defined $Group_Cache{$groupname}{$request->{$groupitem}} and $Group_Cache{$groupname}{$request->{$groupitem}} > $now)
2354					or (scalar keys %{$Group_Cache{$groupname}} >= $postfwd_settings{group}{maxitems})
2355					or ($postfwd_settings{group}{maxitems} < 0)
2356				);
2357			} else {
2358				my $cmd = "CMD=".$postfwd_commands{groupadd}.";TYPE=$groupname;ITEM=".$request->{$groupitem}.$postfwd_settings{seplim}.($now + $groupttl);
2359				my $res = cache_query ($cmd);
2360				log_info ("parent group update '".$cmd."' -> '".($res || '<undef>')."'") if wantsdebug (qw[ all thisrequest groups ]);
2361			};
2362		};
2363		return ($stop,$index,$myaction,$myline);
2364	},
2365	# groupdel() command
2366	"groupdel"	=> sub {
2367		my($index,$now,$mycmd,$myarg,$myline,$request) = @_;
2368		my($myaction) = $postfwd_settings{default};
2369		my($groupname,$groupitem,$stop) = split "/", $myarg, 3; $stop ||= 0;
2370		$groupname =~ s/^$COMP_GROUP//; $groupitem =~ s/^$COMP_VAR//;
2371		if ( $groupname and $groupitem and $request->{$groupitem} ) {
2372			if ($postfwd_settings{group}{noparent}) {
2373				if ( defined $Group_Cache{$groupname}{$request->{$groupitem}} ) {
2374					log_info ("[RULES] ".$myline.", removing item '".$request->{$groupitem}."' from group '".$groupname."'") if wantsdebug (qw[ all thisrequest groups ]);
2375					delete $Group_Cache{$groupname}{$request->{$groupitem}};
2376				} else {
2377					log_note ("[RULES] ".$myline.", item '".($request->{$groupitem} || '<undef>')."' not in group '".($groupname || '<undef>')."'") if wantsdebug (qw[ all thisrequest groups ]);
2378				};
2379			} else {
2380				my $cmd = "CMD=".$postfwd_commands{groupdel}.";TYPE=$groupname;ITEM=".$request->{$groupitem};
2381				my $res = cache_query ($cmd);
2382				log_info ("parent group update '".$cmd."' -> '".($res || '<undef>')."'") if wantsdebug (qw[ all thisrequest groups ]);
2383			};
2384		};
2385		return ($stop,$index,$myaction,$myline);
2386	},
2387	# wait() command
2388	"wait"	=> sub {
2389		my($index,$now,$mycmd,$myarg,$myline,$request) = @_;
2390		my($myaction) = $postfwd_settings{default}; my($stop) = 0;
2391		log_info ("[RULES] ".$myline.", delaying for $myarg seconds");
2392		sleep $myarg;
2393		return ($stop,$index,$myaction,$myline);
2394	},
2395	# note() command
2396	"note"	=> sub {
2397		my($index,$now,$mycmd,$myarg,$myline,$request) = @_;
2398		my($myaction) = $postfwd_settings{default}; my($stop) = 0;
2399		log_info ("[RULES] ".$myline." - note: ".$myarg) if $myarg;
2400		return ($stop,$index,$myaction,$myline);
2401	},
2402	# debug() command
2403	"debug"	=> sub {
2404		my($index,$now,$mycmd,$myarg,$myline,$request) = @_;
2405		my($myaction) = $postfwd_settings{default}; my($stop) = 0;
2406		log_info ("[RULES] ".$myline.", DEBUG=$myarg");
2407		($myarg =~ /^(1|y(es)?|on)$/i) ? $postfwd_settings{debug}{thisrequest} = 1 : delete $postfwd_settings{debug}{thisrequest};
2408		return ($stop,$index,$myaction,$myline);
2409	},
2410	# quit() command - not supported in this version
2411	"quit"	=> sub {
2412		my($index,$now,$mycmd,$myarg,$myline,$request) = @_;
2413		my($myaction) = $postfwd_settings{default}; my($stop) = 0;
2414		log_warn ("[RULES] ".$myline." - critical: quit (".$myarg.") unsupported in this version - ignoring");
2415		return ($stop,$index,$myaction,$myline);
2416	},
2417	# file() command
2418	"file"	=> sub {
2419		my($index,$now,$mycmd,$myarg,$myline,$request) = @_;
2420		my($myaction) = $postfwd_settings{default}; my($stop) = 0;
2421		log_warn ("[RULES] ".$myline." - error: command ".$mycmd."() has not been implemented yet - ignoring");
2422		return ($stop,$index,$myaction,$myline);
2423	},
2424	# dumpcache() command
2425	"dumpcache" => sub {
2426		my($index,$now,$mycmd,$myarg,$myline,$request) = @_;
2427		my($myaction) = $postfwd_settings{default}; my($stop) = 0;
2428		map { log_info ("[RULES] DUMPCACHE: $_") } (dump_cache());
2429		return ($stop,$index,$myaction,$myline);
2430	},
2431	# ask() command
2432	"ask"  => sub {
2433		my($index,$now,$mycmd,$myarg,$myline,$request) = @_;
2434		my($myaction) = $postfwd_settings{default}; my($stop) = 0;
2435		log_info ("Opening socket to '$myarg'") if wantsdebug (qw[ all thisrequest ]);
2436		my($addr,$port,$ignore) = split ':', $myarg;
2437		my %orig = str_to_hash (\$request->{orig});
2438		if ( ($addr and $port) and my $socket = new IO::Socket::INET (
2439			PeerAddr => $addr,
2440			PeerPort => $port,
2441			Proto    => 'tcp',
2442			Timeout  => 9,
2443			Type     => SOCK_STREAM	) ) {
2444
2445				my $sendstr = '';
2446				foreach (keys %orig) {
2447					$sendstr .= $_."=".$orig{$_}."\n";
2448				};
2449				$sendstr .= "\n";
2450				log_info ("Asking service $myarg -> '$sendstr'") if wantsdebug (qw[ all thisrequest ]);
2451				print $socket "$sendstr";
2452				$sendstr = <$socket>;
2453				chomp($sendstr);
2454				log_info ("Answer from $myarg -> '$sendstr'") if wantsdebug (qw[ all thisrequest verbose ]);
2455				$sendstr =~ s/^(action=)//;
2456				if ($1 and $sendstr) {
2457					if ($ignore and ($sendstr =~ /$ignore/i)) {
2458						log_info ("ignoring answer '$sendstr' from $myarg") if wantsdebug (qw[ all thisrequest verbose ]);
2459					} else {
2460						$stop = $myaction = $sendstr;
2461					};
2462				} else {
2463					log_note ("rule: $index got invalid answer '$sendstr' from $myarg");
2464				};
2465		} else {
2466			log_note ("Could not open socket to '$myarg' - $!");
2467		};
2468		return ($stop,$index,$myaction,$myline);
2469	},
2470	# exec() command
2471	"exec"	=> sub { return &{$postfwd_actions{file}}(@_); },
2472);
2473
2474# load plugin-items
2475sub get_plugins {
2476    my(@pluginfiles) = @_;
2477    my($pluginlog)   = '';
2478    foreach my $file (@pluginfiles) {
2479	unless ( -e $file ) {
2480		log_warn ("File not found: $file");
2481	} else {
2482		$file =~ /^(.*)$/;
2483		require $1 if $1;
2484		map { delete $postfwd_items_plugin{$_}   unless ($_ and defined $postfwd_items_plugin{$_})   } (keys %postfwd_items_plugin);
2485		map { delete $postfwd_compare_plugin{$_} unless ($_ and defined $postfwd_compare_plugin{$_}) } (keys %postfwd_compare_plugin);
2486		map { delete $postfwd_actions_plugin{$_} unless ($_ and defined $postfwd_actions_plugin{$_}) } (keys %postfwd_actions_plugin);
2487		map { log_note ("[PLUGIN] overriding prior item \'".$_."\'") 		 if (defined $postfwd_items{$_})   } (keys %postfwd_items_plugin);
2488		map { log_note ("[PLUGIN] overriding prior compare function \'".$_."\'") if (defined $postfwd_compare{$_}) } (keys %postfwd_compare_plugin);
2489		map { log_note ("[PLUGIN] overriding prior action \'".$_."\'") 		 if (defined $postfwd_actions{$_}) } (keys %postfwd_actions_plugin);
2490		%postfwd_items   = ( %postfwd_items, %postfwd_items_plugin )     if %postfwd_items_plugin;
2491		%postfwd_compare = ( %postfwd_compare, %postfwd_compare_plugin ) if %postfwd_compare_plugin;
2492		%postfwd_actions = ( %postfwd_actions, %postfwd_actions_plugin ) if %postfwd_actions_plugin;
2493		$pluginlog =  "[PLUGIN] Loaded plugins file: ".$file;
2494		$pluginlog .= " items: \"".(join ", ", (sort keys %postfwd_items_plugin))."\""
2495			if %postfwd_items_plugin;
2496		$pluginlog .= " compare: \"".(join ", ", (sort keys %postfwd_compare_plugin))."\""
2497			if %postfwd_compare_plugin;
2498		$pluginlog .= " actions: \"".(join ", ", (sort keys %postfwd_actions_plugin))."\""
2499			if %postfwd_actions_plugin;
2500		log_info ($pluginlog);
2501	};
2502    };
2503};
2504
2505
2506### SUB ruleset
2507
2508# compare item main
2509# use: compare_item ( $TYPE, $RULEITEM, $MINIMUMHITS, $REQUESTITEM, %REQUEST, %REQUESTINFO );
2510sub compare_item {
2511    my($now,$mykey,$mymask,$mymin,$myitem,$request) = @_;
2512    my($val,$var,$cmp,$neg,$myresult,$postfwd_compare_proc);
2513    my($rcount) = 0;
2514    $mymin ||= 1;
2515
2516    #
2517    # determine the right compare function
2518    $postfwd_compare_proc = (defined $postfwd_compare{$mykey}) ? $mykey : "default";
2519    #
2520    # save list due to possible modification
2521    my @items = @{$mymask};
2522    # now compare request to every single item
2523    ITEM: foreach (@items) {
2524	($cmp, $val) = split ";";
2525	next ITEM unless ($cmp and (defined $val) and $mykey);
2526	# prepare_file
2527	if ($val =~ /$COMP_LIVE_FILE_TABLE/) {
2528		push @items, prepare_file (0, $1, $cmp, $2);
2529		next ITEM;
2530	};
2531	# prepare_group
2532	if ($val =~ /^$COMP_GROUP/) {
2533		my @mygroup = degroup_item($now, $val);
2534		map { push @items, $cmp.";".$_ } @mygroup if @mygroup;
2535		next ITEM;
2536	};
2537	log_info ("compare $mykey:  \"$myitem\"  \"$cmp\"  \"$val\"") if wantsdebug (qw[ all thisrequest ]);
2538	$val = $neg if ($neg = deneg_item($val));
2539	log_info ("deneg $mykey:  \"$myitem\"  \"$cmp\"  \"$val\"") if ($neg and wantsdebug (qw[ all thisrequest ]));
2540	next ITEM unless (defined $val);
2541	# substitute check for $$vars in rule item
2542	if ( $var = devar_item ($cmp,$val,$myitem,$request) ) {
2543		$val = $var; $val =~ s/([^-_@\.\w\s])/\\$1/g unless ($cmp eq '==');
2544	};
2545	$myresult = &{$postfwd_compare{$postfwd_compare_proc}}($cmp,$val,$myitem,$request);
2546	log_info ("match $mykey:  ".($myresult ? "TRUE" : "FALSE")) if wantsdebug (qw[ all thisrequest ]);
2547	if ($neg) {
2548		$myresult = not($myresult);
2549		log_info ("negate match $mykey:  ".($myresult ? "TRUE" : "FALSE")) if wantsdebug (qw[ all thisrequest ]);
2550	};
2551	$rcount++ if $myresult;
2552	$myresult = not($mymin eq 'all');
2553	$myresult = ( $rcount >= $mymin ) if $myresult;
2554	log_info ("count $mykey:  request=$rcount  minimum: $mymin  result: ".($myresult ? "TRUE" : "FALSE")) if wantsdebug (qw[ all thisrequest ]);
2555	last ITEM if $myresult;
2556    };
2557    $myresult = $rcount if ($myresult or ($mymin eq 'all'));
2558    return $myresult;
2559};
2560
2561
2562#
2563# compare request against a single rule
2564#
2565sub compare_rule {
2566    my($now,$index,$date,$request) = @_;
2567    my(@ruleitems) = keys %{$Rules[$index]};
2568    my($has_rbl) = exists($Rules[$index]{$COMP_RBL_KEY});
2569    my($has_rhl) = (
2570	exists($Rules[$index]{$COMP_RHSBL_KEY}) or exists($Rules[$index]{$COMP_RHSBL_KEY_RCLIENT}) or
2571	exists($Rules[$index]{$COMP_RHSBL_KEY_CLIENT}) or exists($Rules[$index]{$COMP_RHSBL_KEY_SENDER}) or
2572	exists($Rules[$index]{$COMP_RHSBL_KEY_HELO})
2573    );
2574    my($has_senderdns) = ( exists($Rules[$index]{$COMP_NS_NAME})
2575			or exists($Rules[$index]{$COMP_MX_NAME})
2576			or exists($Rules[$index]{$COMP_NS_ADDR})
2577			or exists($Rules[$index]{$COMP_MX_ADDR})
2578    );
2579    my($hasdns) = ( not($postfwd_settings{dns}{disabled}) and ($has_senderdns or $has_rhl or $has_rbl) );
2580    my($myitem,$val,$cmp,$res,$myline,$timed) = undef;
2581    my(@myresult) = (0,0,0,0);
2582    my(@queries,@timedout) = ();
2583    my($num) = 1;
2584    undef @DNSBL_Text;
2585    my($ownres,$ownsel,$bgsock) = undef;
2586    my %ownsock  = ();
2587    my @ownready = ();
2588
2589    log_info ("[RULES] rule: $index, id: $Rules[$index]{$COMP_ID}, items: '".((@ruleitems) ? join ';', @ruleitems: '')."'") if wantsdebug (qw[ all thisrequest ]);
2590
2591    # COMPARE-ITEMS
2592    # check all non-dns items
2593    ITEM: for my $mykey ( keys %{$Rules[$index]} ) {
2594	# always true
2595	if ( ($mykey eq $COMP_ID) or ($mykey eq $COMP_ACTION) or ($mykey eq $COMP_CACHE) ) {
2596		$myresult[0]++;
2597		next ITEM;
2598	};
2599	next ITEM if ( (($mykey eq $COMP_RBL_CNT) or ($mykey eq $COMP_RHSBL_CNT)) );
2600	next ITEM if ( (($mykey eq $COMP_RBL_KEY) or ($mykey eq $COMP_RHSBL_KEY)) );
2601	next ITEM if ( ($mykey eq $COMP_RHSBL_KEY_RCLIENT) or ($mykey eq $COMP_RHSBL_KEY_CLIENT) or ($mykey eq $COMP_RHSBL_KEY_SENDER) or ($mykey eq $COMP_RHSBL_KEY_HELO) );
2602
2603	# integration at this point enables redefining scores within ruleset
2604	if ($mykey eq $COMP_SCORES) {
2605		modify_score ($Rules[$index]{$mykey},$Rules[$index]{$COMP_ACTION});
2606		$myresult[0] = 0;
2607	} else {
2608		$val = ( $mykey =~ /^$COMP_DATECALC$/ )
2609			# prepare date check
2610			? $date
2611			# default: compare against request attribute
2612			: $request->{$mykey};
2613		$myresult[0] = ($res = compare_item($now, $mykey, $Rules[$index]{$mykey}, $num, ((defined $val) ? $val : ''), $request)) ? ($myresult[0] + $res) : 0;
2614	};
2615	last ITEM unless ($myresult[0] > 0);
2616    };
2617    log_info ("[RULES] pre-dns: rule: $index, id: $Rules[$index]{$COMP_ID}, RESULT: ".$myresult[0]) if wantsdebug (qw[ all thisrequest ]);
2618
2619    # DNSQUERY-SECTION
2620    # fire bgsend()s with callback to result cache,
2621    # if they are not contained already,
2622    # and $postfwd_settings{dns}{disabled} is not set
2623    if ($hasdns and $myresult[0]) {
2624
2625	# prepare dns queries
2626	$ownres   = Net::DNS::Resolver->new(
2627		tcp_timeout => $postfwd_settings{dns}{timeout},
2628		udp_timeout => $postfwd_settings{dns}{timeout},
2629		persistent_tcp => 0, persistent_udp => 0,
2630		retrans => 0, retry => 1, dnsrch => 0, defnames => 0,
2631	);
2632	$ownsel   = IO::Select->new();
2633
2634	map { $timed .= (($timed) ? ", $_" : $_) if $Timeouts{$_} > $postfwd_settings{dns}{max_timeout} } (keys %Timeouts);
2635	log_note ("[DNSBL] skipping rbls: $timed - too much timeouts") if $timed;
2636
2637	push @queries, rbl_prepare_lookups ( $COMP_RBL_KEY, $request->{reverse_address}, @{$Rules[$index]{$COMP_RBL_KEY}} )
2638		if (defined $Rules[$index]{$COMP_RBL_KEY});
2639
2640	push @queries, rbl_prepare_lookups ( $COMP_RHSBL_KEY, $request->{client_name}, @{$Rules[$index]{$COMP_RHSBL_KEY}} )
2641		if (defined $Rules[$index]{$COMP_RHSBL_KEY});
2642
2643	push @queries, rbl_prepare_lookups ( $COMP_RHSBL_KEY_CLIENT, $request->{client_name}, @{$Rules[$index]{$COMP_RHSBL_KEY_CLIENT}} )
2644		if (defined $Rules[$index]{$COMP_RHSBL_KEY_CLIENT});
2645
2646	push @queries, rbl_prepare_lookups ( $COMP_RHSBL_KEY_RCLIENT, $request->{reverse_client_name}, @{$Rules[$index]{$COMP_RHSBL_KEY_RCLIENT}} )
2647		if (defined $Rules[$index]{$COMP_RHSBL_KEY_RCLIENT});
2648
2649	push @queries, rbl_prepare_lookups ( $COMP_RHSBL_KEY_HELO, $request->{helo_name}, @{$Rules[$index]{$COMP_RHSBL_KEY_HELO}} )
2650		if (defined $Rules[$index]{$COMP_RHSBL_KEY_HELO});
2651
2652	push @queries, rbl_prepare_lookups ( $COMP_RHSBL_KEY_SENDER, $request->{sender_domain}, @{$Rules[$index]{$COMP_RHSBL_KEY_SENDER}} )
2653		if (defined $Rules[$index]{$COMP_RHSBL_KEY_SENDER});
2654
2655	# send dns queries
2656	if ( @queries ) {
2657		@queries = uniq(@queries);
2658		QUERY: foreach my $query (@queries) {
2659			next QUERY unless $query;
2660			log_info ("[SENDDNS] sending query \'$query\'")
2661				if wantsdebug (qw[ all thisrequest ]);
2662			# send A query
2663			$bgsock = $ownres->bgsend($query, 'A');
2664			$ownsel->add($bgsock);
2665			$ownsock{$bgsock} = 'A:'.$query;
2666			# send TXT query
2667			if ($postfwd_settings{dns}{async_txt}) {
2668				$bgsock = $ownres->bgsend($query, 'TXT');
2669				$ownsel->add($bgsock);
2670				$ownsock{$bgsock} = 'TXT:'.$query;
2671			};
2672		};
2673		log_info ("[SENDDNS] rule: $index, id: $Rules[$index]{$COMP_ID}, lookups: ".($#queries + 1))
2674			if wantsdebug (qw[ all thisrequest ]);
2675		$myresult[3] = "dnsqueries=".($#queries + 1).$postfwd_settings{sepreq}."dnsinterval=".($#queries + 1);
2676	};
2677
2678        # DNSRESULT-SECTION
2679        # wait for select() and check the results unless $postfwd_settings{dns}{disabled}
2680	my($ownstart) = time(); @queries = ();
2681	while ((scalar keys %ownsock) and (@ownready = $ownsel->can_read($postfwd_settings{dns}{timeout}))) {
2682		foreach my $sock (@ownready) {
2683			if (defined $ownsock{$sock}) {
2684				log_note ("[DNSBL] answer for ".$ownsock{$sock})
2685					if wantsdebug (qw[ all thisrequest ]);
2686				my $packet = $ownres->bgread($sock);
2687				push @queries, (split ':', $ownsock{$sock})[1] if rbl_read_dns ($packet);
2688				delete $ownsock{$sock};
2689			} else {
2690				$ownsel->remove($sock);
2691				$sock = undef;
2692			};
2693		};
2694	};
2695
2696	# timeout handling
2697	map { push @timedout, (split ':', $ownsock{$_})[1] } (keys %ownsock);
2698	if (@timedout) {
2699		@timedout = uniq(@timedout);
2700		$myresult[3] .= $postfwd_settings{sepreq}."dnstimeouts=".($#timedout + 1);
2701		foreach (@timedout) {
2702		#	@{$DNS_Cache{$_}{A}}    = ('__TIMEOUT__');
2703			$DNS_Cache{$_}{ttl}     = $postfwd_settings{dns}{ttl} unless $DNS_Cache{$_}{ttl};
2704			$DNS_Cache{$_}{'delay'} = $now - $ownstart;
2705			$DNS_Cache{$_}{'until'} = $now + $DNS_Cache{$_}{ttl};
2706			$DNS_Cache{$_}{'timed'} = 1;
2707			$Timeouts{$DNS_Cache{$_}{name}} = (defined $Timeouts{$DNS_Cache{$_}{name}})
2708				? $Timeouts{$DNS_Cache{$_}{name}} + 1
2709				: 1
2710				if ( $postfwd_settings{dns}{max_timeout} > 0 );
2711			log_note ("[DNSBL] warning: timeout (".$Timeouts{$DNS_Cache{$_}{name}}."/".$postfwd_settings{dns}{max_timeout}.") for ".$DNS_Cache{$_}{name}." after ".ts($DNS_Cache{$_}{'delay'})." seconds");
2712		};
2713	};
2714
2715        # perform outstanding TXT queries unless --dns_async_txt is set
2716        if (not($postfwd_settings{dns}{async_txt}) and @queries) {
2717                @queries = uniq(@queries);
2718                log_info ("[DNSBL] sending TXT queries for ".(join ',', @queries)) if wantsdebug (qw[ all thisrequest debugdns ]);
2719                foreach my $query (@queries) {
2720                        log_info ("[SENDDNS] sending TXT query \'$query\'") if wantsdebug (qw[ all thisrequest ]);
2721                        # send TXT query
2722                        $bgsock = $ownres->bgsend($query, 'TXT');
2723                        $ownsel->add($bgsock);
2724                        $ownsock{$bgsock} = 'TXT:'.$query;
2725                };
2726                while ((scalar keys %ownsock) and (@ownready = $ownsel->can_read($postfwd_settings{dns}{timeout}))) {
2727                        foreach my $sock (@ownready) {
2728                                if (defined $ownsock{$sock}) {
2729                                        log_info ("[DNSBL] answer for ".$ownsock{$sock})
2730                                                if wantsdebug (qw[ all thisrequest ]);
2731                                        my $packet = $ownres->bgread($sock);
2732                                        rbl_read_dns ($packet);
2733                                        delete $ownsock{$sock};
2734                                } else {
2735                                        $ownsel->remove($sock);
2736                                        $sock = undef;
2737                                };
2738                        };
2739                };
2740        };
2741
2742	# compare dns results
2743	if ( ($myresult[0] > 0) and exists($Rules[$index]{$COMP_RBL_KEY}) ) {
2744		$res = compare_item(
2745			$now,
2746			$COMP_RBL_KEY,
2747			$Rules[$index]{$COMP_RBL_KEY},
2748			($Rules[$index]{$COMP_RBL_CNT} ||= 1),
2749			$request->{reverse_address},
2750			$request
2751		);
2752		$myresult[0] = ($res or ($Rules[$index]{$COMP_RBL_CNT} eq 'all')) ? ($myresult[0] + $res) : 0;
2753		$myresult[1] = ($res) ? $res : 0;
2754	};
2755
2756	if ( $has_rhl and ($myresult[0] > 0) ) {
2757		if ( exists($Rules[$index]{$COMP_RHSBL_KEY}) ) {
2758				$res = compare_item(
2759					$now,
2760					$COMP_RHSBL_KEY,
2761					$Rules[$index]{$COMP_RHSBL_KEY},
2762					($Rules[$index]{$COMP_RHSBL_CNT} ||= 1),
2763					$request->{client_name},
2764					$request
2765				);
2766				$myresult[0] = ($res or ($Rules[$index]{$COMP_RHSBL_CNT} eq 'all')) ? ($myresult[0] + $res) : 0;
2767				$myresult[2] += $res if $res;
2768		};
2769		if ( exists($Rules[$index]{$COMP_RHSBL_KEY_CLIENT}) ) {
2770				$res = compare_item(
2771					$now,
2772					$COMP_RHSBL_KEY_CLIENT,
2773					$Rules[$index]{$COMP_RHSBL_KEY_CLIENT},
2774					($Rules[$index]{$COMP_RHSBL_CNT} ||= 1),
2775					$request->{client_name},
2776					$request
2777				);
2778				$myresult[0] = ($res or ($Rules[$index]{$COMP_RHSBL_CNT} eq 'all')) ? ($myresult[0] + $res) : 0;
2779				$myresult[2] += $res if $res;
2780		};
2781		if ( exists($Rules[$index]{$COMP_RHSBL_KEY_SENDER}) ) {
2782				$res = compare_item(
2783					$now,
2784					$COMP_RHSBL_KEY_SENDER,
2785					$Rules[$index]{$COMP_RHSBL_KEY_SENDER},
2786					($Rules[$index]{$COMP_RHSBL_CNT} ||= 1),
2787					$request->{sender_domain},
2788					$request
2789				);
2790				$myresult[0] = ($res or ($Rules[$index]{$COMP_RHSBL_CNT} eq 'all')) ? ($myresult[0] + $res) : 0;
2791				$myresult[2] += $res if $res;
2792		};
2793		if ( exists($Rules[$index]{$COMP_RHSBL_KEY_HELO}) ) {
2794				$res = compare_item(
2795					$now,
2796					$COMP_RHSBL_KEY_HELO,
2797					$Rules[$index]{$COMP_RHSBL_KEY_HELO},
2798					($Rules[$index]{$COMP_RHSBL_CNT} ||= 1),
2799					$request->{helo_name},
2800					$request
2801				);
2802				$myresult[0] = ($res or ($Rules[$index]{$COMP_RHSBL_CNT} eq 'all')) ? ($myresult[0] + $res) : 0;
2803				$myresult[2] += $res if $res;
2804		};
2805		if ( exists($Rules[$index]{$COMP_RHSBL_KEY_RCLIENT}) ) {
2806				$res = compare_item(
2807					$now,
2808					$COMP_RHSBL_KEY_RCLIENT,
2809					$Rules[$index]{$COMP_RHSBL_KEY_RCLIENT},
2810					($Rules[$index]{$COMP_RHSBL_CNT} ||= 1),
2811					$request->{reverse_client_name},
2812					$request
2813				);
2814				$myresult[0] = ($res or ($Rules[$index]{$COMP_RHSBL_CNT} eq 'all')) ? ($myresult[0] + $res) : 0;
2815				$myresult[2] += $res if $res;
2816		};
2817	};
2818    };
2819    if ( wantsdebug (qw[ all thisrequest ]) ) {
2820	$myline  = "[RULES]  RULE: ".$index."  MATCHES: ".((($myresult[0] - 2) > 0) ? ($myresult[0] - 2) : 0);
2821	$myline .= "  RBLCOUNT: ".$myresult[1] if $myresult[1];
2822	$myline .= "  RHSBLCOUNT: ".$myresult[2] if $myresult[2];
2823	$myline .= "  DNSBLTEXT: ".(join ("; ", @DNSBL_Text)) if ( (@DNSBL_Text) and (($myresult[1] > 0) or ($myresult[2] > 0)) );
2824	log_info ($myline);
2825    };
2826    return @myresult;
2827};
2828
2829
2830### SUB access policy
2831
2832# access policy routine
2833sub smtpd_access_policy {
2834    my($parent,$request)		      		= @_;
2835    my(@ruleactions) 					= ();
2836    my($postfixaction) 			      		= $postfwd_settings{default};
2837    my($index)				      		= 1;
2838    my($stop)				      		= 1;
2839    my($now)				      		= time();
2840    my($date)				      		= join(',', localtime($now));
2841    my($counters)					= "request=1".$postfwd_settings{sepreq}."interval=1";
2842    my ($rhit)						= "ruleset";
2843    my($matched,$rblcnt,$rhlcnt,$t1,$t2,$t3,$ai)  	= 0;
2844    my($mykey,$cacheid,$myline,$checkval,$var,$ratehit,$rateindex,$rulehits) = "";
2845
2846    # save original request
2847    $request->{orig} = hash_to_str ($request);
2848
2849    # replace empty sender with <>
2850    $request->{sender} = '<>' unless ($request->{sender});
2851
2852    # load postfwd_items attributes
2853    postfwd_items ($request);
2854
2855    # wipe out old group members
2856    if ( $postfwd_settings{group}{noparent} and $Cleanup_Groups and (scalar keys %Group_Cache > 0) and (($now - ($Cleanup_Groups || 0)) > $postfwd_settings{group}{cleanup}) ) {
2857	$t1 = time();
2858	$t3 = scalar keys %Group_Cache;
2859	cleanup_group_cache($now);
2860	$t2 = time();
2861	log_info ("[CLEANUP] cleaning group-cache needed ".ts($t2 - $t1)
2862		." seconds for group cleanup of "
2863		.($t3 - scalar keys %Group_Cache)." out of ".$t3
2864		." groups after cleanup time ".$postfwd_settings{group}{cleanup}."s")
2865		if ( wantsdebug (qw[ all thisrequest verbose cleanup childcleanup groups ]) or (($t2 - $t1) >= 1) );
2866	$Cleanup_Groups = $t1;
2867    };
2868
2869    # clear dnsbl timeout counters
2870    if ( $Cleanup_Timeouts and ($postfwd_settings{dns}{max_interval} > 0) and (($now - $Cleanup_Timeouts) > $postfwd_settings{dns}{max_interval}) ) {
2871	undef %Timeouts;
2872	log_info ("[CLEANUP] clearing dnsbl timeout counters") if wantsdebug (qw[ all thisrequest cleanup verbose ]);
2873	$Cleanup_Timeouts = $now;
2874    };
2875
2876    # wipe out old cache items
2877    if ( $Cleanup_Rates and ($postfwd_settings{rate}{cleanup} > 0) and (scalar keys %Rate_Cache > 0) and (($now - $Cleanup_Rates) > $postfwd_settings{rate}{cleanup}) ) {
2878	$t1 = time();
2879	$t3 = scalar keys %Rate_Cache;
2880	cleanup_rate_cache($now);
2881	$t2 = time();
2882	log_info ("[CLEANUP] cleaning rate-cache needed ".ts($t2 - $t1)
2883		." seconds for rate cleanup of "
2884		.($t3 - scalar keys %Rate_Cache)." out of ".$t3
2885		." cached items after cleanup time ".$postfwd_settings{rate}{cleanup}."s")
2886		if ( wantsdebug (qw[ all thisrequest verbose rates cleanup childcleanup ]) or (($t2 - $t1) >= 1) );
2887	$Cleanup_Rates = $t1;
2888    };
2889
2890    # Request cache enabled?
2891    if ( $postfwd_settings{request}{ttl} > 0 ) {
2892    	# construct cache identifier
2893	if ($postfwd_settings{cacheid}) {
2894		map { $cacheid .= $request->{$_}.';' if (defined $request->{$_}) } @{$postfwd_settings{cacheid}};
2895	} else {
2896		REQITEM: foreach my $checkreq (sort keys %{$request}) {
2897			next REQITEM unless $request->{$checkreq};
2898			next REQITEM if %AutoCacheID and not defined $AutoCacheID{lc($checkreq)};
2899			next REQITEM if ( ($checkreq eq "instance") or ($checkreq eq "queue_id") or ($checkreq eq "orig"));
2900			next REQITEM if ( $postfwd_settings{request}{no_size} and ($checkreq eq "size") );
2901			next REQITEM if ( $postfwd_settings{request}{no_sender} and ($checkreq eq "sender") );
2902			if ( $postfwd_settings{request}{rdomain_only} and ($checkreq eq "recipient") ) {
2903				$cacheid .= $request->{recipient_domain}.';';
2904			} else {
2905				$cacheid .= $request->{$checkreq}.';';
2906			};
2907		};
2908	};
2909	$cacheid = md5_base64($cacheid) if ($DIGESTMD5 and $postfwd_settings{request}{usemd5} and length($cacheid) > 32);
2910	log_info ("created cache-id: $cacheid") if wantsdebug (qw[ all thisrequest cache ]);
2911
2912    	# wipe out old cache entries
2913	if ( $Cleanup_Requests and (scalar keys %Request_Cache > 0) and (($now - $Cleanup_Requests) > $postfwd_settings{request}{cleanup}) ) {
2914		$t1 = time();
2915		$t3 = scalar keys %Request_Cache;
2916		cleanup_request_cache($now);
2917		$t2 = time();
2918		log_info ("[CLEANUP] cleaning request-cache needed ".ts($t2 - $t1)
2919			." seconds for request cleanup of "
2920			.($t3 - scalar keys %Request_Cache)." out of ".$t3
2921			." cached items after cleanup time ".$postfwd_settings{request}{cleanup}."s")
2922			if ( wantsdebug (qw[ all thisrequest verbose cleanup childcleanup ]) or (($t2 - $t1) >= 1) );
2923		$Cleanup_Requests = $t1;
2924	};
2925    };
2926
2927    # check own cache
2928    if ( ($postfwd_settings{request}{ttl} > 0)
2929	and ((exists($Request_Cache{$cacheid}{$COMP_ACTION})) and ($now <= $Request_Cache{$cacheid}{'until'})) ) {
2930	$counters .= $postfwd_settings{sepreq}."ccache=1";
2931	$postfixaction = $Request_Cache{$cacheid}{$COMP_ACTION};
2932	if ( $Request_Cache{$cacheid}{hit} ) {
2933		$Matches{$Request_Cache{$cacheid}{$COMP_ID}}++;
2934		$rulehits = join $postfwd_settings{sepreq}, (split ';', $Request_Cache{$cacheid}{hits}) if $Request_Cache{$cacheid}{hits};
2935		log_info ("[CACHE] rule=".$Rule_by_ID{$Request_Cache{$cacheid}{$COMP_ID}}
2936			. ", id=".$Request_Cache{$cacheid}{$COMP_ID}
2937			. ( ($request->{queue_id}) ? ", queue=".$request->{queue_id} : '' )
2938			. ", client=".$request->{client_name}."[".$request->{client_address}."]"
2939			. ( ($request->{sasl_username}) ? ", user=".$request->{sasl_username} : '' )
2940			. ", sender=<".(($request->{sender} eq '<>') ? "" : $request->{sender}).">"
2941			. ( ($request->{recipient}) ? ", recipient=<".$request->{recipient}.">" : '' )
2942			. ", helo=<".$request->{helo_name}.">"
2943			. ", proto=".$request->{protocol_name}
2944			. ", state=".$request->{protocol_state}
2945			. ", delay=".ts(time() - $now)."s"
2946			. ", hits=".$Request_Cache{$cacheid}{hits}
2947			. ", action=".$postfixaction
2948			) unless $postfwd_settings{request}{nolog};
2949	};
2950
2951    # check parent cache
2952    } elsif ( ($postfwd_settings{request}{ttl} > 0)
2953	and not($postfwd_settings{request}{noparent})
2954	and not((my $pans = cache_query ("CMD=".$postfwd_commands{getcacheitem}.";TYPE=request;ITEM=$cacheid")) eq '<undef>') ) {
2955	map { $Request_Cache{$cacheid}{$1} = $2 if m/$postfwd_patterns{keyval}/ } (split $postfwd_settings{sepreq}, $pans);
2956	$counters .= $postfwd_settings{sepreq}."pcache=1";
2957	$postfixaction = $Request_Cache{$cacheid}{$COMP_ACTION};
2958	if ( $Request_Cache{$cacheid}{hit} ) {
2959		$Matches{$Request_Cache{$cacheid}{$COMP_ID}}++;
2960		$rulehits = join $postfwd_settings{sepreq}, (split ';', $Request_Cache{$cacheid}{hits}) if $Request_Cache{$cacheid}{hits};
2961		log_info ("[CACHE] rule=".$Rule_by_ID{$Request_Cache{$cacheid}{$COMP_ID}}
2962			. ", id=".$Request_Cache{$cacheid}{$COMP_ID}
2963			. ( ($request->{queue_id}) ? ", queue=".$request->{queue_id} : '' )
2964			. ", client=".$request->{client_name}."[".$request->{client_address}."]"
2965			. ( ($request->{sasl_username}) ? ", user=".$request->{sasl_username} : '' )
2966			. ", sender=<".(($request->{sender} eq '<>') ? "" : $request->{sender}).">"
2967			. ( ($request->{recipient}) ? ", recipient=<".$request->{recipient}.">" : '' )
2968			. ", helo=<".$request->{helo_name}.">"
2969			. ", proto=".$request->{protocol_name}
2970			. ", state=".$request->{protocol_state}
2971			. ", delay=".ts(time() - $now)."s"
2972			. ", hits=".$Request_Cache{$cacheid}{hits}
2973			. ", action=".$postfixaction
2974			) unless $postfwd_settings{request}{nolog};
2975	};
2976
2977    # check rules
2978    } else {
2979
2980	# refresh config if '-I' was set
2981	read_config(0) if $postfwd_settings{instant};
2982
2983	if ($#Rules < 0) {
2984		log_note("critical: no rules found - i feel useless (have you set -f or -r?)");
2985
2986	} else {
2987
2988		# clean up rbl cache
2989		if ( not($postfwd_settings{dns}{disabled}) and (scalar keys %DNS_Cache > 0) and (($now - ($Cleanup_RBLs || 0)) > $postfwd_settings{dns}{cleanup}) ) {
2990			$t1 = time();
2991			$t3 = scalar keys %DNS_Cache;
2992			cleanup_dns_cache($now);
2993			$t2 = time();
2994			log_info ("[CLEANUP] cleaning dns-cache needed ".ts($t2 - $t1)
2995				." seconds for rbl cleanup of "
2996				.($t3 - scalar keys %DNS_Cache)." out of ".$t3
2997				." cached items after cleanup time ".$postfwd_settings{dns}{cleanup}."s")
2998				if ( wantsdebug (qw[ all thisrequest verbose cleanup childcleanup ]) or (($t2 - $t1) >= 1) );
2999			$Cleanup_RBLs = $t1;
3000		};
3001
3002		# prepares hit counters
3003		$request->{$COMP_MATCHES}   = 0;
3004		$request->{$COMP_RBL_CNT}   = 0;
3005		$request->{$COMP_RHSBL_CNT} = 0;
3006
3007		RULE: for ($index=0;$index<=$#Rules;$index++) {
3008
3009			# compare request against rule
3010			next unless exists $Rules[$index];
3011			($matched,$rblcnt,$rhlcnt,my $compcnt) = compare_rule ($now, $index, $date, $request);
3012
3013			# enables/overrides hit counters for later use
3014			$request->{$COMP_MATCHES}    = $matched;
3015			$request->{$COMP_RBL_CNT}    = $rblcnt;
3016			$request->{$COMP_RHSBL_CNT}  = $rhlcnt;
3017			$counters .= $postfwd_settings{sepreq}.$compcnt if $compcnt;
3018
3019			# matched? prepare logline, increase counters
3020			if ($stop = $matched > 0) {
3021				@ruleactions = @{$Rules[$index]{$COMP_ACTION}};
3022				$Matches{$Rules[$index]{$COMP_ID}}++;
3023				$rulehits .= $postfwd_settings{sepreq} if $rulehits;
3024				$rulehits .= $Rules[$index]{$COMP_ID};
3025				$request->{$COMP_HITS} .= ';' if (defined $request->{$COMP_HITS});
3026				$request->{$COMP_HITS} .= $Rules[$index]{$COMP_ID};
3027				$myline = "rule=".$index
3028					. ", id=".$Rules[$index]{$COMP_ID}
3029					. ( ($request->{queue_id}) ? ", queue=".$request->{queue_id} : '' )
3030					. ", client=".$request->{client_name}."[".$request->{client_address}."]"
3031					. ( ($request->{sasl_username}) ? ", user=".$request->{sasl_username} : '' )
3032					. ", sender=<".(($request->{sender} eq '<>') ? "" : $request->{sender}).">"
3033					. ( ($request->{recipient}) ? ", recipient=<".$request->{recipient}.">" : '' )
3034					. ", helo=<".$request->{helo_name}.">"
3035					. ", proto=".$request->{protocol_name}
3036					. ", state=".$request->{protocol_state};
3037
3038				# check for postfwd action
3039				ACT: foreach my $act (@ruleactions) {
3040					# substitute check for $$vars in action
3041					$act = $var if ( $var = devar_item ("==",$act,"action",$request) );
3042					$stop = 1; $ai = 0; # (re)set max_command_recursion counter
3043					while ($ai++ < $postfwd_settings{max_command_recursion} and $act =~ /$COMP_ACTION_MATCH/) {
3044						my($mycmd,$myarg) = ($1, $2); $stop = 0;
3045						if (defined $postfwd_actions{$mycmd}) {
3046							log_info ("[PLUGIN] executing postfwd-action $mycmd") if wantsdebug (qw[ all thisrequest ]);
3047							($stop, $index, $act, $myline) = &{$postfwd_actions{$mycmd}}($index, $now, $mycmd, $myarg, $myline, $request);
3048							$rhit = "rate" if ($stop and $mycmd =~ /^(rate|size|rcpt)$/);
3049							# substitute again after postfwd-actions
3050							$act = $var if ( $var = devar_item ("==",$act,"action",$request) );
3051						} else {
3052							log_warn ("[RULES] ".$myline." - error: unknown command \"".$1."\" - ignoring");
3053							$act = $postfwd_settings{default};
3054						};
3055					};
3056					$postfixaction = $act;
3057					last ACT if $stop;
3058				};
3059				if ($stop) {
3060					$myline .= ", delay=".ts(time() - $now)."s, hits=".$request->{$COMP_HITS}.", action=".$postfixaction;
3061    					log_info ("[RULES] ".$myline) unless $postfwd_settings{request}{nolog};
3062					$counters .= $postfwd_settings{sepreq}.$rhit."=1";
3063					# update cache
3064					if ( $postfwd_settings{request}{ttl} > 0 ) {
3065						$Request_Cache{$cacheid}{ttl}		    = ($Rules[$index]{$COMP_CACHE} || $postfwd_settings{request}{ttl});
3066						$Request_Cache{$cacheid}{'until'}	    = $now + $Request_Cache{$cacheid}{ttl};
3067						$Request_Cache{$cacheid}{$COMP_ACTION}	    = $postfixaction;
3068						$Request_Cache{$cacheid}{$COMP_ID}    	    = $Rules[$index]{$COMP_ID};
3069						$Request_Cache{$cacheid}{hit}	    	    = $matched;
3070						$Request_Cache{$cacheid}{hits}		    = $request->{$COMP_HITS};
3071						cache_query ("CMD=".$postfwd_commands{setcacheitem}.";TYPE=request;ITEM=$cacheid".hash_to_str($Request_Cache{$cacheid}))
3072							unless ($postfwd_settings{request}{noparent});
3073					};
3074					last RULE;
3075				};
3076			} else { undef $myline; };
3077		};
3078	};
3079    };
3080    # increase counters and return action
3081    if ($postfwd_settings{summary}) {
3082	if (defined $parent) {
3083		print $parent "CMD=".$postfwd_commands{countcache}.";TYPE=$counters"
3084		    .(($rulehits) ? $postfwd_settings{seplst}."CMD=".$postfwd_commands{matchcache}.";TYPE=$rulehits" : "")
3085		    ."\n"; $parent->getline();
3086	} else {
3087		count_cache(undef, $counters) if $counters;
3088		match_cache(undef, $rulehits) if $rulehits;
3089	};
3090    };
3091    $postfixaction = $postfwd_settings{default} if ($postfwd_settings{test} or !($postfixaction));
3092    map { log_info ("  %$_") } hash_to_list ('Request_Cache', \%Request_Cache)	if wantsdebug (qw[ child_cache child_request_cache ]);
3093    map { log_info ("  %$_") } hash_to_list ('Rate_Cache', \%Rate_Cache)	if wantsdebug (qw[ child_cache child_rate_cache ]);
3094    map { log_info ("  %$_") } hash_to_list ('DNS_Cache', \%DNS_Cache)		if wantsdebug (qw[ child_cache child_dns_cache ]);
3095    return $postfixaction;
3096};
3097
3098# increase counters
3099sub count_cache { map { $Count{$1} += $2 if m/$postfwd_patterns{cntval}/ } (split ($postfwd_settings{sepreq}, $_[1])) if $_[1] };
3100
3101# increase matches
3102sub match_cache { map { $Hits{$_}++ } (split ($postfwd_settings{sepreq}, $_[1])) if $_[1] };
3103
3104# program usage statistics
3105sub list_stats {
3106	my $now     = time();
3107	my $uptime  = $now - $StartTime;
3108	my @output  =();
3109	return @output unless $uptime and $Count{request};
3110
3111	# averages, hitrates and counters
3112	map { $Count{$_} ||= 0 } qw(ruleset interval top rate pcache ccache dnsqueries dnstimeouts dnsinterval dnstop);
3113	my $lastreq = (($now - $Summary) > 0) ? $Count{interval} / ($now - $Summary) * 60 : 0;
3114	my $lastdns = (($now - $Summary) > 0) ? $Count{dnsinterval} / ($now - $Summary) * 60 : 0;
3115	$Count{top} = $lastreq if $lastreq > $Count{top};
3116	$Count{dnstop} = $lastdns if $lastdns > $Count{dnstop};
3117	my $dnstimeoutrate = ($Count{dnsqueries}) ? $Count{dnstimeouts} / $Count{dnsqueries} * 100 : 0;
3118
3119	# log program  statistics
3120	if ( not($postfwd_settings{syslog}{noidlestats}) or ($Count{interval} > 0) ) {
3121		push ( @output, sprintf (
3122			"[STATS] %s::policy %s: %d requests since %d days, %02d:%02d:%02d hours",
3123			$postfwd_settings{name},
3124			$postfwd_settings{version},
3125			$Count{request},
3126			($uptime / 60 / 60 / 24),
3127			(($uptime / 60 / 60) % 24),
3128			(($uptime / 60) % 60),
3129			($uptime % 60)
3130		) );
3131
3132		push ( @output, sprintf (
3133			"[STATS] Requests: %.2f/min last, %.2f/min overall, %.2f/min top",
3134			$lastreq,
3135			($uptime) ? $Count{request} / $uptime * 60 : 0,
3136			$Count{top}
3137		) );
3138
3139		push ( @output, sprintf (
3140			"[STATS] Dnsstats: %.2f/min last, %.2f/min overall, %.2f/min top",
3141			$lastdns,
3142			($uptime) ? $Count{dnsqueries} / $uptime * 60 : 0,
3143			$Count{dnstop}
3144		) ) unless ($postfwd_settings{dns}{disable});
3145
3146		push ( @output, sprintf (
3147			"[STATS] Hitrates: %.1f%% ruleset, %.1f%% parent, %.1f%% child, %.1f%% rates",
3148			($Count{request}) ? $Count{ruleset} / $Count{request} * 100 : 0,
3149			($Count{request}) ? $Count{pcache} / $Count{request} * 100 : 0,
3150			($Count{request}) ? $Count{ccache} / $Count{request} * 100 : 0,
3151			($Count{request}) ? $Count{rate} / $Count{request} * 100 : 0
3152		) );
3153
3154		push ( @output, sprintf (
3155			"[STATS] Timeouts: %.1f%% (%d of %d dns queries)",
3156			$dnstimeoutrate,
3157			$Count{dnstimeouts},
3158			$Count{dnsqueries}
3159		) ) unless ($postfwd_settings{dns}{disable});
3160
3161		# per rule stats
3162		if (%Hits and not($postfwd_settings{syslog}{norulestats})) {
3163			my @rulecharts = (sort { ($Hits{$b} || 0) <=> ($Hits{$a} || 0) } (keys %Hits)); my $cntln = length(($Hits{$rulecharts[0]} || 2)) + 2;
3164			map { push ( @output, sprintf ("[STATS] %".$cntln."d matches for id:  %s", ($Hits{$_} || 0), $_)) } @rulecharts;
3165		};
3166	};
3167
3168	$Count{interval} = $Count{dnsinterval} = 0;
3169	$Summary = $now;
3170	return @output;
3171};
3172
3173sub set_personality {
3174	@ISA = 'Net::Server::'.$postfwd_settings{personality};
3175};
3176
3177## Net::Server::PreFork methods
3178
3179# ignore syslog failures
3180sub handle_syslog_error {};
3181
3182# reload config on HUP signal
3183sub sig_hup {
3184	my $self = shift;
3185	log_note ("catched HUP signal - reloading ruleset on next request");
3186	read_config(1);
3187	map { kill ("HUP", $_) } (keys %{$self->{server}->{children}}) if defined $self->{server}->{children};
3188};
3189
3190# server end
3191sub post_child_cleanup_hook {
3192	if ($postfwd_settings{personality} eq 'Multiplex') {
3193		save_rates();
3194		save_groups();
3195	};
3196};
3197
3198# parent start
3199sub pre_loop_hook {
3200	my $self = shift;
3201	# change parent's name
3202	$0 = $self->{server}->{commandline} = " ".$postfwd_settings{name}.'::policy';
3203	$postfwd_settings{name} .= "/policy";
3204	# load plugin-items
3205	get_plugins (@{$postfwd_settings{Plugins}}) if $postfwd_settings{Plugins};
3206	# read configuration
3207	read_config(1);
3208	if ($postfwd_settings{personality} eq 'Multiplex') {
3209		load_rates();
3210		load_groups();
3211	};
3212	$StartTime = $Summary = $Cleanup_Timeouts = $Cleanup_Requests = $Cleanup_RBLs = $Cleanup_Rates = $Cleanup_Groups = time();
3213	log_info ("ready for input");
3214};
3215
3216# parent processes child input
3217sub child_is_talking_hook {
3218    my($self,$sock) = @_;
3219    my $answer = "\n";
3220    my $msg = $sock->getline();
3221    # during tests it turned out that children
3222    # send empty messages in some situations
3223    if (defined $msg) {
3224	log_info ("child said '$msg'") if wantsdebug (qw[ all ]);
3225	if ($msg =~ m/$postfwd_patterns{command}/) {
3226	    foreach (split $postfwd_settings{seplst}, $msg) {
3227		if (m/$postfwd_patterns{countcache}/) {
3228		    $self->count_cache($1);
3229		} elsif (m/$postfwd_patterns{matchcache}/) {
3230		    $self->match_cache($1);
3231		} elsif (m/$postfwd_patterns{dumpstats}/) {
3232		    $answer = (join $postfwd_settings{sepreq}.$postfwd_settings{seplst}, list_stats())."\n";
3233		} else {
3234		    log_note ("warning: child sent unknown command '$_'");
3235		};
3236	    };
3237	} else {
3238	    log_note ("warning: child sent unknown message '$msg'");
3239	};
3240    };
3241    print $sock "$answer";
3242};
3243
3244# child start
3245sub child_init_hook {
3246	my $self = shift;
3247	# change children's names
3248	$0 = $self->{server}->{commandline} = " ".$postfwd_settings{name}.'::policy::child';
3249	log_info ("ready for input") if wantsdebug (qw[ all verbose child ]);
3250};
3251
3252# child process request
3253sub process_request {
3254	my($self) = shift;
3255	my(%attr) = ();
3256	my($client) = $self->{server}->{client};
3257	my($parent) = $self->{server}->{parent_sock};
3258	while (<$client>) {
3259		s/\r?\n$//;
3260		# respond to masters ping
3261		if ($_ eq $postfwd_patterns{ping}) {
3262			$client->print("$postfwd_patterns{pong}\n");
3263		} elsif (m/$postfwd_patterns{dumpstats}/) {
3264			$parent->print("$_\n");
3265			$client->print($parent->getline()."\n");
3266		# process input
3267		} else {
3268			process_input ($parent, $client, $_, \%attr);
3269		};
3270	};
3271};
3272
3273# multiplex process request
3274sub mux_input() {
3275        my ($self, $mux, $client, $data) = @_;
3276	my(%attr) = ();
3277        my ($request) = undef;
3278        # check request and print output
3279        while ( $$data =~ s/^([^\r\n]*)\r?\n// ) {
3280                # check request line and print output
3281                next unless defined $1;
3282                $request = $1;
3283		# respond to masters ping
3284		if ($request eq $postfwd_patterns{ping}) {
3285			$client->print("$postfwd_patterns{pong}\n");
3286		} elsif ($request =~ m/$postfwd_patterns{dumpstats}/) {
3287			$client->print((join $postfwd_settings{sepreq}.$postfwd_settings{seplst}, list_stats())."\n");
3288		} elsif ($request =~ m/$postfwd_patterns{dumpcache}/) {
3289			$client->print((join $postfwd_settings{sepreq}.$postfwd_settings{seplst}, dump_cache())."\n");
3290		} elsif ($request =~ m/$postfwd_patterns{delrate}/) {
3291			my $del = $1; $del =~ s/^[%]?//;
3292			my $action;
3293			if (defined $Rate_Cache{$del}) {
3294				delete $Rate_Cache{$del};
3295				log_info ("[DELRATEITEM] rate cache item '$del' removed");
3296				$action = "rate cache item '$del' removed";
3297			} else {
3298				log_info ("[DELRATEITEM] rate cache removal of '$del' failed: item not found");
3299				$action = "rate cache removal of '$del' failed: item not found";
3300			};
3301			$client->print((join $postfwd_settings{sepreq}.$postfwd_settings{seplst}, $action)."\n");
3302		# process input
3303		} else {
3304                	process_input (undef, $client, $request, \%attr);
3305        	};
3306        };
3307};
3308
3309# process delegation protocol input
3310sub process_input {
3311	my($parent,$client,$msg,$attr) = @_;
3312	# remember argument=value
3313	if ( $msg =~ /^([^=]{1,512})=(.{0,512})/ ) {
3314		$$attr{$1} = $2;
3315	# evaluate request
3316	} elsif ( $msg eq '' ) {
3317		map { log_info ("Attribute: $_=$$attr{$_}") } (keys %$attr) if wantsdebug (qw[ all thisrequest request ]);
3318		unless ( (defined $$attr{request}) and ($$attr{request} eq "smtpd_access_policy") ) {
3319			log_note ("Ignoring unrecognized request type: '".((defined $$attr{request}) ? substr($$attr{request},0,100) : '')."'");
3320		} else {
3321			my $action = smtpd_access_policy($parent, \%$attr) || $postfwd_settings{default};
3322			log_info ("Action: $action") if wantsdebug (qw[ all thisrequest verbose ]);
3323			if ($client) {
3324				print $client ("action=$action\n\n");
3325			} else {
3326				print STDOUT ("action=$action\n\n");
3327			};
3328			%$attr = (); delete $postfwd_settings{debug}{thisrequest};
3329		};
3330	# unknown command
3331	} else {
3332		log_note ("Ignoring garbage '".substr($msg, 0, 100)."'");
3333	};
3334};
3335
33361; # EOF postfwd3::server
3337
3338
3339## postfwd::master Start
3340
3341use warnings;
3342use strict;
3343use Getopt::Long 2.25 qw(:config no_ignore_case bundling);
3344use Pod::Usage;
3345use Data::Dumper;
3346# master daemon
3347use Net::Server::Daemonize qw(daemonize);
3348# own modules
3349# program settings, syslogging
3350import postfwd3::basic qw(:DEFAULT %postfwd_commands &check_inet &check_unix &wantsdebug &hash_to_list $TIMEHIRES $NETADDR $DIGESTMD5);
3351# cache daemon (requests, dns, limits), Net::Server::Multiplex
3352import postfwd3::cache qw();
3353# policy daemon, Net::Server::PreFork
3354import postfwd3::server qw(&read_config &show_config &process_input &get_plugins);
3355
3356
3357# functions to start, override with '--daemons' at command line
3358my @daemons = @{$postfwd_settings{master}{daemons}};
3359
3360use vars qw(
3361	%options %children %failures
3362);
3363
3364# parse command-line
3365my $Commandargs = ' '.(join ' ', @ARGV);
3366my $Commandline = $0.$Commandargs;
3367# read settings files before loading commandline options ...
3368while ($Commandargs =~ s/\s+\-(\-load(settings)?|F)[=\s]*(\S+)\b//) { load_settings($3) };
3369# ... and allow commandline options to override
3370GetOptions( \%options,
3371	# Ruleset
3372	'rule|r=s'		  => sub{ my($opt,$value) = @_; push (@{$postfwd_settings{Configs}}, $opt.$postfwd_settings{sepreq}.$value) },
3373	'file|f=s'		  => sub{ my($opt,$value) = @_; push (@{$postfwd_settings{Configs}}, $opt.$postfwd_settings{sepreq}.$value) },
3374	"loadsettings|load|F=s",
3375	"showsettings|savesettings|settings|save:s",
3376	'scores|score|s=s%'	  => \%{$postfwd_settings{scores}},
3377        "test|t"		  => \$postfwd_settings{test},
3378        "instantcfg|I"		  => \$postfwd_settings{instant},
3379	"config_timeout=i"	  => \$postfwd_settings{timeout}{config},
3380	"no_netaddr",
3381	"no_netcidr",
3382	"cidr_method=s",
3383	"aggregate_addrs|A!",
3384	"showconfig|C",
3385	"defaults|D",
3386	# Networking
3387	"umask=s"		  => \$postfwd_settings{base}{umask},
3388	"user|u=s"		  => \$postfwd_settings{base}{user},
3389	"group|g=s"		  => \$postfwd_settings{base}{group},
3390	"server_socket|socket=s"  => sub{ ($postfwd_settings{server}{proto}, $postfwd_settings{server}{host}, $postfwd_settings{server}{port}) = (split ':', $_[1]) },
3391	"interface|i=s"		  => \$postfwd_settings{server}{host},
3392	"port|p=s"		  => \$postfwd_settings{server}{port},
3393	"proto=s"		  => \$postfwd_settings{server}{proto},
3394	"server_umask=s"	  => \$postfwd_settings{server}{umask},
3395	"min_servers=i"	 	  => \$postfwd_settings{server}{min_servers},
3396	"max_servers=i"	 	  => \$postfwd_settings{server}{max_servers},
3397	"min_spare_servers=i"	  => \$postfwd_settings{server}{min_spare_servers},
3398	"max_spare_servers=i"	  => \$postfwd_settings{server}{max_spare_servers},
3399        "nodns|n"		  => \$postfwd_settings{dns}{disabled},
3400	"dns_timeout=i"		  => \$postfwd_settings{dns}{timeout},
3401	"dns_async_txt"		  => \$postfwd_settings{dns}{async_txt},
3402	"dns_timeout_max=i"	  => \$postfwd_settings{dns}{max_timeout},
3403	"dns_timeout_interval=i"  => \$postfwd_settings{dns}{max_interval},
3404	"dns_max_ns_lookups=i"	  => \$postfwd_settings{dns}{max_ns_lookups},
3405	"dns_max_mx_lookups=i"	  => \$postfwd_settings{dns}{max_mx_lookups},
3406	"ipv6_dnsbl"		  => \$postfwd_settings{dns}{ipv6_dnsbl},
3407	"cache-rbl-timeout=i"	  => \$postfwd_settings{dns}{ttl},
3408	"cache-rbl-default=s"	  => \$postfwd_settings{dns}{mask},
3409	"cleanup-rbls=i"	  => \$postfwd_settings{dns}{cleanup},
3410	"no_parent_dns_cache"	  => \$postfwd_settings{dns}{noparent},
3411	"parent_dns_cache"	  => sub { $postfwd_settings{dns}{noparent} = 0 },
3412	# Stats
3413	"summary|stats|S=i"	  => \$postfwd_settings{summary},
3414	"norulestats"		  => \$postfwd_settings{syslog}{norulestats},
3415	"no-rulestats"		  => \$postfwd_settings{syslog}{norulestats},
3416	"noidlestats"		  => \$postfwd_settings{syslog}{noidlestats},
3417	"no-idlestats"		  => \$postfwd_settings{syslog}{noidlestats},
3418	"stdoutlog|stdout|L"	  => \$postfwd_settings{syslog}{stdout},
3419	"stdin"	  		  => \$postfwd_settings{syslog}{stdin},
3420	"commandline|command|cmd" => sub{ $postfwd_settings{syslog}{stdin} = $postfwd_settings{syslog}{stdout} = 1; $postfwd_settings{daemon} = 0 },
3421	# Cache
3422	"cache_socket=s"	  => sub{ ($postfwd_settings{cache}{proto}, $postfwd_settings{cache}{host}, $postfwd_settings{cache}{port}) = (split ':', $_[1]) },
3423	"cache_interface=s"	  => \$postfwd_settings{cache}{host},
3424	"cache_port=s"		  => \$postfwd_settings{cache}{port},
3425	"cache_proto=s"		  => \$postfwd_settings{cache}{proto},
3426	"cache_umask=s"		  => \$postfwd_settings{server}{umask},
3427	"cache|c=i"		  => \$postfwd_settings{request}{ttl},
3428	"cacheid=s"		  => sub { push @{$postfwd_settings{cacheid}}, (split /[,\s]+/, $_[1]) },
3429	"cacheid_md5!"		  => \$postfwd_settings{request}{usemd5},
3430	"cache-rdomain-only"	  => \$postfwd_settings{request}{rdomain_only},
3431	"cache-no-sender"	  => \$postfwd_settings{request}{no_sender},
3432	"cache-no-size"		  => \$postfwd_settings{request}{no_size},
3433	"cleanup-requests=i"	  => \$postfwd_settings{request}{cleanup},
3434	"no_parent_request_cache" => \$postfwd_settings{request}{noparent},
3435	"no_parent_rate_cache"    => \$postfwd_settings{rate}{noparent},
3436	"no_parent_cache"         => sub{ $postfwd_settings{request}{noparent} = $postfwd_settings{rate}{noparent} = $postfwd_settings{dns}{noparent} = $postfwd_settings{group}{noparent} = 1 },
3437	# Groups
3438	"no_parent_group_cache"   => \$postfwd_settings{group}{noparent},
3439	"default_group_ttl=i"	  => \$postfwd_settings{group}{ttl},
3440	"group_maxitems=i"	  => \$postfwd_settings{group}{maxitems},
3441	# Limits
3442	"cleanup-rates=i"	  => \$postfwd_settings{rate}{cleanup},
3443	"keep_rates|keep_limits|keep_rates_on_reload" => \$postfwd_settings{keep_rates},
3444	"keep_groups|keep_groups_on_reload" => \$postfwd_settings{keep_groups},
3445	"save_rates|save_limits|save_rates_on_restart=s" => \$postfwd_settings{rate}{store},
3446	"save_groups|save_groups_on_restart=s" => \$postfwd_settings{group}{store},
3447	"fast_limit_evaluation",
3448	# Control
3449	'version|V'		  => sub{ print "$postfwd_settings{name} $postfwd_settings{version} (Net::DNS ".(Net::DNS->VERSION || '<undef>').", Net::Server ".(Net::Server->VERSION || '<undef>').(($NETADDR) ? ", NetAddr::IP $NETADDR" : '').(($NETCIDR) ? ", Net::CIDR::Lite $NETCIDR" : '').(($DIGESTMD5) ? ", Digest::MD5 $DIGESTMD5" : '').", Sys::Syslog ".($Sys::Syslog::VERSION || '<undef>').", ".(($TIMEHIRES) ? "Time::HiRes $TIMEHIRES, " : '').(($STORABLE) ? "Storable $STORABLE, " : '')."Perl ".$]." on ".$^O.")\n"; exit; },
3450	'versionshort|shortversion' => sub{ print "$postfwd_settings{version}\n"; exit; },
3451	'manual|m'		  => sub{ # contructing command string (de-tainting $0)
3452                                          $postfwd_settings{manual} .= ($0 =~ /^([-\@\/\w. ]+)$/) ? " \"".$1 : " \"".$postfwd_settings{name};
3453                                          $postfwd_settings{manual} .= "\" | ".$postfwd_settings{pager};
3454                                          system ($postfwd_settings{manual}); exit; },
3455        "term|kill|stop|k",
3456        "hup|reload",
3457        "delcache=s",
3458        "delrate=s",
3459        "dumpcache",
3460        "dumpstats",
3461        "pid|pidfile|pid_file=s"  => \$postfwd_settings{master}{pid_file},
3462        "watchdog=i"		  => \$postfwd_settings{master}{watchdog},
3463        "respawn=i"		  => \$postfwd_settings{master}{respawn},
3464        "failures=i"		  => \$postfwd_settings{master}{failures},
3465	"daemon|d!"		  => \$postfwd_settings{daemon},
3466	"daemons=s"		  => sub { push @{$options{daemons}}, (split /[,\s]+/, $_[1]) },
3467	"personality=s",
3468	"autopersonality!"	  => \$postfwd_settings{autopersonality},
3469	"v1|postfwd1"		  => sub { $options{personality} = 'Multiplex' },
3470	"v2|postfwd3"		  => sub { $options{personality} = 'PreFork' },
3471	"chroot|R=s"		  => \$postfwd_settings{chroot},
3472	# Logging
3473        "debug=s"		  => sub { push @{$options{debug}}, (split /[,\s]+/, $_[1]) },
3474        "debugclasses"		  => sub { my %tags = (); my $pfwd = undef; open ($pfwd, '<', $0) or die "Can not open '$0': $! - $@\n\n"; while (<$pfwd>) { chomp; next unless /wantsdebug\s+\(\s*qw\[\s*([^\]]+)\]/; my $cstr = $1; map { $tags{$_}++ } (split /\s+/, $cstr) }; close($pfwd); print STDERR "\n HOOKS NAME\n ----- ----\n"; map { printf STDERR "%6d %s\n", $tags{$_}, $_ } (sort keys %tags); print STDERR "\n"; exit },
3475        "verbose|v+"		  => \$postfwd_settings{verbose},
3476	"logname|l=s"		  => sub{ $postfwd_settings{name}		= $_[1];
3477				     $postfwd_settings{cache}{syslog_ident}	= $_[1].'/cache';
3478				     $postfwd_settings{server}{syslog_ident}	= $_[1].'/policy'; },
3479	"facility=s"		  => \$postfwd_settings{syslog}{facility},
3480	"socktype=s"		  => \$postfwd_settings{syslog}{socktype},
3481	"nodnslog"		  => \$postfwd_settings{dns}{nolog},
3482	"no-dnslog"		  => \$postfwd_settings{dns}{nolog},
3483	"anydnslog"		  => \$postfwd_settings{dns}{anylog},
3484	"norulelog"		  => \$postfwd_settings{request}{nolog},
3485	"no-rulelog"		  => \$postfwd_settings{request}{nolog},
3486	"perfmon|P"		  => \$postfwd_settings{syslog}{nolog},
3487	"plugins=s"		  => sub { push @{$postfwd_settings{Plugins}}, $_[1] },
3488
3489	# Unused
3490	"start",
3491	"shortlog",
3492	"dns_queuesize=i",
3493	"dns_retries=i",
3494) or pod2usage (-msg => "\nPlease see \"".$postfwd_settings{name}." -m\" for detailed instructions.\n", -verbose => 1);
3495
3496# basic syntax checks
3497if ($postfwd_settings{verbose} > 1) {
3498	$postfwd_settings{debug}{all} = 1;
3499} elsif ($postfwd_settings{verbose}) {
3500	$postfwd_settings{debug}{verbose} = 1;
3501};
3502# set personality by argument or ...
3503if (defined $options{personality}) {
3504	$postfwd_settings{personality} = $options{personality};
3505# ... determine personality by program name to keep old scripts compatible
3506} elsif ($postfwd_settings{autopersonality}) {
3507    $postfwd_settings{personality} = 'Multiplex' if $0 =~ /postfwd[1]?$/i;
3508    $postfwd_settings{personality} = 'PreFork'   if $0 =~ /postfwd2$/i;
3509};
3510die "\nERROR: Bad personality type! --personality must be 'Multiplex' or 'Prefork'. You can use --v1 or --v2 as shortcut.\n\n" unless $postfwd_settings{personality} =~ /^(Multiplex|PreFork)$/;
3511if ($postfwd_settings{personality} eq 'Multiplex') {
3512	$postfwd_settings{request}{noparent} = $postfwd_settings{rate}{noparent} = $postfwd_settings{dns}{noparent} = $postfwd_settings{group}{noparent} = 1;
3513	@{$postfwd_settings{master}{daemons}} = @daemons = qw( server );
3514};
3515map { $postfwd_settings{debug}{$_} = 1 } uniq(@{$options{debug}});
3516map { $postfwd_settings{daemons}{$_} = 1 } ((defined $options{daemons}) ? uniq(@{$options{daemons}}) : uniq(@daemons));
3517map { $postfwd_settings{$_}{check} = ($postfwd_settings{$_}{proto} eq 'unix') ? \&check_unix : \&check_inet } @daemons;
3518
3519# de-taint command-line
3520%postfwd_settings = detaint_hash (%postfwd_settings);
3521
3522# check for commands (e.g. 'postfwd stop')
3523if (@ARGV) {
3524	if ($ARGV[0] =~ /^(term|kill|stop)$/i) {
3525		$options{'term'} = 1;
3526	} elsif ($ARGV[0] =~ /^(hup|reload)$/i) {
3527		$options{'hup'} = 1;
3528	} elsif ($ARGV[0] =~ /^(defaults|dumpcache|dumpstats)$/i) {
3529		$options{$1} = 1;
3530	} elsif ($ARGV[0] =~ /^(showsettings|settings)$/i) {
3531		$options{showsettings} = ' ';
3532	} elsif ($ARGV[0] =~ /^(delcache|delrate|debugclasses)$/i) {
3533		my $a = $1;
3534		(defined $ARGV[1] and $ARGV[1] =~ /^(.*)$/)
3535			? $options{$a} = $1
3536			: die "\nCommand '$a' needs an argument!\n\n";
3537	} elsif ($ARGV[0] =~ /^(loadsettings|load)$/i) {
3538		my $a = 'loadsettings';
3539		(defined $ARGV[1] and $ARGV[1] =~ /^(.*)$/)
3540			? $options{$a} = $1
3541			: die "\nCommand '$a' needs an argument!\n\n";
3542	} elsif ($ARGV[0] =~ /^(savesettings|save)$/i) {
3543		my $a = 'showsettings';
3544		(defined $ARGV[1] and $ARGV[1] =~ /^(.*)$/)
3545			? $options{$a} = $1
3546			: die "\nCommand '$a' needs an argument!\n\n";
3547	} else {
3548		die "\nUnknown command '".$ARGV[0]."'\n\n"
3549			unless $ARGV[0] =~ /^(start|run)$/i;
3550	};
3551};
3552map { $postfwd_settings{syslog}{stdout} = 1 if defined $options{$_} } qw(term hup showconfig dumpcache dumpstats defaults delcache delrate debugclasses);
3553
3554# save options
3555%{$postfwd_settings{Options}} = %options;
3556
3557# terminate at -k or --kill
3558if (defined $options{'term'}) {
3559	kill "TERM", get_master_pid();
3560	exit (0);
3561# reload at --reload
3562} elsif (defined $options{'hup'}) {
3563	kill "HUP", get_master_pid();
3564	exit (0);
3565};
3566
3567# chroot master
3568if (defined $postfwd_settings{chroot}) {
3569	unless (eval {chroot($postfwd_settings{chroot});}) {
3570		print "Cannot chroot to $postfwd_settings{chroot}: $!\n";
3571		exit (2);
3572	};
3573};
3574
3575# init_log
3576init_log ($postfwd_settings{name}."/master");
3577log_info ("Loaded program settings from '".$postfwd_settings{Settings}."'") if defined $postfwd_settings{Settings};
3578
3579# determine CIDR functions
3580$NETADDR = 0 if $options{'no_netaddr'};
3581$NETCIDR = 0 if $options{'no_netcidr'};
3582if ($options{'cidr_method'} and $options{'cidr_method'} =~ /^(netcidr|netaddr|postfwd)$/i) {
3583	$postfwd_settings{cidr_method} = lc($1);
3584	if (
3585		( not($NETCIDR) and $postfwd_settings{cidr_method} eq 'netcidr')
3586		or
3587		( not($NETADDR) and $postfwd_settings{cidr_method} eq 'netaddr')
3588	 ) {
3589		log_note("Can not use method '".$postfwd_settings{cidr_method}."' for network checks. Module not available");
3590		$postfwd_settings{cidr_method} = 'postfwd';
3591	};
3592} else {
3593	# prefer Net::CIDR::Lite, because it's a bit faster (v4 and v6 cidr)
3594	if ($NETCIDR) {
3595		$postfwd_settings{cidr_method} = 'netcidr';
3596	# else use NetAddr::IP (v4 and v6 cidr)
3597	} elsif ($NETADDR) {
3598		$postfwd_settings{cidr_method} = 'netaddr';
3599	# else use old builtin method (v4 cidr, v6 regex)
3600	} else {
3601		$postfwd_settings{cidr_method} = 'postfwd';
3602	};
3603};
3604if (defined $postfwd_compare{'cidr_'.$postfwd_settings{cidr_method}}) {
3605	log_info("Using 'cidr_".$postfwd_settings{cidr_method}."'-method for network checks") if wantsdebug (qw[ all verbose cidr ]);
3606	$postfwd_compare{cidr} = $postfwd_compare{'cidr_'.$postfwd_settings{cidr_method}};
3607} else {
3608	log_note("Unknown method 'cidr_".$postfwd_settings{cidr_method}."' for network checks, reverting to 'cidr_postfwd'");
3609	$postfwd_compare{cidr} = $postfwd_compare{cidr_postfwd};
3610};
3611
3612# check for Network aggregation via NetAddr::IP
3613if ($options{aggregate_addrs}) {
3614	if ($NETADDR) {
3615		log_info("Module NetAddr::IP found, aggregating CIDR notated lists") if wantsdebug (qw[ all verbose aggr cidr ]);
3616		$postfwd_settings{aggregate_addrs} = ($options{aggregate_addrs} || 0);
3617	} else {
3618		log_note("Module NetAddr::IP not found, ignoring options --aggregate_addrs");
3619		$postfwd_settings{aggregate_addrs} = 0;
3620	};
3621};
3622
3623# check for Storable
3624if (($postfwd_settings{rate}{keep} or $postfwd_settings{group}{keep}) and not($STORABLE)) {
3625	log_note("Module Storable not found, ignoring options --save_rates and --save_groups");
3626	$postfwd_settings{rate}{keep} = 0; $postfwd_settings{group}{keep} = 0;
3627};
3628
3629# check for Digest::MD5
3630if ($postfwd_settings{request}{usemd5} and not($DIGESTMD5)) {
3631	log_note("Module Digest::MD5 not found, ignoring option --cache_md5");
3632	$postfwd_settings{request}{usemd5} = 0;
3633};
3634
3635# read and display configuration
3636if (defined $options{'showconfig'}) {
3637	read_config(1);
3638        show_config();
3639        exit;
3640};
3641
3642# show program settings
3643if (defined $options{'defaults'}) {
3644	print "\n"; map { print "  %$_\n" } hash_to_list ('postfwd_settings', \%postfwd_settings);
3645	if (wantsdebug (qw[ all verbose ])) {
3646		map { print "  %$_\n" } hash_to_list ('postfwd_commands', \%postfwd_commands);
3647		map { print "  %$_\n" } hash_to_list ('postfwd_patterns', \%postfwd_patterns);
3648	};
3649	print "\n"; exit;
3650};
3651
3652# dump stats
3653if (defined $options{'dumpstats'}) {
3654	foreach my $daemon (sort keys %{$postfwd_settings{daemons}}) {
3655		print "\n";
3656		map { print ("$_\n") } get_stats ($daemon);
3657	};
3658	print "\n";
3659	exit;
3660};
3661
3662# dump cache contents
3663if (defined $options{'dumpcache'}) {
3664	my $dctarget = (defined $postfwd_settings{daemons}{cache}) ? 'cache' : 'server';
3665	print "\n".( join "\n",
3666		split $postfwd_settings{sepreq}.$postfwd_settings{seplst},
3667#			(&{$postfwd_settings{cache}{check}} ('cache', 'CMD=DC;') || '<undef>')
3668			(&{$postfwd_settings{$dctarget}{check}} ($dctarget, 'CMD=DC;') || '<undef>')
3669	)."\n\n";
3670	exit;
3671};
3672
3673# remove cache item
3674if (defined $options{'delcache'} or defined $options{'delrate'}) {
3675	my $dctarget = (defined $postfwd_settings{daemons}{cache}) ? 'cache' : 'server';
3676	print "\n".( join "\n",
3677		split $postfwd_settings{sepreq}.$postfwd_settings{seplst},
3678			(&{$postfwd_settings{$dctarget}{check}} ($dctarget, (($options{'delcache'}) ? 'CMD=RC '.$options{'delcache'} : 'CMD=RR '.$options{'delrate'})) || '<undef>')
3679	)."\n\n";
3680	exit;
3681};
3682
3683# save configuration
3684if (defined $options{'showsettings'}) {
3685	print STDERR "Saving program settings to file '".$options{'showsettings'}."'...\n" if $options{'showsettings'};
3686	save_settings($options{'showsettings'});
3687	exit;
3688};
3689
3690# -n - skip dns based checks
3691log_note ("NODNS: set - will skip all dns based checks") if $postfwd_settings{dns}{disabled};
3692
3693# daemonize master
3694unless ($postfwd_settings{daemon}) {
3695	$postfwd_settings{base}{setsid} = 0;
3696};
3697log_info ($postfwd_settings{name}." "
3698	.$postfwd_settings{version}." starting [daemons: ".(join ',', sort keys %{$postfwd_settings{daemons}})."]"
3699	.((scalar keys %{$postfwd_settings{debug}}) ? " with debug levels: ".(join ',', keys %{$postfwd_settings{debug}}) : '')) unless $postfwd_settings{syslog}{stdin};
3700log_info ("Net::DNS ".(Net::DNS->VERSION || '<undef>').", Net::Server ".(Net::Server->VERSION || '<undef>').(($NETADDR) ? ", NetAddr::IP $NETADDR" : '').(($NETCIDR) ? ", Net::CIDR::Lite $NETCIDR" : '').(($DIGESTMD5) ? ", Digest::MD5 $DIGESTMD5" : '').", Sys::Syslog ".($Sys::Syslog::VERSION || '<undef>').", ".(($TIMEHIRES) ? "Time::HiRes $TIMEHIRES, " : '').(($STORABLE) ? "Storable $STORABLE, " : '')."Perl ".$]." on ".$^O) if wantsdebug (qw[ all verbose ]);
3701umask oct($postfwd_settings{base}{umask});
3702daemonize($postfwd_settings{base}{user}, $postfwd_settings{base}{group}, $postfwd_settings{master}{pid_file}) if $postfwd_settings{daemon};
3703$0 = $Commandline;
3704
3705# prepare shared SIG handlers
3706$SIG{__WARN__} = sub { log_warn("warning: $_[0]") };
3707$SIG{__DIE__}  = sub { log_crit("FATAL: $_[0]"); die @_; };
3708
3709# check for --stdin option
3710if ($postfwd_settings{syslog}{stdin}) {
3711	my(%attr) = ();
3712	get_plugins (@{$postfwd_settings{Plugins}}) if $postfwd_settings{Plugins};
3713	read_config(1);
3714	map { $postfwd_settings{daemons}{$_} = 0 } (keys %{$postfwd_settings{daemons}});
3715	$postfwd_settings{request}{noparent} = $postfwd_settings{rate}{noparent} = $postfwd_settings{dns}{noparent} = $postfwd_settings{group}{noparent} = 1;
3716	while (<>) {
3717		chomp;
3718		process_input (undef, undef, $_, \%attr);
3719	};
3720	exit;
3721};
3722
3723# fork daemons: cache and server
3724umask oct($postfwd_settings{base}{umask});
3725foreach my $daemon (sort keys %{$postfwd_settings{daemons}}) {
3726	umask oct($postfwd_settings{$daemon}{umask});
3727	if (my $pid = spawn_daemon ($daemon)) {
3728		log_info ("Started $daemon at pid $pid");
3729		$children{$daemon} = $pid;
3730	};
3731};
3732$postfwd_settings{name} .= "/master";
3733
3734# prepare master SIG handlers and enter main loop
3735$SIG{TERM} = sub { end_program(); };
3736$SIG{INT} = sub { end_program(); } unless ($postfwd_settings{daemon});
3737$SIG{HUP} = sub { reload_program(); };
3738if ($postfwd_settings{summary}) {
3739	$SIG{ALRM} = sub {
3740		log_stats();
3741		alarm ($postfwd_settings{summary})
3742	};
3743	alarm ($postfwd_settings{summary});
3744};
3745
3746while (1) {
3747	# check daemons every <watchdog> seconds
3748	if ($postfwd_settings{master}{watchdog}) {
3749		sleep ($postfwd_settings{master}{watchdog});
3750		foreach my $daemon (sort keys %{$postfwd_settings{daemons}}) {
3751			if (check_daemon ($daemon)) {
3752				$failures{$daemon} = 0;
3753			} else {
3754				if (++$failures{$daemon} >= $postfwd_settings{master}{failures}) {
3755					# terminate program
3756					log_crit ("$daemon-daemon check failed $failures{$daemon} times - terminating program");
3757					end_program();
3758				} else {
3759					# restart daemon
3760					log_crit ("$daemon-daemon check failed $failures{$daemon} times - respawning in ".$postfwd_settings{master}{respawn}." seconds");
3761					kill 15, $children{$daemon}; sleep $postfwd_settings{master}{respawn};
3762					if (my $pid = spawn_daemon ($daemon)) {
3763						log_info ("Started $daemon at pid $pid");
3764						$children{$daemon} = $pid;
3765					};
3766				};
3767			};
3768		};
3769	# no watchdog -> sleep until signal
3770	} else {
3771		sleep;
3772	};
3773};
3774die "master-daemon: should never see me!\n";
3775
3776
3777## SUBS
3778
3779# cleanup children and files and terminate
3780sub end_program {
3781	# ignore further TERM signals
3782	$SIG{TERM} = 'IGNORE'; $terminating = 1;
3783	if ($postfwd_settings{summary}) {
3784		undef $postfwd_settings{syslog}{noidlestats};
3785		log_stats();
3786	};
3787	log_note ($postfwd_settings{name}." ".$postfwd_settings{version}." terminating...");
3788	unlink $postfwd_settings{master}{pid_file} if (-T $postfwd_settings{master}{pid_file});
3789	# negative signal no. kills the whole process group
3790	kill -15, $$;
3791	exit (0);
3792};
3793
3794# send hup to child processes
3795sub reload_program {
3796	log_note ($postfwd_settings{name}." ".$postfwd_settings{version}." reloading...");
3797	map { kill 1, $_ } (values %children) if %children;
3798};
3799
3800# check a cache or server daemon
3801sub check_daemon { return ((&{$postfwd_settings{$_[0]}{check}}($_[0],$postfwd_patterns{ping}) || '') eq $postfwd_patterns{pong}) };
3802
3803# spawn a cache or server daemon
3804sub spawn_daemon {
3805	my ($type) = @_;
3806	my $pid = fork();
3807	die "Can not fork $type: $!\n" unless defined $pid;
3808	if ($pid == 0) {
3809		my %service =  %{$postfwd_settings{$type}};
3810		# Net::Server dies when a unix domain socket without dot "." is used
3811		$service{port} .= '|unix' if (($service{proto} eq 'unix') and not($service{port} =~ /\|unix$/));
3812 		$service{syslog_logopt}  = $postfwd_settings{syslog}{options}  if defined $postfwd_settings{syslog}{options};
3813 		$service{syslog_logsock} = $postfwd_settings{syslog}{socktype} if defined $postfwd_settings{syslog}{socktype};
3814 		$service{syslog_facility} = $postfwd_settings{syslog}{facility} if defined $postfwd_settings{syslog}{facility};
3815		my %daemonopts = (%{$postfwd_settings{base}}, %service);
3816		map { log_info("spawn $_") } (hash_to_list($type, \%daemonopts)) if wantsdebug (qw[ all verbose daemon spawn ]);
3817		my $daemon = bless { server => { %daemonopts } }, "postfwd3::$type";
3818		$daemon->set_personality();
3819		$daemon->run();
3820		die "$type-daemon: should never see me!\n";
3821	};
3822	return $pid;
3823};
3824
3825# get pid of running master process
3826sub get_master_pid {
3827	(-e $postfwd_settings{master}{pid_file}) or die $postfwd_settings{name}.": Can not find pid_file ".$postfwd_settings{master}{pid_file}.": $!\n";
3828	(-T $postfwd_settings{master}{pid_file}) or die $postfwd_settings{name}.": Can not open pid_file ".$postfwd_settings{master}{pid_file}.": not a textfile\n";
3829	open my $pidf, '<', $postfwd_settings{master}{pid_file} or die $postfwd_settings{name}.": Can open pid_file ".$postfwd_settings{master}{pid_file}.": $!\n";
3830	my $pid = <$pidf>;
3831	close($pidf);
3832	($pid =~ m/^(\d+)$/) or die $postfwd_settings{name}.": Invalid pid_file content '$pid' (pid_file ".$postfwd_settings{master}{pid_file}.")\n";
3833	return $1;
3834};
3835
3836# merge default and loaded program settings
3837sub merge_settings {
3838        my($left, $right) = @_;
3839        foreach my $k (keys %{$right}) {
3840                my $r = ref $right->{$k};
3841                if ($r eq 'HASH') {
3842                        unless (defined $left->{$k} and %{$left->{$k}}) {
3843                                %{$left->{$k}} = %{$right->{$k}};
3844                        } else {
3845                                merge_settings($left->{$k}, $right->{$k});
3846                        };
3847                } elsif ($r eq 'ARRAY') {
3848                        @{$left->{$k}} = @{$right->{$k}};
3849                } else {
3850                        $left->{$k} = $right->{$k};
3851                };
3852        };
3853};
3854
3855# cleanup program settings
3856sub clean_settings {
3857	my %set = @_;
3858	return () unless %set;
3859	map { delete $set{base}{$_}     if defined $set{base}{$_} }    qw( log_file log_level no_client_stdout );
3860	map { delete $set{syslog}{$_}   if defined $set{syslog}{$_} }  qw( logger );
3861	map { delete $set{server}{$_}   if defined $set{server}{$_} }  qw( check child_communication leave_children_open_on_hup );
3862	map { delete $set{cache}{$_}    if defined $set{cache}{$_} }   qw( check );
3863	map { delete $set{Options}{$_}  if defined $set{Options}{$_} } qw( term hup showconfig dumpcache dumpstats defaults delcache delrate loadsettings savesettings showsettings debugclasses );
3864	map { delete $set{$_}           if defined $set{$_} }          qw( Settings manual version seplim seplst sepreq );
3865	return %set;
3866};
3867
3868# save program settings
3869sub save_settings {
3870	my $fil = shift;
3871	my $fh  = *STDOUT;
3872	my %set = clean_settings(%postfwd_settings);
3873	local $Data::Dumper::Indent		= $postfwd_settings{dumper}{Indent};
3874	local $Data::Dumper::Purity		= $postfwd_settings{dumper}{Purity};
3875	local $Data::Dumper::Quotekeys		= $postfwd_settings{dumper}{Quotekeys};
3876	local $Data::Dumper::Sortkeys		= $postfwd_settings{dumper}{Sortkeys};
3877	local $Data::Dumper::Terse      	= $postfwd_settings{dumper}{Terse};
3878	open $fh, '>', $1 or die "\nERROR: Can not write to file '$1': $! - $@\n\n" if $fil =~ /^\s*(\S.*?)\s*$/;
3879	print $fh "#\n# postfwd settings file\n#\n";
3880	print $fh "# Version: $NAME $VERSION (Perl ".$].", Data::Dumper ".Data::Dumper->VERSION.")\n";
3881	print $fh "# Command: ".$NAME.$Commandargs."\n#\n";
3882	print $fh Dumper(\%set);
3883	close($fh) if $fil;
3884};
3885
3886# show program settings
3887sub load_settings {
3888	my $fil = shift;
3889	my %set = (); my $dat = undef;
3890	unless (open ($dat, '<', $fil)) {
3891		warn "\nCan not read settings from '$fil': $! - $@\n\n";
3892		return;
3893	};
3894	my @slurp = <$dat>;
3895	close($dat);
3896	unless (@slurp) {
3897		warn "\nNo valid settings found in '$fil'\n\n";
3898		return;
3899	};
3900	map { $_ = $1 if /^(.*)$/; $_ = '' if /^\s*#/ } @slurp;
3901	{
3902		local $SIG{'__DIE__'};
3903		%set = %{ eval(join ' ', @slurp) };
3904	};
3905	if ($@) {
3906		warn "\nError loading settings from '$fil': $@\n\n";
3907		return;
3908	};
3909	%set = clean_settings(%set) if %set;
3910	unless (%set) {
3911		warn "\nNo valid settings found in '$fil'\n\n";
3912		return;
3913	};
3914	delete $options{loadsettings} if defined $options{loadsettings};
3915	merge_settings( \%postfwd_settings, \%set);
3916	merge_settings( \%options, $postfwd_settings{Options});
3917	$postfwd_settings{Settings} = $fil;
3918};
3919
3920# detaints postfwd3 settings
3921sub detaint_hash {
3922	my (%request) = @_;
3923	# cycle through key=value pairs
3924	while ( my($s, $v) = each %request ) {
3925		my $r = ref $v;
3926		# type hash: recursively call ourself
3927		if ($r eq 'HASH') {
3928			%{$v} = detaint_hash ( %{$v} );
3929		# type array: detaint whole list
3930		} elsif ($r eq 'ARRAY') {
3931			@{$request{$s}} = map { $_ = (($_ =~ m/^(.*)$/) ? $1 : $_ ) if $_ } @{$v};
3932		# type scalar: detaint argument
3933		} elsif ($r eq '') {
3934			$request{$s} = (($v =~ m/^(.*)$/) ? $1 : $v) if ($s and $v);
3935		};
3936	};
3937	return %request;
3938};
3939
3940# send stats to syslog
3941sub log_stats { map { log_note ("$_") unless ($_ eq '<undef>') } get_stats(sort keys %{$postfwd_settings{daemons}}); };
3942
3943# retrieve status from children
3944sub get_stats {
3945	my @daemons = @_; my @output = ();
3946	map { push @output, (split $postfwd_settings{sepreq}.$postfwd_settings{seplst}, (&{$postfwd_settings{$_}{check}} ($_, 'CMD=DS;') || '<undef>')) } @daemons;
3947	return @output;
3948};
3949
3950
3951# EOF postfwd3
3952
3953__END__
3954
3955=head1 NAME
3956
3957postfwd3 - postfix firewall daemon
3958
3959=head1 SYNOPSIS
3960
3961B<postfwd3> [OPTIONS] [COMMAND]
3962
3963B<postfwd3> [OPTIONS] --cmd [SOURCE1, SOURCE2, ...]
3964
3965	Ruleset: (at least one, multiple use is allowed):
3966	-f, --file <file>		reads rules from <file>
3967	-r, --rule <rule>		adds <rule> to config
3968	-s, --scores <v>=<r>		returns <r> when score exceeds <v>
3969
3970	Settings: (multiple use allowed)
3971	-F, --loadsettings <file>	loads program settings from <file>
3972	    --savesettings <file>	saves program settings to <file>
3973	    --showsettings		exports program settings to stdout
3974
3975        Server:
3976        -i, --interface <dev>		listen on interface <dev>
3977        -p, --port <port>		listen on port <port>
3978	    --proto <proto>		socket type (tcp or unix)
3979	    --server_socket <sock>	e.g. tcp:127.0.0.1:10045
3980        -u, --user <name>		set uid to user <name>
3981        -g, --group <name>		set gid to group <name>
3982	    --umask <mask>		umask for master filepermissions
3983	    --server_umask <mask>	umask for server filepermissions
3984            --pidfile <path>		create pidfile under <path>
3985            --min_servers <i>		spawn at least <i> children
3986	    --max_servers <i>		do not spawn more than <i> children
3987            --min_spare_servers <i>	minimum idle children
3988            --max_spare_servers <i>     maximum idle children
3989
3990        Cache:
3991        -c, --cache <int>		sets the request-cache timeout to <int> seconds
3992            --cleanup-requests <int>	cleanup interval in seconds for request cache
3993            --cache_interface <dev>	listen on interface <dev>
3994            --cache_port <port>		listen on port <port>
3995	    --cache_proto <proto>	socket type (tcp or unix)
3996	    --cache_socket <sock>	e.g. tcp:127.0.0.1:10043
3997	    --cache_umask <mask>	umask for cache filepermissions
3998	    --cacheid <list>		list of request items for cache-id
3999	    --cacheid_md5	        cacheid => md5sum(request)
4000	    --cache-rdomain-only	skip recipient localpart for cache-id
4001	    --cache-no-sender		skip sender address for cache-id
4002	    --cache-no-size		skip size for cache-id
4003	    --no_parent_request_cache	disable parent request cache
4004	    --no_parent_rate_cache	disable parent rate cache
4005	    --no_parent_dns_cache	disable parent dns cache (default)
4006	    --no_parent_group_cache	disable parent group cache
4007	    --no_parent_cache		disable all parent caches
4008
4009	Groups:
4010	    --default_group_ttl <i>	default group TTL
4011	    --group_maxitems <i>	max items per group
4012            --cleanup-groups <int>	cleanup interval in seconds for group objects
4013
4014	Rates:
4015            --cleanup-rates <int>	cleanup interval in seconds for rate cache
4016
4017	Control:
4018	-k, --kill, --stop		terminate postfwd3
4019	    --reload, --hup		reload postfwd3
4020	    --watchdog <w>		watchdog timer in seconds
4021	    --respawn <r>		respawn delay in seconds
4022	    --failures <f>		max respawn failure counter
4023	-d, --daemon			execute program in background
4024	    --nodaemon			execute program in foreground
4025	    --daemons <list>		list of daemons to start
4026	    --personality <type>	type of policy server, allows 'PreFork' or 'Multiplex'
4027	    --autopersonality		determine personality by program name (see manpage)
4028	    --noautopersonality		don't do it (see above :)
4029	    --v1			set personality to 'Multiplex'
4030	    --v2			set personality to 'PreFork'
4031            --dumpcache			show cache contents
4032            --dumpstats			show statistics
4033	-R, --chroot <path>		chroot to <path> before start
4034	    --delcache <item>		removes an item from the request cache
4035	    --delrate <item>		removes an item from the rate cache
4036
4037	DNS:
4038	-n, --nodns			skip any dns based test
4039            --dns_timeout <i>		dns query timeout in seconds
4040            --dns_timeout_max <i>	disable dnsbl after <i> timeouts
4041            --dns_timeout_interval <i>	reenable dnsbl after <i> seconds
4042	    --cache-rbl-timeout <i>	default dns ttl if not specified in ruleset
4043	    --cache-rbl-default <s>	default dns pattern if not specified in ruleset
4044	    --cleanup-rbls <i>		cleanup old dns cache items every <i> seconds
4045	    --dns_async_txt		perform dnsbl A and TXT lookups simultaneously
4046	    --dns_max_ns_lookups	max names to look up with sender_ns_addrs
4047	    --dns_max_mx_lookups	max names to look up with sender_mx_addrs
4048	    --ipv6_dnsbl		enables dnsbl checks for IPv6 addresses
4049
4050        Optional:
4051	-t, --test			testing, always returns "dunno"
4052	-S, --summary <i>		show stats every <i> seconds
4053            --noidlestats		disables statistics when idle
4054            --norulestats		disables per rule statistics
4055	-I, --instantcfg		reloads ruleset on every new request
4056	    --config_timeout <i>	parser timeout in seconds
4057            --keep_groups		do not clear group cache on reload
4058            --save_groups <file>	save and load group cache on disk
4059            --keep_rates		do not clear rate limit counters on reload
4060            --save_rates <file>		save and load rate limits on disk
4061	-A, --aggregate_addrs		pre-compute ip address lists to subnets
4062	    --no_netaddr		don't use NetAddr::IP functions
4063	    --no_netcidr		don't use Net::CIDR::Lite functions
4064	    --cidr_method=s		use method <s> for network checks
4065
4066
4067	Plugins:
4068	    --plugins <file>            loads postfwd plugins from file
4069
4070	Logging:
4071        -l, --logname <label>		label for syslog messages
4072	    --facility <s>		use syslog facility <s>
4073	    --socktype <s>		use syslog socktype <s>
4074	    --nodnslog			do not log dns results
4075	    --anydnslog			log any dns (even cached) results
4076	    --norulelog			do not log rule actions
4077	    --nolog|--perfmon		no logging at all
4078        -v, --verbose			verbose logging, use twice to increase
4079	    --debug <s>			list of debugging classes
4080	    --debugclasses		shows all available debug classes
4081					and exits the program
4082
4083        Information (use only at command-line!):
4084 	-h, --help			display this help and exit
4085        -m, --manual			shows program manual
4086	-V, --version			output version information and exit
4087	-D, --defaults			show postfwd3 settings and exit
4088        -C, --showconfig		show postfwd3 ruleset and exit (-v allowed)
4089        -L, --stdout			redirect syslog messages to stdout (--stdoutlog works for compatibility)
4090            --stdin			pull request from stdin instead of a network socket
4091	    --cmd			shorthand form of --stdin, --stdout and --nodaemon
4092        -q, --quiet			no syslogging, no stdout (-P works for compatibility)
4093
4094	Commands:
4095	start				start the program [default]
4096	stop				same as --stop
4097	reload				same as --reload
4098	dumpcache			same as --dumpcache
4099	dumpstats			same as --dumpstats
4100	defaults			same as --defaults
4101	showsettings			same as --showsettings
4102	delcache <item>			same as --delcache <item>
4103	delrate <item>			same as --delrate <item>
4104
4105	Obsolete (only for compatibility with postfwd v1):
4106        --shortlog, --dns_queuesize, --dns_retries
4107
4108
4109=head1 DESCRIPTION
4110
4111
4112=head2 INTRODUCTION
4113
4114postfwd3 is written to combine complex postfix restrictions in a ruleset similar to those of the most firewalls.
4115The program uses the postfix policy delegation protocol to control access to the mail system before a message
4116has been accepted (please visit L<http://www.postfix.org/SMTPD_POLICY_README.html> for more information).
4117
4118postfwd3 allows you to choose an action (e.g. reject, dunno) for a combination of several smtp parameters
4119(like sender and recipient address, size or the client's TLS fingerprint). Also it offers simple macros/acls
4120which should allow straightforward and easy-to-read configurations.
4121
4122I<Features:>
4123
4124* Complex combinations of smtp parameters
4125
4126* Combined RBL/RHSBL lookups with arbitrary actions depending on results
4127
4128* Scoring system
4129
4130* Date/time based rules
4131
4132* Macros/ACLs, Dynamic groups, Negation
4133
4134* Compare request attributes (e.g. client_name and helo_name)
4135
4136* Internal caching for requests and dns lookups
4137
4138* Built in statistics for rule efficiency analysis
4139
4140
4141=head2 CONFIGURATION
4142
4143A configuration line consists of optional item=value pairs, separated by semicolons
4144(`;`) and the appropriate desired action:
4145
4146	[ <item1>=<value>; <item2>=<value>; ... ] action=<result>
4147
4148I<Example:>
4149
4150	client_address=192.168.1.1 ; sender==no@bad.local ; action=REJECT
4151
4152This will deny all mail from 192.168.1.1 with envelope sender no@bad.local. The order of the elements
4153is not important. So the following would lead to the same result as the previous example:
4154
4155	action=REJECT ; client_address=192.168.1.1 ; sender==no@bad.local
4156
4157The way how request items are compared to the ruleset can be influenced in the following way:
4158
4159	====================================================================
4160	 ITEM == VALUE                true if ITEM equals VALUE
4161	 ITEM => VALUE                true if ITEM >= VALUE
4162	 ITEM =< VALUE                true if ITEM <= VALUE
4163	 ITEM >  VALUE                true if ITEM >  VALUE
4164	 ITEM <  VALUE                true if ITEM <  VALUE
4165	 ITEM =~ VALUE                true if ITEM ~= /^VALUE$/i
4166	 ITEM != VALUE                false if ITEM equals VALUE
4167	 ITEM !> VALUE                false if ITEM >= VALUE
4168	 ITEM !< VALUE                false if ITEM <= VALUE
4169	 ITEM !~ VALUE                false if ITEM ~= /^VALUE$/i
4170	 ITEM =  VALUE                default behaviour (see ITEMS section)
4171	====================================================================
4172
4173To identify single rules in your log files, you may add an unique identifier for each of it:
4174
4175	id=R_001 ; action=REJECT ; client_address=192.168.1.1 ; sender==no@bad.local
4176
4177You may use these identifiers as target for the `jump()` command (see ACTIONS section below). Leading
4178or trailing whitespace characters will be ignored. Use '#' to comment your configuration. Others will
4179appreciate.
4180
4181A ruleset consists of one or multiple rules, which can be loaded from files or passed as command line
4182arguments. Please see the COMMAND LINE section below for more information on this topic.
4183
4184Since postfwd version 1.30 rules spanning span multiple lines can be defined by prefixing the following
4185lines with one or multiple whitespace characters (or '}' for macros):
4186
4187	id=RULE001
4188		client_address=192.168.1.0/24
4189		sender==no@bad.local
4190		action=REJECT no access
4191
4192postfwd versions prior to 1.30 require trailing ';' and '\'-characters:
4193
4194	id=RULE001; \
4195		client_address=192.168.1.0/24; \
4196		sender==no@bad.local; \
4197		action=REJECT no access
4198
4199
4200=head2 ITEMS
4201
4202	id			- a unique rule id, which can be used for log analysis
4203				  ids also serve as targets for the "jump" command.
4204
4205	date, time		- a time or date range within the specified rule shall hit
4206				  # FORMAT:
4207				  # Feb, 29th
4208				  date=29.02.2008
4209				  # Dec, 24th - 26th
4210				  date=24.12.2008-26.12.2008
4211				  # from today until Nov, 23rd
4212				  date=-23.09.2008
4213				  # from April, 1st until today
4214				  date=01.04.2008-
4215
4216	days, months		- a range of weekdays (Sun-Sat) or months (Jan-Dec)
4217				  within the specified rule shall hit
4218
4219	score			- when the specified score is hit (see ACTIONS section)
4220				  the specified action will be returned to postfix
4221				  scores are set global until redefined!
4222
4223	request_score		- this value allows to access a request's score. it
4224				  may be used as variable ($$request_score).
4225
4226	rbl, rhsbl,	 	- query the specified RBLs/RHSBLs, possible values are:
4227	rhsbl_client,		  <name>[/<reply>/<maxcache>, <name>/<reply>/<maxcache>]
4228	rhsbl_sender,		  (defaults: reply=^127\.0\.0\.\d+$ maxcache=3600)
4229	rhsbl_reverse_client	  the results of all rhsbl_* queries will be combined
4230				  in rhsbl_count (see below).
4231
4232	rblcount, rhsblcount	- minimum RBL/RHSBL hitcounts to match. if not specified
4233				  a single RBL/RHSBL hit will match the rbl/rhsbl items.
4234				  you may specify 'all' to evaluate all items, and use
4235				  it as variable in an action (see ACTIONS section)
4236				  (default: 1)
4237
4238	sender_localpart,	- the local-/domainpart of the sender address
4239	sender_domain
4240
4241	recipient_localpart,	- the local-/domainpart of the recipient address
4242	recipient_domain
4243
4244        helo_address            - postfwd3 tries to look up the helo_name. use
4245                                  helo_address=!!(0.0.0.0/0) to check for unknown.
4246				  Please do not use this for positive access control
4247				  (whitelisting), as it might be forged.
4248
4249        sender_ns_names,        - postfwd3 tries to look up the names/ip addresses
4250        sender_ns_addrs           of the nameservers for the sender domain part.
4251				  Please do not use this for positive access control
4252				  (whitelisting), as it might be forged.
4253
4254        sender_mx_names,        - postfwd3 tries to look up the names/ip addresses
4255        sender_mx_addrs           of the mx records for the sender domain part.
4256				  Please do not use this for positive access control
4257				  (whitelisting), as it might be forged.
4258
4259	version			- postfwd3 version, contains "postfwd3 n.nn"
4260				  this enables version based checks in your rulesets
4261				  (e.g. for migration). works with old versions too,
4262				  because a non-existing item always returns false:
4263				  # version >= 1.10
4264				  id=R01; version~=1\.[1-9][0-9]; sender_domain==some.org \
4265				  	; action=REJECT sorry no access
4266
4267	postfwd_port		- postfwd server port, allows to use the same ruleset different
4268				  instances:
4269				  # rule only hits for instance at port 10045
4270				  id=PORT10045; postfwd_port==10045; action=DUNNO
4271
4272	postfwd_interface	- postfwd server initerface, see postfwd_port
4273
4274	ratecount		- only available for rate(), size() and rcpt() actions.
4275				  contains the actual limit counter:
4276					id=R01; action=rate(sender/200/600/REJECT limit of 200 exceeded [$$ratecount hits])
4277					id=R02; action=rate(sender/100/600/WARN limit of 100 exceeded [$$ratecount hits])
4278
4279Besides these you can specify any attribute of the postfix policy delegation protocol.
4280Feel free to combine them the way you need it (have a look at the EXAMPLES section below).
4281
4282Most values can be specified as regular expressions (PCRE). Please see the table below
4283for details:
4284
4285	# ==========================================================
4286	# ITEM=VALUE				TYPE
4287	# ==========================================================
4288	id=something				mask = string
4289	date=01.04.2007-22.04.2007		mask = date (DD.MM.YYYY-DD.MM.YYYY)
4290	time=08:30:00-17:00:00			mask = time (HH:MM:SS-HH:MM:SS)
4291	days=Mon-Wed				mask = weekdays (Mon-Wed) or numeric (1-3)
4292	months=Feb-Apr				mask = months (Feb-Apr) or numeric (1-3)
4293	score=5.0				mask = maximum floating point value
4294	rbl=zen.spamhaus.org			mask = <name>/<reply>/<maxcache>[,...]
4295	rblcount=2				mask = numeric, will match if rbl hits >= 2
4296        helo_address=<a.b.c.d/nn>               mask = CIDR[,CIDR,...]
4297        sender_ns_names=some.domain.tld         mask = PCRE
4298        sender_mx_names=some.domain.tld         mask = PCRE
4299        sender_ns_addrs=<a.b.c.d/nn>            mask = CIDR[,CIDR,...]
4300        sender_mx_addrs=<a.b.c.d/nn>            mask = CIDR[,CIDR,...]
4301	# ------------------------------
4302	# Postfix version 2.1 and later:
4303	# ------------------------------
4304	client_address=<a.b.c.d/nn>		mask = CIDR[,CIDR,...]
4305	client_name=another.domain.tld		mask = PCRE
4306	reverse_client_name=another.domain.tld	mask = PCRE
4307	helo_name=some.domain.tld		mask = PCRE
4308	sender=foo@bar.tld			mask = PCRE
4309	recipient=bar@foo.tld			mask = PCRE
4310	recipient_count=5			mask = numeric, will match if recipients >= 5
4311	# ------------------------------
4312	# Postfix version 2.2 and later:
4313	# ------------------------------
4314	sasl_method=plain			mask = PCRE
4315	sasl_username=you			mask = PCRE
4316	sasl_sender=				mask = PCRE
4317	size=12345				mask = numeric, will match if size >= 12345
4318	ccert_subject=blackhole.nowhere.local	mask = PCRE (only if tls verified)
4319	ccert_issuer=John+20Doe			mask = PCRE (only if tls verified)
4320	ccert_fingerprint=AA:BB:CC:DD:EE:...	mask = PCRE (do NOT use "..." here)
4321	# ------------------------------
4322	# Postfix version 2.3 and later:
4323	# ------------------------------
4324	encryption_protocol=TLSv1/SSLv3		mask = PCRE
4325	encryption_cipher=DHE-RSA-AES256-SHA	mask = PCRE
4326	encryption_keysize=256			mask = numeric, will match if keysize >= 256
4327	...
4328
4329the current list can be found at L<http://www.postfix.org/SMTPD_POLICY_README.html>. Please read carefully about which
4330attribute can be used at which level of the smtp transaction (e.g. size will only work reliably at END-OF-MESSAGE level).
4331Pattern matching is performed case insensitive.
4332
4333Multiple use of the same item is allowed and will compared as logical OR, which means that this will work as expected:
4334
4335	id=TRUST001; action=OK; encryption_keysize=64
4336		ccert_fingerprint=11:22:33:44:55:66:77:88:99
4337		ccert_fingerprint=22:33:44:55:66:77:88:99:00
4338		ccert_fingerprint=33:44:55:66:77:88:99:00:11
4339		sender=@domain\.local$
4340
4341client_address, rbl and rhsbl items may also be specified as whitespace-or-comma-separated values:
4342
4343	id=SKIP01; action=dunno
4344		client_address=192.168.1.0/24, 172.16.254.23
4345	id=SKIP02; action=dunno
4346		client_address=	10.10.3.32 10.216.222.0/27
4347
4348The following items must be unique:
4349
4350	id, minimum and maximum values, rblcount and rhsblcount
4351
4352Any item can be negated by preceeding '!!' to it, e.g.:
4353
4354	id=HOST001 ;  hostname == !!secure.trust.local ;  action=REJECT only secure.trust.local please
4355
4356or using the right compare operator:
4357
4358	id=HOST001 ;  hostname != secure.trust.local ;  action=REJECT only secure.trust.local please
4359
4360To avoid confusion with regexps or simply for better visibility you can use '!!(...)':
4361
4362	id=USER01 ;  sasl_username =~ !!( /^(bob|alice)$/ )  ;  action=REJECT who is that?
4363
4364Request attributes can be compared by preceeding '$$' characters, e.g.:
4365
4366	id=R-003 ;  client_name = !! $$helo_name      ;  action=WARN helo does not match DNS
4367	# or
4368	id=R-003 ;  client_name = !!($$(helo_name))   ;  action=WARN helo does not match DNS
4369
4370This is only valid for PCRE values (see list above). The comparison will be performed as case insensitive exact match.
4371Use the '-vv' option to debug.
4372
4373These special items will be reset for any new rule:
4374
4375	rblcount	- contains the number of RBL answers
4376	rhsblcount	- contains the number of RHSBL answers
4377	matches		- contains the number of matched items
4378	dnsbltext	- contains the dns TXT part of all RBL and RHSBL replies in the form
4379			  rbltype:rblname:<txt>; rbltype:rblname:<txt>; ...
4380
4381These special items will be changed for any matching rule:
4382
4383	request_hits	- contains ids of all matching rules
4384
4385This means that it might be necessary to save them, if you plan to use these values in later rules:
4386
4387	# set vals
4388	id=RBL01 ; rhsblcount=all; rblcount=all
4389		action=set(HIT_rhls=$$rhsblcount,HIT_rbls=$$rblcount,HIT_txt=$$dnsbltext)
4390		rbl=list.dsbl.org, bl.spamcop.net, dnsbl.sorbs.net, zen.spamhaus.org
4391		rhsbl_client=rddn.dnsbl.net.au, rhsbl.ahbl.org, rhsbl.sorbs.net
4392		rhsbl_sender=rddn.dnsbl.net.au, rhsbl.ahbl.org, rhsbl.sorbs.net
4393
4394	# compare
4395	id=RBL02 ; HIT_rhls>=1 ; HIT_rbls>=1 ; action=554 5.7.1 blocked using $$HIT_rhls RHSBLs and $$HIT_rbls RBLs [INFO: $$HIT_txt]
4396	id=RBL03 ; HIT_rhls>=2               ; action=554 5.7.1 blocked using $$HIT_rhls RHSBLs [INFO: $$HIT_txt]
4397	id=RBL04 ; HIT_rbls>=2               ; action=554 5.7.1 blocked using $$HIT_rbls RBLs [INFO: $$HIT_txt]
4398
4399
4400=head2 FILES
4401
4402Since postfwd1 v1.15 and postfwd3 v0.18 long item lists can be stored in separate files:
4403
4404	id=R001 ;  ccert_fingerprint==file:/etc/postfwd/wl_ccerts ;  action=DUNNO
4405
4406postfwd3 will read a list of items (one item per line) from /etc/postfwd/wl_ccerts. comments are allowed:
4407
4408	# client1
4409	11:22:33:44:55:66:77:88:99
4410	# client2
4411	22:33:44:55:66:77:88:99:00
4412	# client3
4413	33:44:55:66:77:88:99:00:11
4414
4415To use existing tables in key=value format, you can use:
4416
4417	id=R001 ;  ccert_fingerprint==table:/etc/postfwd/wl_ccerts ;  action=DUNNO
4418
4419This will ignore the right-hand value. Items can be mixed:
4420
4421	id=R002 ;  action=REJECT
4422		client_name==unknown
4423		client_name==file:/etc/postfwd/blacklisted
4424
4425and for non pcre (comma separated) items:
4426
4427	id=R003 ;  action=REJECT
4428		client_address==10.1.1.1, file:/etc/postfwd/blacklisted
4429
4430	id=R004 ;  action=REJECT
4431		rbl=myrbl.home.local, zen.spamhaus.org, file:/etc/postfwd/rbls_changing
4432
4433You can check your configuration with the --show_config option at the command line:
4434
4435	# postfwd3 --showconfig --rule='action=DUNNO; client_address=10.1.0.0/16, file:/etc/postfwd/wl_clients, 192.168.2.1'
4436
4437should give something like:
4438
4439	Rule   0: id->"R-0"; action->"DUNNO"; client_address->"=;10.1.0.0/16, =;194.123.86.10, =;186.4.6.12, =;192.168.2.1"
4440
4441If a file can not be read, it will be ignored:
4442
4443	# postfwd3 --showconfig --rule='action=DUNNO; client_address=10.1.0.0/16, file:/etc/postfwd/wl_clients, 192.168.2.1'
4444	[LOG warning]: error: file /etc/postfwd/wl_clients not found - file will be ignored ?
4445	Rule   0: id->"R-0"; action->"DUNNO"; client_address->"=;10.1.0.0/16, =;192.168.2.1"
4446
4447File items are evaluated at configuration stage. Therefore postfwd3 needs to be reloaded if a file has changed
4448
4449If you want to specify a file, that will be reloaded for each request, you can use lfile: and ltable:
4450
4451	id=R001; client_address=lfile:/etc/postfwd/client_whitelist; action=dunno
4452
4453This will check the modification time of /etc/postfwd/client_whitelist every time the rule is evaluated and reload it as
4454necessary. Of course this might increase the system load, so please use it with care.
4455
4456The --showconfig option illustrates the difference:
4457
4458	## evaluated at configuration stage
4459	# postfwd3 --nodaemon -L --rule='client_address=table:/etc/postfwd/clients; action=dunno' -C
4460	Rule   0: id->"R-0"; action->"dunno"; client_address->"=;1.1.1.1, =;1.1.1.2, =;1.1.1.3"
4461
4462	## evaluated for any rulehit
4463	# postfwd3 --nodaemon -L --rule='client_address=ltable:/etc/postfwd/clients; action=dunno' -C
4464	Rule   0: id->"R-0"; action->"dunno"; client_address->"=;ltable:/etc/postfwd/clients"
4465
4466Files can refer to other files. The following is valid.
4467
4468	-- FILE /etc/postfwd/rules.cf --
4469	id=R01; client_address=file:/etc/postfwd/clients_master.cf; action=DUNNO
4470
4471	-- FILE /etc/postfwd/clients_master.cf --
4472	192.168.1.0/24
4473	file:/etc/postfwd/clients_east.cf
4474	file:/etc/postfwd/clients_west.cf
4475
4476	-- FILE /etc/postfwd/clients_east.cf --
4477	192.168.2.0/24
4478
4479	-- FILE /etc/postfwd/clients_west.cf --
4480	192.168.3.0/24
4481
4482Note that there is currently no loop detection (/a/file calls /a/file) and that this feature is only available
4483with postfwd1 v1.15 and postfwd3 v0.18 and higher.
4484
4485
4486=head2 ACTIONS
4487
4488I<General>
4489
4490Actions will be executed, when all rule items have matched a request (or at least one of any item list). You can refer to
4491request attributes by preceeding $$ characters, like:
4492
4493	id=R-003; client_name = !!$$helo_name; action=WARN helo '$$helo_name' does not match DNS '$$client_name'
4494	# or
4495	id=R-003; client_name = !!$$helo_name; action=WARN helo '$$(helo_name)' does not match DNS '$$(client_name)'
4496
4497I<postfix actions>
4498
4499Actions will be replied to postfix as result to policy delegation requests. Any action that postfix understands is allowed - see
4500"man 5 access" or L<http://www.postfix.org/access.5.html> for a description. If no action is specified, the postfix WARN action
4501which simply logs the event will be used for the corresponding rule.
4502
4503postfwd3 will return dunno if it has reached the end of the ruleset and no rule has matched. This can be changed by placing a last
4504rule containing only an action statement:
4505
4506	...
4507	action=dunno ; sender=@domain.local	# sender is ok
4508	action=reject				# default deny
4509
4510I<postfwd3 actions>
4511
4512postfwd3 actions control the behaviour of the program. Currently you can specify the following:
4513
4514	jump (<id>)
4515	jumps to rule with id <id>, use this to skip certain rules.
4516	you can jump backwards - but remember that there is no loop
4517	detection at the moment! jumps to non-existing ids will be skipped.
4518
4519	score (<score>)
4520	the request's score will be modified by the specified <score>,
4521	which must be a floating point value. the modificator can be either
4522		+n.nn	adds n.nn to current score
4523		-n.nn	sustracts n.nn from the current score
4524		*n.nn	multiplies the current score by n.nn
4525		/n.nn	divides the current score through n.nn
4526		=n.nn	sets the current score to n.nn
4527	if the score exceeds the maximum set by `--scores` option (see
4528	COMMAND LINE) or the score item (see ITEMS section), the action
4529	defined for this case will be returned (default: 5.0=>"REJECT postfwd3 score exceeded").
4530
4531	set (<item>=<value>,<item>=<value>,...)
4532	this command allows you to insert or override request attributes, which then may be
4533	compared to your further ruleset. use this to speed up repeated comparisons to large item lists.
4534	please see the EXAMPLES section for more information. you may separate multiple key=value pairs
4535	by "," characters.
4536
4537	rate (<item>/<max>/<time>/<action>)
4538	this command creates a counter for the given <item>, which will be increased any time a request
4539	containing it arrives. if it exceeds <max> within <time> seconds it will return <action> to postfix.
4540	rate counters are very fast as they are executed before the ruleset is parsed.
4541	please note that <action> was limited to postfix actions (no postfwd actions) for postfwd versions <1.33!
4542	    # no more than 3 requests per 5 minutes
4543	    # from the same "unknown" client
4544	    id=RATE01 ;  client_name==unknown
4545	       action=rate(client_address/3/300/450 4.7.1 sorry, max 3 requests per 5 minutes)
4546
4547	size (<item>/<max>/<time>/<action>)
4548	this command works similar to the rate() command with the difference, that the rate counter is
4549	increased by the request's size attribute. to do this reliably you should call postfwd3 from
4550	smtpd_end_of_data_restrictions. if you want to be sure, you could check it within the ruleset:
4551	   # size limit 1.5mb per hour per client
4552	   id=SIZE01 ;  protocol_state==END-OF-MESSAGE ;  client_address==!!(10.1.1.1)
4553	      action=size(client_address/1572864/3600/450 4.7.1 sorry, max 1.5mb per hour)
4554
4555	rcpt (<item>/<max>/<time>/<action>)
4556	this command works similar to the rate() command with the difference, that the rate counter is
4557	increased by the request's recipient_count attribute. to do this reliably you should call postfwd
4558	from smtpd_data_restrictions or smtpd_end_of_data_restrictions. if you want to be sure, you could
4559	check it within the ruleset:
4560	   # recipient count limit 3 per hour per client
4561	   id=RCPT01 ;  protocol_state==END-OF-MESSAGE ;  client_address==!!(10.1.1.1)
4562	      action=rcpt(client_address/3/3600/450 4.7.1 sorry, max 3 recipients per hour)
4563
4564        rate5321,size5321,rcpt5321 (<item>/<max>/<time>/<action>)
4565        same as the corresponding non-5321 functions, with the difference that the localpart of
4566        sender oder recipient addresses are evaluated case-sensitive according to rfc5321. That
4567	means that requests from bob@example.local and BoB@example.local will be treated differently
4568
4569	groupadd(<groupname>/<item>[/<ttl>])
4570	saves <item> to dynamic group <group> for later use. the object will be removed from that
4571	group after ttl seconds. if not set, postfwd3 will use the default --default_group_ttl [3600s].
4572	Please read the chapter about dynamic groups before use!
4573	   # add client_address to group %%blacklisted_hosts
4574	   id=ADDGROUP
4575		rbl=zen.spamhaus.org,bl.spamcop.net,ix.dnsbl.manitu.net
4576		action=groupadd(%%blacklisted_hosts/client_address/86400)
4577
4578	groupdel(<groupname>/<item>)
4579	removes <item> from dynamic group <group>. Please read the chapter about dynamic groups before use!
4580	   # remove a client_address from group %%blacklisted_hosts
4581	   id=DELGROUP
4582		rbl=lists.dnswl.org
4583		action=groupdel(%%blacklisted_hosts/client_address)
4584
4585	ask (<addr>:<port>[:<ignore>])
4586	allows to delegate the policy decision to another policy service (e.g. postgrey). the first
4587	and the second argument (address and port) are mandatory. a third optional argument may be
4588	specified to tell postfwd3 to ignore certain answers and go on parsing the ruleset:
4589	   # example1: query postgrey and return it's answer to postfix
4590	   id=GREY; client_address==10.1.1.1; action=ask(127.0.0.1:10031)
4591	   # example2: query postgrey but ignore it's answer, if it matches 'DUNNO'
4592	   # and continue parsing postfwd's ruleset
4593	   id=GREY; client_address==10.1.1.1; action=ask(127.0.0.1:10031:^dunno$)
4594
4595        mail(server/helo/from/to/subject/body)
4596	This command is deprecated. You should try to use the sendmail() action instead.
4597        Very basic mail command, that sends a message with the given arguments. LIMITATIONS:
4598        This basically performs a telnet. No authentication or TLS are available. Additionally it does
4599        not track notification state and will notify you any time, the corresponding rule hits.
4600
4601	sendmail(sendmail-path::from::to::subject::body)
4602	Mail command, that uses an existing sendmail binary and sends a message with the given arguments.
4603	LIMITATIONS: The command does not track notification state and will notify you any time, the
4604	corresponding rule hits (which could mean 100 mails for a mail with 100 recipients at RCPT stage).
4605
4606	wait (<delay>)
4607	pauses the program execution for <delay> seconds. use this for
4608	delaying or throtteling connections.
4609
4610	note (<string>)
4611	just logs the given string and continues parsing the ruleset.
4612	if the string is empty, nothing will be logged (noop).
4613
4614	quit (<code>)
4615	terminates the program with the given exit-code. postfix doesn`t
4616	like that too much, so use it with care.
4617
4618You can reference to request attributes, like
4619
4620	id=R-HELO ;  helo_name=^[^\.]+$ ;  action=REJECT invalid helo '$$helo_name'
4621
4622Since postfwd3 version 2.00 a rule can have multiple postfwd actions, like
4623
4624	# if client is found on list.dnswl.org,
4625	#   1. send a note
4626	#   2. add it's ip to group %%WHITELISTED
4627	#   3. permit the request
4628	id=ADDR01
4629		rbl=list.dnswl.org
4630		action=note(adding $$client_address to WHITELIST)
4631		action=groupadd(%%WHITELISTED/client_address)
4632		action=PERMIT_AUTH_DESTINATION
4633
4634
4635=head2 MACROS/ACLS
4636
4637Multiple use of long items or combinations of them may be abbreviated by macros. Those must be prefixed by '&&' (two '&' characters).
4638First the macros have to be defined as follows:
4639
4640	&&RBLS { rbl=zen.spamhaus.org,list.dsbl.org,bl.spamcop.net,dnsbl.sorbs.net,ix.dnsbl.manitu.net; };
4641
4642Then these may be used in your rules, like:
4643
4644	&&RBLS ;  client_name=^unknown$				; action=REJECT
4645	&&RBLS ;  client_name=(\d+[\.-_]){4}			; action=REJECT
4646	&&RBLS ;  client_name=[\.-_](adsl|dynamic|ppp|)[\.-_]	; action=REJECT
4647
4648Macros can contain actions, too:
4649
4650	# definition
4651	&&GONOW { action=REJECT your request caused our spam detection policy to reject this message. More info at http://www.domain.local; };
4652	# rules
4653	&&GONOW ;  &&RBLS ;  client_name=^unknown$
4654	&&GONOW ;  &&RBLS ;  client_name=(\d+[\.-_]){4}
4655	&&GONOW ;  &&RBLS ;  client_name=[\.-_](adsl|dynamic|ppp|)[\.-_]
4656
4657Macros can contain macros, too:
4658
4659	# definition
4660	&&RBLS{
4661		rbl=zen.spamhaus.org
4662		rbl=list.dsbl.org
4663		rbl=bl.spamcop.net
4664		rbl=dnsbl.sorbs.net
4665		rbl=ix.dnsbl.manitu.net
4666	};
4667	&&DYNAMIC{
4668		client_name=^unknown$
4669		client_name=(\d+[\.-_]){4}
4670		client_name=[\.-_](adsl|dynamic|ppp|)[\.-_]
4671	};
4672	&&GOAWAY { &&RBLS; &&DYNAMIC; };
4673	# rules
4674	&&GOAWAY ; action=REJECT dynamic client and listed on RBL
4675
4676Basically macros are simple text substitutions - see the L</PARSER> section for more information.
4677
4678
4679=head2 DYNAMIC GROUPS
4680
4681Dynamic groups are list objects. You can add or remove items and compare request items to their content within your ruleset.
4682Groups are saved persistant for further requests. This way you can save information for later use. Every item of a group has
4683a time-to-live value.
4684
4685Group-names have to be preceeded by double '%'-characters, like %%GROUPNAME:
4686
4687	# add client_address to group %%BLACKLISTED, if sent to spamtrap
4688	id=SPAMTRAP; recipient==spamtrap@domain.local; action=groupadd(%%BLACKLISTED/client_address)
4689
4690	# reject any request from client_address in group %%BLACKLISTED
4691	id=BLACKLISTED; client_address=%%BLACKLISTED; action=REJECT
4692
4693The groupadd() function receives the following arguments. The <ttl> value is optional. If not specified, postfwd3
4694will use the default --default_group_ttl [300s]:
4695
4696	groupadd ( <Groupname> / <item> [ / <ttl> ] )
4697
4698postfwd will save this data in the following structure:
4699
4700	%Group_Cache -> %<Groupname> -> $<item> = <ttl>
4701
4702The groupdel() function removes an item from a group:
4703
4704	groupdel ( <Groupname> / <item> )
4705
4706Expired members will be removed at cleanup stage after --cleanup_groups <i> seconds.
4707
4708To compare requests with dynamic groups, these must be refered by preceeding '%%'-characters:
4709
4710	<item> = %%<Groupname>
4711
4712Empty groups always compare false. The following ruleset is possible:
4713
4714	# allow any request from client_address in group %%WHITELISTED
4715	id=WHITEGROUP; client_address=%%WHITELISTED; action=DUNNO
4716
4717	# reject any request from client_address in group %%BLACKLISTED
4718	id=BLACKGROUP; client_address=%%BLACKLISTED; action=REJECT
4719
4720	# ... other rules ...
4721
4722	# ... whitelist file ...
4723	id=W_TRUST; action=set(WHITE=1)
4724	    client_address=file:/etc/postfwd/whitelist_networks
4725
4726	# ... whitelist dns ...
4727	id=W_DNSWL; action=set(WHITE=1)
4728	    rbl=list.dnswl.org
4729
4730	# ... -> add it to whitelist for a day and allow request
4731	id=W_GROUP; WHITE==1
4732	    action=groupadd(%%WHITELISTED/client_address/86400)
4733	    action=DUNNO
4734
4735	# ... or blacklist by sending to spamtrap ...
4736	id=B_SPAMTRAP; action=set(BLACK=1)
4737	    recipient==spamtrap@domain.local
4738
4739	# ... or blacklisted by dns ...
4740	id=B_DNSBL; action=set(BLACK=1)
4741	    rbl=zen.spamhaus.org
4742
4743	# ... -> add it to group BLACKLISTED for 1 hour and reject
4744	id=B_GROUP; BLACK==1
4745	    action=groupadd(%%BLACKLISTED/client_address/3600)
4746	    action=REJECT
4747
4748B<Limitations>
4749
4750Currently dynamic groups are kept in memory. By default postfwd3 will only accept a maximum of 999999 members
4751in a group. To deactivate this limit set --group_maxitems to '-1'.
4752
4753With PreFork personality, you should be aware that this information must be shared through the cache daemon.
4754
4755In short: Don't let these lists get excessively big unless you have enough system capacity to do so. If necessary
4756tune your config by setting the <ttl> and --group_maxitems.
4757
4758
4759=head2 PLUGINS
4760
4761B<Description>
4762
4763The plugin interface allow you to define your own checks and enhance postfwd's
4764functionality. Feel free to share useful things!
4765
4766B<Warning>
4767
4768Note that the plugin interface is still at devel stage. Please test your plugins
4769carefully, because errors may cause postfwd to break! It is also
4770allowed to override attributes or built-in functions, but be sure that you know
4771what you do because some of them are used internally.
4772
4773Please keep security in mind, when you access sensible ressources and never, ever
4774run postfwd as privileged user! Also never trust your input (especially hostnames,
4775and e-mail addresses).
4776
4777B<ITEMS>
4778
4779Item plugins are perl subroutines which integrate additional attributes to requests
4780before they are evaluated against postfwd's ruleset like any other item of the
4781policy delegation protocol. This allows you to create your own checks.
4782
4783plugin-items can not be used selective. these functions will be executed for every
4784request postfwd receives, so keep performance in mind.
4785
4786	SYNOPSIS: postfwd_items_plugin{<name>}($request)
4787
4788means that your subroutine, called <name>, has access to a hash-reference called
4789$request, which contains all request attributes, like $request->{client_name} and
4790saves values in the following form:
4791
4792	save: $result->{<item>} = <value>
4793
4794this creates the new item <item> containing <value>, which will be integrated in
4795the policy delegation request and therefore may be used in postfwd's ruleset.
4796
4797	# do NOT remove the next line
4798	%postfwd_items_plugin = (
4799
4800		# EXAMPLES - integrated in postfwd. no need to activate them here.
4801
4802			# allows to check postfwd version in ruleset
4803	        	"version" => sub {
4804       	 		       	my($request) = shift;
4805       	 	        	$request->{version} => $NAME." ".$VERSION,
4806			},
4807
4808			# sender_domain and recipient_domain
4809       	 		"address_parts" => sub {
4810       	 		       	my($request) = shift;
4811       	 			$request->{sender} =~ /@([^@]*)$/;
4812       	 			$request->{sender_domain} = ($1 || '');
4813       	 			$request->{recipient} =~ /@([^@]*)$/;
4814				$request->{recipient_domain} = ($1 || '');
4815			},
4816
4817	# do NOT remove the next line
4818	);
4819
4820B<COMPARE>
4821
4822Compare plugins allow you to define how your new items should be compared to the ruleset.
4823These are optional. If you don't specify one, the default (== for exact match, =~ for PCRE, ...)
4824will be used.
4825
4826	SYNOPSIS:  <item> => sub { return &{$postfwd_compare{<type>}}(@_); },
4827
4828	# do NOT remove the next line
4829	%postfwd_compare_plugin = (
4830
4831		EXAMPLES - integrated in postfwd. no need to activate them here.
4832
4833			# Simple example
4834			# SYNOPSIS:  <result> = <item> (return &{$postfwd_compare{<type>}}(@_))
4835			"client_address"  => sub { return &{$postfwd_compare{cidr}}(@_); },
4836			"size"            => sub { return &{$postfwd_compare{numeric}}(@_); },
4837			"recipient_count" => sub { return &{$postfwd_compare{numeric}}(@_); },
4838
4839			# Complex example
4840			# SYNOPSIS:  <result> = <item>(<operator>, <ruleset value>, <request value>, <request>)
4841			"numeric" => sub {
4842				my($cmp,$val,$myitem,$request) = @_;
4843				my($myresult) = undef;	$myitem ||= "0"; $val ||= "0";
4844				if ($cmp eq '==') {
4845					$myresult = ($myitem == $val);
4846				} elsif ($cmp eq '=<') {
4847					$myresult = ($myitem <= $val);
4848				} elsif ($cmp eq '=>') {
4849					$myresult = ($myitem >= $val);
4850				} elsif ($cmp eq '<') {
4851					$myresult = ($myitem < $val);
4852				} elsif ($cmp eq '>') {
4853					$myresult = ($myitem > $val);
4854				} elsif ($cmp eq '!=') {
4855					$myresult = not($myitem == $val);
4856				} elsif ($cmp eq '!<') {
4857					$myresult = not($myitem <= $val);
4858				} elsif ($cmp eq '!>') {
4859					$myresult = not($myitem >= $val);
4860				} else {
4861					$myresult = ($myitem >= $val);
4862				};
4863				return $myresult;
4864			},
4865
4866	# do NOT remove the next line
4867	);
4868
4869B<ACTIONS>
4870
4871Action plugins allow to define new postfwd actions. By setting the $stop-flag you can decide to
4872continue or to stop parsing the ruleset.
4873
4874	SYNOPSIS:  (<stop rule parsing>, <next rule index>, <return action>, <logprefix>) =
4875			<action> (<current rule index>, <current time>, <command name>, <argument>, <logprefix>, <request>)
4876
4877	# do NOT remove the next line
4878	%postfwd_actions_plugin = (
4879
4880		# EXAMPLES - integrated in postfwd. no need to activate them here.
4881
4882			# note(<logstring>) command
4883			"note"  => sub {
4884				my($index,$now,$mycmd,$myarg,$myline,$request) = @_;
4885				my($myaction) = 'dunno'; my($stop) = 0;
4886				log_info "[RULES] ".$myline." - note: ".$myarg if $myarg;
4887				return ($stop,$index,$myaction,$myline);
4888			},
4889
4890			# skips next <myarg> rules
4891        		"skip" => sub {
4892				my($index,$now,$mycmd,$myarg,$myline,$request) = @_;
4893				my($myaction) = 'dunno'; my($stop) = 0;
4894				$index += $myarg if ( $myarg and not(($index + $myarg) > $#Rules) );
4895				return ($stop,$index,$myaction,$myline);
4896        		},
4897
4898			# dumps current request contents to syslog
4899        		"dumprequest" => sub {
4900				my($index,$now,$mycmd,$myarg,$myline,$request) = @_;
4901				my($myaction) = 'dunno'; my($stop) = 0;
4902				map { log_info "[DUMP] rule=$index, Attribute: $_=$request{$_}" } (keys %{$request});
4903				return ($stop,$index,$myaction,$myline);
4904        		},
4905
4906	# do NOT remove the next line
4907	);
4908
4909
4910=head2 COMMAND LINE
4911
4912I<Ruleset>
4913
4914The following arguments are used to specify the source of the postfwd3 ruleset. This means
4915that at least one of the following is required for postfwd3 to work.
4916
4917	-f, --file <file>
4918	Reads rules from <file>. Please see the CONFIGURATION section
4919	for more information.
4920
4921	-r, --rule <rule>
4922	Adds <rule> to ruleset. Remember that you might have to quote
4923	strings that contain whitespaces or shell characters.
4924
4925I<Settings>
4926
4927	-F, --loadsettings <file>
4928	Loads program settings from <file>. data must be in Data::Dumper format.
4929	Please read the SETTINGS section for more information.
4930
4931	--savesettings <file>
4932	Saves program settings to <file> in Data::Dumper format.
4933	Please read the SETTINGS section for more information.
4934
4935	--showsettings
4936	Exports program settings in Data::Dumper format to stdout. This output
4937	can be saved to a file and later used with the --loadsettings option.
4938
4939I<Scoring>
4940
4941	-s, --scores <val>=<action>
4942	Returns <action> to postfix, when the request's score exceeds <val>
4943
4944Multiple usage is allowed. Just chain your arguments, like:
4945
4946	postfwd3 -r "<item>=<value>;action=<result>" -f <file> -f <file> ...
4947	  or
4948	postfwd3 --scores 4.5="WARN high score" --scores 5.0="REJECT postfwd3 score too high" ...
4949
4950In case of multiple scores, the highest match will count. The order of the arguments will be
4951reflected in the postfwd3 ruleset.
4952
4953I<Networking>
4954
4955postfwd3 can be run as daemon so that it listens on the network for incoming requests.
4956The following arguments will control it's behaviour in this case.
4957
4958	-d, --daemon, --nodaemon
4959	postfwd3 will run as daemon and listen on the network for incoming
4960	queries (default 127.0.0.1:10045). Use --nodaemon to keep postfwd3
4961	running in foreground.
4962
4963	-i, --interface <dev>
4964	Bind postfwd3 to the specified interface (default 127.0.0.1).
4965
4966	-p, --port <port>
4967	postfwd3 listens on the specified port (default tcp/10045).
4968
4969        --proto <type>
4970        The protocol type for postfwd's socket. Currently you may use 'tcp' or 'unix' here.
4971        To use postfwd3 with a unix domain socket, run it as follows:
4972            postfwd3 --proto=unix --port=/somewhere/postfwd.socket
4973
4974	-u, --user <name>
4975	Changes real and effective user to <name>.
4976
4977	-g, --group <name>
4978	Changes real and effective group to <name>.
4979
4980	--personality <type>
4981	Type of policy server, allows 'PreFork' or 'Multiplex'.
4982	This option overrides --autopersonality.
4983
4984	--v1
4985	set personality to 'Multiplex'
4986	This option overrides --autopersonality.
4987
4988	--v2
4989	set personality to 'PreFork'
4990	This option overrides --autopersonality.
4991
4992	--autopersonality
4993	postfwd3 determines the  personality by program name:
4994	   * 'Multiplex' for 'postfwd' or 'postfwd1'
4995	   * 'PreFork' for 'postfwd2'
4996	you can disable this by using --noautopersonality or
4997	explicitly using --personality, --v1 or --v2
4998
4999	--umask <mask>
5000        Changes the umask for filepermissions of the master process (pidfile).
5001        Attention: This is umask, not chmod - you have to specify the bits that
5002        should NOT apply. E.g.: umask 077 equals to chmod 700.
5003
5004	--cache_umask <mask>
5005        Changes the umask for filepermissions of the cache process (unix domain socket).
5006
5007	--server_umask <mask>
5008        Changes the umask for filepermissions of the server process (unix domain socket).
5009
5010	-R, --chroot <path>
5011	Chroot the process to the specified path.
5012	Please look at http://postfwd.org/postfwd3-chroot.html before use!
5013
5014	--pidfile <path>
5015	The process id will be saved in the specified file.
5016
5017        --facility <f>
5018        sets the syslog facility, default is 'mail'
5019
5020        --socktype <s>
5021        sets the Sys::Syslog socktype to 'native', 'inet' or 'unix'.
5022        Default is to auto-detect this depening on module version and os.
5023
5024	-l, --logname <label>
5025	Labels the syslog messages. Useful when running multiple
5026	instances of postfwd.
5027
5028	--loglen <int>
5029	Truncates any syslog message after <int> characters.
5030
5031I<Plugins>
5032
5033	--plugins <file>
5034	Loads postfwd plugins from file. Please see http://postfwd.org/postfwd.plugins
5035	or the plugins.postfwd.sample that is available from the tarball for more info.
5036
5037I<Optional arguments>
5038
5039These parameters influence the way postfwd3 is working. Any of them can be combined.
5040
5041	-v, --verbose
5042	Verbose logging displays a lot of useful information but can cause
5043	your logfiles to grow noticeably. So use it with caution. Set the option
5044	twice (-vv) to get more information (logs all request attributes).
5045
5046	-c, --cache <int>    (default=600)
5047	Timeout for request cache, results for identical requests will be
5048	cached until config is reloaded or this time (in seconds) expired.
5049	A setting of 0 disables this feature.
5050
5051	--cache-no-size
5052	Ignores size attribute for cache comparisons which will lead to better
5053	cache-hit rates. You should set this option, if you don't use the size
5054	item in your ruleset.
5055
5056	--cache-no-sender
5057	Ignores sender address for cache comparisons which will lead to better
5058	cache-hit rates. You should set this option, if you don't use the sender
5059	item in your ruleset.
5060
5061	--cache-rdomain-only
5062	This will strip the localpart of the recipient's address before filling the
5063	cache. This may considerably increase cache-hit rates.
5064
5065	--cache-rbl-timeout <timeout>     (default=3600)
5066	This default value will be used as timeout in seconds for rbl cache items,
5067	if not specified in the ruleset.
5068
5069	--cache-rbl-default <pattern>    (default=^127\.0\.0\.\d+$)
5070	Matches <pattern> to rbl/rhsbl answers (regexp) if not specified in the ruleset.
5071
5072	--cacheid <item>, <item>, ...
5073	This csv-separated list of request attributes will be used to construct
5074	the request cache identifier. Use this only, if you know exactly what you
5075	are doing. If you, for example, use postfwd3 only for RBL/RHSBL control,
5076	you may set this to
5077		postfwd3 --cache=3600 --cacheid=client_name,client_address
5078	This increases efficiency of caching and improves postfwd's performance.
5079	Warning: You should list all items here, which are used in your ruleset!
5080
5081	--cacheid_md5, --nocacheid_md5    (default=1)
5082	The cacheid will be created with a md5sum of the request items.
5083
5084	--cleanup-requests <interval>    (default=600)
5085	The request cache will be searched for timed out items after this <interval> in
5086	seconds. It is a minimum value. The cleanup process will only take place, when
5087	a new request arrives.
5088
5089	--cleanup-rbls <interval>    (default=600)
5090	The rbl cache will be searched for timed out items after this <interval> in
5091	seconds. It is a minimum value. The cleanup process will only take place, when
5092	a new request arrives.
5093
5094	--cleanup-rates <interval>    (default=600)
5095	The rate cache will be searched for timed out items after this <interval> in
5096	seconds. It is a minimum value. The cleanup process will only take place, when
5097	a new request arrives.
5098
5099	-S, --summary <int>    (default=600)
5100	Shows some usage statistics (program uptime, request counter, matching rules)
5101	every <int> seconds. This option is included by the -v switch.
5102	This feature uses the alarm signal, so you can force postfwd3 to dump the stats
5103	using `kill -ALRM <pid>` (where <pid> is the process id of postfwd).
5104
5105	Example:
5106	Aug 19 12:39:45 mail1 postfwd[666]: [STATS] Counters: 213000 seconds uptime, 39 rules
5107	Aug 19 12:39:45 mail1 postfwd[666]: [STATS] Requests: 71643 overall, 49 last interval, 62.88% cache hits
5108	Aug 19 12:39:45 mail1 postfwd[666]: [STATS] Averages: 20.18 overall, 4.90 last interval, 557.30 top
5109	Aug 19 12:39:45 mail1 postfwd[666]: [STATS] Contents: 44 cached requests, 239 cached dnsbl results
5110	Aug 19 12:39:45 mail1 postfwd[666]: [STATS] Rule ID: R-001   matched: 2704 times
5111	Aug 19 12:39:45 mail1 postfwd[666]: [STATS] Rule ID: R-002   matched: 9351 times
5112	Aug 19 12:39:45 mail1 postfwd[666]: [STATS] Rule ID: R-003   matched: 3116 times
5113	...
5114
5115	--no-rulestats
5116	Disables per rule statistics. Keeps your log clean, if you do not use them.
5117	This option has no effect without --summary or --verbose set.
5118
5119	-L, --stdout
5120	Redirects all syslog messages to stdout for debugging. Do not use this in daemon mode!
5121
5122       --stdin
5123        Tells postfwd to get the request data from STDIN instead of a network socket. This
5124        may be used to test rulesets at the command-line:
5125
5126        Example:
5127        postfwd -f /etc/postfwd/postfwd.cf --stdin --stdout --nodaemon ../tools/request.sample
5128
5129        --cmd
5130        Shorthand for the combination --stdin, --stdout and --nodaemon
5131
5132        Example:
5133        postfwd -f /etc/postfwd/postfwd.cf --cmd ../tools/request.sample
5134
5135	-t, --test
5136	In test mode postfwd3 always returns "dunno", but logs according
5137	to it`s ruleset. -v will be set automatically with this option.
5138
5139	-n, --nodns
5140	Disables all DNS based checks like RBL checks. Rules containing
5141	such elements will be ignored.
5142
5143	-n, --nodnslog
5144	Disables logging of dns events.
5145
5146	--dns_timeout     (default: 14)
5147	Sets the timeout for asynchonous dns queries in seconds. This value will apply to
5148	all dns items in a rule.
5149
5150	--dns_timeout_max    (default: 10)
5151	Sets the maximum timeout counter for dnsbl lookups. If the timeouts exceed this value
5152	the corresponding dnsbl will be deactivated for a while (see --dns_timeout_interval).
5153
5154	--dns_timeout_interval    (default=1200)
5155	The dnsbl timeout counter will be cleaned after this interval in seconds. Use this
5156	in conjunction with the --dns_timeout_max parameter.
5157
5158	--dns_async_txt
5159	Perform dnsbl A and TXT lookups simultaneously (otherwise only for listings with at
5160	least one A record). This needs more network bandwidth due to increased queries but
5161	might increase throughput because the lookups can be parallelized.
5162
5163	--dns_max_ns_lookups     (default=0)
5164	maximum ns names to lookup up with sender_ns_addrs item. use 0 for no maximum.
5165
5166	--dns_max_mx_lookups     (default=0)
5167	maximum mx names to lookup up with sender_mx_addrs item. use 0 for no maximum.
5168
5169	--ipv6_dnsbl		(default=0)
5170	enables dnsbl checks for IPv6 addresses
5171
5172	-I, --instantcfg
5173	The config files, specified by -f will be re-read for every request
5174	postfwd3 receives. This enables on-the-fly configuration changes
5175	without restarting. Though files will be read only if necessary
5176	(which means their access times changed since last read) this might
5177	significantly increase system load.
5178
5179	--config_timeout    (default=3)
5180	timeout in seconds to parse a single configuration line. if exceeded, the rule will
5181	be skipped. this is used to prevent problems due to large files or loops.
5182
5183	--keep_groups    (default=0)
5184	With this option set postfwd3 does not clear the group cache on reload. Please
5185	note that you have to restart (not reload) postfwd with this option if you change
5186	any group based rules.
5187
5188        --save_groups    (default=none)
5189        With this option postfwd saves existing groups to disk and reloads them on program
5190	start. This allows persistent rate limits across program restarts or reboots.
5191        Please note that postfwd needs read and write access to the specified file.
5192
5193	--keep_rates    (default=0)
5194	With this option set postfwd3 does not clear the rate limit counters on reload. Please
5195	note that you have to restart (not reload) postfwd with this option if you change
5196	any rate limit rules.
5197
5198        --save_rates    (default=none)
5199        With this option postfwd saves existing rate limit counters to disk and reloads them
5200        on program start. This allows persistent rate limits across program restarts or reboots.
5201        Please note that postfwd needs read and write access to the specified file.
5202
5203	-A, --aggregate_addrs, --noaggregate_addrs
5204	Pre-computes ip address lists to subnets, so that e.g.:
5205
5206	   client_address=10.0.0.0/24, 10.0.1.0/24, 10.0.2.0/24, 10.0.3.0/24
5207		will result in
5208	   client_address=10.0.0.0/22
5209
5210	This increases the performance of cidr-matching
5211
5212	--no_netaddr
5213	Do not use functions from module NetAddr::IP, even if it was found
5214	on the system. This option implicitly disables --aggregate_addrs.
5215
5216	--no_netcidr
5217	Do not use functions from module Net::CIDR::Lite, even if it was found
5218	on the system.
5219
5220	--cidr_method=s   (default=autodetect)
5221	Use method <s> for network checks. Valid arguments are:
5222	  netcidr	- compare networks using Net::CIDR::Lite Module (v4 and v6 cidr)
5223	  netaddr	- compare networks using NetAddr::IP Module (v4 and v6 cidr)
5224	  postfwd	- old but very fast method (v4=cidr, v6=regex)
5225	If not specified postfwd tries to load modules in the above order and falls back
5226	to 'postfwd' if nothing found.
5227
5228I<Informational arguments>
5229
5230These arguments are for command line usage only. Never ever use them with postfix!
5231
5232	-C, --showconfig
5233	Displays the current ruleset. Use -v for verbose output.
5234
5235	-V, --version
5236	Displays the program version.
5237
5238	-h, --help
5239	Shows program usage.
5240
5241	-m, --manual
5242	Displays the program manual.
5243
5244	-D, --defaults
5245	displays complete postfwd3 settings.
5246
5247	-P, --perfmon
5248	This option turns of any syslogging and output. It is included
5249	for performance testing.
5250
5251	--dumpstats
5252	Displays program usage statistics.
5253
5254	--dumpcache
5255	Displays cache contents.
5256
5257	--delcache <item>
5258	Removes an item from the request cache. Use --dumpcache to identify objects.
5259	E.g.:
5260		# postfwd --dumpcache
5261		...
5262		%rate_cache -> %sender=gmato@jqvo.org -> %RATE002+2_600 -> @count    -> '1'
5263		%rate_cache -> %sender=gmato@jqvo.org -> %RATE002+2_600 -> @maxcount -> '2'
5264		...
5265		# postfwd --delrate="sender=gmato@jqvo.org"
5266		rate cache item 'sender=gmato@jqvo.org' removed
5267
5268	--delrate <item>
5269	Removes an item from the rate cache. Use --dumpcache to identify objects.
5270
5271
5272=head2 REFRESH
5273
5274In daemon mode postfwd3 reloads it's ruleset after receiving a HUP signal. Please see the description of
5275the '-I' switch to have your configuration refreshed for every request postfwd3 receives.
5276
5277
5278=head2 EXAMPLES
5279
5280	## whitelisting
5281	# 1. networks 192.168.1.0/24, 192.168.2.4
5282	# 2. client_names *.gmx.net and *.gmx.de
5283	# 3. sender *@someshop.tld from 11.22.33.44
5284	id=WL001; action=dunno ; client_address=192.168.1.0/24, 192.168.2.4
5285	id=WL002; action=dunno ; client_name=\.gmx\.(net|de)$
5286	id=WL003; action=dunno ; sender=@someshop\.tld$ ; client_address=11.22.33.44
5287
5288	## TLS control
5289	# 1. *@authority.tld only with correct TLS fingerprint
5290	# 2. *@secret.tld only with keysizes >=64
5291	id=TL001; action=dunno 				; sender=@authority\.tld$ ; ccert_fingerprint=AA:BB:CC..
5292	id=TL002; action=REJECT wrong TLS fingerprint	; sender=@authority\.tld$
5293	id=TL003; action=REJECT tls keylength < 64	; sender=@secret\.tld$ ; encryption_keysize=64
5294
5295	## Combined RBL checks
5296	# This will reject mail if
5297	# 1. listed on ix.dnsbl.manitu.net
5298	# 2. listed on zen.spamhaus.org (sbl and xbl, dns cache timeout 1200s instead of 3600s)
5299	# 3. listed on min 2 of bl.spamcop.net, list.dsbl.org, dnsbl.sorbs.net
5300	# 4. listed on bl.spamcop.net and one of rhsbl.ahbl.org, rhsbl.sorbs.net
5301	id=RBL01 ; action=REJECT listed on ix.dnsbl.manitu.net	; rbl=ix.dnsbl.manitu.net
5302	id=RBL02 ; action=REJECT listed on zen.spamhaus.org	; rbl=zen.spamhaus.org/127.0.0.[2-8]/1200
5303	id=RBL03 ; action=REJECT listed on too many RBLs	; rblcount=2 ; rbl=bl.spamcop.net, list.dsbl.org, dnsbl.sorbs.net
5304	id=RBL04 ; action=REJECT combined RBL+RHSBL check  	; rbl=bl.spamcop.net ; rhsbl=rhsbl.ahbl.org, rhsbl.sorbs.net
5305
5306	## Message size (requires message_size_limit to be set to 30000000)
5307	# 1. 30MB for systems in *.customer1.tld
5308	# 2. 20MB for SASL user joejob
5309	# 3. 10MB default
5310        id=SZ001; protocol_state==END-OF-MESSAGE; action=DUNNO; size<=30000000 ; client_name=\.customer1.tld$
5311        id=SZ002; protocol_state==END-OF-MESSAGE; action=DUNNO; size<=20000000 ; sasl_username==joejob
5312        id=SZ002; protocol_state==END-OF-MESSAGE; action=DUNNO; size<=10000000
5313        id=SZ100; protocol_state==END-OF-MESSAGE; action=REJECT message too large
5314
5315	## Selective Greylisting
5316	##
5317	## Note that postfwd does not include greylisting. This setup requires a running postgrey service
5318	## at port 10031 and the following postfix restriction class in your main.cf:
5319	##
5320	##      smtpd_restriction_classes = check_postgrey, ...
5321	##      check_postgrey = check_policy_service inet:127.0.0.1:10031
5322	#
5323	# 1. if listed on zen.spamhaus.org with results 127.0.0.10 or .11, dns cache timeout 1200s
5324	# 2. Client has no rDNS
5325	# 3. Client comes from several dialin domains
5326	id=GR001; action=check_postgrey ; rbl=dul.dnsbl.sorbs.net, zen.spamhaus.org/127.0.0.1[01]/1200
5327	id=GR002; action=check_postgrey ; client_name=^unknown$
5328	id=GR003; action=check_postgrey ; client_name=\.(t-ipconnect|alicedsl|ish)\.de$
5329
5330	## Date Time
5331	date=24.12.2007-26.12.2007          ;  action=450 4.7.1 office closed during christmas
5332	time=04:00:00-05:00:00              ;  action=450 4.7.1 maintenance ongoing, try again later
5333	time=-07:00:00 ;  sasl_username=jim ;  action=450 4.7.1 to early for you, jim
5334	time=22:00:00- ;  sasl_username=jim ;  action=450 4.7.1 to late now, jim
5335	months=-Apr                         ;  action=450 4.7.1 see you in may
5336	days=!!Mon-Fri                      ;  action=check_postgrey
5337
5338	## Usage of jump
5339	# The following allows a message size of 30MB for different
5340	# users/clients while others will only have 10MB.
5341	id=R001 ; action=jump(R100) ; sasl_username=^(Alice|Bob|Jane)$
5342	id=R002 ; action=jump(R100) ; client_address=192.168.1.0/24
5343	id=R003 ; action=jump(R100) ; ccert_fingerprint=AA:BB:CC:DD:...
5344	id=R004 ; action=jump(R100) ; ccert_fingerprint=AF:BE:CD:DC:...
5345	id=R005 ; action=jump(R100) ; ccert_fingerprint=DD:CC:BB:DD:...
5346	id=R099 ; protocol_state==END-OF-MESSAGE; action=REJECT message too big (max. 10MB); size=10000000
5347	id=R100 ; protocol_state==END-OF-MESSAGE; action=REJECT message too big (max. 30MB); size=30000000
5348
5349	## Usage of score
5350	# The following rejects a mail, if the client
5351	# - is listed on 1 RBL and 1 RHSBL
5352	# - is listed in 1 RBL or 1 RHSBL and has no correct rDNS
5353	# - other clients without correct rDNS will be greylist-checked
5354	# - some whitelists are used to lower the score
5355	id=S01 ; score=2.6              ; action=check_postgrey
5356	id=S02 ; score=5.0              ; action=REJECT postfwd score too high
5357	id=R00 ; action=score(-1.0)     ; rbl=exemptions.ahbl.org,list.dnswl.org,query.bondedsender.org,spf.trusted-forwarder.org
5358	id=R01 ; action=score(2.5)      ; rbl=bl.spamcop.net, list.dsbl.org, dnsbl.sorbs.net
5359	id=R02 ; action=score(2.5)      ; rhsbl=rhsbl.ahbl.org, rhsbl.sorbs.net
5360	id=N01 ; action=score(-0.2)     ; client_name==$$helo_name
5361	id=N02 ; action=score(2.7)      ; client_name=^unknown$
5362	...
5363
5364	## Usage of rate and size
5365	# The following temporary rejects requests from "unknown" clients, if they
5366	# 1. exceeded 30 requests per hour or
5367	# 2. tried to send more than 1.5mb within 10 minutes
5368	id=RATE01 ;  client_name==unknown ;  protocol_state==RCPT
5369		action=rate(client_address/30/3600/450 4.7.1 sorry, max 30 requests per hour)
5370	id=SIZE01 ;  client_name==unknown ;  protocol_state==END-OF-MESSAGE
5371		action=size(client_address/1572864/600/450 4.7.1 sorry, max 1.5mb per 10 minutes)
5372
5373	## Macros
5374        # definition
5375        &&RBLS { rbl=zen.spamhaus.org,list.dsbl.org,bl.spamcop.net,dnsbl.sorbs.net,ix.dnsbl.manitu.net; };
5376        &&GONOW { action=REJECT your request caused our spam detection policy to reject this message. More info at http://www.domain.local; };
5377        # rules
5378        &&GONOW ;  &&RBLS ;  client_name=^unknown$
5379        &&GONOW ;  &&RBLS ;  client_name=(\d+[\.-_]){4}
5380        &&GONOW ;  &&RBLS ;  client_name=[\.-_](adsl|dynamic|ppp|)[\.-_]
5381
5382	## Groups
5383	# definition
5384        &&RBLS{
5385		rbl=zen.spamhaus.org
5386		rbl=list.dsbl.org
5387		rbl=bl.spamcop.net
5388		rbl=dnsbl.sorbs.net
5389		rbl=ix.dnsbl.manitu.net
5390	};
5391	&&RHSBLS{
5392		...
5393	};
5394	&&DYNAMIC{
5395        	client_name==unknown
5396        	client_name~=(\d+[\.-_]){4}
5397        	client_name~=[\.-_](adsl|dynamic|ppp|)[\.-_]
5398		...
5399	};
5400	&&BAD_HELO{
5401		helo_name==my.name.tld
5402		helo_name~=^([^\.]+)$
5403		helo_name~=\.(local|lan)$
5404		...
5405	};
5406	&&MAINTENANCE{
5407		date=15.01.2007
5408		date=15.04.2007
5409		date=15.07.2007
5410		date=15.10.2007
5411		time=03:00:00 - 04:00:00
5412	};
5413	# rules
5414	id=COMBINED    ;  &&RBLS ;  &&DYNAMIC ;  action=REJECT dynamic client and listed on RBL
5415	id=MAINTENANCE ;  &&MAINTENANCE       ;  action=DEFER maintenance time - please try again later
5416
5417	# now with the set() command, note that long item
5418	# lists don't have to be compared twice
5419	id=RBL01    ;  &&RBLS      ;  action=set(HIT_rbls=1)
5420	id=HELO01   ;  &&BAD_HELO  ;  action=set(HIT_helo=1)
5421	id=DYNA01   ;  &&DYNAMIC   ;  action=set(HIT_dyna=1)
5422	id=REJECT01 ;  HIT_rbls==1 ;  HIT_helo==1  ; action=REJECT please see http://some.org/info?reject=01 for more info
5423	id=REJECT02 ;  HIT_rbls==1 ;  HIT_dyna==1  ; action=REJECT please see http://some.org/info?reject=02 for more info
5424	id=REJECT03 ;  HIT_helo==1 ;  HIT_dyna==1  ; action=REJECT please see http://some.org/info?reject=03 for more info
5425
5426	## combined with enhanced rbl features
5427	#
5428	id=RBL01 ; rhsblcount=all ; rblcount=all ; &&RBLS ; &&RHSBLS
5429	     action=set(HIT_dnsbls=$$rhsblcount,HIT_dnsbls+=$$rblcount,HIT_dnstxt=$$dnsbltext)
5430	id=RBL02 ; HIT_dnsbls>=2  ; action=554 5.7.1 blocked using $$HIT_dnsbls DNSBLs [INFO: $$HIT_dnstxt]
5431
5432
5433=head2 PARSER
5434
5435I<Configuration>
5436
5437The postfwd3 ruleset can be specified at the commandline (-r option) or be read from files (-f). The order of your arguments will be kept. You should
5438check the parser with the -C | --showconfig switch at the command line before applying a new config. The following call:
5439
5440	postfwd3 --showconfig \
5441		-r "id=TEST; recipient_count=100; action=WARN mail with 100+ recipients" \
5442		-f /etc/postfwd.cf \
5443		-r "id=DEFAULT; action=dunno";
5444
5445will produce the following output:
5446
5447	Rule   0: id->"TEST" action->"WARN mail with 100+ recipients"; recipient_count->"100"
5448	...
5449	... <content of /etc/postfwd.cf> ...
5450	...
5451	Rule <n>: id->"DEFAULT" action->"dunno"
5452
5453Multiple items of the same type will be added to lists (see the L</ITEMS> section for more info):
5454
5455	postfwd3 --showconfig \
5456		-r "client_address=192.168.1.0/24; client_address=172.16.26.32; action=dunno"
5457
5458will result in:
5459
5460	Rule   0: id->"R-0"; action->"dunno"; client_address->"192.168.1.0/24, 172.16.26.32"
5461
5462Macros are evaluated at configuration stage, which means that
5463
5464	postfwd3 --showconfig \
5465		-r "&&RBLS { rbl=bl.spamcop.net; client_name=^unknown$; };" \
5466		-r "id=RBL001; &&RBLS; action=REJECT listed on spamcop and bad rdns";
5467
5468will result in:
5469
5470	Rule   0: id->"RBL001"; action->"REJECT listed on spamcop and bad rdns"; rbl->"bl.spamcop.net"; client_name->"^unknown$"
5471
5472I<Request processing>
5473
5474When a policy delegation request arrives it will be compared against postfwd`s ruleset. To inspect the processing in detail you should increase
5475verbority using use the "-v" or "-vv" switch. "-L" redirects log messages to stdout.
5476
5477Keeping the order of the ruleset in general, items will be compared in random order, which basically means that
5478
5479	id=R001; action=dunno; client_address=192.168.1.1; sender=bob@alice.local
5480
5481equals to
5482
5483	id=R001; sender=bob@alice.local; client_address=192.168.1.1; action=dunno
5484
5485Lists will be evaluated in the specified order. This allows to place faster expressions at first:
5486
5487	postfwd3 -vv --cmd -r "id=RBL001; rbl=localrbl.local zen.spamhaus.org; action=REJECT" /some/where/request.sample
5488
5489produces the following
5490
5491	[LOGS info]: compare rbl: "remotehost.remote.net[68.10.1.7]"  ->  "localrbl.local"
5492	[LOGS info]: count1 rbl:  "2"  ->  "0"
5493	[LOGS info]: query rbl:   localrbl.local 7.1.10.68 (7.1.10.68.localrbl.local)
5494	[LOGS info]: count2 rbl:  "2"  ->  "0"
5495	[LOGS info]: match rbl:   FALSE
5496	[LOGS info]: compare rbl: "remotehost.remote.net[68.10.1.7]"  ->  "zen.spamhaus.org"
5497	[LOGS info]: count1 rbl:  "2"  ->  "0"
5498	[LOGS info]: query rbl:   zen.spamhaus.org 7.1.10.68 (7.1.10.68.zen.spamhaus.org)
5499	[LOGS info]: count2 rbl:  "2"  ->  "0"
5500	[LOGS info]: match rbl:   FALSE
5501	[LOGS info]: Action: dunno
5502
5503The negation operator !!(<value>) has the highest priority and therefore will be evaluated first. Then variable substitutions are performed:
5504
5505	postfwd3 -vv --cmd -r "id=TEST; action=REJECT; client_name=!!($$heloname)" /some/where/request.sample
5506
5507will give
5508
5509	[LOGS info]: compare client_name:     "unknown"  ->  "!!($$helo_name)"
5510	[LOGS info]: negate client_name:      "unknown"  ->  "$$helo_name"
5511	[LOGS info]: substitute client_name:  "unknown"  ->  "english-breakfast.cloud8.net"
5512	[LOGS info]: match client_name:  TRUE
5513	[LOGS info]: Action: REJECT
5514
5515
5516I<Ruleset evaluation>
5517
5518A rule hits when all items (or at least one element of a list for each item) have matched. As soon as one item (or all elements of a list) fails
5519to compare against the request attribute the parser will jump to the next rule in the postfwd3 ruleset.
5520
5521If a rule matches, there are two options:
5522
5523* Rule returns postfix action (dunno, reject, ...)
5524The parser stops rule processing and returns the action to postfix. Other rules will not be evaluated.
5525
5526* Rule returns postfwd3 action (jump(), note(), ...)
5527The parser evaluates the given action and continues with the next rule (except for the jump() or quit() actions - please see the L</ACTIONS> section
5528for more information). Nothing will be sent to postfix.
5529
5530If no rule has matched and the end of the ruleset is reached postfwd3 will return dunno without logging anything unless in verbose mode. You may
5531place a last catch-all rule to change that behaviour:
5532
5533	... <your rules> ...
5534	id=DEFAULT ;  action=dunno
5535
5536will log any request that passes the ruleset without having hit a prior rule.
5537
5538
5539=head2 SETTINGS
5540
5541Since version postfwd3 2.00-pre6 the program settings can be retrieved from a file. The information must be
5542parseable by perl eval() function. Comments are allowed:
5543
5544       # example settings for postfwd3
5545       {
5546          # enable verbose logging
5547          verbose => 1,
5548
5549          # user and group for postfwd
5550	  base => {
5551	      group => 'postfw',
5552              user => 'postfw'
5553          },
5554
5555          # server settings
5556          server => {
5557              proto => 'tcp',
5558              host => '127.0.0.1',
5559              port => '10099'
5560          }
5561       }
5562
5563postfwd3 can also export the program settings in that format using Data::Dumper.
5564
5565	# show configuration
5566	postfwd3 --showsettings
5567
5568	# save configuration to file
5569	postfwd3 --savesettings=/path/to/file
5570
5571	# read configuration from file
5572	postfwd3 --loadsettings=/path/to/file
5573
5574Multiple usage of --loadsettings or the short form -F is allowed. The order will be kept.
5575Commandline arguments override the settings retrieved from a file.
5576
5577	# load base settings, override with node settings and set port and verbose logging
5578	postfwd3 -v -p 10050 -F /path/to/basefile -F /path/to/nodefile
5579
5580Samples for settings files are distributed in the etc/-folder of the postfwd3 tarball.
5581
5582
5583=head2 DEBUGGING
5584
5585To debug special steps of the parser the '--debug' switch takes a list of debug classes. Since postfwd3 2.00
5586the list of available debug classes can be retrieved by running:
5587
5588	postfwd3 --debugclasses
5589
5590
5591=head2 INTEGRATION
5592
5593I<Integration via daemon mode>
5594
5595The common way to use postfwd3 is to start it as daemon, listening at a specified tcp port.
5596postfwd3 will spawn multiple child processes which communicate with a parent cache. This is
5597the prefered way to use postfwd3 in high volume environments. Start postfwd3 with the following parameters:
5598
5599	postfwd3 -d -f /etc/postfwd.cf -i 127.0.0.1 -p 10045 -u nobody -g nobody -S
5600
5601For efficient caching you should check if you can use the options --cacheid, --cache-rdomain-only,
5602--cache-no-sender and --cache-no-size.
5603
5604Now check your syslogs (default facility "mail") for a line like:
5605
5606	Aug  9 23:00:24 mail postfwd[5158]: postfwd3 n.nn ready for input
5607
5608and use `netstat -an|grep 10045` to check for something like
5609
5610	tcp  0  0  127.0.0.1:10045  0.0.0.0:*  LISTEN
5611
5612If everything works, open your postfix main.cf and insert the following
5613
5614	127.0.0.1:10045_time_limit      = 3600						<--- integration
5615	smtpd_recipient_restrictions    = permit_mynetworks				<--- recommended
5616                                  	  reject_unauth_destination			<--- recommended
5617				  	  check_policy_service inet:127.0.0.1:10045	<--- integration
5618
5619Reload your configuration with `postfix reload` and watch your logs. In it works you should see
5620lines like the following in your mail log:
5621
5622	Aug  9 23:01:24 mail postfwd[5158]: rule=22, id=ML_POSTFIX, client=english-breakfast.cloud9.net[168.100.1.7], sender=owner-postfix-users@postfix.tld, recipient=someone@domain.local, helo=english-breakfast.cloud9.net, proto=ESMTP, state=RCPT, action=dunno
5623
5624If you want to check for size or rcpt_count items you must integrate postfwd3 in smtp_data_restrictions or
5625smtpd_end_of_data_restrictions. Of course you can also specify a restriction class and use it in your access
5626tables. First create a file /etc/postfix/policy containing:
5627
5628	domain1.local		postfwdcheck
5629	domain2.local		postfwdcheck
5630	...
5631
5632Then postmap that file (`postmap hash:/etc/postfix/policy`), open your main.cf and enter
5633
5634	# Restriction Classes
5635	smtpd_restriction_classes       = postfwdcheck, <some more>...				<--- integration
5636	postfwdcheck                    = check_policy_service inet:127.0.0.1:10045		<--- integration
5637
5638	127.0.0.1:10045_time_limit      = 3600							<--- integration
5639	smtpd_recipient_restrictions    = permit_mynetworks,					<--- recommended
5640                                  	  reject_unauth_destination,				<--- recommended
5641				  	  ...							<--- optional
5642				  	  check_recipient_access hash:/etc/postfix/policy,	<--- integration
5643				  	  ...							<--- optional
5644
5645Reload postfix and watch your logs.
5646
5647I<Integration via docker>
5648
5649postfwd can be run in a docker container. The relevant options are --nodaemon and --stdout.
5650More information can be found in the included doc/docker.html or at L<http://postfwd.org/docker>.
5651
5652
5653=head2 TESTING
5654
5655First you have to create a ruleset (see Configuration section). Check it with
5656
5657	postfwd3 -f /etc/postfwd.cf -C
5658
5659There is an example policy request distributed with postfwd, called 'request.sample'.
5660Simply change it to meet your requirements and use
5661
5662	postfwd3 --cmd -f /etc/postfwd.cf request.sample
5663
5664You should get an answer like
5665
5666	action=<whateveryouconfigured>
5667
5668For network tests I use netcat:
5669
5670	nc 127.0.0.1 10045 <request.sample
5671
5672to send a request to postfwd. If you receive nothing, make sure that postfwd3 is running and
5673listening on the specified network settings.
5674
5675
5676=head2 PERFORMANCE
5677
5678Some of these proposals might not match your environment. Please check your requirements and test new options carefully!
5679
5680	- use caching options
5681	- use the correct match operator ==, <=, >=
5682	- use ^ and/or $ in regular expressions
5683	- use item lists (faster than single rules)
5684	- use set() action on repeated item lists
5685	- use jumps and rate limits
5686	- use a pre-lookup rule for rbl/rhsbls with empty note() action
5687
5688
5689=head2 SEE ALSO
5690
5691See L<http://www.postfix.org/SMTPD_POLICY_README.html> for a description
5692of how Postfix policy servers work.
5693
5694
5695=head1 DONATIONS
5696
5697Development, testing and hosting of postfwd consumes time and ressources. It is and will stay free software
5698(see "LICENSE" below). If you want to support this, consider a donation at https://www.paypal.me/postfwd.
5699
5700
5701=head1 LICENSE
5702
5703postfwd3 is free software and released under BSD license, which basically means
5704that you can do what you want as long as you keep the copyright notice:
5705
5706Copyright (c) 2009, Jan Peter Kessler
5707All rights reserved.
5708
5709Redistribution and use in source and binary forms, with or without modification,
5710are permitted provided that the following conditions are met:
5711
5712 * Redistributions of source code must retain the above copyright
5713   notice, this list of conditions and the following disclaimer.
5714 * Redistributions in binary form must reproduce the above copyright
5715   notice, this list of conditions and the following disclaimer in
5716   the documentation and/or other materials provided with the
5717   distribution.
5718 * Neither the name of the authors nor the names of his contributors
5719   may be used to endorse or promote products derived from this
5720   software without specific prior written permission.
5721
5722THIS SOFTWARE IS PROVIDED BY ME ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
5723INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
5724FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY DIRECT,
5725INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
5726NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
5727PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
5728WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
5729ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
5730POSSIBILITY OF SUCH DAMAGE.
5731
5732
5733=head1 AUTHOR
5734
5735S<Jan Peter Kessler E<lt>info (AT) postfwd (DOT) orgE<gt>>. Let me know, if you have any suggestions.
5736
5737=cut
5738
5739