1# sendmail-lib.pl
2# Functions for managing sendmail aliases, domains and mappings.
3# Only sendmail versions 8.8 and above are supported
4
5BEGIN { push(@INC, ".."); };
6use 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?
16sub get_sendmailcf
17{
18if (!@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	}
39return \@sendmailcf_cache;
40}
41
42# check_sendmail_version(&config)
43# Is the sendmail config file a usable version?
44sub check_sendmail_version
45{
46local $ver = &find_type("V", $_[0]);
47return $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
52sub get_sendmail_version
53{
54local $out = &backquote_with_timeout("$config{'sendmail_path'} -d0 -bv 2>&1",
55				     2, undef, 1);
56local $version;
57if ($out =~ /version\s+(\S+)/i) {
58	$version = $1;
59	}
60${$_[0]} = $out if ($_[0]);
61return $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..
69sub save_directives
70{
71local(@old) = @{$_[1]};
72local(@new) = @{$_[2]};
73$lref = &read_file_lines($config{'sendmail_cf'});
74for($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)
102sub directive_line
103{
104return $_[0]->{'type'}.join(' ', @{$_[0]->{'values'}});
105}
106
107# find_type(name, &config)
108# Returns an array of config directives of some type
109sub find_type
110{
111local($c, @rv);
112foreach $c (@{$_[1]}) {
113	if ($c->{'type'} eq $_[0]) {
114		push(@rv, $c);
115		}
116	}
117return @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
123sub find_option
124{
125local(@opts, $o);
126@opts = &find_type("O", $_[1]);
127foreach $o (@opts) {
128	if ($o->{'value'} =~ /^\s*([^=]+)=(.*)$/ && $1 eq $_[0]) {
129		# found it.. return
130		return wantarray ? ($o, $2) : $2;
131		}
132	}
133return undef;
134}
135
136# find_optionss(name, &config)
137# Returns the structures and values of some option directive
138sub find_options
139{
140local(@opts, $o);
141@opts = &find_type("O", $_[1]);
142foreach $o (@opts) {
143	if ($o->{'value'} =~ /^\s*([^=]+)=(.*)$/ && $1 eq $_[0]) {
144		push(@rv, [ $o, $2 ]);
145		}
146	}
147return wantarray ? @rv : $rv[0];
148}
149
150# find_type2(type1, type2, &config)
151# Returns the structure and value of some directive
152sub find_type2
153{
154local @types = &find_type($_[0], $_[2]);
155local $t;
156foreach $t (@types) {
157	if ($t->{'value'} =~ /^(\S)(.*)$/ && $1 eq $_[1]) {
158		return ($t, $2);
159		}
160	}
161return undef;
162}
163
164# restart_sendmail()
165# Send a SIGHUP to sendmail
166sub restart_sendmail
167{
168if ($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	}
173else {
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.
196sub run_makemap
197{
198local($out);
199$out = &backquote_logged(
200	$config{'makemap_path'}." ".quotemeta($_[2])." ".quotemeta($_[1]).
201	" <".quotemeta($_[0])." 2>&1");
202if ($?) { &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.
209sub rebuild_map_cmd
210{
211local ($file) = @_;
212if ($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	}
220return 0;
221}
222
223# find_textfile(config, dbm)
224sub find_textfile
225{
226local($conf, $dbm) = @_;
227if ($conf) { return $conf; }
228elsif (!$dbm) { return undef; }
229elsif ($dbm =~ /^(.*)\.(db|dbm|pag|dir|hash)$/i && -r $1) {
230	# Database is like /etc/virtusertable.db, text is /etc/virtusertable
231	return $1;
232	}
233elsif ($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	}
237elsif (-r "$dbm.txt") {
238	# Database is like /etc/virtusertable, text is /etc/virtusertable.txt
239	return "$dbm.txt";
240	}
241elsif ($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	}
246else {
247	# Text and database have same name
248	return $dbm;
249	}
250}
251
252# mailq_dir($conf)
253sub mailq_dir
254{
255local ($opt, $mqueue) = &find_option("QueueDirectory", $_[0]);
256local @rv;
257if (!$mqueue) { @rv = ( "/var/spool/mqueue" ); }
258elsif ($mqueue =~ /\*|\?/) {
259	@rv = split(/\s+/, `echo $mqueue`);
260	}
261else {
262	@rv = ( $mqueue );
263	}
264push(@rv, split(/\s+/, $config{'queue_dirs'}));
265return @rv;
266}
267
268sub sort_by_domain
269{
270local ($a1, $a2, $b1, $b2);
271if ($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	}
275else {
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
282sub can_view_qfile
283{
284return 1 if (!$access{'qdoms'});
285local $re = $access{'qdoms'};
286if ($access{'qdomsmode'} == 0) {
287	return $_[0]->{'header'}->{'from'} =~ /$re/i;
288	}
289elsif ($access{'qdomsmode'} == 0) {
290	return $_[0]->{'header'}->{'to'} =~ /$re/i;
291	}
292else {
293	return $_[0]->{'header'}->{'from'} =~ /$re/i ||
294	       $_[0]->{'header'}->{'to'} =~ /$re/i;
295	}
296}
297
298# renumber_list(&list, &position-object, lines-offset)
299sub renumber_list
300{
301return if (!$_[2]);
302local $e;
303foreach $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)
315sub get_file_or_config
316{
317local ($conf, $suffix, $addit, $cwref) = @_;
318local ($cwfile, $f);
319foreach $f (&find_type("F", $conf)) {
320	if ($f->{'value'} =~ /^${suffix}[^\/]*(\/\S+)/ ||
321	    $f->{'value'} =~ /^\{${suffix}\}[^\/]*(\/\S+)/) {
322		$cwfile = $1;
323		}
324	}
325local @rv;
326if ($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	}
337else {
338	$$cwref = undef if ($cwref);
339	}
340# Add entries from sendmail.cf
341foreach $f (&find_type("C", $conf)) {
342	if ($f->{'value'} =~ /^${suffix}\s*(.*)$/ ||
343	    $f->{'value'} =~ /^\{${suffix}\}\s*(.*)$/) {
344		push(@rv, split(/\s+/, $1));
345		}
346	}
347if ($addit) {
348	push(@rv, map { $_->{'value'} } &find_type($addit, $conf));
349	}
350return &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
355sub save_file_or_config
356{
357local ($conf, $suffix, $values, $addit) = @_;
358local ($cwfile, $f);
359foreach $f (&find_type("F", $conf)) {
360	if ($f->{'value'} =~ /^${suffix}[^\/]*(\/\S+)/ ||
361	    $f->{'value'} =~ /^\{${suffix}\}[^\/]*(\/\S+)/) {
362		$cwfile = $1;
363		}
364	}
365local @old = grep { $_->{'value'} =~ /^${suffix}/ ||
366		    $_->{'value'} =~ /^\{${suffix}\}/ } &find_type("C", $conf);
367if ($addit) {
368	push(@old, &find_type($addit, $conf));
369	}
370local @new;
371local $d;
372if ($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	}
381else {
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
393sub add_file_or_config
394{
395local ($conf, $suffix, $value) = @_;
396local ($cwfile, $f);
397foreach $f (&find_type("F", $conf)) {
398	if ($f->{'value'} =~ /^${suffix}[^\/]*(\/\S+)/ ||
399	    $f->{'value'} =~ /^\{${suffix}\}[^\/]*(\/\S+)/) {
400		$cwfile = $1;
401		}
402	}
403local @old = grep { $_->{'value'} =~ /^${suffix}/ ||
404		    $_->{'value'} =~ /^\{${suffix}\}/ } &find_type("C", $conf);
405if ($cwfile) {
406	# Add to external file
407	&open_tempfile(CW, ">>$cwfile");
408	&print_tempfile(CW, $value,"\n");
409	&close_tempfile(CW);
410	}
411else {
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
421sub delete_file_or_config
422{
423local ($conf, $suffix, $value) = @_;
424local ($cwfile, $f);
425foreach $f (&find_type("F", $conf)) {
426	if ($f->{'value'} =~ /^${suffix}[^\/]*(\/\S+)/ ||
427	    $f->{'value'} =~ /^\{${suffix}\}[^\/]*(\/\S+)/) {
428		$cwfile = $1;
429		}
430	}
431local @old = grep { $_->{'value'} =~ /^${suffix}/ ||
432		    $_->{'value'} =~ /^\{${suffix}\}/ } &find_type("C", $conf);
433if ($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	}
439else {
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
448sub list_mail_queue
449{
450local ($mqueue, @qfiles);
451local $conf = $_[0] || &get_sendmailcf();
452foreach $mqueue (&mailq_dir($conf)) {
453	opendir(QDIR, $mqueue);
454	push(@qfiles, map { "$mqueue/$_" } grep { /^(qf|hf|Qf)/ } readdir(QDIR));
455	closedir(QDIR);
456	}
457return @qfiles;
458}
459
460# list_dontblames()
461# Returns an array of valid options for the DontBlameSendmail option
462sub list_dontblames
463{
464local @rv;
465open(BLAME, "<$module_root_directory/dontblames");
466while(<BLAME>) {
467	s/\r|\n//g;
468	s/^\s*#.*$//;
469	if (/^(\S+)\s+(\S.*)$/) {
470		push(@rv, [ $1, $2 ]);
471		}
472	}
473close(BLAME);
474return @rv;
475}
476
477# stop_sendmail()
478# Stops the sendmail process, returning undef on success or an error message
479# upon failure.
480sub stop_sendmail
481{
482if ($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	}
488else {
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	}
499return undef;
500}
501
502# start_sendmail()
503# Starts the sendmail server, returning undef on success or an error message
504# upon failure.
505sub start_sendmail
506{
507if ($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	}
511local $out = &backquote_logged("$config{'sendmail_command'} </dev/null 2>&1");
512return $? ? "<pre>$out</pre>" : undef;
513}
514
515sub is_sendmail_running
516{
517if ($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	}
526else {
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.
544sub mailq_table
545{
546local ($qfiles, $qmails) = @_;
547local $quarcount;
548
549# Show buttons to flush and delete
550print "<form action=del_mailqs.cgi method=post>\n";
551local @links = ( &select_all_link("file", 0),
552	         &select_invert_link("file", 0) );
553if ($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
565local (@hcols, @tds);
566if ($access{'mailq'} == 2) {
567	push(@hcols, "");
568	push(@tds, "width=5");
569	}
570local %show;
571foreach my $s (split(/,/, $config{'mailq_show'})) {
572	$show{$s}++;
573	}
574push(@hcols, $text{'mailq_id'});
575push(@hcols, $text{'mailq_sent'}) if ($show{'Date'});
576push(@hcols, $text{'mailq_from'}) if ($show{'From'});
577push(@hcols, $text{'mailq_to'}) if ($show{'To'});
578push(@hcols, $text{'mailq_cc'}) if ($show{'Cc'});
579push(@hcols, $text{'mailq_subject'}) if ($show{'Subject'});
580push(@hcols, $text{'mailq_size'}) if ($show{'Size'});
581push(@hcols, $text{'mailq_status'}) if ($show{'Status'});
582push(@hcols, $text{'mailq_dir'}) if ($show{'Dir'});
583print &ui_columns_start(\@hcols, 100, 0, \@tds);
584
585# Show table rows for emails
586foreach 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	}
636print &ui_columns_end();
637if ($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	}
646print "</form>\n";
647return $quarcount;
648}
649
650# is_table_comment(line, [force-prefix])
651# Returns the comment text if a line contains a comment, like # foo
652sub is_table_comment
653{
654local ($line, $force) = @_;
655if ($config{'prefix_cmts'} || $force) {
656	return $line =~ /^\s*#+\s*Webmin:\s*(.*)/ ? $1 : undef;
657	}
658else {
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
665sub make_table_comment
666{
667local ($cmt, $force) = @_;
668if (!$cmt) {
669	return ( );
670	}
671elsif ($config{'prefix_cmts'} || $force) {
672	return ( "# Webmin: $cmt" );
673	}
674else {
675	return ( "# $cmt" );
676	}
677}
678
6791;
680
681