Python
  Home arrow Python arrow Page 3 - The PyMailGUI Module
Dev Shed Forums 
Administration  
AJAX  
Apache  
BrainDump  
DHTML  
Flash  
Java  
JavaScript  
Multimedia  
MySQL  
Oracle  
Perl  
PHP  
Practices  
Python  
Reviews  
Security  
Style-Sheets  
Web Services  
XML  
Zend  
Zope  
Forums Sitemap 
IBM® developerWorks 
Sun Developer Network 
Dedicated Servers 
E-Commerce Hosting 
Linux Web Hosting 
Managed Hosting 
Small Business Hosting 
Moblin 
JMSL Numerical Library 
VPS Hosting 
Weekly Newsletter

 
Developer Updates  
Free Website Content 
 RSS  Articles
 RSS  Forums
 RSS  All Feeds
Write For Us Get Paid 
Request Media Kit
Contact Us 
Site Map 
Privacy Policy 
Support 
 USERNAME
 
 PASSWORD
 
 
  >>> SIGN UP!  
  Lost Password? 
PYTHON

The PyMailGUI Module
By: O'Reilly Media
  • Search For More Articles!
  • Disclaimer
  • Author Terms
  • Rating: 4 stars4 stars4 stars4 stars4 stars / 2
    2007-08-08

    Table of Contents:
  • The PyMailGUI Module
  • SharedNames: Program-Wide Globals
  • ListWindows: Message List Windows
  • ViewWindows: Message View Windows

  • Rate this Article: Poor Best 
      ADD THIS ARTICLE TO:
      Del.ici.ous Digg
      Blink Simpy
      Google Spurl
      Y! MyWeb Furl
    Email Me Similar Content When Posted
    Add Developer Shed Article Feed To Your Site
    Email Article To Friend
    Print Version Of Article
    PDF Version Of Article
     
     
    ADVERTISEMENT


    The PyMailGUI Module - ListWindows: Message List Windows


    (Page 3 of 4 )

    The code in Example 15-3 implements mail index list windows—for the server inbox window and for one or more local save-mail file windows. These two types of windows look and behave largely the same, and in fact share most of their code in common in a superclass. The window subclasses mostly just customize the superclass to map mail Load and Delete calls to the server or a local file.

    List windows are created on program startup (the initial server window, and possible save-file windows for command-line options), as well as in response to Open button actions in existing list windows (save-file list windows). See the Open button’s callback in this example for initiation code.

    Notice that the basic mail processing operations in themailtoolspackage from Chapter 14 are mixed into PyMailGUI in a variety of ways. The list window classes in Example 15-3 inherit from themailtoolsmail parser class, but the server list window class embeds an instance of the message cache object, which in turn inherits from themailtoolsmail fetcher. Themailtoolsmail sender class is inherited by message view write windows, not list windows; view windows also inherit from the mail parser.

    This is a fairly large file; in principle it could be split into three files, one for each class, but these classes are so closely related that it is handy to have their code in a single file for edits. Really, this is one class, with two minor extensions.

    Example 15-3. PP3E\Internet\Email\PyMailGui\ListWindows.py

    ################################################################ # Implementation of mail-server and save-file message list main windows:
    # one class per kind. Code is factored here for reuse: server and file
    # list windows are customized versions of the PyMailCommon list window class;
    # the server window maps actions to mail transferred from a server, and the
    # file window applies actions to a local file. List windows create View,
    # Write, Reply, and Forward windows on user actions. The server list window
    # is the main window opened on program startup by the top-level file; file
    # list windows are opened on demand via server and file list window "Open".
    # Msgnums may be temporarily out of sync with server if POP inbox changes.
    #
    # Changes here in 2.1:
    # -now checks on deletes and loads to see if msg nums in sync with server
    # -added up to N attachment direct-access buttons on view windows
    # -threaded save-mail file loads, to avoid N-second pause for big files
    # -also threads save-mail file deletes so file write doesn't pause GUI
    # TBD:
    # -save-mail file saves still not threaded: may pause GUI briefly, but
    # uncommon - unlike load and delete, save/send only appends the local file.
    # -implementation of local save-mail files as text files with separators
    # is mostly a prototype: it loads all full mails into memory, and so limits
    # the practical size of these files; better alternative: use 2 DBM keyed
    # access files for hdrs and fulltext, plus a list to map keys to position;
    # in this scheme save-mail files become directories, no longer readable. ############################################################

    from SharedNames import *     # program-wide global objects
    from ViewWindows import ViewWindow, WriteWindow, ReplyWindow, ForwardWindow

    ################################################################# # main frame - general structure for both file and server message lists ##############################################################

    class PyMailCommon(mailtools.MailParser): 
        """
        a widget package, with main mail listbox
        mixed in with a Tk, Toplevel, or Frame
        must be customized with actions() and other
        creates view and write windows: MailSenders
        """
        # class attrs shared by all list windows
        threadLoopStarted = False      # started by first window

        # all windows use same dialogs: remember last dirs
        openDialog = Open(title=appname + ': Open Mail File')

        saveDialog = SaveAs(title=appname + ': Append Mail File')

        def __init__(self):
            self.makeWidgets()                 # draw my contents: list,tools
            if not PyMailCommon.threadLoopStarted:            # server,file can both thread
     
    PyMailCommon.threadLoopStarted = True     # start thread exit check loop threadtools.threadChecker(self)            # just one for all windows

        def makeWidgets(self):
            # add all/none checkbtn at bottom
            tools = Frame(self)
            tools.pack(side=BOTTOM, fill=X)
            self.allModeVar = IntVar()
            chk = Checkbutton(tools, text="All")
            chk.config(variable=self.allModeVar, command=self.onCheckAll)
            chk.pack(side=RIGHT)

            # add main buttons at bottom
            for (title, callback) in self.actions():
                Button(tools, text=title, command=callback).pack(side=LEFT, fill=X)

            # add multiselect listbox with scrollbars
            mails       = Frame(self)
            vscroll     = Scrollbar(mails)
            hscroll     = Scrollbar(mails, orient='horizontal')
            fontsz      = (sys.platform[:3] == 'win' and 8) or 10               # defaults
            listbg      = mailconfig.listbg or 'white'
            listfg      = mailconfig.listfg or 'black'
           
    listfont    = mailconfig.listfont or ('courier', fontsz, 'normal')
            listbox     = Listbox(mails, bg=listbg, fg=listfg, font=listfont) 
            listbox.config(selectmode=EXTENDED)
            listbox.bind('<Double-1>', (lambda event: self.onViewRawMail()))

            # crosslink listbox and scrollbars
            vscroll.config(command=listbox.yview, relief=SUNKEN) 
            hscroll.config(command=listbox.xview, relief=SUNKEN)
            listbox.config(yscrollcommand=vscroll.set, relief=SUNKEN)
            listbox.config(xscrollcommand=hscroll.set)

            # pack last = clip first
            mails.pack(side=TOP, expand=YES, fill=BOTH)
            vscroll.pack(side=RIGHT, fill=BOTH)
            hscroll.pack(side=BOTTOM, fill=BOTH)
            listbox.pack(side=LEFT, expand=YES, fill=BOTH)
            self.listBox = listbox

        #################
        # event handlers
        #################

        def onCheckAll(self):
            # all or none click
            if self.allModeVar.get():
                self.listBox.select_set(0, END)
            else:
                self.listBox.select_clear(0, END)

        def onViewRawMail(self):
            # possibly threaded: view selected messages - raw text headers, body
            msgnums = self.verifySelectedMsgs()
            if msgnums:
               
    self.getMessages(msgnums, after=lambda: self.contViewRaw(msgnums))

        def contViewRaw(self, msgnums):
            
    for msgnum in msgnums:            # could be a nested def
                fulltext = self.getMessage(msgnum)                                  # put in ScrolledText
                from ScrolledText import ScrolledText                              # don't need full TextEditor
               
    window   = windows.QuietPopupWindow(appname, 'raw message viewer')
                browser = ScrolledText(window)
                browser.insert('0.0', fulltext)
                browser.pack(expand=YES, fill=BOTH)

        def onViewFormatMail(self):
            """
           
    possibly threaded: view selected messages – pop up formatted display
            not threaded if in savefile list, or messages are already loaded
            the after action runs only if getMessages prefetch allowed and worked
            """
            msgnums = self.verifySelectedMsgs()
            if msgnums:
               
    self.getMessages(msgnums, after=lambda: self.contViewFmt(msgnums))

        def contViewFmt(self, msgnums):
           
    for msgnum in msgnums:
                fulltext = self.getMessage(msgnum)
                message  = self.parseMessage(fulltext)
                type, content = self.findMainText(message)
                content  = wraplines.wrapText1(content, mailconfig.wrapsz)
                ViewWindow(headermap   = message,
                           showtext    = content,
                           origmessage = message)

                # non-multipart, content-type text/HTML (rude but true!)
                # can also be opened manually from Split or part button
                # if non-multipart, other: must open part manually with
                # Split or part button; no verify if mailconfig says so;

                if type == 'text/html':
                   
    if ((not mailconfig.verifyHTMLTextOpen) or
                       askyesno(appname, 'Open message text in browser?')):
                       try:
                          
    from tempfile import gettempdir # or a Tk HTML viewer?
                           tempname = os.path.join(gettempdir(), 'pymailgui.html')
                           open(tempname, 'w').write(content)
                          
    webbrowser.open_new('file://' + tempname)
                       except:
                           show_error(appname, 'Cannot open in browser')

        def onWriteMail(self):
            # compose new email
            starttext = '\n'          # use auto signature text
            if mailconfig.mysignature:
               
    starttext += '%s\n' % mailconfig.mysignature
            WriteWindow(starttext = starttext,
                        headermap = {'From': mailconfig.myaddress})

        def onReplyMail(self):
            # possibly threaded: reply to selected emails
            msgnums = self.verifySelectedMsgs()
            if msgnums:
               
    self.getMessages(msgnums, after=lambda: self.contReply(msgnums))

        def contReply(self, msgnums):
           
    for msgnum in msgnums:
                # drop attachments, quote with '>', add signature
                fulltext = self.getMessage(msgnum)
                message  = self.parseMessage(fulltext)       # may fail: error obj
                maintext = self.findMainText(message)[1]
                maintext = wraplines.wrapText1(maintext, mailconfig.wrapsz-2)   # >
                maintext = self.quoteOrigText(maintext, message)
                if mailconfig.mysignature:
                   
    maintext = ('\n%s\n' % mailconfig.mysignature) + maintext

                # preset initial to/from values from mail or config
                # don't use original To for From: may be many or listname
                # To keeps name+<addr> format unless any ';' present: separator
                # ideally, send should fully parse instead of splitting on ';'
                # send changes ';' to ',' required by servers; ',' common in name

                origfrom = message.get('From', '')
               
    ToPair   = email.Utils.parseaddr(origfrom)                        # 1st (name, addr)
                ToStr    = email.Utils.formataddr(ToPair)    # ignore Reply-to
                From     = mailconfig.myaddress
                                      # don't try 'To'
               
    Subj     = message.get('Subject', '(no subject)')
                if not Subj.startswith('Re:'):
                    Subj = 'Re: ' + Subj
                if ';' not in ToStr:  # uses separator?
                    To = ToStr        # use name+addr
                else:
                    To = ToPair[1]    # use just addr
                ReplyWindow(starttext = maintext,
                            headermap = {'From': From, 'To': To, 'Subject': Subj})

        def onFwdMail(self):
            # possibly threaded: forward selected emails
            msgnums = self.verifySelectedMsgs()

            if msgnums:
                self.getMessages(msgnums, after=lambda: self.contFwd(msgnums))

        def contFwd(self, msgnums):
           
    for msgnum in msgnums:
                # drop attachments, quote with '>', add signature
                fulltext = self.getMessage(msgnum)
                message  = self.parseMessage(fulltext)
                maintext = self.findMainText(message)[1]
                maintext = wraplines.wrapText1(maintext, mailconfig.wrapsz-2)
                maintext = self.quoteOrigText(maintext, message)
                if mailconfig.mysignature:
                   
    maintext = ('\n%s\n' % mailconfig.mysignature) + maintext

                # initial from value from config, not mail
                From = mailconfig.myaddress
                Subj = message.get('Subject', '(no subject)')
                if not Subj.startswith('Fwd: '):
                    Subj = 'Fwd: ' + Subj
                ForwardWindow(starttext = maintext,
                              headermap = {'From': From, 'Subject': Subj})

        def onSaveMailFile(self):
            """
            save selected emails for offline viewing
            disabled if target file load/delete is in progress
            disabled by getMessages if self is a busy file too
            contSave not threaded: disables all other actions
           
    """
            msgnums = self.selectedMsgs()
            if not msgnums:
                showerror(appname, 'No message selected')
            else:
                # caveat: dialog warns about replacing file
                filename = self.saveDialog.show()                            # shared class attr
                if filename:      # don't verify num msgs
                    filename =
    os.path.abspath(filename)     # normalize / to \
                    self.getMessages(msgnums,
                           
    after=lambda: self.contSave(msgnums, filename))

        def contSave(self, msgnums, filename):
            # test busy now, after poss srvr msgs load
            if (filename in openSaveFiles.keys() and                                      # viewing this file?

                openSaveFiles[filename].openFileBusy):                # load/del occurring?
                showerror(appname, 'Target file busy - cannot save')
            else:
               
    try:
                    fulltextlist = []
                    mailfile = open(filename, 'a')                                     # caveat:not threaded
                    for msgnum in msgnums:   # < 1sec for N megs
                   
    fulltext = self.getMessage(msgnum)                                 # but poss many msgs
                    if fulltext[-1] != '\n': fulltext += '\n'
                   
    mailfile.write(saveMailSeparator)
                    mailfile.write(fulltext)
                    fulltextlist.append(fulltext)
                mailfile.close()
            except:
                showerror(appname, 'Error during save')
                printStack(sys.exc_info( ))
            else:                 # why .keys(): EIBTI
                if filename in openSaveFiles.keys():         # viewing this file?
                    window = openSaveFiles[filename]                    # update list, raise
                    window.addSavedMails(fulltextlist)                # avoid file reload
                    #window.loadMailFileThread()                            # this was very slow

        def onOpenMailFile(self, filename=None):
            # process saved mail offline
            filename = filename or self.openDialog.show()        # shared class attr
            if filename:
                filename = os.path.abspath(filename)                    # match on full name
                if openSaveFiles.has_key(filename):                   # only 1 win per file
                    openSaveFiles[filename].lift()                            # raise file's window
                    showinfo(appname, 'File already open')                # else deletes odd
                else:
                    from PyMailGui2 import PyMailFileWindow              # avoid duplicate win
                    popup = PyMailFileWindow(filename)                    # new list window
                    openSaveFiles[filename] = popup                         # removed in quit
                    popup.loadMailFileThread()
                                  # try load in thread

        def onDeleteMail(self):
            # delete selected mails from server or file
            msgnums = self.selectedMsgs()     # subclass: fillIndex
            if not msgnums:                   # always verify here
                showerror(appname, 'No message selected')
            else:
                if askyesno(appname, 'Verify delete %d mails?' % len(msgnums)):
                    self.doDelete(msgnums)

        ##################
        # utility methods
        ##################

        def selectedMsgs(self):
            # get messages selected in main listbox
            selections = self.listBox.curselection()  # tuple of digit strs, 0..N-1
            return [int(x)+1 for x in selections]                  # convert to ints, make 1..N

        warningLimit = 15
        def verifySelectedMsgs(self):
            msgnums = self.selectedMsgs()
            if not msgnums:
                showerror(appname, 'No message selected')
            else:
                numselects = len(msgnums)
               
    if numselects > self.warningLimit:
                    if not askyesno(appname, 'Open %d selections?' % numselects):
                        msgnums = []
            return msgnums

        def fillIndex(self, maxhdrsize=25):
            # fill all of main listbox
            hdrmaps  = self.headersMaps()     # may be empty
            showhdrs = ('Subject', 'From', 'Date', 'To')                             # default hdrs to show
            if hasattr(mailconfig, 'listheaders'):                           # mailconfig customizes
               
    showhdrs = mailconfig.listheaders or showhdrs

            # compute max field sizes <= hdrsize
            maxsize = {}
            for key in showhdrs:
               
    allLens = [len(msg.get(key, '')) for msg in hdrmaps]
                if not allLens: allLens = [1]
                maxsize[key] = min(maxhdrsize, max(allLens))

            # populate listbox with fixed-width left-justified fields
            self.listBox.delete(0, END)       # show multiparts with *
            for (ix, msg) in enumerate(hdrmaps):
                                              # via content-type hdr
               
    msgtype = msg.get_content_maintype()                # no is_multipart yet
                msgline = (msgtype == 'multipart' and '*') or ' '
                msgline += '%03d' % (ix+1)
                for key in showhdrs:
                    mysize  = maxsize[key] 
                    keytext = msg.get(key, ' ')
                    msgline += ' | %-*s' % (mysize, keytext[:mysize])
               
    msgline += '| %.1fK' % (self.mailSize(ix+1) / 1024.0) 
                self.listBox.insert(END, msgline)
            self.listBox.see(END)     # show most recent mail=last line

        def quoteOrigText(self, maintext, message):
            quoted   = '\n-----Original Message-----\n'
            for hdr in ('From', 'To', 'Subject', 'Date'):
               
    quoted += '%s: %s\n' % (hdr, message.get(hdr, '?'))
            quoted   = quoted + '\n' + maintext
            quoted   = '\n' + quoted.replace('\n', '\n> ')
            return quoted

        ########################
        # subclass requirements 
        ########################

        def getMessages(self, msgnums,
    after):                        # used by view,save,reply,fwd
            after()                # redef if cache, thread test

        # plus okayToQuit?, any unique actions 
        def getMessage(self, msgnum): assert False                          # used by many: full mail text
        def headersMaps(self):
    assert False                   # fillIndex: hdr mappings list
        def mailSize(self, msgnum):
    assert False                   # fillIndex: size of msgnum
        def doDelete(self):
    assert False                   # onDeleteMail: delete button

    ################################################################# # main window - when viewing messages in local save file (or sent-mail file) ################################################################

    class PyMailFile(PyMailCommon):
        """
        customize for viewing saved-mail file offline
        a Tk, Toplevel, or Frame, with main mail listbox
        opens and deletes run in threads for large files

        save and send not threaded, because only append to
        file; save is disabled if source or target file busy
        with load/delete; save disables load, delete, save
        just because it is not run in a thread (blocks GUI);

        TBD: may need thread and O/S file locks if saves ever
        do run in threads: saves could disable other threads
        with openFileBusy, but file may not be open in GUI;
        file locks not sufficient, because GUI updated too;
        TBD: appends to sent-mail file may require O/S locks:
        as is, user gets error pop up if sent during load/del;
        """
        def actions(self):
           
    return [ ('View', self.onViewFormatMail),
                     ('Delete', self.onDeleteMail),
                    
    ('Write', self.onWriteMail),
                     ('Reply', self.onReplyMail),
                     ('Fwd', self.onFwdMail),
                     ('Save', self.onSaveMailFile),
                     ('Open', self.onOpenMailFile),
                     ('Quit', self.quit) ]

        def __init__(self, filename):
            # caller: do loadMailFileThread next
            PyMailCommon.__init__(self) 
            self.filename = filename 
            self.openFileBusy = threadtools.ThreadCounter()       # one per window

        def loadMailFileThread(self):
            """
            Load or reload file and update window index list;
            called on Open, startup, and possibly on Send if
            sent-mail file appended is currently open; there
            is always a bogus first item after the text split;
            alt: [self.parseHeaders(m) for m in self.msglist];
            could pop up a busy dialog, but quick for small files;

            2.1: this is now threaded--else runs < 1sec for N meg
            files, but can pause GUI N seconds if very large file;
            Save now uses addSavedMails to append msg lists for
            speed, not this reload; still called from Send just
            because msg text unavailable - requires refactoring;
            delete threaded too: prevent open and delete overlap;
            """
            if self.openFileBusy:
               
    # don't allow parallel open/delete changes
                errmsg = 'Cannot load, file is busy:\n"%s"' % self.filename
               
    showerror(appname, errmsg)
            else:
                #self.listBox.insert(END, 'loading...')         # error if user clicks
                savetitle =
    self.title()          # set by window class
                self.title(appname + ' - ' + 'Loading...')
                self.openFileBusy.incr()
                threadtools.startThread(
                    action
    =  self.loadMailFile,
                    args      = (),
                    context   = (savetitle,),
                    onExit    = self.onLoadMailFileExit,
                    onFail    = self.onLoadMailFileFail)

        def loadMailFile(self):
            # run in a thread while GUI is active
            # open, read, parser may all raise excs
            allmsgs = open(self.filename).read()
            self.msglist  = allmsgs.split(saveMailSeparator)[1:]             # full text
            self.hdrlist  = map(self.parseHeaders, self.msglist)   # msg objects

        def onLoadMailFileExit(self, savetitle):
            # on thread success
            self.title(savetitle)       # reset window title to filename
            self.fillIndex()            # updates GUI: do in main thread
            self.lift()                 # raise my window
            self.openFileBusy.decr()

        def onLoadMailFileFail(self, exc_info, savetitle):
            # on thread exception
            showerror(appname, 'Error opening "%s"\n%s\n%s' %
                               ((self.filename,) + exc_info[:2]))
            printStack(exc_info)
            self.destroy()              # always close my window?
            self.openFileBusy.decr()    # not needed if destroy

        def addSavedMails(self, fulltextlist): 
            """
            optimization: extend loaded file lists for mails
            newly saved to this window's file; in past called
            loadMailThread to reload entire file on save - slow;
            must be called in main GUI thread only: updates GUI;
            sends still reloads sent file if open: no msg text;
           
    """
            self.msglist.extend(fulltextlist) 
            self.hdrlist.extend(map(self.parseHeaders, fulltextlist))  
            self.fillIndex()
            self.lift()

        def doDelete(self, msgnums):
            """
            simple-minded, but sufficient: rewrite all
            nondeleted mails to file; can't just delete
            from self.msglist in-place: changes item indexes;
            Py2.3 enumerate(L) same as zip(range(len(L)), L)
           
    2.1: now threaded, else N sec pause for large files
            """
            if self.openFileBusy:
               
    # dont allow parallel open/delete changes
                errmsg = 'Cannot delete, file is busy:\n"%s"' % self.filename
                showerror(appname, errmsg)
           
    else:
                savetitle = self.title() 
                self.title(appname + ' - ' + 'Deleting...')
                self.openFileBusy.incr() 
                threadtools.startThread(
                   
    action     = self.deleteMailFile,
                    args       = (msgnums,),
                    context    = (savetitle,),
                    onExit     = self.onDeleteMailFileExit,
                    onFail     = self.onDeleteMailFileFail)

        def deleteMailFile(self, msgnums):
            # run in a thread while GUI active 
            indexed = enumerate(self.msglist)
            keepers = [msg for (ix, msg) in indexed if ix+1 not in msgnums]
            allmsgs = saveMailSeparator.join([''] + keepers)
            open(self.filename, 'w').write(allmsgs)
            self.msglist = keepers
            self.hdrlist = map(self.parseHeaders, self.msglist)

        def onDeleteMailFileExit(self, savetitle):
            self.title(savetitle)
            self.fillIndex()      # updates GUI: do in main thread
            self.lift()           # reset my title, raise my window 
            self.openFileBusy.decr()

        def onDeleteMailFileFail(self, exc_info, savetitle):
            showerror(appname, 'Error deleting "%s"\n%s\n%s' %
                              
    ((self.filename,) + exc_info[:2]))
            printStack(exc_info)
            self.destroy()               # always close my window?
            self.openFileBusy.decr()     # not needed if destroy

        def getMessages(self, msgnums, after):
            """
            used by view,save,reply,fwd: file load and delete
            threads may change the msg and hdr lists, so disable
            all other operations that depend on them to be safe;
            this test is for self: saves also test target file;
            """
            if self.openFileBusy:
               
    errmsg = 'Cannot fetch, file is busy:\n"%s"' % self.filename
               
    showerror(appname, errmsg)
            else:
                after()                  # mail already loaded

        def getMessage(self, msgnum):
            return self.msglist[msgnum-1]   # full text of 1 mail

        def headersMaps(self):
            return self.hdrlist             # email.Message objects

        def mailSize(self, msgnum):
           
    return len(self.msglist[msgnum-1])

        def quit(self):
            # don't destroy during update: fillIndex next
            if self.openFileBusy:
               
    showerror(appname, 'Cannot quit during load or delete')
            else:
               
    if askyesno(appname, 'Verify Quit Window?'):
                    # delete file from open list
                    del openSaveFiles[self.filename]
                    Toplevel.destroy(self)

    ################################################################ # main window - when viewing messages on the mail server ###############################################################

    class PyMailServer(PyMailCommon):
        """
        customize for viewing mail still on server
        a Tk, Toplevel, or Frame, with main mail listbox
        maps load, fetch, delete actions to server inbox
        embeds a MessageCache, which is a MailFetcher
        """
        def actions(self):
           
    return [ ('Load', self.onLoadServer),
                     ('View', self.onViewFormatMail),
                    ('Delete', self.onDeleteMail),
                    ('Write', self.onWriteMail),
                    ('Reply', self.onReplyMail),
                    ('Fwd',   self.onFwdMail),
                    ('Save', self.onSaveMailFile),
                    ('Open', self.onOpenMailFile),
                    ('Quit',  self.quit) ]

        def __init__(self):
            PyMailCommon.__init_ _(self) 
            self.cache = messagecache.GuiMessageCache() 
      # embedded, not inherited
          
    #self.listBox.insert(END, 'Press Load to fetch mail')

        def makeWidgets(self):       # help bar: main win only
            self.addHelp() 
            PyMailCommon.makeWidgets(self)

        def addHelp(self):
            msg = 'PyMailGUI - a Python/Tkinter email client (help)'
            title = Button(self, text=msg) 
            title.config(bg='steelblue', fg='white', relief=RIDGE)
            title.config(command=self.onShowHelp)
            title.pack(fill=X)

        def onShowHelp(self):
            """
            load,show text block string
            could use HTML and web browser module here too
            but that adds an external dependency
            """
            from PyMailGuiHelp import helptext 
            popuputil.HelpPopup(appname, helptext, showsource=self.onShowMySource)

        def onShowMySource(self, showAsMail=False):
            # display my sourcecode file, plus imported modules here & elsewhere
            import PyMailGui2, ListWindows, ViewWindows, SharedNames
            from PP3E.Internet.Email.mailtools import (    # mailtools now a pkg
                
    mailSender, mailFetcher, mailParser) # can't use * in def
           
    mymods = (
                PyMailGui2, ListWindows, ViewWindows, SharedNames,
                PyMailGuiHelp, popuputil, messagecache, wraplines,
                mailtools, mailFetcher, mailSender, mailParser,
                mailconfig, threadtools, windows, textEditor)
           
    for mod in mymods:
                source = mod.__file__
                if source.endswith('.pyc'):
                    source = source[:-4] + '.py'
            # assume in same dir, .py
               
    if showAsMail:
                # this is a bit cheesey...
                code   = open(source).read()
                user   = mailconfig.myaddress
                hdrmap = {'From': appname, 'To': user, 'Subject': mod.__name__}
                ViewWindow(showtext=code,
                         
    headermap=hdrmap, 
               origmessage=email.Message.Message())
            else:

                # more useful text editor
                wintitle = ' - ' + mod.__name__
                textEditor.TextEditorMainPopup(self, source, wintitle)

        def onLoadServer(self, forceReload=False):
            """
            threaded: load or reload mail headers list on request
            Exit,Fail,Progress run by threadChecker after callback via queue
            may overlap with sends, disables all but send
            could overlap with loadingmsgs, but may change msg cache list
            forceReload on delete/synch fail, else loads recent arrivals only;
           
    2.1: cache.loadHeaders may do quick check to see if msgnums
            in synch with server, if we are loading just newly arrived hdrs;
            """
            if loadingHdrsBusy or deletingBusy or loadingMsgsBusy:
               
    showerror(appname, 'Cannot load headers during load or delete')
           
    else:
                loadingHdrsBusy.incr()
                self.cache.setPopPassword(appname)
    # don't update GUI in the thread!
                popup = popuputil.BusyBoxNowait(appname, 'Loading message headers')
                threadtools.startThread(
                   
    action      = self.cache.loadHeaders,
                    args        = (forceReload,),
                    context     = (popup,),
                    onExit      = self.onLoadHdrsExit,
                    onFail      = self.onLoadHdrsFail,

                    onProgress  = self.onLoadHdrsProgress)

        def onLoadHdrsExit(self, popup): 
            self.fillIndex()
            popup.quit()
            self.lift()
            loadingHdrsBusy.decr()

        def onLoadHdrsFail(self, exc_info, popup):
            popup.quit()
            showerror(appname, 'Load failed: \n%s\n%s' % exc_info[:2])
            printStack(exc_info)         # send stack trace to stdout
            loadingHdrsBusy.decr()
            if exc_info[0] == mailtools.MessageSynchError:
             # synch inbox/index
               
    self.onLoadServer
    (forceReload=True)                   # new thread: reload
            else:
                self.cache.popPassword =
    None              # force re-input next time

        def onLoadHdrsProgress(self, i, n, popup):
            popup.changeText('%d of %d' % (i, n))

        def doDelete(self, msgnumlist):
            """
            threaded: delete from server now - changes msg nums;
            may overlap with sends only, disables all except sends;
           
    2.1: cache.deleteMessages now checks TOP result to see
           
    if headers match selected mails, in case msgnums out of
            synch with mail server: poss if mail deleted by other client,
            or server deletes inbox mail automatically - some ISPs may
            move a mail from inbox to undeliverable on load failure;
            """
            if loadingHdrsBusy or deletingBusy or loadingMsgsBusy:
               
    showerror(appname, 'Cannot delete during load or delete')
           
    else:
                deletingBusy.incr()
                popup = popuputil.BusyBoxNowait(appname, 'Deleting selected mails')
                threadtools.startThread(
                   
    action     = self.cache.deleteMessages,
                    args       = (msgnumlist,),
                    context    = (popup,),
                    onExit     = self.onDeleteExit,
                    onFail     = self.onDeleteFail,
                    onProgress = self.onDeleteProgress)

        def onDeleteExit(self, popup): 
            self.fillIndex()           # no need to reload from server
            popup.quit()               # refill index with updated cache
            self.lift()                # raise index window, release lock
            deletingBusy.decr()

        def onDeleteFail(self, exc_info, popup):
            popup.quit()
            showerror(appname, 'Delete failed: \n%s\n%s' % exc_info[:2])
            printStack(exc_info)
            deletingBusy.decr()         # delete or synch check failure
            self.onLoadServer
    (forceReload=True)                  # new thread: some msgnums changed

        def onDeleteProgress(self, i, n, popup):
            popup.changeText('%d of %d' % (i, n))

        def getMessages(self, msgnums, after):
            """
            threaded: prefetch all selected messages into cache now
            used by save, view, reply, and forward to prefill cache
            may overlap with other loadmsgs and sends, disables delete
            only runs "after" action if the fetch allowed and successful;
           
    2.1: cache.getMessages tests if index in synch with server,
            but we only test if we have to go to server, not if cached;
            """
            if loadingHdrsBusy or deletingBusy:
                showerror(appname, 'Cannot fetch message during load or delete')
           
    else:
                toLoad = [num for num in msgnums if not self.cache.isLoaded(num)]
                if not toLoad:
                   
    after()      # all already loaded
                    return       # process now, no wait pop up
                else:
                    loadingMsgsBusy.incr()
                    from popuputil import BusyBoxNowait
                    popup = BusyBoxNowait(appname, 'Fetching message contents')
                    threadtools.startThread(
                       
    action      = self.cache.getMessages,
                        args        = (toLoad,),
                        context     = (after, popup),
                        onExit      = self.onLoadMsgsExit,
                        onFail      = self.onLoadMsgsFail,
                       
    onProgress  = self.onLoadMsgsProgress)

        def onLoadMsgsExit(self, after, popup):
            popup.quit()
            after()
            loadingMsgsBusy.decr()      # allow others after afterExit done

        def onLoadMsgsFail(self, exc_info, after, popup):
            popup.quit()
            showerror(appname, 'Fetch failed: \n%s\n%s' % exc_info[:2])
            printStack(exc_info)
            loadingMsgsBusy.decr()
            if exc_info[0] == mailtools.MessageSynchError:
            # synch inbox/index
               
    self.onLoadServer(forceReload=True)                  # new thread: reload

        def onLoadMsgsProgress(self, i, n, after, popup):
            popup.changeText('%d of %d' % (i, n))

        def getMessage(self, msgnum):
            return self.cache.getMessage (msgnum)                    # full mail text

        def headersMaps(self):
            return map(self.parseHeaders, self.cache.allHdrs())       # email.Message objs

        def mailSize(self, msgnum):
            return self.cache.getSize(msgnum)

        def okayToQuit(self):
            # any threads still running?
            filesbusy = [win for win in openSaveFiles.values() if win.openFileBusy]
            busy = loadingHdrsBusy or deletingBusy or sendingBusy or loadingMsgsBusy
            busy = busy or filesbusy
            return not busy

    More Python Articles
    More By O'Reilly Media


       · This article is an excerpt from the book "Programming Python, Third Edition,"...
     

    Buy this book now. This article is excerpted from chapter 15 of the book Programming Python, Third Edition, written by Mark Lutz (O'Reilly, 2006; ISBN: 0596009259). Check it out today at your favorite bookstore. Buy this book now.

       

    PYTHON ARTICLES

    - SSH with Twisted
    - Mobile Programming in Python using PyS60: UI...
    - Python: Count on It
    - Python Strings: Spinning Yarns
    - Python: More Fun with Strings
    - Python: Stringing You Along
    - Python Operators
    - Bluetooth Programming in Python: Network Pro...
    - Python Sets
    - Python Conditionals, Lists, Dictionaries, an...
    - Python: Input and Variables
    - Introduction to Python Programming
    - Mobile Programming in Python using PyS60: Ge...
    - Bluetooth Programming using Python
    - Finishing the PyMailGUI Client: User Help To...