The PyMailGUI Module

In the fifth part of a six-part series, we take a look at the main module for PyMailGUI. This article is excerpted from chapter 15 of the book Programming Python, Third Edition, written by Mark Lutz (O’Reilly, 2006; ISBN: 0596009259) Copyright © 2006 O’Reilly Media, Inc. All rights reserved. Used with permission from the publisher. Available from booksellers or direct from O’Reilly Media.

PyMailGui2: The Main Module

Example 15-1 defines the file run to start PyMailGUI. It implements top-level list windows in the system—combinations of PyMailGUI’s application logic and the window protocol superclasses we wrote earlier in the text. The latter of these define window titles, icons, and close behavior.

The main documentation is also in this module, as well as command-line logic—the program accepts the names of one or more save-mail files on the command line, and automatically opens them when the GUI starts up. This is used by the PyDemos launcher, for example.

Example 15-1. PP3EInternetEmailPyMailGuiPyMailGui2.py

##############################################################
# PyMailGui 2.1 – A Python/Tkinter email client.
# A client-side Tkinter-based GUI interface for sending and receiving email.
#
# See the help string in PyMailGuiHelp2.py for usage details, and a list of
# enhancements in this version. Version 2.0 is a major rewrite. The changes
# from 2.0 (July ’05) to 2.1 (Jan ’06) were quick-access part buttons on View
# windows, threaded loads and deletes of local save-mail files, and checks for
# and recovery from message numbers out-of-synch with mail server inbox on
# deletes, index loads, and message loads.
#
# This file implements the top-level windows and interface. PyMailGui uses
# a number of modules that know nothing about this GUI, but perform related
# tasks, some of which are developed in other sections of the book. The
# mailconfig module is expanded for this program.
#
# Modules defined elsewhere and reused here:
#
# mailtools (package):
#    server sends and receives, parsing, construction (client-side chapter)
# threadtools.py
#    thread queue manangement for GUI callbacks (GUI tools chapter)
# windows.py
#    border configuration for top-level windows (GUI tools chapter)
# textEditor.py
#    text widget used in mail view windows, some pop ups           (GUI programs chapter)
#
# Generally useful modules defined here:
#
# popuputil.py
#    help and busy windows, for general use
# messagecache.py
#    a cache that keeps track of mail already loaded
# wraplines.py
#    utility for wrapping long lines of messages
# mailconfig.py
#    user configuration parameters: server names, fonts, etc.
#
# Program-specific modules defined here:
#
# SharedNames.py
#    objects shared between window classes and main file
# ViewWindows.py
#    implementation of view, write, reply, forward windows
# ListWindows.py
#    implementation of mail-server and local-file list windows
# PyMailGuiHelp.py
#    user-visible help text, opened by main window bar
# PyMailGui2.py
#    main, top-level file (run this), with main window types
#############################################################

import mailconfig, sys
from SharedNames import appname, windows
from ListWindows import PyMailServer, PyMailFile

 

#############################################################
# Top-level window classes
# View, Write, Reply, Forward, Help, BusyBox all inherit from PopupWindow
# directly: only usage; askpassword calls PopupWindow and attaches; order
# matters here!–PyMail classes redef some method defaults in the Window
# classes, like destroy and okayToExit: must be leftmost; to use
# PyMailFileWindow standalone, imitate logic in PyMailCommon.onOpenMailFile;
###############################################################

# uses icon file in cwd or default in tools dir
srvrname = mailconfig.popservername or ‘Server’

class PyMailServerWindow(PyMailServer, windows.MainWindow):
   def _ _init_ _(self):
       windows.MainWindow._ _init_ _(self, appname, srvrname)
       PyMailServer._ _init_ _(self)

class PyMailServerPopup(PyMailServer, windows.PopupWindow):
   def _ _init_ _(self):
       windows.PopupWindow._ _init_ _(self, appname, srvrnane)
       PyMailServer._ _init_ _(self)

class PyMailServerComponent(PyMailServer, windows.ComponentWindow):
   def _ _init_ _(self):
       windows.ComponentWindow._ _init_ _(self)
       PyMailServer._ _init_ _(self)

class PyMailFileWindow(PyMailFile, windows.PopupWindow):
   def _ _init_ _(self, filename):
  
windows.PopupWindow.__init__(self, appname, filename)
   PyMailFile.__init__(self, filename)

############################################################### # when run as a top-level program: create main mail-server list window ###############################################################

if __name_ _ == ‘__main__':

rootwin = PyMailServerWindow() # open server window
if sys.argv > 1:  
for savename in sys.argv[1:]:  
rootwin.onOpenMailFile(savename) # open save file windows (demo)
rootwin.lift() # save files loaded in threads
rootwin.mainloop()  

{mospagebreak title=SharedNames: Program-Wide Globals}

The module in Example 15-2 implements a shared, system-wide namespace that collects resources used in most modules in the system, and defines global objects that span files. This allows other files to avoid redundantly repeating common imports, and encapsulates the locations of package imports; it is the only file that must be updated if paths change in the future. Using globals can make programs harder to understand in general (the source of some names is not as clear), but it is reasonable if all such names are collected in a single expected module such as this one (because there is only one place to search for unknown names).

Example 15-2. PP3EInternetEmailPyMailGuiSharedNames.py

################################################################ # objects shared by all window classes and main file: program-wide globals ###############################################################

# used in all window, icon titles
appname = ‘PyMailGUI 2.1′

# used for list save, open, delete; also for sent messages file
saveMailSeparator = ‘PyMailGUI’ + (‘-‘*60) + ‘PyMailGUIn’

# currently viewed mail save files; also for sent-mail file
openSaveFiles     = {}           # 1 window per file,{name:win}

# standard library services
import sys, os, email, webbrowser
from Tkinter       import *
from tkFileDialog  import SaveAs, Open, Directory
from tkMessageBox  import showinfo, showerror, askyesno

# reuse book examples
from PP3E.Gui.Tools  import windows     # window border, exit protocols
from PP3E.Gui.Tools  import threadtools # thread callback queue checker

from PP3E.Internet.Email import mailtools # load,send,parse,build utilities
from PP3E.Gui.TextEditor import textEditor # component and pop up
# modules defined here
import mailconfig # user params: servers, fonts, etc.
import popuputil # help, busy, passwd pop-up windows
import wraplines # wrap long message lines
import messagecache # remember already loaded mail
import PyMailGuiHelp # user documentation
   

def printStack(exc_info):
    # debugging: show exception and stack traceback on stdout
   
print exc_info[0]
   
print exc_info[1]
   
import traceback
   
traceback.print_tb(exc_info[2], file=sys.stdout)

# thread busy counters for threads run by this GUI
# sendingBusy shared by all send windows, used by main window quit

loadingHdrsBusy = threadtools.ThreadCounter()   # only 1
deletingBusy    = threadtools.ThreadCounter()   # only 1
loadingMsgsBusy = threadtools.ThreadCounter()   # poss many
sendingBusy     = threadtools.ThreadCounter()   # poss many

{mospagebreak title=ListWindows: Message List Windows}

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 the mailtools package from Chapter 14 are mixed into PyMailGUI in a variety of ways. The list window classes in Example 15-3 inherit from the mailtools mail parser class, but the server list window class embeds an instance of the message cache object, which in turn inherits from the mailtools mail fetcher. The mailtools mail 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. PP3EInternetEmailPyMailGuiListWindows.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 dir s
    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 += ‘%sn’ % 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%sn’ % 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%sn’ % 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: %sn’ % (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%sn%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%sn%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%sn%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%sn%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%sn%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

{mospagebreak title=ViewWindows: Message View Windows}

Example 15-4 lists the implementation of mail view and edit windows. These windows are created in response to actions in list windows—View, Write, Reply, and Forward buttons. See the callbacks for these actions in the list window module of Example 15-3 for view window initiation calls.

As in the prior module (Example 15-3), this file is really one common class and a handful of customizations. The mail view window is nearly identical to the mail edit window, used for Write, Reply, and Forward requests. Consequently, this example defines the common appearance and behavior in the view
window superclass, and extends it by subclassing for edit windows.

Replies and forwards are hardly different from the write window here, because their details (e.g., From: and To: addresses, and quoted message text) are worked out in the list window implementation before an edit window is created.

Example 15-4. PP3EInternetEmailPyMailGuiViewWindows.py

############################################################### # Implementation of View, Write, Reply, Forward windows: one class per kind.
# Code is factored here for reuse: a Write window is a customized View window,
# and Reply and Forward are custom Write windows. Windows defined in this
# file are created by the list windows, in response to user actions. Caveat:
# the ‘split’ pop ups for opening parts/attachments feel a bit nonintuitive.
# 2.1: this caveat was addressed, by adding quick-access attachment buttons.
# TBD: could avoid verifying quits unless text area modified (like PyEdit2.0),
# but these windows are larger, and would not catch headers already changed.
# TBD: should Open dialog in write windows be program-wide? (per-window now). ###############################################################

from SharedNames import *    # program-wide global objects

############################################################### # message view window – also a superclass of write, reply, forward ###############################################################

class ViewWindow(windows.PopupWindow, mailtools.MailParser):
    """
    a custom Toplevel, with embedded TextEditor
    inherits saveParts,partsList from mailtools.MailParser
   
"""
    # class attributes
    modelabel     = ‘View’         # used in window titles
    from mailconfig import
okayToOpenParts                    # open any attachments at all?
    from mailconfig import
verifyPartOpens                    # ask before open each part?
    from mailconfig import
maxPartButtons                     # show up to this many + ‘…’
    tempPartDir    = ‘TempParts’   # where 1 selected part saved

    # all view windows use same dialog: remembers last dir
    partsDialog = Directory(title=appname + ‘: Select parts save directory’)

    def __init__(self, headermap, showtext, origmessage=None):
        """
        header map is origmessage, or custom hdr dict for writing;
        showtext is main text part of the message: parsed or custom;
        origmessage is parsed email.Message for view mail windows
        """
        windows.PopupWindow.__init__(self, appname, self.modelabel)
        self.origMessage = origmessage
        self.makeWidgets(headermap, showtext)

    def makeWidgets(self, headermap, showtext):
        """
        add headers, actions, attachments, text editor
        """
        actionsframe = self.makeHeaders(headermap)
        if self.origMessage and self.okayToOpenParts:
           
self.makePartButtons()
        self.editor  = textEditor.TextEditorComponentMinimal(self)
        myactions    = self.actionButtons()
        for (label, callback) in myactions:
           
b = Button(actionsframe, text=label, command=callback)
            b.config(bg=’beige’, relief=RIDGE, bd=2)
            b.pack(side=TOP, expand=YES, fill=BOTH)

        # body text, pack last=clip first
       
self.editor.pack
(side=BOTTOM)                        # may be multiple editors
        self.editor.setAllText
(showtext)                           # each has own content
        lines = len(showtext.splitlines( ))

        lines = min(lines + 3, mailconfig.viewheight or 20) 
        self.editor.setHeight(lines)  # else height=24, width=80
        self.editor.setWidth(80)      # or from PyEdit textConfig
        if mailconfig.viewbg:
           
self.editor.setBg(mailconfig.viewbg)                   # colors, font in mailconfig
        if mailconfig.viewfg:
            self.editor.setFg(mailconfig.viewfg)
        if mailconfig.viewfont:       # also via editor Tools menu
            self.editor.setFont(mailconfig.viewfont)

    def makeHeaders(self, headermap):
        """
        add header entry fields, return action buttons frame
        """
        top    = Frame(self); top.pack (side=TOP,   fill=X)
        left   = Frame(top);  left.pack (side=LEFT,  expand=NO,   fill=BOTH)
        middle = Frame(top);  middle.pack(side=LEFT,  expand=NO,   fill=NONE)
        right  = Frame(top);  right.pack (side=RIGHT, expand=YES,  fill=BOTH)

        # headers set may be extended in mailconfig (Bcc )
        self.userHdrs = ()
        showhdrs = (‘From’, ‘To’, ‘Cc’, ‘Subject’)
        if hasattr(mailconfig, ‘viewheaders’) and mailconfig.viewheaders:
            self.userHdrs = mailconfig.viewheaders
            showhdrs += self.userHdrs

        self.hdrFields = []
       
for header in showhdrs:
            lab = Label(middle, text=header+':’, justify=LEFT)
            ent = Entry(right)
            lab.pack(side=TOP, expand=YES, fill=X)
            ent.pack(side=TOP, expand=YES, fill=X)
            ent.insert(‘0′, headermap.get(header, ‘?’))
           
self.hdrFields.append
(ent)                           # order matters in onSend
        return left

    def actionButtons(self):    # must be method for self
        return [('Cancel',
self.destroy),                  # close view window silently
                ('Parts',
self.onParts),                  # multiparts list or the body
                ('Split', self.onSplit)]

    def makePartButtons(self):
        """
       
add up to N buttons that open attachments/parts
        when clicked; alternative to Parts/Split (2.1);
        okay that temp dir is shared by all open messages:
        part file not saved till later selected and opened;
        partname=partname is required in lambda in Py2.4;
        caveat: we could try to skip the main text part;
        """
        def makeButton(parent, text, callback): 
           
link = Button(parent, text=text, command=callback, relief=SUNKEN)
            if mailconfig.partfg: link.config(fg=mailconfig.partfg)
            if mailconfig.partbg: link.config(bg=mailconfig.partbg)
            link.pack(side=LEFT, fill=X, expand=YES)

        parts = Frame(self)
        parts.pack(side=TOP, expand=NO, fill=X)
        for (count, partname) in enumerate(self.partsList(self.origMessage)):
           
if count == self.maxPartButtons:
                makeButton(parts, ‘…’, self.onSplit)
                break
           
openpart = (lambda partname=partname: self.onOnePart(partname))
            makeButton(parts, partname, openpart)

    def onOnePart(self, partname):
        """
        locate selected part for button and save and open
        okay if multiple mails open: resaves each time selected
        we could probably just use web browser directly here
        caveat: tempPartDir is relative to cwd – poss anywhere
        caveat: tempPartDir is never cleaned up: might be large,
        could use tempfile module like HTML main text part code;
        """
        try:
           
savedir = self.tempPartDir
            message = self.origMessage
            (contype, savepath) = self.saveOnePart(savedir, partname, message)

        except:
            showerror(appname, ‘Error while writing part file’)
            printStack(sys.exc_info())
       
else:
            self.openParts([(contype, os.path.abspath(savepath))])

    def onParts(self):
        """
        show message part/attachments in pop-up window;
        uses same file naming scheme as save on Split;
        if non-multipart, single part = full body text
        """
        partnames = self.partsList(self.origMessage)
        msg = ‘n’.join(['Message parts:n'] + partnames)
        showinfo(appname, msg)

    def onSplit(self):
        """
        pop up save dir dialog and save all parts/attachments there;
        if desired, pop up HTML and multimedia parts in web browser,
        text in TextEditor, and well-known doc types on windows;
        could show parts in View windows where embedded text editor
        would provide a save button, but most are not readable text;
       
"""
        savedir =
self.partsDialog.show()     # class attr: at prior dir
        if savedir:         # tk dir chooser, not file
            try:
               
partfiles = self.saveParts(savedir, self.origMessage)
           
except:
                showerror(appname, ‘Error while writing part files’)
                printStack(sys.exc_info())

            else:
                if self.okayToOpenParts: self.openParts(partfiles)

    def askOpen(self, appname, prompt):
        if not self.verifyPartOpens:
            return True
        else:
            return askyesno
(appname, prompt)         # pop-up dialog

    def openParts(self, partfiles):
        """
        auto-open well known and safe file types, but
        only if verified by the user in a pop up; other
        types must be opened manually from save dir;
        caveat: punts for type application/octet-stream
        even if safe filename extension such as .html;
        caveat: image/audio/video could use playfile.py;
        """
        for (contype, fullfilename) in partfiles:
           
maintype  =
contype.split(‘/’)[0]                 # left side
            extension =
os.path.splitext(fullfilename)[1]     # or [-4:]
            basename  =
os.path.basename(fullfilename)        # strip dir

            # HTML and XML text, web pages, some media
            if contype in ['text/html', 'text/xml']:
                if self.askOpen(appname, ‘Open "%s" in browser?’ % basename):
                    try:
                        webbrowser.open_new(‘file://’ + fullfilename)
                    
except:
                        showerror(appname, ‘Browser failed: using editor’) 
              textEditor.TextEditorMainPopup(self, fullfilename)

            # text/plain, text/x-python, etc.
            elif maintype == ‘text':
                if self.askOpen(appname, ‘Open text part "%s"?’ % basename): 
              textEditor.TextEditorMainPopup(self, fullfilename)

            # multimedia types: Windows opens mediaplayer, imageviewer, etc.
            elif maintype in ['image', 'audio', 'video']:
                if self.askOpen(appname, ‘Open media part "%s"?’ % basename):
                    try:
                       webbrowser.open_new(‘file://’ + fullfilename)
                    except:
                       showerror(appname, ‘Error opening browser’)

            # common Windows documents: Word, Adobe, Excel, archives, etc.
           
elif (sys.platform[:3] == ‘win’ and
                  maintype == ‘application’ and
                  extension in ['.doc', '.pdf', '.xls', '.zip','.tar', '.wmv']):
                   
if self.askOpen(appname, ‘Open part "%s"?’ % basename):
                        os.startfile(fullfilename)

            else: # punt!
                msg = ‘Cannot open part: "%s"nOpen manually in: "%s"’
                msg = msg % (basename, os.path.dirname(fullfilename))
                showinfo(appname, msg)

############################################################### # message edit windows – write, reply, forward ###############################################################

if mailconfig.smtpuser:         # user set in mailconfig?
    MailSenderClass = mailtools.MailSenderAuth        # login/password required
else:
    MailSenderClass = mailtools.MailSender

class WriteWindow(ViewWindow, MailSenderClass):
    """
    customize view display for composing new mail
    inherits sendMessage from mailtools.MailSender
    """
    modelabel = ‘Write’

    def __init__(self, headermap, starttext):
        ViewWindow.__init__(self, headermap, starttext)
        MailSenderClass.__init_ _(self)
        self.attaches  = []                # each win has own open dialog
        self.openDialog = None             # dialog remembers last dir

    def actionButtons(self):
        return [('Cancel', self.quit),     # need method to use self
                ('Parts', self.onParts),   # PopupWindow verifies cancel
                ('Attach', self.onAttach),
                ('Send ', self.onSend)]

    def onParts(self):
       
# caveat: deletes not currently supported
        if not self.attaches:
            showinfo(appname, ‘Nothing attached’)
       
else:
            msg = ‘n’.join(['Already attached:n'] + self.attaches)
            showinfo(appname, msg)

    def onAttach(self):
        """
        attach a file to the mail: name added
        here will be added as a part on Send;
        """
        if not self.openDialog:
           
self.openDialog = Open(title=appname + ‘: Select Attachment File’)
        filename =
self.openDialog.show()      # remember prior dir
        if filename:
           
self.attaches.append
(filename)                  # to be opened in send method

    def onSend(self):
        """
        threaded: mail edit window send button press
        may overlap with any other thread, disables none but quit
        Exit,Fail run by threadChecker via queue in after callback
        caveat: no progress here, because send mail call is atomic
        assumes multiple recipient addrs are separated with ‘;’

        caveat: should parse To,Cc,Bcc instead of splitting on ‘;’,
        or use a multiline input widgets instead of simple entry;
        as is, reply logic and GUI user must avoid embedded ‘;’
        characters in addresses – very unlikely but not impossible;
        mailtools module saves sent message text in a local file
        """
        fieldvalues = [entry.get() for entry in self.hdrFields]
        From, To, Cc, Subj = fieldvalues[:4]
        extraHdrs  = [('Cc', Cc), ('X-Mailer', appname + ' (Python)')]
        extraHdrs += zip(self.userHdrs, fieldvalues[4:])
        bodytext = self.editor.getAllText()

        # split multiple recipient lists, fix empty fields
        Tos = To.split(‘;’)                # split to list
        Tos = [addr.strip()
for addr in Tos]                           # spaces around
        for (ix, (name, value))
in enumerate(extraHdrs):
           
if value:                    # ignored if ”
                if value == ‘?':         # ? not replaced
                    extraHdrs[ix] = (name, ”)
                elif name.lower( ) in ['cc', 'bcc']:
                    values = value.split(‘;’)
                    extraHdrs[ix] = (name, [addr.strip() for addr in values])

        # withdraw to disallow send during send
        # caveat: withdraw not foolproof- user may deiconify
        self.withdraw()
        self.getPassword()      # if needed; don’t run pop up in send thread!
        popup = popuputil.BusyBoxNowait(appname, ‘Sending message’)
        sendingBusy.incr()
        threadtools.startThread(
           
action = self.sendMessage,
            args   = (From, Tos, Subj, extraHdrs, bodytext, self.attaches,
                saveMailSeparator),
            context = (popup,),
            onExit  = self.onSendExit,
            onFail = self.onSendFail)

    def onSendExit(self, popup):
        # erase wait window, erase view window, decr send count
        # sendMessage call auto saves sent message in local file
        # can’t use window.addSavedMails: mail text unavailable
        popup.quit()
        self.destroy()
        sendingBusy.decr()

        # poss when opened, / in mailconfig
        sentname = os.path.abspath(mailconfig.sentmailfile)     # also expands ‘.’
        if sentname in
openSaveFiles.keys():         # sent file open?
           
window =
openSaveFiles[sentname]       # update list,raise
            window.loadMailFileThread()

    def onSendFail(self, exc_info, popup):
        # pop-up error, keep msg window to save or retry, redraw actions frame
        popup.quit()
        self.deiconify()
        self.lift()
        showerror(appname, ‘Send failed: n%sn%s’ % exc_info[:2])
        printStack(exc_info)
        self.smtpPassword = None       # try again
        sendingBusy.decr()

    def askSmtpPassword(self):
        """
        get password if needed from GUI here, in main thread
        caveat: may try this again in thread if no input first
        time, so goes into a loop until input is provided; see
        pop paswd input logic for a nonlooping alternative
        """
       
password = ”
        while not password:
            prompt = (‘Password for %s on %s?’ %
                     (self.smtpUser, self.smtpServerName))
            password = popuputil.askPasswordWindow(appname, prompt)
        return password

class ReplyWindow(WriteWindow):
    """
    customize write display for replying
    text and headers set up by list window
    """
    modelabel = ‘Reply’

class ForwardWindow(WriteWindow):
    """
    customize reply display for forwarding
    text and headers set up by list window
    """
    modelabel = ‘Forward’

Please check back tomorrow for the conclusion to this article.

[gp-comments width="770" linklove="off" ]
antalya escort bayan antalya escort bayan