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 " \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'} ||= " "; 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 " \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