#!/usr/bin/ruby1.8 # #-- # rbmhshow : rbmhshow 0.4.2 # # Copyright (C) 2004--2005 Merlin Hughes # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program 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. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # #++ # MH mail viewer based on RubyMail. # # A flexible alternative to the standard MH `mhl' and `mhshow' # applications. MIME messages are displayed in a single continuous # `less' session, with non-plaintext components converted to text # by the appropriate utility. PGP and S/MIME signatures are # automatically validated, headers and separators are coloured, # and the prompt displays information about the current mail. # Command keys are then provided within `less' to perform common # operations, such as replying to, refiling or deleting mail, # moving to the next message, etc. In effect, rbmhshow transforms the # `less' pager into a MIME-capable MH mail environment. Operation # is (or will be) fully configurable. # # WARNING: # # This is beta software. # # Requirements are as follows: # # . Ruby: # . ruby v1.8 (http://www.ruby-lang.org/) # . rubymail v0.15 (http://www.lickey.com/rubymail/) # . merlin-let v0.0.1 (http://merlin.org/ruby/) (included) # . merlin-each v0.1.0 (http://merlin.org/ruby/) (included) # # . External: # . nmh # . less # . gnupg # . openssl # . lynx # . antiword # . fold # # Quick start: # # tar xfz rbmhshow-x.y.z.tar.gz # cp -a rbmhshow-x.y.z/bin/* ~/bin/ # cp -a rbmhshow-x.y.z/lib/* ~/.ruby/ # cat rbmhshow-x.y.z/mh-profile.rbmh >> ~/.mh_profile # show last +inbox # # then type ':r' to reply, etc # # Installation: # # Install the files from the lib/ directory of the distribution # in your Ruby library path ($:). # # Install rbmhshow and rbmhl in your PATH, make them executable, # and make sure that eacch first line points at your ruby interpreter. # # Enable the new commands in your .mh_profile: # # showproc: rbmhshow # showmimeproc: rbmhshow # mhlproc: rbmhl # # It is *not* recommended that you only install rbmhshow under # showproc and showmimeproc in your .mh_profile (although # you can): The (n)mh show commands automatically cd to your # mail folders before executing showproc, meaning that if # you try to reply to a message your editor will execute # in a different directory, preventing you, for example, # from easily adding attachments. Similarly, rbmhshow is # not told when you are executing show unseen, so it cannot # automatically check for new unseen messages. # # In addition, it is recommended that you provide the following # aliases in your .bashrc (or appropriate): # # alias show='rbmhshow' # alias next='show next' # alias prev='show prev' # # Edit your .mh_profile and add entries for MIME and color support: # # rbmh-mail-headers: X-Original-To Reply-To Sender X-Spam-Probe Replied # rbmh-mime-cache: ~/.rbmh_mime # rbmh-mime-files: /etc/mime.types ~/.mime.types # rbmh-env-COLUMNS: 80 # rbmh-mime-show-text/plain: fold -s -w $COLUMNS # rbmh-mime-show-text/html: lynx -dump -with_backspaces -stdin # rbmh-mime-show-application/msword: antiword -w $COLUMNS - # rbmh-mime-show-application/pkix-cert: openssl x509 -inform DER -text -noout # rbmh-mime-show-application/pkcs7-signature: openssl pkcs7 -inform DER # -print_certs -noout # rbmh-mime-show-message/delivery-status: fold -s -w $COLUMNS # rbmh-env-SHOW_PGM: pnmscale -xysize $(($COLUMNS*2-2)) $(($COLUMNS*3)) | # pnminvert | pgmnorm | pgmtopbm -fs | pbmtoascii -2x4 # rbmh-mime-show-image/gif: giftopnm | ppmtopgm | $SHOW_PGM # rbmh-mime-show-image/jpeg: djpeg -grayscale -scale 1/4 | $SHOW_PGM # rbmh-mime-show-type/unspecified: fold -s -w $COLUMNS # rbmh-mime-alias-application/rtf: text/rtf # rbmh-mime-alias-application/x-pkcs7-mime: application/pkcs7-mime # rbmh-mime-alias-application/x-pkcs7-signature: application/pkcs7-signature # rbmh-mime-alias-application/x-pgp-signature: application/pgp-signature # rbmh-mime-alias-image/pjpeg: image/jpeg # rbmh-mime-suffix-text/plain: txt # rbmh-mime-suffix-text/html: html # rbmh-mime-suffix-text/*: text # rbmh-mime-suffix-application/x-ruby: rb # rbmh-mime-suffix-application/pkcs7-mime: p7m # rbmh-mime-suffix-application/pkcs7-signature: p7s # rbmh-mime-suffix-application/pgp-signature: pgps # rbmh-mime-suffix-message/rfc822: mail # rbmh-mime-suffix-message/delivery-status: status # rbmh-color-date: bold yellow # rbmh-color-to: bold green # rbmh-color-cc: bold green # rbmh-color-from: bold cyan # rbmh-color-subject: bold magenta # rbmh-color-header: bold red # rbmh-color-part: white # rbmh-color-name: bold white # rbmh-color-type: cyan # rbmh-color-separator: bold cyan # rbmh-color-prompt: bold green # rbmh-color-default: white # rbmh-color-execute: white # rbmh-color-error: red # # Now, when you `show' a message, it should be displayed in # `less' with coloured headers, and MIME components nicely # converted to text. # # Add .mh_profile entries for the `less' command keys: # # rbmh-less-file: ~/.rbmh_less # rbmh-env-NEXT: next # rbmh-env-LAST: last # rbmh-less-key-:n: env(NEXT=next); env(LAST=last); next; last # rbmh-less-key-:p: env(NEXT=prev); env(LAST=first); prev; first # rbmh-less-key-:\40: show($NEXT); show($LAST) # rbmh-less-key-:g: show(last?) # rbmh-less-key-:w: store(/tmp/$FOLDER/$MESSAGE?); cur # rbmh-less-key-:c: system(comp || true); cur # rbmh-less-key-:f: system(forw $MESSAGE +$FOLDER || true); cur # rbmh-less-key-:F: system(forw -mime $MESSAGE +$FOLDER || true); cur # rbmh-less-key-:r: system(repl $MESSAGE +$FOLDER || true); cur # rbmh-less-key-:R: system(repl -mime $MESSAGE +$FOLDER || true); cur # rbmh-less-key-:+: refile; show(cur | $NEXT); show($LAST) # rbmh-less-key-:d: echo(rmm); system(rmm $MESSAGE +$FOLDER); show(cur | $NEXT); show($LAST) # rbmh-less-key-:s: echo(isspam); system(isspam $MESSAGE +$FOLDER); show(cur | $NEXT); show($LAST) # rbmh-less-key-:h: env(RBMH_HEADERS ^= full); cur # rbmh-less-key-:!: system; show(cur); show($NEXT | $LAST) # # Now, when you `show' a message, you should be able to # type `:n' to go to the next message, `:r' to reply to # a message, `:+' to refile a message, etc. # # Some documentation of some .mh_profile entries follows. # # .mh_profile entries: # # . rbmh-less-file: # Defines the less key file to create. If unspecified, a # temporary less key file will be created every time this # application is executed. # # . rbmh-less-prompt: # Defines the prompt to be displayed in the `less' pager. # This has the standard prompt format; see less(1). # Any occurrence of `%f' in the prompt is replaced by the # current message. # # . rbmh-less-key-: # Defines the commands to bind to a key in the `less' pager. # # The key can be a single character (e.g., `R') or it can # be a character sequence (e.g., `:r'). # # The command consists of a sequence of operations separated # by semicolons; each operation consists of a an operation # name followed optionally by a parameter within # parentheses; e.g., `system(rmm); next'. # # The BNF is(ish): # COMMAND := OPERATION ( `;' OPERATION )* # OPERATION := NAME ( `(' PARAMETER ')' )? # NAME := [ `a' - `z' ]+ # PARAMETER := ( PCHAR | ESCAPE )+ # PCHAR := [ ^ `(' `)' `\' `;' ] # ESCAPE := `\' [ `(' `)' `\' `;' ` ' `f' `m' `p' ] # # Significant leading or trailing whitespace, open parentheses, # close parentheses and backslashes must be escaped with backslash # if they occur in the parameter. Other escaped characters in the # parameter are replaced according to the following mapping: # # . \f # The current folder; e.g., `inbox' # . \m # The current message; e.g., `1234' # . \p # The message path; e.g., `/home/merlin/mail/inbox/1234' # # Available commands are: # # . exit(code?) # This exits the pager with an optional return code # . exec(command) # This executes the specified command, and does not continue. # . system(command) # This executes the specified command, and then continues to # execute subsequent steps, or aborts of the command fails. # . echo(parameter) # This echos the specified value. # . show(messages?) # This displays the specified message in the specified folder. # If no value is specified or the value ends with a `?', # the user is prompted with the specified value or the user's # previous entry as a default. `^D' aborts the operation; a # blank line accepts the default. Multiple messages may be # specified, separated by `|'; the first extant message # will be displayed. # . cur, next, prev, last, first # These commands are equivalent to show(...). # . refile(folder?) # This refiles the current message in the specified folder. # If no value is specified or the value ends with a `?', # the user is prompted with the specified value or the user's # previous entry as a default. `^D' aborts the operation; a # blank line accepts the default. A leading + is optional. # . store(directory?) # This stores the components of the current message in the # specified directory. If no value is specified or the value # ends with a `?', the user is prompted with the specified # value or the user's previous entry as a default. `^D' aborts # the operation; a blank line accepts the default. If the # directory already exists, the user is prompted; if it does # not exist, it is automatically created. Existing files will # not be overwritten. # . env(var = value), env(var ^= value), env(var /=) # Assigns an environment variable to the specified value, or # toggles it between unset and the specified value, or # removes the specified environment variables. For example, # env(RBMH_HEADERS ^= full) would toggle full header # display. # # Environment variables: # . RBMH_HEADERS # Controls display of full message headers. If `full', full # headers are enabled; otherwise, normal compact headers are # enabled. # # The following is a sample sequence of .mh_profile entries: # # rbmh-env-NEXT: next # rbmh-env-LAST: last # rbmh-less-key-:n: env(NEXT=next); env(LAST=last); next; last # rbmh-less-key-:p: env(NEXT=prev); env(LAST=first); prev; first # rbmh-less-key-:\40: show($NEXT); show($LAST) # rbmh-less-key-:g: show(last?) # rbmh-less-key-:w: store(/tmp/$FOLDER/$MESSAGE?); cur # rbmh-less-key-:c: system(comp || true); cur # rbmh-less-key-:f: system(forw $MESSAGE +$FOLDER || true); cur # rbmh-less-key-:F: system(forw -mime $MESSAGE +$FOLDER || true); cur # rbmh-less-key-:r: system(repl $MESSAGE +$FOLDER || true); cur # rbmh-less-key-:R: system(repl -mime $MESSAGE +$FOLDER || true); cur # rbmh-less-key-:+: refile; show(cur | $NEXT); show($LAST) # rbmh-less-key-:d: echo(rmm); system(rmm $MESSAGE +$FOLDER); show(cur | $NEXT); show($LAST) # rbmh-less-key-:s: echo(isspam); system(isspam $MESSAGE +$FOLDER); show(cur | $NEXT); show($LAST) # rbmh-less-key-:h: env(RBMH_HEADERS ^= full); cur # rbmh-less-key-:!: system; show(cur); show($NEXT | $LAST) # rbmh-less-prompt: %f ?ltlines %lt-%lb?L/%L. : byte % bB?s/%s. # .?e(END):?pB%pB\%..%t # # Typing `:n' will go to the next message, or the last if there is no next. # The next and last operations are separate (as opposed to `show(next | # last)') so that an error message will be displayed if there is no next. # Typing `:p' will go to the previous message, or the first if there is no # previous. # Typing `: ' (colon space) will show the next in the current direction; i.e., # next or previous, depending on the last command. The environment # variables NEXT and PREV are set by the `:n' and `:p' commands to enable # this. # Type `:g' will prompt for a message to show, defaulting to the last. # Typing `:w' will store the current message in a specified directory, # or /tmp// by default, and then redisplay it. The # user is prompted for the directory and message parts to store. # Typing `:f' or `:F' will forward the current message (optionally # as an attachment) and will then redisplay it. The command is # `||'ed with `true' because forw returns an error if the user # deliberately abandons the operation. The message and folder are # specified explicitly so you can have multiple concurrent rbmhshow # sessions without the different current message/folder interpretations # conflicting. # Typing `:r' or `:R' will reply to the current message (optionally # as an attachment) and will then redisplay it. # Typing `:+' will prompt for refiling the current message to a folder. # If the user aborts the operation (^D), the message will be reshown; # otherwise, the next/previous, or else the last/first message will be # displayed, depending on the direction the user is moving. # Typing `:d' will display `rmm' in the console, delete the current # message and then display the next/previous, or else the last/first # message. # Typing `:s' will similarly mark the message as spam and delete it. # Typing `:h' will toggle display of full message headers. # Typing `:!' will prompt for a command to execute, run the command # and then display the current, next/previous or last/first message. # The sample prompt above is actually the default one; it is rendered # as follows: `Message inbox:7120 lines 1-27' # # You may use the standard mh commands in place of the built-ins, to # keep with the philosophy of one command for one job. Startup time # for rbmhshow will be trivially slower, and expressing some execution # alternatives is more cumbersome: # # # Show the next or fail # rbmh-less-key-:n: exec(next) # # Show the next or else the last if there is no next # # (will also show the last if display of the next fails) # rbmh-less-key-:n: exec(next || show last) # # Show the next or else the last, without the previous problem # # (if folder is patched to return an error code on failure) # rbmh-less-key-:n: exec(\(folder next -fast > /dev/null && show || true\) # || show last) # # echo rmm (1234), then remove the message, then show the next or last # rbmh-less-key-:d: exec(echo rmm "\(\m\)" && rmm && \(next || show last\)) # # TODOC: # # . rbmh-mime-cache: ~/.rbmh_mime # . rbmh-mime-files: /etc/mime.types ~/.mime.types # . rbmh-mime-suffix-text/plain: txt # . rbmh-mime-suffix-text/html: html # . rbmh-mime-suffix-text/*: text # . rbmh-mime-suffix-application/x-ruby: rb # TODO: # # Needs to support pgp/smime decryption. # Currently stores attachments in memory; above a certain size I want to # store them in /tmp or perhaps not at all, can just stream # them from the message when they're wanted - store FD + start + finish. # Ditto for raw-format pre-verification. Want to handle verification # network access delays better. # # If I was willing to forego the table of contents at the # start, the whole thing could be done in a single streaming # pass, which I think would be cool and efficient. # Signatures would be problematic, tho. # # Newlines, wide lines in headers should be handled better (wrt colouring) # # Charsets are not handled at all... # # Use ruby-gpgme and ruby-openssl (when ready). # # TODO: env() env testing command, so I can have a toggle for image display. # # TODO: StoreCommand on a non-mime (no part number) message doesn't work.. # # TODO: man mh-sequence to be sure I'm processing the sequences right.. # # if you have no mh setup, it prompts to create your # directory but `mhpath` just hangs... # # TODO: clean up how command sequences operate; should # have ; || && work as per shell?? # # Support rbmhshow ./file ? # # How to handle show unseen:first w.r.t. next/prev/... # # How to handle show unseen? it changes as we view the messages # and may change as messages arrive! # # handle refile +a +b # # TODO: handle errors in all `mhxxx` or system("mhxxx") # # TODO: when showing a range (and maybe when not) make the less # prompt say message 99 of 1000... may need to automatically # remove entries from @range as the messages are deleted. # # TODO: move coloring into its own class.. # # TODO: handle context initialization errors better. # # TODO: give recursive ToCs in store command if a content is # rfc822mail or s/mime or.. # HISTORY: # # v0.4.2 # 2005-09-29 # Support show +, relative path in .mh_profile, # nested folders. Fix multipart/signed support. # # v0.4.1 # 2004-10-27 # Fix bug in write command # # v0.4.0 # 2004-10-22 # Added rbmhl command enabling repl and forw to use this framework # for formatting messages; set mhlproc: rbmhl in your .mh_profile # Show now goes to a specified IO, not just STDOUT # # v0.3.0 # 2004-04-06 # Proper support for viewing sequences: next/prev/first/last/cur # commands operate within the current sequence. So you can do # show -from bob (args as per pick) and :n/:p will operate correctly. # Unseen sequences are automatically reloaded when you hit the end # so you can do show unseen and :n will keep on showing new messages. # Support for concurrent viewing sessions in different shells; # each session maintains its own folder/message state, so they # don't interfere destructively. # Error handling added, command return values cleared up. # # v0.2.0 # 2004-04-05 # Store command error handling, ^C during prompt. # # v0.1.5 # 2004-02-17 # Environment variable manipulation and expansion. # # v0.1.0 # 2004-02-15 # Refactoring, mh-profile based configuration # # v0.0.1 # 2004-02-14 # Initial release, kludgy and unconfigurable. $: << "#{ENV['HOME']}/.ruby" require 'rbmh' FolderRE = /^\+(.+)$/ # should be alpha(alnum*) SingleMessageRE = /^(\d+|cur|(\S+:)?(first|last|next|prev))$/ # 1 | cur | unseen:first Version = "0.4.2" `pick -version` =~ /nmh-(\S+)/ NMH_Version = $1 ENV['MONOCHROME'] = 'true' if ENV['MONOCHROME'].nil? && !STDOUT.tty? ENV['USER_AGENT'] = "rbmhshow/#{Version} (nmh #{NMH_Version})" context = RBMH::Context.new() begin context.show(ARGV.join(' ')) loop do message = context.cur IO.popen("-") { | pipe | if pipe # parent context.pager.page(pipe, message) else # child STDERR.reopen(STDOUT) # suppress broken pipe if less quits early # should perhaps disable coredumps in case broken pipes break apps context.shower.show(message, STDOUT) end } # tidy this up somehow... break unless context.pager.execute_exit_commands(message) end rescue RBMH::Exception context.error($!.message, 0) exit 1 end