jaromail

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

addressbook (11069B)


      1 #!/usr/bin/env zsh
      2 #
      3 # Jaro Mail, your humble and faithful electronic postman
      4 #
      5 # a tool to easily and privately handle your e-mail communication
      6 #
      7 # Copyleft (C) 2010-2015 Denis Roio <jaromil@dyne.org>
      8 #
      9 # This source  code is free  software; you can redistribute  it and/or
     10 # modify it under the terms of  the GNU Public License as published by
     11 # the Free  Software Foundation; either  version 3 of the  License, or
     12 # (at your option) any later version.
     13 #
     14 # This source code is distributed in  the hope that it will be useful,
     15 # but  WITHOUT ANY  WARRANTY;  without even  the  implied warranty  of
     16 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
     17 # Please refer to the GNU Public License for more details.
     18 #
     19 # You should have received a copy of the GNU Public License along with
     20 # this source code; if not, write to:
     21 # Free Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
     22 
     23 
     24 
     25 ###################
     26 # Jaro Brother DB
     27 create_addressbook() {
     28     # $ADDRESSBOOK is set in jaro
     29     ab="$ADDRESSBOOK"
     30     func "create addressbook: $ab"
     31     [[ -r "$ab" ]] && {
     32         error "Addressbook already exists: $ab"
     33         return 1
     34     }
     35 
     36     [[ -r "$MAILDIRS"/Addressbook ]] && {
     37         notice "Updating the old addressbook to a new format"
     38         _list=$list
     39         list=blacklist; export_old_to_abook
     40         list=whitelist; export_old_to_abook
     41         list=$_list
     42         mv "$MAILDIRS"/Addressbook "$MAILDIRS"/Addressbook.old 
     43     }
     44 
     45     touch "$ab"
     46 
     47     # make sure is private
     48     chmod 600 "$ab"
     49     chown $_uid:$_gid "$ab"
     50 
     51     return 0
     52 }
     53 
     54 insert_address() {
     55     _email="${1}"; _name="${2}";
     56     func "insert address: $_name <$_email>"
     57 
     58     lookup_email "$_email"
     59 
     60     [[ $? = 0 ]] && {
     61         func "address already present in $list"
     62         return 1
     63     }
     64 
     65     print "From: $_name <$_email>" | \
     66         abook --datafile "$ADDRESSBOOK" \
     67         --add-email-quiet > /dev/null
     68 
     69     return 0
     70 }
     71 
     72 search_addressbook() {
     73     fn search_addressbook $*
     74     
     75     abook --datafile "$ADDRESSBOOK" --mutt-query "${*:l}" | awk '
     76 /^$/ { next }
     77 { for(c=2;c<=NF;c++) printf "%s ", $c
     78   print "<" $1 ">" }'
     79 
     80 }
     81 
     82 
     83 lookup_email() {
     84     _addr=${1:l}
     85     func "lookup address $_addr in $list"
     86     abook --datafile "$ADDRESSBOOK" \
     87         --mutt-query "$_addr" > /dev/null
     88     return $?
     89 }
     90 
     91 complete() {
     92 	fn complete $*
     93 	req=(list)
     94 	ckreq || return 1
     95 
     96     # completion on configured groups
     97     { test -r "$MAILDIRS/Groups" } && {
     98         if [[ "$1" =~ "group/" ]]; then
     99             func "completion will look into groups"
    100             needle="${1[(ws:/:)2]}"
    101             if [ "$needle" = "" ]; then
    102                 act "Listing all mailout groups"
    103                 matches=`${=find} "$MAILDIRS/Groups" -type f`
    104             else
    105                 act "Searching for \"$needle\" in mailout groups"
    106                 matches=`${=find} "$MAILDIRS/Groups" -type f -name \"*$needle*\"`
    107             fi
    108 
    109             print "Groups: `print $matches | wc -l` matches"
    110             print
    111             for i in ${(f)matches}; do
    112                 gr=`basename $i`
    113                 print "$gr@jaromail.group\t`wc -l < $i` recipients"
    114             done
    115             return 0
    116         fi
    117     }
    118 
    119     act "Searching for \"$1\" in addressbook $list"
    120     abook --datafile "$ADDRESSBOOK" --mutt-query "$1"
    121     return $?
    122 }
    123 
    124 sender_isknown() {
    125 	fn sender_isknown $*
    126     # extract only headers from stdin
    127     head="`awk '
    128 { print $0 }
    129 /^$/ { exit }' | ${WORKDIR}/bin/fetchaddr -x From -a`"
    130 
    131     email="${head[(ws:,:)1]:l}"
    132     isemail $email
    133     [[ $? = 0 ]] || { return 1 }
    134 
    135     abook --datafile $MAILDIRS/$list.abook \
    136         --mutt-query "$email" > /dev/null
    137     return $?
    138 }
    139 
    140 learn() {
    141 	fn learn $*
    142     [[ $DRYRUN == 1 ]] && func "running in dryrun mode, no entries added to addressbook"
    143 
    144     what=${1:-sender}
    145     func "learning from $what"
    146 
    147     # zero e_addr map before using e_parse
    148     e_addr=()
    149 
    150     case ${what} in
    151         
    152         sender|from) # simple: one address only on From:
    153             
    154             # now e_name e_mail and e_parsed are filled in
    155             
    156             awk '{ print $0 } /^$/ { exit }' | e_parse From
    157             
    158             # no need to cycle, From is always only one field
    159             [[ $DRYRUN == 0 ]] && {
    160                 _e="${(k)e_addr}"
    161                 _n="${(v)e_addr}"
    162                 insert_address "$_e" "$_n"
    163                 [[ $? = 0 ]] && { act "$list <- $_n <$_e>" }
    164             }
    165             return 0
    166             ;;
    167 
    168         all)
    169 
    170             awk '{ print $0 } /^$/ { exit }' | e_parse
    171 
    172             [[ $DRYRUN == 0 ]] && {
    173                 # complex: more addresses in To: and Cc:
    174                 for _e in ${(k)e_addr}; do
    175                     _n="${e_addr[$_e]}"
    176                     insert_address "$_e" "$_n"
    177                     [[ $? = 0 ]] && { act "$list <- $_n <${_e}>" }
    178                 done
    179             }
    180             return 0
    181             ;;
    182         
    183         recipient|to)
    184 
    185             awk '{ print $0 } /^$/ { exit }' | e_parse To
    186 
    187             awk '{ print $0 } /^$/ { exit }' | e_parse Cc
    188 
    189             [[ $DRYRUN == 0 ]] && {
    190                 # complex: more addresses in To: and Cc:
    191                 for _e in ${(k)e_addr}; do
    192                     _n="${e_addr[$_e]}"
    193                     insert_address "$_e" "$_n"
    194                     [[ $? = 0 ]] && { act "$list <- $_n <${_e}>" }
    195                 done
    196             }
    197 
    198             return 0
    199             ;;
    200         
    201         *)
    202             error "Unknown learning function: $what" ;;
    203     esac
    204     return 1
    205 
    206 }
    207 
    208 
    209 # import emails from VCard into abook
    210 # checks if the emails are already known
    211 import_vcard() {
    212 	fn import_vcard $*
    213     vcard="$1"
    214 	req=(vcard ADDRESSBOOK)
    215 	freq=($vcard)
    216 	ckreq || return 1
    217 
    218     head -n1 $vcard | grep '^BEGIN:VCARD' > /dev/null
    219 
    220     [[ $? = 0 ]] || {
    221         error "File to import is not a VCard: $vcard"
    222         return 1
    223     }
    224 
    225     notice "Import VCard in addressbook: ${vcard}"
    226 
    227     # parse the vcard and print a simple name and email list
    228     # each value on a single line, entry tuples followed by a #
    229     # we skip entries that don't have an email
    230     addresses=`cat ${vcard} | awk '
    231 BEGIN { newcard=0; c=0; name=""; email=""; }
    232 /^BEGIN:VCARD/ { newcard=1 }
    233 /^FN:/ { if(newcard = 1) name=$0 }
    234 /^EMAIL/ { if(newcard = 1) email=$0 }
    235 /^END:VCARD/ {
    236   if(newcard = 1) {
    237     newcard=0
    238     if(email != "") {
    239       c+=1
    240       print name
    241       print email
    242       print "# " c
    243     }
    244     email=""
    245     next
    246   }
    247 }
    248 ' | cut -d: -f2`
    249 
    250     # now parse the temporary list of name and emails
    251     # made of name, email and a hash for each, newline separated
    252 
    253     #    ${=rm} $tmp
    254 
    255     lock $ADDRESSBOOK
    256 
    257     newa=1; _name=""; _email=""
    258     for a in ${(f)addresses}; do
    259         { test "${a[1]}" = "#" } && {
    260             newa=1; # its the end of the entry
    261 
    262             # handle lines with multiple emails in vcard
    263             # TODO: generate Groups/${_name} from this
    264             for ee in ${=_email}; do
    265                 # check if we have this email already
    266                 _e=`print ${ee} | extract_emails`
    267                 func "lookup_email: ${_e}"
    268                 lookup_email "${_e}"
    269 
    270                 [[ $? = 0 ]] || {
    271                     act "${a} ${_name} <${_e}>"
    272                     [[ $DRYRUN = 0 ]] && insert_address "${_e}" "${_name}"
    273                 }
    274             done
    275 
    276             continue }
    277         if [[ $newa -eq 1 ]]; then
    278             # (V) makes special chars visible, we need to remove them..
    279             _name="${(V)a[(ws:^:)1]}"; newa=0; continue
    280         elif [[ $newa -eq 0 ]]; then
    281             _email="${(V)a[(ws:^:)1]}"
    282         fi
    283     done
    284 
    285     unlock $ADDRESSBOOK
    286 
    287     notice "Done importing addresses"
    288 }
    289 
    290 # import address lists from stdin
    291 import() {
    292 	fn import $*
    293 
    294     # case insensitive match
    295     unsetopt CASE_MATCH
    296 
    297     notice "Import address list from stdin into addressbook $list"
    298 
    299     _new=0
    300     act "imported new entries will be printed on stdout"
    301 
    302     e_addr=()
    303     
    304     # read_stdin
    305     # _stdin=`cat` # reads into var _stdin
    306     for i in "${(f)$(cat)}"; do
    307         [[ $global_quit = 1 ]] && break
    308 
    309         # skip comments starting with #
    310         [[ "$i[1]" = "#" ]] && continue
    311 
    312         print - "From: $i" | e_parse From
    313         [[ $? = 0 ]] || continue
    314 
    315         _e="${e_parsed[(ws:,:)1]:l}"
    316 
    317 		# this error of parsed '@' occurs sometimes
    318 		[[ "$_e" = "@" ]] && continue
    319 
    320         # check if the email is not already known
    321         lookup_email "$_e"
    322         [[ $? = 0 ]] && {
    323             func "email already known: $_e"
    324             continue
    325         }
    326 
    327         _n="${e_parsed[(ws:,:)2]}"
    328 
    329         print - "$_n <$_e>"
    330         [[ $DRYRUN = 0 ]] && insert_address "$_e" "$_n"
    331 
    332         _new=$(( $_new + 1 ))
    333     done
    334     notice "Valid unique entries parsed: ${#e_addr}"
    335     act "new addresses found: ${_new}"
    336     return 0
    337 }
    338 
    339 
    340 
    341 # export old addressbook format to abook
    342 export_old_to_abook() {
    343 	fn export_old_to_abook $*
    344 
    345     [[ -r $MAILDIRS/Addressbook ]] || {
    346         notice "Old addressbook format not found"
    347         act "there is nothing to convert to abook"
    348         return 1
    349     }
    350 
    351     lock $MAILDIRS/Addressbook
    352 
    353     out=$MAILDIRS/$list.abook
    354     act "Exporting $list to abook format: $out"
    355     rm -f $out
    356 
    357     func "launching SELECT email,name sqlite3 query"
    358     addresses=`cat <<EOF | ${SQL} -column -header -batch $MAILDIRS/Addressbook \
    359     | grep -v '^email'
    360 .width 40 100
    361 .mode list
    362 .separator '|'
    363 SELECT email, name FROM $list;
    364 EOF`
    365 
    366     unlock $MAILDIRS/Addressbook
    367     func "converting database into abook format"
    368     cat <<EOF > $out
    369 # abook addressbook file
    370 
    371 [format]
    372 program=JaroMail
    373 version=$VERSION
    374 
    375 EOF
    376     c=0
    377     for a in ${(f)addresses}; do
    378         _email="${(Q)a[(ws:|:)1]}"
    379         # remove from name all what is an email between brackets
    380         # crop (trim) all beginning and ending whitespaces from name
    381         _name=`print ${(Q)a[(ws:|:)2]} | trim`
    382         { test "${_email}" != "" } && {
    383             cat <<EOF >> $out
    384 [${c}]
    385 name=${_name}
    386 email=${_email}
    387 
    388 EOF
    389             c=$(( $c + 1 ))
    390         }
    391     done
    392 
    393 }
    394 
    395 # export addressbook to vcard
    396 convert_addresses() {
    397     fn convert_addresses $*
    398     _format=${1:-vcard}
    399 
    400     notice "Converting stdin addresses to format $_format"
    401     c=0
    402     ztmp
    403     for i in "${(f)$(cat)}"; do
    404         print "alias $i" >> $ztmpfile
    405         c=$(( $c + 1 ))
    406     done
    407 
    408 
    409     abook --convert --informat mutt \
    410         --infile "$ztmpfile" \
    411         --outformat $_format
    412 
    413     _res=$?
    414 
    415     if [[ $_res = 0 ]]; then
    416         notice "$c addresses converted to $_format"
    417     else
    418         error "Abook fails to convert addresses to format: $_format"
    419     fi
    420             
    421     return $_res
    422 }
    423 
    424 edit_abook() {
    425 	fn edit_abook $*
    426 	req=(ADDRESSBOOK)
    427 	ckreq || return 1
    428 
    429     abook --config <(cat <<EOF
    430 set autosave=true
    431 set mutt_command=jaro compose
    432 set sort_field=name
    433 EOF
    434 ) --datafile "$ADDRESSBOOK"
    435 }
    436 
    437 # print out all addresses into the selected addressbook
    438 list_abook() {
    439 	fn list_abook $*
    440 	req=(list ADDRESSBOOK)
    441 	ckreq || return 1
    442 
    443     notice "Extracting all addresses in $list"
    444     awk -F'=' '
    445 /^name/  { printf("%s ",$2)    }
    446 /^email/ { printf("<%s>\n",$2) }
    447 ' $ADDRESSBOOK
    448     return 0
    449 }
    450 
    451 ###################