3 # postfix-module by Guillaume Cottenceau <gc@mandrakesoft.com>,
4 # for webmin by Jamie Cameron
6 $POSTFIX_MODULE_VERSION = 5;
8 BEGIN { push(@INC, ".."); };
11 %access = &get_module_acl();
12 $access{'postfinger'} = 0 if (&is_readonly_mode());
15 $config{'perpage'} ||= 20; # a value of 0 can cause problems
17 # Get the saved version number
18 if (&open_readfile(VERSION, "$module_config_directory/version")) {
19 chop($postfix_version = <VERSION>);
23 if (!$postfix_version) {
24 # Not there .. work it out
25 if (&has_command($config{'postfix_config_command'}) &&
26 &backquote_command("$config{'postfix_config_command'} mail_version 2>&1", 1) =~ /mail_version\s*=\s*(.*)/) {
28 $postfix_version = $1;
31 # And save for other callers
32 &open_tempfile(VERSION, ">$module_config_directory/version", 0, 1);
33 &print_tempfile(VERSION, "$postfix_version\n");
34 &close_tempfile(VERSION);
37 if ($postfix_version >= 2) {
38 $virtual_maps = "virtual_alias_maps";
39 $ldap_timeout = "ldap_timeout";
42 $virtual_maps = "virtual_maps";
43 $ldap_timeout = "ldap_lookup_timeout";
49 my $answ = $config{'postfix_config_file'};
50 $answ =~ /(.*)\/[^\/]*/;
54 $config_dir = guess_config_dir();
57 ## DOC: compared to other webmin modules, here we don't need to parse
58 ## the config file, because a config command is provided by
59 ## postfix to read and write the config parameters
62 # postfix_module_version()
63 # returns the version of the postfix module
64 sub postfix_module_version
66 return $POSTFIX_MODULE_VERSION;
69 # is_postfix_running()
70 # returns 1 if running, 0 if stopped, calls error() if problem
71 sub is_postfix_running
73 my $queuedir = get_current_value("queue_directory");
74 my $processid = get_current_value("process_id_directory");
76 my $pid_file = $queuedir."/".$processid."/master.pid";
77 my $pid = &check_pid_file($pid_file);
82 sub is_existing_parameter
84 my $out = &backquote_command("$config{'postfix_config_command'} -c $config_dir $_[0] 2>&1", 1);
85 return !($out =~ /unknown parameter/);
89 # get_current_value(parameter_name)
90 # returns a scalar corresponding to the value of the parameter
91 ## modified to allow main_parameter:subparameter
94 # First try to get the value from main.cf directly
95 my ($name,$key)=split /:/,$_[0];
96 my $lref = &read_file_lines($config{'postfix_config_file'});
98 my ($begin_flag, $end_flag);
99 foreach my $l (@$lref) {
100 # changes made to this loop by Dan Hartman of Rae Internet /
101 # Message Partners for multi-line parsing 2007-06-04
102 if ($begin_flag == 1 && $l =~ /\S/ && $l =~ /^(\s+[^#].+)/) {
103 # non-comment continuation line, and replace tabs with spaces
107 if ($l =~ /^\s*([a-z0-9\_]+)\s*=\s*(.*)|^\s*([a-z0-9\_]+)\s*=\s*$/ &&
109 # Found the one we're looking for, set a flag
113 if ($l =~ /^\s*([a-z0-9\_]+)\s*=\s*(.*)|^\s*([a-z0-9\_]+)\s*=\s*$/ &&
114 $1 . $3 ne $name && $begin_flag == 1) {
115 # after the beginning, another configuration variable
121 if (!defined($out)) {
122 # Fall back to asking Postfix
123 # -h tells postconf not to output the name of the parameter
124 $out = &backquote_command(
125 "$config{'postfix_config_command'} -c $config_dir -h $name 2>&1", 1);
127 &error(&text('query_get_efailed', $name, $out));
129 elsif ($out =~ /warning:.*unknown\s+parameter/) {
135 # If the value asked for was like foo:bar, extract from the value
136 # the parts after bar
138 while($out =~ /^(.*?)\Q$key\E\s+(\S+)(.*)$/) {
144 return join(" ", @res);
149 # if_default_value(parameter_name)
150 # returns if the value is the default value
153 my $out = &backquote_command("$config{'postfix_config_command'} -c $config_dir -n $_[0] 2>&1", 1);
154 if ($?) { &error(&text('query_get_efailed', $_[0], $out)); }
158 # get_default_value(parameter_name)
159 # returns the default value of the parameter
160 sub get_default_value
162 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
163 if ($?) { &error(&text('query_get_efailed', $_[0], $out)); }
169 # set_current_value(parameter_name, parameter_value, [always-set])
170 # Update some value in the Postfix configuration file
171 sub set_current_value
174 if ($value eq "__DEFAULT_VALUE_IE_NOT_IN_CONFIG_FILE__" ||
175 $value eq &get_default_value($_[0]) && !$_[2])
177 # there is a special case in which there is no static default value ;
178 # postfix will handle it correctly if I remove the line in `main.cf'
179 my $all_lines = &read_file_lines($config{'postfix_config_file'});
180 my $line_of_parameter = -1;
181 my $end_line_of_parameter = -1;
184 foreach (@{$all_lines})
186 if (/^\s*$_[0]\s*=/) {
187 $line_of_parameter = $i;
188 $end_line_of_parameter = $i;
189 } elsif ($line_of_parameter >= 0 &&
191 # Multi-line continuation
192 $end_line_of_parameter = $i;
197 if ($line_of_parameter != -1) {
198 splice(@{$all_lines}, $line_of_parameter,
199 $end_line_of_parameter - $line_of_parameter + 1);
200 &flush_file_lines($config{'postfix_config_file'});
202 &unflush_file_lines($config{'postfix_config_file'});
208 $ex = &execute_command(
209 "$config{'postfix_config_command'} -c $config_dir ".
210 "-e $_[0]=".quotemeta($value), undef, \$out, \$out);
211 $ex && &error(&text('query_set_efailed', $_[0], $_[1], $out).
212 "<br> $config{'postfix_config_command'} -c $config_dir ".
213 "-e $_[0]=\"$value\" 2>&1");
214 &unflush_file_lines($config{'postfix_config_file'}); # Invalidate cache
222 my $cmd = "$config{'postfix_control_command'} -c $config_dir check";
223 my $out = &backquote_command("$cmd 2>&1 </dev/null", 1);
225 if ($ex && &foreign_check("proc")) {
226 # Get a better error message
227 &foreign_require("proc", "proc-lib.pl");
228 $out = &proc::pty_backquote("$cmd 2>&1 </dev/null");
230 return $ex ? ($out || "$cmd failed") : undef;
237 $access{'startstop'} || &error($text{'reload_ecannot'});
238 if (is_postfix_running())
240 if (check_postfix()) { &error("$text{'check_error'}"); }
242 if (!$config{'reload_cmd'}) {
243 $ex = &system_logged("$config{'postfix_control_command'} -c $config_dir reload >/dev/null 2>&1");
246 $ex = &system_logged("$config{'reload_cmd'} >/dev/null 2>&1");
248 if ($ex) { &error($text{'reload_efailed'}); }
253 # Attempts to stop postfix, returning undef on success or an error message
257 if ($config{'stop_cmd'}) {
258 $out = &backquote_logged("$config{'stop_cmd'} 2>&1");
261 $out = &backquote_logged("$config{'postfix_control_command'} -c $config_dir stop 2>&1");
263 return $? ? "<tt>$out</tt>" : undef;
267 # Attempts to start postfix, returning undef on success or an error message
271 if ($config{'start_cmd'}) {
272 $out = &backquote_logged("$config{'start_cmd'} 2>&1");
275 $out = &backquote_logged("$config{'postfix_control_command'} -c $config_dir start 2>&1");
277 return $? ? "<tt>$out</tt>" : undef;
280 # option_radios_freefield(name_of_option, length_of_free_field, [name_of_radiobutton, text_of_radiobutton]+)
281 # builds an option with variable number of radiobuttons and a free field
282 # WARNING: *FIRST* RADIO BUTTON *MUST* BE THE DEFAULT VALUE OF POSTFIX
283 sub option_radios_freefield
285 my ($name, $length) = ($_[0], $_[1]);
287 my $v = &get_current_value($name);
288 my $key = 'opts_'.$name;
290 my $check_free_field = 1;
292 my $help = -r &help_file($module_name, "opt_".$name) ?
293 &hlink($text{$key}, "opt_".$name) : $text{$key};
296 # first radio button (must be default value!!)
297 $rv .= &ui_oneradio($name."_def", "__DEFAULT_VALUE_IE_NOT_IN_CONFIG_FILE__",
298 $_[2], &if_default_value($name));
300 $check_free_field = 0 if &if_default_value($name);
303 # other radio buttons
304 while (defined($_[2]))
306 $rv .= &ui_oneradio($name."_def", $_[2], $_[3], $v eq $_[2]);
307 if ($v eq $_[2]) { $check_free_field = 0; }
313 $rv .= &ui_oneradio($name."_def", "__USE_FREE_FIELD__", undef,
314 $check_free_field == 1);
315 $rv .= &ui_textbox($name, $check_free_field == 1 ? $v : undef, $length);
316 print &ui_table_row($help, $rv, $length > 20 ? 3 : 1);
319 # option_mapfield(name_of_option, length_of_free_field)
320 # Prints a field for selecting a map, or none
323 my ($name, $length) = ($_[0], $_[1]);
325 my $v = &get_current_value($name);
326 my $key = 'opts_'.$name;
328 my $check_free_field = 1;
330 my $help = -r &help_file($module_name, "opt_".$name) ?
331 &hlink($text{$key}, "opt_".$name) : $text{$key};
333 $rv .= &ui_oneradio($name."_def", "__DEFAULT_VALUE_IE_NOT_IN_CONFIG_FILE__",
334 $text{'opts_nomap'}, &if_default_value($name));
337 $check_free_field = 0 if &if_default_value($name);
341 $rv .= &ui_oneradio($name."_def", "__USE_FREE_FIELD__",
342 $text{'opts_setmap'}, $check_free_field == 1);
343 $rv .= &ui_textbox($name, $check_free_field == 1 ? $v : undef, $length);
344 $rv .= &map_chooser_button($name, $name);
345 print &ui_table_row($help, $rv, $length > 20 ? 3 : 1);
350 # option_freefield(name_of_option, length_of_free_field)
351 # builds an option with free field
354 my ($name, $length) = ($_[0], $_[1]);
356 my $v = &get_current_value($name);
357 my $key = 'opts_'.$name;
359 print &ui_table_row(&hlink($text{$key}, "opt_".$name),
360 &ui_textbox($name."_def", $v, $length),
361 $length > 20 ? 3 : 1);
365 # option_yesno(name_of_option, [help])
366 # if help is provided, displays help link
370 my $v = &get_current_value($name);
371 my $key = 'opts_'.$name;
373 print &ui_table_row(defined($_[1]) ? &hlink($text{$key}, "opt_".$name)
375 &ui_radio($name."_def", lc($v),
376 [ [ "yes", $text{'yes'} ],
377 [ "no", $text{'no'} ] ]));
380 # option_select(name_of_option, &options, [help])
381 # Shows a drop-down menu of options
385 my $v = &get_current_value($name);
386 my $key = 'opts_'.$name;
388 print &ui_table_row(defined($_[2]) ? &hlink($text{$key}, "opt_".$name)
390 &ui_select($name."_def", lc($v), $_[1]));
395 ############################################################################
396 # aliases support [too lazy to create a aliases-lib.pl :-)]
398 # get_aliases_files($alias_maps) : @aliases_files
399 # parses its argument to extract the filenames of the aliases files
400 # supports multiple alias-files
401 sub get_aliases_files
403 return map { $_->[1] }
404 grep { &file_map_type($_->[0]) } &get_maps_types_files($_[0]);
407 # init_new_alias() : $number
408 # gives a new number of alias
411 $aliases = &get_aliases();
415 foreach $trans (@{$aliases})
417 if ($trans->{'number'} > $max_number) { $max_number = $trans->{'number'}; }
420 return $max_number+1;
423 # list_postfix_aliases()
424 # Returns a list of all aliases. These typically come from a file, but may also
425 # be taken from a MySQL or LDAP backend
426 sub list_postfix_aliases
429 foreach my $f (&get_maps_types_files(&get_current_value("alias_maps"))) {
430 if (&file_map_type($f->[0])) {
431 # We can read this file directly
432 local $sofar = scalar(@rv);
433 foreach my $a (&list_aliases([ $f->[1] ])) {
434 $a->{'num'} += $sofar;
440 push(@maps, "$f->[0]:$f->[1]");
444 # Convert values from MySQL and LDAP maps into alias structures
445 local $maps = &get_maps("alias_maps", undef, join(",", @maps));
446 foreach my $m (@$maps) {
447 local $v = $m->{'value'};
449 while($v =~ /^\s*,?\s*()"([^"]+)"(.*)$/ ||
450 $v =~ /^\s*,?\s*(\|)"([^"]+)"(.*)$/ ||
451 $v =~ /^\s*,?\s*()([^,\s]+)(.*)$/) {
452 push(@values, $1.$2);
455 if ($m->{'name'} =~ /^#(.*)$/) {
462 $m->{'values'} = \@values;
463 $m->{'num'} = scalar(@rv);
470 # create_postfix_alias(&alias)
471 # Adds a new alias, either to the local file or another backend
472 sub create_postfix_alias
475 local @afiles = &get_maps_types_files(&get_current_value("alias_maps"));
476 local $last_type = $afiles[$#afiles]->[0];
477 local $last_file = $afiles[$#afiles]->[1];
478 if (&file_map_type($last_type)) {
479 # Just adding to a file
480 &create_alias($alias, [ $last_file ], 1);
483 # Add to appropriate backend map
484 if (!$alias->{'enabled'}) {
485 $alias->{'name'} = '#'.$alias->{'name'};
487 $alias->{'value'} = join(',', map { /\s/ ? "\"$_\"" : $_ }
488 @{$alias->{'values'}});
489 &create_mapping("alias_maps", $alias, undef, "$last_type:$last_file");
493 # delete_postfix_alias(&alias)
494 # Delete an alias, either from the files or from a MySQL or LDAP map
495 sub delete_postfix_alias
498 if ($alias->{'map_type'}) {
499 # This was from a map
500 &delete_mapping("alias_maps", $alias);
504 &delete_alias($alias, 1);
508 # modify_postfix_alias(&oldalias, &alias)
509 # Update an alias, either in a file or in a map
510 sub modify_postfix_alias
512 local ($oldalias, $alias) = @_;
513 if ($oldalias->{'map_type'}) {
515 if (!$alias->{'enabled'}) {
516 $alias->{'name'} = '#'.$alias->{'name'};
518 $alias->{'value'} = join(',', map { /\s/ ? "\"$_\"" : $_ }
519 @{$alias->{'values'}});
520 &modify_mapping("alias_maps", $oldalias, $alias);
523 # Regular alias in a file
524 &modify_alias($oldalias, $alias);
528 # renumber_list(&list, &position-object, lines-offset)
533 foreach $e (@{$_[0]}) {
534 next if (defined($e->{'alias_file'}) &&
535 $e->{'alias_file'} ne $_[1]->{'alias_file'});
536 next if (defined($e->{'map_file'}) &&
537 $e->{'map_file'} ne $_[1]->{'map_file'});
538 $e->{'line'} += $_[2] if ($e->{'line'} > $_[1]->{'line'});
539 $e->{'eline'} += $_[2] if (defined($e->{'eline'}) &&
540 $e->{'eline'} > $_[1]->{'eline'});
544 # save_options(%options, [&always-save])
548 if (check_postfix()) { &error("$text{'check_error'}"); }
550 my %options = %{$_[0]};
552 foreach $key (keys %options)
556 (my $param = $key) =~ s/_def//;
557 my $value = $options{$key} eq "__USE_FREE_FIELD__" ?
558 $options{$param} : $options{$key};
559 if ($value =~ /(\S+):(\/\S+)/ && $access{'dir'} ne '/') {
560 foreach my $f (&get_maps_files("$1:$2")) {
561 if (!&is_under_directory($access{'dir'}, $f)) {
562 &error(&text('opts_edir', $access{'dir'}));
566 &set_current_value($param, $value,
567 &indexof($param, @{$_[1]}) >= 0);
575 sub regenerate_aliases
578 $access{'aliases'} || error($text{'regenerate_ecannot'});
579 if (get_current_value("alias_maps") eq "")
581 $out = &backquote_logged("$config{'postfix_newaliases_command'} 2>&1");
582 if ($?) { &error(&text('regenerate_alias_efailed', $out)); }
587 foreach $map (get_maps_types_files(get_real_value("alias_maps")))
589 if (&file_map_type($map->[0])) {
590 $out = &backquote_logged("$config{'postfix_aliases_table_command'} -c $config_dir $map->[1] 2>&1");
591 if ($?) { &error(&text('regenerate_table_efailed', $map->[1], $out)); }
598 # regenerate_relocated_table()
599 sub regenerate_relocated_table
601 ®enerate_any_table("relocated_maps");
605 # regenerate_virtual_table()
606 sub regenerate_virtual_table
608 ®enerate_any_table($virtual_maps);
611 # regenerate_bcc_table()
612 sub regenerate_bcc_table
614 ®enerate_any_table("sender_bcc_maps");
617 sub regenerate_relay_recipient_table
619 ®enerate_any_table("relay_recipient_maps");
622 # regenerate_recipient_bcc_table()
623 sub regenerate_recipient_bcc_table
625 ®enerate_any_table("recipient_bcc_maps");
628 # regenerate_header_table()
629 sub regenerate_header_table
631 ®enerate_any_table("header_checks");
634 # regenerate_body_table()
635 sub regenerate_body_table
637 ®enerate_any_table("body_checks");
640 # regenerate_canonical_table
642 sub regenerate_canonical_table
644 ®enerate_any_table("canonical_maps");
645 ®enerate_any_table("recipient_canonical_maps");
646 ®enerate_any_table("sender_canonical_maps");
650 # regenerate_transport_table
652 sub regenerate_transport_table
654 ®enerate_any_table("transport_maps");
658 # regenerate_any_table($parameter_where_to_find_the_table_names,
659 # [ &force-files ], [ after-tag ])
661 sub regenerate_any_table
663 my ($name, $force, $after) = @_;
666 @files = map { [ "hash", $_ ] } @$force;
667 } elsif (&get_current_value($name) ne "") {
668 my $value = &get_real_value($name);
670 $value =~ s/^.*\Q$after\E\s+(\S+).*$/$1/ || return;
672 @files = &get_maps_types_files($value);
674 foreach my $map (@files)
677 if (&file_map_type($map->[0]) &&
678 $map->[0] ne 'regexp' && $map->[0] ne 'pcre') {
679 local $out = &backquote_logged("$config{'postfix_lookup_table_command'} -c $config_dir $map->[0]:$map->[1] 2>&1");
680 if ($?) { &error(&text('regenerate_table_efailed', $map->[1], $out)); }
687 ############################################################################
688 # maps [canonical, virtual, transport] support
690 # get_maps_files($maps_param) : @maps_files
691 # parses its argument to extract the filenames of the mapping files
692 # supports multiple maps-files
695 $_[0] =~ /:(\/[^,\s]*)(.*)/ || return ( );
696 (my $returnvalue, my $recurse) = ( $1, $2 );
698 return ( $returnvalue,
699 ($recurse =~ /:\/[^,\s]*/) ?
700 &get_maps_files($recurse)
707 # get_maps($maps_name, [&force-files], [force-map]) : \@maps
708 # Construct the mappings database taken from the map files given from the
712 if (!defined($maps_cache{$_[0]}))
714 my @maps_files = $_[1] ? (map { [ "hash", $_ ] } @{$_[1]}) :
715 $_[2] ? &get_maps_types_files($_[2]) :
716 &get_maps_types_files(&get_real_value($_[0]));
718 foreach my $maps_type_file (@maps_files)
720 my ($maps_type, $maps_file) = @$maps_type_file;
722 if (&file_map_type($maps_type)) {
723 # Read a file on disk
724 &open_readfile(MAPS, $maps_file);
729 s/\r|\n//g; # remove newlines
730 if (/^\s*#+\s*(.*)/) {
732 $cmt = &is_table_comment($_);
734 elsif (/^\s*(\/[^\/]*\/[a-z]*)\s+([^#]*)/ ||
735 /^\s*([^\s]+)\s+([^#]*)/) {
741 $map{'line'} = $cmt ? $i-1 : $i;
743 $map{'map_file'} = $maps_file;
744 $map{'map_type'} = $maps_type;
745 $map{'file'} = $maps_file;
746 $map{'number'} = $number;
748 push(@{$maps_cache{$_[0]}}, \%map);
758 } elsif ($maps_type eq "mysql") {
759 # Get from a MySQL database
760 local $conf = &mysql_value_to_conf($maps_file);
761 local $dbh = &connect_mysql_db($conf);
762 ref($dbh) || &error($dbh);
763 local $cmd = $dbh->prepare(
764 "select ".$conf->{'where_field'}.
765 ",".$conf->{'select_field'}.
766 " from ".$conf->{'table'}.
768 $conf->{'additional_conditions'});
769 if (!$cmd || !$cmd->execute()) {
770 &error(&text('mysql_elist',
771 "<tt>".&html_escape($dbh->errstr)."</tt>"));
773 while(my ($k, $v) = $cmd->fetchrow()) {
779 $map{'map_file'} = $maps_file;
780 $map{'map_type'} = $maps_type;
781 $map{'number'} = $number;
782 push(@{$maps_cache{$_[0]}}, \%map);
787 } elsif ($maps_type eq "ldap") {
788 # Get from an LDAP database
789 local $conf = &ldap_value_to_conf($maps_file);
790 local $ldap = &connect_ldap_db($conf);
791 ref($ldap) || &error($ldap);
792 local ($name_attr, $filter) = &get_ldap_key($conf);
793 local $scope = $conf->{'scope'} || 'sub';
794 local $rv = $ldap->search(base => $conf->{'search_base'},
797 if (!$rv || $rv->code) {
799 &error(&text('ldap_equery',
800 "<tt>$conf->{'search_base'}</tt>",
801 "<tt>".&html_escape($rv->error)."</tt>"));
803 foreach my $o ($rv->all_entries) {
806 $map{'name'} = $o->get_value($name_attr);
807 $map{'value'} = $o->get_value(
808 $conf->{'result_attribute'} || "maildrop");
809 $map{'dn'} = $o->dn();
810 $map{'map_file'} = $maps_file;
811 $map{'map_type'} = $maps_type;
812 $map{'number'} = $number;
813 push(@{$maps_cache{$_[0]}}, \%map);
818 return $maps_cache{$_[0]};
822 # generate_map_edit(name, desc, [wide], [nametitle], [valuetitle])
823 # Prints a table showing map contents, with links to edit and add
824 sub generate_map_edit
826 # Check if map is set
827 if (&get_current_value($_[0]) eq "")
829 print "<b>$text{'no_map2'}</b><p>\n";
833 # Make sure the user is allowed to edit them
834 foreach my $f (&get_maps_types_files(&get_current_value($_[0]))) {
835 if (&file_map_type($f->[0])) {
836 &is_under_directory($access{'dir'}, $f->[1]) ||
837 &error(&text('mapping_ecannot', $access{'dir'}));
841 # Make sure we *can* edit them
842 foreach my $f (&get_maps_types_files(&get_current_value($_[0]))) {
843 my $err = &can_access_map(@$f);
845 print "<b>",&text('map_cannot', $err),"</b><p>\n";
850 my $mappings = &get_maps($_[0]);
851 my $nt = $_[3] || $text{'mapping_name'};
852 my $vt = $_[4] || $text{'mapping_value'};
854 local @links = ( "<a href='edit_mapping.cgi?map_name=$_[0]'>".
855 $text{'new_mapping'}."</a>",);
857 if ($#{$mappings} ne -1)
863 if ($config{'sort_mode'} == 1) {
864 if ($_[0] eq $virtual_maps) {
865 @{$mappings} = sort sort_by_domain @{$mappings};
868 @{$mappings} = sort { $a->{'name'} cmp $b->{'name'} }
873 # Split into two columns, if needed
875 my $split_index = int(($#{$mappings})/2);
876 if ($config{'columns'} == 2) {
877 @parts = ( [ @{$mappings}[0 .. $split_index] ],
878 [ @{$mappings}[$split_index+1 .. $#{$mappings} ] ] );
881 @parts = ( $mappings );
884 # Start of the overall form
885 print &ui_form_start("delete_mappings.cgi", "post");
886 print &ui_hidden("map_name", $_[0]),"\n";
887 unshift(@links, &select_all_link("d", 1),
888 &select_invert_link("d", 1));
889 print &ui_links_row(\@links);
892 foreach my $p (@parts) {
895 foreach my $map (@$p) {
897 { 'type' => 'checkbox', 'name' => 'd',
898 'value' => $map->{'name'} },
899 "<a href=\"edit_mapping.cgi?num=$map->{'number'}&".
900 "map_name=$_[0]\">".&html_escape($map->{'name'}).
902 &html_escape($map->{'value'}),
903 $config{'show_cmts'} ?
904 ( &html_escape($map->{'cmt'}) ) : ( ),
908 # Add a table to the grid
909 push(@grid, &ui_columns_table(
911 $config{'show_cmts'} ? ( $text{'mapping_cmt'} ) : ( ),
920 print &ui_grid_table(\@grid, 2, 100,
921 [ "width=50%", "width=50%" ]);
925 print &ui_links_row(\@links);
926 print &ui_form_end([ [ "delete", $text{'mapping_delete'} ] ]);
929 # None, so just show edit link
930 print "<b>$text{'mapping_none'}</b><p>\n";
931 print &ui_links_row(\@links);
935 if ($access{'manual'} && &can_map_manual($_[0])) {
937 print &ui_buttons_start();
938 print &ui_buttons_row("edit_manual.cgi", $text{'new_manual'},
939 $text{'new_manualmsg'},
940 &ui_hidden("map_name", $_[0]));
941 print &ui_buttons_end();
947 # create_mapping(map, &mapping, [&force-files], [force-map])
950 &get_maps($_[0], $_[2], $_[3]); # force cache init
951 my @maps_files = $_[2] ? (map { [ "hash", $_ ] } @{$_[2]}) :
952 $_[3] ? &get_maps_types_files($_[3]) :
953 &get_maps_types_files(&get_real_value($_[0]));
955 # If multiple maps, find a good one to add to .. avoid regexp if we can
957 if (@maps_files == 1) {
958 $last_map = $maps_files[0];
961 for(my $i=$#maps_files; $i>=0; $i--) {
962 if ($maps_files[$i]->[0] ne 'regexp' &&
963 $maps_files[$i]->[0] ne 'pcre') {
964 $last_map = $maps_files[$i];
968 $last_map ||= $maps_files[$#maps_files]; # Fall back to last one
970 my ($maps_type, $maps_file) = @$last_map;
972 if (&file_map_type($maps_type)) {
973 # Adding to a regular file
974 local $lref = &read_file_lines($maps_file);
975 $_[1]->{'line'} = scalar(@$lref);
976 push(@$lref, &make_table_comment($_[1]->{'cmt'}));
977 push(@$lref, "$_[1]->{'name'}\t$_[1]->{'value'}");
978 $_[1]->{'eline'} = scalar(@$lref)-1;
979 &flush_file_lines($maps_file);
981 elsif ($maps_type eq "mysql") {
982 # Adding to a MySQL table
983 local $conf = &mysql_value_to_conf($maps_file);
984 local $dbh = &connect_mysql_db($conf);
985 ref($dbh) || &error($dbh);
986 local $cmd = $dbh->prepare("insert into ".$conf->{'table'}." ".
987 "(".$conf->{'where_field'}.",".
988 $conf->{'select_field'}.") values (".
990 if (!$cmd || !$cmd->execute($_[1]->{'name'}, $_[1]->{'value'})) {
991 &error(&text('mysql_eadd',
992 "<tt>".&html_escape($dbh->errstr)."</tt>"));
996 $_[1]->{'key'} = $_[1]->{'name'};
998 elsif ($maps_type eq "ldap") {
999 # Adding to an LDAP database
1000 local $conf = &ldap_value_to_conf($maps_file);
1001 local $ldap = &connect_ldap_db($conf);
1002 ref($ldap) || &error($ldap);
1003 local @classes = split(/\s+/, $config{'ldap_class'} ||
1004 "inetLocalMailRecipient");
1005 local @attrs = ( "objectClass", \@classes );
1006 local $name_attr = &get_ldap_key($conf);
1007 push(@attrs, $name_attr, $_[1]->{'name'});
1008 push(@attrs, $conf->{'result_attribute'} || "maildrop",
1010 push(@attrs, &split_props($config{'ldap_attrs'}));
1011 local $dn = &make_map_ldap_dn($_[1], $conf);
1012 if ($dn =~ /^([^=]+)=([^, ]+)/ && !&in_props(\@attrs, $1)) {
1013 push(@attrs, $1, $2);
1016 # Make sure the parent DN exists - for example, when adding a domain
1017 &ensure_ldap_parent($ldap, $dn);
1020 local $rv = $ldap->add($dn, attr => \@attrs);
1022 &error(&text('ldap_eadd', "<tt>$dn</tt>",
1023 "<tt>".&html_escape($rv->error)."</tt>"));
1025 $_[1]->{'dn'} = $dn;
1028 # Update the in-memory cache
1029 $_[1]->{'map_type'} = $maps_type;
1030 $_[1]->{'map_file'} = $maps_file;
1031 $_[1]->{'file'} = $maps_file;
1032 $_[1]->{'number'} = scalar(@{$maps_cache{$_[0]}});
1033 push(@{$maps_cache{$_[0]}}, $_[1]);
1037 # delete_mapping(map, &mapping)
1040 if (&file_map_type($_[1]->{'map_type'}) || !$_[1]->{'map_type'}) {
1041 # Deleting from a file
1042 local $lref = &read_file_lines($_[1]->{'map_file'});
1043 local $dl = $lref->[$_[1]->{'eline'}];
1044 local $len = $_[1]->{'eline'} - $_[1]->{'line'} + 1;
1045 if (($dl =~ /^\s*(\/[^\/]*\/[a-z]*)\s+([^#]*)/ ||
1046 $dl =~ /^\s*([^\s]+)\s+([^#]*)/) &&
1047 $1 eq $_[1]->{'name'}) {
1048 # Found a valid line to remove
1049 splice(@$lref, $_[1]->{'line'}, $len);
1052 print STDERR "Not deleting line $_[1]->{'line'} ",
1053 "from $_[1]->{'file'} for key ",
1054 "$_[1]->{'name'} which actually contains $dl\n";
1056 &flush_file_lines($_[1]->{'map_file'});
1057 &renumber_list($maps_cache{$_[0]}, $_[1], -$len);
1058 local $idx = &indexof($_[1], @{$maps_cache{$_[0]}});
1061 splice(@{$maps_cache{$_[0]}}, $idx, 1);
1064 elsif ($_[1]->{'map_type'} eq 'mysql') {
1065 # Deleting from MySQL
1066 local $conf = &mysql_value_to_conf($_[1]->{'map_file'});
1067 local $dbh = &connect_mysql_db($conf);
1068 ref($dbh) || &error($dbh);
1069 local $cmd = $dbh->prepare("delete from ".$conf->{'table'}.
1070 " where ".$conf->{'where_field'}." = ?".
1071 " ".$conf->{'additional_conditions'});
1072 if (!$cmd || !$cmd->execute($_[1]->{'key'})) {
1073 &error(&text('mysql_edelete',
1074 "<tt>".&html_escape($dbh->errstr)."</tt>"));
1079 elsif ($_[1]->{'map_type'} eq 'ldap') {
1080 # Deleting from LDAP
1081 local $conf = &ldap_value_to_conf($_[1]->{'map_file'});
1082 local $ldap = &connect_ldap_db($conf);
1083 ref($ldap) || &error($ldap);
1084 local $rv = $ldap->delete($_[1]->{'dn'});
1086 &error(&text('ldap_edelete', "<tt>$_[1]->{'dn'}</tt>",
1087 "<tt>".&html_escape($rv->error)."</tt>"));
1091 # Delete from in-memory cache
1092 local $idx = &indexof($_[1], @{$maps_cache{$_[0]}});
1093 splice(@{$maps_cache{$_[0]}}, $idx, 1) if ($idx != -1);
1097 # modify_mapping(map, &oldmapping, &newmapping)
1100 if (&file_map_type($_[1]->{'map_type'}) || !$_[1]->{'map_type'}) {
1101 # Modifying in a file
1102 local $lref = &read_file_lines($_[1]->{'map_file'});
1103 local $oldlen = $_[1]->{'eline'} - $_[1]->{'line'} + 1;
1105 push(@newlines, &make_table_comment($_[2]->{'cmt'}));
1106 push(@newlines, "$_[2]->{'name'}\t$_[2]->{'value'}");
1107 splice(@$lref, $_[1]->{'line'}, $oldlen, @newlines);
1108 &flush_file_lines($_[1]->{'map_file'});
1109 &renumber_list($maps_cache{$_[0]}, $_[1], scalar(@newlines)-$oldlen);
1110 local $idx = &indexof($_[1], @{$maps_cache{$_[0]}});
1113 $_[2]->{'map_file'} = $_[1]->{'map_file'};
1114 $_[2]->{'map_type'} = $_[1]->{'map_type'};
1115 $_[2]->{'line'} = $_[1]->{'line'};
1116 $_[2]->{'eline'} = $_[1]->{'eline'};
1117 $maps_cache{$_[0]}->[$idx] = $_[2];
1120 elsif ($_[1]->{'map_type'} eq 'mysql') {
1122 local $conf = &mysql_value_to_conf($_[1]->{'map_file'});
1123 local $dbh = &connect_mysql_db($conf);
1124 ref($dbh) || &error($dbh);
1125 local $cmd = $dbh->prepare("update ".$conf->{'table'}.
1126 " set ".$conf->{'where_field'}." = ?,".
1127 " ".$conf->{'select_field'}." = ?".
1128 " where ".$conf->{'where_field'}." = ?".
1129 " ".$conf->{'additional_conditions'});
1130 if (!$cmd || !$cmd->execute($_[2]->{'name'}, $_[2]->{'value'},
1132 &error(&text('mysql_eupdate',
1133 "<tt>".&html_escape($dbh->errstr)."</tt>"));
1138 elsif ($_[1]->{'map_type'} eq 'ldap') {
1140 local $conf = &ldap_value_to_conf($_[1]->{'map_file'});
1141 local $ldap = &connect_ldap_db($conf);
1142 ref($ldap) || &error($ldap);
1144 # Work out attribute changes
1146 local $name_attr = &get_ldap_key($conf);
1147 $replace{$name_attr} = [ $_[2]->{'name'} ];
1148 $replace{$conf->{'result_attribute'} || "maildrop"} =
1149 [ $_[2]->{'value'} ];
1151 # Work out new DN, if needed
1152 local $newdn = &make_map_ldap_dn($_[2], $conf);
1153 if ($_[1]->{'name'} ne $_[2]->{'name'} &&
1154 $_[1]->{'dn'} ne $newdn) {
1155 # Changed .. update the object in LDAP
1156 &ensure_ldap_parent($ldap, $newdn);
1157 local ($newprefix, $newrest) = split(/,/, $newdn, 2);
1158 local $rv = $ldap->moddn($_[1]->{'dn'},
1159 newrdn => $newprefix,
1160 newsuperior => $newrest);
1162 &error(&text('ldap_erename',
1163 "<tt>$_[1]->{'dn'}</tt>",
1165 "<tt>".&html_escape($rv->error)."</tt>"));
1167 $_[2]->{'dn'} = $newdn;
1168 if ($newdn =~ /^([^=]+)=([^, ]+)/) {
1169 $replace{$1} = [ $2 ];
1173 $_[2]->{'dn'} = $_[1]->{'dn'};
1177 local $rv = $ldap->modify($_[2]->{'dn'}, replace => \%replace);
1179 &error(&text('ldap_emodify',
1180 "<tt>$_[2]->{'dn'}</tt>",
1181 "<tt>".&html_escape($rv->error)."</tt>"));
1185 # Update in-memory cache
1186 local $idx = &indexof($_[1], @{$maps_cache{$_[0]}});
1187 $_[2]->{'map_file'} = $_[1]->{'map_file'};
1188 $_[2]->{'map_type'} = $_[1]->{'map_type'};
1189 $_[2]->{'file'} = $_[1]->{'file'};
1190 $_[2]->{'line'} = $_[1]->{'line'};
1191 $_[2]->{'eline'} = $_[2]->{'cmt'} ? $_[1]->{'line'}+1 : $_[1]->{'line'};
1192 $maps_cache{$_[0]}->[$idx] = $_[2] if ($idx != -1);
1195 # make_map_ldap_dn(&map, &conf)
1196 # Work out an LDAP DN for a map
1197 sub make_map_ldap_dn
1199 local ($map, $conf) = @_;
1201 local $scope = $conf->{'scope'} || 'sub';
1202 $scope = 'base' if (!$config{'ldap_doms'}); # Never create sub-domains
1203 local $id = $config{'ldap_id'} || 'cn';
1204 if ($map->{'name'} =~ /^(\S+)\@(\S+)$/ && $scope ne 'base') {
1206 $dn = "$id=$1,cn=$2,$conf->{'search_base'}";
1208 elsif ($map->{'name'} =~ /^\@(\S+)$/ && $scope ne 'base') {
1210 $dn = "$id=default,cn=$1,$conf->{'search_base'}";
1214 $dn = "$id=$map->{'name'},$conf->{'search_base'}";
1219 # get_ldap_key(&config)
1220 # Returns the attribute name for the LDAP key. May call &error
1224 local ($filter, $name_attr) = @_;
1225 if ($conf->{'query_filter'}) {
1226 $filter = $conf->{'query_filter'};
1227 $conf->{'query_filter'} =~ /([a-z0-9]+)=\%[su]/i ||
1228 &error("Could not get attribute from ".
1229 $conf->{'query_filter'});
1231 $filter = "($filter)" if ($filter !~ /^\(/);
1232 $filter =~ s/\%s/\*/g;
1235 $filter = "(mailacceptinggeneralid=*)";
1236 $name_attr = "mailacceptinggeneralid";
1238 return wantarray ? ( $name_attr, $filter ) : $name_attr;
1241 # ensure_ldap_parent(&ldap, dn)
1242 # Create the parent of some DN if needed
1243 sub ensure_ldap_parent
1245 local ($ldap, $dn) = @_;
1247 $pdn =~ s/^([^,]+),//;
1248 local $rv = $ldap->search(base => $pdn, scope => 'base',
1249 filter => "(objectClass=top)",
1251 if (!$rv || $rv->code || !$rv->all_entries) {
1252 # Does not .. so add it
1253 local @pclasses = ( "top" );
1254 local @pattrs = ( "objectClass", \@pclasses );
1255 local $rv = $ldap->add($pdn, attr => \@pattrs);
1259 # init_new_mapping($maps_parameter) : $number
1260 # gives a new number of mapping
1261 sub init_new_mapping
1263 my $maps = &get_maps($_[0]);
1265 foreach $trans (@{$maps}) {
1266 if ($trans->{'number'} > $max_number) {
1267 $max_number = $trans->{'number'};
1270 return $max_number+1;
1273 # postfix_mail_file(user)
1274 sub postfix_mail_file
1276 local @s = &postfix_mail_system();
1278 return "$s[1]/$_[0]";
1281 return "$_[7]/$s[1]";
1284 local @u = getpwnam($_[0]);
1285 return "$u[7]/$s[1]";
1289 # postfix_mail_system()
1290 # Returns 0 and the spool dir for sendmail style,
1291 # 1 and the mbox filename for ~/Mailbox style
1292 # 2 and the maildir name for ~/Maildir style
1293 sub postfix_mail_system
1295 if (!scalar(@mail_system_cache)) {
1296 local $home_mailbox = &get_current_value("home_mailbox");
1297 if ($home_mailbox) {
1298 @mail_system_cache = $home_mailbox =~ /^(.*)\/$/ ?
1299 (2, $1) : (1, $home_mailbox);
1302 local $mail_spool_directory =
1303 &get_current_value("mail_spool_directory");
1304 @mail_system_cache = (0, $mail_spool_directory);
1307 return wantarray ? @mail_system_cache : $mail_system_cache[0];
1310 # list_queue([error-on-failure])
1311 # Returns a list of strutures, each containing details of one queued message
1314 local ($throw) = @_;
1316 local $out = &backquote_command("$config{'mailq_cmd'} 2>&1 </dev/null");
1317 &error("$config{'mailq_cmd'} failed : ".&html_escape($out)) if ($? && $throw);
1318 foreach my $l (split(/\r?\n/, $out)) {
1319 next if ($l =~ /^(\S+)\s+is\s+empty/i ||
1320 $l =~ /^\s+Total\s+requests:/i);
1321 if ($l =~ /^([^\s\*\!]+)[\*\!]?\s*(\d+)\s+(\S+\s+\S+\s+\d+\s+\d+:\d+:\d+)\s+(.*)/) {
1322 local $q = { 'id' => $1, 'size' => $2,
1323 'date' => $3, 'from' => $4 };
1324 if (defined(&parse_mail_date)) {
1325 local $t = &parse_mail_date($q->{'date'});
1327 $q->{'date'} = &make_date($t, 0, 'yyyy/mm/dd');
1333 elsif ($l =~ /\((.*)\)/ && @qfiles) {
1334 $qfiles[$#qfiles]->{'status'} = $1;
1336 elsif ($l =~ /^\s+(\S+)/ && @qfiles) {
1337 $qfiles[$#qfiles]->{'to'} .= "$1 ";
1343 # parse_queue_file(id)
1344 # Parses a postfix mail queue file into a standard mail structure
1345 sub parse_queue_file
1347 local @qfiles = ( &recurse_files("$config{'mailq_dir'}/active"),
1348 &recurse_files("$config{'mailq_dir'}/incoming"),
1349 &recurse_files("$config{'mailq_dir'}/deferred"),
1350 &recurse_files("$config{'mailq_dir'}/corrupt"),
1351 &recurse_files("$config{'mailq_dir'}/hold"),
1352 &recurse_files("$config{'mailq_dir'}/maildrop"),
1355 local ($file) = grep { $_ =~ /\/$f$/ } @qfiles;
1356 return undef if (!$file);
1358 local ($mail, @headers);
1359 &open_execute_command(QUEUE, "$config{'postcat_cmd'} ".quotemeta($file), 1, 1);
1361 if (/^\*\*\*\s+MESSAGE\s+CONTENTS/ && !$mode) { # Start of headers
1364 elsif (/^\*\*\*\s+HEADER\s+EXTRACTED/ && $mode) { # End of email
1367 elsif ($mode == 1 && /^\s*$/) { # End of headers
1370 elsif ($mode == 1 && /^(\S+):\s*(.*)/) { # Found a header
1371 push(@headers, [ $1, $2 ]);
1373 elsif ($mode == 1 && /^(\s+.*)/) { # Header continuation
1374 $headers[$#headers]->[1] .= $1 unless($#headers < 0);
1376 elsif ($mode == 2) { # Part of body
1377 $mail->{'size'} += length($_);
1378 $mail->{'body'} .= $_;
1382 $mail->{'headers'} = \@headers;
1383 foreach $h (@headers) {
1384 $mail->{'header'}->{lc($h->[0])} = $h->[1];
1389 # recurse_files(dir)
1392 opendir(DIR, &translate_filename($_[0])) || return ( $_[0] );
1393 local @dir = readdir(DIR);
1397 push(@rv, &recurse_files("$_[0]/$f")) if ($f !~ /^\./);
1404 local ($a1, $a2, $b1, $b2);
1405 if ($a->{'name'} =~ /^(.*)\@(.*)$/ && (($a1, $a2) = ($1, $2)) &&
1406 $b->{'name'} =~ /^(.*)\@(.*)$/ && (($b1, $b2) = ($1, $2))) {
1407 return $a2 cmp $b2 ? $a2 cmp $b2 : $a1 cmp $b1;
1410 return $a->{'name'} cmp $b->{'name'};
1415 # Copy the postfix config file to a backup file, for reversion if
1416 # a post-save check fails
1419 if ($config{'check_config'} && !defined($save_file)) {
1420 $save_file = &transname();
1421 &execute_command("cp $config{'postfix_config_file'} $save_file");
1427 if (defined($save_file)) {
1428 local $err = &check_postfix();
1430 &execute_command("mv $save_file $config{'postfix_config_file'}");
1431 &error(&text('after_err', "<pre>$err</pre>"));
1440 # get_real_value(parameter_name)
1441 # Returns the value of a parameter, with $ substitions done
1444 my $v = &get_current_value($_[0]);
1445 $v =~ s/\$(\{([^\}]+)\}|([A-Za-z0-9\.\-\_]+))/get_real_value($2 || $3)/ge;
1450 # Create some map text file, if needed
1453 foreach my $mf (&get_maps_files(&get_real_value($_[0]))) {
1454 if ($mf =~ /^\// && !-e $mf) {
1455 &open_lock_tempfile(TOUCH, ">$mf", 1) ||
1456 &error(&text("efilewrite", $mf, $!));
1457 &close_tempfile(TOUCH);
1458 &set_ownership_permissions(undef, undef, 0755, $mf);
1463 # Functions for editing the header_checks map nicely
1464 sub edit_name_header_checks
1466 return &ui_table_row($text{'header_name'},
1467 &ui_textbox("name", $_[0]->{'name'}, 60));
1470 sub parse_name_header_checks
1472 $_[1]->{'name'} =~ /^\/.*\S.*\/[a-z]*$/ || &error($text{'header_ename'});
1473 return $_[1]->{'name'};
1476 sub edit_value_header_checks
1478 local ($act, $dest) = split(/\s+/, $_[0]->{'value'}, 2);
1479 return &ui_table_row($text{'header_value'},
1480 &ui_select("action", $act,
1481 [ map { [ $_, $text{'header_'.lc($_)} ] }
1482 @header_checks_actions ], 0, 0, $act)."\n".
1483 &ui_textbox("value", $dest, 40));
1486 sub parse_value_header_checks
1488 local $rv = $_[1]->{'action'};
1489 if ($_[1]->{'value'}) {
1490 $rv .= " ".$_[1]->{'value'};
1495 # Functions for editing the body_checks map (same as header_checks)
1496 sub edit_name_body_checks
1498 return &edit_name_header_checks(@_);
1501 sub parse_name_body_checks
1503 return &parse_name_header_checks(@_);
1506 sub edit_value_body_checks
1508 return &edit_value_header_checks(@_);
1511 sub parse_value_body_checks
1513 return &parse_value_header_checks(@_);
1516 ## added function for sender_access_maps
1517 ## added function for client_access_maps
1518 sub edit_name_check_sender_access
1520 return "<td><b>$text{'access_addresses'}</b></td>\n".
1521 "<td>".&ui_textbox("name", $_[0]->{'name'},40)."</td>\n";
1524 sub edit_value_check_sender_access
1526 local ($act, $dest) = split(/\s+/, $_[0]->{'value'}, 2);
1527 return "<td><b>$text{'header_value'}</b></td>\n".
1528 "<td>".&ui_select("action", $act,
1529 [ map { [ $_, $text{'header_'.lc($_)} ] }
1530 @check_sender_actions ], 0, 0, $act)."\n".
1531 &ui_textbox("value", $dest, 40)."</td>\n";
1534 sub parse_value_check_sender_access
1536 return &parse_value_header_checks(@_);
1539 @header_checks_actions = ( "REJECT", "HOLD", "REDIRECT", "DUNNO", "IGNORE",
1540 "DISCARD", "FILTER",
1541 "PREPEND", "REPLACE", "WARN" );
1543 @check_sender_actions = ( "OK", "REJECT", "DISCARD", "FILTER", "PREPEND",
1544 "REDIRECT", "WARN", "DUNNO" );
1546 # get_master_config()
1547 # Returns an array reference of entries from the Postfix master.cf file
1548 sub get_master_config
1550 if (!scalar(@master_config_cache)) {
1551 @master_config_cache = ( );
1554 open(MASTER, $config{'postfix_master'});
1557 if (/^(#?)\s*(\S+)\s+(inet|unix|fifo)\s+(y|n|\-)\s+(y|n|\-)\s+(y|n|\-)\s+(\S+)\s+(\S+)\s+(.*)$/) {
1559 $prog = { 'enabled' => !$1,
1571 push(@master_config_cache, $prog);
1573 elsif (/^(#?)\s+(.*)$/ && $prog &&
1574 $prog->{'eline'} == $lnum-1 &&
1575 $prog->{'enabled'} == !$1) {
1577 $prog->{'command'} .= " ".$2;
1578 $prog->{'eline'} = $lnum;
1584 return \@master_config_cache;
1587 # create_master(&master)
1588 # Adds a new Postfix server process
1591 local ($master) = @_;
1592 local $conf = &get_master_config();
1593 local $lref = &read_file_lines($config{'postfix_master'});
1594 push(@$lref, &master_line($master));
1595 &flush_file_lines($config{'postfix_master'});
1596 $master->{'line'} = scalar(@$lref)-1;
1597 $master->{'eline'} = scalar(@$lref)-1;
1598 push(@$conf, $master);
1601 # delete_master(&master)
1602 # Removes one Postfix server process
1605 local ($master) = @_;
1606 local $conf = &get_master_config();
1607 local $lref = &read_file_lines($config{'postfix_master'});
1608 local $lines = $master->{'eline'} - $master->{'line'} + 1;
1609 splice(@$lref, $master->{'line'}, $lines);
1610 &flush_file_lines($config{'postfix_master'});
1611 @$conf = grep { $_ ne $master } @$conf;
1612 foreach my $c (@$conf) {
1613 if ($c->{'line'} > $master->{'eline'}) {
1614 $c->{'line'} -= $lines;
1615 $c->{'eline'} -= $lines;
1620 # modify_master(&master)
1621 # Updates one Postfix server process
1624 local ($master) = @_;
1625 local $conf = &get_master_config();
1626 local $lref = &read_file_lines($config{'postfix_master'});
1627 local $lines = $master->{'eline'} - $master->{'line'} + 1;
1628 splice(@$lref, $master->{'line'}, $lines,
1629 &master_line($master));
1630 &flush_file_lines($config{'postfix_master'});
1631 foreach my $c (@$conf) {
1632 if ($c->{'line'} > $master->{'eline'}) {
1633 $c->{'line'} -= $lines-1;
1634 $c->{'eline'} -= $lines-1;
1639 # master_line(&master)
1643 return ($prog->{'enabled'} ? "" : "#").
1644 join("\t", $prog->{'name'}, $prog->{'type'}, $prog->{'private'},
1645 $prog->{'unpriv'}, $prog->{'chroot'}, $prog->{'wakeup'},
1646 $prog->{'maxprocs'}, $prog->{'command'});
1649 sub redirect_to_map_list
1651 local ($map_name) = @_;
1652 if ($map_name =~ /transport/) { &redirect("transport.cgi"); }
1653 elsif ($map_name =~ /canonical/) { &redirect("canonical.cgi"); }
1654 elsif ($map_name =~ /virtual/) { &redirect("virtual.cgi"); }
1655 elsif ($map_name =~ /relocated/) { &redirect("relocated.cgi"); }
1656 elsif ($map_name =~ /header/) { &redirect("header.cgi"); }
1657 elsif ($map_name =~ /body/) { &redirect("body.cgi"); }
1658 elsif ($map_name =~ /sender_bcc/) { &redirect("bcc.cgi?mode=sender"); }
1659 elsif ($map_name =~ /recipient_bcc/) { &redirect("bcc.cgi?mode=recipient"); }
1660 elsif ($map_name =~ /^smtpd_client_restrictions:/) { &redirect("client.cgi"); }
1661 elsif ($map_name =~ /relay_recipient_maps/) { &redirect("smtpd.cgi"); }
1662 else { &redirect(""); }
1665 sub regenerate_map_table
1667 local ($map_name) = @_;
1668 if ($map_name =~ /canonical/) { ®enerate_canonical_table(); }
1669 if ($map_name =~ /relocated/) { ®enerate_relocated_table(); }
1670 if ($map_name =~ /virtual/) { ®enerate_virtual_table(); }
1671 if ($map_name =~ /transport/) { ®enerate_transport_table(); }
1672 if ($map_name =~ /sender_access/) { ®enerate_any_table($map_name); }
1673 if ($map_name =~ /sender_bcc/) { ®enerate_bcc_table(); }
1674 if ($map_name =~ /recipient_bcc/) { ®enerate_recipient_bcc_table(); }
1675 if ($map_name =~ /smtpd_client_restrictions:(\S+)/) {
1676 ®enerate_any_table("smtpd_client_restrictions",
1679 if ($map_name =~ /relay_recipient_maps/) {
1680 ®enerate_relay_recipient_table();
1684 # mailq_table(&qfiles)
1685 # Print a table of queued mail messages
1688 local ($qfiles) = @_;
1692 foreach my $q (@$qfiles) {
1694 push(@cols, { 'type' => 'checkbox', 'name' => 'file',
1695 'value' => $q->{'id'} });
1696 push(@cols, "<a href='view_mailq.cgi?id=$q->{'id'}'>$q->{'id'}</a>");
1697 local $size = &nice_size($q->{'size'});
1698 push(@cols, "<font size=1>$q->{'date'}</font>");
1699 push(@cols, "<font size=1>".&html_escape($q->{'from'})."</font>");
1700 push(@cols, "<font size=1>".&html_escape($q->{'to'})."</font>");
1701 push(@cols, "<font size=1>$size</font>");
1702 push(@cols, "<font size=1>".&html_escape($q->{'status'})."</font>");
1703 push(@table, \@cols);
1706 # Show the table and form
1707 print &ui_form_columns_table("delete_queues.cgi",
1708 [ [ undef, $text{'mailq_delete'} ],
1709 $postfix_version >= 1.1 ? ( [ 'move', $text{'mailq_move'} ] ) : ( ),
1710 $postfix_version >= 2 ? ( [ 'hold', $text{'mailq_hold'} ],
1711 [ 'unhold', $text{'mailq_unhold'} ] ) : ( ),
1716 [ "", $text{'mailq_id'}, $text{'mailq_date'}, $text{'mailq_from'},
1717 $text{'mailq_to'}, $text{'mailq_size'}, $text{'mailq_status'} ],
1722 # is_table_comment(line, [force-prefix])
1723 # Returns the comment text if a line contains a comment, like # foo
1724 sub is_table_comment
1726 local ($line, $force) = @_;
1727 if ($config{'prefix_cmts'} || $force) {
1728 return $line =~ /^\s*#+\s*Webmin:\s*(.*)/ ? $1 : undef;
1731 return $line =~ /^\s*#+\s*(.*)/ ? $1 : undef;
1735 # make_table_comment(comment, [force-tag])
1736 # Returns an array of lines for a comment in a map file, like # foo
1737 sub make_table_comment
1739 local ($cmt, $force) = @_;
1743 elsif ($config{'prefix_cmts'} || $force) {
1744 return ( "# Webmin: $cmt" );
1747 return ( "# $cmt" );
1751 # lock_postfix_files()
1752 # Lock all Postfix config files
1753 sub lock_postfix_files
1755 &lock_file($config{'postfix_config_file'});
1756 &lock_file($config{'postfix_master'});
1759 # unlock_postfix_files()
1760 # Un-lock all Postfix config files
1761 sub unlock_postfix_files
1763 &unlock_file($config{'postfix_config_file'});
1764 &unlock_file($config{'postfix_master'});
1767 # map_chooser_button(field, mapname)
1768 # Returns HTML for a button for popping up a map file chooser
1769 sub map_chooser_button
1771 local ($name, $mapname) = @_;
1772 return &popup_window_button("map_chooser.cgi?mapname=$mapname", 1024, 600, 1,
1773 [ [ "ifield", $name, "map" ] ]);
1776 # get_maps_types_files(value)
1777 # Converts a parameter like hash:/foo/bar,hash:/tmp/xxx to a list of types
1779 sub get_maps_types_files
1781 $_[0] =~ /^\s*([^:]+):(\/[^,\s]*),?(.*)/ || return ( );
1782 (my $returntype, $returnvalue, my $recurse) = ( $1, $2, $3 );
1784 return ( [ $returntype, $returnvalue ],
1785 &get_maps_types_files($recurse) );
1788 # list_mysql_sources()
1789 # Returns a list of global MySQL source names in main.cf
1790 sub list_mysql_sources
1793 my $lref = &read_file_lines($config{'postfix_config_file'});
1794 foreach my $l (@$lref) {
1795 if ($l =~ /^\s*(\S+)_dbname\s*=/) {
1802 # get_backend_config(file)
1803 # Returns a hash ref from names to values in some backend (ie. mysql or ldap)
1805 sub get_backend_config
1809 local $lref = &read_file_lines($file, 1);
1810 foreach my $l (@$lref) {
1811 if ($l =~ /^\s*([a-z0-9\_]+)\s*=\s*(.*)/i) {
1818 # save_backend_config(file, name, [value])
1819 # Updates one setting in a backend config file
1820 sub save_backend_config
1822 local ($file, $name, $value) = @_;
1823 local $lref = &read_file_lines($file);
1825 for(my $i=0; $i<@$lref; $i++) {
1826 if ($lref->[$i] =~ /^\s*([a-z0-9\_]+)\s*=\s*(.*)/i &&
1828 # Found the line to fix
1829 if (defined($value)) {
1830 $lref->[$i] = "$name = $value";
1833 splice(@$lref, $i, 1);
1839 if (!$found && defined($value)) {
1840 push(@$lref, "$name = $value");
1844 # can_access_map(type, value)
1845 # Checks if some map (such as a database) can be accessed
1848 local ($type, $value) = @_;
1849 if (&file_map_type($type)) {
1850 return undef; # Always can
1852 elsif ($type eq "mysql") {
1853 # Parse config, connect to DB
1855 if ($value =~ /^[\/\.]/) {
1857 local $cfile = $value;
1858 if ($cfile !~ /^\//) {
1859 $cfile = &guess_config_dir()."/".$cfile;
1861 -r $cfile || return &text('mysql_ecfile', "<tt>$cfile</tt>");
1862 $conf = &get_backend_config($cfile);
1866 $conf = &mysql_value_to_conf($value);
1867 $conf->{'dbname'} || return &text('mysql_esource', $value);
1870 # Do we have the field and table info?
1871 foreach my $need ('table', 'select_field', 'where_field') {
1872 $conf->{$need} || return &text('mysql_eneed', $need);
1875 # Try a connect, and a query
1876 local $dbh = &connect_mysql_db($conf);
1880 local $cmd = $dbh->prepare("select ".$conf->{'select_field'}." ".
1881 "from ".$conf->{'table'}." ".
1882 "where ".$conf->{'where_field'}." = ".
1883 $conf->{'where_field'}." ".
1885 if (!$cmd || !$cmd->execute()) {
1886 return &text('mysql_equery',
1887 "<tt>".$conf->{'table'}."</tt>",
1888 "<tt>".&html_escape($dbh->errstr)."</tt>");
1894 elsif ($type eq "ldap") {
1895 # Parse config, connect to LDAP server
1896 local $conf = &ldap_value_to_conf($value);
1897 $conf->{'search_base'} || return &text('ldap_esource', $value);
1899 # Try a connect and a search
1900 local $ldap = &connect_ldap_db($conf);
1904 local @classes = split(/\s+/, $config{'ldap_class'} ||
1905 "inetLocalMailRecipient");
1906 local $rv = $ldap->search(base => $conf->{'search_base'},
1907 filter => "(objectClass=$classes[0])",
1909 if (!$rv || $rv->code && !$rv->all_entries) {
1910 return &text('ldap_ebase', "<tt>$conf->{'search_base'}</tt>",
1911 $rv ? $rv->error : "Unknown search error");
1917 return &text('map_unknown', "<tt>$type</tt>");
1921 # connect_mysql_db(&config)
1922 # Attempts to connect to the Postfix MySQL database. Returns
1923 # a driver handle on success, or an error message string on failure.
1924 sub connect_mysql_db
1927 local $driver = "mysql";
1931 \$drh = DBI->install_driver(\$driver);
1934 return &text('mysql_edriver', "<tt>DBD::$driver</tt>");
1936 local @hosts = split(/\s+/, $config{'mysql_hosts'} || $conf->{'hosts'});
1937 @hosts = ( undef ) if (!@hosts); # Localhost only
1939 foreach my $host (@hosts) {
1940 local $dbistr = "database=$conf->{'dbname'}";
1941 if ($host =~ /^unix:(.*)$/) {
1943 $dbistr .= ";mysql_socket=$1";
1947 $dbistr .= ";host=$host";
1949 $dbh = $drh->connect($dbistr,
1950 $config{'mysql_user'} || $conf->{'user'},
1951 $config{'mysql_pass'} || $conf->{'password'},
1955 $dbh || return &text('mysql_elogin',
1956 "<tt>$conf->{'dbname'}</tt>", $drh->errstr)."\n";
1960 # connect_ldap_db(&config)
1961 # Attempts to connect to an LDAP server with Postfix maps. Returns
1962 # a driver handle on success, or an error message string on failure.
1966 if (defined($connect_ldap_db_cache)) {
1967 return $connect_ldap_db_cache;
1969 eval "use Net::LDAP";
1971 return &text('ldap_eldapmod', "<tt>Net::LDAP</tt>");
1973 local @servers = split(/\s+/, $config{'ldap_host'} ||
1974 $conf->{'server_host'} || "localhost");
1975 local ($ldap, $lasterr);
1976 foreach my $server (@servers) {
1977 local ($host, $port, $tls);
1978 if ($server =~ /^(\S+):(\d+)$/) {
1980 ($host, $port) = ($1, $2);
1981 $tls = $conf->{'start_tls'} eq 'yes';
1983 elsif ($server =~ /^(ldap|ldaps):\/\/(\S+)(:(\d+))?/) {
1986 $port = $4 || $conf->{'server_port'} || 389;
1987 $tls = $1 eq "ldaps";
1992 $port = $conf->{'server_port'} || 389;
1993 $tls = $conf->{'start_tls'} eq 'yes';
1995 $ldap = Net::LDAP->new($server, port => $port);
1997 $lasterr = &text('ldap_eldap', "<tt>$server</tt>", $port);
2003 if ($conf->{'bind'} eq 'yes' || $config{'ldap_user'}) {
2004 local $mesg = $ldap->bind(
2005 dn => $config{'ldap_user'} || $conf->{'bind_dn'},
2006 password => $config{'ldap_pass'} || $conf->{'bind_pw'});
2007 if (!$mesg || $mesg->code) {
2008 $lasterr = &text('ldap_eldaplogin',
2010 "<tt>".($config{'ldap_user'} ||
2011 $conf->{'bind_dn'})."</tt>",
2012 $mesg ? $mesg->error : "Unknown error");
2021 $connect_ldap_db_cache = $ldap;
2029 # mysql_value_to_conf(value)
2030 # Converts a MySQL config file or source name to a config hash ref
2031 sub mysql_value_to_conf
2033 local ($value) = @_;
2035 if ($value =~ /^[\/\.]/) {
2037 local $cfile = $value;
2038 if ($cfile !~ /^\//) {
2039 $cfile = &guess_config_dir()."/".$cfile;
2041 -r $cfile || &error(&text('mysql_ecfile', "<tt>$cfile</tt>"));
2042 $conf = &get_backend_config($cfile);
2047 foreach my $k ("hosts", "dbname", "user", "password", "query",
2048 "table", "where_field", "select_field",
2049 "additional_conditions") {
2050 local $v = &get_real_value($value."_".$k);
2057 # ldap_value_to_conf(value)
2058 # Converts an LDAP config file name to a config hash ref
2059 sub ldap_value_to_conf
2061 local ($value) = @_;
2063 local $cfile = $value;
2064 if ($cfile !~ /^\//) {
2065 $cfile = &guess_config_dir()."/".$cfile;
2067 -r $cfile && !-d $cfile || &error(&text('ldap_ecfile', "<tt>$cfile</tt>"));
2068 return &get_backend_config($cfile);
2071 # can_map_comments(name)
2072 # Returns 1 if some map can have comments. Not allowed for MySQL and LDAP.
2073 sub can_map_comments
2076 foreach my $tv (&get_maps_types_files(&get_real_value($name))) {
2077 return 0 if (!&file_map_type($tv->[0]));
2082 # can_map_manual(name)
2083 # Returns 1 if osme map has a file that can be manually edited
2087 foreach my $tv (&get_maps_types_files(&get_real_value($name))) {
2088 return 0 if (!&file_map_type($tv->[0]));
2093 # supports_map_type(type)
2094 # Returns 1 if a map of some type is supported by Postfix
2095 sub supports_map_type
2098 return 1 if ($type eq 'hash'); # Assume always supported
2099 if (!scalar(@supports_map_type_cache)) {
2100 @supports_map_type = ( );
2101 open(POSTCONF, "$config{'postfix_config_command'} -m |");
2104 push(@supports_map_type_cache, $_);
2108 return &indexoflc($type, @supports_map_type_cache) >= 0;
2112 # Converts multiple lines of text into LDAP attributes
2117 foreach $p (split(/\t+/, $text)) {
2118 if ($p =~ /^(\S+):\s*(.*)/) {
2119 push(@{$pmap{$1}}, $2);
2124 foreach $k (keys %pmap) {
2125 local $v = $pmap{$k};
2127 push(@rv, $k, $v->[0]);
2136 # list_smtpd_restrictions()
2137 # Returns a list of SMTP server restrictions known to Webmin
2138 sub list_smtpd_restrictions
2140 return ( "permit_mynetworks",
2141 "permit_inet_interfaces",
2142 $postfix_version < 2.3 ? "reject_unknown_client"
2143 : "reject_unknown_reverse_client_hostname",
2144 "permit_sasl_authenticated",
2145 "reject_unauth_destination",
2146 "check_relay_domains",
2147 "permit_mx_backup" );
2150 # list_client_restrictions()
2151 # Returns a list of boolean values for use in smtpd_client_restrictions
2152 sub list_client_restrictions
2154 return ( "permit_mynetworks",
2155 "permit_inet_interfaces",
2156 $postfix_version < 2.3 ? "reject_unknown_client"
2157 : "reject_unknown_reverse_client_hostname",
2158 "permit_tls_all_clientcerts",
2162 # list_multi_client_restrictions()
2163 # Returns a list of restrictions that have a following value
2164 sub list_multi_client_restrictions
2166 return ( "check_client_access",
2167 "reject_rbl_client",
2168 "reject_rhsbl_client",
2175 return 1 if ($type eq 'hash' || $type eq 'regexp' || $type eq 'pcre' ||
2176 $type eq 'btree' || $type eq 'dbm' || $type eq 'cidr');
2179 # in_props(&props, name)
2180 # Looks up the value of a named property in a list
2183 local ($props, $name) = @_;
2184 for(my $i=0; $i<@$props; $i++) {
2185 if (lc($props->[$i]) eq lc($name)) {
2186 return $props->[$i+1];
2192 # For calling from aliases-lib only