# General subroutines for Majordomo

# $Source: /sources/cvsrepos/majordomo/majordomo.pl,v $
# $Revision: 1.58 $
# $Date: 2000/01/07 12:32:04 $
# $Author: cwilson $
# $State: Exp $
# 
# $Header: /sources/cvsrepos/majordomo/majordomo.pl,v 1.58 2000/01/07 12:32:04 cwilson Exp $
# 

# The exit codes for abort.  Look in /usr/include/sysexits.h.
#
$EX_DATAERR = 65;
$EX_TEMPFAIL = 75;
$EX_NOUSER = 67;

package Majordomo;

$DEBUG = $main'DEBUG;

#  Mail header hacking routines for Majordomo
#
#  Derived from:
#  Routines to parse out an RFC 822 mailheader
#     E. H. Spafford,  last mod: 11/91
#  
#  ParseMailHeader breaks out the header into an % array
#    indexed by a lower-cased keyword, e.g.
#       &ParseMailHeader(STDIN, *Array);
#	use $Array{'subject'}
#
#    Note that some duplicate lines (like "Received:") will get joined
#     into a single entry in %Array; use @Array if you want them separate
#    $Array will contain the unprocessed header, with embedded
#     newlines
#    @Array will contain the header, one line per entry
#
#  RetMailAddr tries to pull out the "preferred" return address
#    based on the presence or absence of various return-reply fields


#  Call as &ParseMailHeader(FileHandle, *array)

sub main'ParseMailHeader  ## Public
{
    local($save1, $save2) = ($*, $/);
    local($FH, *array) =  @_;
    local ($keyw, $val);

    %array = ();

    # force unqualified filehandles into callers' package
    local($package) = caller;
    $FH =~ s/^[^':]+$/$package'$&/;

    ($*, $/) = (1, '');
    $array = $_ = <$FH>;
    s/\n\s+/ /g;
       
    @array = split('\n');
    foreach $_ (@array)
    {
	($keyw, $val) = m/^([^:]+):\s*(.*\S)\s*$/g;
	$keyw =~ y/A-Z/a-z/;
	if (defined($array{$keyw})) {
	    $array{$keyw} .= ", $val";
	} else {
	    $array{$keyw} = $val;
	}
    }
    ($*, $/) = ($save1, $save2); 
}


#  Call as $addr = &RetMailAddr(*array)
#    This assumes that the header is in RFC 822 format
# We used to strip the raw address from the header here, but the address is
# stripped again before it gets to the mailer and we may want to use the
# whole thing when we do a subscription.
sub main'RetMailAddr  ## Public
{
    local(*array) = @_;

    local($ReplyTo) = defined($array{'reply-to'}) ?
		$array{'reply-to'} : $array{'from'};

    $ReplyTo = $array{'apparently-from'} unless $ReplyTo;

    $ReplyTo;
}

# @addrs = &ParseAddrs($addr_list)
sub main'ParseAddrs {
    local($_) = shift;
    1 while s/\([^\(\)]*\)//g; 		# strip comments
    1 while s/"[^"]*"\s//g;		# strip comments"
    split(/,/);				# split into parts
    foreach (@_) {
	1 while s/.*<(.*)>.*/$1/;
	s/^\s+//;
	s/\s+$//;
    }

    @_;
}

# Check to see if a list is valid.  If it is, return the validated list
# name; if it's not, return ""
sub main'valid_list {
    local($listdir) = shift;
    # start with a space-separated list of the rest of the arguments
    local($taint_list) = join(" ", @_);
    # strip harmless matched leading and trailing angle brackets off the list
    1 while $taint_list =~ s/^<(.*)>$/$1/;
    # strip harmless trailing "@.*" off the list
    $taint_list =~ s/\@.*$//;
    # anything else funny with $taint_list probably isn't harmless; let's check
    # start with $clean_list the same as $taint_list
    local($clean_list) = $taint_list;
    # clean up $clean_list
    $clean_list =~ s/[^-_0-9a-zA-Z]*//g;
    # if $clean_list no longer equals $taint_list, something's wrong
    if ($clean_list ne $taint_list) {
	return ""; 
    } 
    # convert to all-lower-case
    $clean_list =~ tr/A-Z/a-z/;
    # check to see that $listdir/$clean_list exists
    if (! -e "$listdir/$clean_list") {
	return "";
    }
    return $clean_list;
}

# compare two email address to see if they "match" by converting  to all
# lower case, then stripping off comments and comparing what's left.  If
# a optional third argument is specified and it's not undefined, then
# partial matches (where the second argument is a substring of the first
# argument) should return true as well as exact matches.
#
# if optional third argument is 2, then compare the two addresses looking
# to see if the addresses are of the form user@dom.ain.com and user@ain.com
# if that is the format of the two addresses, then return true.
sub main'addr_match {
    local($a1) = &main'chop_nl(shift);
    local($a2) = &main'chop_nl(shift);
    local($partial) = shift;	# may be "undef"

    print STDERR "addr_match: enter\n" if $DEBUG;
    print STDERR "addr_match: comparing $a1 against $a2\n" if $DEBUG;

    if ($partial == 1) {
	$a1 =~ tr/A-Z/a-z/;
	$a2 =~ tr/A-Z/a-z/;
	if (index($a1, $a2) >= $[) {
	    return(1);
	} else {
	    return(undef);
	}
    }
	
    local(@a1, @a2);

    $a1 =~ tr/A-Z/a-z/;
    $a2 =~ tr/A-Z/a-z/;

    @a1 = &main'ParseAddrs($a1);
    @a2 = &main'ParseAddrs($a2);
    if (($#a1 != 0) || ($#a2 != 0)) {
	# Can't match, because at least one of them has either zero or
	# multiple addresses
	return(undef);
    }

    if ($partial == 2 && ($a1[0] ne $a2[0])) { # see if addresses are
                                               # foo@baz.bax.edu, foo@bax.edu
       local(@addr1,@addr2);
	  @addr1 = split(/\@/, $a1[0]);
	  @addr2 = split(/\@/, $a2[0]);
	  if ( $#addr1 == $#addr2 && $#addr1 == 1 && 
               $addr1[0] eq $addr2[0] && (index($addr1[1], $addr2[1]) >= $[))
	  {
	    return(1);
	  }
       }

    return($a1[0] eq $a2[0]);
}

# These are package globals referenced by &setabortaddr and &abort

$abort_addr = "owner-majordomo";

sub main'set_abort_addr {
    $abort_addr = shift unless ($#_ < $[);
}

# Abort the process, for the reason stated as the argument

local($log_disabled);
local($logging_abort, $mailing_abort);

sub main'abort { #'
    # first, tell the requestor that something bad happened.
    # XXX is this really meaningful for, say, resend?
    if (-e main'REPLY) {
	print main'REPLY <<END_MSG;
>>> Sorry, an error has occurred while processing your request
>>> The caretaker of Majordomo ( $abort_addr ) has been notified
>>> of the problem.
END_MSG
	close (main'REPLY);
    }

    # print the reason for the abort to stderr; maybe someone will see it
    print STDERR "$main'program_name: ABORT\n", join(" ",  @_), "\n";

    # log the reason for the abort, if possible.  We don't log if the
    # log is inaccessible, or if we're aborting trying to log that we're
    # aborting.
    unless ($log_disabled || $logging_abort) {
	$logging_abort = join(" ", @_);
	&main'log("ABORT", $logging_abort);
	$logging_abort = "";
    }
    else {
	# Use previous message if we recursed
	@_ = ($logging_abort) if $logging_abort;
    }

    # send a message to the Majordomo owner, if possible. We don't mail
    # if we're aborting trying to mail that we're aborting.
    if (! $mailing_abort &&
	defined($abort_addr) && defined($main'bounce_mailer)) {

	$mailing_abort = 1; # Break recursion loops

	# We must set the mailer correctly here just in case it was
	# originally set to the normal mailer; that probably won't get us
	# anywhere
	&main'set_mailer($main'bounce_mailer);
	&main'sendmail(ABORT, $abort_addr, "MAJORDOMO ABORT ($main'program_name)");#'
	print ABORT <<"EOM";

MAJORDOMO ABORT ($main'program_name)!!

@_

EOM
	close(ABORT);
    }

    exit $EX_DATAERR;
}

# bitch about a serious problem, but not fatal.

local($logging_warning, $mailing_warning);

sub main'bitch {
    # print the warning to stderr in case all else fails
    # maybe someone will see it
    print STDERR "$main'program_name: WARNING\n", join(" ", @_), "\n";

    # log the warning, if possible
    unless ($log_disabled || $logging_warning) {
	$logging_warning = 1;
	&main'log("WARNING ", join(" ", @_), "\n"); #';
	$logging_warning = 0;
    }

    # send a message to the Majordomo owner, if possible
    if (! $mailing_warning &&
	defined($abort_addr) && defined($main'bounce_mailer)) {

	$mailing_warning = 1; # Break recursion loops

	# We must set the mailer correctly here just in case it was
	# originally set to the normal mailer; that probably won't get us
	# anywhere
	&main'set_mailer($main'bounce_mailer);
	&main'sendmail(WARN, $abort_addr, "MAJORDOMO WARNING ($main'program_name)");#';
	print WARN <<"EOM";

MAJORDOMO WARNING ($main'program_name)!!

@_

EOM
	close(WARN);
	$mailing_warning = 0;
    }
}



# do a quick check of permissions.
#
sub main'check_permissions {
    local($err);
    if ( ! -w $log_file ) {
	if ( ! -e $log_file ) {			# log file may not exist, check dir perms.
	    local($dir);
	    ($dir) = $log_file =~ m@^(/\S+)/@;
	    if ( ! -w $dir ) {
		$err .= "Unable to create log file in $dir, check permissions.\n"; # 
	    }
	} else {
	    $err .= "Unable to write to log file, check permissions on $log_file\n";
	}
    }

    if ( ! -w $main'listdir ) {
	$err .= "Unable to write to list directory \$listdir, check permissions on $main'listdir\n";
    }

    if (length $err) {
	$err = "While running with an effective uid of $> and an effective gid of $), Majordomo\nran into the following problems:\n" .
	    $err;
	$log_disabled = 1;
	&main'abort($err);#';
    }
}

# These are package globals referenced by &setlogfile and &log
$log_file = "/tmp/log.$$";
$log_host = "UNKNOWN";
$log_program = "UNKNOWN";
$log_session = "UNKNOWN";

# set the log file
sub main'set_log {
    $log_file = shift unless ($#_ < $[);
    $log_host = shift unless ($#_ < $[);
    $log_program = shift unless ($#_ < $[);
    $log_session = shift unless ($#_ < $[);

}

# Log a message to the log
sub main'log {

    print STDERR "$0:  main'log()\n" if $DEBUG;

    local($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime;
    local(*MAILMSG);

    print STDERR "$0:  main'log(): opening logfile $log_file\n" if $DEBUG;

    if (&main'lopen(LOG, ">>", $log_file)) { #';
	# if the log is open, write to the log
	printf LOG "%s %02d %02d:%02d:%02d %s %s[%d] {%s} ",
	    $ctime'MoY[$mon], $mday, $hour, $min, $sec,
	    $log_host, $log_program, $$, $log_session;
	print LOG join(" ", @_), "\n";
	&main'lclose(LOG);
    } else {
	
	print STDERR "$0:  main'log(): log not open, writing to STDERR and attempting to mail.\n" if $DEBUG;

	# otherwise, write to stderr
	printf STDERR "%s[%d] {%s} ", $log_program, $$, $log_session;
	print STDERR join(" ", @_), "\n";

        # send a message to the Majordomo owner, if possible
        if (defined($abort_addr)) {
	    &main'sendmail(MAILMSG, $abort_addr, # '(
						     "MAJORDOMO NOTICE: Can't open log");
	    printf MAILMSG "%s[%d] {%s} ", $log_program, $$, $log_session;
   	    print MAILMSG join(" ", @_), "\n";
	}
    }
    print STDERR "$0:  main'log(): done\n" if $DEBUG;

}

# Globals referenced by &set_mail* and &sendmail
$mail_prog = "$sendmail_command -f\$sender -t";
$mail_from = $whoami;
$mail_sender = $whoami_owner;

# set the mailer
sub main'set_mailer {
     $mail_prog = shift;
}

# set the default from address
sub main'set_mail_from {
    $mail_from = shift;
}

# set the default sender address
sub main'set_mail_sender {
    $mail_sender = shift;
}

# Exec a mailer process
sub main'do_exec_sendmail {
    &main'abort("do_exec_sendmail, number of args <= 1 unsafe to exec")
      if scalar(@_) <= 1;
    # It makes sense to check to see that the mailer is valid here, but the
    # abort routine must make certain that recursion doesn't develop,
    # because abort calls this routine.
    &main'abort("$main'program_name: do_exec_sendmail, mailer $_[0] not executable")
      unless (-x $_[0]);
    exec(@_);
    die("Failed to exec mailer \"@_\": $!");
}

# Open a mailer on the far end of a filehandle
sub main'sendmail { #''
    local($MAIL) = shift;
    local($to) = shift;
    local($subject) = shift;
    local($from) = $mail_from;
    local($sender) = $mail_sender;
    # The following eval expands embedded variables like $sender
    local($mail_cmd) = eval qq/"$mail_prog"/;
    local($isParent);
    if ($#_ >= $[) { $from = shift; }
    if ($#_ >= $[) { $sender = shift; }

    # force unqualified filehandles into caller's package
    local($package) = caller;
    $MAIL =~ s/^[^':]+$/$package'$&/;

    # clean up the addresses, for use on the mailer command line
    local(@to) = &main'ParseAddrs($to);
    for (@to) {
	$_ = join(", ", &main'ParseAddrs($_));
    }
    $to = join(", ", @to);  #';

print STDERR "$0: main'sendmail:  To $to, Subject $subject, From $from\n" 
    if $DEBUG;
print STDERR "$0: main'sendmail:  Sender $sender, mail_cmd = $mail_cmd\n"
    if $DEBUG;

    # open the process
    if (defined($isParent = open($MAIL, "|-"))) {
 	&main'do_exec_sendmail(split(' ', $mail_cmd))
 	    unless ($isParent);
     } else {
 	&main'abort("Failed to fork prior to mailer exec");
     }

    # Generate the header.  Note the line beginning with "-"; this keeps
    # this message from being reprocessed by Majordomo if some misbegotten
    # mailer out there bounces it back.
    print $MAIL 
"To: $to
From: $from
Subject: $subject
Reply-To: $from

--

";
    
    return;
}

# check the password for a list
sub main'valid_passwd {
    local($listdir, $list, $passwd) = @_;

    # is it a valid list?
    local($clean_list) = &main'valid_list($listdir, $list);
    if ($clean_list ne "") {
	# it's a valid list check config passwd first 
        if (defined($main'config_opts{$clean_list,"admin_passwd"}) &&
            $passwd eq $main'config_opts{$clean_list,"admin_passwd"} ) 
	      {	return 1; }

	# read the password from the file in any case
	if (&main'lopen(PASSWD, "", "$listdir/$clean_list.passwd")) {
	    local($file_passwd) = <PASSWD>;
	    &main'lclose(PASSWD);
	    $file_passwd = &main'chop_nl($file_passwd);
	    # got the password; now compare it to what the user sent
	    if ($passwd eq $file_passwd) {
		return 1;
	    } else {
		return 0;
	    }
	} else {
	    return 0;
	}
    } else {
	return 0;
    }
}

# Check to see that this is a valid address. 
# A valid address is a single address with 
# no "|" in the address part. It may not start with a - either.
# If it has a / in it, we use some heuristics to find out if the address
# may be a file. Some other heuristics attempt to look for a valid X.400
# address. This is not infalible.
sub main'valid_addr {
    local($addr, $list) = @_;
    local(@addrs, $temp);

    # Parse the address out into parts
    @addrs = &main'ParseAddrs($addr);

    # if there's not exactly 1 part, it's no good
    # XXX Should inform the poor user of this fact.
    if ($#addrs != 0) {
	return undef;
    }

    local($_) = $addrs[0];

    # Deal with unbalanced brackets or parenthesis in an address.
    $temp = $_;

    # Nuke anything within quotes.
    1 while $temp =~ s/(^|([^\\\"]|\\.)+)\"(([^\\\"]|\\.)*|$)\"?/$1/g;

    # Remove nested parentheses " <- placate emacs' highlighting
    1 while $temp =~ s/\([^\(\)]*\)//g;

    # Remove nested angle brackets
    1 while $temp =~ s/\<[^\<\>]*\>//g;

    # remove nested square brackets
    1 while $temp =~ s/\[[^\[\]]*\]//g;

    # If any parentheses of brackets remain, they are unbalanced and the
    # address is illegal.
    if ($temp =~ /[\(\)\<\>\[\]]/) {
	if (-e main'REPLY) {
	    print main'REPLY <<"EOM"
**** The address you supplied, $_
**** Does not seem to be a legal Internet address.  It seems to have an
**** uneven number of parentheses or brackets.
	
EOM

	}
	&main'log("WARNING", "Unbalanced address: $_");
	return undef;
    }

    if ($temp =~ /[,;:]/) {
        if (-e main'REPLY) {
            print main'REPLY <<"EOM"
**** The address you supplied, $_
**** Does not seem to be a legal Internet address.  It seems to have
**** unquoted colons, commas, or semicolons.

EOM

        }
        &main'log("WARNING", "Illegal chars in address: $_");
        return undef;
    }


    # Deal with legal spaces in a stripped address, then check and reject
    # any remaining space.  Note that as I write this, the comment stripper
    # ParseAddrs does not handle things like a quoted local part but I've
    # included the correct routines just in case it ever does.
    $temp = $_;

    # We assume that the comment stripper will have eaten leading and
    # trailing space.

    # This mess turns "jason ti bb i tt s"@hpc.uh.edu into
    # "jasontibbitts"@hpc.uh.edu
    1 while $temp =~ s/\"(.*)\s(.*)\"/\"$1$2\"/g;

    # This compresses space before dots or `@'s. " <- placate emacs' highlighting
    1 while $temp =~ s/\s(\.|@)/$1/g;

    # This compresses space after dots or `@'s.
    1 while $temp =~ s/(\.|@)\s/$1/g;

    # We've taken out all legitimate space from the address (yes, RFC822
    # permits that kind of bogosity), so if the address has spaces, we have
    # a problem.
    if ($temp =~ /\s/) {
	if (-e main'REPLY) {
	    print main'REPLY <<"EOM";
**** The address you supplied, $_
**** does not seem to be a legal Internet address.  You may have supplied
**** your full name instead of your address, or you may have included your
**** name along with your address in a manner that does not comply with
**** Internet standards for addresses.
**** It is also possible that you are using a mailer that wraps long lines
**** and the end of your request ended up on the following line.  If the
**** latter is true, try using backslashes to split long lines.  (Split the
**** line between words, then put a backslash at the end of all but the
**** last line.)

EOM
	}
	&main'log("WARNING", "Illegal space in address: $_");
	return undef;
    }

    # Addresses must have both an @ and a .
    if (!(/\@/ && /\./)) {
	if (-e main'REPLY) {
	    print main'REPLY <<"EOM";
**** The address you supplied, $_
**** is not a complete address.  When providing an address, you must give
**** the full name of the machine including the domain part (like
**** host.corp.com), not just your user name or your name and the short
**** name of the machine (just user or user\@host is not legal).

EOM
	}
	&main'log("WARNING", "Non-domained address: $_");
	return undef;
    }

    
    # o  if there's a "|" in it, it's hostile
    # o  if there is a - sign at the front of the address, it may be an attempt
    #    to pass a flag to the MTA
    # o  bail if they're attempting to subscribe the list to itself
    # 

    print STDERR "$0: valid_addr: comparing '$addr' to '$list'\n" if $DEBUG;

    # XXX Should at least tell the user that there was a problem.
    if ( /\|/ || /^-/ ) {
	&main'abort("HOSTILE ADDRESS (invalid first char or |) $addr"); #'
	return undef;
    }

    # Some sendmails are dumb enough to do bad things with this
    if (/\:include\:/) {
        &main'abort("HOSTILE ADDRESS (tried to use :include: syntax) $addr"); #'
        return undef;
    }

    if ( $addr eq $list ) {
	&main'abort("HOSTILE ADDRESS (tried to subscribe list) $addr"); # '
	return undef;
    }

    # if the is a / in it, it may be an attempt to write to a file.
    # or it may be an X.400, HP Openmail or some other dain bramaged
    # address 8-(. We check this by breaking the address on '/'s
    # and checking to see if the first component of the address
    # exists. If it does we bounce it as a hostile address.

    # XXX Again, we shouldn't be aborting without telling the user
    if ( m#/# ) {
	local(@components) = ($_ =~ /([\/\@]?[^\/\@]+)/g);

	&main'abort("HOSTILE ADDRESS (path exists to /file) $addr")
                if (-e "/$components[0]"); #'
	&main'abort("HOSTILE ADDRESS (path exists to file) $addr")
                if (-e "$components[0]"); #'

       # then as an extra check that can be turned off in the majordomo.cf
       # file we make sure that the last component of the address has an
       # @ sign on it for an X.400->smtp gateway translation.

        if (!$main'no_x400at) {
	    &main'abort("HOSTILE ADDRESS (no x400 \@) $addr") if (
                    "$components[$#components]" !~ /\@/);  #'
	}

        # check to see that the c= and a[dm]= parts exist
	if (!$main'no_true_x400) {
            &main'abort("HOSTILE ADDRESS (no x400 c=) $addr")
                    if ($_ !~ m#/c=#); #'
            &main'abort("HOSTILE ADDRESS (no x400 a[dm]=) $addr")
                    if ($_ !~ m#/a[dm]=#); #'
       }
   }

print STDERR "$0: valid_addr: exit\n" if $DEBUG;

   return $_;
}

# is this a valid filename?
sub main'valid_filename {
    local($directory) = shift;
    local($list) = shift;
    local($suffix) = shift;
    local($taint_filename) = shift;
    local($clean_filename);

    # Safety check the filename.
    if ($taint_filename =~ /^[\/.]|\.\.|[^-_0-9a-zA-Z.\/] /) {
	return undef;
    } else {
	$clean_filename = $taint_filename;
    }
    if (! -f "$directory/$list$suffix/$clean_filename") {
	return undef;
    }
    return "$directory/$list$suffix/$clean_filename";
}

# Chop any trailing newlines off of a string, and return the string
sub main'chop_nl {
    if ($#_ >= $[) {
	local($x) = shift;
	$x =~ s/\n+$//;
	return($x);
    } else {
	return(undef);
    }
}

# Perform simple filename globbing, so we don't have to use the <...> glib
# syntax which has caused problems.
sub main'fileglob {
    local($dir) = shift;
    local($pat) = shift;
    local(@files) = ();

    opendir(DIR, $dir) || return undef;
    @files = grep(/$pat/, readdir(DIR));
    grep($_ = "$dir/$_", @files);  # perl4 doesn't have map!

    closedir(DIR);

    return @files;
}

sub main'is_list_member {
    local($subscriber, $listdir, $clean_list, $file) = @_;
    local($matches) = 0;
    local(*LIST);
    local($_);

    print STDERR "is_list_member: enter\n" if $DEBUG;

    $file = "$listdir/$file" if defined $file && $file !~ m|^/|;
    $file = "$listdir/$clean_list" unless defined $file;
    print STDERR "is_list_member: checking $file for $subscriber\n"
	if $DEBUG; 
    if (open(LIST, $file)) {
	while (<LIST>) {
	    if (&main'addr_match($subscriber, $_, 
	       (&main'cf_ck_bool($clean_list,"mungedomain") ? 2 : undef))) {
		$matches++;
		last;
	    }
	}
	close(LIST);
    }
    else {
	&main'bitch("Can't read $file: $!"); #'"";
    }

    print STDERR "is_list_member: exit $matches\n" if $DEBUG;

    return($matches);
}

# From: pdc@lunch.engr.sgi.com (Paul Close)
# > Shouldn't list and list-digest  be equivalent for things like
# > retrieval of files? As it stands now, if I subscribe to
# > foo-list-digest and I want to retrieve a file for foo-list or list the
# > members of foo-list, and foo-list is a private list for these
# > purposes, then I'm out of luck.
# 
# I agree.  The approach I took for solving this was to add a function called
# private_okay() to use instead of list_member() in cases where you wanted to
# restrict function to members of the list or list-digest.
# 
# If restrict_post is defined, private_okay searches those lists, otherwise
# it searches list and list-digest.  Anywhere majordomo consults a private_*
# variable, I use private_okay instead of list_member.  Works quite nicely.
#
# Added in access checking mechanisms as well to replace
# private_XYZ with some flexability.  This will be exanded to be
# more flexible than the current [open|list|closed] capability.
#  --Chan 96/04/23
#
sub main'access_check {
    local($cmd, $subscriber,$listdir,$clean_list) = @_;
    local(@lists,$list,$altlist,$total);

    print STDERR "access_check: enter\n" if $DEBUG;

    # bail right away if the command is disabled.
    # 
    if ($main'config_opts{$clean_list, "${cmd}_access"} =~ /closed/) {#'
	print STDERR "access_check: ${cmd}_access is closed.\n" if $DEBUG;
	return 0 ;
    }

    # bail right away if the command is wide open
    #
    if ($main'config_opts{$clean_list, "${cmd}_access"} =~ /open/) {#'
	print STDERR "access_check: ${cmd}_access is open.\n" if $DEBUG;
	return 1;
    }
    
    # now check a little deeper.
    #
    if ( length($main'config_opts{$clean_list,'restrict_post'} )) {
        @lists = split(/[:\s]+/,
                     $main'config_opts{$clean_list,'restrict_post'});
    } else {
        if ($clean_list =~ /(.*)-digest/) {
            $altlist = $1;
        } else {
            $altlist = "$clean_list-digest";
        }
        @lists = ($clean_list);
	push(@lists, $altlist) if -e "$listdir/$altlist";
    }

    print STDERR "access_check: checking lists " , join(', ', @lists), "\n" 
	if $DEBUG;

    $total = 0;
    foreach $list (@lists) {
	$total += &main'is_list_member($subscriber, $listdir, $clean_list, $list);
    }
    print STDERR "access_check: exit\n" if $DEBUG;
    return $total;
}

1;
