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 filename in openSaveFiles.keys(): # only 1 win per file
openSaveFiles[filename].lift() # raise file's window
showinfo(appname, 'File already open') # else deletes odd
else:
from PyMailGui 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 from message header mappings;
3.0: decode headers per email/mime/unicode here if encoded;
3.0: caveat: large chinese characters can break '|' alignment;
"""
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
addrhdrs = ('From', 'To', 'Cc', 'Bcc') # 3.0: decode i18n specially
# compute max field sizes <= hdrsize
maxsize = {}
for key in showhdrs:
allLens = [] # too big for a list comp!
for msg in hdrmaps:
keyval = msg.get(key, ' ')
if key not in addrhdrs:
allLens.append(len(self.decodeHeader(keyval)))
else:
allLens.append(len(self.decodeAddrHeader(keyval)))
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]
if key not in addrhdrs:
keytext = self.decodeHeader(msg.get(key, ' '))
else:
keytext = self.decodeAddrHeader(msg.get(key, ' '))
msgline += ' | %-*s' % (mysize, keytext[:mysize])
msgline += '| %.1fK' % (self.mailSize(ix+1) / 1024) # 3.0: .0 optional
self.listBox.insert(END, msgline)
self.listBox.see(END) # show most recent mail=last line
def replyCopyTo(self, message):
"""
3.0: replies copy all original recipients, by prefilling
Cc header with all addreses in original To and Cc after
removing duplicates and new sender; could decode i18n addrs
here, but the view window will decode to display (and send
will reencode) and the unique set filtering here will work
either way, though a sender's i18n address is assumed to be
in encoded form in mailconfig (else it is not removed here);
empty To or Cc headers are okay: split returns empty lists;
"""
if not mailconfig.repliesCopyToAll:
# reply to sender only
Cc = ''
else:
# copy all original recipients (3.0)
allRecipients = (self.splitAddresses(message.get('To', '')) +
self.splitAddresses(message.get('Cc', '')))
uniqueOthers = set(allRecipients) - set([mailconfig.myaddress])
Cc = ', '.join(uniqueOthers)
return Cc or '?'
def formatQuotedMainText(self, message):
"""
3.0: factor out common code shared by Reply and Forward:
fetch decoded text, extract text if html, line wrap, add > quote
"""
type, maintext = self.findMainText(message) # 3.0: decoded str
if type in ['text/html', 'text/xml']: # 3.0: get plain text
maintext = html2text.html2text(maintext)
maintext = wraplines.wrapText1(maintext, mailconfig.wrapsz-2) # 2 = '> '
maintext = self.quoteOrigText(maintext, message) # add hdrs, >
if mailconfig.mysignature:
maintext = ('\n%s\n' % mailconfig.mysignature) + maintext
return maintext
def quoteOrigText(self, maintext, message):
"""
3.0: we need to decode any i18n (internationalizd) headers here too,
or they show up in email+MIME encoded form in the quoted text block;
decodeAddrHeader works on one addr or all in a comma-separated list;
this may trigger full text encoding on sends, but the main text is
also already in fully decoded form: could be in any Unicode scheme;
"""
quoted = '\n-----Original Message-----\n'
for hdr in ('From', 'To', 'Subject', 'Date'):
rawhdr = message.get(hdr, '?')
if hdr not in ('From', 'To'):
dechdr = self.decodeHeader(rawhdr) # full value
else:
dechdr = self.decodeAddrHeader(rawhdr) # name parts only
quoted += '%s: %s\n' % (hdr, dechdr)
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 PyMailCommon for viewing saved-mail file offline;
mixed with a Tk, Toplevel, or Frame, adds main mail listbox;
maps load, fetch, delete actions to local text file storage;
file opens and deletes here 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;
3.0: mail save files are now Unicode text, encoded per an encoding
name setting in the mailconfig module; this may not support worst
case scenarios of unusual or mixed encodings, but most full mail
text is ascii, and the Python 3.1 email package is partly broken;
"""
def actions(self):
return [ ('Open', self.onOpenMailFile),
('Write', self.onWriteMail),
(' ', None), # 3.0: separators
('View', self.onViewFormatMail),
('Reply', self.onReplyMail),
('Fwd', self.onFwdMail),
('Save', self.onSaveMailFile),
('Delete', self.onDeleteMail),
(' ', None),
('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: caught in thread utility
file = open(self.filename, 'r', encoding=mailconfig.fetchEncoding) # 3.0
allmsgs = file.read()
self.msglist = allmsgs.split(saveMailSeparator)[1:] # full text
self.hdrlist = list(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)) # 3.x iter ok
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)
file = open(self.filename, 'w', encoding=mailconfig.fetchEncoding) # 3.0
file.write(allmsgs)
self.msglist = keepers
self.hdrlist = list(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.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 PyMailCommon for viewing mail still on server;
mixed with a Tk, Toplevel, or Frame, adds main mail listbox;
maps load, fetch, delete actions to email server inbox;
embeds a MessageCache, which is a mailtools MailFetcher;
"""
def actions(self):
return [ ('Load', self.onLoadServer),
('Open', self.onOpenMailFile),
('Write', self.onWriteMail),
(' ', None), # 3.0: separators
('View', self.onViewFormatMail),
('Reply', self.onReplyMail),
('Fwd', self.onFwdMail),
('Save', self.onSaveMailFile),
('Delete', self.onDeleteMail),
(' ', None),
('Quit', self.quit) ]