<html><head><meta name="color-scheme" content="light dark"></head><body><pre style="word-wrap: break-word; white-space: pre-wrap;"># postfix-lib.pl
#
# postfix-module by Guillaume Cottenceau &lt;gc@mandrakesoft.com&gt;,
# for webmin by Jamie Cameron

$POSTFIX_MODULE_VERSION = 5;

BEGIN { push(@INC, ".."); };
use WebminCore;
&amp;init_config();
%access = &amp;get_module_acl();
$access{'postfinger'} = 0 if (&amp;is_readonly_mode());
do 'aliases-lib.pl';

$config{'perpage'} ||= 20;      # a value of 0 can cause problems

# Get the saved version number
$version_file = "$module_config_directory/version";
if (&amp;open_readfile(VERSION, $version_file)) {
	chop($postfix_version = &lt;VERSION&gt;);
	close(VERSION);
	my @vst = stat($version_file);
	my @cst = stat(&amp;has_command($config{'postfix_config_command'}));
	if (@cst &amp;&amp; $cst[9] &gt; $vst[9]) {
		# Postfix was probably upgraded
		$postfix_version = undef;
		}
	}

if (!$postfix_version) {
	# Not there .. work it out
	if (&amp;has_command($config{'postfix_config_command'}) &amp;&amp;
	    &amp;backquote_command("$config{'postfix_config_command'} -d mail_version 2&gt;&amp;1", 1) =~ /mail_version\s*=\s*(.*)/) {
		# Got the version
		$postfix_version = $1;
		}

	# And save for other callers
	&amp;open_tempfile(VERSION, "&gt;$version_file", 0, 1);
	&amp;print_tempfile(VERSION, "$postfix_version\n");
	&amp;close_tempfile(VERSION);
	}

if (&amp;compare_version_numbers($postfix_version, 2) &gt;= 0) {
	$virtual_maps = "virtual_alias_maps";
	$ldap_timeout = "ldap_timeout";
	}
else {
	$virtual_maps = "virtual_maps";
	$ldap_timeout = "ldap_lookup_timeout";
	}


sub guess_config_dir
{
    my $answ = $config{'postfix_config_file'};
    $answ =~ /(.*)\/[^\/]*/;
    return $1;
}

$config_dir = guess_config_dir();


## DOC: compared to other webmin modules, here we don't need to parse
##      the config file, because a config command is provided by
##      postfix to read and write the config parameters


# postfix_module_version()
# returns the version of the postfix module
sub postfix_module_version
{
    return $POSTFIX_MODULE_VERSION;
}

# is_postfix_running()
# returns 1 if running, 0 if stopped, calls error() if problem
sub is_postfix_running
{
    my $queuedir = get_current_value("queue_directory");
    my $processid = get_current_value("process_id_directory");

    my $pid_file = $queuedir."/".$processid."/master.pid";
    my $pid = &amp;check_pid_file($pid_file);
    return $pid ? 1 : 0;
}


sub is_existing_parameter
{
    my $out = &amp;backquote_command("$config{'postfix_config_command'} -c $config_dir $_[0] 2&gt;&amp;1", 1);
    return !($out =~ /unknown parameter/);
}


# get_current_value(parameter_name)
# returns a scalar corresponding to the value of the parameter
## modified to allow main_parameter:subparameter 
sub get_current_value
{
# First try to get the value from main.cf directly
my ($name,$key)=split /:/,$_[0];
my $lref = &amp;read_file_lines($config{'postfix_config_file'});
my $out;
my ($begin_flag, $end_flag);
foreach my $l (@$lref) {
	# changes made to this loop by Dan Hartman of Rae Internet /
	# Message Partners for multi-line parsing 2007-06-04
	if ($begin_flag == 1 &amp;&amp; $l =~ /\S/ &amp;&amp; $l =~ /^(\s+[^#].+)/) {
		# non-comment continuation line, and replace tabs with spaces
		$out .= $1;
		$out =~ s/^\s+/ /;
		}
 	if ($l =~ /^\s*([a-z0-9\_]+)\s*=\s*(.*)|^\s*([a-z0-9\_]+)\s*=\s*$/ &amp;&amp;
 	    $1 . $3 eq $name) {
		# Found the one we're looking for, set a flag
		$out = $2;
		$begin_flag = 1;
		}
 	if ($l =~ /^\s*([a-z0-9\_]+)\s*=\s*(.*)|^\s*([a-z0-9\_]+)\s*=\s*$/ &amp;&amp;
 	    $1 . $3 ne $name &amp;&amp; $begin_flag == 1) {
		# after the beginning, another configuration variable
		# found!  Stop!
		$end_flag = 1;
		last;
		}
	}
if (!defined($out)) {
	# Fall back to asking Postfix
	# -h tells postconf not to output the name of the parameter
	$out = &amp;backquote_command("$config{'postfix_config_command'} -c $config_dir -h ".
				  quotemeta($name)." 2&gt;/dev/null", 1);
	if ($?) {
		&amp;error(&amp;text('query_get_efailed', $name, $out));
		}
	elsif ($out =~ /warning:.*unknown\s+parameter/) {
		return undef;
		}
	chop($out);
	}
else {
	# Trim trailing whitespace
	$out =~ s/\s+$//;
	}
if ($key) {
	# If the value asked for was like foo:bar, extract from the value
	# the parts after bar
	my @res = ( );
        while($out =~ /^(.*?)\Q$key\E\s+(\S+)(.*)$/) {
		my $v = $2;
		$out = $3;
		$v =~ s/,$//;
		push(@res, $v);
		}
	return join(" ", @res);
	}
return $out;
}

# if_default_value(parameter_name)
# returns if the value is the default value
sub if_default_value
{
    my $out = &amp;backquote_command("$config{'postfix_config_command'} -c $config_dir -n $_[0] 2&gt;&amp;1", 1);
    if ($?) { &amp;error(&amp;text('query_get_efailed', $_[0], $out)); }
    return ($out eq "");
}

# get_default_value(parameter_name)
# returns the default value of the parameter
sub get_default_value
{
    my $out = &amp;backquote_command("$config{'postfix_config_command'} -c $config_dir -dh $_[0] 2&gt;&amp;1", 1);  # -h tells postconf not to output the name of the parameter
    if ($?) { &amp;error(&amp;text('query_get_efailed', $_[0], $out)); }
    chop($out);
    return $out;
}


# set_current_value(parameter_name, parameter_value, [always-set])
# Update some value in the Postfix configuration file
sub set_current_value
{
    my $value = $_[1];
    if ($value eq "__DEFAULT_VALUE_IE_NOT_IN_CONFIG_FILE__" ||
	$value eq &amp;get_default_value($_[0]) &amp;&amp; !$_[2])
    {
	# there is a special case in which there is no static default value ;
	# postfix will handle it correctly if I remove the line in `main.cf'
	my $all_lines = &amp;read_file_lines($config{'postfix_config_file'});
	my $line_of_parameter = -1;
	my $end_line_of_parameter = -1;
	my $i = 0;

	foreach (@{$all_lines})
	{
	    if (/^\s*$_[0]\s*=/) {
		$line_of_parameter = $i;
		$end_line_of_parameter = $i;
	    } elsif ($line_of_parameter &gt;= 0 &amp;&amp;
		     /^\t+\S/) {
		# Multi-line continuation
		$end_line_of_parameter = $i;
	    }
	    $i++;
	}

	if ($line_of_parameter != -1) {
	    splice(@{$all_lines}, $line_of_parameter,
		   $end_line_of_parameter - $line_of_parameter + 1);
	    &amp;flush_file_lines($config{'postfix_config_file'});
	} else {
	    &amp;unflush_file_lines($config{'postfix_config_file'});
	}
    }
    else
    {
        local ($out, $ex);
	$ex = &amp;execute_command(
		"$config{'postfix_config_command'} -c $config_dir ".
		"-e $_[0]=".quotemeta($value), undef, \$out, \$out);
	$ex &amp;&amp; &amp;error(&amp;text('query_set_efailed', $_[0], $_[1], $out).
		      "&lt;br&gt; $config{'postfix_config_command'} -c $config_dir ".
		     "-e $_[0]=\"$value\" 2&gt;&amp;1");
        &amp;unflush_file_lines($config{'postfix_config_file'}); # Invalidate cache
    }
}

# check_postfix()
#
sub check_postfix
{
	my $cmd = "$config{'postfix_control_command'} -c $config_dir check";
	my $out = &amp;backquote_command("$cmd 2&gt;&amp;1 &lt;/dev/null", 1);
	my $ex = $?;
	if ($ex &amp;&amp; &amp;foreign_check("proc")) {
		# Get a better error message
		&amp;foreign_require("proc", "proc-lib.pl");
		$out = &amp;proc::pty_backquote("$cmd 2&gt;&amp;1 &lt;/dev/null");
		}
	return $ex ? ($out || "$cmd failed") : undef;
}

# reload_postfix()
#
sub reload_postfix
{
    if (is_postfix_running())
    {
	if (check_postfix()) {
		return $text{'check_error'};
		}
	my $cmd;
	if (!$config{'reload_cmd'}) {
		$cmd = "$config{'postfix_control_command'} -c $config_dir ".
		       "reload";
		}
	else {
		$cmd = $config{'reload_cmd'};
		}
	my $ex = &amp;system_logged("$cmd &gt;/dev/null 2&gt;&amp;1");
	return $ex ? ($out || "$cmd failed") : undef;
    }
    return undef;
}

# stop_postfix()
# Attempts to stop postfix, returning undef on success or an error message
sub stop_postfix
{
local $out;
if ($config{'stop_cmd'}) {
	$out = &amp;backquote_logged("$config{'stop_cmd'} 2&gt;&amp;1");
	}
else {
	$out = &amp;backquote_logged("$config{'postfix_control_command'} -c $config_dir stop 2&gt;&amp;1");
	}
return $? ? "&lt;tt&gt;$out&lt;/tt&gt;" : undef;
}

# start_postfix()
# Attempts to start postfix, returning undef on success or an error message
sub start_postfix
{
local $out;
if ($config{'start_cmd'}) {
	$out = &amp;backquote_logged("$config{'start_cmd'} 2&gt;&amp;1");
	}
else {
	$out = &amp;backquote_logged("$config{'postfix_control_command'} -c $config_dir start 2&gt;&amp;1");
	}
return $? ? "&lt;tt&gt;$out&lt;/tt&gt;" : undef;
}

# option_radios_freefield(name_of_option, length_of_free_field, [name_of_radiobutton, text_of_radiobutton]+)
# builds an option with variable number of radiobuttons and a free field
# WARNING: *FIRST* RADIO BUTTON *MUST* BE THE DEFAULT VALUE OF POSTFIX
sub option_radios_freefield
{
    my ($name, $length) = ($_[0], $_[1]);

    my $v = &amp;get_current_value($name);
    my $key = 'opts_'.$name;

    my $check_free_field = 1;
    
    my $help = -r &amp;help_file($module_name, "opt_".$name) ?
		&amp;hlink($text{$key}, "opt_".$name) : $text{$key};
    my $rv;

    # first radio button (must be default value!!)
    $rv .= &amp;ui_oneradio($name."_def", "__DEFAULT_VALUE_IE_NOT_IN_CONFIG_FILE__",
		       $_[2], &amp;if_default_value($name));

    $check_free_field = 0 if &amp;if_default_value($name);
    shift;
    
    # other radio buttons
    while (defined($_[2]))
    {
	$rv .= &amp;ui_oneradio($name."_def", $_[2], $_[3], $v eq $_[2]);
	if ($v eq $_[2]) { $check_free_field = 0; }
	shift;
	shift;
    }

    # the free field
    $rv .= &amp;ui_oneradio($name."_def", "__USE_FREE_FIELD__", undef,
		       $check_free_field == 1);
    $rv .= &amp;ui_textbox($name, $check_free_field == 1 ? $v : undef, $length);
    print &amp;ui_table_row($help, $rv, $length &gt; 20 ? 3 : 1);
}

# option_mapfield(name_of_option, length_of_free_field)
# Prints a field for selecting a map, or none
sub option_mapfield
{
    my ($name, $length) = ($_[0], $_[1]);

    my $v = &amp;get_current_value($name);
    my $key = 'opts_'.$name;

    my $check_free_field = 1;
    
    my $help = -r &amp;help_file($module_name, "opt_".$name) ?
		&amp;hlink($text{$key}, "opt_".$name) : $text{$key};
    my $rv;
    $rv .= &amp;ui_oneradio($name."_def", "__DEFAULT_VALUE_IE_NOT_IN_CONFIG_FILE__",
		        $text{'opts_nomap'}, &amp;if_default_value($name));
    $rv .= "&lt;br&gt;\n";

    $check_free_field = 0 if &amp;if_default_value($name);
    shift;
    
    # the free field
    $rv .= &amp;ui_oneradio($name."_def", "__USE_FREE_FIELD__",
		        $text{'opts_setmap'}, $check_free_field == 1);
    $rv .= &amp;ui_textbox($name, $check_free_field == 1 ? $v : undef, $length);
    $rv .= &amp;map_chooser_button($name, $name);
    print &amp;ui_table_row($help, $rv, $length &gt; 20 ? 3 : 1);
}



# option_freefield(name_of_option, length_of_free_field)
# builds an option with free field
sub option_freefield
{
    my ($name, $length) = ($_[0], $_[1]);

    my $v = &amp;get_current_value($name);
    my $key = 'opts_'.$name;
    
    print &amp;ui_table_row(&amp;hlink($text{$key}, "opt_".$name),
	&amp;ui_textbox($name."_def", $v, $length),
	$length &gt; 20 ? 3 : 1);
}


# option_yesno(name_of_option, [help])
# if help is provided, displays help link
sub option_yesno
{
    my $name = $_[0];
    my $v = &amp;get_current_value($name);
    my $key = 'opts_'.$name;

    print &amp;ui_table_row(defined($_[1]) ? &amp;hlink($text{$key}, "opt_".$name)
		       		       : $text{$key},
			&amp;ui_radio($name."_def", lc($v),
				  [ [ "yes", $text{'yes'} ],
				    [ "no", $text{'no'} ] ]));
}

# option_select(name_of_option, &amp;options, [help])
# Shows a drop-down menu of options
sub option_select
{
    my $name = $_[0];
    my $v = &amp;get_current_value($name);
    my $key = 'opts_'.$name;

    print &amp;ui_table_row(defined($_[2]) ? &amp;hlink($text{$key}, "opt_".$name)
				       : $text{$key},
    			&amp;ui_select($name."_def", lc($v), $_[1]));
}



############################################################################
# aliases support    [too lazy to create a aliases-lib.pl :-)]

# get_aliases_files($alias_maps) : @aliases_files
# parses its argument to extract the filenames of the aliases files
# supports multiple alias-files
sub get_aliases_files
{
    return map { $_-&gt;[1] }
	       grep { &amp;file_map_type($_-&gt;[0]) } &amp;get_maps_types_files($_[0]);
}

# init_new_alias() : $number
# gives a new number of alias
sub init_new_alias
{
    $aliases = &amp;get_aliases();

    my $max_number = 0;

    foreach $trans (@{$aliases})
    {
	if ($trans-&gt;{'number'} &gt; $max_number) { $max_number = $trans-&gt;{'number'}; }
    }
    
    return $max_number+1;
}

# list_postfix_aliases()
# Returns a list of all aliases. These typically come from a file, but may also
# be taken from a MySQL or LDAP backend
sub list_postfix_aliases
{
local @rv;
foreach my $f (&amp;get_maps_types_files(&amp;get_current_value("alias_maps"))) {
	if (&amp;file_map_type($f-&gt;[0])) {
		# We can read this file directly
		local $sofar = scalar(@rv);
		foreach my $a (&amp;list_aliases([ $f-&gt;[1] ])) {
			$a-&gt;{'num'} += $sofar;
			push(@rv, $a);
			}
		}
	else {
		# Treat as a map
		push(@maps, "$f-&gt;[0]:$f-&gt;[1]");
		}
	}
if (@maps) {
	# Convert values from MySQL and LDAP maps into alias structures
	local $maps = &amp;get_maps("alias_maps", undef, join(",", @maps));
	foreach my $m (@$maps) {
		local $v = $m-&gt;{'value'};
		local @values;
		while($v =~ /^\s*,?\s*()"([^"]+)"(.*)$/ ||
		      $v =~ /^\s*,?\s*(\|)"([^"]+)"(.*)$/ ||
		      $v =~ /^\s*,?\s*()([^,\s]+)(.*)$/) {
			push(@values, $1.$2);
			$v = $3;
			}
		if ($m-&gt;{'name'} =~ /^#(.*)$/) {
			$m-&gt;{'enabled'} = 0;
			$m-&gt;{'name'} = $1;
			}
		else {
			$m-&gt;{'enabled'} = 1;
			}
		$m-&gt;{'values'} = \@values;
		$m-&gt;{'num'} = scalar(@rv);
		push(@rv, $m);
		}
	}
return @rv;
}

# create_postfix_alias(&amp;alias)
# Adds a new alias, either to the local file or another backend
sub create_postfix_alias
{
local ($alias) = @_;
local @afiles = &amp;get_maps_types_files(&amp;get_current_value("alias_maps"));
local $last_type = $afiles[$#afiles]-&gt;[0];
local $last_file = $afiles[$#afiles]-&gt;[1];
if (&amp;file_map_type($last_type)) {
	# Just adding to a file
	&amp;create_alias($alias, [ $last_file ], 1);
	}
else {
	# Add to appropriate backend map
	if (!$alias-&gt;{'enabled'}) {
		$alias-&gt;{'name'} = '#'.$alias-&gt;{'name'};
		}
	$alias-&gt;{'value'} = join(',', map { /\s/ ? "\"$_\"" : $_ }
					  @{$alias-&gt;{'values'}});
	&amp;create_mapping("alias_maps", $alias, undef, "$last_type:$last_file");
	}
}

# delete_postfix_alias(&amp;alias)
# Delete an alias, either from the files or from a MySQL or LDAP map
sub delete_postfix_alias
{
local ($alias) = @_;
if ($alias-&gt;{'map_type'}) {
	# This was from a map
	&amp;delete_mapping("alias_maps", $alias);
	}
else {
	# Regular alias
	&amp;delete_alias($alias, 1);
	}
}

# modify_postfix_alias(&amp;oldalias, &amp;alias)
# Update an alias, either in a file or in a map
sub modify_postfix_alias
{
local ($oldalias, $alias) = @_;
if ($oldalias-&gt;{'map_type'}) {
	# In the map
	if (!$alias-&gt;{'enabled'}) {
		$alias-&gt;{'name'} = '#'.$alias-&gt;{'name'};
		}
	$alias-&gt;{'value'} = join(',', map { /\s/ ? "\"$_\"" : $_ }
					  @{$alias-&gt;{'values'}});
	&amp;modify_mapping("alias_maps", $oldalias, $alias);
	}
else {
	# Regular alias in a file
	&amp;modify_alias($oldalias, $alias);
	}
}

# renumber_list(&amp;list, &amp;position-object, lines-offset)
sub renumber_list
{
return if (!$_[2]);
local $e;
foreach $e (@{$_[0]}) {
	next if (defined($e-&gt;{'alias_file'}) &amp;&amp;
	         $e-&gt;{'alias_file'} ne $_[1]-&gt;{'alias_file'});
	next if (defined($e-&gt;{'map_file'}) &amp;&amp;
	         $e-&gt;{'map_file'} ne $_[1]-&gt;{'map_file'});
	$e-&gt;{'line'} += $_[2] if ($e-&gt;{'line'} &gt; $_[1]-&gt;{'line'});
	$e-&gt;{'eline'} += $_[2] if (defined($e-&gt;{'eline'}) &amp;&amp;
				   $e-&gt;{'eline'} &gt; $_[1]-&gt;{'eline'});
	}
}

# save_options(%options, [&amp;always-save])
#
sub save_options
{
    if (check_postfix()) { &amp;error("$text{'check_error'}"); }

    my %options = %{$_[0]};

    foreach $key (keys %options)
    {
	if ($key =~ /_def$/)
	{
	    (my $param = $key) =~ s/_def$//;
	    my $value = $options{$key} eq "__USE_FREE_FIELD__" ?
			$options{$param} : $options{$key};
	    $value =~ s/\0/, /g;
            if ($value =~ /(\S+):(\/\S+)/ &amp;&amp; $access{'dir'} ne '/') {
		foreach my $f (&amp;get_maps_files("$1:$2")) {
		   if (!&amp;is_under_directory($access{'dir'}, $f)) {
			&amp;error(&amp;text('opts_edir', $access{'dir'}));
		   }
		}
            }
	    &amp;set_current_value($param, $value,
			       &amp;indexof($param, @{$_[1]}) &gt;= 0);
	}
    }
}


# regenerate_aliases
#
sub regenerate_aliases
{
    local $out;
    $access{'aliases'} || error($text{'regenerate_ecannot'});
    if (get_current_value("alias_maps") eq "")
    {
	$out = &amp;backquote_logged("$config{'postfix_newaliases_command'} 2&gt;&amp;1");
	if ($?) { &amp;error(&amp;text('regenerate_alias_efailed', $out)); }
    }
    else
    {
	local $map;
	foreach $map (get_maps_types_files(get_real_value("alias_maps")))
	{
	    if (&amp;file_map_type($map-&gt;[0])) {
		    my $cmd = $config{'postfix_aliases_table_command'};
		    if ($cmd =~ /newaliases/) {
			$cmd .= " -oA$map-&gt;[1]";
		    } else {
			$cmd .= " $map-&gt;[1]";
		    }
		    $out = &amp;backquote_logged("$cmd 2&gt;&amp;1");
		    if ($?) { &amp;error(&amp;text('regenerate_table_efailed', $map-&gt;[1], $out)); }
	    }
	}
    }
}


# regenerate_relocated_table()
sub regenerate_relocated_table
{
    &amp;regenerate_any_table("relocated_maps");
}


# regenerate_virtual_table()
sub regenerate_virtual_table
{
    &amp;regenerate_any_table($virtual_maps);
}

# regenerate_bcc_table()
sub regenerate_bcc_table
{
    &amp;regenerate_any_table("sender_bcc_maps");
}

sub regenerate_relay_recipient_table
{ 
    &amp;regenerate_any_table("relay_recipient_maps");
}

sub regenerate_sender_restrictions_table
{
    &amp;regenerate_any_table("smtpd_sender_restrictions");
}

# regenerate_recipient_bcc_table()
sub regenerate_recipient_bcc_table
{
    &amp;regenerate_any_table("recipient_bcc_maps");
}

# regenerate_header_table()
sub regenerate_header_table
{
    &amp;regenerate_any_table("header_checks");
}

# regenerate_body_table()
sub regenerate_body_table
{
    &amp;regenerate_any_table("body_checks");
}

# regenerate_canonical_table
#
sub regenerate_canonical_table
{
    &amp;regenerate_any_table("canonical_maps");
    &amp;regenerate_any_table("recipient_canonical_maps");
    &amp;regenerate_any_table("sender_canonical_maps");
}


# regenerate_transport_table
#
sub regenerate_transport_table
{
    &amp;regenerate_any_table("transport_maps");
}

# regenerate_dependent_table
#
sub regenerate_dependent_table
{
    &amp;regenerate_any_table("sender_dependent_default_transport_maps");
}


# regenerate_any_table($parameter_where_to_find_the_table_names,
#		       [ &amp;force-files ], [ after-tag ])
#
sub regenerate_any_table
{
    my ($name, $force, $after) = @_;
    my @files;
    if ($force) {
	@files = map { [ "hash", $_ ] } @$force;
    } elsif (&amp;get_current_value($name) ne "") {
	my $value = &amp;get_real_value($name);
	if ($after) {
		$value =~ s/^.*\Q$after\E\s+(\S+).*$/$1/ || return;
		}
	@files = &amp;get_maps_types_files($value);
    }
    foreach my $map (@files)
    {
        next unless $map;
	if (&amp;file_map_type($map-&gt;[0]) &amp;&amp;
	    $map-&gt;[0] ne 'regexp' &amp;&amp; $map-&gt;[0] ne 'pcre') {
		local $out = &amp;backquote_logged("$config{'postfix_lookup_table_command'} -c $config_dir $map-&gt;[0]:$map-&gt;[1] 2&gt;&amp;1");
		if ($?) { &amp;error(&amp;text('regenerate_table_efailed', $map-&gt;[1], $out)); }
	}
    }
}



############################################################################
# maps [canonical, virtual, transport] support

# get_maps_files($maps_param) : @maps_files
# parses its argument to extract the filenames of the mapping files
# supports multiple maps-files
sub get_maps_files
{
    $_[0] =~ /:(\/[^,\s]*)(.*)/ || return ( );
    (my $returnvalue, my $recurse) = ( $1, $2 );

    return ( $returnvalue,
	     ($recurse =~ /:\/[^,\s]*/) ?
	         &amp;get_maps_files($recurse)
	     :
	         ()
           )
}

 
# get_maps($maps_name, [&amp;force-files], [force-map]) : \@maps
# Construct the mappings database taken from the map files given from the
# parameters.
sub get_maps
{
    if (!defined($maps_cache{$_[0]}))
    {
	my @maps_files = $_[1] ? (map { [ "hash", $_ ] } @{$_[1]}) :
			 $_[2] ? &amp;get_maps_types_files($_[2]) :
			         &amp;get_maps_types_files(&amp;get_real_value($_[0]));
	my $number = 0;
	$maps_cache{$_[0]} = [ ];
	foreach my $maps_type_file (@maps_files)
	{
	    my ($maps_type, $maps_file) = @$maps_type_file;

	    if (&amp;file_map_type($maps_type)) {
		    # Read a file on disk
		    &amp;open_readfile(MAPS, $maps_file);
		    my $i = 0;
		    my $cmt;
		    while (&lt;MAPS&gt;)
		    {
			s/\r|\n//g;	# remove newlines
			if (/^\s*#+\s*(.*)/) {
			    # A comment line
			    $cmt = &amp;is_table_comment($_);
			    }
			elsif (/^\s*(\/[^\/]*\/[a-z]*)\s+([^#]*)/ ||
			       /^\s*([^\s]+)\s+([^#]*)/) {
			    # An actual map
			    $number++;
			    my %map;
			    $map{'name'} = $1;
			    $map{'value'} = $2;
			    $map{'line'} = $cmt ? $i-1 : $i;
			    $map{'eline'} = $i;
			    $map{'map_file'} = $maps_file;
			    $map{'map_type'} = $maps_type;
			    $map{'file'} = $maps_file;
			    $map{'number'} = $number;
			    $map{'cmt'} = $cmt;
			    push(@{$maps_cache{$_[0]}}, \%map);
			    $cmt = undef;
			    }
			else {
			    $cmt = undef;
			    }
			$i++;
		    }
		    close(MAPS);

	     } elsif ($maps_type eq "mysql") {
		    # Get from a MySQL database
		    local $conf = &amp;mysql_value_to_conf($maps_file);
		    local $dbh = &amp;connect_mysql_db($conf);
		    ref($dbh) || &amp;error($dbh);
		    local $cmd = $dbh-&gt;prepare(
				       "select ".$conf-&gt;{'where_field'}.
				       ",".$conf-&gt;{'select_field'}.
				       " from ".$conf-&gt;{'table'}.
				       " where 1 = 1 ".
				       $conf-&gt;{'additional_conditions'});
		    if (!$cmd || !$cmd-&gt;execute()) {
			&amp;error(&amp;text('mysql_elist',
			     "&lt;tt&gt;".&amp;html_escape($dbh-&gt;errstr)."&lt;/tt&gt;"));
			}
		    while(my ($k, $v) = $cmd-&gt;fetchrow()) {
			$number++;
			my %map;
			$map{'name'} = $k;
			$map{'value'} = $v;
			$map{'key'} = $k;
			$map{'map_file'} = $maps_file;
			$map{'map_type'} = $maps_type;
			$map{'number'} = $number;
			push(@{$maps_cache{$_[0]}}, \%map);
		    }
		    $cmd-&gt;finish();
		    $dbh-&gt;disconnect();

	     } elsif ($maps_type eq "ldap") {
		    # Get from an LDAP database
	     	    local $conf = &amp;ldap_value_to_conf($maps_file);
		    local $ldap = &amp;connect_ldap_db($conf);
		    ref($ldap) || &amp;error($ldap);
		    local ($name_attr, $filter) = &amp;get_ldap_key($conf);
		    local $scope = $conf-&gt;{'scope'} || 'sub';
		    local $rv = $ldap-&gt;search(base =&gt; $conf-&gt;{'search_base'},
					      scope =&gt; $scope,
					      filter =&gt; $filter);
		    if (!$rv || $rv-&gt;code) {
			# Search failed!
			&amp;error(&amp;text('ldap_equery',
				     "&lt;tt&gt;$conf-&gt;{'search_base'}&lt;/tt&gt;",
				     "&lt;tt&gt;".&amp;html_escape($rv-&gt;error)."&lt;/tt&gt;"));
		    }
		    foreach my $o ($rv-&gt;all_entries) {
			$number++;
			my %map;
			$map{'name'} = $o-&gt;get_value($name_attr);
			$map{'value'} = $o-&gt;get_value(
				$conf-&gt;{'result_attribute'} || "maildrop");
			$map{'dn'} = $o-&gt;dn();
			$map{'map_file'} = $maps_file;
			$map{'map_type'} = $maps_type;
			$map{'number'} = $number;
			push(@{$maps_cache{$_[0]}}, \%map);
		    }
	     }
	}
    }
    return $maps_cache{$_[0]};
}


# generate_map_edit(name, desc, [wide], [nametitle], [valuetitle])
# Prints a table showing map contents, with links to edit and add
sub generate_map_edit
{
    # Check if map is set
    if (&amp;get_current_value($_[0]) eq "")
    {
	print "&lt;b&gt;$text{'no_map2'}&lt;/b&gt;&lt;p&gt;\n";
        return;
    }

    # Make sure the user is allowed to edit them
    foreach my $f (&amp;get_maps_types_files(&amp;get_real_value($_[0]))) {
      if (&amp;file_map_type($f-&gt;[0])) {
	  &amp;is_under_directory($access{'dir'}, $f-&gt;[1]) ||
		&amp;error(&amp;text('mapping_ecannot', $access{'dir'}));
      }
    }

    # Make sure we *can* edit them
    foreach my $f (&amp;get_maps_types_files(&amp;get_real_value($_[0]))) {
       my $err = &amp;can_access_map(@$f);
       if ($err) {
	  print "&lt;b&gt;",&amp;text('map_cannot', $err),"&lt;/b&gt;&lt;p&gt;\n";
	  return;
       }
    }

    my $mappings = &amp;get_maps($_[0]);
    my $nt = $_[3] || $text{'mapping_name'};
    my $vt = $_[4] || $text{'mapping_value'};

    local @links = ( &amp;ui_link("edit_mapping.cgi?map_name=$_[0]",
			      $text{'new_mapping'}),);
    if ($access{'manual'} &amp;&amp; &amp;can_map_manual($_[0])) {
	push(@links, &amp;ui_link("edit_manual.cgi?map_name=$_[0]",
			      $text{'new_manual'}));
	}

    if ($#{$mappings} ne -1)
    {
        # Map description
	print $_[1],"&lt;p&gt;\n";

	# Sort the map
	if ($config{'sort_mode'} == 1) {
		if ($_[0] eq $virtual_maps) {
			@{$mappings} = sort sort_by_domain @{$mappings};
			}
		else {
			@{$mappings} = sort { $a-&gt;{'name'} cmp $b-&gt;{'name'} }
					    @{$mappings};
			}
		}

	# Split into two columns, if needed
	my @parts;
	my $split_index = int(($#{$mappings})/2);
	if ($config{'columns'} == 2) {
		@parts = ( [ @{$mappings}[0 .. $split_index] ],
			   [ @{$mappings}[$split_index+1 .. $#{$mappings} ] ] );
		}
	else {
		@parts = ( $mappings );
		}
	
	# Start of the overall form
	print &amp;ui_form_start("delete_mappings.cgi", "post");
	print &amp;ui_hidden("map_name", $_[0]),"\n";
	unshift(@links, &amp;select_all_link("d", 1),
			&amp;select_invert_link("d", 1));
	print &amp;ui_links_row(\@links);

	my @grid;
	foreach my $p (@parts) {
		# Build one table
		my @table;
		foreach my $map (@$p) {
			push(@table, [
			    { 'type' =&gt; 'checkbox', 'name' =&gt; 'd',
			      'value' =&gt; $map-&gt;{'name'} },
			    "&lt;a href=\"edit_mapping.cgi?num=$map-&gt;{'number'}&amp;".
			     "map_name=$_[0]\"&gt;".&amp;html_escape($map-&gt;{'name'}).
			     "&lt;/a&gt;",
			    &amp;html_escape($map-&gt;{'value'}),
			    $config{'show_cmts'} ?
			     ( &amp;html_escape($map-&gt;{'cmt'}) ) : ( ),
			    ]);
			}

		# Add a table to the grid
		push(@grid, &amp;ui_columns_table(
			[ "", $nt, $vt,
                          $config{'show_cmts'} ? ( $text{'mapping_cmt'} ) : ( ),
			],
			100,
			\@table));
		}
	if (@grid == 1) {
		print $grid[0];
		}
	else {
		print &amp;ui_grid_table(\@grid, 2, 100,
			[ "width=50%", "width=50%" ]);
		}
	
 	# Main form end
	print &amp;ui_links_row(\@links);
	print &amp;ui_form_end([ [ "delete", $text{'mapping_delete'} ] ]);
    }
    else {
        # None, so just show edit link
        print "&lt;b&gt;$text{'mapping_none'}&lt;/b&gt;&lt;p&gt;\n";
        print &amp;ui_links_row(\@links);
    }
}


# create_mapping(map, &amp;mapping, [&amp;force-files], [force-map])
sub create_mapping
{
&amp;get_maps($_[0], $_[2], $_[3]);	# force cache init
my @maps_files = $_[2] ? (map { [ "hash", $_ ] } @{$_[2]}) :
		 $_[3] ? &amp;get_maps_types_files($_[3]) :
		         &amp;get_maps_types_files(&amp;get_real_value($_[0]));

# If multiple maps, find a good one to add to .. avoid regexp if we can
my $last_map;
if (@maps_files == 1) {
	$last_map = $maps_files[0];
	}
else {
	for(my $i=$#maps_files; $i&gt;=0; $i--) {
		if ($maps_files[$i]-&gt;[0] ne 'regexp' &amp;&amp;
	 	    $maps_files[$i]-&gt;[0] ne 'pcre') {
			$last_map = $maps_files[$i];
			last;
			}
		}
	$last_map ||= $maps_files[$#maps_files];	# Fall back to last one
	}
my ($maps_type, $maps_file) = @$last_map;

if (&amp;file_map_type($maps_type)) {
	# Adding to a regular file
	local $lref = &amp;read_file_lines($maps_file);
	$_[1]-&gt;{'line'} = scalar(@$lref);
	push(@$lref, &amp;make_table_comment($_[1]-&gt;{'cmt'}));
	push(@$lref, "$_[1]-&gt;{'name'}\t$_[1]-&gt;{'value'}");
	$_[1]-&gt;{'eline'} = scalar(@$lref)-1;
	&amp;flush_file_lines($maps_file);
	}
elsif ($maps_type eq "mysql") {
	# Adding to a MySQL table
	local $conf = &amp;mysql_value_to_conf($maps_file);
	local $dbh = &amp;connect_mysql_db($conf);
	ref($dbh) || &amp;error($dbh);
	local $cmd = $dbh-&gt;prepare("insert into ".$conf-&gt;{'table'}." ".
				   "(".$conf-&gt;{'where_field'}.",".
					$conf-&gt;{'select_field'}.") values (".
				   "?, ?)");
	if (!$cmd || !$cmd-&gt;execute($_[1]-&gt;{'name'}, $_[1]-&gt;{'value'})) {
		&amp;error(&amp;text('mysql_eadd',
			     "&lt;tt&gt;".&amp;html_escape($dbh-&gt;errstr)."&lt;/tt&gt;"));
		}
	$cmd-&gt;finish();
	$dbh-&gt;disconnect();
	$_[1]-&gt;{'key'} = $_[1]-&gt;{'name'};
	}
elsif ($maps_type eq "ldap") {
	# Adding to an LDAP database
	local $conf = &amp;ldap_value_to_conf($maps_file);
	local $ldap = &amp;connect_ldap_db($conf);
	ref($ldap) || &amp;error($ldap);
	local @classes = split(/\s+/, $config{'ldap_class'} ||
				      "inetLocalMailRecipient");
	local @attrs = ( "objectClass", \@classes );
	local $name_attr = &amp;get_ldap_key($conf);
	push(@attrs, $name_attr, $_[1]-&gt;{'name'});
	push(@attrs, $conf-&gt;{'result_attribute'} || "maildrop",
		     $_[1]-&gt;{'value'});
	push(@attrs, &amp;split_props($config{'ldap_attrs'}));
	local $dn = &amp;make_map_ldap_dn($_[1], $conf);
	if ($dn =~ /^([^=]+)=([^, ]+)/ &amp;&amp; !&amp;in_props(\@attrs, $1)) {
		push(@attrs, $1, $2);
		}

	# Make sure the parent DN exists - for example, when adding a domain
	&amp;ensure_ldap_parent($ldap, $dn);

	# Actually add
	local $rv = $ldap-&gt;add($dn, attr =&gt; \@attrs);
	if ($rv-&gt;code) {
		&amp;error(&amp;text('ldap_eadd', "&lt;tt&gt;$dn&lt;/tt&gt;",
			     "&lt;tt&gt;".&amp;html_escape($rv-&gt;error)."&lt;/tt&gt;"));
		}
	$_[1]-&gt;{'dn'} = $dn;
	}

# Update the in-memory cache
$_[1]-&gt;{'map_type'} = $maps_type;
$_[1]-&gt;{'map_file'} = $maps_file;
$_[1]-&gt;{'file'} = $maps_file;
$_[1]-&gt;{'number'} = scalar(@{$maps_cache{$_[0]}});
push(@{$maps_cache{$_[0]}}, $_[1]);
}


# delete_mapping(map, &amp;mapping)
sub delete_mapping
{
if (&amp;file_map_type($_[1]-&gt;{'map_type'}) || !$_[1]-&gt;{'map_type'}) {
	# Deleting from a file
	local $lref = &amp;read_file_lines($_[1]-&gt;{'map_file'});
	local $dl = $lref-&gt;[$_[1]-&gt;{'eline'}];
	local $len = $_[1]-&gt;{'eline'} - $_[1]-&gt;{'line'} + 1;
	if (($dl =~ /^\s*(\/[^\/]*\/[a-z]*)\s+([^#]*)/ ||
	     $dl =~ /^\s*([^\s]+)\s+([^#]*)/) &amp;&amp;
	    $1 eq $_[1]-&gt;{'name'}) {
		# Found a valid line to remove
		splice(@$lref, $_[1]-&gt;{'line'}, $len);
		}
	else {
		print STDERR "Not deleting line $_[1]-&gt;{'line'} ",
			     "from $_[1]-&gt;{'file'} for key ",
			     "$_[1]-&gt;{'name'} which actually contains $dl\n";
		}
	&amp;flush_file_lines($_[1]-&gt;{'map_file'});
	&amp;renumber_list($maps_cache{$_[0]}, $_[1], -$len);
	local $idx = &amp;indexof($_[1], @{$maps_cache{$_[0]}});
	if ($idx &gt;= 0) {
		# Take out of cache
		splice(@{$maps_cache{$_[0]}}, $idx, 1);
		}
	}
elsif ($_[1]-&gt;{'map_type'} eq 'mysql') {
	# Deleting from MySQL
	local $conf = &amp;mysql_value_to_conf($_[1]-&gt;{'map_file'});
	local $dbh = &amp;connect_mysql_db($conf);
	ref($dbh) || &amp;error($dbh);
	local $cmd = $dbh-&gt;prepare("delete from ".$conf-&gt;{'table'}.
				   " where ".$conf-&gt;{'where_field'}." = ?".
				   " ".$conf-&gt;{'additional_conditions'});
	if (!$cmd || !$cmd-&gt;execute($_[1]-&gt;{'key'})) {
		&amp;error(&amp;text('mysql_edelete',
			     "&lt;tt&gt;".&amp;html_escape($dbh-&gt;errstr)."&lt;/tt&gt;"));
		}
	$cmd-&gt;finish();
	$dbh-&gt;disconnect();
	}
elsif ($_[1]-&gt;{'map_type'} eq 'ldap') {
	# Deleting from LDAP
	local $conf = &amp;ldap_value_to_conf($_[1]-&gt;{'map_file'});
	local $ldap = &amp;connect_ldap_db($conf);
	ref($ldap) || &amp;error($ldap);
	local $rv = $ldap-&gt;delete($_[1]-&gt;{'dn'});
	if ($rv-&gt;code) {
		&amp;error(&amp;text('ldap_edelete', "&lt;tt&gt;$_[1]-&gt;{'dn'}&lt;/tt&gt;",
			     "&lt;tt&gt;".&amp;html_escape($rv-&gt;error)."&lt;/tt&gt;"));
		}
	}

# Delete from in-memory cache
local $idx = &amp;indexof($_[1], @{$maps_cache{$_[0]}});
splice(@{$maps_cache{$_[0]}}, $idx, 1) if ($idx != -1);
}


# modify_mapping(map, &amp;oldmapping, &amp;newmapping)
sub modify_mapping
{
if (&amp;file_map_type($_[1]-&gt;{'map_type'}) || !$_[1]-&gt;{'map_type'}) {
	# Modifying in a file
	local $lref = &amp;read_file_lines($_[1]-&gt;{'map_file'});
	local $oldlen = $_[1]-&gt;{'eline'} - $_[1]-&gt;{'line'} + 1;
	local @newlines;
	push(@newlines, &amp;make_table_comment($_[2]-&gt;{'cmt'}));
	push(@newlines, "$_[2]-&gt;{'name'}\t$_[2]-&gt;{'value'}");
	splice(@$lref, $_[1]-&gt;{'line'}, $oldlen, @newlines);
	&amp;flush_file_lines($_[1]-&gt;{'map_file'});
	&amp;renumber_list($maps_cache{$_[0]}, $_[1], scalar(@newlines)-$oldlen);
	local $idx = &amp;indexof($_[1], @{$maps_cache{$_[0]}});
	if ($idx &gt;= 0) {
		# Update in cache
		$_[2]-&gt;{'map_file'} = $_[1]-&gt;{'map_file'};
		$_[2]-&gt;{'map_type'} = $_[1]-&gt;{'map_type'};
		$_[2]-&gt;{'line'} = $_[1]-&gt;{'line'};
		$_[2]-&gt;{'eline'} = $_[1]-&gt;{'eline'};
		$maps_cache{$_[0]}-&gt;[$idx] = $_[2];
		}
	}
elsif ($_[1]-&gt;{'map_type'} eq 'mysql') {
	# Updating in MySQL
	local $conf = &amp;mysql_value_to_conf($_[1]-&gt;{'map_file'});
	local $dbh = &amp;connect_mysql_db($conf);
	ref($dbh) || &amp;error($dbh);
	local $cmd = $dbh-&gt;prepare("update ".$conf-&gt;{'table'}.
				   " set ".$conf-&gt;{'where_field'}." = ?,".
				   " ".$conf-&gt;{'select_field'}." = ?".
				   " where ".$conf-&gt;{'where_field'}." = ?".
				   " ".$conf-&gt;{'additional_conditions'});
	if (!$cmd || !$cmd-&gt;execute($_[2]-&gt;{'name'}, $_[2]-&gt;{'value'},
				    $_[1]-&gt;{'key'})) {
		&amp;error(&amp;text('mysql_eupdate',
			     "&lt;tt&gt;".&amp;html_escape($dbh-&gt;errstr)."&lt;/tt&gt;"));
		}
	$cmd-&gt;finish();
	$dbh-&gt;disconnect();
	}
elsif ($_[1]-&gt;{'map_type'} eq 'ldap') {
	# Updating in LDAP
	local $conf = &amp;ldap_value_to_conf($_[1]-&gt;{'map_file'});
	local $ldap = &amp;connect_ldap_db($conf);
	ref($ldap) || &amp;error($ldap);

	# Work out attribute changes
	local %replace;
	local $name_attr = &amp;get_ldap_key($conf);
	$replace{$name_attr} = [ $_[2]-&gt;{'name'} ];
	$replace{$conf-&gt;{'result_attribute'} || "maildrop"} =
		[ $_[2]-&gt;{'value'} ];

	# Work out new DN, if needed
	local $newdn = &amp;make_map_ldap_dn($_[2], $conf);
	if ($_[1]-&gt;{'name'} ne $_[2]-&gt;{'name'} &amp;&amp;
	    $_[1]-&gt;{'dn'} ne $newdn) {
		# Changed .. update the object in LDAP
		&amp;ensure_ldap_parent($ldap, $newdn);
		local ($newprefix, $newrest) = split(/,/, $newdn, 2);
		local $rv = $ldap-&gt;moddn($_[1]-&gt;{'dn'},
					 newrdn =&gt; $newprefix,
					 newsuperior =&gt; $newrest);
		if ($rv-&gt;code) {
			&amp;error(&amp;text('ldap_erename',
				     "&lt;tt&gt;$_[1]-&gt;{'dn'}&lt;/tt&gt;",
				     "&lt;tt&gt;$newdn&lt;/tt&gt;",
				     "&lt;tt&gt;".&amp;html_escape($rv-&gt;error)."&lt;/tt&gt;"));
			}
		$_[2]-&gt;{'dn'} = $newdn;
		if ($newdn =~ /^([^=]+)=([^, ]+)/) {
			$replace{$1} = [ $2 ];
			}
		}
	else {
		$_[2]-&gt;{'dn'} = $_[1]-&gt;{'dn'};
		}

	# Modify attributes
	local $rv = $ldap-&gt;modify($_[2]-&gt;{'dn'}, replace =&gt; \%replace);
	if ($rv-&gt;code) {
		&amp;error(&amp;text('ldap_emodify',
			     "&lt;tt&gt;$_[2]-&gt;{'dn'}&lt;/tt&gt;",
			     "&lt;tt&gt;".&amp;html_escape($rv-&gt;error)."&lt;/tt&gt;"));
		}
	}

# Update in-memory cache
local $idx = &amp;indexof($_[1], @{$maps_cache{$_[0]}});
$_[2]-&gt;{'map_file'} = $_[1]-&gt;{'map_file'};
$_[2]-&gt;{'map_type'} = $_[1]-&gt;{'map_type'};
$_[2]-&gt;{'file'} = $_[1]-&gt;{'file'};
$_[2]-&gt;{'line'} = $_[1]-&gt;{'line'};
$_[2]-&gt;{'eline'} = $_[2]-&gt;{'cmt'} ? $_[1]-&gt;{'line'}+1 : $_[1]-&gt;{'line'};
$maps_cache{$_[0]}-&gt;[$idx] = $_[2] if ($idx != -1);
}

# make_map_ldap_dn(&amp;map, &amp;conf)
# Work out an LDAP DN for a map
sub make_map_ldap_dn
{
local ($map, $conf) = @_;
local $dn;
local $scope = $conf-&gt;{'scope'} || 'sub';
$scope = 'base' if (!$config{'ldap_doms'});	# Never create sub-domains
local $id = $config{'ldap_id'} || 'cn';
if ($map-&gt;{'name'} =~ /^(\S+)\@(\S+)$/ &amp;&amp; $scope ne 'base') {
	# Within a domain
	$dn = "$id=$1,cn=$2,$conf-&gt;{'search_base'}";
	}
elsif ($map-&gt;{'name'} =~ /^\@(\S+)$/ &amp;&amp; $scope ne 'base') {
	# Domain catchall
	$dn = "$id=default,cn=$1,$conf-&gt;{'search_base'}";
	}
else {
	# Some other string
	$dn = "$id=$map-&gt;{'name'},$conf-&gt;{'search_base'}";
	}
return $dn;
}

# get_ldap_key(&amp;config)
# Returns the attribute name for the LDAP key. May call &amp;error
sub get_ldap_key
{
local ($conf) = @_;
local ($filter, $name_attr) = @_;
if ($conf-&gt;{'query_filter'}) {
	$filter = $conf-&gt;{'query_filter'};
	$conf-&gt;{'query_filter'} =~ /([a-z0-9]+)=\%[su]/i ||
		&amp;error("Could not get attribute from ".
		       $conf-&gt;{'query_filter'});
	$name_attr = $1;
	$filter = "($filter)" if ($filter !~ /^\(/);
	$filter =~ s/\%s/\*/g;
	}
else {
	$filter = "(mailacceptinggeneralid=*)";
	$name_attr = "mailacceptinggeneralid";
	}
return wantarray ? ( $name_attr, $filter ) : $name_attr;
}

# ensure_ldap_parent(&amp;ldap, dn)
# Create the parent of some DN if needed
sub ensure_ldap_parent
{
local ($ldap, $dn) = @_;
local $pdn = $dn;
$pdn =~ s/^([^,]+),//;
local $rv = $ldap-&gt;search(base =&gt; $pdn, scope =&gt; 'base',
			  filter =&gt; "(objectClass=top)",
			  sizelimit =&gt; 1);
if (!$rv || $rv-&gt;code || !$rv-&gt;all_entries) {
	# Does not .. so add it
	local @pclasses = ( "top" );
	local @pattrs = ( "objectClass", \@pclasses );
	local $rv = $ldap-&gt;add($pdn, attr =&gt; \@pattrs);
	}
}

# init_new_mapping($maps_parameter) : $number
# gives a new number of mapping
sub init_new_mapping
{
my $maps = &amp;get_maps($_[0]);
my $max_number = 0;
foreach $trans (@{$maps}) {
	if ($trans-&gt;{'number'} &gt; $max_number) {
		$max_number = $trans-&gt;{'number'};
		}
	}
return $max_number+1;
}

# postfix_mail_file(user|user-details-list)
sub postfix_mail_file
{
local @s = &amp;postfix_mail_system();
if ($s[0] == 0) {
	return "$s[1]/$_[0]";
	}
elsif (@_ &gt; 1) {
	return "$_[7]/$s[1]";
	}
else {
	local @u = getpwnam($_[0]);
	return "$u[7]/$s[1]";
	}
}

# postfix_mail_system()
# Returns 0 and the spool dir for sendmail style,
#         1 and the mbox filename for ~/Mailbox style
#         2 and the maildir name for ~/Maildir style
sub postfix_mail_system
{
if (!scalar(@mail_system_cache)) {
	local $home_mailbox = &amp;get_current_value("home_mailbox");
	if ($home_mailbox) {
		@mail_system_cache = $home_mailbox =~ /^(.*)\/$/ ?
			(2, $1) : (1, $home_mailbox);
		}
	else {
		local $mail_spool_directory =
			&amp;get_current_value("mail_spool_directory");
		@mail_system_cache = (0, $mail_spool_directory);
		}
	}
return wantarray ? @mail_system_cache : $mail_system_cache[0];
}

# list_queue([error-on-failure])
# Returns a list of strutures, each containing details of one queued message
sub list_queue
{
local ($throw) = @_;
local @qfiles;
local $out = &amp;backquote_command("$config{'mailq_cmd'} 2&gt;&amp;1 &lt;/dev/null");
&amp;error("$config{'mailq_cmd'} failed : ".&amp;html_escape($out)) if ($? &amp;&amp; $throw);
foreach my $l (split(/\r?\n/, $out)) {
	next if ($l =~ /^(\S+)\s+is\s+empty/i ||
		 $l =~ /^\s+Total\s+requests:/i);
	if ($l =~ /^([^\s\*\!]+)[\*\!]?\s*(\d+)\s+(\S+\s+\S+\s+\d+\s+\d+:\d+:\d+)\s+(.*)/) {
		local $q = { 'id' =&gt; $1, 'size' =&gt; $2,
                             'date' =&gt; $3, 'from' =&gt; $4 };
		if (defined(&amp;parse_mail_date)) {
			local $t = &amp;parse_mail_date($q-&gt;{'date'});
			if ($t) {
				$q-&gt;{'date'} = &amp;make_date($t, 0, 'yyyy/mm/dd');
				$q-&gt;{'time'} = $t;
				}
			}
		push(@qfiles, $q);
		}
	elsif ($l =~ /\((.*)\)/ &amp;&amp; @qfiles) {
		$qfiles[$#qfiles]-&gt;{'status'} = $1;
		}
	elsif ($l =~ /^\s+(\S+)/ &amp;&amp; @qfiles) {
		$qfiles[$#qfiles]-&gt;{'to'} .= "$1 ";
		}
	}
return @qfiles;
}

# parse_queue_file(id)
# Parses a postfix mail queue file into a standard mail structure
sub parse_queue_file
{
local @qfiles = ( &amp;recurse_files("$config{'mailq_dir'}/active"),
		  &amp;recurse_files("$config{'mailq_dir'}/incoming"),
		  &amp;recurse_files("$config{'mailq_dir'}/deferred"),
		  &amp;recurse_files("$config{'mailq_dir'}/corrupt"),
		  &amp;recurse_files("$config{'mailq_dir'}/hold"),
		  &amp;recurse_files("$config{'mailq_dir'}/maildrop"),
		);
local $f = $_[0];
local ($file) = grep { $_ =~ /\/$f$/ } @qfiles;
return undef if (!$file);
local $mode = 0;
local ($mail, @headers);
&amp;open_execute_command(QUEUE, "$config{'postcat_cmd'} ".quotemeta($file), 1, 1);
while(&lt;QUEUE&gt;) {
	if (/^\*\*\*\s+MESSAGE\s+CONTENTS/ &amp;&amp; !$mode) {	   # Start of headers
		$mode = 1;
		}
	elsif (/^\*\*\*\s+HEADER\s+EXTRACTED/ &amp;&amp; $mode) {  # End of email
		last;
		}
	elsif ($mode == 1 &amp;&amp; /^\s*$/) {			   # End of headers
		$mode = 2;
		}
	elsif ($mode == 1 &amp;&amp; /^(\S+):\s*(.*)/) {	   # Found a header
		push(@headers, [ $1, $2 ]);
		}
	elsif ($mode == 1 &amp;&amp; /^(\s+.*)/) {		   # Header continuation
		$headers[$#headers]-&gt;[1] .= $1 unless($#headers &lt; 0);
		}
	elsif ($mode == 2) {				   # Part of body
		$mail-&gt;{'size'} += length($_);
		$mail-&gt;{'body'} .= $_;
		}
	}
close(QUEUE);
$mail-&gt;{'headers'} = \@headers;
foreach $h (@headers) {
	$mail-&gt;{'header'}-&gt;{lc($h-&gt;[0])} = $h-&gt;[1];
	}
return $mail;
}

# recurse_files(dir)
sub recurse_files
{
opendir(DIR, &amp;translate_filename($_[0])) || return ( $_[0] );
local @dir = readdir(DIR);
closedir(DIR);
local ($f, @rv);
foreach $f (@dir) {
	push(@rv, &amp;recurse_files("$_[0]/$f")) if ($f !~ /^\./);
	}
return @rv;
}

sub sort_by_domain
{
local ($a1, $a2, $b1, $b2);
if ($a-&gt;{'name'} =~ /^(.*)\@(.*)$/ &amp;&amp; (($a1, $a2) = ($1, $2)) &amp;&amp;
    $b-&gt;{'name'} =~ /^(.*)\@(.*)$/ &amp;&amp; (($b1, $b2) = ($1, $2))) {
	return $a2 cmp $b2 ? $a2 cmp $b2 : $a1 cmp $b1;
	}
else {
	return $a-&gt;{'name'} cmp $b-&gt;{'name'};
	}
}

# before_save()
# Copy the postfix config file to a backup file, for reversion if
# a post-save check fails
sub before_save
{
if ($config{'check_config'} &amp;&amp; !defined($save_file)) {
	$save_file = &amp;transname();
	&amp;execute_command("cp $config{'postfix_config_file'} $save_file");
	}
}

sub after_save
{
if (defined($save_file)) {
	local $err = &amp;check_postfix();
	if ($err) {
		&amp;execute_command("mv $save_file $config{'postfix_config_file'}");
		&amp;error(&amp;text('after_err', "&lt;pre&gt;$err&lt;/pre&gt;"));
		}
	else {
		unlink($save_file);
		$save_file = undef;
		}
	}
}

# get_real_value(parameter_name)
# Returns the value of a parameter, with $ substitions done
sub get_real_value
{
my ($name) = @_;
my $v = &amp;get_current_value($name);
if ($postfix_version &gt;= 2.1 &amp;&amp; $v =~ /\$/) {
	# Try to use the built-in command to expand the param
	my $out = &amp;backquote_command("$config{'postfix_config_command'} -c $config_dir -x -h ".
				     quotemeta($name)." 2&gt;/dev/null", 1);
	if (!$? &amp;&amp; $out !~ /warning:.*unknown\s+parameter/) {
		chop($out);
		return $out;
		}
	}
$v =~ s/\$(\{([^\}]+)\}|([A-Za-z0-9\.\-\_]+))/get_real_value($2 || $3)/ge;
return $v;
}

# ensure_map(name)
# Create some map text file, if needed
sub ensure_map
{
foreach my $mf (&amp;get_maps_files(&amp;get_real_value($_[0]))) {
	if ($mf =~ /^\// &amp;&amp; !-e $mf) {
		&amp;open_lock_tempfile(TOUCH, "&gt;$mf", 1) ||
			&amp;error(&amp;text("efilewrite", $mf, $!));
		&amp;close_tempfile(TOUCH);
		&amp;set_ownership_permissions(undef, undef, 0755, $mf);
		}
	}
}

# Functions for editing the header_checks map nicely
sub edit_name_header_checks
{
return &amp;ui_table_row($text{'header_name'},
		     &amp;ui_textbox("name", $_[0]-&gt;{'name'}, 60));
}

sub parse_name_header_checks
{
$_[1]-&gt;{'name'} =~ /^\/.*\S.*\/[a-z]*$/ || &amp;error($text{'header_ename'});
return $_[1]-&gt;{'name'};
}

sub edit_value_header_checks
{
local ($act, $dest) = split(/\s+/, $_[0]-&gt;{'value'}, 2);
return &amp;ui_table_row($text{'header_value'},
              &amp;ui_select("action", $act,
			 [ map { [ $_, $text{'header_'.lc($_)} ] }
			       @header_checks_actions ], 0, 0, $act)."\n".
	      &amp;ui_textbox("value", $dest, 40));
}

sub parse_value_header_checks
{
local $rv = $_[1]-&gt;{'action'};
if ($_[1]-&gt;{'value'}) {
	$rv .= " ".$_[1]-&gt;{'value'};
	}
return $rv;
}

# Functions for editing the body_checks map (same as header_checks)
sub edit_name_body_checks
{
return &amp;edit_name_header_checks(@_);
}

sub parse_name_body_checks
{
return &amp;parse_name_header_checks(@_);
}

sub edit_value_body_checks
{
return &amp;edit_value_header_checks(@_);
}

sub parse_value_body_checks
{
return &amp;parse_value_header_checks(@_);
}

## added function for sender_access_maps
## added function for client_access_maps
sub edit_name_check_sender_access
{
return "&lt;td&gt;&lt;b&gt;$text{'access_addresses'}&lt;/b&gt;&lt;/td&gt;\n".
       "&lt;td&gt;".&amp;ui_textbox("name", $_[0]-&gt;{'name'},40)."&lt;/td&gt;\n";
}

sub edit_value_check_sender_access
{
local ($act, $dest) = split(/\s+/, $_[0]-&gt;{'value'}, 2);
return "&lt;td&gt;&lt;b&gt;$text{'header_value'}&lt;/b&gt;&lt;/td&gt;\n".
       "&lt;td&gt;".&amp;ui_select("action", $act,
			 [ map { [ $_, $text{'header_'.lc($_)} ] }
			       @check_sender_actions ], 0, 0, $act)."\n".
	      &amp;ui_textbox("value", $dest, 40)."&lt;/td&gt;\n";
}

sub parse_value_check_sender_access
{
return &amp;parse_value_header_checks(@_);
}

@header_checks_actions = ( "REJECT", "HOLD", "REDIRECT", "DUNNO", "IGNORE",
			   "DISCARD", "FILTER",
			   "PREPEND", "REPLACE", "WARN" );

@check_sender_actions = ( "OK", "REJECT", "DISCARD", "FILTER", "PREPEND", 
        "REDIRECT", "WARN", "DUNNO" );			   

# get_master_config()
# Returns an array reference of entries from the Postfix master.cf file
sub get_master_config
{
if (!scalar(@master_config_cache)) {
	@master_config_cache = ( );
	local $lnum = 0;
	local $prog;
	open(MASTER, $config{'postfix_master'});
	while(&lt;MASTER&gt;) {
		s/\r|\n//g;
		if (/^(#?)\s*(\S+)\s+(inet|unix|fifo)\s+(y|n|\-)\s+(y|n|\-)\s+(y|n|\-)\s+(\S+)\s+(\S+)\s+(.*)$/) {
			# A program line
			$prog = { 'enabled' =&gt; !$1,
				  'name' =&gt; $2,
				  'type' =&gt; $3,
				  'private' =&gt; $4,
				  'unpriv' =&gt; $5,
				  'chroot' =&gt; $6,
				  'wakeup' =&gt; $7,
				  'maxprocs' =&gt; $8,
				  'command' =&gt; $9,
				  'line' =&gt; $lnum,
				  'eline' =&gt; $lnum,
				 };
			push(@master_config_cache, $prog);
			}
		elsif (/^(#?)\s+(.*)$/ &amp;&amp; $prog &amp;&amp;
		       $prog-&gt;{'eline'} == $lnum-1 &amp;&amp;
		       $prog-&gt;{'enabled'} == !$1) {
			# Continuation line
			$prog-&gt;{'command'} .= " ".$2;
			$prog-&gt;{'eline'} = $lnum;
			}
		$lnum++;
		}
	close(MASTER);
	}
return \@master_config_cache;
}

# create_master(&amp;master)
# Adds a new Postfix server process
sub create_master
{
local ($master) = @_;
local $conf = &amp;get_master_config();
local $lref = &amp;read_file_lines($config{'postfix_master'});
push(@$lref, &amp;master_line($master));
&amp;flush_file_lines($config{'postfix_master'});
$master-&gt;{'line'} = scalar(@$lref)-1;
$master-&gt;{'eline'} = scalar(@$lref)-1;
push(@$conf, $master);
}

# delete_master(&amp;master)
# Removes one Postfix server process
sub delete_master
{
local ($master) = @_;
local $conf = &amp;get_master_config();
local $lref = &amp;read_file_lines($config{'postfix_master'});
local $lines = $master-&gt;{'eline'} - $master-&gt;{'line'} + 1;
splice(@$lref, $master-&gt;{'line'}, $lines);
&amp;flush_file_lines($config{'postfix_master'});
@$conf = grep { $_ ne $master } @$conf;
foreach my $c (@$conf) {
	if ($c-&gt;{'line'} &gt; $master-&gt;{'eline'}) {
		$c-&gt;{'line'} -= $lines;
		$c-&gt;{'eline'} -= $lines;
		}
	}
}

# modify_master(&amp;master)
# Updates one Postfix server process
sub modify_master
{
local ($master) = @_;
local $conf = &amp;get_master_config();
local $lref = &amp;read_file_lines($config{'postfix_master'});
local $lines = $master-&gt;{'eline'} - $master-&gt;{'line'} + 1;
splice(@$lref, $master-&gt;{'line'}, $lines,
       &amp;master_line($master));
&amp;flush_file_lines($config{'postfix_master'});
foreach my $c (@$conf) {
	if ($c-&gt;{'line'} &gt; $master-&gt;{'eline'}) {
		$c-&gt;{'line'} -= $lines-1;
		$c-&gt;{'eline'} -= $lines-1;
		}
	}
}

# master_line(&amp;master)
sub master_line
{
local ($prog) = @_;
return ($prog-&gt;{'enabled'} ? "" : "#").
       join("\t", $prog-&gt;{'name'}, $prog-&gt;{'type'}, $prog-&gt;{'private'},
		  $prog-&gt;{'unpriv'}, $prog-&gt;{'chroot'}, $prog-&gt;{'wakeup'},
		  $prog-&gt;{'maxprocs'}, $prog-&gt;{'command'});
}

sub redirect_to_map_list
{
local ($map_name) = @_;
if ($map_name =~ /sender_dependent_default_transport_maps/) {
	redirect("dependent.cgi");
	}
elsif ($map_name =~ /transport/) { &amp;redirect("transport.cgi"); }
elsif ($map_name =~ /canonical/) { &amp;redirect("canonical.cgi"); }
elsif ($map_name =~ /virtual/) { &amp;redirect("virtual.cgi"); }
elsif ($map_name =~ /relocated/) { &amp;redirect("relocated.cgi"); }
elsif ($map_name =~ /header/) { &amp;redirect("header.cgi"); }
elsif ($map_name =~ /body/) { &amp;redirect("body.cgi"); }
elsif ($map_name =~ /sender_bcc/) { &amp;redirect("bcc.cgi?mode=sender"); }
elsif ($map_name =~ /recipient_bcc/) { &amp;redirect("bcc.cgi?mode=recipient"); }
elsif ($map_name =~ /^smtpd_client_restrictions:/) { &amp;redirect("client.cgi"); }
elsif ($map_name =~ /relay_recipient_maps|smtpd_sender_restrictions/) { &amp;redirect("smtpd.cgi"); }
else { &amp;redirect(""); }
}

sub regenerate_map_table
{
local ($map_name) = @_;
if ($map_name =~ /canonical/) { &amp;regenerate_canonical_table(); }
if ($map_name =~ /relocated/) { &amp;regenerate_relocated_table(); }
if ($map_name =~ /virtual/) { &amp;regenerate_virtual_table(); }
if ($map_name =~ /transport/) { &amp;regenerate_transport_table(); }
if ($map_name =~ /sender_access/) { &amp;regenerate_any_table($map_name); }
if ($map_name =~ /sender_bcc/) { &amp;regenerate_bcc_table(); }
if ($map_name =~ /recipient_bcc/) { &amp;regenerate_recipient_bcc_table(); }
if ($map_name =~ /smtpd_client_restrictions:(\S+)/) {
	&amp;regenerate_any_table("smtpd_client_restrictions",
			      undef, $1);
	}
if ($map_name =~ /relay_recipient_maps/) {
	&amp;regenerate_relay_recipient_table();
	}
if ($map_name =~ /sender_dependent_default_transport_maps/) {
	&amp;regenerate_dependent_table();
	}
if ($map_name =~ /smtpd_sender_restrictions/) {
	&amp;regenerate_sender_restrictions_table();
	}
}

# mailq_table(&amp;qfiles)
# Print a table of queued mail messages
sub mailq_table
{
local ($qfiles) = @_;

# Build table data
my @table;
foreach my $q (@$qfiles) {
	local @cols;
	push(@cols, { 'type' =&gt; 'checkbox', 'name' =&gt; 'file',
		      'value' =&gt; $q-&gt;{'id'} });
	push(@cols, &amp;ui_link("view_mailq.cgi?id=$q-&gt;{'id'}",$q-&gt;{'id'}));
	local $size = &amp;nice_size($q-&gt;{'size'});
	push(@cols, "&lt;font size=1&gt;$q-&gt;{'date'}&lt;/font&gt;");
	push(@cols, "&lt;font size=1&gt;".&amp;html_escape($q-&gt;{'from'})."&lt;/font&gt;");
	push(@cols, "&lt;font size=1&gt;".&amp;html_escape($q-&gt;{'to'})."&lt;/font&gt;");
	push(@cols, "&lt;font size=1&gt;$size&lt;/font&gt;");
	push(@cols, "&lt;font size=1&gt;".&amp;html_escape($q-&gt;{'status'})."&lt;/font&gt;");
	push(@table, \@cols);
	}

# Show the table and form
print &amp;ui_form_columns_table("delete_queues.cgi",
	[ [ undef, $text{'mailq_delete'} ],
	  &amp;compare_version_numbers($postfix_version, 1.1) &gt;= 0 ?
		( [ 'move', $text{'mailq_move'} ] ) : ( ),
	  &amp;compare_version_numbers($postfix_version, 2) &gt;= 0 ?
		( [ 'hold', $text{'mailq_hold'} ],
		  [ 'unhold', $text{'mailq_unhold'} ] ) : ( ),
	],
	1,
	undef,
	undef,
	[ "", $text{'mailq_id'}, $text{'mailq_date'}, $text{'mailq_from'},
          $text{'mailq_to'}, $text{'mailq_size'}, $text{'mailq_status'} ],
	100,
	\@table);
}

# is_table_comment(line, [force-prefix])
# Returns the comment text if a line contains a comment, like # foo
sub is_table_comment
{
local ($line, $force) = @_;
if ($config{'prefix_cmts'} || $force) {
	return $line =~ /^\s*#+\s*Webmin:\s*(.*)/ ? $1 : undef;
	}
else {
	return $line =~ /^\s*#+\s*(.*)/ ? $1 : undef;
	}
}

# make_table_comment(comment, [force-tag])
# Returns an array of lines for a comment in a map file, like # foo
sub make_table_comment
{
local ($cmt, $force) = @_;
if (!$cmt) {
	return ( );
	}
elsif ($config{'prefix_cmts'} || $force) {
	return ( "# Webmin: $cmt" );
	}
else {
	return ( "# $cmt" );
	}
}

# lock_postfix_files()
# Lock all Postfix config files
sub lock_postfix_files
{
&amp;lock_file($config{'postfix_config_file'});
&amp;lock_file($config{'postfix_master'});
}

# unlock_postfix_files()
# Un-lock all Postfix config files
sub unlock_postfix_files
{
&amp;unlock_file($config{'postfix_config_file'});
&amp;unlock_file($config{'postfix_master'});
}

# map_chooser_button(field, mapname)
# Returns HTML for a button for popping up a map file chooser
sub map_chooser_button
{
local ($name, $mapname) = @_;
return &amp;popup_window_button("map_chooser.cgi?mapname=$mapname", 1024, 600, 1,
			    [ [ "ifield", $name, "map" ] ]);
}

# get_maps_types_files(value)
# Converts a parameter like hash:/foo/bar,hash:/tmp/xxx to a list of types
# and file paths.
sub get_maps_types_files
{
my ($v) = @_;
my @rv;
foreach my $w (split(/[, \t]+/, $v)) {
	if ($w =~ /^(proxy:)?([^:]+):(\/.*)$/) {
		push(@rv, [ $2, $3 ]);
		}
	}
return @rv;
}

# list_mysql_sources()
# Returns a list of global MySQL source names in main.cf
sub list_mysql_sources
{
local @rv;
my $lref = &amp;read_file_lines($config{'postfix_config_file'});
foreach my $l (@$lref) {
	if ($l =~ /^\s*(\S+)_dbname\s*=/) {
		push(@rv, $1);
		}
	}
return @rv;
}

# get_backend_config(file)
# Returns a hash ref from names to values in some backend (ie. mysql or ldap)
# config file.
sub get_backend_config
{
local ($file) = @_;
local %rv;
local $lref = &amp;read_file_lines($file, 1);
foreach my $l (@$lref) {
	if ($l =~ /^\s*([a-z0-9\_]+)\s*=\s*(.*)/i) {
		$rv{$1} = $2;
		}
	}
return \%rv;
}

# save_backend_config(file, name, [value])
# Updates one setting in a backend config file
sub save_backend_config
{
local ($file, $name, $value) = @_;
local $lref = &amp;read_file_lines($file);
local $found = 0;
for(my $i=0; $i&lt;@$lref; $i++) {
	if ($lref-&gt;[$i] =~ /^\s*([a-z0-9\_]+)\s*=\s*(.*)/i &amp;&amp;
	    $1 eq $name) {
		# Found the line to fix
		if (defined($value)) {
			$lref-&gt;[$i] = "$name = $value";
			}
		else {
			splice(@$lref, $i, 1);
			}
		$found = 1;
		last;
		}
	}
if (!$found &amp;&amp; defined($value)) {
	push(@$lref, "$name = $value");
	}
}

# can_access_map(type, value)
# Checks if some map (such as a database) can be accessed
sub can_access_map
{
local ($type, $value) = @_;
if (&amp;file_map_type($type)) {
	return undef;	# Always can
	}
elsif ($type eq "mysql") {
	# Parse config, connect to DB
	local $conf;
	if ($value =~ /^[\/\.]/) {
		# Config file
		local $cfile = $value;
		if ($cfile !~ /^\//) {
			$cfile = &amp;guess_config_dir()."/".$cfile;
			}
		-r $cfile || return &amp;text('mysql_ecfile', "&lt;tt&gt;$cfile&lt;/tt&gt;");
		$conf = &amp;get_backend_config($cfile);
		}
	else {
		# Backend name
		$conf = &amp;mysql_value_to_conf($value);
		$conf-&gt;{'dbname'} || return &amp;text('mysql_esource', $value);
		}

	if (!$conf-&gt;{"query"}) {
		# Do we have the field and table info?
		foreach my $need ('table', 'select_field', 'where_field') {
			$conf-&gt;{$need} || return &amp;text('mysql_eneed', $need);
			}
		}
	# Try a connect, and a query
	local $dbh = &amp;connect_mysql_db($conf);
	if (!ref($dbh)) {
		return $dbh;
		}
	local $cmd = $dbh-&gt;prepare("select ".$conf-&gt;{'select_field'}." ".
				   "from ".$conf-&gt;{'table'}." ".
				   "where ".$conf-&gt;{'where_field'}." = ".
					    $conf-&gt;{'where_field'}." ".
				   "limit 1");
	if (!$cmd || !$cmd-&gt;execute()) {
		return &amp;text('mysql_equery',
			     "&lt;tt&gt;".$conf-&gt;{'table'}."&lt;/tt&gt;",
			     "&lt;tt&gt;".&amp;html_escape($dbh-&gt;errstr)."&lt;/tt&gt;");
		}
	$cmd-&gt;finish();
	$dbh-&gt;disconnect();
	return undef;
	}
elsif ($type eq "ldap") {
	# Parse config, connect to LDAP server
	local $conf = &amp;ldap_value_to_conf($value);
	$conf-&gt;{'search_base'} || return &amp;text('ldap_esource', $value);

	# Try a connect and a search
	local $ldap = &amp;connect_ldap_db($conf);
	if (!ref($ldap)) {
		return $ldap;
		}
	local @classes = split(/\s+/, $config{'ldap_class'} ||
				      "inetLocalMailRecipient");
	local $rv = $ldap-&gt;search(base =&gt; $conf-&gt;{'search_base'},
				  filter =&gt; "(objectClass=$classes[0])",
				  sizelimit =&gt; 1);
	if (!$rv || $rv-&gt;code &amp;&amp; !$rv-&gt;all_entries) {
		return &amp;text('ldap_ebase', "&lt;tt&gt;$conf-&gt;{'search_base'}&lt;/tt&gt;",
			     $rv ? $rv-&gt;error : "Unknown search error");
		}

	return undef;
	}
else {
	return &amp;text('map_unknown', "&lt;tt&gt;$type&lt;/tt&gt;");
	}
}

# connect_mysql_db(&amp;config)
# Attempts to connect to the Postfix MySQL database. Returns
# a driver handle on success, or an error message string on failure.
sub connect_mysql_db
{
local ($conf) = @_;
local $driver = "mysql";
local $drh;
eval &lt;&lt;EOF;
use DBI;
\$drh = DBI-&gt;install_driver(\$driver);
EOF
if ($@) {
	return &amp;text('mysql_edriver', "&lt;tt&gt;DBD::$driver&lt;/tt&gt;");
        }
local @hosts = split(/\s+/, $config{'mysql_hosts'} || $conf-&gt;{'hosts'});
@hosts = ( undef ) if (!@hosts);	# Localhost only
local $dbh;
foreach my $host (@hosts) {
	local $dbistr = "database=$conf-&gt;{'dbname'}";
	if ($host =~ /^unix:(.*)$/) {
		# Socket file
		$dbistr .= ";mysql_socket=$1";
		}
	elsif ($host) {
		# Remote host
		$dbistr .= ";host=$host";
		}
	$dbh = $drh-&gt;connect($dbistr,
			     $config{'mysql_user'} || $conf-&gt;{'user'},
			     $config{'mysql_pass'} || $conf-&gt;{'password'},
			     { });
	last if ($dbh);
	}
$dbh || return &amp;text('mysql_elogin',
		     "&lt;tt&gt;$conf-&gt;{'dbname'}&lt;/tt&gt;", $drh-&gt;errstr)."\n";
return $dbh;
}

# connect_ldap_db(&amp;config)
# Attempts to connect to an LDAP server with Postfix maps. Returns
# a driver handle on success, or an error message string on failure.
sub connect_ldap_db
{
local ($conf) = @_;
if (defined($connect_ldap_db_cache)) {
	return $connect_ldap_db_cache;
	}
eval "use Net::LDAP";
if ($@) {
	return &amp;text('ldap_eldapmod', "&lt;tt&gt;Net::LDAP&lt;/tt&gt;");
	}
local @servers = split(/\s+/, $config{'ldap_host'} ||
			      $conf-&gt;{'server_host'} || "localhost");
local ($ldap, $lasterr);
foreach my $server (@servers) {
	local ($host, $port, $tls);
	if ($server =~ /^(\S+):(\d+)$/) {
		# Host and port
		($host, $port) = ($1, $2);
		$tls = $conf-&gt;{'start_tls'} eq 'yes';
		}
	elsif ($server =~ /^(ldap|ldaps):\/\/(\S+)(:(\d+))?/) {
		# LDAP URL
		$host = $2;
		$port = $4 || $conf-&gt;{'server_port'} || 389;
		$tls = $1 eq "ldaps";
		}
	else {
		# Host only
		$host = $server;
		$port = $conf-&gt;{'server_port'} || 389;
		$tls = $conf-&gt;{'start_tls'} eq 'yes';
		}
	$ldap = Net::LDAP-&gt;new($server, port =&gt; $port);
	if (!$ldap) {
		$lasterr = &amp;text('ldap_eldap', "&lt;tt&gt;$server&lt;/tt&gt;", $port);
		next;
		}
	if ($tls) {
		$ldap-&gt;start_tls;
		}
	if ($conf-&gt;{'bind'} eq 'yes' || $config{'ldap_user'}) {
		local $mesg = $ldap-&gt;bind(
			dn =&gt; $config{'ldap_user'} || $conf-&gt;{'bind_dn'},
			password =&gt; $config{'ldap_pass'} || $conf-&gt;{'bind_pw'});
		if (!$mesg || $mesg-&gt;code) {
			$lasterr = &amp;text('ldap_eldaplogin',
				     "&lt;tt&gt;$server&lt;/tt&gt;",
				     "&lt;tt&gt;".($config{'ldap_user'} ||
					     $conf-&gt;{'bind_dn'})."&lt;/tt&gt;",
				     $mesg ? $mesg-&gt;error : "Unknown error");
			$ldap = undef;
			next;
			}
		}
	last if ($ldap);
	}
if ($ldap) {
	# Connected OK
	$connect_ldap_db_cache = $ldap;
	return $ldap;
	}
else {
	return $lasterr;
	}
}

# mysql_value_to_conf(value)
# Converts a MySQL config file or source name to a config hash ref
sub mysql_value_to_conf
{
local ($value) = @_;
local $conf;
if ($value =~ /^[\/\.]/) {
	# Config file
	local $cfile = $value;
	if ($cfile !~ /^\//) {
		$cfile = &amp;guess_config_dir()."/".$cfile;
		}
	-r $cfile || &amp;error(&amp;text('mysql_ecfile', "&lt;tt&gt;$cfile&lt;/tt&gt;"));
	$conf = &amp;get_backend_config($cfile);
		
	if ($conf-&gt;{'query'} =~ /^select\s+(\S+)\s+from\s+(\S+)\s+where\s+(\S+)\s*=\s*'\%s'/i &amp;&amp; !$conf-&gt;{'table'}) {
		# Try to extract table and fields from the query
		$conf-&gt;{'select_field'} = $1;
		$conf-&gt;{'table'} = $2;
		$conf-&gt;{'where_field'} = $3;
		}
	}
else {
	# Backend name
	$conf = { };
	foreach my $k ("hosts", "dbname", "user", "password", "query",
		       "table", "where_field", "select_field",
		       "additional_conditions") {
		local $v = &amp;get_real_value($value."_".$k);
		$conf-&gt;{$k} = $v;
		}
	if ($conf-&gt;{'query'} =~ /^select\s+(\S+)\s+from\s+(\S+)\s+where\s+(\S+)\s*=\s*'\%s'/i &amp;&amp; !$conf-&gt;{'table'}) {
		# Try to extract table and fields from the query
		$conf-&gt;{'select_field'} = $1;
		$conf-&gt;{'table'} = $2;
		$conf-&gt;{'where_field'} = $3;
		}
	}
return $conf;
}

# ldap_value_to_conf(value)
# Converts an LDAP config file name to a config hash ref
sub ldap_value_to_conf
{
local ($value) = @_;
local $conf;
local $cfile = $value;
if ($cfile !~ /^\//) {
	$cfile = &amp;guess_config_dir()."/".$cfile;
	}
-r $cfile &amp;&amp; !-d $cfile || &amp;error(&amp;text('ldap_ecfile', "&lt;tt&gt;$cfile&lt;/tt&gt;"));
return &amp;get_backend_config($cfile);
}

# can_map_comments(name)
# Returns 1 if some map can have comments. Not allowed for MySQL and LDAP.
sub can_map_comments
{
local ($name) = @_;
foreach my $tv (&amp;get_maps_types_files(&amp;get_real_value($name))) {
	return 0 if (!&amp;file_map_type($tv-&gt;[0]));
	}
return 1;
}

# can_map_manual(name)
# Returns 1 if osme map has a file that can be manually edited
sub can_map_manual
{
local ($name) = @_;
foreach my $tv (&amp;get_maps_types_files(&amp;get_real_value($name))) {
	return 0 if (!&amp;file_map_type($tv-&gt;[0]));
	}
return 1;
}

# supports_map_type(type)
# Returns 1 if a map of some type is supported by Postfix
sub supports_map_type
{
local ($type) = @_;
return 1 if ($type eq 'hash');	# Assume always supported
if (!scalar(@supports_map_type_cache)) {
	@supports_map_type = ( );
	open(POSTCONF, "$config{'postfix_config_command'} -m |");
	while(&lt;POSTCONF&gt;) {
		s/\r|\n//g;
		push(@supports_map_type_cache, $_);
		}
	close(POSTCONF);
	}
return &amp;indexoflc($type, @supports_map_type_cache) &gt;= 0;
}

# split_props(text)
# Converts multiple lines of text into LDAP attributes
sub split_props
{
local ($text) = @_;
local %pmap;
foreach $p (split(/\t+/, $text)) {
        if ($p =~ /^(\S+):\s*(.*)/) {
                push(@{$pmap{$1}}, $2);
                }
        }
local @rv;
local $k;
foreach $k (keys %pmap) {
        local $v = $pmap{$k};
        if (@$v == 1) {
                push(@rv, $k, $v-&gt;[0]);
                }
        else {
                push(@rv, $k, $v);
                }
        }
return @rv;
}

# list_smtpd_restrictions()
# Returns a list of SMTP server restrictions known to Webmin
sub list_smtpd_restrictions
{
return ( "permit_mynetworks",
	 "permit_inet_interfaces",
	 &amp;compare_version_numbers($postfix_version, 2.3) &lt; 0 ?
		"reject_unknown_client" :
		"reject_unknown_reverse_client_hostname",
	 "permit_sasl_authenticated",
	 "reject_unauth_destination",
	 "check_relay_domains",
	 "permit_mx_backup" );
}

# list_client_restrictions()
# Returns a list of boolean values for use in smtpd_client_restrictions
sub list_client_restrictions
{
return ( "permit_mynetworks",
	 "permit_inet_interfaces",
	 &amp;compare_version_numbers($postfix_version, 2.3) &lt; 0 ?
		"reject_unknown_client" :
		"reject_unknown_reverse_client_hostname",
	 "permit_tls_all_clientcerts",
	 "permit_sasl_authenticated",
	);
}

# list_multi_client_restrictions()
# Returns a list of restrictions that have a following value
sub list_multi_client_restrictions
{
return ( "check_client_access",
	 "reject_rbl_client",
	 "reject_rhsbl_client",
       );
}

sub file_map_type
{
local ($type) = @_;
return 1 if ($type eq 'hash' || $type eq 'regexp' || $type eq 'pcre' ||
	     $type eq 'btree' || $type eq 'dbm' || $type eq 'cidr');
}

# in_props(&amp;props, name)
# Looks up the value of a named property in a list
sub in_props
{
local ($props, $name) = @_;
for(my $i=0; $i&lt;@$props; $i++) {
	if (lc($props-&gt;[$i]) eq lc($name)) {
		return $props-&gt;[$i+1];
		}
	}
return undef;
}

# For calling from aliases-lib only
sub rebuild_map_cmd
{
return 0;
}

# valid_postfix_command(cmd)
# Check if some command exists on the system. Strips off args.
sub valid_postfix_command
{
my ($cmd) = @_;
($cmd) = &amp;split_quoted_string($cmd);
return &amp;has_command($cmd);
}

# get_all_config_files()
# Returns a list of all possible postfix config files
sub get_all_config_files
{
my @rv;

# Add main config file
push(@rv, $config{'postfix_config_file'});
push(@rv, $config{'postfix_master'});

# Add known map files
push(@rv, &amp;get_maps_files("alias_maps"));
push(@rv, &amp;get_maps_files("alias_database"));
push(@rv, &amp;get_maps_files("canonical_maps"));
push(@rv, &amp;get_maps_files("recipient_canonical_maps"));
push(@rv, &amp;get_maps_files("sender_canonical_maps"));
push(@rv, &amp;get_maps_files($virtual_maps));
push(@rv, &amp;get_maps_files("transport_maps"));
push(@rv, &amp;get_maps_files("relocated_maps"));
push(@rv, &amp;get_maps_files("relay_recipient_maps"));
push(@rv, &amp;get_maps_files("smtpd_sender_restrictions"));

# Add other files in /etc/postfix
local $cdir = &amp;guess_config_dir();
opendir(DIR, $cdir);
foreach $f (readdir(DIR)) {
	next if ($f eq "." || $f eq ".." || $f =~ /\.(db|dir|pag)$/i);
	push(@rv, "$cdir/$f");
	}
closedir(DIR);

return &amp;unique(@rv);
}

1;

</pre></body></html>