1#!/usr/local/bin/bash 2# see several paragraphs below for more on the choice of shell interpreter. 3 4# ml is a mail reading interface for mh(1). the design is that of 5# a thin wrapper (this script) which uses 'less' for message 6# display, and mh commands for doing the real work. 7# 8# this script was completely and utterly inspired by a message 9# posted by Ralph Corderoy to the nmh developer's list, describing 10# his similar, unpublished, script: 11# http://lists.nongnu.org/archive/html/nmh-workers/2012-02/msg00148.html 12# 13# see the usage() and help() functions, below, for more detail. (or 14# use 'ml -?' for usage, and '?' within ml for help.) 15# 16# ml creates its own lesskeys map file the first time you run it, 17# called ~/Mail/ml_lesskeymap. 18# 19# there are a number of places where i let ml invoke my own wrapper 20# scripts to do something mh-like. these wrappers do things like 21# provide safe(r) message deletion, select among repl formats, etc. 22# all of these can be easily changed -- see the do_xxxx() functions. 23# all are assumed to operate on mh-style message specifications, and 24# on 'cur' by default. 25# 26# this script uses the sequences 'ml', 'mldel', 'mlspam', 'mlunr', 27# 'mlkeep', and 'mlrepl'. it also manipulates the user's Unseen-sequence. 28# 29# the shell dialect used is very conservative. dash (debian's 30# /bin/sh), bash, ksh, and solaris /bin/sh will all work, and probably 31# the current bsd /bin/sh variants as well, with very slightly 32# improved function in bash or ksh. the only non-posix shell feature 33# used is an argument to "read". search for the function ask_init() 34# for more detail. 35# 36# paul fox, pgf@foxharp.boston.ma.us, february 2012 37# ------------ 38 39 40create_lesskey_map() 41{ 42 # the lesskey(1) bindings that cause less to work well with ml are: 43 lesskey -o $lesskeymap -- - <<-EOF 44 \^ quit \^ 45 \? quit \? 46 E quit E 47 H quit H 48 J quit J 49 n quit n 50 K quit K 51 P quit P 52 p quit p 53 Q quit Q 54 q quit q 55 R quit R 56 S quit S 57 U quit U 58 V quit V 59 X quit X 60 d quit d 61 f quit f 62 r quit r 63 s quit s 64 u quit u 65 i quit i 66 67 # \40 maps the space char, to force the last page to start at 68 # the end of prev page, rather than lining up with bottom of 69 # screen. 70 \40 forw-screen-force 71EOF 72 73} 74 75 76# 77# the functions named do_xxxx() are the ones that are most ripe for 78# customization. feel free to nuke my personal preferences. 79# 80 81do_rmm() 82{ 83 # d "$@" ; return # pgf's private alias 84 rmm "$@" 85} 86 87do_spamremove() 88{ 89 # spam "$@" ; return # pgf's private alias 90 refile +spambucket "$@" # you're on your own 91} 92 93do_reply() 94{ 95 # rf "$@" ; return # pgf's private alias 96 repl "$@" 97} 98 99do_replyall() 100{ 101 # R "$@" ; return # pgf's private alias 102 repl -cc to -cc cc "$@" 103} 104 105do_forw() 106{ 107 # f "$@" ; return # pgf's private alias 108 forw "$@" 109} 110 111do_edit() 112{ 113 ${VISUAL:-${EDITOR:-vi}} $(mhpath cur) 114} 115 116do_urlview() 117{ 118 urlview $(mhpath cur) 119} 120 121do_viewhtml() 122{ 123 echo 'mhshow-show-text/html: ' \ 124 ' %p/usr/bin/lynx -force_html '%F' -dump | less' \ 125 > /tmp/ml-mhshow-html$$ 126 127 MHSHOW=/tmp/ml-mhshow-html$$ \ 128 LC_ALL=C \ 129 mhshow -type text/html "$@" 130 131 rm -f /tmp/ml-mhshow-html$$ 132} 133 134do_sort() 135{ 136 # the intent is to apply some sort of thread/date ordering. 137 # be sure no sequences have been started 138 verify_empty "Sorting requires starting over." mldel || return 139 verify_empty "Sorting requires starting over." mlspam || return 140 verify_empty "Sorting requires starting over." mlunr || return 141 142 # sort by date, then by subject, to get, to get subject-major, 143 # date-minor ordering 144 sortm ml 145 sortm -textfield subject ml 146} 147 148 149 150usage() 151{ 152 cat <<EOF >&2 153usage: $me [ msgs | -s | -a ] 154 $me will present the specified 'msgs' (any valid MH message 155 specification). With no arguments, messages will come from 156 the '$ml_unseen_seq' sequence. 157 Use "$me -s" to get the status of sequences used internally by $me, or 158 "$me -a" to apply previous results (shouldn't usually be needed). 159 Use ? when in less to display help for '$me'. 160EOF 161 exit 1 162} 163 164help() 165{ 166 167 less -c <<EOF 168 169 170 171 "ml" takes an MH message specification as argument. 172 If none is specified, ml will operate on the sequence named "$ml_unseen_seq". 173 174 Messages are repeatedly displayed using 'less', which mostly 175 behaves as usual. less is configured with some special key 176 bindings which cause it to quit with special exit codes. These 177 in turn cause ml to execute distinct commands: they might cause 178 ml to display the next message, to mark the current message as 179 spam, to quit, etc. 180 181 The special key bindings within less are: 182 183 ? display this help (in a separate 'less' invocation) 184 185 ^ show first message 186 n,J show next message 187 p,P,K show previous message 188 189 d mark message for later deletion, by adding to sequence 'mldel'. 190 s mark message for later spam training, by adding to sequence 'mlspam'. 191 u mark message to remain "unread", by adding to sequence 'mlunr'. 192 U undo, i.e., remove it from any of 'mldel', 'mlspam', and 'mlunr'. 193 194 r compose a reply 195 R compose a reply to all message recipients 196 f forward the current message 197 198 S sort the messages, by subject and date 199 H render html from the message 200 V run 'urlview' on the message 201 E edit the raw message file 202 203 q quit. The 'mlunr' sequence will be added back to '$ml_unseen_seq', 204 messages in the 'mldel' are deleted, and those in 'mlspam' 205 are dealt with accordingly. Any messages that were read, 206 but not deleted or marked as spam will be left in the 207 'mlkeep' sequence. If ml dies unexpectedly (or the 'Q' 208 command is used instead of 'q'), "ml -a" (see below) can 209 be used to apply the changes that would have been made. 210 211 Q,X exit. Useful if you want to "start over". The '$ml_unseen_seq' 212 sequence will be restored to its previous state, and the 213 current message list is preserved to 'mlprev'. No other 214 message processing is done. 215 216 Any other command which causes less to quit will simply display 217 the next message. ('q', for instance) 218 219 ml recognizes three special commandline arguments: 220 "ml -s" will report the status of the sequences ml uses, which is 221 handy after quitting with 'X', for example. 222 "ml -a" will apply the changes indicated by the user -- messages 223 in the 'mldel' sequence are deleted, messages in the 224 'mlspam' sequence are trained and marked as spam, and 225 the 'mlunr' sequence is added to the '$ml_unseen_seq' 226 sequence. 227 "ml -k" will recreate the ml_lesskey file used by ml when running 228 less. ml will usually handle this automatically. 229 230EOF 231} 232 233normal_quit() 234{ 235 apply_changes 236 mark -sequence ml -delete all 2>/dev/null 237 exit 238} 239 240ask_init() 241{ 242 # if "read -n 1" gives an error message, it's unsupported, 243 # so don't use it -- the user will need to hit <enter>. 244 # this test works on dash, bash, ksh, and a recent solaris 245 # /bin/sh, but fails on zsh. 246 no_immed=$(read -n 1 < /dev/null 2>&1) 247} 248 249ask() 250{ 251 immed=; 252 253 if [ "$1" = -i ] 254 then 255 test "$no_immed" || immed="-n 1" 256 shift 257 fi 258 echo -n "${1}? [N/y] " 259 read $immed a 260 case $a in 261 [Yy]*) return 0 ;; 262 *) return 1 ;; 263 esac 264 265} 266 267# ensure the given sequence is empty 268verify_empty() 269{ 270 pre="$1" 271 seq=$2 272 if pick $seq:first >/dev/null 2>&1 273 then 274 echo $pre 275 if ask "Non-empty '$seq' sequence found, okay to continue" 276 then 277 mark -sequence $seq -delete all 2>/dev/null 278 else 279 return 1 280 fi 281 fi 282 return 0 283} 284 285# safely return the (non-zero) length of given sequence, with error if empty 286seq_count() 287{ 288 msgs=$(pick $1 2>/dev/null) || return 1 289 echo "$msgs" | wc -l 290} 291 292# move 'ml' to 'mlprev' 293preserve_ml_seq() 294{ 295 mark -sequence mlprev -zero -add ml 2>/dev/null 296 mark -sequence ml -delete all 2>/dev/null 297} 298 299# restore the unseen sequence to its value on entry 300restore_unseen() 301{ 302 mark -sequence $ml_unseen_seq -add saveunseen 2>/dev/null 303} 304 305# add the message to just one of the special sequences. 306markit() 307{ 308 case $1 in 309 mlkeep) # this is really an undo, since it restores default action 310 mark -add -sequence mlkeep cur 311 mark -delete -sequence mlspam cur 2>/dev/null 312 mark -delete -sequence mldel cur 2>/dev/null 313 mark -delete -sequence mlunr cur 2>/dev/null 314 ;; 315 mlspam) 316 mark -delete -sequence mlkeep cur 2>/dev/null 317 mark -add -sequence mlspam cur 318 mark -delete -sequence mldel cur 2>/dev/null 319 mark -delete -sequence mlunr cur 2>/dev/null 320 ;; 321 mldel) 322 mark -delete -sequence mlkeep cur 2>/dev/null 323 mark -delete -sequence mlspam cur 2>/dev/null 324 mark -add -sequence mldel cur 325 mark -delete -sequence mlunr cur 2>/dev/null 326 ;; 327 mlunr) 328 mark -delete -sequence mlkeep cur 2>/dev/null 329 mark -delete -sequence mlspam cur 2>/dev/null 330 mark -delete -sequence mldel cur 2>/dev/null 331 mark -add -sequence mlunr cur 332 ;; 333 mlrepl) # this sequence only affects the displayed header of the message. 334 mark -add -sequence mlrepl cur 335 ;; 336 esac 337} 338 339# emit an informational header at the top of each message. 340header() 341{ 342 local msg=$1 343 344 this_mess="${BOLD}Message $folder:$msg${NORMAL}" 345 346 # get index of current message 347 mindex=$(echo "$ml_contents" | grep -xn $msg) 348 mindex=${mindex%:*} 349 350 # are we on the first or last or only messages? 351 if [ $ml_len != 1 ] 352 then 353 if [ $mindex = 1 ] 354 then 355 mindex="${BOLD}FIRST${NORMAL}" 356 elif [ $mindex = $ml_len ] 357 then 358 mindex="${BOLD}LAST${NORMAL}" 359 fi 360 fi 361 position="($mindex of $ml_len)" 362 363 # have we done anything to this message? 364 r=; s=; 365 if pick mlrepl 2>/dev/null | grep -qx $msg 366 then 367 r="${BLUE}Replied ${NORMAL}" 368 fi 369 if pick mldel 2>/dev/null | grep -qx $msg 370 then 371 s="${RED}Deleted ${NORMAL}" 372 elif pick mlspam 2>/dev/null | grep -qx $msg 373 then 374 s="${RED}Spam ${NORMAL}" 375 elif pick mlunr 2>/dev/null | grep -qx $msg 376 then 377 s="${RED}Unread ${NORMAL}" 378 fi 379 status=${r}${s} 380 381 # show progress for whole ml run (how many deleted, etc.) 382 scnt=$(seq_count mlspam) 383 dcnt=$(seq_count mldel) 384 ucnt=$(seq_count mlunr) 385 others="${scnt:+$scnt spam }${dcnt:+$dcnt deleted }${ucnt:+$ucnt marked unread}" 386 others="${others:+[$others]}" 387 388 statusline="$this_mess $position $status $others" 389 390 echo $statusline 391 392} 393 394# emit the header again 395footer() 396{ 397 echo "-----------" 398 echo "$statusline" 399} 400 401# make the Subject: and From: headers stand out 402colorize() 403{ 404 sed \ 405 -e 's/^\(Subject: *\)\(.*\)/\1'"$RED"'\2'"$NORMAL"'/' \ 406 -e 's/^\(From: *\)\(.*\)/\1'"$BLUE"'\2'"$NORMAL"'/' # 2>/dev/null 407} 408 409cleanup() 410{ 411 # the first replacement gets rid of the default header that 412 # show emits with every message -- we provide our own. 413 # for the second: i think the 'Press <return> text is a bug in 414 # mhl. there's no reason to display this message when not 415 # actually pausing for <return> to be pressed. 416 sed -e '1s/^(Message .*)$/---------/' \ 417 -e 's/Press <return> to show content\.\.\.//' 418} 419 420# this is the where the message is displayed, using less 421show_msg() 422{ 423 local nmsg 424 local which=$1 425 426 427 # only (re)set $msg if pick succeeds 428 if nmsg=$(pick ml:$which 2>/dev/null) 429 then 430 msg=$nmsg 431 viewcount=0 432 else 433 # do we keep hitting the same message? 434 : $(( viewcount += 1 )) 435 if [ $viewcount -gt 2 ] 436 then 437 if ask -i "See message $msg yet again" 438 then 439 viewcount=0 440 else 441 normal_quit 442 fi 443 fi 444 fi 445 446 ( 447 header $msg 448 Mail=$(mhpath +) 449 export NMH_NON_INTERACTIVE=1 450 export MHSHOW=$Mail/mhn.noshow 451 mhshow $msg | 452 cleanup | 453 colorize 454 footer 455 ) | LESS=miXcR less $lesskeyfileopt 456 return $? # return less' exit code 457} 458 459# bad things would happen if we were to keep going after the current 460# folder has been changed from another shell. 461check_current_folder() 462{ 463 curfold=$(folder -fast) 464 if [ "$curfold" != "$folder" ] # danger, will robinson!! 465 then 466 echo "Current folder has changed to '$curfold'!" 467 echo "Answering 'no' will discard changes, and exit." 468 if ask "Switch back to '$folder'" 469 then 470 folder +$folder 471 else 472 restore_unseen 473 preserve_ml_seq 474 exit 475 fi 476 fi 477} 478 479loop() 480{ 481 local nextmsg 482 483 nextmsg=first 484 while : 485 do 486 check_current_folder 487 488 show_msg $nextmsg 489 cmd=$? # save the less exit code 490 491 check_current_folder 492 493 # by default, stay on the same message 494 nextmsg=cur 495 496 case $cmd in 497 498 # help 499 $_ques) help 500 ;; 501 502 # dispatch 503 $_d) markit mldel 504 ##nextmsg=next 505 ;; 506 $_s) markit mlspam 507 ##nextmsg=next 508 ;; 509 $_u) markit mlunr 510 ##nextmsg=next 511 ;; 512 $_U) markit mlkeep 513 ##nextmsg=next 514 ;; 515 516 # send mail 517 $_r) do_reply 518 markit mlrepl 519 #nextmsg=cur 520 ;; 521 $_R) do_replyall 522 markit mlrepl 523 #nextmsg=cur 524 ;; 525 $_f) do_forw 526 markit mlrepl 527 #nextmsg=cur 528 ;; 529 530 # special viewers 531 $_H) do_viewhtml 532 #nextmsg=cur 533 ;; 534 $_V) do_urlview 535 #nextmsg=cur 536 ;; 537 $_E) do_edit 538 #nextmsg=cur 539 ;; 540 $_i) show_status | less -c 541 #nextmsg=cur 542 ;; 543 544 # quitting 545 $_q) normal_quit 546 ;; 547 548 $_X|$_Q) restore_unseen 549 preserve_ml_seq 550 exit 551 ;; 552 553 # other 554 $_S) do_sort 555 nextmsg=first 556 ;; 557 558 # navigation 559 $_up) nextmsg=first 560 ;; 561 562 $_K) nextmsg=prev 563 ;; 564 $_p|$_P) nextmsg=prev 565 ;; 566 $_n|$_J) nextmsg=next 567 ;; 568 *) nextmsg=next 569 ;; 570 571 esac 572 done 573} 574 575# summarize ml's internal sequences, for "ml -s" 576show_status() 577{ 578 echo Folder: $folder 579 for s in mlspam mldel mlrepl mlunr 580 do 581 #pick $s:first >/dev/null 2>&1 || continue 582 case $s in 583 mlrepl) echo "Have attempted a reply: (sequence $s)" ;; 584 mldel) echo "Will delete: (sequence $s)" ;; 585 mlspam) echo "Will mark as spam: (sequence $s)" ;; 586 mlunr) echo "Will mark as unseen: (sequence $s)" ;; 587 # mlkeep) echo "Will leave as seen: (sequence $s)" ;; 588 esac 589 scan $s 2>/dev/null || echo ' none' 590 done 591} 592 593apply_changes() 594{ 595 if cnt=$(seq_count mlspam) 596 then 597 echo "Marking $cnt messages as spam." 598 do_spamremove mlspam 599 fi 600 601 if cnt=$(seq_count mldel) 602 then 603 echo "Removing $cnt messages." 604 do_rmm mldel 605 fi 606 607 if cnt=$(seq_count mlunr) 608 then 609 echo "Marking $cnt messages unread." 610 mark -add -sequence $ml_unseen_seq mlunr 2>/dev/null 611 mark -sequence mlunr -delete all 612 fi 613 614 if cnt=$(seq_count mlkeep) 615 then 616 echo "Keeping $cnt messages in sequence 'mlkeep':" 617 scan mlkeep 618 fi 619} 620 621# decimal to character mappings. lesskeys lets you specify exit codes 622# from less as ascii characters, but the shell really wants them to be 623# numeric, in decimal. these definitions let you do "quit S" in 624# lesskeys, and then check against $_S here in the shell. 625char_init() 626{ 627 _A=65; _B=66; _C=67; _D=68; _E=69; _F=70; _G=71; _H=72; _I=73; 628 _J=74; _K=75; _L=76; _M=77; _N=78; _O=79; _P=80; _Q=81; _R=82; 629 _S=83; _T=84; _U=85; _V=86; _W=87; _X=88; _Y=89; _Z=90; 630 631 _a=97; _b=98; _c=99; _d=100; _e=101; _f=102; _g=103; _h=104; _i=105; 632 _j=106; _k=107; _l=108; _m=109; _n=110; _o=111; _p=112; _q=113; _r=114; 633 _s=115; _t=116; _u=117; _v=118; _w=119; _x=120; _y=121; _z=122; 634 635 _up=94; _ques=63; 636} 637 638color_init() 639{ 640 RED="$(printf \\033[1\;31m)" 641 GREEN="$(printf \\033[1\;32m)" 642 YELLOW="$(printf \\033[1\;33m)" 643 BLUE="$(printf \\033[1\;34m)" 644 PURPLE="$(printf \\033[1\;35m)" 645 CYAN="$(printf \\033[1\;36m)" 646 BOLD="$(printf \\033[1m)" 647 NORMAL="$(printf \\033[m)" 648 ESC="$(printf \\033)" 649} 650 651 652# in-line execution starts here 653 654set -u # be defensive 655 656me=${0##*/} 657 658folder=$(folder -fast) 659lesskeymap=$(mhpath +)/ml_lesskeymap 660lesskeyfileopt="--lesskey-file=$lesskeymap" 661 662if [ ! -f $lesskeymap -o $0 -nt $lesskeymap ] 663then 664 create_lesskey_map 665fi 666 667ml_unseen_seq=$(mhparam Unseen-Sequence) 668: ${ml_unseen_seq:=unseen} # default to "unseen" 669 670# check arguments 671case ${1:-} in 672 -s) show_status; exit ;; # "ml -s" 673 -a) apply_changes; exit ;; # "ml -a" 674 -k) create_lesskey_map; exit ;; # "ml -k" (should be automatic) 675 -*) usage ;; # "ml -?" 676 "") starting_seq=$ml_unseen_seq ;; # "ml" 677 *) starting_seq="$*" ;; # "ml picked ..." 678esac 679 680 681# if sequence ml isn't empty, another instance may be running 682verify_empty "Another instance of ml may be running." ml || exit 683 684# gather any user message specifications into the sequence 'ml' 685if ! mark -sequence ml -zero -add $starting_seq >/dev/null 2>&1 686then 687 echo "No messages (or message sequence) specified." 688 exit 1 689fi 690 691# uncomment for debug 692# exec 2>/tmp/ml.log; set -x 693 694# get the full list of messages, and count them 695ml_contents=$(pick ml) 696ml_len=$(echo "$ml_contents" | wc -l) 697 698# if these aren't empty, we might not have "ml a"pplied changes from 699# a previous invocation, so warn. 700verify_empty "You might want to run 'ml -a'." mldel || exit 701verify_empty "You might want to run 'ml -a'." mlspam || exit 702verify_empty "You might want to run 'ml -a'." mlunr || exit 703 704mark -sequence mlrepl -delete all 2>/dev/null 705 706# initialize 'mlkeep' to 'ml', since we assume all undeleted non-spam 707# messages will be kept. 708mark -zero -sequence mlkeep ml 709 710# save a copy of the unseen sequence, for restore if 'X' is used to quit. 711mark -zero -sequence saveunseen $ml_unseen_seq 712 713ask_init 714 715char_init 716color_init 717 718loop 719 720 721