2 # Functions for managing sendmail aliases, domains and mappings.
3 # Only sendmail versions 8.8 and above are supported
5 BEGIN { push(@INC, ".."); };
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' );
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?
18 if (!@sendmailcf_cache) {
21 open(CF, $config{'sendmail_cf'});
23 s/^#.*$//g; # remove comments
24 s/\r|\n//g; # remove newlines
25 if (/^(\S)(\s*(.*))$/) {
29 $opt{'values'} = [ split(/\s+/, $2) ];
31 $opt{'eline'} = $opt{'line'};
33 push(@sendmailcf_cache, \%opt);
39 return \@sendmailcf_cache;
42 # check_sendmail_version(&config)
43 # Is the sendmail config file a usable version?
44 sub check_sendmail_version
46 local $ver = &find_type("V", $_[0]);
47 return $ver && $ver->{'value'} =~ /^(\d+)/ && $1 >= 7 ? $1 : undef;
50 # get_sendmail_version(&out)
51 # Returns the actual sendmail executable version, if it is available
52 sub get_sendmail_version
54 local $out = &backquote_with_timeout("$config{'sendmail_path'} -d0 -bv 2>&1",
57 if ($out =~ /version\s+(\S+)/i) {
60 ${$_[0]} = $out if ($_[0]);
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..
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++) {
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]);
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);
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];
101 # directive_line(&details)
104 return $_[0]->{'type'}.join(' ', @{$_[0]->{'values'}});
107 # find_type(name, &config)
108 # Returns an array of config directives of some type
112 foreach $c (@{$_[1]}) {
113 if ($c->{'type'} eq $_[0]) {
117 return @rv ? wantarray ? @rv : $rv[0]
118 : wantarray ? () : undef;
121 # find_option(name, &config)
122 # Returns the structure and value of some option directive
126 @opts = &find_type("O", $_[1]);
128 if ($o->{'value'} =~ /^\s*([^=]+)=(.*)$/ && $1 eq $_[0]) {
130 return wantarray ? ($o, $2) : $2;
136 # find_optionss(name, &config)
137 # Returns the structures and values of some option directive
141 @opts = &find_type("O", $_[1]);
143 if ($o->{'value'} =~ /^\s*([^=]+)=(.*)$/ && $1 eq $_[0]) {
144 push(@rv, [ $o, $2 ]);
147 return wantarray ? @rv : $rv[0];
150 # find_type2(type1, type2, &config)
151 # Returns the structure and value of some directive
154 local @types = &find_type($_[0], $_[2]);
156 foreach $t (@types) {
157 if ($t->{'value'} =~ /^(\S)(.*)$/ && $1 eq $_[1]) {
165 # Send a SIGHUP to sendmail
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;
174 # Just HUP the process
176 foreach my $pidfile (split(/\t+/, $config{'sendmail_pid'})) {
177 if (open(PID, $pidfile)) {
180 if ($pid) { &kill_logged('HUP', $pid); }
185 local @pids = &find_byname("sendmail");
186 @pids || return $text{'restart_epids'};
187 &kill_logged('HUP', @pids) ||
188 return &text('restart_ekill', $!);
194 # run_makemap(textfile, dbmfile, type)
195 # Run makemap to rebuild some map. Calls error if it fails.
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>"); }
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.
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>"); }
223 # find_textfile(config, dbm)
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
233 elsif ($dbm =~ /^(.*)\.(db|dbm|pag|dir|hash)$/i && -r "$1.txt") {
234 # Database is like /etc/virtusertable.db, text is /etc/virtusertable.txt
237 elsif (-r "$dbm.txt") {
238 # Database is like /etc/virtusertable, text is /etc/virtusertable.txt
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.
247 # Text and database have same name
255 local ($opt, $mqueue) = &find_option("QueueDirectory", $_[0]);
257 if (!$mqueue) { @rv = ( "/var/spool/mqueue" ); }
258 elsif ($mqueue =~ /\*|\?/) {
259 @rv = split(/\s+/, `echo $mqueue`);
264 push(@rv, split(/\s+/, $config{'queue_dirs'}));
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;
276 return $a->{'from'} cmp $b->{'from'};
280 # can_view_qfile(&mail)
281 # Returns 1 if some queued message can be viewed, 0 if not
284 return 1 if (!$access{'qdoms'});
285 local $re = $access{'qdoms'};
286 if ($access{'qdomsmode'} == 0) {
287 return $_[0]->{'header'}->{'from'} =~ /$re/i;
289 elsif ($access{'qdomsmode'} == 0) {
290 return $_[0]->{'header'}->{'to'} =~ /$re/i;
293 return $_[0]->{'header'}->{'from'} =~ /$re/i ||
294 $_[0]->{'header'}->{'to'} =~ /$re/i;
298 # renumber_list(&list, &position-object, lines-offset)
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'});
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
317 local ($conf, $suffix, $addit, $cwref) = @_;
319 foreach $f (&find_type("F", $conf)) {
320 if ($f->{'value'} =~ /^${suffix}[^\/]*(\/\S+)/ ||
321 $f->{'value'} =~ /^\{${suffix}\}[^\/]*(\/\S+)/) {
327 # get entries listed in a separate file
328 $$cwref = $cwfile if ($cwref);
333 if (/\S/) { push(@rv, $_); }
338 $$cwref = undef if ($cwref);
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));
348 push(@rv, map { $_->{'value'} } &find_type($addit, $conf));
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
357 local ($conf, $suffix, $values, $addit) = @_;
359 foreach $f (&find_type("F", $conf)) {
360 if ($f->{'value'} =~ /^${suffix}[^\/]*(\/\S+)/ ||
361 $f->{'value'} =~ /^\{${suffix}\}[^\/]*(\/\S+)/) {
365 local @old = grep { $_->{'value'} =~ /^${suffix}/ ||
366 $_->{'value'} =~ /^\{${suffix}\}/ } &find_type("C", $conf);
368 push(@old, &find_type($addit, $conf));
373 # If there is a .cw file, write all entries to it and take any
375 &open_tempfile(CW, ">$cwfile");
376 foreach $d (@$values) {
377 &print_tempfile(CW, $d,"\n");
382 # Stick all entries in sendmail.cf
383 foreach $d (@$values) {
384 push(@new, { 'type' => 'C',
385 'values' => [ $suffix.$d ] });
388 &save_directives($conf, \@old, \@new);
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
395 local ($conf, $suffix, $value) = @_;
397 foreach $f (&find_type("F", $conf)) {
398 if ($f->{'value'} =~ /^${suffix}[^\/]*(\/\S+)/ ||
399 $f->{'value'} =~ /^\{${suffix}\}[^\/]*(\/\S+)/) {
403 local @old = grep { $_->{'value'} =~ /^${suffix}/ ||
404 $_->{'value'} =~ /^\{${suffix}\}/ } &find_type("C", $conf);
406 # Add to external file
407 &open_tempfile(CW, ">>$cwfile");
408 &print_tempfile(CW, $value,"\n");
413 local @new = ( @old, { 'type' => 'C',
414 'values' => [ $suffix.$value ] });
415 &save_directives($conf, \@old, \@new);
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
423 local ($conf, $suffix, $value) = @_;
425 foreach $f (&find_type("F", $conf)) {
426 if ($f->{'value'} =~ /^${suffix}[^\/]*(\/\S+)/ ||
427 $f->{'value'} =~ /^\{${suffix}\}[^\/]*(\/\S+)/) {
431 local @old = grep { $_->{'value'} =~ /^${suffix}/ ||
432 $_->{'value'} =~ /^\{${suffix}\}/ } &find_type("C", $conf);
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);
440 # Remove from sendmail.cf
441 local @new = grep { $_->{'values'}->[0] ne $suffix.$value } @old;
442 &save_directives($conf, \@old, \@new);
446 # list_mail_queue([&conf])
447 # Returns a list of all files in the mail queue
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));
461 # Returns an array of valid options for the DontBlameSendmail option
465 open(BLAME, "$module_root_directory/dontblames");
469 if (/^(\S+)\s+(\S.*)$/) {
470 push(@rv, [ $1, $2 ]);
478 # Stops the sendmail process, returning undef on success or an error message
482 if ($config{'sendmail_stop_command'}) {
483 local $out = &backquote_logged("$config{'sendmail_stop_command'} </dev/null 2>&1");
485 return "<pre>$out</pre>";
489 foreach my $pidfile (split(/\t+/, $config{'sendmail_pid'})) {
490 local $pid = &check_pid_file($pidfile);
491 if ($pid && &kill_logged('KILL', $pid)) {
495 return $text{'stop_epid'};
503 # Starts the sendmail server, returning undef on success or an error message
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");
511 local $out = &backquote_logged("$config{'sendmail_command'} </dev/null 2>&1");
512 return $? ? "<pre>$out</pre>" : undef;
515 sub is_sendmail_running
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");
522 &error("Failed to get Sendmail status from SMF : $out");
524 return $out =~ /online/i ? 1 : 0;
527 # Use PID files, or check for process
528 local @pidfiles = split(/\t+/, $config{'sendmail_pid'});
530 foreach my $p (@pidfiles) {
531 local $c = &check_pid_file($p);
537 return &find_byname("sendmail");
542 # mailq_table(&qfiles)
543 # Print a table showing queued emails. Returns the number quarantined.
546 local ($qfiles, $qmails) = @_;
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 " \n";
558 print "<input type=submit name=flush value='$text{'mailq_flushsel'}'>\n";
560 print &ui_links_row(\@links);
564 # Generate table header
565 local (@hcols, @tds);
566 if ($access{'mailq'} == 2) {
568 push(@tds, "width=5");
571 foreach my $s (split(/,/, $config{'mailq_show'})) {
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);
585 # Show table rows for emails
586 foreach my $f (@$qfiles) {
588 ($n = $f) =~ s/^.*\///;
589 local $mail = $qmails->{$f} || &mail_from_queue($f);
592 $dir =~ s/\/[^\/]+$//;
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'};
603 $mail->{'status'} ||= $text{'mailq_sending'};
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;
612 $size = &nice_size($mail->{'size'});
613 if ($access{'mailq'} == 2) {
614 push(@cols, "<a href=\"view_mailq.cgi?".
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'});
629 if ($access{'mailq'} == 2) {
630 print &ui_checked_columns_row(\@cols, \@tds, "file",$f);
633 print &ui_columns_row(\@cols, \@tds);
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";
642 print " \n";
643 print "<input type=submit name=flush value='$text{'mailq_flushsel'}'>\n";
650 # is_table_comment(line, [force-prefix])
651 # Returns the comment text if a line contains a comment, like # foo
654 local ($line, $force) = @_;
655 if ($config{'prefix_cmts'} || $force) {
656 return $line =~ /^\s*#+\s*Webmin:\s*(.*)/ ? $1 : undef;
659 return $line =~ /^\s*#+\s*(.*)/ ? $1 : undef;
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
667 local ($cmt, $force) = @_;
671 elsif ($config{'prefix_cmts'} || $force) {
672 return ( "# Webmin: $cmt" );