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