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 ###################