1# spam-lib.pl
2# Common functions for parsing and editing the spamassassin config file
3
4BEGIN { push(@INC, ".."); };
5use WebminCore;
6use Fcntl;
7&init_config();
8
9$warn_procmail = $config{'warn_procmail'};
10if ($module_info{'usermin'}) {
11	# Running under Usermin, editing user's personal config file
12	&switch_to_remote_user();
13	&create_user_config_dirs();
14	if ($config{'local_cf'} !~ /^\//) {
15		# Path is relative to home dir
16		&set_config_file("$remote_user_info[7]/$config{'local_cf'}");
17		if ($local_cf =~ /^(.*)\// && !-d $1) {
18			mkdir($1, 0700);
19			}
20		}
21	else {
22		&set_config_file($config{'local_cf'});
23		}
24	$database_userpref_name = $remote_user;
25	$include_config_files = !$config{'mode'} || $config{'readfiles'};
26	$add_to_db = 1;
27	$max_awl_keys = $userconfig{'max_awl'} || 200;
28	}
29else {
30	# Running under Webmin, typically editing global config file
31	%access = &get_module_acl();
32	if ($access{'file'}) {
33		&set_config_file($access{'file'});
34		}
35	else {
36		if (!-r $config{'local_cf'} && -r $config{'alt_local_cf'}) {
37			# Copy in default config file
38			&copy_source_dest($config{'alt_local_cf'},
39					  $config{'local_cf'});
40			}
41		&set_config_file($config{'local_cf'});
42		}
43	if ($access{'nocheck'}) {
44		$warn_procmail = 0;
45		}
46	$database_userpref_name = $config{'dbglobal'} || '@GLOBAL';
47	$include_config_files = 1;
48	$add_to_db = $config{'addto'};
49	$max_awl_keys = $config{'max_awl'} || 200;
50	}
51$ldap_spamassassin_attr = $config{'attr'} || 'spamassassin';
52$ldap_username_attr = $config{'uid'} || 'uid';
53
54# set_config_file(file)
55# Change the default file read by get_config. Under Webmin, checks if this file
56# is accessible to the current user
57sub set_config_file
58{
59local ($file) = @_;
60if (!$module_info{'usermin'}) {
61	# Check for valid file
62	local %cans;
63	$cans{$access{'file'}} = 1 if ($access{'file'});
64	foreach my $f (split(/\s+/, $access{'files'})) {
65		$cans{$f} = 1;
66		}
67	if (keys %cans) {
68		$cans{$file} || &error(&text('index_ecannot',
69					"<tt>".&html_escape($file)."</tt>"));
70		}
71	}
72$local_cf = $file;
73$add_cf = !-d $local_cf ? $local_cf :
74	  $module_info{'usermin'} ? "$local_cf/user_prefs" :
75				    "$local_cf/local.cf";
76}
77
78sub set_config_file_in
79{
80local ($in) = @_;
81$header_subtext = undef;
82$redirect_url = "";
83$form_hiddens = "";
84if (!$module_info{'usermin'} && $in{'file'}) {
85	&set_config_file($in{'file'});
86	$header_subtext = $in{'title'} || "<tt>$in{'file'}</tt>";
87	$redirect_url = "index.cgi?file=".&urlize($in{'file'}).
88			"&title=".&urlize($in{'title'});
89	$form_hiddens = &ui_hidden("file", $in{'file'}).
90			&ui_hidden("title", $in{'title'});
91	$module_index_link = $redirect_url;
92	}
93}
94
95# get_config([file], [for-global])
96# Return a structure containing the contents of the spamassassin config file
97sub get_config
98{
99local $forglobal = $_[1];
100local @rv;
101if ($include_config_files || $forglobal) {
102	# Reading from file(s)
103	local $lnum = 0;
104	local $file = $_[0] || $local_cf;
105	if (-d $file) {
106		# A directory of files - read them all
107		opendir(DIR, $file);
108		local @files = sort { $a cmp $b } readdir(DIR);
109		closedir(DIR);
110		local $f;
111		foreach $f (@files) {
112			if ($f =~ /\.(cf|pre)$/) {
113				local $add = &get_config("$file/$f",$forglobal);
114				map { $_->{'index'} += scalar(@rv) } @$add;
115				push(@rv, @$add);
116				}
117			}
118		}
119	else {
120		# A single file that can be read right here
121		open(FILE, "<".$file);
122		while(<FILE>) {
123			s/\r|\n//g;
124			s/^#.*$//;
125			if (/^(\S+)\s*(.*)$/) {
126				local $dir = { 'name' => $1,
127					       'value' => $2,
128					       'index' => scalar(@rv),
129					       'file' => $file,
130					       'mode' => 0,
131					       'line' => $lnum };
132				$dir->{'words'} =
133					[ split(/\s+/, $dir->{'value'}) ];
134				push(@rv, $dir);
135				}
136			$lnum++;
137			}
138		close(FILE);
139		}
140	}
141
142if ($config{'mode'} == 1 || $config{'mode'} == 2) {
143	# Add from SQL database
144	local $dbh = &connect_spamassasin_db();
145	&error($dbh) if (!ref($dbh));
146	local $cmd = $dbh->prepare("select preference,value from userpref where username = ?");
147	$cmd->execute(!$forglobal ? $database_userpref_name :
148		      $config{'dbglobal'} ? $config{'dbglobal'} : '@GLOBAL');
149	while(my ($name, $value) = $cmd->fetchrow()) {
150		local $dir = { 'name' => $name,
151			       'value' => $value,
152			       'index' => scalar(@rv),
153			       'mode' => $config{'mode'} };
154		$dir->{'words'} =
155			[ split(/\s+/, $dir->{'value'}) ];
156		push(@rv, $dir);
157		}
158	$cmd->finish();
159	}
160elsif ($config{'mode'} == 3 && !$forglobal) {
161	# From LDAP
162	local $ldap = &connect_spamassassin_ldap();
163	&error($ldap) if (!ref($ldap));
164	local $uinfo = &get_ldap_user($ldap);
165	if ($uinfo) {
166		local $aindex = 0;
167		foreach my $a ($uinfo->get_value($ldap_spamassassin_attr)) {
168			local ($name, $value) = split(/\s+/, $a, 2);
169			local $dir = { 'name' => $name,
170				       'value' => $value,
171				       'index' => scalar(@rv),
172				       'aindex' => $aindex++,
173				       'oldattr' => $a,
174				       'mode' => $config{'mode'} };
175			$dir->{'words'} =
176				[ split(/\s+/, $dir->{'value'}) ];
177			push(@rv, $dir);
178			}
179		}
180	}
181
182return \@rv;
183}
184
185# find(name, &config)
186sub find
187{
188local @rv;
189foreach $c (@{$_[1]}) {
190	push(@rv, $c) if (lc($c->{'name'}) eq lc($_[0]));
191	}
192return wantarray ? @rv : $rv[0];
193}
194
195# find_value(name, &config)
196sub find_value
197{
198local @rv = map { $_->{'value'} } &find(@_);
199return wantarray ? @rv : $rv[0];
200}
201
202# save_directives(&config, name|&old, &new, valuesonly)
203# Update the config file with some directives
204sub save_directives
205{
206if ($module_info{'usermin'} && $local_cf =~ /^(.*)\/([^\/]+)$/) {
207	# Under Usermin, make sure .spamassassin exists
208	local $spamdir = $1;
209	if (!-d $spamdir) {
210		&make_dir($spamdir, 0755);
211		}
212	}
213local @old = ref($_[1]) ? @{$_[1]} : &find($_[1], $_[0]);
214local @new = $_[3] ? &make_directives($_[1], $_[2]) : @{$_[2]};
215local $i;
216for($i=0; $i<@old || $i<@new; $i++) {
217	local $line;
218	if ($new[$i]) {
219		$line = $new[$i]->{'name'};
220		$line .= " ".$new[$i]->{'value'} if ($new[$i]->{'value'} ne '');
221		}
222	if ($old[$i] && $new[$i]) {
223		# Replacing a directive
224		if ($old[$i]->{'name'} eq $new[$i]->{'name'} &&
225		    $old[$i]->{'value'} eq $new[$i]->{'value'}) {
226			# Nothing to do!
227			next;
228			}
229		if ($old[$i]->{'mode'} == 0) {
230			# In a file
231			local $lref = &read_file_lines($old[$i]->{'file'});
232			$lref->[$old[$i]->{'line'}] = $line;
233			}
234		elsif ($old[$i]->{'mode'} == 1 || $old[$i]->{'mode'} == 2) {
235			# In an SQL DB
236			local $dbh = &connect_spamassasin_db();
237			&error($dbh) if (!ref($dbh));
238			local $cmd = $dbh->prepare("update userpref set value = ? where username = ? and preference = ? and value = ?");
239			$cmd->execute($new[$i]->{'value'},
240				      $database_userpref_name,
241				      $old[$i]->{'name'},
242				      $old[$i]->{'value'});
243			$cmd->finish();
244			}
245		elsif ($old[$i]->{'mode'} == 3) {
246			# In LDAP - modify the attribute
247			local $ldap = &connect_spamassassin_ldap();
248			&error($ldap) if (!ref($ldap));
249			local $uinfo = &get_ldap_user($ldap);
250			$uinfo || &error(&text('ldap_euser',
251					       $database_userpref_name));
252			local @values = $uinfo->get_value(
253						$ldap_spamassassin_attr);
254			$values[$old[$i]->{'aindex'}] = $new[$i]->{'name'}." ".
255						        $new[$i]->{'value'};
256			local $rv = $ldap->modify(
257			    $uinfo->dn(),
258			    replace => { $ldap_spamassassin_attr =>
259					 \@values });
260			if (!$rv || $rv->code) {
261				&error(&text('eldap',
262				    $rv ? $rv->error : "Unknown modify error"));
263				}
264			}
265		$_[0]->[$old[$i]->{'index'}] = $new[$i];
266		}
267	elsif ($old[$i]) {
268		# Deleting a directive
269		if ($old[$i]->{'mode'} == 0) {
270			# From a file
271			local $lref = &read_file_lines($old[$i]->{'file'});
272			splice(@$lref, $old[$i]->{'line'}, 1);
273			foreach $c (@{$_[0]}) {
274				if ($c->{'line'} > $old[$i]->{'line'} &&
275				    $c->{'file'} eq $old[$i]->{'file'}) {
276					$c->{'line'}--;
277					}
278				}
279			}
280		elsif ($old[$i]->{'mode'} == 1 || $old[$i]->{'mode'} == 2) {
281			# From an SQL DB
282			local $dbh = &connect_spamassasin_db();
283			&error($dbh) if (!ref($dbh));
284			local $cmd = $dbh->prepare("delete from userpref where username = ? and preference = ? and value = ?");
285			$cmd->execute($database_userpref_name,
286				      $old[$i]->{'name'},
287				      $old[$i]->{'value'});
288			$cmd->finish();
289			}
290		elsif ($old[$i]->{'mode'} == 3) {
291			# From LDAP .. get current values, and remove this one
292			local $ldap = &connect_spamassassin_ldap();
293			&error($ldap) if (!ref($ldap));
294			local $uinfo = &get_ldap_user($ldap);
295			$uinfo || &error(&text('ldap_euser',
296					       $database_userpref_name));
297			local @values = $uinfo->get_value(
298						$ldap_spamassassin_attr);
299			splice(@values, $old[$i]->{'aindex'}, 1);
300			local $rv = $ldap->modify(
301			    $uinfo->dn(),
302			    replace => { $ldap_spamassassin_attr =>
303					 \@values });
304			if (!$rv || $rv->code) {
305				&error(&text('eldap',
306				    $rv ? $rv->error : "Unknown delete error"));
307				}
308			}
309
310		# Fix up indexes
311		splice(@{$_[0]}, $old[$i]->{'index'}, 1);
312		foreach $c (@{$_[0]}) {
313			if ($c->{'index'} > $old[$i]->{'index'}) {
314				$c->{'index'}--;
315				}
316			}
317		}
318	elsif ($new[$i]) {
319		# Adding a directive
320		local $addmode = scalar(@old) ? $old[0]->{'mode'} :
321				 $new[$i]->{'name'} =~ /^user_scores_/ ? 0 :
322				 $add_to_db ? $config{'mode'} : 0;
323		if ($addmode == 0) {
324			# To a file
325			local $lref = &read_file_lines($add_cf);
326			$new[$i]->{'line'} = @$lref;
327			push(@$lref, $line);
328			}
329		elsif ($addmode == 1 || $addmode == 2) {
330			# To an SQL DB
331			local $dbh = &connect_spamassasin_db();
332			&error($dbh) if (!ref($dbh));
333			local $cmd = $dbh->prepare("insert into userpref (username, preference, value) values (?, ?, ?)");
334			$cmd->execute($database_userpref_name,
335				      $new[$i]->{'name'},
336				      $new[$i]->{'value'});
337			$cmd->finish();
338			}
339		elsif ($addmode == 3) {
340			# To LDAP
341			local $ldap = &connect_spamassassin_ldap();
342			&error($ldap) if (!ref($ldap));
343			local $uinfo = &get_ldap_user($ldap);
344			$uinfo || &error(&text('ldap_euser',
345					       $database_userpref_name));
346			local $rv = $ldap->modify(
347			    $uinfo->dn(),
348			    add => { $ldap_spamassassin_attr =>
349				$new[$i]->{'name'}." ".$new[$i]->{'value'} });
350			if (!$rv || $rv->code) {
351				&error(&text('eldap',
352				     $rv ? $rv->error : "Unknown add error"));
353				}
354			}
355		$new[$i]->{'mode'} = $addmode;
356		$new[$i]->{'index'} = @{$_[0]};
357		push(@{$_[0]}, $new[$i]);
358		}
359	}
360}
361
362# make_directives(name, &values)
363sub make_directives
364{
365return map { { 'name' => $_[0],
366	       'value' => $_ } } @{$_[1]};
367}
368
369### UI functions ###
370
371# edit_table(name, &headings, &&values, &sizes, [&convfunc], blankrows)
372# Display a table of values for editing, with one blank row
373sub edit_table
374{
375local ($h, $v);
376local $rv = &ui_columns_start($_[1]);
377local $i = 0;
378local $cfunc = $_[4] || \&default_convfunc;
379local $blanks = $_[5] || 1;
380foreach $v (@{$_[2]}, map { [ ] } (1 .. $blanks)) {
381	local @cols;
382	for($j=0; $j<@{$_[1]}; $j++) {
383		push(@cols, &$cfunc($j, "$_[0]_${i}_${j}", $_[3]->[$j],
384				     $v->[$j], $v));
385		}
386	$rv .= &ui_columns_row(\@cols);
387	$i++;
388	}
389$rv .= &ui_columns_end();
390return $rv;
391}
392
393# default_convfunc(column, name, size, value)
394sub default_convfunc
395{
396return "<input name=$_[1] size=$_[2] value='".&html_escape($_[3])."'>";
397}
398
399# parse_table(name, &parser)
400# Parse the inputs from a table and return an array of results
401sub parse_table
402{
403local ($i, @rv);
404local $pfunc = $_[1] || \&default_parsefunc;
405for($i=0; defined($in{"$_[0]_${i}_0"}); $i++) {
406	local ($j, $v, @vals);
407	for($j=0; defined($v = $in{"$_[0]_${i}_${j}"}); $j++) {
408		push(@vals, $v);
409		}
410	local $p = &$pfunc("$_[0]_${i}", @vals);
411	push(@rv, $p) if (defined($p));
412	}
413return @rv;
414}
415
416# default_parsefunc(rowname, value, ...)
417# Returns a value or undef if empty, or calls &error if invalid
418sub default_parsefunc
419{
420return $_[1] ? join(" ", @_[1..$#_]) : undef;
421}
422
423# start_form(cgi, header, [right-header])
424sub start_form
425{
426local ($cgi, $header, $right) = @_;
427print &ui_form_start($cgi, "post");
428print &ui_table_start($header, "width=100%", 2, undef, $right);
429print $form_hiddens;
430}
431
432# end_form(buttonname, buttonvalue, ...)
433sub end_form
434{
435print &ui_table_end();
436local @buts;
437for(my $i=0; $i<@_; $i+=2 ) {
438	local $al = $i == 0 ? "align=left" :
439		    $i == @_-2 ? "align=right" : "align=center";
440	push(@buts, [ $_[$i], $_[$i+1] ]);
441	}
442print &ui_form_end(\@buts);
443}
444
445# yes_no_field(name, value, default)
446sub yes_no_field
447{
448local $v = !$_[1] ? -1 : $_[1]->{'value'};
449local $def = &find_default($_[0], $_[2]) ? $text{'yes'} : $text{'no'};
450return &ui_radio($_[0], $v,
451		 [ [ 1, $text{'yes'} ], [ 0, $text{'no'} ],
452		   [ -1, $text{'default'}." (".$def.")" ] ]);
453}
454
455# parse_yes_no(&config, name)
456sub parse_yes_no
457{
458&save_directives($_[0], $_[1], $in{$_[1]} == 1 ? [ 1 ] :
459			       $in{$_[1]} == 0 ? [ 0 ] : [ ], 1);
460}
461
462# option_field(name, value, default, &opts)
463sub option_field
464{
465local $v = !$_[1] ? -1 : $_[1]->{'value'};
466local $def = &find_default($_[0], $_[2]);
467local ($defopt) = grep { $_->[0] eq $def } @{$_[3]};
468return &ui_radio($_[0], $v,
469		 [ @{$_[3]}, [ -1, "$text{'default'} ($defopt->[1])" ] ]);
470}
471
472sub parse_option
473{
474&save_directives($_[0], $_[1], $in{$_[1]} == -1 ? [ ] : [ $in{$_[1]} ], 1);
475}
476
477# opt_field(name, value, size, default)
478sub opt_field
479{
480local $def = &find_default($_[0], $_[3]) if ($_[3]);
481return &ui_opt_textbox($_[0],
482	!$_[1] ? undef : ref($_[1]) ? $_[1]->{'value'} : $_[1],
483	$_[2], $text{'default'}.($_[3] ? " ($def)" : ""));
484}
485
486# parse_opt(&config, name, [&checkfunc])
487sub parse_opt
488{
489if (defined($in{"$_[1]_default"}) && $in{"$_[1]_default"} eq $in{$_[1]} ||
490    !defined($in{"$_[1]_default"}) && $in{"$_[1]_def"}) {
491	&save_directives($_[0], $_[1], [ ], 1);
492	}
493else {
494	&{$_[2]}($in{$_[1]}) if ($_[2]);
495	&save_directives($_[0], $_[1], [ $in{$_[1]} ], 1);
496	}
497}
498
499# edit_textbox(name, &values, width, height, [disabled])
500sub edit_textbox
501{
502return &ui_textarea($_[0], join("\n", @{$_[1]}), $_[3], $_[2], undef, $_[4]);
503}
504
505# parse_textbox(&config, name)
506sub parse_textbox
507{
508$in{$_[1]} =~ s/^\s+//;
509$in{$_[1]} =~ s/\s+$//;
510local @v = split(/\s+/, $in{$_[1]});
511&save_directives($_[0], $_[1], \@v, 1);
512}
513
514# get_procmailrc()
515# Returns the full paths to the procmail config files in use, the last one
516# being the user's config
517sub get_procmailrc
518{
519if ($module_info{'usermin'}) {
520	local @rv;
521	push(@rv, $config{'global_procmailrc'});
522	push(@rv, $config{'procmailrc'} || $procmail::procmailrc);
523	return @rv;
524	}
525else {
526	return ( $access{'procmailrc'} || $config{'procmailrc'} || $procmail::procmailrc );
527	}
528}
529
530# find_default(name, compiled-in-default)
531sub find_default
532{
533if ($config{'global_cf'}) {
534	if (!defined($global_config_cache)) {
535		$global_config_cache = &get_config($config{'global_cf'}, 1);
536		}
537	local $v = &find_value($_[0], $global_config_cache);
538	return $v if (defined($v));
539	}
540return $_[1];
541}
542
543# can_use_page(page)
544# Returns 1 if some page can be used, 0 if not
545sub can_use_page
546{
547local %avail_icons;
548if ($module_info{'usermin'}) {
549	%avail_icons = map { $_, 1 } split(/,/, $config{'avail_icons'});
550	}
551else {
552	%avail_icons = map { $_, 1 } split(/,/, $access{'avail'});
553	}
554local $p = $_[0] eq "simple" ? "header" : $_[0];
555return $avail_icons{$p};
556}
557
558# can_use_check(page)
559# Calls error if some page cannot be used
560sub can_use_check
561{
562&can_use_page($_[0]) || &error($text{'ecannot'});
563}
564
565# get_spamassassin_version(&out)
566sub get_spamassassin_version
567{
568local $out;
569&execute_command("$config{'spamassassin'} -V", undef, \$out, \$out, 0, 1);
570${$_[0]} = $out if ($_[0]);
571return $out =~ /(version|Version:)\s+(\S+)/ ? $2 : undef;
572}
573
574# version_atleast(num)
575sub version_atleast
576{
577if (!$version_cache) {
578	$version_cache = &get_spamassassin_version();
579	}
580return $version_cache >= $_[0];
581}
582
583# spam_file_folder()
584sub spam_file_folder
585{
586&foreign_require("mailbox", "mailbox-lib.pl");
587local ($sf) = grep { $_->{'spam'} } &mailbox::list_folders();
588return $sf;
589}
590
591# disable_indexing(&folder)
592sub disable_indexing
593{
594if (!$config{'index_spam'}) {
595	$mailbox::config{'index_min'} = 1000000000;
596	unlink(&mailbox::user_index_file($_[0]->{'file'}));
597	}
598}
599
600# get_process_pids()
601# Returns the PIDs and names of SpamAssassin daemon processes like spamd
602sub get_process_pids
603{
604local ($pn, @pids);
605foreach $pn (split(/\s+/, $config{'processes'})) {
606	push(@pids, map { [ $_, $pn ] } &find_byname($pn));
607	}
608return @pids;
609}
610
611sub lock_spam_files
612{
613local $conf = &get_config();
614@spam_files = &unique(map { $_->{'file'} } @$conf);
615local $f;
616foreach $f (@spam_files) {
617	&lock_file($f);
618	}
619}
620
621sub unlock_spam_files
622{
623local $f;
624foreach $f (@spam_files) {
625	&unlock_file($f);
626	}
627}
628
629# show_buttons(number)
630sub show_buttons
631{
632print "<table width=100%> <tr>\n";
633local $onclick = "onClick='return check_clicks(form)'"
634	if (defined(&check_clicks_function));
635print "<td align=left><input type=submit name=inbox value=\"$text{'mail_inbox'}\" $onclick></td>\n";
636print "<td align=left><input type=submit name=whitelist value=\"$text{'mail_whitelist2'}\" $onclick></td>\n";
637if (&has_command($config{'sa_learn'})) {
638	print "<td align=center><input type=submit name=ham value=\"$text{'mail_ham'}\" $onclick></td>\n";
639	}
640print "<td align=right><input type=submit name=delete value=\"$text{'mail_delete'}\" $onclick></td>\n";
641print "<td align=right><input type=submit name=razor value=\"$text{'mail_razor'}\" $onclick></td>\n";
642print "</tr></table>\n";
643}
644
645# restart_spamd()
646# Re-start all SpamAssassin processes, or return an error message
647sub restart_spamd
648{
649if ($config{'restart_cmd'}) {
650	local $out = &backquote_logged(
651		"$config{'restart_cmd'} 2>&1 </dev/null");
652	if ($? || $out =~ /error|failed/i) {
653		return "<pre>$out</pre>";
654		}
655	}
656else {
657	local @pids = &get_process_pids();
658	@pids || return $text{'apply_none'};
659	local $p;
660	foreach $p (@pids) {
661		&kill_logged("HUP", $p->[0]);
662		}
663	}
664return undef;
665}
666
667# find_spam_recipe(&recipes)
668# Returns the recipe that runs spamassassin
669sub find_spam_recipe
670{
671local $r;
672foreach $r (@{$_[0]}) {
673	if ($r->{'action'} =~ /spamassassin/i ||
674	    $r->{'action'} =~ /spamc/i) {
675		return $r;
676		}
677	}
678return undef;
679}
680
681# find_file_recipe(&recipes)
682# returns the recipe for delivering mail based on the x-spam-status header
683sub find_file_recipe
684{
685local ($r, $c);
686foreach $r (@{$_[0]}) {
687	foreach $c (@{$r->{'conds'}}) {
688		if ($c->[1] =~ /x-spam-status/i) {
689			return $r;
690			}
691		}
692	}
693return undef;
694}
695
696# find_delete_recipe(&recipes)
697# returns the recipe for delete mail based on the x-spam-level header, and
698# the level it deletes at.
699sub find_delete_recipe
700{
701local ($r, $c);
702foreach $r (grep { $_->{'action'} eq '/dev/null' } @{$_[0]}) {
703	foreach $c (@{$r->{'conds'}}) {
704		if ($c->[1] =~ /x-spam-level:\s+((\\\*)+)/i) {
705			return ($r, length($1)/2);
706			}
707		}
708	}
709return ( );
710}
711
712# find_virtualmin_recipe(&recipes)
713# Returns the recipe that runs the Virtualmin lookup command
714sub find_virtualmin_recipe
715{
716local ($r, $c);
717foreach $r (@{$_[0]}) {
718	if ($r->{'action'} =~ /^VIRTUALMIN=/) {
719		return $r;
720		}
721	}
722return undef;
723}
724
725# find_force_default_receipe(&recipes)
726# Returns the recipe that forces delivery to $DEFAULT, used by Virtualmin and
727# others to prevent per-user .procmailrc settings
728sub find_force_default_receipe
729{
730local ($r, $c);
731foreach $r (@{$_[0]}) {
732	if ($r->{'action'} eq '$DEFAULT' && !@{$r->{'conds'}}) {
733		return $r;
734		}
735	}
736return undef;
737}
738
739# get_simple_tests(&conf)
740sub get_simple_tests
741{
742local ($conf) = @_;
743local (@simple, %simple);
744foreach my $h (&find("header", $conf)) {
745	if ($h->{'value'} =~ /^(\S+)\s+(\S+)\s+=~\s+\/(.*)\/(\S*)\s*$/) {
746		push(@simples, { 'header_dir' => $h,
747				 'name' => $1,
748				 'header' => lc($2),
749			 	 'regexp' => $3,
750				 'flags' => $4, });
751		$simples{$1} = $simples[$#simples];
752		}
753	}
754foreach my $b (&find("body", $conf), &find("full", $conf),
755	       &find("uri", $conf)) {
756	if ($b->{'value'} =~ /^(\S+)\s+\/(.*)\/(\S*)\s*$/) {
757		push(@simples, { $b->{'name'}.'_dir' => $b,
758				 'name' => $1,
759				 'header' => $b->{'name'},
760			 	 'regexp' => $2,
761				 'flags' => $3, });
762		$simples{$1} = $simples[$#simples];
763		}
764	}
765foreach my $s (&find("score", $conf)) {
766	if ($s->{'value'} =~ /^(\S+)\s+(\S+)/ && $simples{$1}) {
767		$simples{$1}->{'score_dir'} = $s;
768		$simples{$1}->{'score'} = $2;
769		}
770	}
771foreach my $d (&find("describe", $conf)) {
772	if ($d->{'value'} =~ /^(\S+)\s+(\S.*)/ && $simples{$1}) {
773		$simples{$1}->{'describe_dir'} = $d;
774		$simples{$1}->{'describe'} = $2;
775		}
776	}
777return @simples;
778}
779
780# get_procmail_command()
781# Returns the command that should be used in /etc/procmailrc to call
782# spamassassin, such as spamc or the full spamassassin path
783sub get_procmail_command
784{
785if ($config{'procmail_cmd'} eq '*') {
786	# Is spamd running?
787	if (&get_process_pids()) {
788		local $spamc = &has_command("spamc");
789		return $spamc if ($spamc);
790		}
791	return &has_command($config{'spamassassin'});
792	}
793elsif ($config{'procmail_cmd'}) {
794	return $config{'procmail_cmd'};
795	}
796else {
797	return &has_command($config{'spamassassin'});
798	}
799}
800
801# execute_before(section)
802# If a before-change command is configured, run it. If it fails, call error
803sub execute_before
804{
805local ($section) = @_;
806if ($config{'before_cmd'}) {
807	$ENV{'SPAM_SECTION'} = $section;
808	local $out;
809	local $rv = &execute_command(
810			$config{'before_cmd'}, undef, \$out, \$out);
811	$rv && &error(&text('before_ecmd',
812			    "<pre>".&html_escape($out)."</pre>"));
813	}
814}
815
816# execute_after(section)
817# If a after-change command is configured, run it. If it fails, call error
818sub execute_after
819{
820local ($section) = @_;
821if ($config{'after_cmd'}) {
822	$ENV{'SPAM_SECTION'} = $section;
823	local $out;
824	local $rv = &execute_command(
825			$config{'after_cmd'}, undef, \$out, \$out);
826	$rv && &error(&text('after_ecmd',
827			    "<pre>".&html_escape($out)."</pre>"));
828	}
829}
830
831# check_spamassassin_db()
832# Checks if the LDAP or MySQL backend can be contacted, and if not returns
833# an error message.
834sub check_spamassassin_db
835{
836if ($config{'mode'} == 0) {
837	return undef;	# Local files always work
838	}
839elsif ($config{'mode'} == 1 || $config{'mode'} == 2) {
840	# Connect to a database
841	local $dbh = &connect_spamassasin_db();
842	return $dbh if (!ref($dbh));
843	local $testcmd = $dbh->prepare("select * from userpref limit 1");
844	if (!$testcmd || !$testcmd->execute()) {
845		undef($connect_spamassasin_db_cache);
846		$dbh->disconnect();
847		return &text('connect_equery', "<tt>$config{'db'}</tt>",
848					       "<tt>userpref</tt>");
849		}
850	$testcmd->finish();
851	undef($connect_spamassasin_db_cache);
852	$dbh->disconnect();
853	return undef;
854	}
855elsif ($config{'mode'} == 3) {
856	# Connect to LDAP
857	local $ldap = &connect_spamassassin_ldap();
858	return $ldap if (!ref($ldap));
859	local $rv = $ldap->search(base => $config{'base'},
860				  filter => "(uid=$remote_user)",
861				  sizelimit => 1);
862	if (!$rv || $rv->code) {
863		return &text('connect_ebase', "<tt>$config{'base'}</tt>",
864			     $rv ? $rv->error : "Unknown search error");
865		}
866	return undef;
867	}
868else {
869	return "Unknown config mode $config{'mode'} !";
870	}
871}
872
873# connect_spamassasin_db()
874# Attempts to connect to the SpamAssasin MySQL or PostgreSQL database. Returns
875# a driver handle on success, or an error message string on failure.
876sub connect_spamassasin_db
877{
878if (defined($connect_spamassasin_db_cache)) {
879	return $connect_spamassasin_db_cache;
880	}
881local $driver = $config{'mode'} == 1 ? "mysql" : "Pg";
882local $drh;
883eval <<EOF;
884use DBI;
885\$drh = DBI->install_driver(\$driver);
886EOF
887if ($@) {
888	return &text('connect_edriver', "<tt>DBD::$driver</tt>");
889        }
890local $dbistr = &make_dbistr($driver, $config{'db'}, $config{'server'});
891local $dbh = $drh->connect($dbistr,
892                           $config{'user'}, $config{'pass'}, { });
893$dbh || return &text('connect_elogin',
894		     "<tt>$config{'db'}</tt>", $drh->errstr)."\n";
895$connect_spamassasin_db_cache = $dbh;
896return $dbh;
897}
898
899# connect_spamassassin_ldap()
900# Attempts to connect to the configured LDAP DB, and returns the handle on
901# success, or an error message on failure.
902sub connect_spamassassin_ldap
903{
904if (defined($connect_spamassasin_ldap_cache)) {
905	return $connect_spamassasin_ldap_cache;
906	}
907eval "use Net::LDAP";
908if ($@) {
909	return &text('connect_eldapmod', "<tt>Net::LDAP</tt>");
910	}
911local $port = $config{'port'} || 389;
912local $inet6 = !&to_ipaddress($config{'server'}) &&
913	        &to_ip6address($config{'server'});
914local $ldap = Net::LDAP->new($config{'server'},
915			     port => $port,
916			     inet6 => $inet6);
917if (!$ldap) {
918	return &text('connect_eldap', "<tt>$config{'server'}</tt>", $port);
919	}
920local $mesg = $ldap->bind(dn => $config{'user'}, password => $config{'pass'});
921if (!$mesg || $mesg->code) {
922	return &text('connect_eldaplogin', "<tt>$config{'server'}</tt>",
923		     "<tt>$config{'user'}</tt>",
924		     $mesg ? $mesg->error : "Unknown error");
925	}
926$connect_spamassasin_ldap_cache = $ldap;
927return $ldap;
928}
929
930sub make_dbistr
931{
932local ($driver, $db, $host) = @_;
933local $rv;
934if ($driver eq "mysql") {
935	$rv = "database=$db";
936	}
937elsif ($driver eq "Pg") {
938	$rv = "dbname=$db";
939	}
940else {
941	$rv = $db;
942	}
943if ($host) {
944	$rv .= ";host=$host";
945	}
946return $rv;
947}
948
949# get_ldap_user(&ldap, [username])
950# Returns the LDAP object for a user, or undef if not found
951sub get_ldap_user
952{
953local ($ldap, $user) = @_;
954$user ||= $database_userpref_name;
955#if (exists($get_ldap_user_cache{$user})) {
956#	return $get_ldap_user_cache{$user};
957#	}
958local $rv = $ldap->search(base => $config{'base'},
959			  filter => "($ldap_username_attr=$user)",
960			 );
961if (!$rv || $rv->code) {
962	&error(&text('eldap', $rv ? $rv->error : "Search failed"));
963	}
964local ($uinfo) = $rv->all_entries;
965$get_ldap_user_cache{$user} = $uinfo;
966return $uinfo;
967}
968
969# get_auto_whitelist_file([user])
970# Returns the base path to the auto whitelist DBM, if any.
971sub get_auto_whitelist_file
972{
973local ($user) = @_;
974local @uinfo = $module_info{'usermin'} ? @remote_user_info :
975	       $user ? getpwnam($user) : ( );
976local $conf = &get_config();
977local $awp = &find("auto_whitelist_path", $conf);
978if (!$awp) {
979	$awp = &find_default("auto_whitelist_path");
980	}
981$awp ||= "~/.spamassassin/auto-whitelist";
982if ($awp !~ /^\//) {
983	# Make absolute
984	return undef if (scalar(@uinfo) == 0);
985	$awp =~ s/^(\~|\$HOME)\//$uinfo[7]\//;
986	if ($awp !~ /^\//) {
987		$awp = "$uinfo[7]/$awp";
988		}
989	}
990# Does it exist?
991if (!-r $awp) {
992	local @real = glob("$awp.*");
993	$awp = undef if (!@real);
994	}
995# Is it under the user's home?
996if (!&is_under_directory($uinfo[7], $awp)) {
997	$awp = undef;
998	}
999return $awp;
1000}
1001
1002# open_auto_whitelist_dbm([user])
1003# Ties the %awl hash to the autowhitelist DBM file. Returns 1 if successful, or
1004# 0 if it could not be opened, or -1 if empty.
1005sub open_auto_whitelist_dbm
1006{
1007local ($user) = @_;
1008local $awp = &get_auto_whitelist_file($user);
1009return 0 if (!$awp);
1010local $anyok;
1011foreach my $cls ('DB_File', 'GDBM_File', 'SDBM_File') {
1012	$@ = undef;
1013	eval "use $cls";
1014	next if ($@);
1015	tie(%awl, $cls, $awp, O_RDWR, 0755) || next;
1016	if (scalar(keys %awl)) {
1017		return 1;
1018		}
1019	$anyok = 1;
1020	}
1021return $anyok ? -1 : 0;
1022}
1023
1024# close_auto_whitelist_dbm()
1025# Disconnects the global %awl hash from the DBM file, flushing changes to disk
1026sub close_auto_whitelist_dbm
1027{
1028untie(%awl);
1029}
1030
1031# supports_auto_whitelist()
1032# Returns 1 if SpamAssassin is doing auto-whitelisting for the current user,
1033# 2 if for multiple users.
1034sub supports_auto_whitelist
1035{
1036if ($module_info{'usermin'}) {
1037	return &get_auto_whitelist_file() ? 1 : 0;
1038	}
1039else {
1040	return 2;
1041	}
1042}
1043
1044sub can_edit_awl
1045{
1046local ($user) = @_;
1047return 1 if ($module_info{'usermin'});		# Only one user anyway
1048if ($access{'awl_users'}) {
1049	# Check if on user list
1050	return &indexof($user, split(/\s+/, $access{'awl_users'})) >= 0;
1051	}
1052elsif ($access{'awl_groups'}) {
1053	# Check if the user is a member of any of the allowed groups
1054	local %ugroups;
1055	local @uinfo = getpwnam($user);
1056	return 0 if (!scalar(@uinfo));
1057	local @ginfo = getgrgid($uinfo[3]);
1058	$ugroups{$ginfo[0]}++ if (scalar(@ginfo));
1059	foreach my $o (&other_groups($user)) {
1060		$ugroups{$o}++;
1061		}
1062	local @can = grep { $ugroups{$_} } split(/\s+/, $access{'awl_groups'});
1063	return @can ? 1 : 0;
1064	}
1065else {
1066	# No restrictions
1067	return 1;
1068	}
1069}
1070
1071# list_spamassassin_languages()
1072# Returns a list of language codes and descriptions
1073sub list_spamassassin_languages
1074{
1075local @rv;
1076open(LANGS, "<$module_root_directory/langs");
1077while(<LANGS>) {
1078	if (/^(\S+)\s+(.*)/) {
1079		push(@rv, [ $1, $2 ]);
1080		}
1081	}
1082close(LANGS);
1083return @rv;
1084}
1085
1086# list_spamassassin_locales()
1087# Returns a list of locale codes and descriptions
1088sub list_spamassassin_locales
1089{
1090local @rv;
1091open(LANGS, "<$module_root_directory/locales");
1092while(<LANGS>) {
1093	if (/^(\S+)\s+(.*)/) {
1094		push(@rv, [ $1, $2 ]);
1095		}
1096	}
1097close(LANGS);
1098return @rv;
1099}
1100
1101# list_spamassassin_plugins()
1102# Returns a list of plugins enabled, both globally and for this user
1103sub list_spamassassin_plugins
1104{
1105my @rv;
1106if ($config{'global_cf'}) {
1107	my $gconf = &get_config($config{'global_cf'}, 1);
1108	push(@rv, &find_value("loadplugin", $gconf));
1109	}
1110my $conf = &get_config();
1111push(@rv, &find_value("loadplugin", $conf));
1112return @rv;
1113}
1114
11151;
1116
1117