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 ©_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