jaromail

a commandline tool to easily and privately handle your e-mail
git clone git://parazyd.org/jaromail.git
Log | Files | Refs | Submodules | README

commit 9501d4cb5628adc99c770d09a57fdc0f8b460db0
parent 79ecdf9e5222d47c6699f999f3dace46027ad0d6
Author: Jaromil <jaromil@dyne.org>
Date:   Mon, 14 May 2012 16:20:18 +0200

new account system for multiple accounts
fixes to procmail filters
and several other improvements

Diffstat:
Minstall.sh | 187+++++++++++++++++++++++++++++++++++++++++++++----------------------------------
Mshare/lbdb/lbdb-fetchaddr.sh.in | 2+-
Mshare/lbdb/m_inmail.sh.in | 2+-
Mshare/mutt/general | 7-------
Mshare/procmail/pf-save.rc | 2+-
Asrc/jaro | 746+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dsrc/postino | 569-------------------------------------------------------------------------------
7 files changed, 855 insertions(+), 660 deletions(-)

diff --git a/install.sh b/install.sh @@ -1,85 +1,59 @@ #!/usr/bin/env zsh -# Postino install script - -if ! [ -r src/postino ]; then - echo "Error: this script should be run from inside a postino software distribution" +# jaromail install script +# +# Copyleft (C) 2010-2012 Denis Roio <jaromil@dyne.org> +# +# This source code is free software; you can redistribute it and/or +# modify it under the terms of the GNU Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This source code is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +# Please refer to the GNU Public License for more details. +# +# You should have received a copy of the GNU Public License along with +# this source code; if not, write to: +# Free Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + +if ! [ -r src/jaro ]; then + echo "Error: this script should be run from inside a jaromail software distribution" exit 1 fi +if [ $1 ]; then + MAILDIRS=$1; +else + MAILDIRS=$HOME/Mail +fi -WORKDIR=$HOME/.postino -MAILDIRS=$HOME/Mail +WORKDIR=$MAILDIRS/jaro +PROCMAILDIR=$WORKDIR/.procmail +MUTTDIR=$WORKDIR/.mutt umask 007 # James Bond ;^) -if [ $1 ]; then WORKDIR=$1; fi -# make sure the directory is private -mkdir -p $WORKDIR -mkdir -p $MAILDIRS -source src/postino +source src/jaro -notice "Installing Postino in $WORKDIR" +# make sure the directory is private +${=mkdir} $MAILDIRS +${=mkdir} $WORKDIR -if [ $? != 0 ]; then - error "Postino directory $WORKDIR is not private, set its permissions to 700" - error "Refusing to proceed." - exit 1 -fi +notice "Installing Jaromail in $WORKDIR" -# install the main postino script -mkdir -p ${WORKDIR}/bin -cp src/postino ${WORKDIR}/bin +# install the main jaromail script +${=mkdir} ${WORKDIR}/bin +cp src/jaro ${WORKDIR}/bin # make sure we have a temp and cache dir -mkdir -p $WORKDIR/tmp $WORKDIR/cache +${=mkdir} $WORKDIR/tmp $WORKDIR/cache -if ! [ -r $MAILDIRS/Configuration.txt ]; then - cat <<EOF > $MAILDIRS/Configuration.txt -# Name appearing in From: field -FULLNAME="Anonymous" - -# MAIL USER (the left and right parts of an email) -USER=username -# @ -DOMAIN=gmail.com - -# IMAP (RECEIVE) -IMAP_ADDRESS=imap.gmail.com -IMAP_LOGIN=\${USER}@\${DOMAIN} - -# SMTP (SEND) -SMTP_ADDRESS=smtp.gmail.com -SMTP_LOGIN=\${USER}@\${DOMAIN} -SMTP_PORT=465 - -# SMTP_CERTIFICATE=gmail.pem -# LOCAL FILES -# to change the location of this directory, -# export POSTINO_DIR as env var -# the defaults below should be ok, they place -# mutt, procmail, mstmp and other confs in ~/.postino - -MAILDIRS=$MAILDIRS -MUTTDIR=$WORKDIR/.mutt -PROCMAILDIR=$WORKDIR/.procmail -CERTIFICATES=$HOME/.ssl/certs - -# directory of the sieve filter -# REMOTE_FILTER=/var/mail/... -EOF - act "Default configuration created" -else - error "Existing $MAILDIRS/Configuration.txt skipped" -fi - -# source the default configuration -source $MAILDIRS/Configuration.txt - -if ! [ -r $MAILDIRS/Filters.txt ]; then - cat <<EOF > $MAILDIRS/Filters.txt -# Example filter configuration for Postino +if ! [ -r $WORKDIR/Filters.txt ]; then + cat <<EOF > $WORKDIR/Filters.txt +# Example filter configuration for Jaromail # accepted email addresses to jaromil@dyne.org save priv @@ -112,36 +86,88 @@ from academia.edu save web.academia EOF act "Default filters created" else - error "Existing configuration $MAILDIRS/Filters.txt skipped" + error "Existing configuration $WORKDIR/Filters.txt skipped" fi -source $MAILDIRS/Configuration.txt +if ! [ -r $WORKDIR/Accounts ]; then + ${=mkdir} $WORKDIR/Accounts + cat <<EOF > $WORKDIR/Accounts/README.txt +Directory containing account information + +Each file contains a different account: imap, pop or gmail +each account contains all information needed to connect it + +For example a file named imap.gmail.txt should contain: + +----8<----8<----8<----8<----8<----8<----8<----8<----8<---- +# Name and values are separated by spaces or tabs + +# Name appearing in From: field +name Anonymous + +host imap.gmail.com + +login USERNAME@gmail.com + +auth plain # or kerberos, etc + +transport ssl + +port 993 + +cert /path/to/cert + +# the password field will be filled in automatically + +----8<----8<----8<----8<----8<----8<----8<----8<----8<---- + +Or a file named smtp.gmail.txt should contain: + +----8<----8<----8<----8<----8<----8<----8<----8<----8<---- +# Name and values are separated by spaces or tabs + +host smtp.gmail.com + +login USERNAME@gmail.com + +transport ssl # or "tls" or "plain" + +port 465 + +----8<----8<----8<----8<----8<----8<----8<----8<----8<---- + +EOF + act "Default accounts directory created" +else + error "Existing configuration $WORKDIR/Accounts skipped" +fi # procmail is entirely generated # so overwriting it won't hurt act "Installing procmail scripts" -mkdir -p $PROCMAILDIR +${=mkdir} $PROCMAILDIR cp -a share/procmail/* $PROCMAILDIR # also mutt is safe to override -mkdir -p $MUTTDIR +${=mkdir} $MUTTDIR cp -a share/mutt/* $MUTTDIR act "Installing little brother database" # safe to override -mkdir -p $WORKDIR/.lbdb +${=mkdir} $WORKDIR/.lbdb for aw in munge.awk.in munge-keeporder.awk.in tac.awk.in; do dst=`echo $aw | sed -e 's/.awk.in$//'` cat share/lbdb/$aw \ - | sed -e "s&@AWK@&/usr/bin/env awk&g" \ + | sed -e "s&@AWK@&`which awk`&g" \ > $WORKDIR/.lbdb/$dst done for sh in lbdb-fetchaddr.sh.in lbdb-munge.sh.in lbdb_lib.sh.in lbdbq.sh.in; do dst=`echo $sh | sed -e 's/.sh.in$//'` cat share/lbdb/$sh \ | sed -e "s&@SH@&/usr/bin/env zsh&g" \ - | sed -e "s&@DOTLOCK@&${WORKDIR}/tmp/.lbdb.lock&g" \ - | sed -e "s&@LBDB_VERSION@&0.38-postino&g" \ + | sed -e "s&@DOTLOCK@&mutt_dotlock&g" \ + | sed -e "s&@LBDB_FILE&${WORKDIR}/.lbdb/m_inmail.list&g" \ + | sed -e "s&@LBDB_VERSION@&0.38-jaromail&g" \ | sed -e "s&@prefix@&${WORKDIR}/.lbdb&g" \ | sed -e "s&@exec_prefix@&${WORKDIR}/.lbdb&g" \ | sed -e "s&@libdir@&${WORKDIR}/.lbdb&g" \ @@ -154,7 +180,7 @@ lbdb_modules=(m_finger m_gpg m_inmail m_muttalias m_osx_addressbook m_vcf) for mod in ${lbdb_modules}; do cat share/lbdb/${mod}.sh.in \ | sed -e "s&@SH@&/usr/bin/env zsh&g" \ - | sed -e "s&@LBDB_VERSION@&0.38-postino&g" \ + | sed -e "s&@LBDB_VERSION@&0.38-jaromail&g" \ | sed -e "s&@prefix@&${WORKDIR}/.lbdb&g" \ | sed -e "s&@exec_prefix@&${WORKDIR}/.lbdb&g" \ | sed -e "s&@libdir@&${WORKDIR}/.lbdb&g" \ @@ -178,7 +204,7 @@ ln -sf $WORKDIR/.lbdb $HOME/ # generate initial configuration -src/postino update +MAILDIRS=$MAILDIRS WORKDIR=$WORKDIR src/jaro update case $OS in @@ -187,10 +213,10 @@ case $OS in cp -a build/osx/* $WORKDIR/bin fi touch $HOME/.profile - cat $HOME/.profile | grep '^# Postino' > /dev/null + cat $HOME/.profile | grep '^# Jaromail' > /dev/null if [ $? != 0 ]; then cat <<EOF >> $HOME/.profile -# Postino Installer addition on `date` +# Jaromail Installer addition on `date` export PATH=$WORKDIR/bin:\$PATH # Finished adapting your PATH EOF @@ -201,14 +227,13 @@ EOF ;; esac -notice "Installation completed, now edit your personal settings:" -act "$MAILDIRS/Configuration.txt" +notice "Installation completed" #, now edit your personal settings:" case $OS in GNU) ;; MAC) - open /Applications/TextEdit.app $MAILDIRS/Configuration.txt ;; +# open /Applications/TextEdit.app $WORKDIR/Configuration.txt *) ;; esac diff --git a/share/lbdb/lbdb-fetchaddr.sh.in b/share/lbdb/lbdb-fetchaddr.sh.in @@ -28,7 +28,7 @@ prefix=@prefix@ exec_prefix=@exec_prefix@ dotlock=@DOTLOCK@ fetchaddr=@libdir@/fetchaddr -db=$HOME/.lbdb/m_inmail.list +db=@LBDB_FILE@ datefmt='%Y-%m-%d %H:%M' additional_param="" diff --git a/share/lbdb/m_inmail.sh.in b/share/lbdb/m_inmail.sh.in @@ -23,7 +23,7 @@ prefix=@prefix@ exec_prefix=@exec_prefix@ libdir=@libdir@ -m_inmail_db=$HOME/.lbdb/m_inmail.list +m_inmail_db=@libdir@/m_inmail.list m_inmail_query() { diff --git a/share/mutt/general b/share/mutt/general @@ -1,13 +1,6 @@ # Postino's generic config settings for Mutt # originally used by Jaromil -# specific configuration files -# hardcoded path for now (TODO) -source ~/.postino/.mutt/crypto -source ~/.postino/.mutt/colors -source ~/.postino/.mutt/formats -source ~/.postino/.mutt/keybindings - ############### ## SMTP options unset use_8bitmime diff --git a/share/procmail/pf-save.rc b/share/procmail/pf-save.rc @@ -53,7 +53,7 @@ :0 c * PF_DEST ?? [^ ] * $! DEST ?? $PF_DEST - |procmail -pm PF_RECURSE=yes pf-save.rc + |procmail -pm PF_RECURSE=yes $PMSRC/pf-save.rc # If we were recursing, don't file multiple copies into DEFAULT :0 diff --git a/src/jaro b/src/jaro @@ -0,0 +1,746 @@ +#!/usr/bin/env zsh +# +# Jaro Mail, your humble and faithful electronic postman +# +# a tool to easily and privately handle your e-mail communication +# +# Copyleft (C) 2010-2012 Denis Roio <jaromil@dyne.org> +# +# This source code is free software; you can redistribute it and/or +# modify it under the terms of the GNU Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This source code is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +# Please refer to the GNU Public License for more details. +# +# You should have received a copy of the GNU Public License along with +# this source code; if not, write to: +# Free Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + +VERSION=0.6 +DATE=March/2012 +JAROMAILEXEC=$0 +typeset -a OLDARGS +for arg in ${argv}; do OLDARGS+=($arg); done + +########################## +# declare global variables + +QUIET=0 +DEBUG=1 + + + +# which command to use when creating dirs +mkdir="`which mkdir` -m 700 -p" + + +########################## + + +PARAM=() +typeset -A global_opts +typeset -A opts + +typeset -h name login host protocol port password auth cert + +autoload colors; colors + +# standard output message routines +# it's always useful to wrap them, in case we change behaviour later +notice() { if [[ $QUIET == 0 ]]; then print "$fg_bold[green][*]$fg_no_bold[white] $1" >&2; fi } +error() { if [[ $QUIET == 0 ]]; then print "$fg[red][!]$fg[white] $1" >&2; fi } +func() { if [[ $DEBUG == 1 ]]; then print "$fg[blue][D]$fg[white] $1" >&2; fi } +act() { + if [[ $QUIET == 0 ]]; then + if [ "$1" = "-n" ]; then + print -n "$fg_bold[white] . $fg_no_bold[white] $2" >&2; + else + print "$fg_bold[white] . $fg_no_bold[white] $1" >&2; + fi + fi +} + +# what operating system are we in? use os_detect() +# simplifying modes of operation: GNU or MAC +case $(uname) in + Linux) OS=GNU + notice "Jaro Mail v$VERSION running on GNU/Linux" ;; + + Apple) OS=MAC + notice "Jaro Mail v$VERSION running on Mac/OSX" ;; + + *) OS=GNU # default + error "Running on an unknown operating system, assuming GNU" ;; +esac + + +if [ -z $MAILDIRS ]; then + MAILDIRS=$HOME/Mail +fi + + +# default working dir +WORKDIR=$MAILDIRS/jaro + +${=mkdir} $MAILDIRS +${=mkdir} $WORKDIR + + +if [ -r $WORKDIR/Configuration.txt ]; then + source $WORKDIR/Configuration.txt +fi + +# make sure the directory is private +chmod 700 $WORKDIR + +# make sure we have a temp and cache dir +${=mkdir} "$WORKDIR/tmp" +${=mkdir} "$WORKDIR/cache" +${=mkdir} "$WORKDIR/log" + +PROCMAILDIR=$WORKDIR/.procmail +MUTTDIR=$WORKDIR/.mutt + +# make sure tmp is wiped from sensitive data in case of sigINT +TRAPINT() { + error "Caught signal, aborting operations." + for f in `ls $WORKDIR/tmp/`; do + ${=rm} $f + done + + if [ "$DEBUG" = "1" ]; then + return 1 + else + exit 1 + fi +} + +# we use pinentry +# comes from gpg project and is secure +# it also conveniently uses the right toolkit +pin_entry() { + cat <<EOF | pinentry 2>/dev/null | awk '/^D / { sub(/^D /, ""); print }' +OPTION ttyname=$TTY +OPTION lc-ctype=$LANG +SETTITLE Type your password +SETDESC Type the password for $1 @ $2 +SETPROMPT Password: +GETPIN +EOF +} + + +check_bin() { + # check for required programs + for req in pinentry fetchmail procmail; do + which $req >/dev/null || die "Cannot find $req. Please install it." 1 + done + + # which wipe command to use + which wipe > /dev/null && rm="wipe -f -s" \ + || which srm > /dev/null && rm="srm -fm" \ + || rm="rm -f" +} + +# retrieve a password for user @ domain +# put it in variable password +# up to the caller to unset it after use +ask_password() { + case $OS in + MAC) + func "Looking for password in keyring: $1 @ $2" + security find-internet-password -a $1 -s $2 > /dev/null + if [ $? != 0 ]; then # its a new password + password=`pin_entry $1 @ $2` + act "New password set for $1 @ $2" + security add-internet-password -a $1 -s $2 -w "${password}" + else + act "Using saved password for $1 @ $2" + password=`security find-internet-password -a $1 -s $2 -g \ + 2>&1| awk '/^password:/ { print $2 }' | sed -e 's/"//g'` + fi + return 0 + ;; + GNU) + password=`pin_entry $login $host` + return 0 + ;; + *) + error "Unknown system, can't figure out how to handle passwords" + return 1 + esac +} + +switch_identity() { + act "switch to identity: $name <$login>" + rm -f $MUTTDIR/identity + cat <<EOF > $MUTTDIR/identity +set hostname = $host +set realname = "$name" +set from = "$name <$login>" +EOF +} + + +option_is_set() { + #First argument, the option (something like "-s") + #Second (optional) argument: if it's "out", command will print it out 'set'/'unset' + # This is useful for if conditions + #Return 0 if is set, 1 otherwise + [[ -n ${(k)opts[$1]} ]]; + r=$? + if [[ $2 == out ]]; then + if [[ $r == 0 ]]; then + echo 'set' + else + echo 'unset' + fi + fi + return $r; +} +option_value() { + #First argument, the option (something like "-s") + <<< ${opts[$1]} +} +maildirmake() { + + if [ -z $1 ]; then + error "internal error: missing argument for maildirmake" + return + fi + + if [ -r $1 ]; then + func "maildir exists: $1" + return + fi + + ${=mkdir} ${1}/cur + ${=mkdir} ${1}/new + ${=mkdir} ${1}/tmp + +} +# arg 1 : account name, es: imap.gmail +read_account() { + adir=$WORKDIR/Accounts + acct=$1 + type="`echo $1|cut -d. -f1`" + # find the file + if [ -r $adir/$acct.txt ]; then + acct=$adir/$acct.txt + elif [ -r $adir/$acct ]; then # try without .txt + acct=$adir/$acct + else + error "account $acct not found in $adir" + return 1 + fi + atmp=$WORKDIR/tmp/$1 + case $type in + imap|smtp) + ttmp=`cat $acct | awk ' + /^#/ { next } + /^name/ { for(i=2;i<=NF;i++) printf "%s ", $i; printf ";" } + /^host/ { printf "%s;", $2 } + /^login/ { printf "%s;", $2 } + /^transport/ { printf "%s;", $2 } + /^port/ { printf "%s;", $2 } + /^password/ {printf "%s;", $2 } + /^auth/ { printf "%s;", $2 } + /^cert/ { printf "%s;", $2 } + '` + name=${ttmp[(ws:;:)1]} + host=${ttmp[(ws:;:)2]} + login=${ttmp[(ws:;:)3]} + transport=${ttmp[(ws:;:)4]} + port=${ttmp[(ws:;:)5]} + password=${ttmp[(ws:;:)6]} + auth=${ttmp[(ws:;:)7]} + cert=${ttmp[(ws:;:)8]} + + # if not present the field is equal to the preceeding one + [ "$cert" = "$auth" ] && unset cert + [ "$auth" = "$password" ] && unset auth + [ "$password" = "$port" ] && unset password + + func "name: $name" + func "host: $host" + func "login: $login" + func "trans: $transport" + func "port: $port" + func "cert: $cert" + func "auth: $auth" + ;; + *) + error "Account type \"$type\" not recognized." + return 1 + ;; + esac + return 0 +} + +queue() { + local base; + local tmp; + local mailfile; + local msmtpfile; + + # add mails to the sendout queue + umask 007 + ${=mkdir} $MAILDIRS/outbox + cd $MAILDIRS/outbox || return 1 + notice "Adding mail to the outbox queue" + # Create new unique filenames of the form + # MAILFILE: ccyy-mm-dd-hh.mm.ss[-x].mail + # MSMTPFILE: ccyy-mm-dd-hh.mm.ss[-x].msmtp + # where x is a consecutive number only appended if you send more than one + # mail per second. + base="`date +%Y-%m-%d-%H.%M.%S`" + func "[$base] queue called with params: ${PARAM[@]}" + + if [ -f "$base.mail" -o -f "$base.msmtp" ]; then + tmp="$base" + i=1 + while [ -f "$tmp-$i.mail" -o -f "$tmp-$i.msmtp" ]; do + i=`expr $i + 1` + done + base="$base-$i" + fi + mailfile="$base.mail" + msmtpfile="$base.msmtp" + # Write command line to $MSMTPFILE + echo "$@" > "$msmtpfile" + # Write the mail to $MAILFILE + cat > "$mailfile" + cd - + return 0 +} + + +########### +# FETCHMAIL +fetch() { + adir=$WORKDIR/Accounts + acct=$1 + typeset -al all + if [ -z $acct ]; then # fetch all accounts + for a in `find $adir -name "imap*"`; do all+=($a); done + for a in `find $adir -name "pop*"`; do all+=($a); done + else + # fetch only one account + for a in `find $adir -name "$acct*"`; do all+=($a); done + fi + if ! [ -r $PROCMAILDIR/rc ]; then + act "updating procmail configuration" + update + fi + for a in ${all}; do + read_account `basename $a` + notice "Fetching mails from $name" + touch $WORKDIR/tmp/$host.fetch + chmod 600 $WORKDIR/tmp/$host.fetch + ask_password $login $host + if [ $? != 0 ]; then + error "Error retrieving password for $login on $host" + unset password all + return 1 + fi + + cat <<EOF > $WORKDIR/tmp/$host.fetch +poll $host with proto IMAP user "$login" there with password "$password" +keep fetchall and ssl warnings 3600 and wants mda "procmail -m $PROCMAILDIR/rc" +antispam 571 550 501 554 +EOF + unset password + # this function generates a fetchmail configuration and downloads emails + act "launching fetchmail" + ( sleep 2; act "deleting temporary files" + ${=rm} $WORKDIR/tmp/$host.fetch ) & + fetchmail -f $WORKDIR/tmp/$host.fetch + done + return 0 +} + +###### +# SMTP +send() { + # this function should send all mails in queue + notice "Sending out all mails in outbox queue" + + local -aU smtp_set #behave like a set; that is, an array with unique elements + smtp_set="${SMTP_LOGIN};${SMTP_ADDRESS};${SMTP_PORT}" + for s in ${(f)smtp_set}; do + slogin="${s[(ws:;:)1]}" + shost="${s[(ws:;:)2]}" + sport="${s[(ws:;:)3]}" + func "SMTP: $slogin $shost:$sport" + done + cd ${MAILDIRS}/outbox + mailnum=`ls | grep 'mail$' | wc -l` + mailnum=${mailnum// /} # trim whitespace + if [ "$mailnum" = "0" ]; then + act "Outbox is empty, no mails to send." + return + fi + ask_password ${slogin} ${shost} + cat <<EOF > $WORKDIR/tmp/send +account default +from ${USER}@${DOMAIN} +tls on +host ${SMTP_ADDRESS} +port ${SMTP_PORT} +tls_starttls on +tls_certcheck off +logfile ${WORKDIR}/log/msmtp.log +auth plain +user ${SMTP_LOGIN} +password ${password} +EOF + unset password + chmod 600 $WORKDIR/tmp/send + mailaddr=`cat *.mail | grep '^To:'` + notice "${mailnum} mails to send:" + for mail in *.mail; do + act "`cat ${mail} | grep '^To:'`" + smtp=`echo ${mail} | sed -e 's/mail/msmtp/'` + msmtp -C $WORKDIR/tmp/send "`cat ${smtp}`" < "${mail}" + if [ $? != 0 ]; then + error "Error sending mail, skipped" + else + act "Mail sent succesfully" + rm -f ${mail} ${smtp} + fi + done + cd - + ${=rm} ${WORKDIR}/tmp/send + return 0 +} + +###### +# PEEK +# this function will open the MTA to the imap server without fetching mails locally +peek() { + adir=$WORKDIR/Accounts + if [ -z $1 ]; then + error "Peek needs a specific imap account to login." + act "List of accounts you have configured:" + for i in `find $adir -name "imap*"`; do act "`basename $i`"; done + return 1 + fi + acct=$1 + typeset -al all + for a in `find $adir -name "imap.$acct*"`; do all+=($a); done + read_account `basename ${all[1]}` + notice "Peek into remote imap account $name" + switch_identity + case $transport in + ssl) iproto="imaps" ;; + plain) iproto="imap" ;; + esac + # escape at sign in login + ilogin=`echo $login | sed 's/@/\\@/'` + mutt -F $MUTTDIR/rc -f ${iproto}://${ilogin}@${host} + # TODO automatic input of password in mutt + return $? +} + +update() { + notice "Updating all configurations and filters" + # this function should: + # parse all filters + # generate procmailrc + # generate muttrc + # backup what's too old in the maildirs + # ... + + # debug configuration + func "MAILDIRS: $MAILDIRS" + func "WORKDIR: $WORKDIR" + func "MUTTDIR: $MUTTDIR" + func "PROCMAILDIR: $PROCMAILDIR" + + # make sure maildirs where to put mails exist + ${=mkdir} $MAILDIRS + maildirmake $MAILDIRS/known + maildirmake $MAILDIRS/sent + maildirmake $MAILDIRS/priv + maildirmake $MAILDIRS/postponed + maildirmake $MAILDIRS/unsorted + ${=mkdir} $MAILDIRS/outbox + + ###### + # MUTT + ${=mkdir} $MUTTDIR + rm -f $MUTTDIR/rc + cat<<EOF > $MUTTDIR/rc +# mutt config generated by Jaro Mail +unset use_domain +set folder = $MAILDIRS +set spoolfile = $MAILDIRS/known/ +set record = $MAILDIRS/sent/ +set postponed= $MAILDIRS/postponed/ +set tmpdir = $WORKDIR/tmp +set query_command = "jaro query '%s'" +set sendmail = "jaro queue" +set header_cache= $WORKDIR/cache +set maildir_header_cache_verify=no +set editor = "$EDITOR" +set mailcap_path = "$WORKDIR/mailcap" +EOF + + if [ -r $MUTTDIR/general ]; then + echo "# user tweaked configuration" >> $MUTTDIR/rc + echo "source ${MUTTDIR}/general" >> $MUTTDIR/rc + fi + +cat <<EOF >> $MUTTDIR/rc +# mailboxes in order of priority +source $MUTTDIR/mboxes + +# specific configuration files +# hardcoded path for now (TODO) +source $MUTTDIR/crypto +source $MUTTDIR/colors +source $MUTTDIR/formats +source $MUTTDIR/keybindings +source $MUTTDIR/identity +## end of Jaro Mail generated muttrc +#################################### + +EOF + + + # just the header, will be completed later in procmail loop + rm -f $MUTTDIR/mboxes + echo -n "mailboxes +priv" > $MUTTDIR/mboxes + + ########## + # PROCMAIL + ${=mkdir} $PROCMAILDIR + rm -f $PROCMAILDIR/rc + touch $PROCMAILDIR/rc + cat<<EOF >> $PROCMAILDIR/rc +# procmail configuration file generated by Jaro Mail +MAILDIR=$MAILDIRS +DEFAULT=unsorted/ +VERBOSE=off +LOGFILE=$WORKDIR/log/procmail.log +SHELL = /bin/sh # VERY IMPORTANT +UMASK = 007 # James Bond :-) +LINEBUF = 8192 # avoid procmail choke + +# Using Procmail Module Library http://sf.net/projects/pm-lib +PMSRC = $PROCMAILDIR +# Load the central initial startup code. +INCLUDERC = \$PMSRC/pm-javar.rc +PF_DEST = "" # clear these vars +PF_FROM = "" +# don't save multiple copies +PF_RECURSE = yes +:0 +* ? test \$PMSRC/pf-chkto.rc +{ +EOF + + ####### + echo "# filters generated from Accounts" >> $PROCMAILDIR/rc + typeset -al accts + for f in `cat $WORKDIR/Accounts/* | awk '/^login/ { print $2 }'`; do + echo "ADDR=${f}\tDEST=priv\tINCLUDERC=\$PMSRC/pf-chkto.rc" >> $PROCMAILDIR/rc + done + + ####### + echo "# filters generated from Filters.txt" >> $PROCMAILDIR/rc + + for f in `cat $WORKDIR/Filters.txt | awk '/^#/ {next} /^./ { print $1 ";" $2 ";" $3 ";" $4 }'`; do + header="${f[(ws:;:)1]}" + address="${f[(ws:;:)2]}" + action="${f[(ws:;:)3]}" + destination="${f[(ws:;:)4]}" + case $header in + to) + print "ADDR=${address}\tDEST=${destination}\tINCLUDERC=\$PMSRC/pf-chkto.rc" \ + >> $PROCMAILDIR/rc + ;; + from) + print "ADDR=${address}\tDEST=${destination}\tINCLUDERC=\$PMSRC/pf-check.rc" \ + >> $PROCMAILDIR/rc + ;; + *) + error "unsupported filter: $header (skipped)" + ;; + esac + # MUTT (generate mailboxes priority this parser) + echo " \\" >> $MUTTDIR/mboxes + echo -n " +${destination} " >> $MUTTDIR/mboxes + done + + echo " \\" >> $MUTTDIR/mboxes + echo " +unsorted" >> $MUTTDIR/mboxes + uniq $MUTTDIR/mboxes > $WORKDIR/tmp/mboxes + mv $WORKDIR/tmp/mboxes $MUTTDIR/mboxes + rm -f $WORKDIR/tmp/mboxes + + cat <<EOF >> $PROCMAILDIR/rc +} + +# save the mails +:0 +* PF_DEST ?? . +* ? test \$PMSRC/pf-save.rc +{ INCLUDERC=\$PMSRC/pf-save.rc } + +# if the sender is known (ldbd recognizes it) then put mail in high priority 'known' +:0 w: +* ? formail -x"From:" | head -n1 | tr 'A-Z' 'a-z' | sed 's/.*\W\([0-9a-z_.-]\+@[0-9a-z_.-]\+\).*/\1/' | xargs jaro query +known/ + +# if got here, go to unsorted + +# save the mails +:0 +* PF_DEST ?? . +* ? test \$PMSRC/pf-save.rc +{ INCLUDERC=\$PMSRC/pf-save.rc } + +EOF + return 0 +} + +usage() { + cat <<EOF +Jaro Mail $VERSION - your humble and faithful electronic postman + + Copyright (C) 2010-2012 Dyne.org Foundation, License GNU GPL v3+ + This is free software: you are free to change and redistribute it + The latest Tomb sourcecode is published on <http://tomb.dyne.org> + +Syntact: jaro command [options] [account] + +Commands: + + queue + send + peek + fetch + sync + +Options: + + -h print this help + -v version information for this tool + -q run quietly without printing informations + -D print debugging information at runtime + + +For more informations on Jaro Mail read the manual: man jaro +Please report bugs on <http://bugs.dyne.org>. +EOF +} + + +main() + { + local -A subcommands_opts + ### Options configuration + #Hi, dear developer! Are you trying to add a new subcommand, or to add some options? + #Well, keep in mind that: + # 1. An option CAN'T have differente meanings/behaviour in different subcommands. + # For example, "-s" means "size" and accept an argument. If you are tempted to add + # an option "-s" (that means, for example "silent", and doesn't accept an argument) + # DON'T DO IT! + # There are two reasons for that: + # I. usability; user expect that "-s" is "size + # II. Option parsing WILL EXPLODE if you do this kind of bad things + # (it will say "option defined more than once, and he's right) + main_opts=(q -quiet=q D -debug=D h -help=h v -version=v n -dry-run=n) + subcommands_opts[__default]="" + subcommands_opts[queue]="" + subcommands_opts[fetch]="" + subcommands_opts[send]="" + subcommands_opts[peek]="" + subcommands_opts[update]="" + subcommands_opts[query]="" + subcommands_opts[source]="" +# subcommands_opts[mount]=${subcommands_opts[open]} +# subcommands_opts[create]="s: -size=s -ignore-swap k: -key=k" + ### Detect subcommand + local -aU every_opts #every_opts behave like a set; that is, an array with unique elements + for optspec in $subcommands_opts$main_opts; do + for opt in ${=optspec}; do + every_opts+=${opt} + done + done + local -a oldstar + oldstar=($argv) + zparseopts -M -E -D -Adiscardme ${every_opts} + unset discardme + subcommand=$1 + if [[ -z $subcommand ]]; then + subcommand="__default" + fi + if [[ -z ${(k)subcommands_opts[$subcommand]} ]]; then #there's no such subcommand + error "Subcommand '$subcommand' doesn't exist" + exit 127 + fi + argv=(${oldstar}) + unset oldstar + + ### Parsing global + command-specific options + # zsh magic: ${=string} will split to multiple arguments when spaces occur + set -A cmd_opts ${main_opts} ${=subcommands_opts[$subcommand]} + if [[ -n $cmd_opts ]]; then #if there is no option, we don't need parsing + zparseopts -M -E -D -Aopts ${cmd_opts} + if [[ $? != 0 ]]; then + error "Some error occurred during option processing." + exit 127 + fi + fi + #build PARAM (array of arguments) and check if there are unrecognized options + local ok=0 +# PARAM=() + for arg in $*; do + if [[ $arg == '--' || $arg == '-' ]]; then + ok=1 + continue #it shouldnt be appended to PARAM + elif [[ $arg[1] == '-' ]]; then + if [[ $ok == 0 ]]; then + error "unrecognized option $arg" + exit 127 + fi + fi + PARAM+=$arg + done + #first parameter actually is the subcommand: delete it and shift + if [[ $subcommand != '__default' ]]; then + PARAM[1]=() + shift + fi + ### End parsing command-specific options + + if option_is_set -v; then act "Jaro Mail - $VERSION"; fi + if option_is_set -h; then usage; fi + if option_is_set -q; then QUIET=1; fi + if option_is_set -D; then func "Debug messages ON"; DEBUG=1; fi + + case "$subcommand" in + queue) queue ${PARAM} ;; + fetch) fetch ${PARAM} ;; + send) send ${PARAM} ;; + peek) peek ${PARAM} ;; + update) update ;; + query) ${WORKDIR}/.lbdb/lbdbq ${PARAM} ;; + 'source') return 0 ;; + __default) ;; + *) error "command \"$subcommand\" not recognized" + act "try -h for help" + return 1 + ;; + esac + return 0 +} + +check_bin +main $@ diff --git a/src/postino b/src/postino @@ -1,569 +0,0 @@ -#!/usr/bin/env zsh -# -# Postino, your humble and faithful electronic postman -# -# a tool to easily and privately handle your e-mail communication -# -# Copyleft (C) 2010-2012 Denis Roio <jaromil@dyne.org> -# -# This source code is free software; you can redistribute it and/or -# modify it under the terms of the GNU Public License as published by -# the Free Software Foundation; either version 3 of the License, or -# (at your option) any later version. -# -# This source code is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. -# Please refer to the GNU Public License for more details. -# -# You should have received a copy of the GNU Public License along with -# this source code; if not, write to: -# Free Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. - -VERSION=0.6 -DATE=March/2012 -POSTINOEXEC=$0 -typeset -a OLDARGS -for arg in ${argv}; do OLDARGS+=($arg); done - -########################## -# declare global variables - -QUIET=0 -DEBUG=1 - - - -# which command to use when creating dirs -mkdir="`which mkdir` -m 700 -p" - - -########################## - - -PARAM=() -typeset -A global_opts -typeset -A opts - -autoload colors; colors - -# standard output message routines -# it's always useful to wrap them, in case we change behaviour later -notice() { if [[ $QUIET == 0 ]]; then print "$fg_bold[green][*]$fg_no_bold[white] $1" >&2; fi } -error() { if [[ $QUIET == 0 ]]; then print "$fg[red][!]$fg[white] $1" >&2; fi } -func() { if [[ $DEBUG == 1 ]]; then print "$fg[blue][D]$fg[white] $1" >&2; fi } -act() { - if [[ $QUIET == 0 ]]; then - if [ "$1" = "-n" ]; then - print -n "$fg_bold[white] . $fg_no_bold[white] $2" >&2; - else - print "$fg_bold[white] . $fg_no_bold[white] $1" >&2; - fi - fi -} - -# what operating system are we in? use os_detect() -# simplifying modes of operation: GNU or MAC -case $(uname) in - Linux) OS=GNU - act "Running on GNU/Linux" ;; - - Apple) OS=MAC - act "Running on Mac/OSX" ;; - - *) OS=GNU # default - error "Running on an unknown operating system, assuming GNU" ;; -esac - - -if ! [ -r $HOME/Mail/Configuration.txt ]; then return; fi - -source $HOME/Mail/Configuration.txt - -# default working dir -if [ -z $POSTINO_DIR ]; then - export WORKDIR=$HOME/.postino -else - export WORKDIR=$POSTINO_DIR -fi - - -# make sure the directory is private -chmod 700 $WORKDIR -if [ $? != 0 ]; then - error "Postino directory $WORKDIR is not private, set permissions to 700" - error "Refusing to proceed." - exit 1 -fi - -# make sure we have a temp and cache dir -${=mkdir} "$WORKDIR/tmp" -${=mkdir} "$WORKDIR/cache" - - -# make sure tmp is wiped from sensitive data in case of sigINT -TRAPINT() { - error "Caught signal, aborting operations." - for f in `ls $WORKDIR/tmp/`; do - srm -fm $f - done - exit 1 -} - -# we use pinentry -# comes from gpg project and is secure -# it also conveniently uses the right toolkit -pin_entry() { - cat <<EOF | pinentry 2>/dev/null | awk '/^D / { sub(/^D /, ""); print }' -OPTION ttyname=$TTY -OPTION lc-ctype=$LANG -SETTITLE Type your password -SETDESC Type the password for $1 @ $2 -SETPROMPT Password: -GETPIN -EOF -} - -# retrieve a password for user @ domain -# put it in variable password -# up to the caller to unset it after use -ask_password() { - func "Looking for password in keyring: $1 @ $2" - security find-internet-password -a $1 -s $2 > /dev/null - if [ $? != 0 ]; then # its a new password - export password=`pin_entry $1 @ $2` - act "New password set for $1 @ $2" - security add-internet-password -a $1 -s $2 -w "${password}" - else - act "Using saved password for $1 @ $2" - export password=`security find-internet-password -a $1 -s $2 -g \ - 2>&1| awk '/^password:/ { print $2 }' | sed -e 's/"//g'` - fi -} - -option_is_set() { - #First argument, the option (something like "-s") - #Second (optional) argument: if it's "out", command will print it out 'set'/'unset' - # This is useful for if conditions - #Return 0 if is set, 1 otherwise - [[ -n ${(k)opts[$1]} ]]; - r=$? - if [[ $2 == out ]]; then - if [[ $r == 0 ]]; then - echo 'set' - else - echo 'unset' - fi - fi - return $r; -} -option_value() { - #First argument, the option (something like "-s") - <<< ${opts[$1]} -} -maildirmake() { - - if [ -z $1 ]; then - error "internal error: missing argument for maildirmake" - return - fi - - if [ -r $1 ]; then - func "maildir exists: $1" - return - fi - - ${=mkdir} ${1}/cur - ${=mkdir} ${1}/new - ${=mkdir} ${1}/tmp - -} - -queue() { - local base; - local tmp; - local mailfile; - local msmtpfile; - - # add mails to the sendout queue - umask 007 - ${=mkdir} $MAILDIRS/outbox - cd $MAILDIRS/outbox || return 1 - notice "Adding mail to the outbox queue" - # Create new unique filenames of the form - # MAILFILE: ccyy-mm-dd-hh.mm.ss[-x].mail - # MSMTPFILE: ccyy-mm-dd-hh.mm.ss[-x].msmtp - # where x is a consecutive number only appended if you send more than one - # mail per second. - base="`date +%Y-%m-%d-%H.%M.%S`" - func "[$base] queue called with params: ${PARAM[@]}" - - if [ -f "$base.mail" -o -f "$base.msmtp" ]; then - tmp="$base" - i=1 - while [ -f "$tmp-$i.mail" -o -f "$tmp-$i.msmtp" ]; do - i=`expr $i + 1` - done - base="$base-$i" - fi - mailfile="$base.mail" - msmtpfile="$base.msmtp" - # Write command line to $MSMTPFILE - echo "$@" > "$msmtpfile" - # Write the mail to $MAILFILE - cat > "$mailfile" - cd - - return 0 -} - - -########### -# FETCHMAIL -fetch() { - notice "Fetching mails from server" - # this function generates a fetchmail configuration and downloads emails - local -aU imap_set # behave like a set - imap_set="${IMAP_LOGIN};${IMAP_ADDRESS}" - for i in ${(f)imap_set}; do - ilogin="${i[(ws:;:)1]}" - ihost="${i[(ws:;:)2]}" - func "IMAP: $ilogin $ihost" - done - if ! [ -r $PROCMAILDIR/rc ]; then - act "Syncing configuration" - sync - fi - ask_password $ilogin $ihost - touch $WORKDIR/tmp/fetch; chmod 600 $WORKDIR/tmp/fetch - cat <<EOF > $WORKDIR/tmp/fetch -poll $ihost with proto IMAP user "$ilogin" there with password "$password" -keep and ssl warnings 3600 and wants mda "procmail -m $PROCMAILDIR/rc" -antispam 571 550 501 554 -EOF - unset password - - act "Launching fetchmail" - fetchmail -f $WORKDIR/tmp/fetch - srm -fm $WORKDIR/tmp/fetch - return 0 -} - -###### -# SMTP -send() { - # this function should send all mails in queue - notice "Sending out all mails in outbox queue" - - local -aU smtp_set #behave like a set; that is, an array with unique elements - smtp_set="${SMTP_LOGIN};${SMTP_ADDRESS};${SMTP_PORT}" - for s in ${(f)smtp_set}; do - slogin="${s[(ws:;:)1]}" - shost="${s[(ws:;:)2]}" - sport="${s[(ws:;:)3]}" - func "SMTP: $slogin $shost:$sport" - done - cd ${MAILDIRS}/outbox - mailnum=`ls | grep 'mail$' | wc -l` - mailnum=${mailnum// /} # trim whitespace - if [ "$mailnum" = "0" ]; then - act "Outbox is empty, no mails to send." - return - fi - ask_password ${slogin} ${shost} - cat <<EOF > $WORKDIR/tmp/send -account default -from ${USER}@${DOMAIN} -tls on -host ${SMTP_ADDRESS} -port ${SMTP_PORT} -tls_starttls on -tls_certcheck off -logfile ${WORKDIR}/msmtp.log -auth plain -user ${SMTP_LOGIN} -password ${password} -EOF - unset password - chmod 600 $WORKDIR/tmp/send - mailaddr=`cat *.mail | grep '^To:'` - notice "${mailnum} mails to send:" - for mail in *.mail; do - act "`cat ${mail} | grep '^To:'`" - smtp=`echo ${mail} | sed -e 's/mail/msmtp/'` - msmtp -C $WORKDIR/tmp/send "`cat ${smtp}`" < "${mail}" - if [ $? != 0 ]; then - error "Error sending mail, skipped" - else - act "Mail sent succesfully" - rm -f ${mail} ${smtp} - fi - done - cd - - srm -fm ${WORKDIR}/tmp/send - return 0 -} - -###### -# IMAP -peek() { - notice "Peeking into remote mailbox" - # this function will open the MTA to the imap server without fetching mails locally - local -aU imap_set #behave like a set; that is, an array with unique elements - imap_set="${IMAP_LOGIN};${IMAP_ADDRESS}" - for i in ${(f)imap_set}; do - ilogin="${i[(ws:;:)1]}" - ihost="${i[(ws:;:)2]}" - func "IMAP: $ilogin $ihost" - done - # escape at sign in login - mutt -f imaps://`echo $ilogin | sed 's/@/\\@/'`@${ihost} -} - -update() { - notice "Updating all configurations and filters" - # this function should: - # parse all filters - # generate procmailrc - # generate muttrc - # backup what's too old in the maildirs - # ... - - # debug configuration - func "WORKDIR: $WORKDIR" - func "MAILDIRS: $MAILDIRS" - - # make sure maildirs where to put mails exist - ${=mkdir} $MAILDIRS - maildirmake $MAILDIRS/known - maildirmake $MAILDIRS/sent - maildirmake $MAILDIRS/priv - maildirmake $MAILDIRS/postponed - maildirmake $MAILDIRS/unsorted - ${=mkdir} $MAILDIRS/outbox - - ###### - # MUTT - if ! [ -z $MUTTDIR ]; then - ${=mkdir} $MUTTDIR - rm -f $MUTTDIR/rc - cat<<EOF > $MUTTDIR/rc -# mutt config generated by postino -unset use_domain -set hostname = $DOMAIN -set realname = $FULLNAME -set from = "$FULLNAME <${USER}@${DOMAIN}>" -set folder = $MAILDIRS -set spoolfile = $MAILDIRS/known/ -set record = $MAILDIRS/sent/ -set postponed= $MAILDIRS/postponed/ -set tmpdir = $WORKDIR/tmp -set query_command = "postino query '%s'" -set sendmail = "postino queue" -set header_cache= $MUTTDIR/cache -set maildir_header_cache_verify=no -set editor = "$EDITOR" -set mailcap_path = "$WORKDIR/mailcap" - -# mailboxes in order of priority -source $MUTTDIR/mboxes -EOF - - if [ -r $MUTTDIR/general ]; then - echo "# user tweaked configuration" >> $MUTTDIR/rc - echo "source ${MUTTDIR}/general" >> $MUTTDIR/rc - fi - echo "## end of generated muttrc" >> $MUTTDIR/rc - echo "##########################" >> $MUTTDIR/rc - echo >> $MUTTDIR/rc - - # just the header, will be completed later in procmail loop - rm -f $WORKDIR/mutt/mboxes - echo -n "mailboxes +priv" > $MUTTDIR/mboxes - fi - - ########## - # PROCMAIL - ${=mkdir} $PROCMAILDIR - rm -f $PROCMAILDIR/rc - touch $PROCMAILDIR/rc - cat<<EOF >> $PROCMAILDIR/rc -# procmail configuration file generated by postino -MAILDIR=$MAILDIRS -DEFAULT=unsorted/ -VERBOSE=off -LOGFILE=$PROCMAILDIR/log -SHELL = /bin/sh # VERY IMPORTANT -UMASK = 007 # James Bond :-) -LINEBUF = 8192 # avoid procmail choke -src/postino update -# Using Procmail Module Library http://sf.net/projects/pm-lib -PMSRC = $PROCMAILDIR -# Load the central initial startup code. -INCLUDERC = $PMSRC/pm-javar.rc -PF_DEST = "" # clear these vars -PF_FROM = "" -# don't save multiple copies -PF_RECURSE = yes -:0 -* ? test pf-chkto.rc -{ -# filters generated from postino Filters.txt -EOF - for f in `cat $MAILDIRS/Filters.txt | awk '/^#/ {next} /^./ { print $1 ";" $2 ";" $3 ";" $4 }'`; do - header="${f[(ws:;:)1]}" - address="${f[(ws:;:)2]}" - action="${f[(ws:;:)3]}" - destination="${f[(ws:;:)4]}" - case $header in - to) - print "ADDR=${address}\tDEST=${destination}\tINCLUDERC=$PMSRC/pf-chkto.rc" \ - >> $PROCMAILDIR/rc - ;; - from) - print "ADDR=${address}\tDEST=${destination}\tINCLUDERC=$PMSRC/pf-check.rc" \ - >> $PROCMAILDIR/rc - ;; - *) - error "unsupported filter: $header (skipped)" - ;; - esac - # MUTT (generate mailboxes priority this parser) - echo " \\" >> $MUTTDIR/mboxes - echo -n " +${destination} " >> $MUTTDIR/mboxes - done - - echo " \\" >> $MUTTDIR/mboxes - echo " +unsorted" >> $MUTTDIR/mboxes - uniq $MUTTDIR/mboxes > $WORKDIR/tmp/mboxes - mv $WORKDIR/tmp/mboxes $MUTTDIR/mboxes - rm -f $WORKDIR/tmp/mboxes - - cat <<EOF >> $PROCMAILDIR/rc -} - -# save the mails -:0 -* PF_DEST ?? . -* ? test $PMSRC/pf-save.rc -{ INCLUDERC=$PMSRC/pf-save.rc } - -# if the sender is known (ldbd recognizes it) then put mail in high priority 'known' -:0 w: -* ? formail -x"From:" | head -n1 | tr 'A-Z' 'a-z' | sed 's/.*\W\([0-9a-z_.-]\+@[0-9a-z_.-]\+\).*/\1/' | xargs lbdbq -known/ - -# if got here, go to unsorted - -# save the mails -:0 -* PF_DEST ?? . -* ? test $PMSRC/pf-save.rc -{ INCLUDERC=$PMSRC/pf-save.rc } - -EOF - return 0 -} - - -main() - { - local -A subcommands_opts - ### Options configuration - #Hi, dear developer! Are you trying to add a new subcommand, or to add some options? - #Well, keep in mind that: - # 1. An option CAN'T have differente meanings/behaviour in different subcommands. - # For example, "-s" means "size" and accept an argument. If you are tempted to add - # an option "-s" (that means, for example "silent", and doesn't accept an argument) - # DON'T DO IT! - # There are two reasons for that: - # I. usability; user expect that "-s" is "size - # II. Option parsing WILL EXPLODE if you do this kind of bad things - # (it will say "option defined more than once, and he's right) - main_opts=(q -quiet=q D -debug=D h -help=h v -version=v n -dry-run=n) - subcommands_opts[__default]="" - subcommands_opts[queue]="" - subcommands_opts[fetch]="" - subcommands_opts[send]="" - subcommands_opts[peek]="" - subcommands_opts[update]="" - subcommands_opts[query]="" - subcommands_opts[source]="" -# subcommands_opts[mount]=${subcommands_opts[open]} -# subcommands_opts[create]="s: -size=s -ignore-swap k: -key=k" - ### Detect subcommand - local -aU every_opts #every_opts behave like a set; that is, an array with unique elements - for optspec in $subcommands_opts$main_opts; do - for opt in ${=optspec}; do - every_opts+=${opt} - done - done - local -a oldstar - oldstar=($argv) - zparseopts -M -E -D -Adiscardme ${every_opts} - unset discardme - subcommand=$1 - if [[ -z $subcommand ]]; then - subcommand="__default" - fi - if [[ -z ${(k)subcommands_opts[$subcommand]} ]]; then #there's no such subcommand - error "Subcommand '$subcommand' doesn't exist" - exit 127 - fi - argv=(${oldstar}) - unset oldstar - - ### Parsing global + command-specific options - # zsh magic: ${=string} will split to multiple arguments when spaces occur - set -A cmd_opts ${main_opts} ${=subcommands_opts[$subcommand]} - if [[ -n $cmd_opts ]]; then #if there is no option, we don't need parsing - zparseopts -M -E -D -Aopts ${cmd_opts} - if [[ $? != 0 ]]; then - error "Some error occurred during option processing." - exit 127 - fi - fi - #build PARAM (array of arguments) and check if there are unrecognized options - local ok=0 -# PARAM=() - for arg in $*; do - if [[ $arg == '--' || $arg == '-' ]]; then - ok=1 - continue #it shouldnt be appended to PARAM - elif [[ $arg[1] == '-' ]]; then - if [[ $ok == 0 ]]; then - error "unrecognized option $arg" - exit 127 - fi - fi - PARAM+=$arg - done - #first parameter actually is the subcommand: delete it and shift - if [[ $subcommand != '__default' ]]; then - PARAM[1]=() - shift - fi - ### End parsing command-specific options - - if option_is_set -v; then act "Postino - $VERSION"; fi - if option_is_set -h; then error "TODO usage here"; fi - if option_is_set -q; then QUIET=1; fi - if option_is_set -D; then func "Debug messages ON"; DEBUG=1; fi - - case "$subcommand" in - queue) queue ${PARAM} ;; - fetch) fetch ;; - send) send ;; - peek) peek ;; - update) update ;; - query) lbdbq $@ ;; - 'source') return 0 ;; - __default) ;; - *) error "command \"$subcommand\" not recognized" - act "try -h for help" - return 1 - ;; - esac - return 0 -} - -main $@