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