I finally have a mail client that is, in many ways, better than
less(1) for reading email.  It has tagging, searching, index display,
reasonable message display, and it runs reasonably fast (more than an
order of magnitude faster than Pine or mutt) but no replying or
email-writing functionality yet.  Still, it's made a bunch of my past
email noticeably more accessible.

Here's the history:

Well, back in 1999, I started writing on kragen-tol about how I wanted
a better mail client, which I called "ecommit", which (due to current
circumstances) expands to "electronic communication for the
information technologist":
    <[EMAIL PROTECTED]>
  "vaporware ecommit"
    <[EMAIL PROTECTED]>

(Sorry to not include URLs for all these old mailing list posts; I'm
off the 'net at the moment and don't have the URL structure of my mail
archives written down.)

I've spent some time writing programs that help me read my email:
  Grovelmail, in 2000, forwarded selected messages to my pager:
    "grovelmail"
      <[EMAIL PROTECTED]>
    "grovelmail CORRECTION"
      <[EMAIL PROTECTED]>
  mailmsg.py, in 2001, was a sort of MUA that used Emacs's
  outline-mode for its UI.  (Dave Winer was very unimpressed when he
  saw it.)
    "a novel toy MUA"
      <[EMAIL PROTECTED]>
    "outline-based mailreader is usable"
      <[EMAIL PROTECTED]>
    "newer mailmsg.py"
      <[EMAIL PROTECTED]>
    "the current iteration of my MUA"
      <[EMAIL PROTECTED]>
  In 2002, I wrote some elisp macros that interface with mailmsg.py
  (because I'd given up using it for reading email and was just using
  it for sending):
    "my email composition system"
      <[EMAIL PROTECTED]>
  Four months later, in 2002, I wrote a program to burn a CD of
  synthesized speech from a mailbox file, so that I could listen to
  the CD in transit:
    "reading email with a CD player"
      <[EMAIL PROTECTED]>
  The same month, I wrote 'syncmaildir', a small program to
  synchronize maildirs with rsync, so that I could read mail offline
  with mutt:
    "synchronizing maildirs with rsync"
      <[EMAIL PROTECTED]>
    "updated syncmaildir"
      <[EMAIL PROTECTED]>
  In 2003, I wrote a prototype with ZODB and PyGTK that displayed a
  message index:
    "beginnings of a mailreader"
      <[EMAIL PROTECTED]>
  In 2004, I wrote a "quick-and-dirty prototype of query-based
  mailreading" which I called "mailquery" --- it's somewhat like
  mboxgrep with fielded search:
    "brute-force querying mbox files"
      <[EMAIL PROTECTED]>
  In 2004, I also started writing a full-text indexer for my email,
  called "maildex.py" and then "merge.py", which had the unusual
  feature that the index files were plain ASCII text:
    "full-text search of email"
      <[EMAIL PROTECTED]>
    "full-text indexing of arbitrarily large corpuses in mbox format"
      <[EMAIL PROTECTED]>
  And then I rewrote the indexing part in C:
    "faster full-text mbox indexing"
      <[EMAIL PROTECTED]>
    "fast mbox inverted indexing in C"
      <[EMAIL PROTECTED]>
  And then I rewrote the merging part in C:
    "merging mailbox indices faster (in C)"
      <[EMAIL PROTECTED]>
  And then I optimized it some more:
    "faster mail indexing in C"
      <[EMAIL PROTECTED]>
  In 2005, I started working on the problem of indexing corpuses
  larger than virtual memory (mmap simplified the code but imposed
  this limitation) but I don't think I ever hooked the indexing code 
  up to it:
    "sliding file windows over an mbox (for full-text indexing)"
      <[EMAIL PROTECTED]>
  In 2006, because I didn't have my own laptop and Bea's didn't have a
  C compiler, I started working on a new text indexer that supported
  mailbox files and used a novel underlying data structure:
    "full-text indexing with hash hints (Bloom-filter-like) (implementation)"
      <[EMAIL PROTECTED]>
    "my new full-text indexer"
      <[EMAIL PROTECTED]>
  I also created a CGI front-end to mailmsg.py so I could send email
  from "cibercafe" machines without giving them my password:
    "uploading mail batches"
      <[EMAIL PROTECTED]>
  
In March of this year, I started writing yet another MUA, but it still
hasn't gotten good enough to post to kragen-hacks.  I've continued
working on it on and off: according to darcs, I've committed 79
patches to it, in March, April, June, September, and October.
However, it *is* good enough that I have been using it a lot, so I'm
posting it anyway.

There are dozens of things wrong with it; here are a few of the most
egregious:

- it uses curses.  Isn't that bad enough by itself?
- at some times, hitting ^C aborts the current time-consuming operation;
  at other times, it dumps you out of the program.  This is a bummer if 
  that operation just finished.
- it completely fails to handle screen resizes, sometimes crashing
- the editing mode (for tags and searches) is a mode
- searching (with '/') is completely undocumented, but searching for
  foo bar finds messages containing both 'foo' and 'bar', while
  searching for foo @bar @baz finds messages containing 'foo' and
  tagged with both 'bar' and 'baz'.  [EMAIL PROTECTED] is a useful search.
- many of the other commands are undocumented too (like left-arrow to see the
  message index, 'f' to save the current message, '\' to toggle source view,
  's' to toggle the tag 'spam')
- there's no way to scroll up.  Can you believe I've been working on
  it (and using it) for months without adding a scroll-up command?
- the editing mode (for tags and searches) has a terrible UI (inherited
  from the curses.textpad module, so at least I didn't have to write it)
- it's slower than less(1) at going to the end of a mailbox file (5
  minutes for a 1GB mailbox, in contrast to less's <200ms)
- it wastes screen real estate by displaying only one message
- because it uses curses, it pretty much sucks at charset handling.
- it uses blinking text.  Christ.  Blinking text.
- there are lots of internal problems too: lots of duplicated code,
  overlong routines, misnamed classes, use of undocumented interfaces,
  inconsistent naming, overcomplicated code, stamp coupling, total
  failure to comment code, functions that ought to be methods,
  surprising return values, magic numbers, inefficient algorithms, and
  so on.

So you can see why I've put off posting it to kragen-hacks for more
than six months, and am prepending two pages of apologies to it.

#!/usr/bin/python
import curses, time, cgitb, sys, email, mailbox, re, os, curses.textpad, cPickle

# Embarrassingly ugly and fairly minimal mail reader.
# TODO:
# - CLEAN UP MESSY AND DUPLICATED CODE
# - parse search terms only once, into search term objects.
# D go to previous message
# - display headers (well, minimally happening now)
#   D in default display, display only person's name, not email address (or
#     vice versa)
#   D make subject not wrap onto subsequent lines
#   D provide a command for full message headers display
# D speed up index display!
# D scroll around message
# - take other actions such as bounce, approve, reply, or tag
#   D got a primitive 'tag' function
#   D add some amount of filtering
# - refactor:
#   - don't use email.Message for parsing?
#   - remove duplication among many redraw() calls
#     - now it's down to two
# - make it possible to display multiple messages
# D handle IndexError without crashing
# - handle other exceptions without crashing
# - cache message summary data on disk?
# - adjust to screen resizing
#   - probably not possible without modifying Python curses binding to
#     support resizeterm(3NCURSES).  Maybe use ctypes?
#     - how does Urwid do it?
# - display multiple messages at once
# - interface with mailman code
# - search
#   - ok, faster search, using an index!
# D make it lazier about loading messages
# D support fielded searches (to:kragen s:[silk]) for better speed and accuracy
#   X done in a really crappy way
# D just use file.read() for .as_string()
# - implement command-line history for searches
# - how about page-up and page-down (KEY_NPAGE, KEY_PPAGE) to display next
#   or previous summary page?  Searches are now fast enough that's worthwhile...
# - how about message history? ('go to last-seen message')
# - make stuff more concurrent so as to prefetch search results and message
#   metadata
# - handle ^C while waiting for keystroke sanely?  May require
#   becoming more event-driven, since it's possible to ^C the "while
#   1:" and things like that.
# - display menu letters in different color rather than in []
# - support initial backward searches ([EMAIL PROTECTED] RET)
# - maybe transcode for display charsets like ISO-8859-1?
#   - both in contents and in =?ISO-8859-1?Q?=BFno=3F?=
# - come up with a way to go to the end of the mailbox quickly!

# Mail parsing performance on my 1.1GHz laptop is on the order of 2
# megabytes and 200 messages per second.  For my current email, it
# uses 462 bytes of virtual memory per message. It used to use only 19
# bytes per message, but I wanted to be able to do some kinds of
# searches quickly, without reading and reparsing the entire mailbox
# again.

# So, once it's fully parsed my nearly-1-gig mailbox, it needs less
# than 50MB of virtual memory, which is good.  However, it needed
# something like 7 minutes of CPU time to do that, which is still too
# slow --- if it started out displaying the last message instead of
# the first, I'd be happy.

# OK, now I pickle the current state when the user hits '>' (pickling
# takes 4-5 seconds) and unpickle it at startup (another maybe 5
# seconds).  The pickled file is roughly half the size of the virtual
# memory footprint (18 MB in my case), so it's not a big deal.  The
# 5-second startup is still a big deal (to me), as is the potential
# for fragility.

def cargo_cult_routine(win):
    win.clear()
    win.refresh()
    curses.nl()
    curses.noecho()

class msglines:
    def __init__(self, body): self.lines = body.split('\n')
    def __getitem__(self, ii):
        if ii < len(self.lines): return self.lines[ii]
        else: return ''

# I was using UnixMailbox, but it broke on squeak-dev archives, which look
# like this:
# From johnmci at smalltalkconsulting.com  Sat May  1 00:52:54 2004
# so now I use PortableUnixMailbox instead.
class SeekableUnixMailbox(mailbox.PortableUnixMailbox):
    def tell(self): return self.seekp
    def seek(self, pointer): self.seekp = pointer

def fastparse(fp):
    wsp = re.compile(r'\s+')
    hdr = re.compile(r'([^\s:]+):\s+(.*)')
    curhdr = None
    rv = {}
    while 1:
        line = fp.readline()
        while line.endswith('\n') or line.endswith('\r'): line = line[:-1]
        h = hdr.match(line)
        if h:
            curhdr = h.group(1).lower()
            rv[curhdr] = h.group(2)
        elif wsp.match(line): rv[curhdr] += '\n' + line
        elif not line:
            return rv
        else:
            pass # probably the From line

# Identifying headers produced by various mailing list managers:
#               Mailman  Listserv  ezmlm  Yahoo Groups  Google Groups  Majordomo
# Sender        X        X         -      X             X              X
# Mailing-List  -        -         X      X             X              -
# List-Id       X        -         -      X             X              -
# List-Post     X        -         X      -             X              -

# So if we had to pick just one header to make quickly available for
# mailing-list filtering, it would be Sender, because Listserv and
# Majordomo only support Sender.  But Sender doesn't support ezmlm,
# and usually doesn't contain the actual list name; some examples:
# Sender                                            List address                
                 Software       
# [EMAIL PROTECTED]       [EMAIL PROTECTED]                   Mailman        
# [EMAIL PROTECTED]                       beowulf@beowulf.org                   
       Mailman        
# [EMAIL PROTECTED]                  [EMAIL PROTECTED]                   
Majordomo      
# [EMAIL PROTECTED]               [EMAIL PROTECTED]          Yahoo Groups   
# Vanagon Mailing List <[EMAIL PROTECTED]>  [EMAIL PROTECTED]                   
 Listserv       
# [EMAIL PROTECTED]       [EMAIL PROTECTED]  Google Groups  

# Like Sender, Mailing-List usually doesn't contain the actual list
# address.  The others (List-Id and List-Post) usually do, so I'm
# going to use List-Post.

def mintern(obj):
    try: return intern(obj)
    except TypeError: return obj

class MessageProxy:
    # 41988K 6:10
    # keys = 'from subject message-id date'.split()

    # Sender and List-Post allow identification of most mailing lists.
    # 48784K 5:40
    # keys = 'from subject message-id date sender list-post'.split()
    # 56264K 6:32 without interning; 44116K 7:05 with interning
    keys = 'from subject message-id date sender list-post to cc'.split()
    def __init__(self, fileobj):
        self.fileobj = fileobj
        self._fastparse = None
        self._msg = None
        self.cached_metadata = {}
    def __repr__(self): return '<MessageProxy %r>' % (self.__dict__,)
    def msg(self):
        if self._msg is not None: return self._msg
        self.fileobj.seek(0)
        self._msg = email.message_from_file(self.fileobj)
        return self._msg
    def fastparse(self):
        # This speeds up e.g. searching for tags in a previously
        # unread part of the mailbox by about a factor of pi:
        if self._fastparse is None:
            self.fileobj.seek(0)
            self._fastparse = fastparse(self.fileobj)
        return self._fastparse
    def __getitem__(self, key):
        if key in self.cached_metadata: return self.cached_metadata[key]
        # For efficiency, crash the program and make the programmer
        # think about the time/space tradeoffs, and fix it, instead of
        # running slowly.
        elif key not in self.keys: raise KeyError, key
        return mintern(self.fastparse()[key])
    def get(self, key, default=None):
        if self.cached_metadata.get(key) is not None:
            return self.cached_metadata[key]
        elif key not in self.keys: raise KeyError, key
        return mintern(self.fastparse().get(key, default))
    def as_string(self):
        # This is at least 10x faster than self.msg().as_string():
        self.fileobj.seek(0)
        return self.fileobj.read()
    def get_payload(self):
        # This is the only operation that routinely still reads from
        # the file, and the only operation that uses the slow
        # email.Message parser instead of fastparse:
        return self.msg().get_payload()        
    # hmm, maybe this should be a different kind of object, one with
    # the cached metadata:
    def cached_metadata_is(self, values):
        for key, value in zip(self.keys, values):
            self.cached_metadata[key] = value

class MessageListFacade:
    def __init__(self, fp):
        self.fp = fp
        # We don't really care what kind of objects self.mbox.next()
        # returns, as long as they aren't None.
        self.mbox = SeekableUnixMailbox(fp, lambda subfile: subfile)
        self.msgs = [self.mbox.tell()]
        self.metadata = []
    def __getitem__(self, index):
        self.mbox.seek(self.msgs[-1])
        while index + 1 >= len(self.msgs):
            msg = self.mbox.next()
            if msg is None:
                # Someone was unclear on the iterator protocol
                # when they created the mailbox module; should
                # have used StopIteration!
                raise IndexError(index)
            # This puts the end of each message onto self.msgs
            self.msgs.append(self.mbox.tell())
        subfile = mailbox._Subfile(self.fp,
                                   self.msgs[index], self.msgs[index+1])
        rv = MessageProxy(subfile)
        while len(self.metadata) <= index: self.metadata.append(None)
        if not self.metadata[index]:
            self.metadata[index] = tuple(map(rv.get, rv.keys))
        rv.cached_metadata_is(self.metadata[index])
        return rv
    def write_cached_metadata_to(self, cachefilename):
        try:
            newfilename = cachefilename + '.new'
            outfile = file(newfilename, 'w')
            cPickle.dump(self.metadata, outfile, 2)
            cPickle.dump(self.msgs, outfile, 2)
            outfile.close()
            os.rename(newfilename, cachefilename)
        except KeyboardInterrupt: pass
    def read_cached_metadata(self, cachefilename):
        try: infile = file(cachefilename)
        except IOError: return
        self.metadata = cPickle.load(infile)
        self.msgs = cPickle.load(infile)
        # XXX need to validate this

class tagstore:
    def __init__(self, filename=None):
        if filename is None:
            filename = os.path.join(os.environ['HOME'], '.cursmailmsgtags')
        self.file = file(filename, 'a+')
        self.tags = {}
        for line in self.file:
            fields = line.split()
            msgid = fields[0]
            self.set_tags(msgid, fields[1:])
    def __getitem__(self, msgid):
        try: return self.tags[msgid]
        except KeyError: return ('untagged',)
    def has_key(self, msgid):
        return self.tags.has_key(msgid)
    def set_tags(self, msgid, tags):
        self.tags[msgid] = tuple(tags)
    def __setitem__(self, msgid, tags):
        if tags == (): tags = ('untagged',)
        self.set_tags(msgid, tags)
        self.file.write(' '.join([msgid] + list(tags)) + '\n')
        self.file.flush()

def body(msg):
    # Whoever was designing the email.Message API was smoking crack
    payload = [msg]
    while isinstance(payload, type([])):
        payload = payload[0].get_payload()
    return payload

def add_highlighted_str(win, terms, row, astr):
    win.move(row, 0)
    while astr:
        positions = ([(len(astr), 0)] +
                     [(astr.find(term), len(term))
                       for term in get_yes_terms(terms)
                       if term in astr])
        pos, hitsize = min(positions)
        normal = astr[:pos]
        highlighted = astr[pos:pos+hitsize]
        astr = astr[pos+hitsize:]
        try:
            win.addstr(normal, curses.color_pair(bodytext))
            win.addstr(highlighted, curses.color_pair(white))
        except:
            win.addstr(row, 0, 'ERROR')

def add_wrapped_str(win, row, width, astr, terms=''):
    cur_row = row
    while 1:
        front, astr = astr[:width], astr[width:]
        add_highlighted_str(win, terms, cur_row, front)
        cur_row += 1
        if not astr: break
        if cur_row >= curses.LINES-2: break
    return cur_row

def realname(addr):
    realname, email_address = email.Utils.parseaddr(addr)
    return realname or email_address

def joinlines(datum):
    return re.compile(r'\n\s+').sub(' ', datum)

def msgdate(msg):
    try:
        date = email.Utils.parsedate(joinlines(msg['date']))
        return time.strftime('%Y-%m-%d %H:%M', date)
    except:
        return "(couldn't parse date)"

def message_id(msg):
    return msg.get('message-id', 'spam without a message id').replace(' ', '-')

yellow = 1
white = 2
bodytext = 3

def adjwidth(astr, width):
    if len(astr) < width: return astr + ' ' * (width - len(astr))
    else: return astr[:width]

def draw_hdr_line(stdscr, msg, tagstore=None):
    name = realname(msg.get('from', '(no sender)')) + ' '
    date = ' ' + msgdate(msg)
    tags = ''
    remaining_space = curses.COLS - len(name) - len(date)
    if tagstore:
        tags = (' ' + ' '.join(tagstore[message_id(msg)]))[:remaining_space]
    subj = adjwidth(joinlines(msg.get('subject', '(no subject)')),
                    remaining_space - len(tags))
    stdscr.addstr(name, curses.color_pair(white) | curses.A_BOLD)
    stdscr.addstr(subj, curses.color_pair(yellow) | curses.A_BOLD)
    stdscr.addstr(tags, curses.color_pair(yellow))
    stdscr.addstr(date, curses.color_pair(white) | curses.A_BOLD)

# If you have a procedure with ten parameters, you probably missed some.
# -- Perlis
# (but I think it's true at five)
def redraw(stdscr, tags, msglist, msgnum, lineoffset, terms, view_source):
    msg = msglist[msgnum]
    if view_source: msgbody = msg.as_string()
    else: msgbody = body(msg)

    stdscr.bkgd(' ', curses.color_pair(bodytext))
    stdscr.clear()
    stdscr.attrset(curses.color_pair(yellow) | curses.A_BOLD)
    stdscr.addstr(0, 0, ' ' * curses.COLS)
    stdscr.addstr(1, 0, ' ' * curses.COLS)
    stdscr.addstr(0, 0, "[q]uit [n]ext [t]ag")
    if msgnum != 0: stdscr.addstr(" [p]revious")
    stdscr.addstr('  ' + ' '.join(tags[message_id(msg)]))

    stdscr.move(1, 0)
    draw_hdr_line(stdscr, msg)
    stdscr.attrset(curses.color_pair(bodytext))

    lines = iter(msglines(msgbody))
    for line in range(lineoffset): lines.next() # laaame
    row = 2
    while row < curses.LINES:
        row = add_wrapped_str(stdscr, row, curses.COLS, lines.next(), terms)

def toggle_spam_tag(tags, msg):
    msgid = message_id(msg)
    curtags = tags[msgid]
    if 'spam' in curtags:
        tags[msgid] = tuple([tag for tag in curtags if tag != 'spam'])
    else:
        tags[msgid] = tuple([tag for tag in curtags if tag != 'untagged'] +
                            ['spam'])

def tag_message(stdscr, tags, mboxlist, message_index):
    msgid = message_id(mboxlist[message_index])
    editwindow = stdscr.derwin(1, curses.COLS, 0, 0)
    editwindow.clear()
    if tags.has_key(msgid):
        editwindow.addstr(' '.join(tags[msgid]))
    textbox = curses.textpad.Textbox(editwindow)
    tags[msgid] = textbox.edit().split()

# This approach to searching will probably always be too slow.
def search_forward(tags, search_terms, mboxlist, message_index):
    try:
        newmi = message_index
        while 1:
            newmi += 1
            if message_matches(tags, search_terms, mboxlist[newmi]):
                return newmi
    except IndexError:
        return message_index
def search_backward(tags, search_terms, mboxlist, message_index):
    while message_index > 0:
        message_index -= 1
        if message_matches(tags, search_terms, mboxlist[message_index]):
            break
    return message_index
def last_message(mboxlist, message_index):
    try:
        while 1:
            mboxlist[message_index]
            message_index += 1
    except (IndexError, KeyboardInterrupt):
        return message_index - 1

def message_contains_term(msg, tags, term):
    if term.startswith('@'): return term[1:] in tags[message_id(msg)]
    elif term.startswith('s:'): return term[2:] in msg.get('subject', '')
    elif term.startswith('f:'): return term[2:] in msg.get('from', '')
    elif term.startswith('l:'):
        term = term[2:]
        return ((term in msg.get('sender', '')) or
                (term in msg.get('list-post', '')))
    elif term.startswith('t:'):
        term = term[2:]
        return ((term in msg.get('to', '')) or
                (term in msg.get('cc', '')))
    else: return term in msg.as_string()
def get_yes_terms(search_terms):
    terms = search_terms.split()
    return [term for term in terms if not term.startswith('-')]
def get_no_terms(search_terms):
    terms = search_terms.split()
    return [term[1:] for term in terms if term.startswith('-')]
def message_matches(tags, search_terms, msg):
    # This is too slow.
    for term in get_yes_terms(search_terms):
        if not message_contains_term(msg, tags, term): return False
    for term in get_no_terms(search_terms):
        if message_contains_term(msg, tags, term): return False
    return True
def set_search(stdscr, search_terms):
    editwindow = stdscr.derwin(1, curses.COLS, 0, 0)
    editwindow.clear()
    editwindow.addstr(search_terms)
    textbox = curses.textpad.Textbox(editwindow)
    return textbox.edit()
def flash_msg(stdscr, msg):
    stdscr.move(0, 0)
    stdscr.addstr(msg,
                  curses.color_pair(yellow) | curses.A_BLINK | curses.A_BOLD)
    stdscr.refresh()
def display_message_summary(stdscr, mboxlist, tags, search_terms, 
message_index):
    # This is painfully slow sometimes, depending on your
    # search.  Thus the .refresh().
    stdscr.clear()
    try:
        for ii in range(curses.LINES - 1):
            stdscr.move(ii, 0)
            draw_hdr_line(stdscr, mboxlist[message_index], tags)
            stdscr.refresh()
            next_mi = search_forward(tags, search_terms, mboxlist,
                                     message_index)
            if next_mi == message_index: break  # no more matches!
            message_index = next_mi
    except KeyboardInterrupt:
        pass

def write_to_file(file_to_write, msg):
    fp = file(file_to_write, 'ab')
    fp.write(msg.as_string())
    fp.close()

def realmain(stdscr, argv):
    tags = tagstore()
    mboxfilename = argv[1]
    mbox = file(mboxfilename)
    mboxobj = mbox
    mboxlist = MessageListFacade(mboxobj)
    cachefilename = mboxfilename + '.cached-summary.pck'
    mboxlist.read_cached_metadata(cachefilename)
    message_index = 0
    lineoffset = 0
    search_terms = ''
    file_to_write = 'tmp.mail'
    view_source = False
    viewing_summary = False

    cargo_cult_routine(stdscr)
    curses.init_pair(yellow, curses.COLOR_YELLOW, curses.COLOR_BLUE)
    curses.init_pair(white, curses.COLOR_WHITE, curses.COLOR_BLUE)
    curses.init_pair(bodytext, curses.COLOR_BLACK, curses.COLOR_WHITE)

    redraw(stdscr, tags, mboxlist, message_index, lineoffset, search_terms,
           view_source)
    while 1:
        ch = stdscr.getch()
        if ch == ord(' '):  # scroll down
            lineoffset += 4
        elif ch in (curses.KEY_DOWN, ord('n')):  # next message
            flash_msg(stdscr, "Sorry, searching...")
            try:
                message_index = search_forward(tags, search_terms, mboxlist,
                                               message_index)
                lineoffset = 0
            except KeyboardInterrupt:
                pass
        elif ch == ord('t'): # tag
            tag_message(stdscr, tags, mboxlist, message_index)
        elif message_index != 0 and ch in (curses.KEY_UP, ord('p')): # prev msg
            flash_msg(stdscr, "Sorry, searching...")
            try:
                message_index = search_backward(tags, search_terms, mboxlist,
                                                message_index)
                lineoffset = 0
            except KeyboardInterrupt:
                pass
        elif ch == ord('d'):  # debug
            stdscr.clear()
            items = mboxlist[message_index].cached_metadata.items()
            row = add_wrapped_str(stdscr, 0, curses.COLS,
                                  repr([(k, id(v), v) for k, v in items]))
            add_wrapped_str(stdscr, row, curses.COLS, repr(mboxlist.__dict__))
            continue
        elif ch == curses.KEY_LEFT:  # message summary
            viewing_summary = True
        elif ch in (curses.KEY_RIGHT, 10):  # view message
            viewing_summary = False
        elif ch in (curses.KEY_PPAGE, curses.KEY_NPAGE):  # pgup/pgdn summary
            flash_msg(stdscr, "Sorry, searching...")
            lineoffset = 0
            if ch == curses.KEY_PPAGE: search_direction = search_backward
            else: search_direction = search_forward
            try:
                for ii in range(curses.LINES-2):
                    new_mi = search_direction(tags, search_terms,
                                              mboxlist, message_index)
                    if new_mi == message_index: break
                    message_index = new_mi
            except KeyboardInterrupt: pass
            viewing_summary = True
        elif ch == ord('/'):  # search (not incremental, sadly)
            search_terms = set_search(stdscr, search_terms)
            if not message_matches(tags, search_terms, mboxlist[message_index]):
                flash_msg(stdscr, "Sorry, searching...")
                try:
                    message_index = search_forward(tags, search_terms, mboxlist,
                                                   message_index)
                    lineoffset = 0
                except KeyboardInterrupt:
                    pass
        elif ch == ord('\\'):
            view_source = not view_source
        elif ch == ord('q'): return  # quit
        elif ch == ord('s'):
            toggle_spam_tag(tags, mboxlist[message_index])
        elif ch == ord('>'):
            flash_msg(stdscr, "Reading to end of mailbox...")
            message_index = last_message(mboxlist, message_index)
            # Note that this still updates the cached state if the user hit ^C:
            flash_msg(stdscr, "Updating cached mailbox summary...")
            start = time.time()
            mboxlist.write_cached_metadata_to(cachefilename)
        elif ch == ord('f'):  # write to file, or forward
            flash_msg(stdscr, "Writing...")
            write_to_file(file_to_write, mboxlist[message_index])
            flash_msg(stdscr, "Written   ")
            continue  # to not erase the flashing message
        if viewing_summary:
            display_message_summary(stdscr, mboxlist, tags, search_terms,
                                    message_index)
        else:
            redraw(stdscr, tags, mboxlist, message_index, lineoffset,
                   search_terms, view_source)

def main(argv):
    cgitb.enable(format="text")
    curses.wrapper(lambda stdscr: realmain(stdscr, argv))
if __name__ == '__main__': main(sys.argv)

Reply via email to