1# postfix-lib.pl 2# 3# postfix-module by Guillaume Cottenceau <gc@mandrakesoft.com>, 4# for webmin by Jamie Cameron 5 6$POSTFIX_MODULE_VERSION = 5; 7 8BEGIN { push(@INC, ".."); }; 9use WebminCore; 10&init_config(); 11%access = &get_module_acl(); 12$access{'postfinger'} = 0 if (&is_readonly_mode()); 13do 'aliases-lib.pl'; 14 15$config{'perpage'} ||= 20; # a value of 0 can cause problems 16 17# Get the saved version number 18$version_file = "$module_config_directory/version"; 19$postfix_config_command = $config{'postfix_config_command'}; 20$has_postfix_config_command = &has_command($postfix_config_command); 21if (&open_readfile(VERSION, $version_file)) { 22 chop($postfix_version = <VERSION>); 23 close(VERSION); 24 my @vst = stat($version_file); 25 my @cst = stat($postfix_config_command); 26 if (@cst && $cst[9] > $vst[9]) { 27 # Postfix was probably upgraded 28 $postfix_version = undef; 29 } 30 } 31 32if (!$postfix_version) { 33 # Not there .. work it out 34 if ($has_postfix_config_command && 35 &backquote_command("$postfix_config_command -d mail_version 2>&1", 1) =~ /mail_version\s*=\s*(.*)/) { 36 # Got the version 37 $postfix_version = $1; 38 } 39 40 # And save for other callers 41 &open_tempfile(VERSION, ">$version_file", 0, 1); 42 &print_tempfile(VERSION, "$postfix_version\n"); 43 &close_tempfile(VERSION); 44 } 45 46if (&compare_version_numbers($postfix_version, 2) >= 0) { 47 $virtual_maps = "virtual_alias_maps"; 48 $ldap_timeout = "ldap_timeout"; 49 } 50else { 51 $virtual_maps = "virtual_maps"; 52 $ldap_timeout = "ldap_lookup_timeout"; 53 } 54 55 56sub guess_config_dir 57{ 58 my $answ = $config{'postfix_config_file'}; 59 $answ =~ /(.*)\/[^\/]*/; 60 return $1; 61} 62 63$config_dir = guess_config_dir(); 64 65 66## DOC: compared to other webmin modules, here we don't need to parse 67## the config file, because a config command is provided by 68## postfix to read and write the config parameters 69 70 71# postfix_module_version() 72# returns the version of the postfix module 73sub postfix_module_version 74{ 75 return $POSTFIX_MODULE_VERSION; 76} 77 78# is_postfix_running() 79# returns 1 if running, 0 if stopped, calls error() if problem 80sub is_postfix_running 81{ 82 my $queuedir = get_current_value("queue_directory"); 83 my $processid = get_current_value("process_id_directory"); 84 85 my $pid_file = $queuedir."/".$processid."/master.pid"; 86 my $pid = &check_pid_file($pid_file); 87 return $pid ? 1 : 0; 88} 89 90 91sub is_existing_parameter 92{ 93 my $out = &backquote_command("$config{'postfix_config_command'} -c $config_dir $_[0] 2>&1", 1); 94 return !($out =~ /unknown parameter/); 95} 96 97 98# get_current_value(parameter_name) 99# returns a scalar corresponding to the value of the parameter 100## modified to allow main_parameter:subparameter 101sub get_current_value 102{ 103# First try to get the value from main.cf directly 104my ($name,$key)=split /:/,$_[0]; 105my $lref = &read_file_lines($config{'postfix_config_file'}); 106my $out; 107my ($begin_flag, $end_flag); 108foreach my $l (@$lref) { 109 # changes made to this loop by Dan Hartman of Rae Internet / 110 # Message Partners for multi-line parsing 2007-06-04 111 if ($begin_flag == 1 && $l =~ /\S/ && $l =~ /^(\s+[^#].+)/) { 112 # non-comment continuation line, and replace tabs with spaces 113 $out .= $1; 114 $out =~ s/^\s+/ /; 115 } 116 if ($l =~ /^\s*([a-z0-9\_]+)\s*=\s*(.*)|^\s*([a-z0-9\_]+)\s*=\s*$/ && 117 $1 . $3 eq $name) { 118 # Found the one we're looking for, set a flag 119 $out = $2; 120 $begin_flag = 1; 121 } 122 if ($l =~ /^\s*([a-z0-9\_]+)\s*=\s*(.*)|^\s*([a-z0-9\_]+)\s*=\s*$/ && 123 $1 . $3 ne $name && $begin_flag == 1) { 124 # after the beginning, another configuration variable 125 # found! Stop! 126 $end_flag = 1; 127 last; 128 } 129 } 130if (!defined($out) && !$_[1]) { 131 # Fall back to asking Postfix 132 # -h tells postconf not to output the name of the parameter 133 my $err; 134 &execute_command("$config{'postfix_config_command'} -c $config_dir -h ". 135 quotemeta($name), undef, \$out, \$err, 0, 1); 136 if ($?) { 137 &error(&text('query_get_efailed', $name, $out)); 138 } 139 elsif ($out =~ /warning:.*unknown\s+parameter/ || 140 $err =~ /warning:.*unknown\s+parameter/) { 141 return undef; 142 } 143 chop($out); 144 } 145else { 146 # Trim trailing whitespace 147 $out =~ s/\s+$//; 148 } 149if ($key) { 150 # If the value asked for was like foo:bar, extract from the value 151 # the parts after bar 152 my @res = ( ); 153 while($out =~ /^(.*?)\Q$key\E\s+(\S+)(.*)$/) { 154 my $v = $2; 155 $out = $3; 156 $v =~ s/,$//; 157 push(@res, $v); 158 } 159 return join(" ", @res); 160 } 161return $out; 162} 163 164# if_default_value(parameter_name) 165# returns if the value is the default value 166sub if_default_value 167{ 168 my ($name) = @_; 169 my $out = &backquote_command( 170 "$config{'postfix_config_command'} -c $config_dir -n ". 171 quotemeta($name)." 2>&1", 1); 172 if ($?) { &error(&text('query_get_efailed', $_[0], $out)); } 173 return ($out eq ""); 174} 175 176# get_default_value(parameter_name) 177# returns the default value of the parameter 178sub get_default_value 179{ 180 my $out = &backquote_command("$config{'postfix_config_command'} -c $config_dir -dh $_[0] 2>&1", 1); # -h tells postconf not to output the name of the parameter 181 if ($?) { &error(&text('query_get_efailed', $_[0], $out)); } 182 chop($out); 183 return $out; 184} 185 186 187# set_current_value(parameter_name, parameter_value, [always-set]) 188# Update some value in the Postfix configuration file 189sub set_current_value 190{ 191 my $value = $_[1]; 192 if ($value eq "__DEFAULT_VALUE_IE_NOT_IN_CONFIG_FILE__" || 193 $value eq &get_default_value($_[0]) && !$_[2]) 194 { 195 # there is a special case in which there is no static default value ; 196 # postfix will handle it correctly if I remove the line in `main.cf' 197 my $all_lines = &read_file_lines($config{'postfix_config_file'}); 198 my $line_of_parameter = -1; 199 my $end_line_of_parameter = -1; 200 my $i = 0; 201 202 foreach (@{$all_lines}) 203 { 204 if (/^\s*$_[0]\s*=/) { 205 $line_of_parameter = $i; 206 $end_line_of_parameter = $i; 207 } elsif ($line_of_parameter >= 0 && 208 /^\t+\S/) { 209 # Multi-line continuation 210 $end_line_of_parameter = $i; 211 } 212 $i++; 213 } 214 215 if ($line_of_parameter != -1) { 216 splice(@{$all_lines}, $line_of_parameter, 217 $end_line_of_parameter - $line_of_parameter + 1); 218 &flush_file_lines($config{'postfix_config_file'}); 219 } else { 220 &unflush_file_lines($config{'postfix_config_file'}); 221 } 222 } 223 else 224 { 225 local ($out, $ex); 226 $ex = &execute_command( 227 "$config{'postfix_config_command'} -c $config_dir ". 228 "-e $_[0]=".quotemeta($value), undef, \$out, \$out); 229 $ex && &error(&text('query_set_efailed', $_[0], $_[1], $out). 230 "<br> $config{'postfix_config_command'} -c $config_dir ". 231 "-e $_[0]=\"$value\" 2>&1"); 232 &unflush_file_lines($config{'postfix_config_file'}); # Invalidate cache 233 } 234} 235 236# check_postfix() 237# 238sub check_postfix 239{ 240 my $cmd = "$config{'postfix_control_command'} -c $config_dir check"; 241 my $out = &backquote_command("$cmd 2>&1 </dev/null", 1); 242 my $ex = $?; 243 if ($ex && &foreign_check("proc")) { 244 # Get a better error message 245 &foreign_require("proc", "proc-lib.pl"); 246 $out = &proc::pty_backquote("$cmd 2>&1 </dev/null"); 247 } 248 return $ex ? ($out || "$cmd failed") : undef; 249} 250 251# reload_postfix() 252# 253sub reload_postfix 254{ 255 if (is_postfix_running()) 256 { 257 if (check_postfix()) { 258 return $text{'check_error'}; 259 } 260 my $cmd; 261 if (!$config{'reload_cmd'}) { 262 $cmd = "$config{'postfix_control_command'} -c $config_dir ". 263 "reload"; 264 } 265 else { 266 $cmd = $config{'reload_cmd'}; 267 } 268 my $ex = &system_logged("$cmd >/dev/null 2>&1"); 269 return $ex ? ($out || "$cmd failed") : undef; 270 } 271 return undef; 272} 273 274# get_bootup_action() 275# Returns the name of the init script to start and stop Postfix, if found. 276sub get_bootup_action 277{ 278return undef if (!&foreign_check("init")); 279&foreign_require("init"); 280my $name = $config{'init_name'} || 'postfix'; 281my $st = &init::action_status($name); 282return $st == 0 ? undef : $name; 283} 284 285# stop_postfix() 286# Attempts to stop postfix, returning undef on success or an error message 287sub stop_postfix 288{ 289my ($ok, $out, $init); 290if ($config{'stop_cmd'}) { 291 # Use the user-configured stop command 292 $out = &backquote_logged("$config{'stop_cmd'} 2>&1"); 293 $ok = !$?; 294 } 295else { 296 # Run the init script if there is one, and also the control command in 297 # case this is a systemd server and it assumes Postfix isn't running 298 if ($init = &get_bootup_action()) { 299 ($ok, $out) = &init::stop_action($init); 300 } 301 if (&is_postfix_running()) { 302 $out = &backquote_logged("$config{'postfix_control_command'} -c $config_dir stop 2>&1"); 303 $ok = !$?; 304 } 305 } 306return $ok ? undef : "<tt>".&html_escape($out)."</tt>"; 307} 308 309# start_postfix() 310# Attempts to start postfix, returning undef on success or an error message 311sub start_postfix 312{ 313my ($ok, $out, $init); 314if ($config{'start_cmd'}) { 315 # Use the user-configured start command 316 $out = &backquote_logged("$config{'start_cmd'} 2>&1"); 317 $ok = !$?; 318 } 319elsif ($init = &get_bootup_action()) { 320 # Run the init script if there is one 321 ($ok, $out) = &init::start_action($init); 322 $ok = !$?; 323 } 324else { 325 # Fall back to the Postfix control command 326 $out = &backquote_logged("$config{'postfix_control_command'} -c $config_dir start 2>&1"); 327 } 328return $ok ? undef : "<tt>".&html_escape($out)."</tt>"; 329} 330 331# option_radios_freefield(name_of_option, length_of_free_field, [name_of_radiobutton, text_of_radiobutton]+) 332# builds an option with variable number of radiobuttons and a free field 333# WARNING: *FIRST* RADIO BUTTON *MUST* BE THE DEFAULT VALUE OF POSTFIX 334sub option_radios_freefield 335{ 336 my ($name, $length) = ($_[0], $_[1]); 337 338 my $v = &get_current_value($name); 339 my $key = 'opts_'.$name; 340 341 my $check_free_field = 1; 342 343 my $help = -r &help_file($module_name, "opt_".$name) ? 344 &hlink($text{$key}, "opt_".$name) : $text{$key}; 345 my $rv; 346 347 # first radio button (must be default value!!) 348 $rv .= &ui_oneradio($name."_def", "__DEFAULT_VALUE_IE_NOT_IN_CONFIG_FILE__", 349 $_[2], &if_default_value($name)); 350 351 $check_free_field = 0 if &if_default_value($name); 352 shift; 353 354 # other radio buttons 355 while (defined($_[2])) 356 { 357 $rv .= &ui_oneradio($name."_def", $_[2], $_[3], $v eq $_[2]); 358 if ($v eq $_[2]) { $check_free_field = 0; } 359 shift; 360 shift; 361 } 362 363 # the free field 364 $rv .= &ui_oneradio($name."_def", "__USE_FREE_FIELD__", undef, 365 $check_free_field == 1); 366 $rv .= &ui_textbox($name, $check_free_field == 1 ? $v : undef, $length); 367 print &ui_table_row($help, $rv, $length > 20 ? 3 : 1); 368} 369 370# option_mapfield(name_of_option, length_of_free_field) 371# Prints a field for selecting a map, or none 372sub option_mapfield 373{ 374 my ($name, $length) = ($_[0], $_[1]); 375 376 my $v = &get_current_value($name); 377 my $key = 'opts_'.$name; 378 379 my $check_free_field = 1; 380 381 my $help = -r &help_file($module_name, "opt_".$name) ? 382 &hlink($text{$key}, "opt_".$name) : $text{$key}; 383 my $rv; 384 $rv .= &ui_oneradio($name."_def", "__DEFAULT_VALUE_IE_NOT_IN_CONFIG_FILE__", 385 $text{'opts_nomap'}, &if_default_value($name)); 386 $rv .= "<br>\n"; 387 388 $check_free_field = 0 if &if_default_value($name); 389 shift; 390 391 # the free field 392 $rv .= &ui_oneradio($name."_def", "__USE_FREE_FIELD__", 393 $text{'opts_setmap'}, $check_free_field == 1); 394 $rv .= &ui_textbox($name, $check_free_field == 1 ? $v : undef, $length); 395 $rv .= &map_chooser_button($name, $name); 396 print &ui_table_row($help, $rv, $length > 20 ? 3 : 1); 397} 398 399 400 401# option_freefield(name_of_option, length_of_free_field) 402# builds an option with free field 403sub option_freefield 404{ 405 my ($name, $length) = ($_[0], $_[1]); 406 407 my $v = &get_current_value($name); 408 my $key = 'opts_'.$name; 409 410 print &ui_table_row(&hlink($text{$key}, "opt_".$name), 411 &ui_textbox($name."_def", $v, $length), 412 $length > 20 ? 3 : 1); 413} 414 415 416# option_yesno(name_of_option, [help]) 417# if help is provided, displays help link 418sub option_yesno 419{ 420 my $name = $_[0]; 421 my $v = &get_current_value($name); 422 my $key = 'opts_'.$name; 423 424 print &ui_table_row(defined($_[1]) ? &hlink($text{$key}, "opt_".$name) 425 : $text{$key}, 426 &ui_radio($name."_def", lc($v), 427 [ [ "yes", $text{'yes'} ], 428 [ "no", $text{'no'} ] ])); 429} 430 431# option_select(name_of_option, &options, [help]) 432# Shows a drop-down menu of options 433sub option_select 434{ 435 my $name = $_[0]; 436 my $v = &get_current_value($name); 437 my $key = 'opts_'.$name; 438 439 print &ui_table_row(defined($_[2]) ? &hlink($text{$key}, "opt_".$name) 440 : $text{$key}, 441 &ui_select($name."_def", lc($v), $_[1])); 442} 443 444 445 446############################################################################ 447# aliases support [too lazy to create a aliases-lib.pl :-)] 448 449# get_aliases_files($alias_maps) : @aliases_files 450# parses its argument to extract the filenames of the aliases files 451# supports multiple alias-files 452sub get_aliases_files 453{ 454 return map { $_->[1] } 455 grep { &file_map_type($_->[0]) } &get_maps_types_files($_[0]); 456} 457 458# init_new_alias() : $number 459# gives a new number of alias 460sub init_new_alias 461{ 462 $aliases = &get_aliases(); 463 464 my $max_number = 0; 465 466 foreach $trans (@{$aliases}) 467 { 468 if ($trans->{'number'} > $max_number) { $max_number = $trans->{'number'}; } 469 } 470 471 return $max_number+1; 472} 473 474# list_postfix_aliases() 475# Returns a list of all aliases. These typically come from a file, but may also 476# be taken from a MySQL or LDAP backend 477sub list_postfix_aliases 478{ 479local @rv; 480foreach my $f (&get_maps_types_files(&get_current_value("alias_maps"))) { 481 if (&file_map_type($f->[0])) { 482 # We can read this file directly 483 local $sofar = scalar(@rv); 484 foreach my $a (&list_aliases([ $f->[1] ])) { 485 $a->{'num'} += $sofar; 486 push(@rv, $a); 487 } 488 } 489 else { 490 # Treat as a map 491 push(@maps, "$f->[0]:$f->[1]"); 492 } 493 } 494if (@maps) { 495 # Convert values from MySQL and LDAP maps into alias structures 496 local $maps = &get_maps("alias_maps", undef, join(",", @maps)); 497 foreach my $m (@$maps) { 498 local $v = $m->{'value'}; 499 local @values; 500 while($v =~ /^\s*,?\s*()"([^"]+)"(.*)$/ || 501 $v =~ /^\s*,?\s*(\|)"([^"]+)"(.*)$/ || 502 $v =~ /^\s*,?\s*()([^,\s]+)(.*)$/) { 503 push(@values, $1.$2); 504 $v = $3; 505 } 506 if ($m->{'name'} =~ /^#(.*)$/) { 507 $m->{'enabled'} = 0; 508 $m->{'name'} = $1; 509 } 510 else { 511 $m->{'enabled'} = 1; 512 } 513 $m->{'values'} = \@values; 514 $m->{'num'} = scalar(@rv); 515 push(@rv, $m); 516 } 517 } 518return @rv; 519} 520 521# create_postfix_alias(&alias) 522# Adds a new alias, either to the local file or another backend 523sub create_postfix_alias 524{ 525local ($alias) = @_; 526local @afiles = &get_maps_types_files(&get_current_value("alias_maps")); 527local $last_type = $afiles[$#afiles]->[0]; 528local $last_file = $afiles[$#afiles]->[1]; 529if (&file_map_type($last_type)) { 530 # Just adding to a file 531 &create_alias($alias, [ $last_file ], 1); 532 } 533else { 534 # Add to appropriate backend map 535 if (!$alias->{'enabled'}) { 536 $alias->{'name'} = '#'.$alias->{'name'}; 537 } 538 $alias->{'value'} = join(',', map { /\s/ ? "\"$_\"" : $_ } 539 @{$alias->{'values'}}); 540 &create_mapping("alias_maps", $alias, undef, "$last_type:$last_file"); 541 } 542} 543 544# delete_postfix_alias(&alias) 545# Delete an alias, either from the files or from a MySQL or LDAP map 546sub delete_postfix_alias 547{ 548local ($alias) = @_; 549if ($alias->{'map_type'}) { 550 # This was from a map 551 &delete_mapping("alias_maps", $alias); 552 } 553else { 554 # Regular alias 555 &delete_alias($alias, 1); 556 } 557} 558 559# modify_postfix_alias(&oldalias, &alias) 560# Update an alias, either in a file or in a map 561sub modify_postfix_alias 562{ 563local ($oldalias, $alias) = @_; 564if ($oldalias->{'map_type'}) { 565 # In the map 566 if (!$alias->{'enabled'}) { 567 $alias->{'name'} = '#'.$alias->{'name'}; 568 } 569 $alias->{'value'} = join(',', map { /\s/ ? "\"$_\"" : $_ } 570 @{$alias->{'values'}}); 571 &modify_mapping("alias_maps", $oldalias, $alias); 572 } 573else { 574 # Regular alias in a file 575 &modify_alias($oldalias, $alias); 576 } 577} 578 579# renumber_list(&list, &position-object, lines-offset) 580sub renumber_list 581{ 582return if (!$_[2]); 583local $e; 584foreach $e (@{$_[0]}) { 585 next if (defined($e->{'alias_file'}) && 586 $e->{'alias_file'} ne $_[1]->{'alias_file'}); 587 next if (defined($e->{'map_file'}) && 588 $e->{'map_file'} ne $_[1]->{'map_file'}); 589 $e->{'line'} += $_[2] if ($e->{'line'} > $_[1]->{'line'}); 590 $e->{'eline'} += $_[2] if (defined($e->{'eline'}) && 591 $e->{'eline'} > $_[1]->{'eline'}); 592 } 593} 594 595# save_options(%options, [&always-save]) 596# 597sub save_options 598{ 599 if (check_postfix()) { &error("$text{'check_error'}"); } 600 601 my %options = %{$_[0]}; 602 603 foreach $key (keys %options) 604 { 605 if ($key =~ /_def$/) 606 { 607 (my $param = $key) =~ s/_def$//; 608 my $value = $options{$key} eq "__USE_FREE_FIELD__" ? 609 $options{$param} : $options{$key}; 610 $value =~ s/\0/, /g; 611 if ($value =~ /(\S+):(\/\S+)/ && $access{'dir'} ne '/') { 612 foreach my $f (&get_maps_files("$1:$2")) { 613 if (!&is_under_directory($access{'dir'}, $f)) { 614 &error(&text('opts_edir', $access{'dir'})); 615 } 616 } 617 } 618 &set_current_value($param, $value, 619 &indexof($param, @{$_[1]}) >= 0); 620 } 621 } 622} 623 624 625# regenerate_aliases 626# 627sub regenerate_aliases 628{ 629 local $out; 630 $access{'aliases'} || error($text{'regenerate_ecannot'}); 631 if (get_current_value("alias_maps") eq "") 632 { 633 $out = &backquote_logged("$config{'postfix_newaliases_command'} 2>&1"); 634 if ($?) { &error(&text('regenerate_alias_efailed', $out)); } 635 } 636 else 637 { 638 local $map; 639 foreach $map (get_maps_types_files(get_real_value("alias_maps"))) 640 { 641 if (&file_map_type($map->[0])) { 642 my $cmd = $config{'postfix_aliases_table_command'}; 643 if ($cmd =~ /newaliases/) { 644 $cmd .= " -oA$map->[1]"; 645 } else { 646 $cmd .= " $map->[1]"; 647 } 648 $out = &backquote_logged("$cmd 2>&1"); 649 if ($?) { &error(&text('regenerate_table_efailed', $map->[1], $out)); } 650 } 651 } 652 } 653} 654 655 656# regenerate_relocated_table() 657sub regenerate_relocated_table 658{ 659 ®enerate_any_table("relocated_maps"); 660} 661 662 663# regenerate_virtual_table() 664sub regenerate_virtual_table 665{ 666 ®enerate_any_table($virtual_maps); 667} 668 669# regenerate_bcc_table() 670sub regenerate_bcc_table 671{ 672 ®enerate_any_table("sender_bcc_maps"); 673} 674 675sub regenerate_relay_recipient_table 676{ 677 ®enerate_any_table("relay_recipient_maps"); 678} 679 680sub regenerate_sender_restrictions_table 681{ 682 ®enerate_any_table("smtpd_sender_restrictions"); 683} 684 685# regenerate_recipient_bcc_table() 686sub regenerate_recipient_bcc_table 687{ 688 ®enerate_any_table("recipient_bcc_maps"); 689} 690 691# regenerate_header_table() 692sub regenerate_header_table 693{ 694 ®enerate_any_table("header_checks"); 695} 696 697# regenerate_body_table() 698sub regenerate_body_table 699{ 700 ®enerate_any_table("body_checks"); 701} 702 703# regenerate_canonical_table 704# 705sub regenerate_canonical_table 706{ 707 ®enerate_any_table("canonical_maps"); 708 ®enerate_any_table("recipient_canonical_maps"); 709 ®enerate_any_table("sender_canonical_maps"); 710} 711 712 713# regenerate_transport_table 714# 715sub regenerate_transport_table 716{ 717 ®enerate_any_table("transport_maps"); 718} 719 720# regenerate_sni_table 721# 722sub regenerate_sni_table 723{ 724 ®enerate_any_table("tls_server_sni_maps", undef, undef, 1); 725} 726 727# regenerate_dependent_table 728# 729sub regenerate_dependent_table 730{ 731 ®enerate_any_table("sender_dependent_default_transport_maps"); 732} 733 734 735# regenerate_any_table($parameter_where_to_find_the_table_names, 736# [ &force-files ], [ after-tag ], [ base-64 ]) 737# 738sub regenerate_any_table 739{ 740 my ($name, $force, $after, $base64) = @_; 741 my @files; 742 if ($force) { 743 @files = map { [ "hash", $_ ] } @$force; 744 } elsif (&get_current_value($name) ne "") { 745 my $value = &get_real_value($name); 746 if ($after) { 747 $value =~ s/^.*\Q$after\E\s+(\S+).*$/$1/ || return; 748 } 749 @files = &get_maps_types_files($value); 750 } 751 foreach my $map (@files) 752 { 753 next unless $map; 754 if (&file_map_type($map->[0]) && 755 $map->[0] ne 'regexp' && $map->[0] ne 'pcre') { 756 local $out = &backquote_logged( 757 $config{'postfix_lookup_table_command'}. 758 " -c $config_dir". 759 ($base64 ? " -F" : ""). 760 " $map->[0]:$map->[1] 2>&1"); 761 if ($?) { &error(&text('regenerate_table_efailed', $map->[1], $out)); } 762 } 763 } 764} 765 766 767 768############################################################################ 769# maps [canonical, virtual, transport] support 770 771# get_maps_files($maps_param) : @maps_files 772# parses its argument to extract the filenames of the mapping files 773# supports multiple maps-files 774sub get_maps_files 775{ 776 $_[0] =~ /:(\/[^,\s]*)(.*)/ || return ( ); 777 (my $returnvalue, my $recurse) = ( $1, $2 ); 778 779 return ( $returnvalue, 780 ($recurse =~ /:\/[^,\s]*/) ? 781 &get_maps_files($recurse) 782 : 783 () 784 ) 785} 786 787 788# get_maps($maps_name, [&force-files], [force-map]) : \@maps 789# Construct the mappings database taken from the map files given from the 790# parameters. 791sub get_maps 792{ 793 if (!defined($maps_cache{$_[0]})) 794 { 795 my @maps_files = $_[1] ? (map { [ "hash", $_ ] } @{$_[1]}) : 796 $_[2] ? &get_maps_types_files($_[2]) : 797 &get_maps_types_files(&get_real_value($_[0])); 798 my $number = 0; 799 $maps_cache{$_[0]} = [ ]; 800 foreach my $maps_type_file (@maps_files) 801 { 802 my ($maps_type, $maps_file) = @$maps_type_file; 803 804 if (&file_map_type($maps_type)) { 805 # Read a file on disk 806 &open_readfile(MAPS, $maps_file); 807 my $i = 0; 808 my $cmt; 809 while (<MAPS>) 810 { 811 s/\r|\n//g; # remove newlines 812 if (/^\s*#+\s*(.*)/) { 813 # A comment line 814 $cmt = &is_table_comment($_); 815 } 816 elsif (/^\s*(\/[^\/]*\/[a-z]*)\s+(.*)/ || 817 /^\s*([^\s]+)\s+(.*)/) { 818 # An actual map 819 $number++; 820 my %map; 821 $map{'name'} = $1; 822 $map{'value'} = $2; 823 $map{'line'} = $cmt ? $i-1 : $i; 824 $map{'eline'} = $i; 825 $map{'map_file'} = $maps_file; 826 $map{'map_type'} = $maps_type; 827 $map{'file'} = $maps_file; 828 $map{'number'} = $number; 829 $map{'cmt'} = $cmt; 830 push(@{$maps_cache{$_[0]}}, \%map); 831 $cmt = undef; 832 } 833 else { 834 $cmt = undef; 835 } 836 $i++; 837 } 838 close(MAPS); 839 840 } elsif ($maps_type eq "mysql") { 841 # Get from a MySQL database 842 local $conf = &mysql_value_to_conf($maps_file); 843 local $dbh = &connect_mysql_db($conf); 844 ref($dbh) || &error($dbh); 845 local $cmd = $dbh->prepare( 846 "select ".$conf->{'where_field'}. 847 ",".$conf->{'select_field'}. 848 " from ".$conf->{'table'}. 849 " where 1 = 1 ". 850 $conf->{'additional_conditions'}); 851 if (!$cmd || !$cmd->execute()) { 852 &error(&text('mysql_elist', 853 "<tt>".&html_escape($dbh->errstr)."</tt>")); 854 } 855 while(my ($k, $v) = $cmd->fetchrow()) { 856 $number++; 857 my %map; 858 $map{'name'} = $k; 859 $map{'value'} = $v; 860 $map{'key'} = $k; 861 $map{'map_file'} = $maps_file; 862 $map{'map_type'} = $maps_type; 863 $map{'number'} = $number; 864 push(@{$maps_cache{$_[0]}}, \%map); 865 } 866 $cmd->finish(); 867 $dbh->disconnect(); 868 869 } elsif ($maps_type eq "ldap") { 870 # Get from an LDAP database 871 local $conf = &ldap_value_to_conf($maps_file); 872 local $ldap = &connect_ldap_db($conf); 873 ref($ldap) || &error($ldap); 874 local ($name_attr, $filter) = &get_ldap_key($conf); 875 local $scope = $conf->{'scope'} || 'sub'; 876 local $rv = $ldap->search(base => $conf->{'search_base'}, 877 scope => $scope, 878 filter => $filter); 879 if (!$rv || $rv->code) { 880 # Search failed! 881 &error(&text('ldap_equery', 882 "<tt>$conf->{'search_base'}</tt>", 883 "<tt>".&html_escape($rv->error)."</tt>")); 884 } 885 foreach my $o ($rv->all_entries) { 886 $number++; 887 my %map; 888 $map{'name'} = $o->get_value($name_attr); 889 $map{'value'} = $o->get_value( 890 $conf->{'result_attribute'} || "maildrop"); 891 $map{'dn'} = $o->dn(); 892 $map{'map_file'} = $maps_file; 893 $map{'map_type'} = $maps_type; 894 $map{'number'} = $number; 895 push(@{$maps_cache{$_[0]}}, \%map); 896 } 897 } 898 } 899 } 900 return $maps_cache{$_[0]}; 901} 902 903 904# generate_map_edit(name, desc, [wide], [nametitle], [valuetitle]) 905# Prints a table showing map contents, with links to edit and add 906sub generate_map_edit 907{ 908 # Check if map is set 909 if (&get_current_value($_[0]) eq "") 910 { 911 print "<b>$text{'no_map2'}</b><p>\n"; 912 return; 913 } 914 915 # Make sure the user is allowed to edit them 916 foreach my $f (&get_maps_types_files(&get_real_value($_[0]))) { 917 if (&file_map_type($f->[0])) { 918 &is_under_directory($access{'dir'}, $f->[1]) || 919 &error(&text('mapping_ecannot', $access{'dir'})); 920 } 921 } 922 923 # Make sure we *can* edit them 924 foreach my $f (&get_maps_types_files(&get_real_value($_[0]))) { 925 my $err = &can_access_map(@$f); 926 if ($err) { 927 print "<b>",&text('map_cannot', $err),"</b><p>\n"; 928 return; 929 } 930 } 931 932 my $mappings = &get_maps($_[0]); 933 my $nt = $_[3] || $text{'mapping_name'}; 934 my $vt = $_[4] || $text{'mapping_value'}; 935 936 local @links = ( &ui_link("edit_mapping.cgi?map_name=$_[0]", 937 $text{'new_mapping'}),); 938 if ($access{'manual'} && &can_map_manual($_[0])) { 939 push(@links, &ui_link("edit_manual.cgi?map_name=$_[0]", 940 $text{'new_manual'})); 941 } 942 943 if ($in{'search'}) { 944 # Filter down to matching entries 945 $mappings = [ grep { $_->{'name'} =~ /\Q$in{'search'}\E/i || 946 $_->{'value'} =~ /\Q$in{'search'}\E/i } @$mappings ]; 947 print "<b>",&text('mapping_match', &html_escape($in{'search'})), 948 "</b><p>\n"; 949 } 950 951 if ($#{$mappings} == -1) { 952 # None, so just show edit link 953 print "<b>$text{'mapping_none'}</b><p>\n"; 954 print &ui_links_row(\@links); 955 } 956 elsif ($config{'max_maps'} && @{$mappings} > $config{'max_maps'} && 957 !$in{'search'}) { 958 # If there are too many, show a search form 959 print &ui_form_start($gconfig{'webprefix'}.$ENV{'SCRIPT_NAME'}); 960 foreach my $i (keys %in) { 961 next if ($i eq 'search'); 962 print &ui_hidden($i, $in{$i}); 963 } 964 print &text('mapping_toomany', scalar(@{$mappings}), 965 $config{'max_maps'}),"<p>\n"; 966 print $text{'mapping_find'}," ", 967 &ui_textbox("search", $in{'search'}, 20)," ", 968 &ui_submit($text{'mapping_search'}),"\n"; 969 print &ui_form_end(); 970 print &ui_links_row(\@links); 971 } 972 else { 973 # Map description 974 print $_[1],"<p>\n"; 975 976 # Sort the map 977 if ($config{'sort_mode'} == 1) { 978 if ($_[0] eq $virtual_maps) { 979 @{$mappings} = sort sort_by_domain @{$mappings}; 980 } 981 else { 982 @{$mappings} = sort { $a->{'name'} cmp $b->{'name'} } 983 @{$mappings}; 984 } 985 } 986 987 # Split into two columns, if needed 988 my @parts; 989 my $split_index = int(($#{$mappings})/2); 990 if ($config{'columns'} == 2) { 991 @parts = ( [ @{$mappings}[0 .. $split_index] ], 992 [ @{$mappings}[$split_index+1 .. $#{$mappings} ] ] ); 993 } 994 else { 995 @parts = ( $mappings ); 996 } 997 998 # Start of the overall form 999 print &ui_form_start("delete_mappings.cgi", "post"); 1000 print &ui_hidden("map_name", $_[0]),"\n"; 1001 unshift(@links, &select_all_link("d", 1), 1002 &select_invert_link("d", 1)); 1003 print &ui_links_row(\@links); 1004 1005 my @grid; 1006 foreach my $p (@parts) { 1007 # Build one table 1008 my @table; 1009 foreach my $map (@$p) { 1010 push(@table, [ 1011 { 'type' => 'checkbox', 'name' => 'd', 1012 'value' => $map->{'name'} }, 1013 "<a href=\"edit_mapping.cgi?num=$map->{'number'}&". 1014 "map_name=$_[0]\">".&html_escape($map->{'name'}). 1015 "</a>", 1016 &html_escape($map->{'value'}), 1017 $config{'show_cmts'} ? 1018 ( &html_escape($map->{'cmt'}) ) : ( ), 1019 ]); 1020 } 1021 1022 # Add a table to the grid 1023 push(@grid, &ui_columns_table( 1024 [ "", $nt, $vt, 1025 $config{'show_cmts'} ? ( $text{'mapping_cmt'} ) : ( ), 1026 ], 1027 100, 1028 \@table)); 1029 } 1030 if (@grid == 1) { 1031 print $grid[0]; 1032 } 1033 else { 1034 print &ui_grid_table(\@grid, 2, 100, 1035 [ "width=50%", "width=50%" ]); 1036 } 1037 1038 # Main form end 1039 print &ui_links_row(\@links); 1040 print &ui_form_end([ [ "delete", $text{'mapping_delete'} ] ]); 1041 } 1042} 1043 1044 1045# create_mapping(map, &mapping, [&force-files], [force-map]) 1046sub create_mapping 1047{ 1048&get_maps($_[0], $_[2], $_[3]); # force cache init 1049my @maps_files = $_[2] ? (map { [ "hash", $_ ] } @{$_[2]}) : 1050 $_[3] ? &get_maps_types_files($_[3]) : 1051 &get_maps_types_files(&get_real_value($_[0])); 1052 1053# If multiple maps, find a good one to add to .. avoid regexp if we can 1054my $last_map; 1055if (@maps_files == 1) { 1056 $last_map = $maps_files[0]; 1057 } 1058else { 1059 for(my $i=$#maps_files; $i>=0; $i--) { 1060 if ($maps_files[$i]->[0] ne 'regexp' && 1061 $maps_files[$i]->[0] ne 'pcre') { 1062 $last_map = $maps_files[$i]; 1063 last; 1064 } 1065 } 1066 $last_map ||= $maps_files[$#maps_files]; # Fall back to last one 1067 } 1068my ($maps_type, $maps_file) = @$last_map; 1069 1070if (&file_map_type($maps_type)) { 1071 # Adding to a regular file 1072 local $lref = &read_file_lines($maps_file); 1073 $_[1]->{'line'} = scalar(@$lref); 1074 push(@$lref, &make_table_comment($_[1]->{'cmt'})); 1075 push(@$lref, "$_[1]->{'name'}\t$_[1]->{'value'}"); 1076 $_[1]->{'eline'} = scalar(@$lref)-1; 1077 &flush_file_lines($maps_file); 1078 } 1079elsif ($maps_type eq "mysql") { 1080 # Adding to a MySQL table 1081 local $conf = &mysql_value_to_conf($maps_file); 1082 local $dbh = &connect_mysql_db($conf); 1083 ref($dbh) || &error($dbh); 1084 local $cmd = $dbh->prepare("insert into ".$conf->{'table'}." ". 1085 "(".$conf->{'where_field'}.",". 1086 $conf->{'select_field'}.") values (". 1087 "?, ?)"); 1088 if (!$cmd || !$cmd->execute($_[1]->{'name'}, $_[1]->{'value'})) { 1089 &error(&text('mysql_eadd', 1090 "<tt>".&html_escape($dbh->errstr)."</tt>")); 1091 } 1092 $cmd->finish(); 1093 $dbh->disconnect(); 1094 $_[1]->{'key'} = $_[1]->{'name'}; 1095 } 1096elsif ($maps_type eq "ldap") { 1097 # Adding to an LDAP database 1098 local $conf = &ldap_value_to_conf($maps_file); 1099 local $ldap = &connect_ldap_db($conf); 1100 ref($ldap) || &error($ldap); 1101 local @classes = split(/\s+/, $config{'ldap_class'} || 1102 "inetLocalMailRecipient"); 1103 local @attrs = ( "objectClass", \@classes ); 1104 local $name_attr = &get_ldap_key($conf); 1105 push(@attrs, $name_attr, $_[1]->{'name'}); 1106 push(@attrs, $conf->{'result_attribute'} || "maildrop", 1107 $_[1]->{'value'}); 1108 push(@attrs, &split_props($config{'ldap_attrs'})); 1109 local $dn = &make_map_ldap_dn($_[1], $conf); 1110 if ($dn =~ /^([^=]+)=([^, ]+)/ && !&in_props(\@attrs, $1)) { 1111 push(@attrs, $1, $2); 1112 } 1113 1114 # Make sure the parent DN exists - for example, when adding a domain 1115 &ensure_ldap_parent($ldap, $dn); 1116 1117 # Actually add 1118 local $rv = $ldap->add($dn, attr => \@attrs); 1119 if ($rv->code) { 1120 &error(&text('ldap_eadd', "<tt>$dn</tt>", 1121 "<tt>".&html_escape($rv->error)."</tt>")); 1122 } 1123 $_[1]->{'dn'} = $dn; 1124 } 1125 1126# Update the in-memory cache 1127$_[1]->{'map_type'} = $maps_type; 1128$_[1]->{'map_file'} = $maps_file; 1129$_[1]->{'file'} = $maps_file; 1130$_[1]->{'number'} = scalar(@{$maps_cache{$_[0]}}); 1131push(@{$maps_cache{$_[0]}}, $_[1]); 1132} 1133 1134 1135# delete_mapping(map, &mapping) 1136sub delete_mapping 1137{ 1138if (&file_map_type($_[1]->{'map_type'}) || !$_[1]->{'map_type'}) { 1139 # Deleting from a file 1140 local $lref = &read_file_lines($_[1]->{'map_file'}); 1141 local $dl = $lref->[$_[1]->{'eline'}]; 1142 local $len = $_[1]->{'eline'} - $_[1]->{'line'} + 1; 1143 if (($dl =~ /^\s*(\/[^\/]*\/[a-z]*)\s+([^#]*)/ || 1144 $dl =~ /^\s*([^\s]+)\s+([^#]*)/) && 1145 $1 eq $_[1]->{'name'}) { 1146 # Found a valid line to remove 1147 splice(@$lref, $_[1]->{'line'}, $len); 1148 } 1149 else { 1150 print STDERR "Not deleting line $_[1]->{'line'} ", 1151 "from $_[1]->{'file'} for key ", 1152 "$_[1]->{'name'} which actually contains $dl\n"; 1153 } 1154 &flush_file_lines($_[1]->{'map_file'}); 1155 &renumber_list($maps_cache{$_[0]}, $_[1], -$len); 1156 local $idx = &indexof($_[1], @{$maps_cache{$_[0]}}); 1157 if ($idx >= 0) { 1158 # Take out of cache 1159 splice(@{$maps_cache{$_[0]}}, $idx, 1); 1160 } 1161 } 1162elsif ($_[1]->{'map_type'} eq 'mysql') { 1163 # Deleting from MySQL 1164 local $conf = &mysql_value_to_conf($_[1]->{'map_file'}); 1165 local $dbh = &connect_mysql_db($conf); 1166 ref($dbh) || &error($dbh); 1167 local $cmd = $dbh->prepare("delete from ".$conf->{'table'}. 1168 " where ".$conf->{'where_field'}." = ?". 1169 " ".$conf->{'additional_conditions'}); 1170 if (!$cmd || !$cmd->execute($_[1]->{'key'})) { 1171 &error(&text('mysql_edelete', 1172 "<tt>".&html_escape($dbh->errstr)."</tt>")); 1173 } 1174 $cmd->finish(); 1175 $dbh->disconnect(); 1176 } 1177elsif ($_[1]->{'map_type'} eq 'ldap') { 1178 # Deleting from LDAP 1179 local $conf = &ldap_value_to_conf($_[1]->{'map_file'}); 1180 local $ldap = &connect_ldap_db($conf); 1181 ref($ldap) || &error($ldap); 1182 local $rv = $ldap->delete($_[1]->{'dn'}); 1183 if ($rv->code) { 1184 &error(&text('ldap_edelete', "<tt>$_[1]->{'dn'}</tt>", 1185 "<tt>".&html_escape($rv->error)."</tt>")); 1186 } 1187 } 1188 1189# Delete from in-memory cache 1190local $idx = &indexof($_[1], @{$maps_cache{$_[0]}}); 1191splice(@{$maps_cache{$_[0]}}, $idx, 1) if ($idx != -1); 1192} 1193 1194 1195# modify_mapping(map, &oldmapping, &newmapping) 1196sub modify_mapping 1197{ 1198if (&file_map_type($_[1]->{'map_type'}) || !$_[1]->{'map_type'}) { 1199 # Modifying in a file 1200 local $lref = &read_file_lines($_[1]->{'map_file'}); 1201 local $oldlen = $_[1]->{'eline'} - $_[1]->{'line'} + 1; 1202 local @newlines; 1203 push(@newlines, &make_table_comment($_[2]->{'cmt'})); 1204 push(@newlines, "$_[2]->{'name'}\t$_[2]->{'value'}"); 1205 splice(@$lref, $_[1]->{'line'}, $oldlen, @newlines); 1206 &flush_file_lines($_[1]->{'map_file'}); 1207 &renumber_list($maps_cache{$_[0]}, $_[1], scalar(@newlines)-$oldlen); 1208 local $idx = &indexof($_[1], @{$maps_cache{$_[0]}}); 1209 if ($idx >= 0) { 1210 # Update in cache 1211 $_[2]->{'map_file'} = $_[1]->{'map_file'}; 1212 $_[2]->{'map_type'} = $_[1]->{'map_type'}; 1213 $_[2]->{'line'} = $_[1]->{'line'}; 1214 $_[2]->{'eline'} = $_[1]->{'eline'}; 1215 $maps_cache{$_[0]}->[$idx] = $_[2]; 1216 } 1217 } 1218elsif ($_[1]->{'map_type'} eq 'mysql') { 1219 # Updating in MySQL 1220 local $conf = &mysql_value_to_conf($_[1]->{'map_file'}); 1221 local $dbh = &connect_mysql_db($conf); 1222 ref($dbh) || &error($dbh); 1223 local $cmd = $dbh->prepare("update ".$conf->{'table'}. 1224 " set ".$conf->{'where_field'}." = ?,". 1225 " ".$conf->{'select_field'}." = ?". 1226 " where ".$conf->{'where_field'}." = ?". 1227 " ".$conf->{'additional_conditions'}); 1228 if (!$cmd || !$cmd->execute($_[2]->{'name'}, $_[2]->{'value'}, 1229 $_[1]->{'key'})) { 1230 &error(&text('mysql_eupdate', 1231 "<tt>".&html_escape($dbh->errstr)."</tt>")); 1232 } 1233 $cmd->finish(); 1234 $dbh->disconnect(); 1235 } 1236elsif ($_[1]->{'map_type'} eq 'ldap') { 1237 # Updating in LDAP 1238 local $conf = &ldap_value_to_conf($_[1]->{'map_file'}); 1239 local $ldap = &connect_ldap_db($conf); 1240 ref($ldap) || &error($ldap); 1241 1242 # Work out attribute changes 1243 local %replace; 1244 local $name_attr = &get_ldap_key($conf); 1245 $replace{$name_attr} = [ $_[2]->{'name'} ]; 1246 $replace{$conf->{'result_attribute'} || "maildrop"} = 1247 [ $_[2]->{'value'} ]; 1248 1249 # Work out new DN, if needed 1250 local $newdn = &make_map_ldap_dn($_[2], $conf); 1251 if ($_[1]->{'name'} ne $_[2]->{'name'} && 1252 $_[1]->{'dn'} ne $newdn) { 1253 # Changed .. update the object in LDAP 1254 &ensure_ldap_parent($ldap, $newdn); 1255 local ($newprefix, $newrest) = split(/,/, $newdn, 2); 1256 local $rv = $ldap->moddn($_[1]->{'dn'}, 1257 newrdn => $newprefix, 1258 newsuperior => $newrest); 1259 if ($rv->code) { 1260 &error(&text('ldap_erename', 1261 "<tt>$_[1]->{'dn'}</tt>", 1262 "<tt>$newdn</tt>", 1263 "<tt>".&html_escape($rv->error)."</tt>")); 1264 } 1265 $_[2]->{'dn'} = $newdn; 1266 if ($newdn =~ /^([^=]+)=([^, ]+)/) { 1267 $replace{$1} = [ $2 ]; 1268 } 1269 } 1270 else { 1271 $_[2]->{'dn'} = $_[1]->{'dn'}; 1272 } 1273 1274 # Modify attributes 1275 local $rv = $ldap->modify($_[2]->{'dn'}, replace => \%replace); 1276 if ($rv->code) { 1277 &error(&text('ldap_emodify', 1278 "<tt>$_[2]->{'dn'}</tt>", 1279 "<tt>".&html_escape($rv->error)."</tt>")); 1280 } 1281 } 1282 1283# Update in-memory cache 1284local $idx = &indexof($_[1], @{$maps_cache{$_[0]}}); 1285$_[2]->{'map_file'} = $_[1]->{'map_file'}; 1286$_[2]->{'map_type'} = $_[1]->{'map_type'}; 1287$_[2]->{'file'} = $_[1]->{'file'}; 1288$_[2]->{'line'} = $_[1]->{'line'}; 1289$_[2]->{'eline'} = $_[2]->{'cmt'} ? $_[1]->{'line'}+1 : $_[1]->{'line'}; 1290$maps_cache{$_[0]}->[$idx] = $_[2] if ($idx != -1); 1291} 1292 1293# make_map_ldap_dn(&map, &conf) 1294# Work out an LDAP DN for a map 1295sub make_map_ldap_dn 1296{ 1297local ($map, $conf) = @_; 1298local $dn; 1299local $scope = $conf->{'scope'} || 'sub'; 1300$scope = 'base' if (!$config{'ldap_doms'}); # Never create sub-domains 1301local $id = $config{'ldap_id'} || 'cn'; 1302if ($map->{'name'} =~ /^(\S+)\@(\S+)$/ && $scope ne 'base') { 1303 # Within a domain 1304 $dn = "$id=$1,cn=$2,$conf->{'search_base'}"; 1305 } 1306elsif ($map->{'name'} =~ /^\@(\S+)$/ && $scope ne 'base') { 1307 # Domain catchall 1308 $dn = "$id=default,cn=$1,$conf->{'search_base'}"; 1309 } 1310else { 1311 # Some other string 1312 $dn = "$id=$map->{'name'},$conf->{'search_base'}"; 1313 } 1314return $dn; 1315} 1316 1317# get_ldap_key(&config) 1318# Returns the attribute name for the LDAP key. May call &error 1319sub get_ldap_key 1320{ 1321local ($conf) = @_; 1322local ($filter, $name_attr) = @_; 1323if ($conf->{'query_filter'}) { 1324 $filter = $conf->{'query_filter'}; 1325 $conf->{'query_filter'} =~ /([a-z0-9]+)=\%[su]/i || 1326 &error("Could not get attribute from ". 1327 $conf->{'query_filter'}); 1328 $name_attr = $1; 1329 $filter = "($filter)" if ($filter !~ /^\(/); 1330 $filter =~ s/\%s/\*/g; 1331 } 1332else { 1333 $filter = "(mailacceptinggeneralid=*)"; 1334 $name_attr = "mailacceptinggeneralid"; 1335 } 1336return wantarray ? ( $name_attr, $filter ) : $name_attr; 1337} 1338 1339# ensure_ldap_parent(&ldap, dn) 1340# Create the parent of some DN if needed 1341sub ensure_ldap_parent 1342{ 1343local ($ldap, $dn) = @_; 1344local $pdn = $dn; 1345$pdn =~ s/^([^,]+),//; 1346local $rv = $ldap->search(base => $pdn, scope => 'base', 1347 filter => "(objectClass=top)", 1348 sizelimit => 1); 1349if (!$rv || $rv->code || !$rv->all_entries) { 1350 # Does not .. so add it 1351 local @pclasses = ( "top" ); 1352 local @pattrs = ( "objectClass", \@pclasses ); 1353 local $rv = $ldap->add($pdn, attr => \@pattrs); 1354 } 1355} 1356 1357# init_new_mapping($maps_parameter) : $number 1358# gives a new number of mapping 1359sub init_new_mapping 1360{ 1361my $maps = &get_maps($_[0]); 1362my $max_number = 0; 1363foreach $trans (@{$maps}) { 1364 if ($trans->{'number'} > $max_number) { 1365 $max_number = $trans->{'number'}; 1366 } 1367 } 1368return $max_number+1; 1369} 1370 1371# postfix_mail_file(user|user-details-list) 1372sub postfix_mail_file 1373{ 1374local @s = &postfix_mail_system(); 1375if ($s[0] == 0) { 1376 return "$s[1]/$_[0]"; 1377 } 1378elsif (@_ > 1) { 1379 return "$_[7]/$s[1]"; 1380 } 1381else { 1382 local @u = getpwnam($_[0]); 1383 return "$u[7]/$s[1]"; 1384 } 1385} 1386 1387# postfix_mail_system() 1388# Returns 0 and the spool dir for sendmail style, 1389# 1 and the mbox filename for ~/Mailbox style 1390# 2 and the maildir name for ~/Maildir style 1391sub postfix_mail_system 1392{ 1393if (!scalar(@mail_system_cache)) { 1394 local $home_mailbox = &get_current_value("home_mailbox"); 1395 if ($home_mailbox) { 1396 @mail_system_cache = $home_mailbox =~ /^(.*)\/$/ ? 1397 (2, $1) : (1, $home_mailbox); 1398 } 1399 else { 1400 local $mail_spool_directory = 1401 &get_current_value("mail_spool_directory"); 1402 @mail_system_cache = (0, $mail_spool_directory); 1403 } 1404 } 1405return wantarray ? @mail_system_cache : $mail_system_cache[0]; 1406} 1407 1408# list_queue([error-on-failure]) 1409# Returns a list of strutures, each containing details of one queued message 1410sub list_queue 1411{ 1412local ($throw) = @_; 1413local @qfiles; 1414local $out = &backquote_command("$config{'mailq_cmd'} 2>&1 </dev/null"); 1415&error("$config{'mailq_cmd'} failed : ".&html_escape($out)) if ($? && $throw); 1416foreach my $l (split(/\r?\n/, $out)) { 1417 next if ($l =~ /^(\S+)\s+is\s+empty/i || 1418 $l =~ /^\s+Total\s+requests:/i); 1419 if ($l =~ /^([^\s\*\!]+)[\*\!]?\s*(\d+)\s+(\S+\s+\S+\s+\d+\s+\d+:\d+:\d+)\s+(.*)/) { 1420 local $q = { 'id' => $1, 'size' => $2, 1421 'date' => $3, 'from' => $4 }; 1422 if (defined(&parse_mail_date)) { 1423 local $t = &parse_mail_date($q->{'date'}); 1424 if ($t) { 1425 $q->{'date'} = &make_date($t, 0, 'yyyy/mm/dd'); 1426 $q->{'time'} = $t; 1427 } 1428 } 1429 push(@qfiles, $q); 1430 } 1431 elsif ($l =~ /\((.*)\)/ && @qfiles) { 1432 $qfiles[$#qfiles]->{'status'} = $1; 1433 } 1434 elsif ($l =~ /^\s+(\S+)/ && @qfiles) { 1435 $qfiles[$#qfiles]->{'to'} .= "$1 "; 1436 } 1437 } 1438return @qfiles; 1439} 1440 1441# parse_queue_file(id) 1442# Parses a postfix mail queue file into a standard mail structure 1443sub parse_queue_file 1444{ 1445local @qfiles = ( &recurse_files("$config{'mailq_dir'}/active"), 1446 &recurse_files("$config{'mailq_dir'}/incoming"), 1447 &recurse_files("$config{'mailq_dir'}/deferred"), 1448 &recurse_files("$config{'mailq_dir'}/corrupt"), 1449 &recurse_files("$config{'mailq_dir'}/hold"), 1450 &recurse_files("$config{'mailq_dir'}/maildrop"), 1451 ); 1452local $f = $_[0]; 1453local ($file) = grep { $_ =~ /\/$f$/ } @qfiles; 1454return undef if (!$file); 1455local $mode = 0; 1456local ($mail, @headers); 1457&open_execute_command(QUEUE, "$config{'postcat_cmd'} ".quotemeta($file), 1, 1); 1458while(<QUEUE>) { 1459 if (/^\*\*\*\s+MESSAGE\s+CONTENTS/ && !$mode) { # Start of headers 1460 $mode = 1; 1461 } 1462 elsif (/^\*\*\*\s+HEADER\s+EXTRACTED/ && $mode) { # End of email 1463 last; 1464 } 1465 elsif ($mode == 1 && /^\s*$/) { # End of headers 1466 $mode = 2; 1467 } 1468 elsif ($mode == 1 && /^(\S+):\s*(.*)/) { # Found a header 1469 push(@headers, [ $1, $2 ]); 1470 } 1471 elsif ($mode == 1 && /^(\s+.*)/) { # Header continuation 1472 $headers[$#headers]->[1] .= $1 unless($#headers < 0); 1473 } 1474 elsif ($mode == 2) { # Part of body 1475 $mail->{'size'} += length($_); 1476 $mail->{'body'} .= $_; 1477 } 1478 } 1479close(QUEUE); 1480$mail->{'headers'} = \@headers; 1481foreach $h (@headers) { 1482 $mail->{'header'}->{lc($h->[0])} = $h->[1]; 1483 } 1484return $mail; 1485} 1486 1487# recurse_files(dir) 1488sub recurse_files 1489{ 1490opendir(DIR, &translate_filename($_[0])) || return ( $_[0] ); 1491local @dir = readdir(DIR); 1492closedir(DIR); 1493local ($f, @rv); 1494foreach $f (@dir) { 1495 push(@rv, &recurse_files("$_[0]/$f")) if ($f !~ /^\./); 1496 } 1497return @rv; 1498} 1499 1500sub sort_by_domain 1501{ 1502local ($a1, $a2, $b1, $b2); 1503if ($a->{'name'} =~ /^(.*)\@(.*)$/ && (($a1, $a2) = ($1, $2)) && 1504 $b->{'name'} =~ /^(.*)\@(.*)$/ && (($b1, $b2) = ($1, $2))) { 1505 return $a2 cmp $b2 ? $a2 cmp $b2 : $a1 cmp $b1; 1506 } 1507else { 1508 return $a->{'name'} cmp $b->{'name'}; 1509 } 1510} 1511 1512# before_save() 1513# Copy the postfix config file to a backup file, for reversion if 1514# a post-save check fails 1515sub before_save 1516{ 1517if ($config{'check_config'} && !defined($save_file)) { 1518 $save_file = &transname(); 1519 &execute_command("cp $config{'postfix_config_file'} $save_file"); 1520 } 1521} 1522 1523sub after_save 1524{ 1525if (defined($save_file)) { 1526 local $err = &check_postfix(); 1527 if ($err) { 1528 &execute_command("mv $save_file $config{'postfix_config_file'}"); 1529 &error(&text('after_err', "<pre>$err</pre>")); 1530 } 1531 else { 1532 unlink($save_file); 1533 $save_file = undef; 1534 } 1535 } 1536} 1537 1538# get_real_value(parameter_name) 1539# Returns the value of a parameter, with $ substitions done 1540sub get_real_value 1541{ 1542my ($name) = @_; 1543my $v = &get_current_value($name); 1544if ($postfix_version >= 2.1 && $v =~ /\$/) { 1545 # Try to use the built-in command to expand the param 1546 my $out = &backquote_command("$config{'postfix_config_command'} -c $config_dir -x -h ". 1547 quotemeta($name)." 2>/dev/null", 1); 1548 if (!$? && $out !~ /warning:.*unknown\s+parameter/) { 1549 chop($out); 1550 return $out; 1551 } 1552 } 1553$v =~ s/\$(\{([^\}]+)\}|([A-Za-z0-9\.\-\_]+))/get_real_value($2 || $3)/ge; 1554return $v; 1555} 1556 1557# ensure_map(name) 1558# Create some map text file, if needed 1559sub ensure_map 1560{ 1561foreach my $mf (&get_maps_files(&get_real_value($_[0]))) { 1562 if ($mf =~ /^\// && !-e $mf) { 1563 &open_lock_tempfile(TOUCH, ">$mf", 1) || 1564 &error(&text("efilewrite", $mf, $!)); 1565 &close_tempfile(TOUCH); 1566 &set_ownership_permissions(undef, undef, 0755, $mf); 1567 } 1568 } 1569} 1570 1571# Functions for editing the header_checks map nicely 1572sub edit_name_header_checks 1573{ 1574return &ui_table_row($text{'header_name'}, 1575 &ui_textbox("name", $_[0]->{'name'}, 60)); 1576} 1577 1578sub parse_name_header_checks 1579{ 1580$_[1]->{'name'} =~ /^\/.*\S.*\/[a-z]*$/ || &error($text{'header_ename'}); 1581return $_[1]->{'name'}; 1582} 1583 1584sub edit_value_header_checks 1585{ 1586local ($act, $dest) = split(/\s+/, $_[0]->{'value'}, 2); 1587return &ui_table_row($text{'header_value'}, 1588 &ui_select("action", $act, 1589 [ map { [ $_, $text{'header_'.lc($_)} ] } 1590 @header_checks_actions ], 0, 0, $act)."\n". 1591 &ui_textbox("value", $dest, 40)); 1592} 1593 1594sub parse_value_header_checks 1595{ 1596local $rv = $_[1]->{'action'}; 1597if ($_[1]->{'value'}) { 1598 $rv .= " ".$_[1]->{'value'}; 1599 } 1600return $rv; 1601} 1602 1603# Functions for editing the body_checks map (same as header_checks) 1604sub edit_name_body_checks 1605{ 1606return &edit_name_header_checks(@_); 1607} 1608 1609sub parse_name_body_checks 1610{ 1611return &parse_name_header_checks(@_); 1612} 1613 1614sub edit_value_body_checks 1615{ 1616return &edit_value_header_checks(@_); 1617} 1618 1619sub parse_value_body_checks 1620{ 1621return &parse_value_header_checks(@_); 1622} 1623 1624## added function for sender_access_maps 1625## added function for client_access_maps 1626sub edit_name_check_sender_access 1627{ 1628return "<td><b>$text{'access_addresses'}</b></td>\n". 1629 "<td>".&ui_textbox("name", $_[0]->{'name'},40)."</td>\n"; 1630} 1631 1632sub edit_value_check_sender_access 1633{ 1634local ($act, $dest) = split(/\s+/, $_[0]->{'value'}, 2); 1635return "<td><b>$text{'header_value'}</b></td>\n". 1636 "<td>".&ui_select("action", $act, 1637 [ map { [ $_, $text{'header_'.lc($_)} ] } 1638 @check_sender_actions ], 0, 0, $act)."\n". 1639 &ui_textbox("value", $dest, 40)."</td>\n"; 1640} 1641 1642sub parse_value_check_sender_access 1643{ 1644return &parse_value_header_checks(@_); 1645} 1646 1647@header_checks_actions = ( "REJECT", "HOLD", "REDIRECT", "DUNNO", "IGNORE", 1648 "DISCARD", "FILTER", 1649 "PREPEND", "REPLACE", "WARN" ); 1650 1651@check_sender_actions = ( "OK", "REJECT", "DISCARD", "FILTER", "PREPEND", 1652 "REDIRECT", "WARN", "DUNNO" ); 1653 1654# get_master_config() 1655# Returns an array reference of entries from the Postfix master.cf file 1656sub get_master_config 1657{ 1658if (!scalar(@master_config_cache)) { 1659 @master_config_cache = ( ); 1660 local $lnum = 0; 1661 local $prog; 1662 open(MASTER, "<".$config{'postfix_master'}); 1663 while(<MASTER>) { 1664 s/\r|\n//g; 1665 if (/^(#?)\s*(\S+)\s+(inet|unix|fifo)\s+(y|n|\-)\s+(y|n|\-)\s+(y|n|\-)\s+(\S+)\s+(\S+)\s+(.*)$/) { 1666 # A program line 1667 $prog = { 'enabled' => !$1, 1668 'name' => $2, 1669 'type' => $3, 1670 'private' => $4, 1671 'unpriv' => $5, 1672 'chroot' => $6, 1673 'wakeup' => $7, 1674 'maxprocs' => $8, 1675 'command' => $9, 1676 'line' => $lnum, 1677 'eline' => $lnum, 1678 }; 1679 push(@master_config_cache, $prog); 1680 } 1681 elsif (/^(#?)\s+(.*)$/ && $prog && 1682 $prog->{'eline'} == $lnum-1 && 1683 $prog->{'enabled'} == !$1) { 1684 # Continuation line 1685 $prog->{'command'} .= " ".$2; 1686 $prog->{'eline'} = $lnum; 1687 } 1688 $lnum++; 1689 } 1690 close(MASTER); 1691 } 1692return \@master_config_cache; 1693} 1694 1695# create_master(&master) 1696# Adds a new Postfix server process 1697sub create_master 1698{ 1699local ($master) = @_; 1700local $conf = &get_master_config(); 1701local $lref = &read_file_lines($config{'postfix_master'}); 1702push(@$lref, &master_line($master)); 1703&flush_file_lines($config{'postfix_master'}); 1704$master->{'line'} = scalar(@$lref)-1; 1705$master->{'eline'} = scalar(@$lref)-1; 1706push(@$conf, $master); 1707} 1708 1709# delete_master(&master) 1710# Removes one Postfix server process 1711sub delete_master 1712{ 1713local ($master) = @_; 1714local $conf = &get_master_config(); 1715local $lref = &read_file_lines($config{'postfix_master'}); 1716local $lines = $master->{'eline'} - $master->{'line'} + 1; 1717splice(@$lref, $master->{'line'}, $lines); 1718&flush_file_lines($config{'postfix_master'}); 1719@$conf = grep { $_ ne $master } @$conf; 1720foreach my $c (@$conf) { 1721 if ($c->{'line'} > $master->{'eline'}) { 1722 $c->{'line'} -= $lines; 1723 $c->{'eline'} -= $lines; 1724 } 1725 } 1726} 1727 1728# modify_master(&master) 1729# Updates one Postfix server process 1730sub modify_master 1731{ 1732local ($master) = @_; 1733local $conf = &get_master_config(); 1734local $lref = &read_file_lines($config{'postfix_master'}); 1735local $lines = $master->{'eline'} - $master->{'line'} + 1; 1736splice(@$lref, $master->{'line'}, $lines, 1737 &master_line($master)); 1738&flush_file_lines($config{'postfix_master'}); 1739foreach my $c (@$conf) { 1740 if ($c->{'line'} > $master->{'eline'}) { 1741 $c->{'line'} -= $lines-1; 1742 $c->{'eline'} -= $lines-1; 1743 } 1744 } 1745} 1746 1747# master_line(&master) 1748sub master_line 1749{ 1750local ($prog) = @_; 1751return ($prog->{'enabled'} ? "" : "#"). 1752 join("\t", $prog->{'name'}, $prog->{'type'}, $prog->{'private'}, 1753 $prog->{'unpriv'}, $prog->{'chroot'}, $prog->{'wakeup'}, 1754 $prog->{'maxprocs'}, $prog->{'command'}); 1755} 1756 1757sub redirect_to_map_list 1758{ 1759local ($map_name) = @_; 1760if ($map_name =~ /sender_dependent_default_transport_maps/) { 1761 redirect("dependent.cgi"); 1762 } 1763elsif ($map_name =~ /transport/) { &redirect("transport.cgi"); } 1764elsif ($map_name =~ /canonical/) { &redirect("canonical.cgi"); } 1765elsif ($map_name =~ /virtual/) { &redirect("virtual.cgi"); } 1766elsif ($map_name =~ /relocated/) { &redirect("relocated.cgi"); } 1767elsif ($map_name =~ /header/) { &redirect("header.cgi"); } 1768elsif ($map_name =~ /body/) { &redirect("body.cgi"); } 1769elsif ($map_name =~ /sender_bcc/) { &redirect("bcc.cgi?mode=sender"); } 1770elsif ($map_name =~ /recipient_bcc/) { &redirect("bcc.cgi?mode=recipient"); } 1771elsif ($map_name =~ /^smtpd_client_restrictions:/) { &redirect("client.cgi"); } 1772elsif ($map_name =~ /relay_recipient_maps|smtpd_sender_restrictions/) { &redirect("smtpd.cgi"); } 1773elsif ($map_name =~ /tls_server_sni_maps/) { &redirect("sni.cgi"); } 1774else { &redirect(""); } 1775} 1776 1777sub regenerate_map_table 1778{ 1779local ($map_name) = @_; 1780if ($map_name =~ /canonical/) { ®enerate_canonical_table(); } 1781if ($map_name =~ /relocated/) { ®enerate_relocated_table(); } 1782if ($map_name =~ /virtual/) { ®enerate_virtual_table(); } 1783if ($map_name =~ /transport/) { ®enerate_transport_table(); } 1784if ($map_name =~ /sender_access/) { ®enerate_any_table($map_name); } 1785if ($map_name =~ /sender_bcc/) { ®enerate_bcc_table(); } 1786if ($map_name =~ /recipient_bcc/) { ®enerate_recipient_bcc_table(); } 1787if ($map_name =~ /tls_server_sni_maps/) { ®enerate_sni_table(); } 1788if ($map_name =~ /smtpd_client_restrictions:(\S+)/) { 1789 ®enerate_any_table("smtpd_client_restrictions", 1790 undef, $1); 1791 } 1792if ($map_name =~ /relay_recipient_maps/) { 1793 ®enerate_relay_recipient_table(); 1794 } 1795if ($map_name =~ /sender_dependent_default_transport_maps/) { 1796 ®enerate_dependent_table(); 1797 } 1798if ($map_name =~ /smtpd_sender_restrictions/) { 1799 ®enerate_sender_restrictions_table(); 1800 } 1801} 1802 1803# mailq_table(&qfiles) 1804# Print a table of queued mail messages 1805sub mailq_table 1806{ 1807local ($qfiles) = @_; 1808 1809# Build table data 1810my @table; 1811foreach my $q (@$qfiles) { 1812 local @cols; 1813 push(@cols, { 'type' => 'checkbox', 'name' => 'file', 1814 'value' => $q->{'id'} }); 1815 push(@cols, &ui_link("view_mailq.cgi?id=$q->{'id'}",$q->{'id'})); 1816 local $size = &nice_size($q->{'size'}); 1817 push(@cols, "<font size=1>$q->{'date'}</font>"); 1818 push(@cols, "<font size=1>".&html_escape($q->{'from'})."</font>"); 1819 push(@cols, "<font size=1>".&html_escape($q->{'to'})."</font>"); 1820 push(@cols, "<font size=1>$size</font>"); 1821 push(@cols, "<font size=1>".&html_escape($q->{'status'})."</font>"); 1822 push(@table, \@cols); 1823 } 1824 1825# Show the table and form 1826print &ui_form_columns_table("delete_queues.cgi", 1827 [ [ undef, $text{'mailq_delete'} ], 1828 &compare_version_numbers($postfix_version, 1.1) >= 0 ? 1829 ( [ 'move', $text{'mailq_move'} ] ) : ( ), 1830 &compare_version_numbers($postfix_version, 2) >= 0 ? 1831 ( [ 'hold', $text{'mailq_hold'} ], 1832 [ 'unhold', $text{'mailq_unhold'} ] ) : ( ), 1833 ], 1834 1, 1835 undef, 1836 undef, 1837 [ "", $text{'mailq_id'}, $text{'mailq_date'}, $text{'mailq_from'}, 1838 $text{'mailq_to'}, $text{'mailq_size'}, $text{'mailq_status'} ], 1839 100, 1840 \@table); 1841} 1842 1843# is_table_comment(line, [force-prefix]) 1844# Returns the comment text if a line contains a comment, like # foo 1845sub is_table_comment 1846{ 1847local ($line, $force) = @_; 1848if ($config{'prefix_cmts'} || $force) { 1849 return $line =~ /^\s*#+\s*Webmin:\s*(.*)/ ? $1 : undef; 1850 } 1851else { 1852 return $line =~ /^\s*#+\s*(.*)/ ? $1 : undef; 1853 } 1854} 1855 1856# make_table_comment(comment, [force-tag]) 1857# Returns an array of lines for a comment in a map file, like # foo 1858sub make_table_comment 1859{ 1860local ($cmt, $force) = @_; 1861if (!$cmt) { 1862 return ( ); 1863 } 1864elsif ($config{'prefix_cmts'} || $force) { 1865 return ( "# Webmin: $cmt" ); 1866 } 1867else { 1868 return ( "# $cmt" ); 1869 } 1870} 1871 1872# lock_postfix_files() 1873# Lock all Postfix config files 1874sub lock_postfix_files 1875{ 1876&lock_file($config{'postfix_config_file'}); 1877&lock_file($config{'postfix_master'}); 1878} 1879 1880# unlock_postfix_files() 1881# Un-lock all Postfix config files 1882sub unlock_postfix_files 1883{ 1884&unlock_file($config{'postfix_config_file'}); 1885&unlock_file($config{'postfix_master'}); 1886} 1887 1888# map_chooser_button(field, mapname) 1889# Returns HTML for a button for popping up a map file chooser 1890sub map_chooser_button 1891{ 1892local ($name, $mapname) = @_; 1893return &popup_window_button("map_chooser.cgi?mapname=$mapname", 1024, 600, 1, 1894 [ [ "ifield", $name, "map" ] ]); 1895} 1896 1897# get_maps_types_files(value) 1898# Converts a parameter like hash:/foo/bar,hash:/tmp/xxx to a list of types 1899# and file paths. 1900sub get_maps_types_files 1901{ 1902my ($v) = @_; 1903my @rv; 1904foreach my $w (split(/[, \t]+/, $v)) { 1905 if ($w =~ /^(proxy:)?([^:]+):(\/.*)$/) { 1906 push(@rv, [ $2, $3 ]); 1907 } 1908 } 1909return @rv; 1910} 1911 1912# list_mysql_sources() 1913# Returns a list of global MySQL source names in main.cf 1914sub list_mysql_sources 1915{ 1916local @rv; 1917my $lref = &read_file_lines($config{'postfix_config_file'}); 1918foreach my $l (@$lref) { 1919 if ($l =~ /^\s*(\S+)_dbname\s*=/) { 1920 push(@rv, $1); 1921 } 1922 } 1923return @rv; 1924} 1925 1926# get_backend_config(file) 1927# Returns a hash ref from names to values in some backend (ie. mysql or ldap) 1928# config file. 1929sub get_backend_config 1930{ 1931local ($file) = @_; 1932local %rv; 1933local $lref = &read_file_lines($file, 1); 1934foreach my $l (@$lref) { 1935 if ($l =~ /^\s*([a-z0-9\_]+)\s*=\s*(.*)/i) { 1936 $rv{$1} = $2; 1937 } 1938 } 1939return \%rv; 1940} 1941 1942# save_backend_config(file, name, [value]) 1943# Updates one setting in a backend config file 1944sub save_backend_config 1945{ 1946local ($file, $name, $value) = @_; 1947local $lref = &read_file_lines($file); 1948local $found = 0; 1949for(my $i=0; $i<@$lref; $i++) { 1950 if ($lref->[$i] =~ /^\s*([a-z0-9\_]+)\s*=\s*(.*)/i && 1951 $1 eq $name) { 1952 # Found the line to fix 1953 if (defined($value)) { 1954 $lref->[$i] = "$name = $value"; 1955 } 1956 else { 1957 splice(@$lref, $i, 1); 1958 } 1959 $found = 1; 1960 last; 1961 } 1962 } 1963if (!$found && defined($value)) { 1964 push(@$lref, "$name = $value"); 1965 } 1966} 1967 1968# can_access_map(type, value) 1969# Checks if some map (such as a database) can be accessed 1970sub can_access_map 1971{ 1972local ($type, $value) = @_; 1973if (&file_map_type($type)) { 1974 return undef; # Always can 1975 } 1976elsif ($type eq "mysql") { 1977 # Parse config, connect to DB 1978 local $conf; 1979 if ($value =~ /^[\/\.]/) { 1980 # Config file 1981 local $cfile = $value; 1982 if ($cfile !~ /^\//) { 1983 $cfile = &guess_config_dir()."/".$cfile; 1984 } 1985 -r $cfile || return &text('mysql_ecfile', "<tt>$cfile</tt>"); 1986 $conf = &get_backend_config($cfile); 1987 } 1988 else { 1989 # Backend name 1990 $conf = &mysql_value_to_conf($value); 1991 $conf->{'dbname'} || return &text('mysql_esource', $value); 1992 } 1993 1994 if (!$conf->{"query"}) { 1995 # Do we have the field and table info? 1996 foreach my $need ('table', 'select_field', 'where_field') { 1997 $conf->{$need} || return &text('mysql_eneed', $need); 1998 } 1999 } 2000 # Try a connect, and a query 2001 local $dbh = &connect_mysql_db($conf); 2002 if (!ref($dbh)) { 2003 return $dbh; 2004 } 2005 local $cmd = $dbh->prepare("select ".$conf->{'select_field'}." ". 2006 "from ".$conf->{'table'}." ". 2007 "where ".$conf->{'where_field'}." = ". 2008 $conf->{'where_field'}." ". 2009 "limit 1"); 2010 if (!$cmd || !$cmd->execute()) { 2011 return &text('mysql_equery', 2012 "<tt>".$conf->{'table'}."</tt>", 2013 "<tt>".&html_escape($dbh->errstr)."</tt>"); 2014 } 2015 $cmd->finish(); 2016 $dbh->disconnect(); 2017 return undef; 2018 } 2019elsif ($type eq "ldap") { 2020 # Parse config, connect to LDAP server 2021 local $conf = &ldap_value_to_conf($value); 2022 $conf->{'search_base'} || return &text('ldap_esource', $value); 2023 2024 # Try a connect and a search 2025 local $ldap = &connect_ldap_db($conf); 2026 if (!ref($ldap)) { 2027 return $ldap; 2028 } 2029 local @classes = split(/\s+/, $config{'ldap_class'} || 2030 "inetLocalMailRecipient"); 2031 local $rv = $ldap->search(base => $conf->{'search_base'}, 2032 filter => "(objectClass=$classes[0])", 2033 sizelimit => 1); 2034 if (!$rv || $rv->code && !$rv->all_entries) { 2035 return &text('ldap_ebase', "<tt>$conf->{'search_base'}</tt>", 2036 $rv ? $rv->error : "Unknown search error"); 2037 } 2038 2039 return undef; 2040 } 2041else { 2042 return &text('map_unknown', "<tt>$type</tt>"); 2043 } 2044} 2045 2046# connect_mysql_db(&config) 2047# Attempts to connect to the Postfix MySQL database. Returns 2048# a driver handle on success, or an error message string on failure. 2049sub connect_mysql_db 2050{ 2051local ($conf) = @_; 2052local $driver = "mysql"; 2053local $drh; 2054eval <<EOF; 2055use DBI; 2056\$drh = DBI->install_driver(\$driver); 2057EOF 2058if ($@) { 2059 return &text('mysql_edriver', "<tt>DBD::$driver</tt>"); 2060 } 2061local @hosts = split(/\s+/, $config{'mysql_hosts'} || $conf->{'hosts'}); 2062@hosts = ( undef ) if (!@hosts); # Localhost only 2063local $dbh; 2064foreach my $host (@hosts) { 2065 local $dbistr = "database=$conf->{'dbname'}"; 2066 if ($host =~ /^unix:(.*)$/) { 2067 # Socket file 2068 $dbistr .= ";mysql_socket=$1"; 2069 } 2070 elsif ($host) { 2071 # Remote host 2072 $dbistr .= ";host=$host"; 2073 } 2074 $dbh = $drh->connect($dbistr, 2075 $config{'mysql_user'} || $conf->{'user'}, 2076 $config{'mysql_pass'} || $conf->{'password'}, 2077 { }); 2078 last if ($dbh); 2079 } 2080$dbh || return &text('mysql_elogin', 2081 "<tt>$conf->{'dbname'}</tt>", $drh->errstr)."\n"; 2082return $dbh; 2083} 2084 2085# connect_ldap_db(&config) 2086# Attempts to connect to an LDAP server with Postfix maps. Returns 2087# a driver handle on success, or an error message string on failure. 2088sub connect_ldap_db 2089{ 2090local ($conf) = @_; 2091if (defined($connect_ldap_db_cache)) { 2092 return $connect_ldap_db_cache; 2093 } 2094eval "use Net::LDAP"; 2095if ($@) { 2096 return &text('ldap_eldapmod', "<tt>Net::LDAP</tt>"); 2097 } 2098local @servers = split(/\s+/, $config{'ldap_host'} || 2099 $conf->{'server_host'} || "localhost"); 2100local ($ldap, $lasterr); 2101foreach my $server (@servers) { 2102 local ($host, $port, $tls); 2103 if ($server =~ /^(\S+):(\d+)$/) { 2104 # Host and port 2105 ($host, $port) = ($1, $2); 2106 $tls = $conf->{'start_tls'} eq 'yes'; 2107 } 2108 elsif ($server =~ /^(ldap|ldaps):\/\/(\S+)(:(\d+))?/) { 2109 # LDAP URL 2110 $host = $2; 2111 $port = $4 || $conf->{'server_port'} || 389; 2112 $tls = $1 eq "ldaps"; 2113 } 2114 else { 2115 # Host only 2116 $host = $server; 2117 $port = $conf->{'server_port'} || 389; 2118 $tls = $conf->{'start_tls'} eq 'yes'; 2119 } 2120 $ldap = Net::LDAP->new($server, port => $port); 2121 if (!$ldap) { 2122 $lasterr = &text('ldap_eldap', "<tt>$server</tt>", $port); 2123 next; 2124 } 2125 if ($tls) { 2126 $ldap->start_tls; 2127 } 2128 if ($conf->{'bind'} eq 'yes' || $config{'ldap_user'}) { 2129 local $mesg = $ldap->bind( 2130 dn => $config{'ldap_user'} || $conf->{'bind_dn'}, 2131 password => $config{'ldap_pass'} || $conf->{'bind_pw'}); 2132 if (!$mesg || $mesg->code) { 2133 $lasterr = &text('ldap_eldaplogin', 2134 "<tt>$server</tt>", 2135 "<tt>".($config{'ldap_user'} || 2136 $conf->{'bind_dn'})."</tt>", 2137 $mesg ? $mesg->error : "Unknown error"); 2138 $ldap = undef; 2139 next; 2140 } 2141 } 2142 last if ($ldap); 2143 } 2144if ($ldap) { 2145 # Connected OK 2146 $connect_ldap_db_cache = $ldap; 2147 return $ldap; 2148 } 2149else { 2150 return $lasterr; 2151 } 2152} 2153 2154# mysql_value_to_conf(value) 2155# Converts a MySQL config file or source name to a config hash ref 2156sub mysql_value_to_conf 2157{ 2158local ($value) = @_; 2159local $conf; 2160if ($value =~ /^[\/\.]/) { 2161 # Config file 2162 local $cfile = $value; 2163 if ($cfile !~ /^\//) { 2164 $cfile = &guess_config_dir()."/".$cfile; 2165 } 2166 -r $cfile || &error(&text('mysql_ecfile', "<tt>$cfile</tt>")); 2167 $conf = &get_backend_config($cfile); 2168 2169 if ($conf->{'query'} =~ /^select\s+(\S+)\s+from\s+(\S+)\s+where\s+(\S+)\s*=\s*'\%s'\s*(.*)/i && !$conf->{'table'}) { 2170 # Try to extract table and fields from the query 2171 $conf->{'select_field'} = $1; 2172 $conf->{'table'} = $2; 2173 $conf->{'where_field'} = $3; 2174 $conf->{'additional_conditions'} = $4; 2175 } 2176 $conf->{'table'} || &error(&text('mysql_ecfile2', "<tt>$cfile</tt>")); 2177 } 2178else { 2179 # Backend name 2180 $conf = { }; 2181 foreach my $k ("hosts", "dbname", "user", "password", "query", 2182 "table", "where_field", "select_field", 2183 "additional_conditions") { 2184 local $v = &get_real_value($value."_".$k); 2185 $conf->{$k} = $v; 2186 } 2187 if ($conf->{'query'} =~ /^select\s+(\S+)\s+from\s+(\S+)\s+where\s+(\S+)\s*=\s*'\%s'\s*(.*)/i && !$conf->{'table'}) { 2188 # Try to extract table and fields from the query 2189 $conf->{'select_field'} = $1; 2190 $conf->{'table'} = $2; 2191 $conf->{'where_field'} = $3; 2192 $conf->{'additional_conditions'} = $4; 2193 } 2194 } 2195return $conf; 2196} 2197 2198# ldap_value_to_conf(value) 2199# Converts an LDAP config file name to a config hash ref 2200sub ldap_value_to_conf 2201{ 2202local ($value) = @_; 2203local $conf; 2204local $cfile = $value; 2205if ($cfile !~ /^\//) { 2206 $cfile = &guess_config_dir()."/".$cfile; 2207 } 2208-r $cfile && !-d $cfile || &error(&text('ldap_ecfile', "<tt>$cfile</tt>")); 2209return &get_backend_config($cfile); 2210} 2211 2212# can_map_comments(name) 2213# Returns 1 if some map can have comments. Not allowed for MySQL and LDAP. 2214sub can_map_comments 2215{ 2216local ($name) = @_; 2217foreach my $tv (&get_maps_types_files(&get_real_value($name))) { 2218 return 0 if (!&file_map_type($tv->[0])); 2219 } 2220return 1; 2221} 2222 2223# can_map_manual(name) 2224# Returns 1 if osme map has a file that can be manually edited 2225sub can_map_manual 2226{ 2227local ($name) = @_; 2228foreach my $tv (&get_maps_types_files(&get_real_value($name))) { 2229 return 0 if (!&file_map_type($tv->[0])); 2230 } 2231return 1; 2232} 2233 2234# supports_map_type(type) 2235# Returns 1 if a map of some type is supported by Postfix 2236sub supports_map_type 2237{ 2238local ($type) = @_; 2239return 1 if ($type eq 'hash'); # Assume always supported 2240if (!scalar(@supports_map_type_cache)) { 2241 @supports_map_type = ( ); 2242 open(POSTCONF, "$config{'postfix_config_command'} -m |"); 2243 while(<POSTCONF>) { 2244 s/\r|\n//g; 2245 push(@supports_map_type_cache, $_); 2246 } 2247 close(POSTCONF); 2248 } 2249return &indexoflc($type, @supports_map_type_cache) >= 0; 2250} 2251 2252# split_props(text) 2253# Converts multiple lines of text into LDAP attributes 2254sub split_props 2255{ 2256local ($text) = @_; 2257local %pmap; 2258foreach $p (split(/\t+/, $text)) { 2259 if ($p =~ /^(\S+):\s*(.*)/) { 2260 push(@{$pmap{$1}}, $2); 2261 } 2262 } 2263local @rv; 2264local $k; 2265foreach $k (keys %pmap) { 2266 local $v = $pmap{$k}; 2267 if (@$v == 1) { 2268 push(@rv, $k, $v->[0]); 2269 } 2270 else { 2271 push(@rv, $k, $v); 2272 } 2273 } 2274return @rv; 2275} 2276 2277# list_smtpd_restrictions() 2278# Returns a list of SMTP server restrictions known to Webmin 2279sub list_smtpd_restrictions 2280{ 2281return ( "permit_mynetworks", 2282 "permit_inet_interfaces", 2283 &compare_version_numbers($postfix_version, 2.3) < 0 ? 2284 "reject_unknown_client" : 2285 "reject_unknown_reverse_client_hostname", 2286 "permit_sasl_authenticated", 2287 "reject_unauth_destination", 2288 "check_relay_domains", 2289 "permit_mx_backup" ); 2290} 2291 2292# list_client_restrictions() 2293# Returns a list of boolean values for use in smtpd_client_restrictions 2294sub list_client_restrictions 2295{ 2296return ( "permit_mynetworks", 2297 "permit_inet_interfaces", 2298 &compare_version_numbers($postfix_version, 2.3) < 0 ? 2299 "reject_unknown_client" : 2300 "reject_unknown_reverse_client_hostname", 2301 "permit_tls_all_clientcerts", 2302 "permit_sasl_authenticated", 2303 ); 2304} 2305 2306# list_multi_client_restrictions() 2307# Returns a list of restrictions that have a following value 2308sub list_multi_client_restrictions 2309{ 2310return ( "check_client_access", 2311 "reject_rbl_client", 2312 "reject_rhsbl_client", 2313 ); 2314} 2315 2316sub file_map_type 2317{ 2318local ($type) = @_; 2319return 1 if ($type eq 'hash' || $type eq 'regexp' || $type eq 'pcre' || 2320 $type eq 'btree' || $type eq 'dbm' || $type eq 'cidr'); 2321} 2322 2323# in_props(&props, name) 2324# Looks up the value of a named property in a list 2325sub in_props 2326{ 2327local ($props, $name) = @_; 2328for(my $i=0; $i<@$props; $i++) { 2329 if (lc($props->[$i]) eq lc($name)) { 2330 return $props->[$i+1]; 2331 } 2332 } 2333return undef; 2334} 2335 2336# For calling from aliases-lib only 2337sub rebuild_map_cmd 2338{ 2339return 0; 2340} 2341 2342# valid_postfix_command(cmd) 2343# Check if some command exists on the system. Strips off args. 2344sub valid_postfix_command 2345{ 2346my ($cmd) = @_; 2347($cmd) = &split_quoted_string($cmd); 2348return &has_command($cmd); 2349} 2350 2351# get_all_config_files() 2352# Returns a list of all possible postfix config files 2353sub get_all_config_files 2354{ 2355my @rv; 2356 2357# Add main config file 2358push(@rv, $config{'postfix_config_file'}); 2359push(@rv, $config{'postfix_master'}); 2360 2361# Add known map files 2362push(@rv, &get_maps_files("alias_maps")); 2363push(@rv, &get_maps_files("alias_database")); 2364push(@rv, &get_maps_files("canonical_maps")); 2365push(@rv, &get_maps_files("recipient_canonical_maps")); 2366push(@rv, &get_maps_files("sender_canonical_maps")); 2367push(@rv, &get_maps_files($virtual_maps)); 2368push(@rv, &get_maps_files("transport_maps")); 2369push(@rv, &get_maps_files("relocated_maps")); 2370push(@rv, &get_maps_files("relay_recipient_maps")); 2371push(@rv, &get_maps_files("smtpd_sender_restrictions")); 2372 2373# Add other files in /etc/postfix 2374local $cdir = &guess_config_dir(); 2375opendir(DIR, $cdir); 2376foreach $f (readdir(DIR)) { 2377 next if ($f eq "." || $f eq ".." || $f =~ /\.(db|dir|pag)$/i); 2378 push(@rv, "$cdir/$f"); 2379 } 2380closedir(DIR); 2381 2382return &unique(@rv); 2383} 2384 23851; 2386 2387