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 3e7cc408365cef1918e3a735a524f414a746b48b
parent ea66118e18b4b9c8fcdd4022352d03b72b2cc268
Author: Jaromil <jaromil@dyne.org>
Date:   Mon,  3 Feb 2014 20:57:32 +0100

Complete refactoring of the send queue, Groups new feature

now it is possible to browse the outbox from mutt and even
delete mails that were meant to be sent. This also introduces
a new feature: Groups is a directory containing simple files
with recipient addresses one per line: they are basically
one-way mailout lists and the main feature is that every email
sent to each recipient will have it as unique To: definitely
avoiding huge Cc: lists but also Bcc: blacklisting on SMTP etc.

Diffstat:
Msrc/zlibs/addressbook | 29++++++++++++++++++++++++++---
Msrc/zlibs/email | 196++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------
Msrc/zlibs/filters | 4++--
Msrc/zlibs/maildirs | 5+++--
4 files changed, 161 insertions(+), 73 deletions(-)

diff --git a/src/zlibs/addressbook b/src/zlibs/addressbook @@ -113,10 +113,32 @@ EOF } complete() { - func "complete from $list: ${PARAM[1]}" - act "Searching for \"${PARAM[1]}\" in $list" - matches="${matches}\n`search_name ${PARAM[1]}`" + func "complete from $list: $1" + + # completion on configured groups + { test -r $MAILDIRS/Groups } && { + if [[ "$1" =~ "group/" ]]; then + needle="${1[(ws:/:)2]}" + if [ "$needle" = "group" ]; then + act "Listing all mailout groups" + matches="`${=find} $MAILDIRS/Groups -type f`" + else + act "Searching for \"$needle\" in mailout groups" + matches="`#{=find} $MAILDIRS/Groups -type f -name \"*$needle*\"`" + fi + print "jaro: ${#matches} matches" + print + for i in ${(f)matches}; do + gr=`basename $i` + print "$gr@jaromail.group\t`wc -l < $i` recipients" + done + return 0 + fi + } + act "Searching for \"$1\" in addressbook $list" + matches="${matches}\n`search_name $1`" + # mutt query requires something like this print "jaro: $((`print $matches | wc -l` -1)) matches" print "$matches" | awk ' @@ -127,6 +149,7 @@ complete() { if($i!=$1) printf "%s ", $i } printf "\n" }' + return 0 } isknown() { diff --git a/src/zlibs/email b/src/zlibs/email @@ -31,42 +31,88 @@ compose() { return $_res } + queue() { local base; - local tmp; - local mailfile; - local msmtpfile; - # add mails to the sendout queue - ${=mkdir} $MAILDIRS/outbox - pushd $MAILDIRS/outbox 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` + base="`hostname`-queue-`date +%Y-%m-%d-%H.%M.%S`" + + queue_to=($@) + # set it ready for saving in outbux + queue_body="$base" + cat > $TMPDIR/$queue_body.mail + + + + maildirmake $MAILDIRS/outbox + { test $? = 1 } && { + # silently migrate the outbox from the old format to the new + tmpp=(`find $MAILDIRS/outbox -type f`) + if [ ${#tmpp} = 0 ]; then + # the old format outbox is just empty + rmdir $MAILDIRS/outbox + maildirmake $MAILDIRS/outbox + else + # there are some mails to be sent in the old format outbox + # preserve them while migrating to the new format + tmppp=$TMPDIR/jaro-outbox-migration-$RANDOM + mkdir -p $tmppp + mv $MAILDIRS/outbox/* $tmppp/ + rmdir $MAILDIRS/outbox + maildirmake $MAILDIRS/outbox + mkdir -p $MAILDIRS/outbox/send + bodies=(`find $tmppp -type f -name '*.mail'`) + rcpt=(`find $tmppp -type f -name '*.msmtp'`) + for i in ${bodies}; do mv $i $MAILDIRS/outbox/new/; done + for i in ${rcpt}; do mv $i $MAILDIRS/outbox/send/${i%%.msmtp}.rcpt; done + rmdir $tmppp + fi + } + mkdir -p $MAILDIRS/outbox/send + + lock $MAILDIRS/outbox + + if [[ "${=queue_to}" =~ '@jaromail.group$' ]]; then + # recipients are in a group + + groupfile="${=queue_to[(ws:@:)1]}" + act "email recipients are in group ${groupfile}" + + { test -r $MAILDIRS/Groups/$groupfile } || { + maildirmake $MAILDIRS/postponed + mv ${TMPDIR}/${queue_body}.mail $MAILDIRS/postponed/new + unlock $MAILDIRS/outbox + error "Group not found: $groupfile" + return 1 } + + groupnum="`cat wc -l < $MAILDIRS/Groups/$groupfile`" + + recipients=`cat $MAILDIRS/Groups/$groupfile | grep -v '^#'` + groupnum="`print ${recipients} | wc -l`" + act "$groupnum recipients in total" + + for i in ${(f)recipients}; do + ig=${base}-${RANDOM} + cat ${TMPDIR}/${queue_body}.mail | \ + awk '/^To:/ { print "'"To: $i"'"; next } { print $0 }' \ + > ${MAILDIRS}/outbox/new/${ig}.mail + # TODO: check email validation and <stripping> + print "$i" | sed 's/.*<//;s/>$//' \ + > ${MAILDIRS}/outbox/send/${ig}.rcpt done - base="$base-$i" + + else + # recipients are in the email envelope + act "email recipients: $queue_to" + mv $TMPDIR/$queue_body.mail $MAILDIRS/outbox/new/$queue_body.mail + print "${=queue_to}" > $MAILDIRS/outbox/send/$queue_body.rcpt fi - mailfile="$base.mail" - msmtpfile="$base.msmtp" - # Write command line to $MSMTPFILE - print "$@" > "$msmtpfile" - lock $msmtpfile - # Write the mail to $MAILFILE - cat > "$mailfile" - unlock $msmtpfile - popd - return 0 + + unlock $MAILDIRS/outbox + { test -r ${TMPDIR}/${queue_body}.mail } && { + ${=rm} ${TMPDIR}/${queue_body}.mail } + } ########### @@ -216,13 +262,12 @@ 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 + queue_rcpt=(`${=find} ${MAILDIRS}/outbox/send -type f -name '*.rcpt'`) + { test ${#queue_rcpt} = 0 } && { act "Outbox is empty, no mails to send." - return 0 - fi + return 0 } read_account smtp ${account} { test $? != 0 } && { @@ -237,19 +282,24 @@ send() { is_online ${host} ${port} { test $? = 0 } || { return 1 } - notice "Sending out ${mailnum} mails via ${type}.${account}" + notice "Sending out ${#queue_rcpt} mails via ${type}.${account}" - { test $DRYRUN != 1 } && { - ask_password $login $host - { test $? != 0 } && { - error "Error retrieving password for $login on $host" - unset password all; return 1 - } + { test $DRYRUN = 1 } && { return 0 } - tmp=$TMPDIR/msmtp.$host.$datestamp.$RANDOM - newlock $tmp - cat <<EOF > $tmp + # from here on we must unlock on error + lock ${MAILDIRS}/outbox + + ask_password $login $host + { test $? = 0 } || { + error "Error retrieving password for $login on $host" + unset password all + unlock ${MAILDIRS}/outbox + return 1 } + + tmp=$TMPDIR/msmtp.$host.$datestamp.$RANDOM + newlock $tmp + cat <<EOF > $tmp account default from ${email} user ${login} @@ -262,28 +312,42 @@ logfile ${MAILDIRS}/logs/msmtp.log auth ${auth} password ${password} EOF - unset password - - _outq=`${=find} $MAILDIRS/outbox -name '*.mail'` - for mail in ${(f)_outq}; do - smtp=`print "${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" + unset password + + for q in ${queue_rcpt}; do + qr=`basename ${q%%.rcpt}` + func "looking for a mail body to send labeled $qr" + qbody=(`${=find} $MAILDIRS/outbox -type f -name "${qr}.mail*"`) + func "found ${#qbody} hits: ${=qbody}" + recipients=(`cat $q`) + func "recipients: ${=recipients}" + + { test ${#qbody} = 0 } && { + # body for rcpt not found, mail must have been deleted from outbox + act "canceled delivery for a deleted mail to: `cat $q`" + ${=rm} $q + continue } + + { test ${#qbody} -gt 1 } && { + error "too many mail bodies found for a message to: `cat $q`" + error "this is quite confusing, check your outbox, we'll skip for now." + continue } + + act "To: ${=recipients}" + msmtp -C $tmp -- ${=recipients} < "${qbody}" + if [ $? != 0 ]; then + error "Error sending mail, skipped" + else + act "Mail sent succesfully" # whitelist those to whom we send mails - cat "$mail" | $WORKDIR/bin/jaro -q learn recipient - unlink "$smtp" - ${=rm} "$mail" - fi - done - unlink $tmp - } # DRYRUN + cat "$qbody" | $WORKDIR/bin/jaro -q learn recipient + ${=rm} "$q" "$qbody" + fi + done + + unlock $MAILDIRS/outbox + unlink $tmp + return 0 } diff --git a/src/zlibs/filters b/src/zlibs/filters @@ -199,7 +199,7 @@ EOF # procmail configuration file generated by Jaro Mail MAILDIR=$MAILDIRS JARO=$WORKDIR/bin/jaro -DEFAULT=unsorted/ +DEFAULT=\$MAILDIR/unsorted/ VERBOSE=off LOGFILE=\$MAILDIR/logs/procmail.log SHELL = /bin/sh # VERY IMPORTANT @@ -207,7 +207,7 @@ UMASK = 007 # James Bond :-) LINEBUF = 8192 # avoid procmail choke # Using Procmail Module Library http://sf.net/projects/pm-lib -PMSRC = $PROCMAILDIR +PMSRC = $WORKDIR/.procmail # Load the central initial startup code. INCLUDERC = \$PMSRC/pm-javar.rc PF_DEST = "" # clear these vars diff --git a/src/zlibs/maildirs b/src/zlibs/maildirs @@ -44,16 +44,17 @@ maildirmake() { { test -z "$1" } && { error "internal error: missing argument for maildirmake" - return } + return 255 } { test -r "$1" } && { func "directory exists: $1" - return } + return 1 } ${=mkdir} ${1}/cur ${=mkdir} ${1}/new ${=mkdir} ${1}/tmp + return 0 }