The code in
Example 14-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 (for
opening new save-file list windows). See the Open button’s callback in
this example for initiation code.
Notice that the basic mail processing operations in themailtools
package from
Chapter 13
are mixed into PyMailGUI in a variety
of ways. The list window classes in
Example 14-3
inherit from themailtools
mail parser class, but the
server list window class embeds an instance of the message cache object,
which in turn inherits from themailtools
mail fetcher. Themailtools
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 14-3. PP4E\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 (triggers full reload here).
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):
"""
an abstract widget package, with main mail listbox;
mixed in with a Tk, Toplevel, or Frame by top-level window classes;
must be customized in mode-specific subclass with actions() and other;
creates view and write windows on demand: they serve as MailSenders;
"""
# class attrs shared by all list windows
threadLoopStarted = False # started by first window
queueChecksPerSecond = 20 # tweak if CPU use too high
queueDelay = 1000 // queueChecksPerSecond # min msecs between timer events
queueBatch = 5 # max callbacks per timer event
# all windows use same dialogs: remember last dirs
openDialog = Open(title=appname + ': Open Mail File')
saveDialog = SaveAs(title=appname + ': Append Mail File')
# 3.0: avoid downloading (fetching) same message in parallel
beingFetched = set()
def __init__(self):
self.makeWidgets() # draw my contents: list,tools
if not PyMailCommon.threadLoopStarted:
# start thread exit check loop
# a timer event loop that dispatches queued GUI callbacks;
# just one loop for all windows: server,file,views can all thread;
# self is a Tk, Toplevel,or Frame: any widget type will suffice;
# 3.0/4E: added queue delay/batch for progress speedup: ~100x/sec;
PyMailCommon.threadLoopStarted = True
threadtools.threadChecker(self, self.queueDelay, self.queueBatch)
def makeWidgets(self):
# add all/none checkbtn at bottom
tools = Frame(self, relief=SUNKEN, bd=2, cursor='hand2') # 3.0: configs
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 toolbar
for (title, callback) in self.actions():
if not callback:
sep = Label(tools, text=title) # 3.0: separator
sep.pack(side=LEFT, expand=YES, fill=BOTH) # expands with window
else:
Button(tools, text=title, command=callback).pack(side=LEFT)
# add multiselect listbox with scrollbars
listwide = mailconfig.listWidth or 74 # 3.0: config start size
listhigh = mailconfig.listHeight or 15 # wide=chars, high=lines
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.config(width=listwide, height=listhigh) # 3.0: init wider
listbox.bind('', (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, pyedit=True): # do we need full TextEditor?
for msgnum in msgnums: # could be a nested def
fulltext = self.getMessage(msgnum) # fulltext is Unicode decoded
if not pyedit:
# display in a scrolledtext
from tkinter.scrolledtext import ScrolledText
window = windows.QuietPopupWindow(appname, 'raw message viewer')
browser = ScrolledText(window)
browser.insert('0.0', fulltext)
browser.pack(expand=YES, fill=BOTH)
else:
# 3.0/4E: more useful PyEdit text editor
wintitle = ' - raw message text'
browser = textEditor.TextEditorMainPopup(self, winTitle=wintitle)
browser.update()
browser.setAllText(fulltext)
browser.clearModified()
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):
"""
finish View: extract main text, popup view window(s) to display;
extracts plain text from html text if required, wraps text lines;
html mails: show extracted text, then save in temp file and open
in web browser; part can also be opened manually from view window
Split or part button; if non-multipart, other: part must be opened
manually with Split or part button; verify html open per mailconfig;
3.0: for html-only mails, main text is str here, but save its raw
bytes in binary mode to finesse encodings; worth the effort because
many mails are just html today; this first tried N encoding guesses
(utf-8, latin-1, platform dflt), but now gets and saves raw bytes to
minimize any fidelity loss; if a part is later opened on demand, it
is saved in a binary file as raw bytes in the same way;
caveat: the spawned web browser won't have any original email headers:
it may still have to guess or be told the encoding, unless the html
already has its own encoding headers (these take the form of
html tags within sections if present; none are inserted in the
html here, as some well-formed html parts have them); IE seems to
handle most html part files anyhow; always encoding html parts to
utf-8 may suffice too: this encoding can handle most types of text;
"""
for msgnum in msgnums:
fulltext = self.getMessage(msgnum) # 3.0: str for parser
message = self.parseMessage(fulltext)
type, content = self.findMainText(message) # 3.0: Unicode decoded
if type in ['text/html', 'text/xml']: # 3.0: get plain text
content = html2text.html2text(content)
content = wraplines.wrapText1(content, mailconfig.wrapsz)
ViewWindow(headermap = message,
showtext = content,
origmessage = message) # 3.0: decodes headers
# non-multipart, content-type text/HTML (rude but true!)
if type == 'text/html':
if ((not mailconfig.verifyHTMLTextOpen) or
askyesno(appname, 'Open message text in browser?')):
# 3.0: get post mime decode, pre unicode decode bytes
type, asbytes = self.findMainText(message, asStr=False)
try:
from tempfile import gettempdir # or a Tk HTML viewer?
tempname = os.path.join(gettempdir(), 'pymailgui.html')
tmp = open(tempname, 'wb') # already encoded
tmp.write(asbytes)
webbrowser.open_new('file://' + tempname)
except:
showerror(appname, 'Cannot open in browser')
def onWriteMail(self):
"""
compose a new email from scratch, without fetching others;
nothing to quote here, but adds sig, and prefills Bcc with the
sender's address if this optional header enabled in mailconfig;
From may be i18N encoded in mailconfig: view window will decode;
"""
starttext = '\n' # use auto signature text
if mailconfig.mysignature:
starttext += '%s\n' % mailconfig.mysignature
From = mailconfig.myaddress
WriteWindow(starttext = starttext,
headermap = dict(From=From, Bcc=From)) # 3.0: prefill bcc
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):
"""
finish Reply: drop attachments, quote with '>', add signature;
presets initial to/from values from mail or config module;
don't use original To for From: may be many or a listname;
To keeps name+format even if ',' separator in name;
Uses original From for To, ignores reply-to header is any;
3.0: replies also copy to all original recipients by default;
3.0: now uses getaddresses/parseaddr full parsing to separate
addrs on commas, and handle any commas that appear nested in
email name parts; multiple addresses are separated by comma
in GUI, we copy comma separators when displaying headers, and
we use getaddresses to split addrs as needed; ',' is required
by servers for separator; no longer uses parseaddr to get 1st
name/addr pair of getaddresses result: use full From for To;
3.0: we decode the Subject header here because we need its text,
but the view window superclass of edit windows performs decoding
on all displayed headers (the extra Subject decode is a no-op);
on sends, all non-ASCII hdrs and hdr email names are in decoded
form in the GUI, but are encoded within the mailtools package;
quoteOrigText also decodes the initial headers it inserts into
the quoted text block, and index lists decode for display;
"""
for msgnum in msgnums:
fulltext = self.getMessage(msgnum)
message = self.parseMessage(fulltext) # may fail: error obj
maintext = self.formatQuotedMainText(message) # same as forward
# from and to are decoded by view window
From = mailconfig.myaddress # not original To
To = message.get('From', '') # 3.0: ',' sept
Cc = self.replyCopyTo(message) # 3.0: cc all recipients?
Subj = message.get('Subject', '(no subject)')
Subj = self.decodeHeader(Subj) # deocde for str
if Subj[:4].lower() != 're: ': # 3.0: unify case
Subj = 'Re: ' + Subj
ReplyWindow(starttext = maintext,
headermap =
dict(From=From, To=To, Cc=Cc, Subject=Subj, Bcc=From))
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):
"""
finish Forward: drop attachments, quote with '>', add signature;
see notes about headers decoding in the Reply action methods;
view window superclass will decode the From header we pass here;
"""
for msgnum in msgnums:
fulltext = self.getMessage(msgnum)
message = self.parseMessage(fulltext)
maintext = self.formatQuotedMainText(message) # same as reply
# initial From value from config, not mail
From = mailconfig.myaddress # encoded or not
Subj = message.get('Subject', '(no subject)')
Subj = self.decodeHeader(Subj) # 3.0: send encodes
if Subj[:5].lower() != 'fwd: ': # 3.0: unify case
Subj = 'Fwd: ' + Subj
ForwardWindow(starttext = maintext,
headermap = dict(From=From, Subject=Subj, Bcc=From))
def onSaveMailFile(self):
"""
save selected emails to file 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: # caveat:not threaded
fulltextlist = [] # 3.0: use encoding
mailfile = open(filename, 'a', encoding=mailconfig.fetchEncoding)
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