#!/bin/ksh
# @(#) maillist.ksh 1.1 94/04/23
# 92/09/27 john h. dubois iii (john@armory.com)
# 92/10/13 Added dellist capability.
# 92/10/15 Check for whether user is already on list.
# 92/10/17 Allow multiple users on cmd line.
# 92/11/09 Added help.
# 93/07/13 Do not create list- if no changes are made.
# 93/07/17 Store lists with multiple addresses per line.  Added -c option.
# 94/01/10 Make default for LISTS be home dir, not current dir.
# 94/04/05 Added A and R options.
# 94/04/23 Use .maillist file.

alias istrue="test 0 -ne"
alias isfalse="test 0 -eq"
rcfile=.maillist

# Usage: ReadList <array> <listname>
# The comma/whitespace separated words in file <listname> are read, sorted,
# and put into array <array>
function ReadList {
    typeset IFS="$IFS," arr=$1 list=$2

    set -s -A $arr -- $(<$list)
}

# Usage: IsOnList user-name
# Returns success if user-name is in any element of global MList[]
function IsOnList {
    typeset -i nelem=${#MList[*]} i=0
    while [ i -lt nelem ]; do
	[ "$1" = "${MList[i]}" ] && return 0
	let i+=1
    done
    return 1
}

# Print words passed as args with max line length
# Usage: PrintMaxLines <maxlinelength> <sep> <end> word ...
# <sep> is used to separate words on a line.
# <end> is put at the end of each line except the last.
function PrintMaxLines {
    typeset maxline=$1 sep=$2 end=$3 outline= word
    typeset -i printedline=0 extra=${#sep}+${#end}

    shift 3
    for word; do
	if [[ $(( ${#outline}+${#word}+$extra )) -gt $maxline ]]; then
	    isfalse printedline && printedline=1 || print -r "$end"
	    print -nr "$outline"
	    outline=$word
	else
	    [ -z "$outline" ] && outline=$word || outline="$outline$sep$word"
	fi
    done
    # print final line if it hasn't been printed yet
    istrue printedline && print -r "$end"
    print -r "$outline"
}

# Usage: WriteList <listname>
# Writes mailing list in MList[] to file <listname>
function WriteList {
    cp $list $list- 
    PrintMaxLines 79 ", " "," "${MList[@]}" > $list ||
    print -u2 "$name: List write failed."
}

# Usage: Ind <arrayname> <varname> <value> <nsearch> <firstelem>
# Sets <varname> to the index of the first element of <arrayname>
# that has value <value>.
# If the value is not found, <varname> is not set and 1 is returned;
# if it is, 0 is returned.
# If <nsearch> is given, the first <nsearch> elements of the array are
# searched, with only nonempty elements counted.
# If not, the first n nonempty elements are searched (up to 1024),
# where n is the number of elements in the array.
# If a fourth argument is given, it is the index to start with; the search
# continues for <nsearch> elements.
function Ind {
    typeset -i NElem ElemNum=${5:-0} NumNonNull=0
    typeset Arr=$1 var=$2 Val=$3 ElemVal

    [ $# -ge 4 ] && NElem=$4 || eval NElem=\${#$Arr[*]}
    while [ ElemNum -le 1023 -a NumNonNull -lt NElem ]; do
	eval ElemVal=\"\${$Arr[ElemNum]}\" 
	if [ "$Val" = "$ElemVal" ]; then
	    eval $var=\$ElemNum
	    return 0
	fi
	[ -n "$ElemVal" ] && let NumNonNull+=1
	let ElemNum+=1
    done
    return -1
}

# Usage: ChangeList listname old-address new-address
# If old-address exists in listname, remove it and added new-address to
# listname.
function ChangeList {
    typeset list=$1 old=$2 new=$3

    if DoUsers 0 "$list" "$old"; then
	DoUsers 1 "$list" "$new"
    else
	print -u2 "$name: $new not added to list $list."
    fi
}

# Usage: DoUsers 0|1 <listname> user ...
# Use 0 to remove a user, 1 to add a user
# Returns 0 on complete success, 1 if a user was already on a list to be
# added to or not on a list to be remove from or if any other error occurs.
function DoUsers {
    typeset -i add=$1
    typeset list=$2
    typeset OnList OffList User
    typeset -i OnInd=0 OffInd=0 UInd err=0

    shift 2

    ReadList MList $list || return 1
    for User; do
	if IsOnList "$User"; then
	    OnList[OnInd]=$User
	    let OnInd+=1
	else
	    OffList[OffInd]=$User
	    let OffInd+=1
	fi
    done
    if istrue add; then
	if [ OnInd -gt 0 ]; then
	    echo "Warning: already on list \"$list\": ${OnList[*]}."
	    err=1
	fi
	if [ OffInd -gt 0 ]; then
	    set -s -A MList -- "${MList[@]}" "${OffList[@]}"
	    WriteList $list
	    echo "Added to list $list: ${OffList[*]}"
	fi
    else
	if [ OffInd -gt 0 ]; then
	    echo "Warning: not on list \"$list\": ${OffList[*]}."
	    err=1
	fi
	if [ OnInd -gt 0 ]; then
	    for User in "${OnList[@]}"; do
		if Ind MList UInd "$User"; then
		    unset MList[UInd]
		else
		    print -u2 "$name: Failed to find index of $User."
		    err=1
		fi
	    done
	    WriteList $list
	    echo "Removed from list $list: ${OnList[*]}"
	fi
    fi
    return $err
}

function ProcArgs {
    typeset opt Users ListArr OFS add list Args
    typeset -i notminus

    opt=${1#-}
    shift
    case $opt in
    h)
	echo \
"$name: Manipulate mailing lists.
$Usage
$name prints and modifies included mailing lists.  See the 'tables' man page
for a description of how to set up mail aliases that include addresses from
a file.  Addresses are given as a list separated by commas.  Mailing list names
are given separately.  A mailing list is maintained in a file with the same
name as the list.  It must already exist.  The file is searched for in each
directory given by the environment variable \"MAILLISTS\".  MAILLISTS can also
be set by creating a file named $rcfile in your home directory with a line in
it of the form:
MAILLISTS=maillist-directory
If MAILLISTS is not set, it defaults to your home directory.  Mailing list
files should contain addresses separated by commas and optional whitespace, and
should be readable by other or by group mmdf.  
A copy of each old list is saved in listname- (the same name as the list
but with a '-' attached).  Mailing list names that end in '-' are ignored,
so '*' can be used in a directory of mailing lists to specify all of them.
Exactly one of the following options must be given:
-h: Print this help information.
-p listname ...
    Print the specified mailing lists.
-a user[,user...] listname [listname ...]
-A listname[,listname...] user [user ...]
-r user[,user...] listname [listname ...]
-R listname[,listname...] user [user ...]
    Add/remove the specified users to/from the mailing list(s).
-c current-address new-address listname ...
    Change the email address of the specified user on the mailing list(s).
    Only one user can be changed per invokation.  The current subscription
    address should be given first, then the new subscription address.  If
    current-address does not exist in a list, new-address is not added to it.
    For example, to change the subscription address of user@foo to user@bar
    on the lists in the current directory, use: $name user@foo user@bar *"
	exit 0
	;;
    [arAR])
	OFS=$IFS
	IFS=,
	if [[ $opt = [ar] ]]; then
	    Users=$1
	    shift
	    set -A ListArr "$@"
	    set -A Args $Users
	else
	    ListList=$1
	    shift
	    set -A Args "$@"
	    set -A ListArr $ListList
	fi
	IFS=$OFS
	[[ $opt = [rR] ]]
	ListCmd="DoUsers $?"
	set -- "${ListArr[@]}"
	;;
    c)
	ListCmd=ChangeList
	Args[1]=$1
	Args[2]=$2
	shift 2
	;;
    p)
	ListCmd=cat
	;;
    ?) 
	print -u2 "$name: $opt: bad option.  Use -h for help."
	exit 1
	;;
    esac
    for list; do
	[[ "$list" = *- ]] && continue
	notminus=1
	$ListCmd "$list" "${Args[@]}"
    done
    isfalse notminus && print -u2 "$name: No lists."
}

# Start of main program
 
if [ -z "$MAILLISTS" ]; then
    rc=$HOME/$rcfile
    [ -r $rc -a -f $rc ] && . $rc
    [ -z "$MAILLISTS" ] && MAILLISTS=$HOME
fi

cd $MAILLISTS

name=${0##*/}

Usage="Usage: $name [-paArRch] arg[,arg...] arg ..."

if [[ $# -eq 0 || "$1" != ?(-)? ]]; then
    print -u2 "$Usage\nUse -h for help."
    exit
fi

ProcArgs "$@"
