#!/bin/sh
#
######################################################################
# Simple Cyrus Administration Tool
# Copyright 2003 Fastpath research
# Written by David Santinoli <david at santinoli.com>
#
# Released under the GNU General Public License version 2.
# See http://www.gnu.org/licenses/gpl.html .
#
######################################################################
#
# Changelog:
#
# version 1.1.1 (9 Apr 2003)
# - There was no function "refresh_postfix", invoked after changing the alias
#   table! The correct name is "postfix_reload".
#
# version 1.1 (27 Mar 2003)
# - batch user creation (from text file)
#
# version 1.0 (22 Mar 2003)
# - alias handling can now be disabled
# - code cleanup; public release
#
# version 0.9 (20 Mar 2003)
# - alias deletion
#
# version 0.8 (19 Mar 2003)
# - initial release
#
######################################################################

# disable globbing
set -f


# PATH must reach saslpasswd2, sasldblistusers2, newaliases, postfix
PATH=/usr/local/sbin:/usr/sbin:/usr/bin:/sbin:/bin

# set to 0 to disable alias handling
ENABLE_ALIASES=1

# Postfix aliases file. It is wise to use a dedicated file for Cyrus users
# to avoid corruption of the main aliases file.
# This file must be specified in alias_maps and alias_database in main.cf.
ALIASES=/etc/postfix/aliases2

# set to 0 to eliminate annoying "no user has alias 'root'" warnings
CHECK_ROOT_ALIAS=1

# admin user and password to manage the Cyrus system. Refer to
# the "admins:" line in your imapd.conf.
# If CYRUS_PASSWD is unset, the password is asked interactively.
CYRUS_ADMIN=cyrus
CYRUS_PASSWD="cyrus"


# end of user-modifiable section.

result=0
passwd=""
userlist=""
aliaslist=""

function help () {
 echo "Available commands:"
 echo "  a  add user"
 echo "  c  change user password"
 echo "  d  delete user"
 echo "  l  list users"
 echo "  i  import user list from file"
 echo "  p  change admin password (for user '$CYRUS_ADMIN')"
 if [ "$ENABLE_ALIASES" = "1" ] ; then
    echo "  s  add user alias"
 fi
 echo "  h  help"
 echo "  q  quit"
 echo
}


#
#  IMAP functions
#
#

function imap_send() {
 echo ". $1" >& 3
}

# if 1st argument is != "", accept it from server in addition to OK
# (useful when deleting non-existent mailboxes ["NO"] or when logging out
# ["BYE"] )
#
function imap_check() {
 x_allowed=""
 if [ "$1" != "" ] ; then x_allowed=$1 ; fi
 read tag st data <& 3 || exit 1
 data=`echo $data | tr -d "\r"`
 if [ "$st" = "$x_allowed" -o "$st" = "OK" ] ; then
    return 0
 else
    echo "Server error ($st $data)"
    return 1
 fi
}

function imap_login () {
 imap_send "LOGIN $CYRUS_ADMIN $1"
 if imap_check ; then                      
    echo "Logged in."
    return 0
 else
    echo "Login failed!"
    return 1
 fi
}

function imap_create () {
 imap_send "CREATE user/$1"
 imap_check && echo "Mailbox created."
}

function imap_delete () {
 imap_send "SETACL user/$1 $CYRUS_ADMIN c"
 imap_check
 imap_send "DELETE user/$1"
 imap_check NO && echo "Mailbox deleted."
}

function imap_logout() {
 imap_send "LOGOUT"
 imap_check BYE
}

#
#  misc functions
#
#

function get_list () {
 # here we should make sure that mailboxes actually exist...
 userlist=`sasldblistusers2 | cut -d '@' -f 1 | sort | uniq`          
}

function is_in_list() {
 get_list
 for i in $userlist ; do
    if [ "$1" = "$i" ] ; then
       return 0
    fi
 done
 return 1
}

function find_aliases () {
 if [ "$ENABLE_ALIASES" != "1" ] ; then
    aliaslist=""
    return
 fi
 aliaslist=`grep "^.*:.*\<$1\>" $ALIASES | cut -d : -f 1`
}

function list_mail_users () {
 # we expect *two* entries per users (userPassword + cmusaslsecretOTP)
 get_list
 echo "User list:"
 for i in $userlist ; do
    if [ $i = $CYRUS_ADMIN ] ; then
       echo -e "  $i  (admin user, with no mailbox associated)"
    else
       echo -n "  $i"
       find_aliases $i
       if [ "$aliaslist" != "" ] ; then
          echo "  [aliases: "$aliaslist"]"
       else
          echo
       fi
    fi
 done
 echo
}

# if $1 != "" then create user $1 and give it the password $2 (or $1 if
# $2 = "").
function add_mail_user () {
 if [ "$1" != "" ] ; then
    user=$1
 else
    echo -n "User: "
    read user
    if [ "$user" = "" ] ; then
       return
    fi
 fi
 if is_in_list $user ; then
    echo "User already exists!"
    return
 fi
 if [ "`echo $user | sed -e 's/[^0-9a-zA-Z_\.-]//g'`" != $user ] ; then
    echo "Invalid characters in user name! Use only 0-9 a-z A-Z . _ -" ;
    return
 fi
 if [ "$1" != "" ] ; then
    if [ "$2" != "" ] ; then passwd=$2 ; else passwd=$1 ; fi
 else
    ask_passwd 2
    if [ "$passwd" = "" ] ; then
       echo "User not added."
       return
    fi
 fi
 echo $passwd | saslpasswd2 -c -p $user && imap_create $user \
   && echo "User added."
}

function del_mail_user () {
 echo -n "User: "
 read user
 if [ "$user" = "" ] ; then
    return
 fi
 if [ "$user" = "$CYRUS_ADMIN" ] ; then
    echo "This user cannot be deleted!"
    return
 fi
 if ! is_in_list $user ; then
    echo "User not found!"
    return
 fi
 find_aliases $user
 if [ "$aliaslist" != "" ] ; then
    echo "Removing aliases associated with this user..."
    grep -v "^.*:.*\<$user\>" $ALIASES > $ALIASES..tmp
    mv $ALIASES..tmp $ALIASES
    find_aliases $user
    if [ "$aliaslist" != "" ] ; then
       echo "** WARNING: not all aliases for this user could be deleted."
       echo "** Please consider editing the aliases file by hand."
    fi
    echo "Updating alias database..."
    newaliases                      
    echo "Reloading Postfix configuration..."
    postfix reload
 fi

 saslpasswd2 -d $user && imap_delete $user && echo "User deleted."
 if [ "$CHECK_ROOT_ALIAS" = "1" ] ; then
    check_root_alias
 fi
}

function check_root_alias () {
 a=`grep "^root:" $ALIASES | sed -e 's/root: *//'`
 get_list
 if [ "$a" = "" ] || ! is_in_list $a ; then
    echo "** WARNING: no valid user is set to receive mail for 'root'."
    echo "** Please add the alias 'root' to an existing user."
 fi
}

function change_passwd () {
 user=$1
 if [ "$user" = "" ] ; then
    echo -n "User: "
    read user
 fi
 if [ "$user" = "" ] ; then
    return
 fi
 if ! is_in_list $user ; then
    echo "User not found!"
    return
 fi
 if [ "$user" = "$CYRUS_ADMIN" ] ; then
    echo "Changing administrative password (for user '$CYRUS_ADMIN')"
    if [ "$CYRUS_PASSWD" != "" ] ; then
       echo "** WARNING: the password hardcoded in "`which $0`
       echo "** should be adjusted by hand!"
    fi
 fi
 ask_passwd 2
 if [ "$passwd" = "" ] ; then
    echo "Password not changed."
    return
 fi
 echo $passwd | saslpasswd2 -p $user && echo "Password changed."
}

# if $1 = "2", ask twice and return "" if the two inputs don't match.
function ask_passwd () {
 echo -n "Password: "
 read -s passwd
 echo
 if [ "$1" = "2" ] ; then
    echo -n "Password (for confirmation): "
    read -s passwd2
    echo
    if [ "$passwd" != "$passwd2" ] ; then
       echo "Sorry, passwords do not match."
       passwd=""
    fi
 fi
}

# if $1 != "", user and alias are passed in $1 and $2 (batch mode).
# In this case, "newaliases" and "postfix reload" are not issued; it will be
# responsibility of the caller.
#
function add_user_alias () {

 if [ "$1" != "" ] ; then
    user=$1
    if [ "$2" != "" ] ; then
       alias=$2
    else
       return
    fi
 else
    if [ "$ENABLE_ALIASES" != "1" ] ; then
       return
    fi
    echo -n "User: "
    read user
    if [ "$user" = "" ] ; then
       return
    fi
    if ! is_in_list $user ; then
       echo "User not found!"
       return
    fi
    find_aliases $user
    if [ "$aliaslist" != "" ] ; then
       echo "The following aliases for this user already exist: "$aliaslist
       echo -n "Add another one (y/n)? "
       read a
       if [ "$a" != "y" ] ; then
          return
       fi
    fi
    echo -n "Alias: "
    read alias

 fi

 echo "$alias: $user" >> $ALIASES

 # force alias table update only if not in batch mode
 if [ "$1" = "" ] ; then
    postfix_reload
 fi
}


function postfix_reload() {
 echo "Updating alias database..."
 newaliases
 echo "Reloading Postfix configuration..."
 postfix reload
}


function import_users_wrapper() {
 echo -n "File: "
 read file
 if [ "$file" = "" ] ; then
    return
 fi
 if [ ! -r $file ] ; then
    echo "Unable to read file $file!"
    return
 fi

 if import_users $file 0 ; then
    echo "Is it OK (y/n)? "
    read a
    if [ "$a" = "y" ] ; then
       import_users $file 1
    fi
 fi
}


# Import a list of users from a text file.
#
# Each line of the file must have the form
#
#  username   password   alias
#
# Both the "password" and "alias" fields are optional.
# If the username alone appears on a line, it will be given a default password
# consisting of the username itself, and no alias.
# To specify an alias for a user, the password field must be explicitly
# included.
#
# Any number of blanks can appear between the fields.
# Empty lines are ignored.
#
# $1 is the path to the file to be imported.
# if $2 = 0, just check that the file is well-formed.
#
function import_users () {

 users=0
 line=0
 need_postfix_reload=0

 while true ; do

    line=$(($line+1))
    if ! read l ; then break ; fi
    read a b c d << EOF
       `echo $l | tr -d \\\r`
EOF
    
    if [ "$d" != "" ] ; then
       echo "File format error at line $line. Aborting."
       return 1
    fi
    
    # skip empty lines (Windows-style too)
    if [ "$a" = "" ] ; then continue ; fi
    
    if [ "$2" != "0" ] ; then
       echo "Creating user $a..."
       add_mail_user $a $b
       if [ "$c" != "" ] ; then
          add_user_alias $a $c
          need_postfix_reload=1
       fi
       echo
    fi
    
    users=$(($users+1))

 done < $1

 if [ "$2" = "0" ] ; then
    echo "$users user(s) are going to be created."
 else
    echo "$users user(s) created."
    if [ $need_postfix_reload = 1 ] ; then postfix_reload ; fi
 fi
 return 0
}


#
# Everything start here.
#

if [ "$ENABLE_ALIASES" != "1" ] ; then
   CHECK_ROOT_ALIAS=0
fi
echo "Simple Cyrus Admin Tool v1.1.1"
echo "Copyright 2003 Fastpath Research"
echo
echo "Connecting to the IMAP server..."

3<> /dev/tcp/localhost/imap
if [ $? = 1 ] ; then
   echo "Unable to connect to Cyrus server. (Is it actually running?)"
   exit 1
fi

if imap_check ; then
   echo "Connection established."
else
   exit 1
fi

cyr_passwd=$CYRUS_PASSWD

if [ "$cyr_passwd" = "" ] ; then
   echo "Please enter administrative password (for user '$CYRUS_ADMIN'):"
   ask_passwd
   cyr_passwd=$passwd
fi

if [ "$cyr_passwd" = "" ] ; then
   exit 1
fi

if [ "$CYRUS_PASSWD" != "" ] ; then
   echo "Trying login with hardcoded password..."
fi

if ! imap_login $cyr_passwd ; then
   if [ "$CYRUS_PASSWD" = "" ] ; then
      exit 1
   fi
   echo "Trying interactive login..."
   ask_passwd
   if [ "$passwd" = "" ] ; then
      exit 0
   else
      imap_login $passwd || exit 1
   fi
fi

if [ "$CHECK_ROOT_ALIAS" = "1" ] ; then
   check_root_alias
fi

result="x"
while [ "$result" != "q" ] ; do
   echo -n "Command (h for help): "
   read result || break
   case $result in
      a) add_mail_user ;;
      d) del_mail_user ;;
      c) change_passwd ;;
      p) change_passwd $CYRUS_ADMIN ;;
      l) list_mail_users ;;
      i) import_users_wrapper ;;
      s) add_user_alias ;;
      h) help ;;
   esac
done

imap_logout
