Handle hostnames with upper-case letters
[webmin.git] / sendmail / sendmail-lib.pl
1 # sendmail-lib.pl
2 # Functions for managing sendmail aliases, domains and mappings.
3 # Only sendmail versions 8.8 and above are supported
4
5 BEGIN { push(@INC, ".."); };
6 use WebminCore;
7 &init_config();
8 %access = &get_module_acl();
9 $features_access = $access{'opts'} && $access{'ports'} && $access{'cws'} && $access{'masq'} && $access{'trusts'} && $access{'vmode'} && $access{'amode'} && $access{'omode'} && $access{'cgs'} && $access{'relay'} && $access{'mailers'} && $access{'access'} && $access{'domains'};
10 $config{'perpage'} ||= 20;      # a value of 0 can cause problems
11 @port_modifier_flags = ( 'a', 'b', 'c', 'f', 'h', 'C', 'E' );
12
13 # get_sendmailcf()
14 # Parses sendmail.cf and return a reference to an array of options.
15 # Each line is a single character directive, followed by a list of values?
16 sub get_sendmailcf
17 {
18 if (!@sendmailcf_cache) {
19         local($lnum, $i);
20         $lnum = 0; $i = 0;
21         open(CF, $config{'sendmail_cf'});
22         while(<CF>) {
23                 s/^#.*$//g;     # remove comments
24                 s/\r|\n//g;     # remove newlines
25                 if (/^(\S)(\s*(.*))$/) {
26                         local(%opt);
27                         $opt{'type'} = $1;
28                         $opt{'value'} = $3;
29                         $opt{'values'} = [ split(/\s+/, $2) ];
30                         $opt{'line'} = $lnum;
31                         $opt{'eline'} = $opt{'line'};
32                         $opt{'pos'} = $i++;
33                         push(@sendmailcf_cache, \%opt);
34                         }
35                 $lnum++;
36                 }
37         close(CF);
38         }
39 return \@sendmailcf_cache;
40 }
41
42 # check_sendmail_version(&config)
43 # Is the sendmail config file a usable version?
44 sub check_sendmail_version
45 {
46 local $ver = &find_type("V", $_[0]);
47 return $ver && $ver->{'value'} =~ /^(\d+)/ && $1 >= 7 ? $1 : undef;
48 }
49
50 # get_sendmail_version(&out)
51 # Returns the actual sendmail executable version, if it is available
52 sub get_sendmail_version
53 {
54 local $out = &backquote_with_timeout("$config{'sendmail_path'} -d0 -bv 2>&1",
55                                      2, undef, 1);
56 local $version;
57 if ($out =~ /version\s+(\S+)/i) {
58         $version = $1;
59         }
60 ${$_[0]} = $out if ($_[0]);
61 return $version;
62 }
63
64 # save_directives(&config, &oldvalues, &newvalues)
65 # Given 2 arrays of directive structures, this function will replace the
66 # old ones with the new. If the old list is empty, new directives are added
67 # to the end of the config file. If the new list is empty, all old directives
68 # are removed. If both exist, new ones replace old..
69 sub save_directives
70 {
71 local(@old) = @{$_[1]};
72 local(@new) = @{$_[2]};
73 $lref = &read_file_lines($config{'sendmail_cf'});
74 for($i=0; $i<@old || $i<@new; $i++) {
75         if ($i >= @old) {
76                 # A new directive has been added.. put it at the end of the file
77                 $new[$i]->{'line'} = scalar(@$lref);
78                 $new[$i]->{'eline'} = $new[$i]->{'line'}+1;
79                 push(@$lref, &directive_line($new[$i]));
80                 push(@{$_[0]}, $new[$i]);
81                 }
82         elsif ($i >= @new) {
83                 # A directive was deleted
84                 $ol = $old[$i]->{'eline'} - $old[$i]->{'line'} + 1;
85                 splice(@$lref, $old[$i]->{'line'}, $ol);
86                 &renumber_list($_[0], $old[$i], -$ol);
87                 splice(@{$_[0]}, &indexof($old[$i], @{$_[0]}), 1);
88                 }
89         else {
90                 # A directive was changed
91                 $ol = $old[$i]->{'eline'} - $old[$i]->{'line'} + 1;
92                 splice(@$lref, $old[$i]->{'line'}, $ol,
93                        &directive_line($new[$i]));
94                 $new[$i]->{'line'} = $new[$i]->{'eline'} = $old[$i]->{'line'};
95                 &renumber_list($_[0], $old[$i], 1-$ol);
96                 $_[0]->[&indexof($old[$i], @{$_[0]})] = $new[$i];
97                 }
98         }
99 }
100
101 # directive_line(&details)
102 sub directive_line
103 {
104 return $_[0]->{'type'}.join(' ', @{$_[0]->{'values'}});
105 }
106
107 # find_type(name, &config)
108 # Returns an array of config directives of some type
109 sub find_type
110 {
111 local($c, @rv);
112 foreach $c (@{$_[1]}) {
113         if ($c->{'type'} eq $_[0]) {
114                 push(@rv, $c);
115                 }
116         }
117 return @rv ? wantarray ? @rv : $rv[0]
118            : wantarray ? () : undef;
119 }
120
121 # find_option(name, &config)
122 # Returns the structure and value of some option directive
123 sub find_option
124 {
125 local(@opts, $o);
126 @opts = &find_type("O", $_[1]);
127 foreach $o (@opts) {
128         if ($o->{'value'} =~ /^\s*([^=]+)=(.*)$/ && $1 eq $_[0]) {
129                 # found it.. return
130                 return wantarray ? ($o, $2) : $2;
131                 }
132         }
133 return undef;
134 }
135
136 # find_optionss(name, &config)
137 # Returns the structures and values of some option directive
138 sub find_options
139 {
140 local(@opts, $o);
141 @opts = &find_type("O", $_[1]);
142 foreach $o (@opts) {
143         if ($o->{'value'} =~ /^\s*([^=]+)=(.*)$/ && $1 eq $_[0]) {
144                 push(@rv, [ $o, $2 ]);
145                 }
146         }
147 return wantarray ? @rv : $rv[0];
148 }
149
150 # find_type2(type1, type2, &config)
151 # Returns the structure and value of some directive
152 sub find_type2
153 {
154 local @types = &find_type($_[0], $_[2]);
155 local $t;
156 foreach $t (@types) {
157         if ($t->{'value'} =~ /^(\S)(.*)$/ && $1 eq $_[1]) {
158                 return ($t, $2);
159                 }
160         }
161 return undef;
162 }
163
164 # restart_sendmail()
165 # Send a SIGHUP to sendmail
166 sub restart_sendmail
167 {
168 if ($config{'sendmail_restart_command'}) {
169         # Use the restart command
170         local $out = &backquote_logged("$config{'sendmail_restart_command'} 2>&1 </dev/null");
171         return $? || $out =~ /failed|error/i ? "<pre>$out</pre>" : undef;
172         }
173 else {
174         # Just HUP the process
175         local ($pid, $any);
176         foreach my $pidfile (split(/\t+/, $config{'sendmail_pid'})) {
177                 if (open(PID, $pidfile)) {
178                         chop($pid = <PID>);
179                         close(PID);
180                         if ($pid) { &kill_logged('HUP', $pid); }
181                         $any++;
182                         }
183                 }
184         if (!$any) {
185                 local @pids = &find_byname("sendmail");
186                 @pids || return $text{'restart_epids'};
187                 &kill_logged('HUP', @pids) ||
188                         return &text('restart_ekill', $!);
189                 }
190         return undef;
191         }
192 }
193
194 # run_makemap(textfile, dbmfile, type)
195 # Run makemap to rebuild some map. Calls error if it fails.
196 sub run_makemap
197 {
198 local($out);
199 $out = &backquote_logged(
200         $config{'makemap_path'}." ".quotemeta($_[2])." ".quotemeta($_[1]).
201         " <".quotemeta($_[0])." 2>&1");
202 if ($?) { &error("makemap failed : <pre>".
203                  &html_escape($out)."</pre>"); }
204 }
205
206 # rebuild_map_cmd(textfile)
207 # If a map rebuild command is defined, run it and return 1, otherwise return 0.
208 # Calls error if it fails.
209 sub rebuild_map_cmd
210 {
211 local ($file) = @_;
212 if ($config{'rebuild_cmd'}) {
213         local $cmd = &substitute_template($config{'rebuild_cmd'},
214                                           { 'map_file' => $file });
215         local $out = &backquote_logged("($cmd) 2>&1");
216         if ($?) { &error("Map rebuild failed : <pre>".
217                          &html_escape($out)."</pre>"); }
218         return 1;
219         }
220 return 0;
221 }
222
223 # find_textfile(config, dbm)
224 sub find_textfile
225 {
226 local($conf, $dbm) = @_;
227 if ($conf) { return $conf; }
228 elsif (!$dbm) { return undef; }
229 elsif ($dbm =~ /^(.*)\.(db|dbm|pag|dir|hash)$/i && -r $1) {
230         # Database is like /etc/virtusertable.db, text is /etc/virtusertable
231         return $1;
232         }
233 elsif ($dbm =~ /^(.*)\.(db|dbm|pag|dir|hash)$/i && -r "$1.txt") {
234         # Database is like /etc/virtusertable.db, text is /etc/virtusertable.txt
235         return "$1.txt";
236         }
237 elsif (-r "$dbm.txt") {
238         # Database is like /etc/virtusertable, text is /etc/virtusertable.txt
239         return "$dbm.txt";
240         }
241 elsif ($dbm =~ /^(.*)\.(db|dbm|pag|dir|hash)$/i) {
242         # Database is like /etc/virtusertable.db, text is /etc/virtusertable,
243         # but doesn't exist yet.
244         return $1;
245         }
246 else {
247         # Text and database have same name
248         return $dbm;
249         }
250 }
251
252 # mailq_dir($conf)
253 sub mailq_dir
254 {
255 local ($opt, $mqueue) = &find_option("QueueDirectory", $_[0]);
256 local @rv;
257 if (!$mqueue) { @rv = ( "/var/spool/mqueue" ); }
258 elsif ($mqueue =~ /\*|\?/) {
259         @rv = split(/\s+/, `echo $mqueue`);
260         }
261 else {
262         @rv = ( $mqueue );
263         }
264 push(@rv, split(/\s+/, $config{'queue_dirs'}));
265 return @rv;
266 }
267
268 sub sort_by_domain
269 {
270 local ($a1, $a2, $b1, $b2);
271 if ($a->{'from'} =~ /^(.*)\@(.*)$/ && (($a1, $a2) = ($1, $2)) &&
272     $b->{'from'} =~ /^(.*)\@(.*)$/ && (($b1, $b2) = ($1, $2))) {
273         return $a2 cmp $b2 ? $a2 cmp $b2 : $a1 cmp $b1;
274         }
275 else {
276         return $a->{'from'} cmp $b->{'from'};
277         }
278 }
279
280 # can_view_qfile(&mail)
281 # Returns 1 if some queued message can be viewed, 0 if not
282 sub can_view_qfile
283 {
284 return 1 if (!$access{'qdoms'});
285 local $re = $access{'qdoms'};
286 if ($access{'qdomsmode'} == 0) {
287         return $_[0]->{'header'}->{'from'} =~ /$re/i;
288         }
289 elsif ($access{'qdomsmode'} == 0) {
290         return $_[0]->{'header'}->{'to'} =~ /$re/i;
291         }
292 else {
293         return $_[0]->{'header'}->{'from'} =~ /$re/i ||
294                $_[0]->{'header'}->{'to'} =~ /$re/i;
295         }
296 }
297
298 # renumber_list(&list, &position-object, lines-offset)
299 sub renumber_list
300 {
301 return if (!$_[2]);
302 local $e;
303 foreach $e (@{$_[0]}) {
304         if (!defined($e->{'file'}) || $e->{'file'} eq $_[1]->{'file'}) {
305                 $e->{'line'} += $_[2] if ($e->{'line'} > $_[1]->{'line'});
306                 $e->{'eline'} += $_[2] if (defined($e->{'eline'}) &&
307                                            $e->{'eline'} > $_[1]->{'eline'});
308                 }
309         }
310 }
311
312 # get_file_or_config(&config, suffix, [additional-conf], [&cwfile])
313 # Returns all values for some config file entries, which may be in sendmail.cf
314 # (like Cw) or externally (like Fw)
315 sub get_file_or_config
316 {
317 local ($conf, $suffix, $addit, $cwref) = @_;
318 local ($cwfile, $f);
319 foreach $f (&find_type("F", $conf)) {
320         if ($f->{'value'} =~ /^${suffix}[^\/]*(\/\S+)/ ||
321             $f->{'value'} =~ /^\{${suffix}\}[^\/]*(\/\S+)/) {
322                 $cwfile = $1;
323                 }
324         }
325 local @rv;
326 if ($cwfile) {
327         # get entries listed in a separate file
328         $$cwref = $cwfile if ($cwref);
329         open(CW, $cwfile);
330         while(<CW>) {
331                 s/\r|\n//g;
332                 s/#.*$//g;
333                 if (/\S/) { push(@rv, $_); }
334                 }
335         close(CW);
336         }
337 else {
338         $$cwref = undef if ($cwref);
339         }
340 # Add entries from sendmail.cf
341 foreach $f (&find_type("C", $conf)) {
342         if ($f->{'value'} =~ /^${suffix}\s*(.*)$/ ||
343             $f->{'value'} =~ /^\{${suffix}\}\s*(.*)$/) {
344                 push(@rv, split(/\s+/, $1));
345                 }
346         }
347 if ($addit) {
348         push(@rv, map { $_->{'value'} } &find_type($addit, $conf));
349         }
350 return &unique(@rv);
351 }
352
353 # save_file_or_config(&conf, suffix, &values, [additional-conf])
354 # Updates the values in some external file or in sendmail.cf
355 sub save_file_or_config
356 {
357 local ($conf, $suffix, $values, $addit) = @_;
358 local ($cwfile, $f);
359 foreach $f (&find_type("F", $conf)) {
360         if ($f->{'value'} =~ /^${suffix}[^\/]*(\/\S+)/ ||
361             $f->{'value'} =~ /^\{${suffix}\}[^\/]*(\/\S+)/) {
362                 $cwfile = $1;
363                 }
364         }
365 local @old = grep { $_->{'value'} =~ /^${suffix}/ ||
366                     $_->{'value'} =~ /^\{${suffix}\}/ } &find_type("C", $conf);
367 if ($addit) {
368         push(@old, &find_type($addit, $conf));
369         }
370 local @new;
371 local $d;
372 if ($cwfile) {
373         # If there is a .cw file, write all entries to it and take any
374         # out of sendmail.cf
375         &open_tempfile(CW, ">$cwfile");
376         foreach $d (@$values) {
377                 &print_tempfile(CW, $d,"\n");
378                 }
379         &close_tempfile(CW);
380         }
381 else {
382         # Stick all entries in sendmail.cf
383         foreach $d (@$values) {
384                 push(@new, { 'type' => 'C',
385                              'values' => [ $suffix.$d ] });
386                 }
387         }
388 &save_directives($conf, \@old, \@new);
389 }
390
391 # add_file_or_config(&config, suffix, value)
392 # Adds an entry to sendmail.cf or an external file
393 sub add_file_or_config
394 {
395 local ($conf, $suffix, $value) = @_;
396 local ($cwfile, $f);
397 foreach $f (&find_type("F", $conf)) {
398         if ($f->{'value'} =~ /^${suffix}[^\/]*(\/\S+)/ ||
399             $f->{'value'} =~ /^\{${suffix}\}[^\/]*(\/\S+)/) {
400                 $cwfile = $1;
401                 }
402         }
403 local @old = grep { $_->{'value'} =~ /^${suffix}/ ||
404                     $_->{'value'} =~ /^\{${suffix}\}/ } &find_type("C", $conf);
405 if ($cwfile) {
406         # Add to external file
407         &open_tempfile(CW, ">>$cwfile");
408         &print_tempfile(CW, $value,"\n");
409         &close_tempfile(CW);
410         }
411 else {
412         # Add to sendmail.cf
413         local @new = ( @old, { 'type' => 'C',
414                                'values' => [ $suffix.$value ] });
415         &save_directives($conf, \@old, \@new);
416         }
417 }
418
419 # delete_file_or_config(&config, suffix, value)
420 # Removes an entry from sendmail.cf or an external file
421 sub delete_file_or_config
422 {
423 local ($conf, $suffix, $value) = @_;
424 local ($cwfile, $f);
425 foreach $f (&find_type("F", $conf)) {
426         if ($f->{'value'} =~ /^${suffix}[^\/]*(\/\S+)/ ||
427             $f->{'value'} =~ /^\{${suffix}\}[^\/]*(\/\S+)/) {
428                 $cwfile = $1;
429                 }
430         }
431 local @old = grep { $_->{'value'} =~ /^${suffix}/ ||
432                     $_->{'value'} =~ /^\{${suffix}\}/ } &find_type("C", $conf);
433 if ($cwfile) {
434         # Remove from external file
435         local $lref = &read_file_lines($cwfile);
436         @$lref = grep { $_ !~ /^\s*\Q$value\E\s*$/i } @$lref;
437         &flush_file_lines($cwfile);
438         }
439 else {
440         # Remove from sendmail.cf
441         local @new = grep { $_->{'values'}->[0] ne $suffix.$value } @old;
442         &save_directives($conf, \@old, \@new);
443         }
444 }
445
446 # list_mail_queue([&conf])
447 # Returns a list of all files in the mail queue
448 sub list_mail_queue
449 {
450 local ($mqueue, @qfiles);
451 local $conf = $_[0] || &get_sendmailcf();
452 foreach $mqueue (&mailq_dir($conf)) {
453         opendir(QDIR, $mqueue);
454         push(@qfiles, map { "$mqueue/$_" } grep { /^(qf|hf|Qf)/ } readdir(QDIR));
455         closedir(QDIR);
456         }
457 return @qfiles;
458 }
459
460 # list_dontblames()
461 # Returns an array of valid options for the DontBlameSendmail option
462 sub list_dontblames
463 {
464 local @rv;
465 open(BLAME, "$module_root_directory/dontblames");
466 while(<BLAME>) {
467         s/\r|\n//g;
468         s/^\s*#.*$//;
469         if (/^(\S+)\s+(\S.*)$/) {
470                 push(@rv, [ $1, $2 ]);
471                 }
472         }
473 close(BLAME);
474 return @rv;
475 }
476
477 # stop_sendmail()
478 # Stops the sendmail process, returning undef on success or an error message
479 # upon failure.
480 sub stop_sendmail
481 {
482 if ($config{'sendmail_stop_command'}) {
483         local $out = &backquote_logged("$config{'sendmail_stop_command'} </dev/null 2>&1");
484         if ($?) {
485                 return "<pre>$out</pre>";
486                 }
487         }
488 else {
489         foreach my $pidfile (split(/\t+/, $config{'sendmail_pid'})) {
490                 local $pid = &check_pid_file($pidfile);
491                 if ($pid && &kill_logged('KILL', $pid)) {
492                         unlink($pidfile);
493                         }
494                 else {
495                         return $text{'stop_epid'};
496                         }
497                 }
498         }
499 return undef;
500 }
501
502 # start_sendmail()
503 # Starts the sendmail server, returning undef on success or an error message
504 # upon failure.
505 sub start_sendmail
506 {
507 if ($config{'sendmail_stop_command'}) {
508         # Make sure any init script lock files are gone
509         &backquote_logged("$config{'sendmail_stop_command'} </dev/null 2>&1");
510         }
511 local $out = &backquote_logged("$config{'sendmail_command'} </dev/null 2>&1");
512 return $? ? "<pre>$out</pre>" : undef;
513 }
514
515 sub is_sendmail_running
516 {
517 if ($config{'sendmail_smf'}) {
518         # Ask SMF, as on Solaris there is no PID file
519         local $out = &backquote_command("svcs -H -o STATE ".
520                         quotemeta($config{'sendmail_smf'})." 2>&1");
521         if ($?) {
522                 &error("Failed to get Sendmail status from SMF : $out");
523                 }
524         return $out =~ /online/i ? 1 : 0;
525         }
526 else {
527         # Use PID files, or check for process
528         local @pidfiles = split(/\t+/, $config{'sendmail_pid'});
529         if (@pidfiles) {
530                 foreach my $p (@pidfiles) {
531                         local $c = &check_pid_file($p);
532                         return $c if ($c);
533                         }
534                 return undef;
535                 }
536         else {
537                 return &find_byname("sendmail");
538                 }
539         }
540 }
541
542 # mailq_table(&qfiles)
543 # Print a table showing queued emails. Returns the number quarantined.
544 sub mailq_table
545 {
546 local ($qfiles, $qmails) = @_;
547 local $quarcount;
548
549 # Show buttons to flush and delete
550 print "<form action=del_mailqs.cgi method=post>\n";
551 local @links = ( &select_all_link("file", 0),
552                  &select_invert_link("file", 0) );
553 if ($config{'top_buttons'}) {
554         if ($access{'mailq'} == 2) {
555                 print "<input type=submit value='$text{'mailq_delete'}'>\n";
556                 print "<input type=checkbox name=locked value=1> $text{'mailq_locked'}\n";
557                 print "&nbsp;&nbsp;\n";
558                 print "<input type=submit name=flush value='$text{'mailq_flushsel'}'>\n";
559                 print "<p>\n";
560                 print &ui_links_row(\@links);
561                 }
562         }
563
564 # Generate table header
565 local (@hcols, @tds);
566 if ($access{'mailq'} == 2) {
567         push(@hcols, "");
568         push(@tds, "width=5");
569         }
570 local %show;
571 foreach my $s (split(/,/, $config{'mailq_show'})) {
572         $show{$s}++;
573         }
574 push(@hcols, $text{'mailq_id'});
575 push(@hcols, $text{'mailq_sent'}) if ($show{'Date'});
576 push(@hcols, $text{'mailq_from'}) if ($show{'From'});
577 push(@hcols, $text{'mailq_to'}) if ($show{'To'});
578 push(@hcols, $text{'mailq_cc'}) if ($show{'Cc'});
579 push(@hcols, $text{'mailq_subject'}) if ($show{'Subject'});
580 push(@hcols, $text{'mailq_size'}) if ($show{'Size'});
581 push(@hcols, $text{'mailq_status'}) if ($show{'Status'});
582 push(@hcols, $text{'mailq_dir'}) if ($show{'Dir'});
583 print &ui_columns_start(\@hcols, 100, 0, \@tds);
584
585 # Show table rows for emails
586 foreach my $f (@$qfiles) {
587         local $n;
588         ($n = $f) =~ s/^.*\///;
589         local $mail = $qmails->{$f} || &mail_from_queue($f);
590         next if (!$mail);
591         local $dir = $f;
592         $dir =~ s/\/[^\/]+$//;
593
594         $mail->{'header'}->{'from'} ||= $text{'mailq_unknown'};
595         $mail->{'header'}->{'to'} ||= $text{'mailq_unknown'};
596         $mail->{'header'}->{'date'} ||= $text{'mailq_unknown'};
597         $mail->{'header'}->{'subject'} ||= $text{'mailq_unknown'};
598         $mail->{'header'}->{'cc'} ||= "&nbsp;";
599         if ($mail->{'quar'}) {
600                 $mail->{'status'} = $text{'mailq_quar'};
601                 $quarcount++;
602                 }
603         $mail->{'status'} ||= $text{'mailq_sending'};
604
605         $mail->{'header'}->{'from'} =
606                 &html_escape($mail->{'header'}->{'from'});
607         $mail->{'header'}->{'to'} =
608                 &html_escape($mail->{'header'}->{'to'});
609         $mail->{'header'}->{'date'} =~ s/\+.*//g;
610
611         local @cols;
612         $size = &nice_size($mail->{'size'});
613         if ($access{'mailq'} == 2) {
614                 push(@cols, "<a href=\"view_mailq.cgi?".
615                             "file=$f\">$n</a>");
616                 }
617         else {
618                 push(@cols, $n);
619                 }
620         push(@cols, "<font size=1>".&simplify_date($mail->{'header'}->{'date'}, "ymd")."</font>") if ($show{'Date'});
621         push(@cols, "<font size=1>$mail->{'header'}->{'from'}</font>") if ($show{'From'});
622         push(@cols, "<font size=1>$mail->{'header'}->{'to'}</font>") if ($show{'To'});
623         push(@cols, "<font size=1>$mail->{'header'}->{'cc'}</font>") if ($show{'Cc'});
624         push(@cols, "<font size=1>$mail->{'header'}->{'subject'}</font>") if ($show{'Subject'});
625         push(@cols, "<font size=1>$size</font>") if ($show{'Size'});
626         push(@cols, "<font size=1>$mail->{'status'}</font>") if ($show{'Status'});
627         push(@cols, "<font size=1>$dir</font>") if ($show{'Dir'});
628         print "</tr>\n";
629         if ($access{'mailq'} == 2) {
630                 print &ui_checked_columns_row(\@cols, \@tds, "file",$f);
631                 }
632         else {
633                 print &ui_columns_row(\@cols, \@tds);
634                 }
635         }
636 print &ui_columns_end();
637 if ($access{'mailq'} == 2) {
638         print &ui_links_row(\@links);
639         print "<input type=submit value='$text{'mailq_delete'}'>\n";
640         print "<input type=checkbox name=locked value=1> $text{'mailq_locked'}\n";
641
642         print "&nbsp;&nbsp;\n";
643         print "<input type=submit name=flush value='$text{'mailq_flushsel'}'>\n";
644         print "<p>\n";
645         }
646 print "</form>\n";
647 return $quarcount;
648 }
649
650 # is_table_comment(line, [force-prefix])
651 # Returns the comment text if a line contains a comment, like # foo
652 sub is_table_comment
653 {
654 local ($line, $force) = @_;
655 if ($config{'prefix_cmts'} || $force) {
656         return $line =~ /^\s*#+\s*Webmin:\s*(.*)/ ? $1 : undef;
657         }
658 else {
659         return $line =~ /^\s*#+\s*(.*)/ ? $1 : undef;
660         }
661 }
662
663 # make_table_comment(comment, [force-tag])
664 # Returns an array of lines for a comment in a map file, like # foo
665 sub make_table_comment
666 {
667 local ($cmt, $force) = @_;
668 if (!$cmt) {
669         return ( );
670         }
671 elsif ($config{'prefix_cmts'} || $force) {
672         return ( "# Webmin: $cmt" );
673         }
674 else {
675         return ( "# $cmt" );
676         }
677 }
678
679 1;
680