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
Next: ViewWindows: Message View Windows >>
More Python Articles
More By O'Reilly Media
|
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.
|
|