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 c545395aac97837ce33d96710aff08fb519e93c6
parent b37c1ea65b76962077502faf1aa4a34da1b70ef3
Author: Jaromil <jaromil@dyne.org>
Date:   Mon, 18 Jun 2012 01:52:55 +0200

main script broken up into zsh libraries compiled at install time

Diffstat:
Msrc/fetchdate.c | 4+---
Msrc/jaro | 1675+------------------------------------------------------------------------------
Asrc/zlibs/accounts | 356+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/zlibs/addressbook | 153+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/zlibs/cmdline | 111+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/zlibs/email | 298+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/zlibs/helpers | 213+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/zlibs/locking | 81+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/zlibs/maildirs | 147+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/zlibs/password | 166+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/zlibs/search | 259+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/zlibs/stats | 208+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
12 files changed, 2007 insertions(+), 1664 deletions(-)

diff --git a/src/fetchdate.c b/src/fetchdate.c @@ -52,10 +52,8 @@ int main (int argc, char **argv) { #endif free_rfc822(parsed); - } else { - printf("Error parsing %s\n",argv[1]); - exit(1); } + exit(0); } diff --git a/src/jaro b/src/jaro @@ -123,6 +123,18 @@ chmod 700 $WORKDIR PATH=$WORKDIR/bin:/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/opt/local/bin +# load our ZLibs +. $WORKDIR/zlibs/accounts +. $WORKDIR/zlibs/addressbook +. $WORKDIR/zlibs/cmdline +. $WORKDIR/zlibs/email +. $WORKDIR/zlibs/helpers +. $WORKDIR/zlibs/locking +. $WORKDIR/zlibs/maildirs +. $WORKDIR/zlibs/password +. $WORKDIR/zlibs/search +. $WORKDIR/zlibs/stats + # temporary directory TMPDIR=$WORKDIR/tmp case $OS in @@ -197,1663 +209,6 @@ TRAPINT() { else exit 1; fi } -lock() { - func "lock: $1" - $WORKDIR/bin/dotlock ${1} - case $? in - 1) error "Cannot lock non existing file: $1" - return 1 ;; - 3) error "Locked file in use: $1" - return 3 ;; - 5) error "Impossible to lock: $1" - return 5 ;; - # success - 0) echo "$$" > ${1}.pid - return 0 ;; - esac -} -newlock() { # create locked - func "creating locked file: $1" - touch $1 - chmod 600 $1 - lock $1 -} -unlock() { - func "unlock: $1" - lockpid="`cat ${1}.pid`" - { test -r ${1}.pid } && { - { test "$$" != "$lockpid" } && { - error "Unlock attempt by different PID: $1" - error "Created by $lockpid now $$ is trying to unlock" - return 1 } - } - - $WORKDIR/bin/dotlock -u ${=@} - { test $? != 0 } && { error "Unable to unlock: $1"; return 1 } - { test -r ${1}.pid } && { rm -f ${1}.pid } - return 0 -} -unlink() { # delete a file that we are locking - func "unlink: $1" - # use with care! this can permanently erase currently locked files - # only the locking PID should use it on its own locks - - { test -r ${1}.pid } && { - lockpid="`cat ${1}.pid`" - { test "$$" != "$lockpid" } && { - error "Unlock attempt by different PID: $1" - error "Created by $lockpid now $$ is trying to unlock" - return 1 } - } - - ( - ${=rm} ${1} - touch ${1} - $WORKDIR/bin/dotlock -d -f ${1} - { test $? != 0 } && { error "Unable to unlink: $1"; return 1 } - { test -r ${1}.pid } && { rm -f ${1}.pid } - ) &! - return 0 -} - - -# 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 mutt; do - which $req >/dev/null - { test $? != 0 } && { - error "Cannot find $req. Please install it." - return 1 - } - done - - # which wipe command to use - which wipe > /dev/null - { test $? = 0 } && { - rm="wipe -f -s -q -R /dev/urandom"; return 0 } - which srm > /dev/null - { test $? = 0 } && { - rm="srm -m"; return 0 } - rm="rm -f" - return 0 -} - -# 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: $name" - case $OS in - MAC) - security find-internet-password \ - -c JARO -a $email -s $host \ - -p $transport -P $port > /dev/null - if [ $? != 0 ]; then # its a new password - new_password - { test $? != 0 } && { - error "Password input aborted." - return 1 } - else - password=`security find-internet-password -c JARO -a $email -s $host -p $transport -P $port -g 2>&1| awk '/^password:/ { print $2 }' | sed -e 's/"//g'` - fi - return 0 - ;; - ##################################### - GNU) - func "Looking for password in keyring: $name" - ################### - # USE GNOME KEYRING - { test $GNOMEKEY = 1 } && { - echo "protocol=${type}\npath=jaromail/${email}\nusername=${login}\nhost=${host}\n\n" \ - | $WORKDIR/bin/jaro-gnome-keyring check - if [ $? != 0 ]; then # its a new password - new_password - { test $? != 0 } && { - error "Password input aborted." - return 1 } - else # password found into gnome keyring - act "Using saved password for $1 @ $2" - password=`echo "protocol=${type}\npath=jaromail/${email}\nusername=${login}\nhost=${host}\n\n" | $WORKDIR/bin/jaro-gnome-keyring get` - fi - return 0 - } - #################### - # USE PINENTRY ALONE - new_password - { test $? != 0 } && { - error "Password input aborted." - return 1 } - return 0 - ;; - *) - error "Unknown system, can't figure out how to handle passwords" - return 1 - esac -} - -new_password() { - notice "Setting a new password for $name" - password=`pin_entry $login $host` - res=0 - case $OS in - MAC) - if [ "$password" != "" ]; then - security add-internet-password \ - -c JARO -a $email -s $host \ - -p $transport -P $port -w "${password}" - if [ $? != 0 ]; then - error "Error adding password to keyring." - else - act "New password saved in keyring" - fi - return 0 - else - security delete-internet-password \ - -c JARO -a $email -s $host \ - -p $transport -P $port > /dev/null - res=$?; unset password - { test $res != 0 } && { - error "Error deleting password from keyring." - return 1 } - act "No new password given, old password erased." - return 0 - fi - ;; - GNU) - if [ "$password" != "" ]; then # password was written - - # USE GNOME KEYRING - { test $GNOMEKEY = 1 } && { - - cat <<EOF | $WORKDIR/bin/jaro-gnome-keyring store -protocol=${type} -path=jaromail/${email} -username=${login} -host=${host} -password=${password} -EOF - { test $? != 0 } && { error "Error saving password in Gnome keyring" } - return 0 - } - - return 0 - - else # password is blank or aborted - - # USE GNOME KEYRING - { test $GNOMEKEY = 1 } && { - - cat <<EOF | $WORKDIR/bin/jaro-gnome-keyring erase -protocol=${type} -path=jaromail/${email} -username=${login} -host=${host} -EOF - { test $? != 0 } && { - error "Error accessing password in Gnome keyring" - return 1 } - act "No new password given, old password erased." - return 0 - } - - return 1 - - fi - ;; - *) - error "Unknown system, can't figure out how to handle passwords" - return 1 - esac -} -switch_identity() { - if [ "$name" != "" ]; then - 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 - else - error "No identity found, left blank." - touch $MUTTDIR/identity - 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]} -} - -# checks if its a maildir -# returns 0 (success) if yes -# no in all other cases -maildircheck() { - { test -r $1 } || { - error "Maildir not existing: $1" - return 1 } - { test -w $1 } || { - error "Directory not writable: $1" - return 1 } - { test -r $1/cur } \ - && { return 0 } # Yes is a maildir -# shortened test to speedup -# && { test -r $1/new } \ -# && { test -r $1/tmp } \ - func "Not a maildir: $1" - return 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 type, es: imap or smtp -# -a defines which account name other than 'default' -read_account() { - typeset -al all - type=$1 - # find the file - func "read_account looking for \"$account\"" - for a in `find $WORKDIR/Accounts -name "$type.$account*"`; do - func "found account: $a" - all+=($a) - done - if [ ${#all} = 0 ]; then - error "No $type account found: $account" - act "Refine your argument using '-a type.account'" - act "For instance adding to the commandline: -a imap.default" - act "Available accounts (excluding symbolic links, omit final .txt):" - for a in `find $WORKDIR/Accounts -type f | grep -v README | sed 's/.txt//'`; do - act -n "`basename $a`\t :: " - awk ' -/^name/ { for(i=2;i<=NF;i++) printf "%s ", $i } -/^email/ { printf "<%s>", $2 } -/^host/ { printf " on %s", $2 } -{next}' $a.txt - echo " (`basename $a | cut -d. -f1`)" - done - return 1 - elif [ ${#all} != 1 ]; then - error "Too many $type accounts named $account" - act -n "" - for i in ${=all}; do echo -n "`basename ${i}` "; done - echo; error "Refine your account keyword using -a option" - return 1 - fi - - acct=${all[1]} - type=`basename $acct | cut -d. -f1` - - case $type in - imap|smtp) - - lock $acct - ttmp=`cat $acct | awk ' - /^#/ { next } - /^name/ { printf "name=\""; for(i=2;i<=NF;i++) printf "%s ", $i; printf "\";" } - /^email/ { printf "email=\"%s\";", $2 } - /^host/ { printf "host=\"%s\";", $2 } - /^login/ { printf "login=\"%s\";", $2 } - /^transport/ { printf "transport=\"%s\";", $2 } - /^port/ { printf "port=\"%s\";", $2 } - /^password/ { printf "password=\"%s\";", $2 } - /^auth/ { printf "auth=\"%s\";", $2 } - /^cert/ { printf "cert=\"%s\";", $2 } - /^options/ { printf "accountopt=\""; for(i=2;i<=NF;i++) printf "%s ", $i; printf "\";" } - /^folders/ { printf "folders=\""; for(i=2;i<=NF;i++) printf "%s ", $i; printf "\";" } - '` - unlock $acct - - eval "$ttmp" - # check required fields - { test -z $host } && { error "Field missing in account $acct: host"; return 1 } - - # fill in defaults - { test -z $name } && { name="$type" } - { test -z $login } && { login="$email" } # usually email and login are the same - { test -z $email } && { email="$login" } # so if one is specified, deduce the other - { test -z $transport } && { transport=plain } - { test -z $port } && { port=143 } - { test -z $auth } && { auth=plain } - { test -z $cert } && { cert=ignore } - { test -z $accountopt } && { accountopt=keep } - # cert and password can be missing - - func "type: $type" - func "name: $name" - func "email: $email" - func "host: $host" - func "login: $login" - func "trans: $transport" - func "port: $port" - func "cert: $cert" - func "auth: $auth" - func "options: $accountopt" - func "folders: $folders" - ;; - *) - error "Account type \"$type\" not recognized." - return 1 - ;; - esac - return 0 -} - -# start without options: auto -# read or compose depending if there is an argument that is an email -# or a folder to start with - -autostart() { -# no argument passed. open known folder - { test -z ${1} } && { - - { test ! -r $MUTTDIR/rc } \ - && { error "Jaro Mail is not yet configured."; return 1 } - - mutt -F $MUTTDIR/rc - return $? - } - - # argument passed: determine if an email - echo "${1}" \ - | tr 'A-Z' 'a-z' \ - | grep '^[a-zA-Z0-9._%+-]*@[a-zA-Z0-9]*[\.[a-zA-Z0-9]*]*[a-zA-Z0-9]$' \ - > /dev/null - { test $? = 0 } && { - notice "Composing message to: ${1}" - # its an email, TODO see if we have it in our addressbook - mutt -F $MUTTDIR/rc "${1}" - return 0 - } - # or a path to folder - { test -r ${1} } && { - mutt -F $MUTTDIR/rc -f ${1} - return 0 - } - - # or the name of a folder in Jaro Mail - { test -r $MAILDIRS/${1} } && { - notice "Opening folder ${1}" - mutt -F $MUTTDIR/rc -f "$MAILDIRS/${1}" - return 0 - } - - return $? -} - -queue() { - local base; - local tmp; - local mailfile; - local msmtpfile; - - # add mails to the sendout queue - ${=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" - lock $msmtpfile - # Write the mail to $MAILFILE - cat > "$mailfile" - unlock $msmtpfile - cd - - return 0 -} - -###### -# CERT -# downloads and/or installs certificates -cert() { - if [ -z $1 ]; then - error "Certificate handler called without an argument" - return 1; - fi - - certificate="$1" - - case $certificate in - gmail) - cc=Equifax_Secure_Certificate_Authority - if ! [ -r $WORKDIR/certs/${cc}.pem ]; then - - curl -o $WORKDIR/certs/${cc}.pem \ - "https://www.geotrust.com/resources/root_certificates/certificates/${cc}.cer" - openssl x509 -in \ - $WORKDIR/certs/${cc}.pem -fingerprint \ - -subject -issuer -serial -hash -noout - fi - ;; - dyne|autistici|freaknet) - cc=Autistici_Certificate_Authority - if ! [ -r $WORKDIR/certs/${cc}.pem ]; then - curl -o $WORKDIR/certs/${cc}.pem \ - "http://ca.autistici.org/ca.pem" - openssl x509 -in \ - $WORKDIR/certs/${cc}.pem \ - -fingerprint -subject -issuer -serial -hash -noout - fi - ;; - riseup) - cc=RiseupCA - if ! [ -r $WORKDIR/certs/${cc}.pem ]; then - curl -o $WORKDIR/certs/${cc}.pem "https://help.riseup.net/assets/43052/RiseupCA.pem" - openssl x509 -in \ - $WORKDIR/certs/${cc}.pem \ - -fingerprint -subject -issuer -serial -hash -noout - fi - ;; - *) - cc="`basename $certificate`" - curl -o "$WORKDIR/certs/${cc}" "$certificate" - if [ $? != 0 ]; then - error "Error downloading certificate: $certificate" - return 1 - fi - openssl x509 -in \ - "$WORKDIR/certs/${cc}" \ - -fingerprint -subject -issuer -serial -hash -noout - ;; - esac - act "refreshing certificates" - c_rehash $WORKDIR/certs > /dev/null - if [ $? != 0 ]; then - error "Error refreshing certificates in $WORKDIR/certs" - c_rehash $WORKDIR/certs - fi - return 0 -} - -########### -# FETCHMAIL -fetchall() { - notice "Fetching all accounts in $MAILDIRS" - res=0 - for i in `find $WORKDIR/Accounts -type f | grep -v README`; do - account=`basename $i` - type=`echo $account | cut -d. -f1` - func "fetchall: $account type $type" - fetch - if [ $? != 0 ]; then res=1; fi - # returns an error if just one of the accounts did - done - return $res -} -fetch() { - adir=$WORKDIR/Accounts - - typeset -al all - - # recursion here - { test $account = default } && { fetchall; return $? } - - read_account "$account" - if [ $? != 0 ]; then - error "Account configuration not found, or broken. Aborting operation." - return 1 - fi - - if [ "$type" = "smtp" ]; then # we skip smtp in fetch - return 0; fi - - if ! [ -r $PROCMAILDIR/rc ]; then - act "updating procmail configuration" - update - fi - - notice "Fetching mails for $name" - ask_password $login $host - { test $? != 0 } && { - error "Error retrieving password for $login on $host" - unset password all; return 1 - } - - tmp=$TMPDIR/$host.fetch.$RANDOM - newlock $tmp - cat <<EOF > $tmp -poll $host with proto IMAP user "$login" there with password "$password" -EOF - unset password - - if ! [ -z $accountopt ]; then # add option configuration - echo "${accountopt}" >> $tmp; fi - - if ! [ -z $folders ]; then # add folder configuration - echo "folder ${folders}" >> $tmp; fi - - cat <<EOF >> $tmp -ssl warnings 3600 and wants mda "procmail -m $PROCMAILDIR/rc" -EOF - if [ "$cert" = "check" ]; then - cat <<EOF >> $tmp -sslcertck sslcertpath '$WORKDIR/certs' -EOF - fi - cat <<EOF >> $tmp -antispam 571 550 501 554 -EOF - - # try login without doing anything - fetchmail -c -f $tmp - # examine result - case $? in - 1) - notice "No mails for $name" - unlink $tmp - return 1 - ;; - 2) - error "Invalid or unknown certificate for $host" - unlink $tmp - return 1 - ;; - 3) - error "Invalid password for user $login at $host" - unlink $tmp - return 1 - ;; - *) ;; - esac - - # archive old procmail log - if [ -r $WORKDIR/log/procmail.log ]; then - newlock $WORKDIR/log/procmail-${datestamp}.log - cat $WORKDIR/log/procmail.log \ - >> $WORKDIR/log/procmail-${datestamp}.log - rm -f $WORKDIR/log/procmail.log - unlock $WORKDIR/log/procmail-${datestamp}.log - fi - act "please wait while downloading mails..." - - cat $tmp | fetchmail -f $tmp - # TODO: substitute this with cat conf | fetchmail -f - - # to avoid writing the password in clear on filesystem - - unlink $tmp - - total=`mailstat -k $WORKDIR/log/procmail.log | tail -n1 | awk '{print $2}'` - briefing=`mailstat -kt $WORKDIR/log/procmail.log |awk '!/procmail/ { print " " $2 "\t" $3 }'|sort -nr` - notice "$total emails fetched" - echo "${briefing}" - - return 0 -} - -list_maildirs() { - maildirs=() - for m in `find $MAILDIRS -type d`; do - maildircheck $m - { test $? = 0 } && { maildirs+=(`basename $m`) } - done - return ${#maildirs} -} - -stats() { - # make index of all maildirs - notice "Maildirs status" - typeset -alU ml - typeset -al empty - for i in `ls $MAILDIRS`; do - { maildircheck $MAILDIRS/${i} } || { continue } - cur=`ls $MAILDIRS/$i/cur | wc -l` - new=`ls $MAILDIRS/$i/cur | wc -l` - tot=$(( $cur + $new )) - ml+=("$tot\t:: $i\t ($cur/$new)") - done - for m in ${ml}; do - { test ${m[1]} = "0" } && { - empty+=${m} - continue } - act "$m" - done - { test ${#empty} != 0 } && { emptystr="(${#empty} are empty)" } - notice "Total maildirs: ${#ml} $emptystr" - return 0 -} - -timecloud() { - id=$RANDOM - list_maildirs - notice "Computing timecloud statistics on ${#maildirs} maildirs" - func "Creating temporary database" - cat <<EOF | ${SQL} -batch $TMPDIR/timecloud.db.$id -CREATE TABLE stats -( - date text collate nocase, - tag text collate nocase, - hits int -); -EOF - { test $? != 0 } && { - error "Error creating temporary timecloud database." - return 1 } - - for m in ${maildirs}; do - for f in `find $MAILDIRS/${m} -type f`; do - timestamp=`fetchdate "%Y-%m-%d" ${f}` - cat <<EOF | ${SQL} -batch $TMPDIR/timecloud.db.$id > $TMPDIR/timecloud.select.$id -SELECT * FROM stats -WHERE tag IS "${m}" AND date IS "${timestamp}"; -EOF - res=`cat $TMPDIR/timecloud.select.$id` - if [ "$res" = "" ]; then - # new tag - cat <<EOF | ${SQL} -batch $TMPDIR/timecloud.db.$id -INSERT INTO stats (date, tag, hits) -VALUES ("${timestamp}", "${m}", 1); -EOF - else - cat <<EOF | ${SQL} -batch $TMPDIR/timecloud.db.$id -UPDATE stats SET hits = hits + 1 -WHERE tag IS "${m}" AND date IS "${timestamp}"; -EOF - fi - done - done - # gather results from the database - cat <<EOF | ${SQL} -batch $TMPDIR/timecloud.db.$id > $TMPDIR/timecloud.results.$id -.separator \t -SELECT * FROM stats ORDER BY date; -EOF - # format the results into a JSON string - awk 'BEGIN { date=0; printf "[" } -{ if(date != $1) { - if( date != 0 ) printf "]]," - date=$1 - printf "[\"" date "\",[" - printf "[\"" $2 "\",\"" $3 "\"]" - } else { - printf ",[\"" $2 "\",\"" $3 "\"]" - } -} -END { print "]]];" } -' $TMPDIR/timecloud.results.$id > $TMPDIR/timecloud.json.$id - ${=mkdir} $WORKDIR/timecloud - cat <<EOF > $WORKDIR/timecloud/jaromail.js -var jaromail=`cat $TMPDIR/timecloud.json.$id` -EOF - rm $TMPDIR/timecloud.*.$id -} - -###### -# SEND -# this function should send all mails in outbox -send() { - adir=$WORKDIR/Accounts - typeset -al all - - # list mails to send - mailnum=`ls ${MAILDIRS}/outbox | grep 'mail$' | wc -l` - mailnum=${mailnum// /} # trim whitespace - if [ "$mailnum" = "0" ]; then - act "Outbox is empty, no mails to send." - return 0 - fi - - read_account smtp - if [ $? != 0 ]; then - error "Account configuration not found, or broken. Aborting operation." - return 1 - fi - - notice "Sending out ${mailnum} mails via $name" - - # defaults - [ -z $auth ] && auth=plain - [ -z $port ] && port=25 - - - ask_password $login $host - { test $? != 0 } && { - error "Error retrieving password for $login on $host" - unset password all; return 1 - } - - tmp=$TMPDIR/$host.send.$RANDOM - newlock $tmp - cat <<EOF > $tmp -account default -from ${email} -user ${login} -host ${host} -port ${port} -tls on -tls_starttls on -tls_certcheck off -logfile ${WORKDIR}/log/msmtp.log -auth ${auth} -password ${password} -EOF - unset password - - for mail in `find $MAILDIRS/outbox -name "*.mail"`; do - smtp=`echo ${mail} | sed -e 's/mail/msmtp/'` - lock ${smtp} - recipients="`cat ${smtp}`" - act "To: ${recipients}" - msmtp -C $tmp -- ${=recipients} < "${mail}" - if [ $? != 0 ]; then - error "Error sending mail, skipped" - unlock ${smtp} - else - act "Mail sent succesfully" - # whitelist those to whom we send mails - cat ${mail} | $WORKDIR/bin/jaro -l whitelist -q learn to: - ${=rm} ${mail} & - unlink ${smtp} & - fi - done - unlink $tmp - return 0 -} - -###### -# PEEK -# this function will open the MTA to the imap server without fetching mails locally -peek() { - read_account imap - if [ $? != 0 ]; then - error "Account configuration not found, or broken. Aborting operation." - return 1 - fi - - notice "Peek into remote imap account $name" - - folder="" - if ! [ -z ${1} ]; then - folder="/${1}" - act "opening folder ${folder}" - fi - - switch_identity - case $transport in - ssl) act "using secure connection (SSL)" - iproto="imaps" ;; - plain) act "using clear text connection" - iproto="imap" ;; - esac - # escape at sign in login - ilogin=`echo $login | sed 's/@/\\@/'` - - ask_password $login $host - { test $? != 0 } && { - error "Error retrieving password for $login on $host" - unset password all; return 1 - } - tmp=$TMPDIR/$host.peek.$RANDOM - newlock $tmp - cat <<EOF >> $tmp -set imap_pass = "${password}" -# set imap_peek = yes -EOF - unset password - echo "source $tmp" >> $MUTTDIR/password - (sleep 1; - cp /dev/null $MUTTDIR/password - cp /dev/null $tmp - unlink $tmp - ) & - mutt -F $MUTTDIR/rc -f ${iproto}://${ilogin}@${host}${folder} - 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 - maildirmake $MAILDIRS/ml.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/cache -set sendmail = "$WORKDIR/bin/jaro queue" -set header_cache= $WORKDIR/cache -set maildir_header_cache_verify=no -set editor = "$WORKDIR/bin/jaro edit" -set mailcap_path = "$WORKDIR/.mutt/mailcap:$WORKDIR/mailcap:$HOME/.mailcap:/etc/mailcap:/etc/mailcap:/usr/etc/mailcap:/usr/local/etc/mailcap" - -# Little Brother Database -set query_command = "$WORKDIR/bin/jaro -q addr '%s'" -macro index,pager a "<pipe-message>$WORKDIR/bin/jaro -l whitelist -q learn from:<enter>" "add to whitelist everyone in the message" -macro index,pager A "<pipe-message>$WORKDIR/bin/jaro -l whitelist -q forget from:<enter>" "remove sender from whitelist -macro index,pager z "<pipe-message>$WORKDIR/bin/jaro -l blacklist -q learn from:<enter>" "add sender to blacklist" -macro index,pager Z "<pipe-message>$WORKDIR/bin/jaro -l blacklist -q forget from:<enter>" "remove sender from blacklist - -# mailboxes in order of priority -source $MUTTDIR/mboxes - -# specific configuration files -source $MUTTDIR/crypto -source $MUTTDIR/general -source $MUTTDIR/formats -source $MUTTDIR/keybindings -source $MUTTDIR/identity -source $MUTTDIR/password -source $MUTTDIR/colors -source $WORKDIR/Mutt.txt -## end of Jaro Mail generated muttrc -#################################### - -EOF - - # making sure we have the minimum mailcap necessary -wwwtext=w3m -{ which lynx > /dev/null } && { wwwtext=lynx } -{ which elinks > /dev/null } && { wwwtext=elinks } -cat <<EOF > $MUTTDIR/mailcap -text/html; ${wwwtext} -dump %s; nametemplate=%s.html; copiousoutput -application/*; a=$WORKDIR/tmp/attach-$RANDOM$RANDOM && cp %s \$a && jaro open \$a -text/plain; iconv -f iso-8859-1 -t utf-8; test=charset=%{charset} \ - && test x`echo \"$charset\" | tr a-z A-Z` = xISO-8859-1; copiousoutput -text/plain; cat %s -EOF - - # this one is empty and sources files in temp when necessary - touch $MUTTDIR/password - - # just the header, will be completed later in procmail loop - rm -f $MUTTDIR/mboxes - echo -n "mailboxes +priv" > $MUTTDIR/mboxes - - # update identity with default - read_account imap - - switch_identity - - ########## - # PROCMAIL - act "generating procmail filters" - ${=mkdir} $PROCMAILDIR - rm -f $PROCMAILDIR/rc - touch $PROCMAILDIR/rc - cat<<EOF >> $PROCMAILDIR/rc -# procmail configuration file generated by Jaro Mail -MAILDIR=$MAILDIRS -JARO=$WORKDIR/bin/jaro -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 = "" -PF_RECURSE = yes - -# blacklist filters -:0 w: -* ? \$JARO -l blacklist -q query from: -zz.blacklist/ - -# filters generated from Filters.txt -:0 -* ? test \$PMSRC/pf-chkto.rc -{ -EOF - - ####### - 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 - func "messages to <${address}> in folder: ${destination}" - maildirmake $MAILDIRS/$destination - ;; - from) - print "ADDR=${address}\tDEST=${destination}/\tINCLUDERC=\$PMSRC/pf-check.rc" \ - >> $PROCMAILDIR/rc - func "messages from <${address}> in folder: {$destination}" - maildirmake $MAILDIRS/$destination - ;; - *) - error "unsupported filter: $header (skipped)" - ;; - esac - # MUTT (generate mailboxes priority this parser) - echo " \\" >> $MUTTDIR/mboxes - echo -n " +${destination} " >> $MUTTDIR/mboxes - done - - # if the sender is known (ldbd recognizes it) then put mail in high priority 'known' - cat <<EOF >> $PROCMAILDIR/rc -} - -:0 -* PF_DEST ?? . -* ? test \$PMSRC/pf-save.rc -{ INCLUDERC=\$PMSRC/pf-save.rc } - - -# whitelisting filters -:0 w: -* ? \$JARO -l whitelist -q query from: -known/ - - -EOF - - ####### -cat <<EOF >> $PROCMAILDIR/rc -# filters generated from Accounts -:0 -* ? test \$PMSRC/pf-chkto.rc -{ -EOF - for f in `cat $WORKDIR/Accounts/* | awk '/^email/ { print $2 }'`; do - echo "ADDR=${f}\tDEST=priv/\tINCLUDERC=\$PMSRC/pf-chkto.rc" >> $PROCMAILDIR/rc - func "private account: <${f}>" - done - - cat <<EOF >> $PROCMAILDIR/rc -} - -:0 -* PF_DEST ?? . -* ? test \$PMSRC/pf-save.rc -{ INCLUDERC=\$PMSRC/pf-save.rc } - -# if its an unknown mailinglist, save it into ml.unsorted -:0 -* ^(List-Id|X-(Mailing-)?List): -ml.unsorted/ - -EOF - - # MUTT (generate mailboxes priority this parser) - echo " \\" >> $MUTTDIR/mboxes - echo " +ml.unsorted +unsorted" >> $MUTTDIR/mboxes - - uniq $MUTTDIR/mboxes > $TMPDIR/mboxes - mv $TMPDIR/mboxes $MUTTDIR/mboxes - rm -f $TMPDIR/mboxes - - # conclude - cat <<EOF >> $PROCMAILDIR/rc - -# if got here, go to unsorted -:0: -\$DEFAULT - -# -# End of generated procmail rc -# -EOF - return 0 -} # end of update() - -################### -# Jaro Brother DB -create_addressbook() { - { test -r $WORKDIR/addressbook } && { - error "Addressbook already exists: $WORKDIR/addressbook" - return 1 - } - cat <<EOF | ${SQL} -batch $WORKDIR/addressbook -CREATE TABLE whitelist -( - email text collate nocase, - name text collate nocase unique, - hits int -); -CREATE TABLE blacklist -( - email text collate nocase, - name text collate nocase unique, - hits int -); -EOF - { test $? != 0 } && { - error "Error creating addressbook database." - return 1 } - return 0 -} -insert_address() { - func "insert address: $1, $2" - cat <<EOF | ${SQL} -batch $WORKDIR/addressbook 2> /dev/null -INSERT INTO $list (email, name) -VALUES ("${1}", "${2}"); -EOF - { test $? != 0 } && { - func "address already present in $list" } - return $0 -} -remove_address() { - func "remove address <$1> from $list" - cat <<EOF | ${SQL} -batch $WORKDIR/addressbook -DELETE FROM $list -WHERE email LIKE "${1}"; -EOF - { test $? != 0 } && { - func "address not found or error occurred" } -} -search_name() { - cat <<EOF | ${SQL} -column -batch $WORKDIR/addressbook -.width 64 128 -SELECT * FROM $list -WHERE name LIKE "%${1}%"; -EOF -} -search_email() { - cat <<EOF | ${SQL} -column -batch $WORKDIR/addressbook -SELECT rowid FROM $list -WHERE email IS "${1}"; -EOF -return $? -} -address() { - { test ! -r ${WORKDIR}/addressbook } && { create_addressbook } - act "Searching for \"${PARAM[1]}\" in $list" - { test "$OS" = "MAC" } && { - matches=`$WORKDIR/bin/ABQuery ${PARAM[1]}` - } - matches="${matches}\n`search_name ${PARAM[1]}`" - - # mutt query requires something like this - echo "jaro: `echo $matches | wc -l` matches" - echo "$matches" | awk ' -{ printf "%s\t", $1 - for(i=2;i<=NF;i++) { - sub("<","",$i) - sub(">","",$i) - if($i!=$1) printf "%s ", $i - } - printf "\n" }' -} -query() { - { test ! -r ${WORKDIR}/addressbook } && { create_addressbook } - if [ -z ${PARAM[1]} ]; then - email="`${WORKDIR}/bin/fetchaddr -a| awk '{print $1}'`" - else - email="`${WORKDIR}/bin/fetchaddr -x ${PARAM[1]} -a| awk '{print $1}'`" - fi - exitcode=1 - lookup="`search_email ${email}`" - { test "$lookup" != "" } && { exitcode=0 } - act "Email <$email> found in $list with id $lookup" -} - -learn() { - { test ! -r ${WORKDIR}/addressbook } && { create_addressbook } - tmp=$TMPDIR/learn.$datestamp.$RANDOM - from="`tee $tmp | formail -xFrom: | sed -e 's/\"//g'`" - if [ -z ${PARAM[1]} ]; then - email="`cat $tmp | ${WORKDIR}/bin/fetchaddr| awk '{print $1}'`" - else - email="`cat $tmp | ${WORKDIR}/bin/fetchaddr -x ${PARAM[1]}| awk '{print $1}'`" - fi - ${=rm} $tmp & - insert_address "$email" "$from" -} - -forget() { - { test ! -r ${WORKDIR}/addressbook } && { create_addressbook } - act "Expecting mail from stdin pipe" - if [ -z ${PARAM[1]} ]; then - email="`${WORKDIR}/bin/fetchaddr| awk '{print $1}'`" - else - email="`${WORKDIR}/bin/fetchaddr -x ${PARAM[1]}| awk '{print $1}'`" - fi - remove_address "${email}" -} -list_addresses() { - { test ${PARAM[1]} } && { list=${PARAM[1]} } - act "Listing all contents for $list" - cat <<EOF | ${SQL} -column -header -batch $WORKDIR/addressbook -.width 32 40 -SELECT * FROM $list; -EOF - - echo ".dump" | ${SQL} -batch $WORKDIR/addressbook \ - | bzip2 > $WORKDIR/addressbook.bz2 - notice "Backup of all addressbook created" - act -n ""; ls -lh $WORKDIR/addressbook.bz2 -} - -################### - - -######### -## Editor -# this part guesses what is the best editor already present on the system -# of course we have a preference for AutOrg, the editor from our suite -# however the default is nano if nothing else is choosen. -editor() { - { test ${EDITOR} } && { - ${=EDITOR} ${PARAM[1]} - return $? } - case $OS in - MAC) open -t ${PARAM[1]} ;; - GNU) - { which nano > /dev/null } && { - nano -m -S -Q ">" -I -E -D -T 4 -U -W -c -i -k -r 72 ${PARAM[1]} } - error "No editor found, please configure the EDITOR environment variable." - sleep 3 - ;; - esac - return $? -} - -####################### -## Search into maildirs -# using mairix -search() { - { which mairix > /dev/null } || { return 1 } - id=$RANDOM - rc=$TMPDIR/search.conf.$id - typeset -al expr - typeset -al fold - # intelligent parse of args, position independent - # check if its a folder, if not is an expression - mlstr="all folders"; ml=""; c=0 - basedir=$MAILDIRS - # check if the name of a maildir is among params - for p in ${PARAM}; do - c=$(( $c + 1 )) - func "checking param: ${p}" - if [ -r ${p} ]; then - - { maildircheck ${p} } && { - fold+=(${p}) - { test ${#fold} = 1 } && { - # base path is the dir of the first folder - pushd `dirname ${p}` - basedir=`pwd` - popd } - } - - elif [ -r ${MAILDIRS}/${p} ]; then - - { maildircheck ${MAILDIRS}/${p} } && { fold+=(${MAILDIRS}/${p}) } - - else # not a folder, add it to expressions array - expr+=(${p}) - fi - done - - - # now fold is an array of specified folders - # expr is an array of specified search expressions - - - # to search only one maildir then we need to index it - # separate from the rest of the maildirs - if [ ${#fold} != 0 ]; then - { test ${#expr} = 0 } && { - error "no search expression given for folders ${fold[@]}" - return 1 } - # forge the folder string for mairix conf - folders=""; for f in ${fold}; do folders="$folders`basename $f`:"; done - cat <<EOF > $rc -base=$basedir -database=$TMPDIR/search.db.$id -maildir=${folders} -mfolder=$TMPDIR/search.result.$id -mformat=maildir -EOF - - exitcode=1 - act "Indexing ${folders}" - mairix -F -f $rc 2> /dev/null - { test $? = 0 } && { - act "Searching for: ${expr}" - found=`mairix -F -f $rc ${=expr} 2> /dev/null | awk '{ print $2}'` - if [ $found != 0 ]; then - mutt -F $MUTTDIR/rc -R -f $TMPDIR/search.result.$id - notice "Found $found matches looking for '$expr' in $folders" - find $TMPDIR/search.result.$id - cat $rc - exitcode=0 - else error "No matches found."; fi - } - - - rm -f $rc - rm -f $TMPDIR/search.db.$id -# rm -rf $TMPDIR/search.result.$id - return $exitcode - - - #################################################### - else # no folder specified on commandline, search all - # make index if no params given - c=0 - for i in `ls $MAILDIRS`; do - # is it a maildir? - { maildircheck $MAILDIRS/${i} } && { - c=`expr $c + 1`; ml="$ml:$i" } - done - fi - - cat <<EOF > $rc -base=$MAILDIRS -database=$WORKDIR/search.db -maildir=${ml} -mfolder=$TMPDIR/search.result.$id -mformat=maildir -EOF - # just index - { test ${#PARAM} = 0 } && { - act "Indexing $c maildirs for search" - act "please be patient..." - mairix -F -f $rc - rm -f $rc - exitcode=$? - if [ $exitcode = 0 ]; then notice "Done." - else error "Error, indexing aborted."; fi - rm -f $rc - return $exitcode - } - act "Searching $mlstr for: ${expr}" - exitcode=1 - found=`mairix -F -f $rc ${=expr} 2> /dev/null | awk '{print $2}'` - if [ $CALLMUTT = 1 ]; then - - if [ $found != 0 ]; then - mutt -F $MUTTDIR/rc -R -f $TMPDIR/search.result.$id - notice "Found $found matches looking for '$expr' in all mail folders" - exitcode=0 - else error "Nothing found matching '$expr'"; fi - - rm -rf $TMPDIR/search.*.$id - rm -f $rc - return $exitcode - - else ### if not calling mutt, internal use mode: - # print out the full path to the results maildir - # return number of found hits - echo $TMPDIR/search.result.$id - return $found - fi -} -############## -## Open a File -open_file() { - a=$WORKDIR/cache/attach-$RANDOM$RANDOM - cp ${PARAM[1]} $a - case $OS in - GNU) - # TODO - ;; - MAC) - open -g $a - ;; - esac -} - -###################### -# Maildir manipulation -backup() { - id=$RANDOM - rc=$TMPDIR/backup.conf.$id - typeset -al expr - typeset -al fold - - src=""; dst="" - basedir=$MAILDIRS - # check if the name of a maildir is among params - # we need at least 2 maildirs, the second is the destination - for p in ${PARAM}; do - c=$(( $c + 1 )) - - if [ $c = ${#PARAM} ]; then - # last one is always the destination - func "destination is ${p}" - fold+=(${p}) - - elif [ -r ${p} ]; then - - { maildircheck ${p} } && { - func "param ${p} is a maildir" - fold+=(${p}) - { test ${#fold} = 1 } && { - # base path is the dir of the first folder - pushd `dirname ${p}` - basedir=`pwd` - popd } - } - - elif [ -r ${MAILDIRS}/${p} ]; then - - { maildircheck ${MAILDIRS}/${p} } && { - func "param ${p} is a jaro maildir" - fold+=(${MAILDIRS}/${p}) - } - - else # not a folder, add it to expressions array - func "param ${p} is an expression" - expr+=(${p}) - fi - done - - { test ${#fold} -lt 2 } && { - error "Not enough folders specified for backup: minimum is 2" - act "When specifying more than 2, the last one is the destination" - return 1 - } - - dst=${fold[${#fold}]} - { test -r $dst } && { - error "Backup destination already exists: $dst" - return 1 } - - maildirmake "${dst}" - - { test ${#expr} = 0 } && { - error "No expression set for backup, please indicate what you want to backup" - act "For example: d:10y-2y (all mails older than 1 year up to 10 years ago" - act "Or a simple search string, all expressions can be verified using search." - return 1 - } - - # forge the folder string for mairix conf - folders="" - for f in ${fold}; do - { test $f = $dst } || { - folders="$folders`basename $f`:" } - done - - notice "Backup of all mails in '$folders' matching expression '$expr'" - - act "Indexing folders" - cat <<EOF > $rc -base=$basedir -database=$TMPDIR/backup.db.$id -maildir=${folders} -mfolder=$dst -mformat=maildir -EOF - mairix -F -f $rc 2> /dev/null - act "Moving matches to $dst" - pushd `dirname $dst`; basedir=`pwd`; popd - rm -f $rc; cat <<EOF > $rc -base=$basedir -database=$TMPDIR/backup.db.$id -maildir=${folders} -mfolder=$dst -mformat=maildir -EOF - found=`mairix -F -f $rc -H ${expr} 2> /dev/null | awk '{print $2}'` - notice "$found matches found, destination folder size is `du -hs $basedir/$dst | awk '{print $1}'`" - # invert the order of folders to start with destination in rmdupes - typeset -al PARAM - c=$(( ${#fold} )) - while [ $c -gt 0 ]; do - func "${fold[$c]}" - PARAM+=(${fold[$c]}) - c=$(( $c - 1 )) - done - QUIET=1 - rmdupes -} - -rmdupes() { - tmp=$TMPDIR/$datestamp.rmdupes.$RANDOM - newlock $tmp - for folder in ${=PARAM}; do - { test ! -r $folder } && { folder=$MAILDIRS/$folder } - { test ! -r $folder } && { error "Maildir not found: $folder"; continue } - notice "Removing duplicates in $folder" - c=0 - for i in `find ${folder} -type f`; do - # 5MB should be enough ehre? - formail -D 5000000 $tmp <$i \ - && rm $i && c=`expr $c + 1` - done - done - unlink $tmp - notice "$c duplicates found and deleted" -} - -merge() { - src=${PARAM[1]} - dst=${PARAM[2]} - if ! [ -r ${src}/cur ]; then - error "No source maildir found in $src" - return 1 - fi - if ! [ -r ${dst}/cur ]; then - error "No destination maildir found in $dst" - return 1 - fi - notice "Merging maildir ${src} into ${dst}" - c=0 - for i in `find ${src}/cur -type f`; do mv $i ${dst}/cur/; c=`expr $c + 1`; done - for i in `find ${src}/new -type f`; do mv $i ${dst}/new/; c=`expr $c + 1`; done - for i in `find ${src}/tmp -type f`; do mv $i ${src}/tmp/; c=`expr $c + 1`; done - act "$c mails succesfully moved." - notice "Operation completed, you can now safely remove ${src}" -} - -# re-sort all maildir through the procmail filters -# it can create duplicates, up to the user to rmdupes -filter() { - - update # update all filters - - # archive old procmail log - if [ -r $WORKDIR/log/procmail.log ]; then - newlock $WORKDIR/log/procmail-${datestamp}.log - cat $WORKDIR/log/procmail.log \ - >> $WORKDIR/log/procmail-${datestamp}.log - rm -f $WORKDIR/log/procmail.log - unlock $WORKDIR/log/procmail-${datestamp}.log - fi - - for folder in ${=PARAM}; do - typeset -al fall - { test ! -r $folder } && { folder=$MAILDIRS/$folder } - { test ! -r $folder } && { error "Maildir not found: $folder"; return 1 } - notice "Filtering folder $folder" - # first index current state - for m in `find $folder -type f`; do fall+=($m); done - # then process it, this way ignoring new mails send to same folder - for n in ${=fall}; do - cat $n | procmail -m $PROCMAILDIR/rc - done - unset fall - done - - total=`mailstat -k $WORKDIR/log/procmail.log | tail -n1 | awk '{print $2}'` - briefing=`mailstat -kt $WORKDIR/log/procmail.log |awk '!/procmail/ { print " " $2 "\t" $3 }'|sort -nr` - echo "${briefing}" -} - -# opens and closes a ramdisk for temporary files -# users can do this explicitly between session to speed up operations -ramdisk() { - case $OS in - GNU) - # TODO - # not so urgent, since usually /dev/shm is mounted and writable - ;; - MAC) - case ${PARAM[1]} in - open) - mount | grep 'JaroTmp' > /dev/null - { test $? = 0 } && { - error "A Jaro Mail ramdisk is already open" - return 1 } - { test -z ${PARAM[2]} } && { size=10 } || { size=${PARAM[2]} } - act "Creating ramdisk of ${size}MB" - - # 2048 is a megabyte here - devsize=$((1024*2*$size)) - devname=`hdid -nomount ram://${devsize}` - act "Mounting ramdisk on $devname" - diskutil eraseVolume HFS+ JaroTmp `basename $devname` > /dev/null - { test $? != 0 } && { - error "Error initializing ramdisk" - hdiutil detach `basename $devname` - return 1 } - notice "Operation successful, ramdisk ready on /Volume/JaroTmp" - TMPRAM=1 - ;; - close) - devname=`mount | awk '/JaroTmp/ {print $1}'` - { test "$devname" = "" } && { - error "No ramdisk seems to be open" - return 1 } - act "Unmounting ramdisk: $devname" - diskutil unmount /Volumes/JaroTmp > /dev/null - hdiutil detach `basename $devname` > /dev/null - notice "Ramdisk succesfully detached" - TMPRAM=0 - ;; - esac - ;; - esac -} -###################### - -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 Jaro Mail sourcecode is on <http://jaromail.dyne.org> - -Synopsis: jaro [options] [command] [command-options] - -Main commands: - - fetch download unread emails from [account] - send send all mails queued in the outbox - peek look into the [account] mailbox without downloading - -Options: - - -a use a particular account instead of default (keyword) - -l whitelist or blacklist to use with learn/query/forget - -h print this help - -v version information for this tool - -q run quietly without printing informations - -n dry run, show operations without executing them - -D print debugging information at runtime - -Maintenance commands: - - passwd reset password for the account in use - - update refresh configurations - queue add a mail into outbox - - addr look for a matching address in list - query read mail from stdin, return 0 if known to list - learn learn addresses from mails piped in stdin - forget remove addresses found in mails piped in stdin - - backup move all mails older than N days from a maildir to another - rmdupes remove all duplicate mails into a maildir - merge merge a maildir into another, removing all duplicates - -Please report bugs on <http://bugs.dyne.org>. -EOF -} # TODO: For more informations on Jaro Mail read the manual: man jaro @@ -1879,8 +234,7 @@ main() subcommands_opts[peek]="" subcommands_opts[update]="" - subcommands_opts[stats]="" - subcommands_opts[timecloud]="" + subcommands_opts[stat]="" subcommands_opts[search]="" subcommands_opts[addr]="" @@ -1994,8 +348,7 @@ main() search) CLEANEXIT=0; search ${PARAM} ;; - stats) CLEANEXIT=0; stats ;; - timecloud) CLEANEXIT=0; timecloud ;; + stat) CLEANEXIT=0; stats ${PARAM} ;; addr) CLEANEXIT=0; address ${PARAM} ;; query) CLEANEXIT=0; query ${PARAM} ;; diff --git a/src/zlibs/accounts b/src/zlibs/accounts @@ -0,0 +1,356 @@ +#!/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. + +switch_identity() { + if [ "$name" != "" ]; then + 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 + else + error "No identity found, left blank." + touch $MUTTDIR/identity + fi +} + + + +# arg 1 : account type, es: imap or smtp +# -a defines which account name other than 'default' +read_account() { + typeset -al all + type=$1 + # find the file + func "read_account looking for \"$account\"" + for a in `find $WORKDIR/Accounts -name "$type.$account*"`; do + func "found account: $a" + all+=($a) + done + if [ ${#all} = 0 ]; then + error "No $type account found: $account" + act "Refine your argument using '-a type.account'" + act "For instance adding to the commandline: -a imap.default" + act "Available accounts (excluding symbolic links, omit final .txt):" + for a in `find $WORKDIR/Accounts -type f | grep -v README | sed 's/.txt//'`; do + act -n "`basename $a`\t :: " + awk ' +/^name/ { for(i=2;i<=NF;i++) printf "%s ", $i } +/^email/ { printf "<%s>", $2 } +/^host/ { printf " on %s", $2 } +{next}' $a.txt + echo " (`basename $a | cut -d. -f1`)" + done + return 1 + elif [ ${#all} != 1 ]; then + error "Too many $type accounts named $account" + act -n "" + for i in ${=all}; do echo -n "`basename ${i}` "; done + echo; error "Refine your account keyword using -a option" + return 1 + fi + + acct=${all[1]} + type=`basename $acct | cut -d. -f1` + + case $type in + imap|smtp) + + lock $acct + ttmp=`cat $acct | awk ' + /^#/ { next } + /^name/ { printf "name=\""; for(i=2;i<=NF;i++) printf "%s ", $i; printf "\";" } + /^email/ { printf "email=\"%s\";", $2 } + /^host/ { printf "host=\"%s\";", $2 } + /^login/ { printf "login=\"%s\";", $2 } + /^transport/ { printf "transport=\"%s\";", $2 } + /^port/ { printf "port=\"%s\";", $2 } + /^password/ { printf "password=\"%s\";", $2 } + /^auth/ { printf "auth=\"%s\";", $2 } + /^cert/ { printf "cert=\"%s\";", $2 } + /^options/ { printf "accountopt=\""; for(i=2;i<=NF;i++) printf "%s ", $i; printf "\";" } + /^folders/ { printf "folders=\""; for(i=2;i<=NF;i++) printf "%s ", $i; printf "\";" } + '` + unlock $acct + + eval "$ttmp" + # check required fields + { test -z $host } && { error "Field missing in account $acct: host"; return 1 } + + # fill in defaults + { test -z $name } && { name="$type" } + { test -z $login } && { login="$email" } # usually email and login are the same + { test -z $email } && { email="$login" } # so if one is specified, deduce the other + { test -z $transport } && { transport=plain } + { test -z $port } && { port=143 } + { test -z $auth } && { auth=plain } + { test -z $cert } && { cert=ignore } + { test -z $accountopt } && { accountopt=keep } + # cert and password can be missing + + func "type: $type" + func "name: $name" + func "email: $email" + func "host: $host" + func "login: $login" + func "trans: $transport" + func "port: $port" + func "cert: $cert" + func "auth: $auth" + func "options: $accountopt" + func "folders: $folders" + ;; + *) + error "Account type \"$type\" not recognized." + return 1 + ;; + esac + return 0 +} + + +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 + maildirmake $MAILDIRS/ml.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/cache +set sendmail = "$WORKDIR/bin/jaro queue" +set header_cache= $WORKDIR/cache +set maildir_header_cache_verify=no +set editor = "$WORKDIR/bin/jaro edit" +set mailcap_path = "$WORKDIR/.mutt/mailcap:$WORKDIR/mailcap:$HOME/.mailcap:/etc/mailcap:/etc/mailcap:/usr/etc/mailcap:/usr/local/etc/mailcap" + +# Little Brother Database +set query_command = "$WORKDIR/bin/jaro -q addr '%s'" +macro index,pager a "<pipe-message>$WORKDIR/bin/jaro -l whitelist -q learn from:<enter>" "add to whitelist everyone in the message" +macro index,pager A "<pipe-message>$WORKDIR/bin/jaro -l whitelist -q forget from:<enter>" "remove sender from whitelist +macro index,pager z "<pipe-message>$WORKDIR/bin/jaro -l blacklist -q learn from:<enter>" "add sender to blacklist" +macro index,pager Z "<pipe-message>$WORKDIR/bin/jaro -l blacklist -q forget from:<enter>" "remove sender from blacklist + +# mailboxes in order of priority +source $MUTTDIR/mboxes + +# specific configuration files +source $MUTTDIR/crypto +source $MUTTDIR/general +source $MUTTDIR/formats +source $MUTTDIR/keybindings +source $MUTTDIR/identity +source $MUTTDIR/password +source $MUTTDIR/colors +source $WORKDIR/Mutt.txt +## end of Jaro Mail generated muttrc +#################################### + +EOF + + # making sure we have the minimum mailcap necessary +wwwtext=w3m +{ which lynx > /dev/null } && { wwwtext=lynx } +{ which elinks > /dev/null } && { wwwtext=elinks } +cat <<EOF > $MUTTDIR/mailcap +text/html; ${wwwtext} -dump %s; nametemplate=%s.html; copiousoutput +application/*; a=$WORKDIR/tmp/attach-$RANDOM$RANDOM && cp %s \$a && jaro open \$a +text/plain; iconv -f iso-8859-1 -t utf-8; test=charset=%{charset} \ + && test x`echo \"$charset\" | tr a-z A-Z` = xISO-8859-1; copiousoutput +text/plain; cat %s +EOF + + # this one is empty and sources files in temp when necessary + touch $MUTTDIR/password + + # just the header, will be completed later in procmail loop + rm -f $MUTTDIR/mboxes + echo -n "mailboxes +priv" > $MUTTDIR/mboxes + + # update identity with default + read_account imap + + switch_identity + + ########## + # PROCMAIL + act "generating procmail filters" + ${=mkdir} $PROCMAILDIR + rm -f $PROCMAILDIR/rc + touch $PROCMAILDIR/rc + cat<<EOF >> $PROCMAILDIR/rc +# procmail configuration file generated by Jaro Mail +MAILDIR=$MAILDIRS +JARO=$WORKDIR/bin/jaro +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 = "" +PF_RECURSE = yes + +# blacklist filters +:0 w: +* ? \$JARO -l blacklist -q query from: +zz.blacklist/ + +# filters generated from Filters.txt +:0 +* ? test \$PMSRC/pf-chkto.rc +{ +EOF + + ####### + 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 + func "messages to <${address}> in folder: ${destination}" + maildirmake $MAILDIRS/$destination + ;; + from) + print "ADDR=${address}\tDEST=${destination}/\tINCLUDERC=\$PMSRC/pf-check.rc" \ + >> $PROCMAILDIR/rc + func "messages from <${address}> in folder: {$destination}" + maildirmake $MAILDIRS/$destination + ;; + *) + error "unsupported filter: $header (skipped)" + ;; + esac + # MUTT (generate mailboxes priority this parser) + echo " \\" >> $MUTTDIR/mboxes + echo -n " +${destination} " >> $MUTTDIR/mboxes + done + + # if the sender is known (ldbd recognizes it) then put mail in high priority 'known' + cat <<EOF >> $PROCMAILDIR/rc +} + +:0 +* PF_DEST ?? . +* ? test \$PMSRC/pf-save.rc +{ INCLUDERC=\$PMSRC/pf-save.rc } + + +# whitelisting filters +:0 w: +* ? \$JARO -l whitelist -q query from: +known/ + + +EOF + + ####### +cat <<EOF >> $PROCMAILDIR/rc +# filters generated from Accounts +:0 +* ? test \$PMSRC/pf-chkto.rc +{ +EOF + for f in `cat $WORKDIR/Accounts/* | awk '/^email/ { print $2 }'`; do + echo "ADDR=${f}\tDEST=priv/\tINCLUDERC=\$PMSRC/pf-chkto.rc" >> $PROCMAILDIR/rc + func "private account: <${f}>" + done + + cat <<EOF >> $PROCMAILDIR/rc +} + +:0 +* PF_DEST ?? . +* ? test \$PMSRC/pf-save.rc +{ INCLUDERC=\$PMSRC/pf-save.rc } + +# if its an unknown mailinglist, save it into ml.unsorted +:0 +* ^(List-Id|X-(Mailing-)?List): +ml.unsorted/ + +EOF + + # MUTT (generate mailboxes priority this parser) + echo " \\" >> $MUTTDIR/mboxes + echo " +ml.unsorted +unsorted" >> $MUTTDIR/mboxes + + uniq $MUTTDIR/mboxes > $TMPDIR/mboxes + mv $TMPDIR/mboxes $MUTTDIR/mboxes + rm -f $TMPDIR/mboxes + + # conclude + cat <<EOF >> $PROCMAILDIR/rc + +# if got here, go to unsorted +:0: +\$DEFAULT + +# +# End of generated procmail rc +# +EOF + return 0 +} # end of update() + diff --git a/src/zlibs/addressbook b/src/zlibs/addressbook @@ -0,0 +1,153 @@ +#!/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. + + + +################### +# Jaro Brother DB +create_addressbook() { + { test -r $WORKDIR/addressbook } && { + error "Addressbook already exists: $WORKDIR/addressbook" + return 1 + } + cat <<EOF | ${SQL} -batch $WORKDIR/addressbook +CREATE TABLE whitelist +( + email text collate nocase, + name text collate nocase unique, + hits int +); +CREATE TABLE blacklist +( + email text collate nocase, + name text collate nocase unique, + hits int +); +EOF + { test $? != 0 } && { + error "Error creating addressbook database." + return 1 } + return 0 +} +insert_address() { + func "insert address: $1, $2" + cat <<EOF | ${SQL} -batch $WORKDIR/addressbook 2> /dev/null +INSERT INTO $list (email, name) +VALUES ("${1}", "${2}"); +EOF + { test $? != 0 } && { + func "address already present in $list" } + return $0 +} +remove_address() { + func "remove address <$1> from $list" + cat <<EOF | ${SQL} -batch $WORKDIR/addressbook +DELETE FROM $list +WHERE email LIKE "${1}"; +EOF + { test $? != 0 } && { + func "address not found or error occurred" } +} +search_name() { + cat <<EOF | ${SQL} -column -batch $WORKDIR/addressbook +.width 64 128 +SELECT * FROM $list +WHERE name LIKE "%${1}%"; +EOF +} +search_email() { + cat <<EOF | ${SQL} -column -batch $WORKDIR/addressbook +SELECT rowid FROM $list +WHERE email IS "${1}"; +EOF +return $? +} +address() { + { test ! -r ${WORKDIR}/addressbook } && { create_addressbook } + act "Searching for \"${PARAM[1]}\" in $list" + { test "$OS" = "MAC" } && { + matches=`$WORKDIR/bin/ABQuery ${PARAM[1]}` + } + matches="${matches}\n`search_name ${PARAM[1]}`" + + # mutt query requires something like this + echo "jaro: `echo $matches | wc -l` matches" + echo "$matches" | awk ' +{ printf "%s\t", $1 + for(i=2;i<=NF;i++) { + sub("<","",$i) + sub(">","",$i) + if($i!=$1) printf "%s ", $i + } + printf "\n" }' +} +query() { + { test ! -r ${WORKDIR}/addressbook } && { create_addressbook } + if [ -z ${PARAM[1]} ]; then + email="`${WORKDIR}/bin/fetchaddr -a| awk '{print $1}'`" + else + email="`${WORKDIR}/bin/fetchaddr -x ${PARAM[1]} -a| awk '{print $1}'`" + fi + exitcode=1 + lookup="`search_email ${email}`" + { test "$lookup" != "" } && { exitcode=0 } + act "Email <$email> found in $list with id $lookup" +} + +learn() { + { test ! -r ${WORKDIR}/addressbook } && { create_addressbook } + tmp=$TMPDIR/learn.$datestamp.$RANDOM + from="`tee $tmp | formail -xFrom: | sed -e 's/\"//g'`" + if [ -z ${PARAM[1]} ]; then + email="`cat $tmp | ${WORKDIR}/bin/fetchaddr| awk '{print $1}'`" + else + email="`cat $tmp | ${WORKDIR}/bin/fetchaddr -x ${PARAM[1]}| awk '{print $1}'`" + fi + ${=rm} $tmp & + insert_address "$email" "$from" +} + +forget() { + { test ! -r ${WORKDIR}/addressbook } && { create_addressbook } + act "Expecting mail from stdin pipe" + if [ -z ${PARAM[1]} ]; then + email="`${WORKDIR}/bin/fetchaddr| awk '{print $1}'`" + else + email="`${WORKDIR}/bin/fetchaddr -x ${PARAM[1]}| awk '{print $1}'`" + fi + remove_address "${email}" +} +list_addresses() { + { test ${PARAM[1]} } && { list=${PARAM[1]} } + act "Listing all contents for $list" + cat <<EOF | ${SQL} -column -header -batch $WORKDIR/addressbook +.width 32 40 +SELECT * FROM $list; +EOF + + echo ".dump" | ${SQL} -batch $WORKDIR/addressbook \ + | bzip2 > $WORKDIR/addressbook.bz2 + notice "Backup of all addressbook created" + act -n ""; ls -lh $WORKDIR/addressbook.bz2 +} + +################### diff --git a/src/zlibs/cmdline b/src/zlibs/cmdline @@ -0,0 +1,111 @@ +#!/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. + +check_bin() { + # check for required programs + for req in pinentry fetchmail procmail mutt; do + which $req >/dev/null + { test $? != 0 } && { + error "Cannot find $req. Please install it." + return 1 + } + done + + # which wipe command to use + which wipe > /dev/null + { test $? = 0 } && { + rm="wipe -f -s -q -R /dev/urandom"; return 0 } + which srm > /dev/null + { test $? = 0 } && { + rm="srm -m"; return 0 } + rm="rm -f" + return 0 +} + + +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]} +} + + +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 Jaro Mail sourcecode is on <http://jaromail.dyne.org> + +Synopsis: jaro [options] [command] [command-options] + +Main commands: + + fetch download unread emails from [account] + send send all mails queued in the outbox + peek look into the [account] mailbox without downloading + +Options: + + -a use a particular account instead of default (keyword) + -l whitelist or blacklist to use with learn/query/forget + -h print this help + -v version information for this tool + -q run quietly without printing informations + -n dry run, show operations without executing them + -D print debugging information at runtime + +Maintenance commands: + + passwd reset password for the account in use + + update refresh configurations + queue add a mail into outbox + + addr look for a matching address in list + query read mail from stdin, return 0 if known to list + learn learn addresses from mails piped in stdin + forget remove addresses found in mails piped in stdin + + backup move all mails older than N days from a maildir to another + rmdupes remove all duplicate mails into a maildir + merge merge a maildir into another, removing all duplicates + +Please report bugs on <http://bugs.dyne.org>. +EOF +} diff --git a/src/zlibs/email b/src/zlibs/email @@ -0,0 +1,298 @@ +#!/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. + +queue() { + local base; + local tmp; + local mailfile; + local msmtpfile; + + # add mails to the sendout queue + ${=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" + lock $msmtpfile + # Write the mail to $MAILFILE + cat > "$mailfile" + unlock $msmtpfile + cd - + return 0 +} + +########### +# FETCHMAIL +fetchall() { + notice "Fetching all accounts in $MAILDIRS" + res=0 + for i in `find $WORKDIR/Accounts -type f | grep -v README`; do + account=`basename $i` + type=`echo $account | cut -d. -f1` + func "fetchall: $account type $type" + fetch + if [ $? != 0 ]; then res=1; fi + # returns an error if just one of the accounts did + done + return $res +} +fetch() { + adir=$WORKDIR/Accounts + + typeset -al all + + # recursion here + { test $account = default } && { fetchall; return $? } + + read_account "$account" + if [ $? != 0 ]; then + error "Account configuration not found, or broken. Aborting operation." + return 1 + fi + + if [ "$type" = "smtp" ]; then # we skip smtp in fetch + return 0; fi + + if ! [ -r $PROCMAILDIR/rc ]; then + act "updating procmail configuration" + update + fi + + notice "Fetching mails for $name" + ask_password $login $host + { test $? != 0 } && { + error "Error retrieving password for $login on $host" + unset password all; return 1 + } + + tmp=$TMPDIR/$host.fetch.$RANDOM + newlock $tmp + cat <<EOF > $tmp +poll $host with proto IMAP user "$login" there with password "$password" +EOF + unset password + + if ! [ -z $accountopt ]; then # add option configuration + echo "${accountopt}" >> $tmp; fi + + if ! [ -z $folders ]; then # add folder configuration + echo "folder ${folders}" >> $tmp; fi + + cat <<EOF >> $tmp +ssl warnings 3600 and wants mda "procmail -m $PROCMAILDIR/rc" +EOF + if [ "$cert" = "check" ]; then + cat <<EOF >> $tmp +sslcertck sslcertpath '$WORKDIR/certs' +EOF + fi + cat <<EOF >> $tmp +antispam 571 550 501 554 +EOF + + # try login without doing anything + fetchmail -c -f $tmp + # examine result + case $? in + 1) + notice "No mails for $name" + unlink $tmp + return 1 + ;; + 2) + error "Invalid or unknown certificate for $host" + unlink $tmp + return 1 + ;; + 3) + error "Invalid password for user $login at $host" + unlink $tmp + return 1 + ;; + *) ;; + esac + + # archive old procmail log + if [ -r $WORKDIR/log/procmail.log ]; then + newlock $WORKDIR/log/procmail-${datestamp}.log + cat $WORKDIR/log/procmail.log \ + >> $WORKDIR/log/procmail-${datestamp}.log + rm -f $WORKDIR/log/procmail.log + unlock $WORKDIR/log/procmail-${datestamp}.log + fi + act "please wait while downloading mails..." + + cat $tmp | fetchmail -f $tmp + # TODO: substitute this with cat conf | fetchmail -f - + # to avoid writing the password in clear on filesystem + + unlink $tmp + + total=`mailstat -k $WORKDIR/log/procmail.log | tail -n1 | awk '{print $2}'` + briefing=`mailstat -kt $WORKDIR/log/procmail.log |awk '!/procmail/ { print " " $2 "\t" $3 }'|sort -nr` + notice "$total emails fetched" + echo "${briefing}" + + return 0 +} + + +###### +# SEND +# this function should send all mails in outbox +send() { + adir=$WORKDIR/Accounts + typeset -al all + + # list mails to send + mailnum=`ls ${MAILDIRS}/outbox | grep 'mail$' | wc -l` + mailnum=${mailnum// /} # trim whitespace + if [ "$mailnum" = "0" ]; then + act "Outbox is empty, no mails to send." + return 0 + fi + + read_account smtp + if [ $? != 0 ]; then + error "Account configuration not found, or broken. Aborting operation." + return 1 + fi + + notice "Sending out ${mailnum} mails via $name" + + # defaults + [ -z $auth ] && auth=plain + [ -z $port ] && port=25 + + + ask_password $login $host + { test $? != 0 } && { + error "Error retrieving password for $login on $host" + unset password all; return 1 + } + + tmp=$TMPDIR/$host.send.$RANDOM + newlock $tmp + cat <<EOF > $tmp +account default +from ${email} +user ${login} +host ${host} +port ${port} +tls on +tls_starttls on +tls_certcheck off +logfile ${WORKDIR}/log/msmtp.log +auth ${auth} +password ${password} +EOF + unset password + + for mail in `find $MAILDIRS/outbox -name "*.mail"`; do + smtp=`echo ${mail} | sed -e 's/mail/msmtp/'` + lock ${smtp} + recipients="`cat ${smtp}`" + act "To: ${recipients}" + msmtp -C $tmp -- ${=recipients} < "${mail}" + if [ $? != 0 ]; then + error "Error sending mail, skipped" + unlock ${smtp} + else + act "Mail sent succesfully" + # whitelist those to whom we send mails + cat ${mail} | $WORKDIR/bin/jaro -l whitelist -q learn to: + ${=rm} ${mail} & + unlink ${smtp} & + fi + done + unlink $tmp + return 0 +} + +###### +# PEEK +# this function will open the MTA to the imap server without fetching mails locally +peek() { + read_account imap + if [ $? != 0 ]; then + error "Account configuration not found, or broken. Aborting operation." + return 1 + fi + + notice "Peek into remote imap account $name" + + folder="" + if ! [ -z ${1} ]; then + folder="/${1}" + act "opening folder ${folder}" + fi + + switch_identity + case $transport in + ssl) act "using secure connection (SSL)" + iproto="imaps" ;; + plain) act "using clear text connection" + iproto="imap" ;; + esac + # escape at sign in login + ilogin=`echo $login | sed 's/@/\\@/'` + + ask_password $login $host + { test $? != 0 } && { + error "Error retrieving password for $login on $host" + unset password all; return 1 + } + tmp=$TMPDIR/$host.peek.$RANDOM + newlock $tmp + cat <<EOF >> $tmp +set imap_pass = "${password}" +# set imap_peek = yes +EOF + unset password + echo "source $tmp" >> $MUTTDIR/password + (sleep 1; + cp /dev/null $MUTTDIR/password + cp /dev/null $tmp + unlink $tmp + ) & + mutt -F $MUTTDIR/rc -f ${iproto}://${ilogin}@${host}${folder} + return $? +} + diff --git a/src/zlibs/helpers b/src/zlibs/helpers @@ -0,0 +1,213 @@ +#!/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. + +# start without options: auto +# read or compose depending if there is an argument that is an email +# or a folder to start with + +autostart() { +# no argument passed. open known folder + { test -z ${1} } && { + + { test ! -r $MUTTDIR/rc } \ + && { error "Jaro Mail is not yet configured."; return 1 } + + mutt -F $MUTTDIR/rc + return $? + } + + # argument passed: determine if an email + echo "${1}" \ + | tr 'A-Z' 'a-z' \ + | grep '^[a-zA-Z0-9._%+-]*@[a-zA-Z0-9]*[\.[a-zA-Z0-9]*]*[a-zA-Z0-9]$' \ + > /dev/null + { test $? = 0 } && { + notice "Composing message to: ${1}" + # its an email, TODO see if we have it in our addressbook + mutt -F $MUTTDIR/rc "${1}" + return 0 + } + # or a path to folder + { test -r ${1} } && { + mutt -F $MUTTDIR/rc -f ${1} + return 0 + } + + # or the name of a folder in Jaro Mail + { test -r $MAILDIRS/${1} } && { + notice "Opening folder ${1}" + mutt -F $MUTTDIR/rc -f "$MAILDIRS/${1}" + return 0 + } + + return $? +} + + +######### +## Editor +# this part guesses what is the best editor already present on the system +# of course we have a preference for AutOrg, the editor from our suite +# however the default is nano if nothing else is choosen. +editor() { + { test ${EDITOR} } && { + ${=EDITOR} ${PARAM[1]} + return $? } + case $OS in + MAC) open -t ${PARAM[1]} ;; + GNU) + { which nano > /dev/null } && { + nano -m -S -Q ">" -I -E -D -T 4 -U -W -c -i -k -r 72 ${PARAM[1]} } + error "No editor found, please configure the EDITOR environment variable." + sleep 3 + ;; + esac + return $? +} + +############## +## Open a File +open_file() { + a=$WORKDIR/cache/attach-$RANDOM$RANDOM + cp ${PARAM[1]} $a + case $OS in + GNU) + # TODO + ;; + MAC) + open -g $a + ;; + esac +} + + +# opens and closes a ramdisk for temporary files +# users can do this explicitly between session to speed up operations +ramdisk() { + case $OS in + GNU) + # TODO + # not so urgent, since usually /dev/shm is mounted and writable + ;; + MAC) + case ${PARAM[1]} in + open) + mount | grep 'JaroTmp' > /dev/null + { test $? = 0 } && { + error "A Jaro Mail ramdisk is already open" + return 1 } + { test -z ${PARAM[2]} } && { size=10 } || { size=${PARAM[2]} } + act "Creating ramdisk of ${size}MB" + + # 2048 is a megabyte here + devsize=$((1024*2*$size)) + devname=`hdid -nomount ram://${devsize}` + act "Mounting ramdisk on $devname" + diskutil eraseVolume HFS+ JaroTmp `basename $devname` > /dev/null + { test $? != 0 } && { + error "Error initializing ramdisk" + hdiutil detach `basename $devname` + return 1 } + notice "Operation successful, ramdisk ready on /Volume/JaroTmp" + TMPRAM=1 + ;; + close) + devname=`mount | awk '/JaroTmp/ {print $1}'` + { test "$devname" = "" } && { + error "No ramdisk seems to be open" + return 1 } + act "Unmounting ramdisk: $devname" + diskutil unmount /Volumes/JaroTmp > /dev/null + hdiutil detach `basename $devname` > /dev/null + notice "Ramdisk succesfully detached" + TMPRAM=0 + ;; + esac + ;; + esac +} + + +###### +# CERT +# downloads and/or installs certificates +cert() { + if [ -z $1 ]; then + error "Certificate handler called without an argument" + return 1; + fi + + certificate="$1" + + case $certificate in + gmail) + cc=Equifax_Secure_Certificate_Authority + if ! [ -r $WORKDIR/certs/${cc}.pem ]; then + + curl -o $WORKDIR/certs/${cc}.pem \ + "https://www.geotrust.com/resources/root_certificates/certificates/${cc}.cer" + openssl x509 -in \ + $WORKDIR/certs/${cc}.pem -fingerprint \ + -subject -issuer -serial -hash -noout + fi + ;; + dyne|autistici|freaknet) + cc=Autistici_Certificate_Authority + if ! [ -r $WORKDIR/certs/${cc}.pem ]; then + curl -o $WORKDIR/certs/${cc}.pem \ + "http://ca.autistici.org/ca.pem" + openssl x509 -in \ + $WORKDIR/certs/${cc}.pem \ + -fingerprint -subject -issuer -serial -hash -noout + fi + ;; + riseup) + cc=RiseupCA + if ! [ -r $WORKDIR/certs/${cc}.pem ]; then + curl -o $WORKDIR/certs/${cc}.pem "https://help.riseup.net/assets/43052/RiseupCA.pem" + openssl x509 -in \ + $WORKDIR/certs/${cc}.pem \ + -fingerprint -subject -issuer -serial -hash -noout + fi + ;; + *) + cc="`basename $certificate`" + curl -o "$WORKDIR/certs/${cc}" "$certificate" + if [ $? != 0 ]; then + error "Error downloading certificate: $certificate" + return 1 + fi + openssl x509 -in \ + "$WORKDIR/certs/${cc}" \ + -fingerprint -subject -issuer -serial -hash -noout + ;; + esac + act "refreshing certificates" + c_rehash $WORKDIR/certs > /dev/null + if [ $? != 0 ]; then + error "Error refreshing certificates in $WORKDIR/certs" + c_rehash $WORKDIR/certs + fi + return 0 +} + +###################### diff --git a/src/zlibs/locking b/src/zlibs/locking @@ -0,0 +1,81 @@ +#!/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. + +lock() { + func "lock: $1" + $WORKDIR/bin/dotlock ${1} + case $? in + 1) error "Cannot lock non existing file: $1" + return 1 ;; + 3) error "Locked file in use: $1" + return 3 ;; + 5) error "Impossible to lock: $1" + return 5 ;; + # success + 0) echo "$$" > ${1}.pid + return 0 ;; + esac +} +newlock() { # create locked + func "creating locked file: $1" + touch $1 + chmod 600 $1 + lock $1 +} +unlock() { + func "unlock: $1" + lockpid="`cat ${1}.pid`" + { test -r ${1}.pid } && { + { test "$$" != "$lockpid" } && { + error "Unlock attempt by different PID: $1" + error "Created by $lockpid now $$ is trying to unlock" + return 1 } + } + + $WORKDIR/bin/dotlock -u ${=@} + { test $? != 0 } && { error "Unable to unlock: $1"; return 1 } + { test -r ${1}.pid } && { rm -f ${1}.pid } + return 0 +} +unlink() { # delete a file that we are locking + func "unlink: $1" + # use with care! this can permanently erase currently locked files + # only the locking PID should use it on its own locks + + { test -r ${1}.pid } && { + lockpid="`cat ${1}.pid`" + { test "$$" != "$lockpid" } && { + error "Unlock attempt by different PID: $1" + error "Created by $lockpid now $$ is trying to unlock" + return 1 } + } + + ( + ${=rm} ${1} + touch ${1} + $WORKDIR/bin/dotlock -d -f ${1} + { test $? != 0 } && { error "Unable to unlink: $1"; return 1 } + { test -r ${1}.pid } && { rm -f ${1}.pid } + ) &! + return 0 +} + diff --git a/src/zlibs/maildirs b/src/zlibs/maildirs @@ -0,0 +1,147 @@ +#!/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. + + +# checks if its a maildir +# returns 0 (success) if yes +# no in all other cases +maildircheck() { + { test -r $1 } || { + error "Maildir not existing: $1" + return 1 } + { test -w $1 } || { + error "Directory not writable: $1" + return 1 } + { test -r $1/cur } \ + && { return 0 } # Yes is a maildir +# shortened test to speedup +# && { test -r $1/new } \ +# && { test -r $1/tmp } \ + func "Not a maildir: $1" + return 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 + +} + + +list_maildirs() { + maildirs=() + for m in `find $MAILDIRS -type d`; do + maildircheck $m + { test $? = 0 } && { + # is a maildir + { test "`find $m -type f`" != "" } && { + # and is not empty + maildirs+=(`basename $m`) + } + } + done + return ${#maildirs} +} + +rmdupes() { + tmp=$TMPDIR/$datestamp.rmdupes.$RANDOM + newlock $tmp + for folder in ${=PARAM}; do + { test ! -r $folder } && { folder=$MAILDIRS/$folder } + { test ! -r $folder } && { error "Maildir not found: $folder"; continue } + notice "Removing duplicates in $folder" + c=0 + for i in `find ${folder} -type f`; do + # 5MB should be enough ehre? + formail -D 5000000 $tmp <$i \ + && rm $i && c=`expr $c + 1` + done + done + unlink $tmp + notice "$c duplicates found and deleted" +} + +merge() { + src=${PARAM[1]} + dst=${PARAM[2]} + if ! [ -r ${src}/cur ]; then + error "No source maildir found in $src" + return 1 + fi + if ! [ -r ${dst}/cur ]; then + error "No destination maildir found in $dst" + return 1 + fi + notice "Merging maildir ${src} into ${dst}" + c=0 + for i in `find ${src}/cur -type f`; do mv $i ${dst}/cur/; c=`expr $c + 1`; done + for i in `find ${src}/new -type f`; do mv $i ${dst}/new/; c=`expr $c + 1`; done + for i in `find ${src}/tmp -type f`; do mv $i ${src}/tmp/; c=`expr $c + 1`; done + act "$c mails succesfully moved." + notice "Operation completed, you can now safely remove ${src}" +} + +# re-sort all maildir through the procmail filters +# it can create duplicates, up to the user to rmdupes +filter() { + + update # update all filters + + # archive old procmail log + if [ -r $WORKDIR/log/procmail.log ]; then + newlock $WORKDIR/log/procmail-${datestamp}.log + cat $WORKDIR/log/procmail.log \ + >> $WORKDIR/log/procmail-${datestamp}.log + rm -f $WORKDIR/log/procmail.log + unlock $WORKDIR/log/procmail-${datestamp}.log + fi + + for folder in ${=PARAM}; do + typeset -al fall + { test ! -r $folder } && { folder=$MAILDIRS/$folder } + { test ! -r $folder } && { error "Maildir not found: $folder"; return 1 } + notice "Filtering folder $folder" + # first index current state + for m in `find $folder -type f`; do fall+=($m); done + # then process it, this way ignoring new mails send to same folder + for n in ${=fall}; do + cat $n | procmail -m $PROCMAILDIR/rc + done + unset fall + done + + total=`mailstat -k $WORKDIR/log/procmail.log | tail -n1 | awk '{print $2}'` + briefing=`mailstat -kt $WORKDIR/log/procmail.log |awk '!/procmail/ { print " " $2 "\t" $3 }'|sort -nr` + echo "${briefing}" +} diff --git a/src/zlibs/password b/src/zlibs/password @@ -0,0 +1,166 @@ +#!/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. + + + +# 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: $name" + case $OS in + MAC) + security find-internet-password \ + -c JARO -a $email -s $host \ + -p $transport -P $port > /dev/null + if [ $? != 0 ]; then # its a new password + new_password + { test $? != 0 } && { + error "Password input aborted." + return 1 } + else + password=`security find-internet-password -c JARO -a $email -s $host -p $transport -P $port -g 2>&1| awk '/^password:/ { print $2 }' | sed -e 's/"//g'` + fi + return 0 + ;; + ##################################### + GNU) + func "Looking for password in keyring: $name" + ################### + # USE GNOME KEYRING + { test $GNOMEKEY = 1 } && { + echo "protocol=${type}\npath=jaromail/${email}\nusername=${login}\nhost=${host}\n\n" \ + | $WORKDIR/bin/jaro-gnome-keyring check + if [ $? != 0 ]; then # its a new password + new_password + { test $? != 0 } && { + error "Password input aborted." + return 1 } + else # password found into gnome keyring + act "Using saved password for $1 @ $2" + password=`echo "protocol=${type}\npath=jaromail/${email}\nusername=${login}\nhost=${host}\n\n" | $WORKDIR/bin/jaro-gnome-keyring get` + fi + return 0 + } + #################### + # USE PINENTRY ALONE + new_password + { test $? != 0 } && { + error "Password input aborted." + return 1 } + return 0 + ;; + *) + error "Unknown system, can't figure out how to handle passwords" + return 1 + esac +} + +new_password() { + notice "Setting a new password for $name" + password=`pin_entry $login $host` + res=0 + case $OS in + MAC) + if [ "$password" != "" ]; then + security add-internet-password \ + -c JARO -a $email -s $host \ + -p $transport -P $port -w "${password}" + if [ $? != 0 ]; then + error "Error adding password to keyring." + else + act "New password saved in keyring" + fi + return 0 + else + security delete-internet-password \ + -c JARO -a $email -s $host \ + -p $transport -P $port > /dev/null + res=$?; unset password + { test $res != 0 } && { + error "Error deleting password from keyring." + return 1 } + act "No new password given, old password erased." + return 0 + fi + ;; + GNU) + if [ "$password" != "" ]; then # password was written + + # USE GNOME KEYRING + { test $GNOMEKEY = 1 } && { + + cat <<EOF | $WORKDIR/bin/jaro-gnome-keyring store +protocol=${type} +path=jaromail/${email} +username=${login} +host=${host} +password=${password} +EOF + { test $? != 0 } && { error "Error saving password in Gnome keyring" } + return 0 + } + + return 0 + + else # password is blank or aborted + + # USE GNOME KEYRING + { test $GNOMEKEY = 1 } && { + + cat <<EOF | $WORKDIR/bin/jaro-gnome-keyring erase +protocol=${type} +path=jaromail/${email} +username=${login} +host=${host} +EOF + { test $? != 0 } && { + error "Error accessing password in Gnome keyring" + return 1 } + act "No new password given, old password erased." + return 0 + } + + return 1 + + fi + ;; + *) + error "Unknown system, can't figure out how to handle passwords" + return 1 + esac +} diff --git a/src/zlibs/search b/src/zlibs/search @@ -0,0 +1,259 @@ +#!/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. + +####################### +## Search into maildirs +# using mairix +search() { + { which mairix > /dev/null } || { return 1 } + id=$RANDOM + rc=$TMPDIR/search.conf.$id + typeset -al expr + typeset -al fold + # intelligent parse of args, position independent + # check if its a folder, if not is an expression + mlstr="all folders"; ml=""; c=0 + basedir=$MAILDIRS + # check if the name of a maildir is among params + for p in ${PARAM}; do + c=$(( $c + 1 )) + func "checking param: ${p}" + if [ -r ${p} ]; then + + { maildircheck ${p} } && { + fold+=(${p}) + { test ${#fold} = 1 } && { + # base path is the dir of the first folder + pushd `dirname ${p}` + basedir=`pwd` + popd } + } + + elif [ -r ${MAILDIRS}/${p} ]; then + + { maildircheck ${MAILDIRS}/${p} } && { fold+=(${MAILDIRS}/${p}) } + + else # not a folder, add it to expressions array + expr+=(${p}) + fi + done + + + # now fold is an array of specified folders + # expr is an array of specified search expressions + + + # to search only one maildir then we need to index it + # separate from the rest of the maildirs + if [ ${#fold} != 0 ]; then + { test ${#expr} = 0 } && { + error "no search expression given for folders ${fold[@]}" + return 1 } + # forge the folder string for mairix conf + folders=""; for f in ${fold}; do folders="$folders`basename $f`:"; done + cat <<EOF > $rc +base=$basedir +database=$TMPDIR/search.db.$id +maildir=${folders} +mfolder=$TMPDIR/search.result.$id +mformat=maildir +EOF + + exitcode=1 + act "Indexing ${folders}" + mairix -F -f $rc 2> /dev/null + { test $? = 0 } && { + act "Searching for: ${expr}" + found=`mairix -F -f $rc ${=expr} 2> /dev/null | awk '{ print $2}'` + if [ $found != 0 ]; then + mutt -F $MUTTDIR/rc -R -f $TMPDIR/search.result.$id + notice "Found $found matches looking for '$expr' in $folders" + find $TMPDIR/search.result.$id + cat $rc + exitcode=0 + else error "No matches found."; fi + } + + + rm -f $rc + rm -f $TMPDIR/search.db.$id +# rm -rf $TMPDIR/search.result.$id + return $exitcode + + + #################################################### + else # no folder specified on commandline, search all + # make index if no params given + c=0 + for i in `ls $MAILDIRS`; do + # is it a maildir? + { maildircheck $MAILDIRS/${i} } && { + c=`expr $c + 1`; ml="$ml:$i" } + done + fi + + cat <<EOF > $rc +base=$MAILDIRS +database=$WORKDIR/search.db +maildir=${ml} +mfolder=$TMPDIR/search.result.$id +mformat=maildir +EOF + # just index + { test ${#PARAM} = 0 } && { + act "Indexing $c maildirs for search" + act "please be patient..." + mairix -F -f $rc + rm -f $rc + exitcode=$? + if [ $exitcode = 0 ]; then notice "Done." + else error "Error, indexing aborted."; fi + rm -f $rc + return $exitcode + } + act "Searching $mlstr for: ${expr}" + exitcode=1 + found=`mairix -F -f $rc ${=expr} 2> /dev/null | awk '{print $2}'` + if [ $CALLMUTT = 1 ]; then + + if [ $found != 0 ]; then + mutt -F $MUTTDIR/rc -R -f $TMPDIR/search.result.$id + notice "Found $found matches looking for '$expr' in all mail folders" + exitcode=0 + else error "Nothing found matching '$expr'"; fi + + rm -rf $TMPDIR/search.*.$id + rm -f $rc + return $exitcode + + else ### if not calling mutt, internal use mode: + # print out the full path to the results maildir + # return number of found hits + echo $TMPDIR/search.result.$id + return $found + fi +} + + +backup() { + id=$RANDOM + rc=$TMPDIR/backup.conf.$id + typeset -al expr + typeset -al fold + + src=""; dst="" + basedir=$MAILDIRS + # check if the name of a maildir is among params + # we need at least 2 maildirs, the second is the destination + for p in ${PARAM}; do + c=$(( $c + 1 )) + + if [ $c = ${#PARAM} ]; then + # last one is always the destination + func "destination is ${p}" + fold+=(${p}) + + elif [ -r ${p} ]; then + + { maildircheck ${p} } && { + func "param ${p} is a maildir" + fold+=(${p}) + { test ${#fold} = 1 } && { + # base path is the dir of the first folder + pushd `dirname ${p}` + basedir=`pwd` + popd } + } + + elif [ -r ${MAILDIRS}/${p} ]; then + + { maildircheck ${MAILDIRS}/${p} } && { + func "param ${p} is a jaro maildir" + fold+=(${MAILDIRS}/${p}) + } + + else # not a folder, add it to expressions array + func "param ${p} is an expression" + expr+=(${p}) + fi + done + + { test ${#fold} -lt 2 } && { + error "Not enough folders specified for backup: minimum is 2" + act "When specifying more than 2, the last one is the destination" + return 1 + } + + dst=${fold[${#fold}]} + { test -r $dst } && { + error "Backup destination already exists: $dst" + return 1 } + + maildirmake "${dst}" + + { test ${#expr} = 0 } && { + error "No expression set for backup, please indicate what you want to backup" + act "For example: d:10y-2y (all mails older than 1 year up to 10 years ago" + act "Or a simple search string, all expressions can be verified using search." + return 1 + } + + # forge the folder string for mairix conf + folders="" + for f in ${fold}; do + { test $f = $dst } || { + folders="$folders`basename $f`:" } + done + + notice "Backup of all mails in '$folders' matching expression '$expr'" + + act "Indexing folders" + cat <<EOF > $rc +base=$basedir +database=$TMPDIR/backup.db.$id +maildir=${folders} +mfolder=$dst +mformat=maildir +EOF + mairix -F -f $rc 2> /dev/null + act "Moving matches to $dst" + pushd `dirname $dst`; basedir=`pwd`; popd + rm -f $rc; cat <<EOF > $rc +base=$basedir +database=$TMPDIR/backup.db.$id +maildir=${folders} +mfolder=$dst +mformat=maildir +EOF + found=`mairix -F -f $rc -H ${expr} 2> /dev/null | awk '{print $2}'` + notice "$found matches found, destination folder size is `du -hs $basedir/$dst | awk '{print $1}'`" + # invert the order of folders to start with destination in rmdupes + typeset -al PARAM + c=$(( ${#fold} )) + while [ $c -gt 0 ]; do + func "${fold[$c]}" + PARAM+=(${fold[$c]}) + c=$(( $c - 1 )) + done + QUIET=1 + rmdupes +} diff --git a/src/zlibs/stats b/src/zlibs/stats @@ -0,0 +1,208 @@ +#!/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. + + + + +stats() { + # make index of all maildirs + notice "Maildirs status" + case ${PARAM[1]} in + + timecloud) timecloud ;; + + weeks) weeks ;; + + *) # simple stats + typeset -alU ml + typeset -al empty + list_maildirs + for i in ${maildirs}; do + cur=`ls $MAILDIRS/$i/cur | wc -l` + new=`ls $MAILDIRS/$i/cur | wc -l` + tot=$(( $cur + $new )) + ml+=("$tot\t:: $i\t ($cur/$new)") + done + for m in ${ml}; do + { test ${m[1]} = "0" } && { + empty+=${m} + continue } + print " ${m}" + done + { test ${#empty} != 0 } && { emptystr="(${#empty} are empty)" } + notice "Total maildirs: ${#ml} $emptystr" + ;; + esac + return 0 +} + +weeks() { + id=$RANDOM + db=$TMPDIR/weekstats.db.$id + sql=$TMPDIR/weekstats.select.$id + list_maildirs + notice "Computing weekly statistics on ${#maildirs} maildirs" + func "Creating temporary database" + cat <<EOF | ${SQL} -batch $db +CREATE TABLE stats +( + date text nocase, + ml text collate nocase, + hits int +); +EOF + { test $? != 0 } && { + error "Error creating temporary week stats database." + return 1 } + + for m in ${maildirs}; do + for f in `find $MAILDIRS/${m} -type f`; do + timestamp=`fetchdate "%Y-%U" ${f}` + cat <<EOF | ${SQL} -batch $db > $sql +SELECT * FROM stats +WHERE ml IS "${m}" AND date IS "${timestamp}"; +EOF + res=`cat $sql` + if [ "$res" = "" ]; then + # new tag + cat <<EOF | ${SQL} -batch $db +INSERT INTO stats (date, ml, hits) +VALUES ("${timestamp}", "${m}", 1); +EOF + else + cat <<EOF | ${SQL} -batch $db +UPDATE stats SET hits = hits + 1 +WHERE ml IS "${m}" AND date IS "${timestamp}"; +EOF + fi + done + done + # gather results from the database + table=$WORKDIR/.stats/jaromail.html + ${=mkdir} $WORKDIR/.stats + + cat <<EOF > $table +<table > + <caption>Jaro Mail weekly statistics</caption> + <thead> + <tr> + <td></td> +EOF + typeset -al week + cat <<EOF | ${SQL} -batch $db > $sql +SELECT date FROM stats ORDER BY date; +EOF + for w in `cat $sql | uniq`; do + week+=($w) + echo "<th scope=\"col\">${w}</th>" >> $table + done + cat <<EOF >> $table + </tr> + </thead> + <tbody> + +EOF + for m in ${maildirs}; do + echo -n "<tr><th scopre=\"row\">$m</th>" >> $table + for w in ${week}; do + cat <<EOF | ${SQL} -batch $db > $sql +SELECT hits FROM stats +WHERE ml IS "${m}" AND date IS "${w}"; +EOF + echo -n "<td>`cat $sql`</td>" >> $table + done + echo "</tr>" >> $table + done + + cat <<EOF >> $table + </tbody> +</table> +EOF + cp $WORKDIR/.stats/visualize/header.html \ + $WORKDIR/.stats/index.html + cat $table >> $WORKDIR/.stats/index.html + echo "</body>\n</html>\n" >> $WORKDIR/.stats/index.html +} + +timecloud() { + id=$RANDOM + list_maildirs + notice "Computing timecloud statistics on ${#maildirs} maildirs" + func "Creating temporary database" + cat <<EOF | ${SQL} -batch $TMPDIR/timecloud.db.$id +CREATE TABLE stats +( + date text collate nocase, + tag text collate nocase, + hits int +); +EOF + { test $? != 0 } && { + error "Error creating temporary timecloud database." + return 1 } + + for m in ${maildirs}; do + for f in `find $MAILDIRS/${m} -type f`; do + timestamp=`fetchdate "%Y-%m-%d" ${f}` + cat <<EOF | ${SQL} -batch $TMPDIR/timecloud.db.$id > $TMPDIR/timecloud.select.$id +SELECT * FROM stats +WHERE tag IS "${m}" AND date IS "${timestamp}"; +EOF + res=`cat $TMPDIR/timecloud.select.$id` + if [ "$res" = "" ]; then + # new tag + cat <<EOF | ${SQL} -batch $TMPDIR/timecloud.db.$id +INSERT INTO stats (date, tag, hits) +VALUES ("${timestamp}", "${m}", 1); +EOF + else + cat <<EOF | ${SQL} -batch $TMPDIR/timecloud.db.$id +UPDATE stats SET hits = hits + 1 +WHERE tag IS "${m}" AND date IS "${timestamp}"; +EOF + fi + done + done + # gather results from the database + cat <<EOF | ${SQL} -batch $TMPDIR/timecloud.db.$id > $TMPDIR/timecloud.results.$id +.separator \t +SELECT * FROM stats ORDER BY date; +EOF + # format the results into a JSON string + awk 'BEGIN { date=0; printf "[" } +{ if(date != $1) { + if( date != 0 ) printf "]]," + date=$1 + printf "[\"" date "\",[" + printf "[\"" $2 "\",\"" $3 "\"]" + } else { + printf ",[\"" $2 "\",\"" $3 "\"]" + } +} +END { print "]]];" } +' $TMPDIR/timecloud.results.$id > $TMPDIR/timecloud.json.$id + ${=mkdir} $WORKDIR/timecloud + cat <<EOF > $WORKDIR/timecloud/jaromail.js +var jaromail=`cat $TMPDIR/timecloud.json.$id` +EOF + rm $TMPDIR/timecloud.*.$id +}