def actionButtons(self):
return [('Cancel', self.quit), # need method to use self
('Parts', self.onParts), # PopupWindow verifies cancel
('Attach', self.onAttach),
('Send', self.onSend)] # 4E: don't pad: centered
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, inside the mailtools pkg;
4E: could ask Unicode type here instead of 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 resolveUnicodeEncodings(self):
"""
3.0/4E: to prepare for send, resolve Unicode encoding for text parts:
both main text part, and any text part attachments; the main text part
may have had a known encoding if this is a reply or forward, but not for
a write, and it may require a different encoding after editing anyhow;
smtplib in 3.1 requires that full message text be encodable per ASCII
when sent (if it's a str), so it's crucial to get this right here; else
fails if reply/fwd to UTF8 text when config=ascii if any non-ascii chars;
try user setting and reply but fall back on general UTF8 as a last resort;
"""
def isTextKind(filename):
contype, encoding = mimetypes.guess_type(filename)
if contype is None or encoding is not None: # 4E utility
return False # no guess, compressed?
maintype, subtype = contype.split('/', 1) # check for text/?
return maintype == 'text'
# resolve many body text encoding
bodytextEncoding = mailconfig.mainTextEncoding
if bodytextEncoding == None:
asknow = askstring('PyMailGUI', 'Enter main text Unicode encoding name')
bodytextEncoding = asknow or 'latin-1' # or sys.getdefaultencoding()?
# last chance: use utf-8 if can't encode per prior selections
if bodytextEncoding != 'utf-8':
try:
bodytext = self.editor.getAllText()
bodytext.encode(bodytextEncoding)
except (UnicodeError, LookupError): # lookup: bad encoding name
bodytextEncoding = 'utf-8' # general code point scheme
# resolve any text part attachment encodings
attachesEncodings = []
config = mailconfig.attachmentTextEncoding
for filename in self.attaches:
if not isTextKind(filename):
attachesEncodings.append(None) # skip non-text: don't ask
elif config != None:
attachesEncodings.append(config) # for all text parts if set
else:
prompt = 'Enter Unicode encoding name for %' % filename
asknow = askstring('PyMailGUI', prompt)
attachesEncodings.append(asknow or 'latin-1')
# last chance: use utf-8 if can't decode per prior selections
choice = attachesEncodings[-1]
if choice != None and choice != 'utf-8':
try:
attachbytes = open(filename, 'rb').read()
attachbytes.decode(choice)
except (UnicodeError, LookupError, IOError):
attachesEncodings[-1] = 'utf-8'
return bodytextEncoding, attachesEncodings
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 ',';
mailtools module handles encodings, attachments, Date, etc;
mailtools module also saves sent message text in a local file
3.0: now fully parses To,Cc,Bcc (in mailtools) instead of
splitting on the separator naively; could also use multiline
input widgets instead of simple entry; Bcc added to envelope,
not headers;
3.0: Unicode encodings of text parts is resolved here, because
it may require GUI prompts; mailtools performs the actual
encoding for parts as needed and requested;
3.0: i18n headers are already decoded in the GUI fields here;
encoding of any non-ASCII i18n headers is performed in mailtools,
not here, because no GUI interaction is required;
"""
# resolve Unicode encoding for text parts;
bodytextEncoding, attachesEncodings = self.resolveUnicodeEncodings()
# get components from GUI; 3.0: i18n headers are decoded
fieldvalues = [entry.get() for entry in self.hdrFields]
From, To, Cc, Subj = fieldvalues[:4]
extraHdrs = [('Cc', Cc), ('X-Mailer', appname + ' (Python)')]
extraHdrs += list(zip(self.userHdrs, fieldvalues[4:]))
bodytext = self.editor.getAllText()
# split multiple recipient lists on ',', fix empty fields
Tos = self.splitAddresses(To)
for (ix, (name, value)) in enumerate(extraHdrs):
if value: # ignored if ''
if value == '?': # ? not replaced
extraHdrs[ix] = (name, '')
elif name.lower() in ['cc', 'bcc']: # split on ','
extraHdrs[ix] = (name, self.splitAddresses(value))
# withdraw to disallow send during send
# caveat: might not be foolproof - user may deiconify if icon visible
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,
bodytextEncoding,
attachesEncodings),
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%s\n%s' % exc_info[:2])
printStack(exc_info)
MailSenderClass.smtpPassword = None # try again; 3.0/4E: not on self
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'
The
class in
Example 14-5
implements a cache for
already loaded messages. Its logic is split off into this file in order
to avoid further complicating list window implementations. The server
list window creates and embeds an instance of this class to interface
with the mail server and to keep track of already loaded mail headers
and full text. In this version, the server list window also keeps track
of mail fetches in progress, to avoid attempting to load the same mail
more than once in parallel. This task isn’t performed here, because it
may require GUI operations.
Example 14-5. PP4E\Internet\Email\PyMailGui\messagecache.py
"""
##############################################################################
manage message and header loads and context, but not GUI;
a MailFetcher, with a list of already loaded headers and messages;
the caller must handle any required threading or GUI interfaces;
3.0 change: use full message text Unicode encoding name in local
mailconfig module; decoding happens deep in mailtools, when a message
is fetched - mail text is always Unicode str from that point on;
this may change in a future Python/email: see Chapter 13 for details;
3.0 change: inherits the new mailconfig.fetchlimit feature of mailtools,
which can be used to limit the maximum number of most recent headers or
full mails (if no TOP) fetched on each load request; note that this
feature is independent of the loadfrom used here to limit loads to
newly-arrived mails only, though it is applied at the same time: at
most fetchlimit newly-arrived mails are loaded;
3.0 change: though unlikely, it's not impossible that a user may trigger a
new fetch of a message that is currently being fetched in a thread, simply
by clicking the same message again (msg fetches, but not full index loads,
may overlap with other fetches and sends); this seems to be thread safe here,
but can lead to redundant and possibly parallel downloads of the same mail
which are pointless and seem odd (selecting all mails and pressing View
twice downloads most messages twice!); fixed by keeping track of fetches in
progress in the main GUI thread so that this overlap is no longer possible:
a message being fetched disables any fetch request which it is part of, and
parallel fetches are still allowed as long as their targets do not intersect;
##############################################################################
"""
from PP4E.Internet.Email import mailtools
from popuputil import askPasswordWindow
class MessageInfo:
"""
an item in the mail cache list
"""
def __init__(self, hdrtext, size):
self.hdrtext = hdrtext # fulltext is cached msg
self.fullsize = size # hdrtext is just the hdrs
self.fulltext = None # fulltext=hdrtext if no TOP
class MessageCache(mailtools.MailFetcher):
"""
keep track of already loaded headers and messages;
inherits server transfer methods from MailFetcher;
useful in other apps: no GUI or thread assumptions;
3.0: raw mail text bytes are decoded to str to be
parsed with Py3.1's email pkg and saved to files;
uses the local mailconfig module's encoding setting;
decoding happens automatically in mailtools on fetch;
"""
def __init__(self):
mailtools.MailFetcher.__init__(self) # 3.0: inherits fetchEncoding
self.msglist = [] # 3.0: inherits fetchlimit
def loadHeaders(self, forceReloads, progress=None):
"""
three cases to handle here: the initial full load,
load newly arrived, and forced reload after delete;
don't refetch viewed msgs if hdrs list same or extended;
retains cached msgs after a delete unless delete fails;
2.1: does quick check to see if msgnums still in sync
3.0: this is now subject to mailconfig.fetchlimit max;
"""
if forceReloads:
loadfrom = 1
self.msglist = [] # msg nums have changed
else:
loadfrom = len(self.msglist)+1 # continue from last load
# only if loading newly arrived
if loadfrom != 1:
self.checkSynchError(self.allHdrs()) # raises except if bad
# get all or newly arrived msgs
reply = self.downloadAllHeaders(progress, loadfrom)
headersList, msgSizes, loadedFull = reply
for (hdrs, size) in zip(headersList, msgSizes):
newmsg = MessageInfo(hdrs, size)
if loadedFull: # zip result may be empty
newmsg.fulltext = hdrs # got full msg if no 'top'
self.msglist.append(newmsg)
def getMessage(self, msgnum): # get raw msg text
cacheobj = self.msglist[msgnum-1] # add to cache if fetched
if not cacheobj.fulltext: # harmless if threaded
fulltext = self.downloadMessage(msgnum) # 3.0: simpler coding
cacheobj.fulltext = fulltext
return cacheobj.fulltext
def getMessages(self, msgnums, progress=None):
"""
prefetch full raw text of multiple messages, in thread;
2.1: does quick check to see if msgnums still in sync;
we can't get here unless the index list already loaded;
"""
self.checkSynchError(self.allHdrs()) # raises except if bad
nummsgs = len(msgnums) # adds messages to cache
for (ix, msgnum) in enumerate(msgnums): # some poss already there
if progress: progress(ix+1, nummsgs) # only connects if needed
self.getMessage(msgnum) # but may connect > once
def getSize(self, msgnum): # encapsulate cache struct
return self.msglist[msgnum-1].fullsize # it changed once already!
def isLoaded(self, msgnum):
return self.msglist[msgnum-1].fulltext
def allHdrs(self):
return [msg.hdrtext for msg in self.msglist]
def deleteMessages(self, msgnums, progress=None):
"""
if delete of all msgnums works, remove deleted entries
from mail cache, but don't reload either the headers list
or already viewed mails text: cache list will reflect the
changed msg nums on server; if delete fails for any reason,
caller should forceably reload all hdrs next, because _some_
server msg nums may have changed, in unpredictable ways;
2.1: this now checks msg hdrs to detect out of synch msg
numbers, if TOP supported by mail server; runs in thread
"""
try:
self.deleteMessagesSafely(msgnums, self.allHdrs(), progress)
except mailtools.TopNotSupported:
mailtools.MailFetcher.deleteMessages(self, msgnums, progress)
# no errors: update index list
indexed = enumerate(self.msglist)
self.msglist = [msg for (ix, msg) in indexed if ix+1 not in msgnums]
class GuiMessageCache(MessageCache):
"""
add any GUI-specific calls here so cache usable in non-GUI apps
"""
def setPopPassword(self, appname):
"""
get password from GUI here, in main thread
forceably called from GUI to avoid pop ups in threads
"""
if not self.popPassword:
prompt = 'Password for %s on %s?' % (self.popUser, self.popServer)
self.popPassword = askPasswordWindow(appname, prompt)
def askPopPassword(self):
"""
but don't use GUI pop up here: I am run in a thread!
when tried pop up in thread, caused GUI to hang;
may be called by MailFetcher superclass, but only
if passwd is still empty string due to dialog close
"""
return self.popPassword