File: pymailgui-products/unzipped/PyMailGui-PP4E/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, parent=None): """ get password from GUI here, in main thread: forceably called from GUI to avoid popups in threads; Dec2015: extend to try local password file first in GUI thread (if set and exists), before asking user in popup; this effectively disables mailfetcher class's getPassword(), which really wasn't designed ideally for use in a GUI context; Dec2015, subtlety: it's possible that the password could be blank due to a later reload failure after the initial load (e.g., on a server connection error); in this case, a view/reply/fwd/delete could be attempted on a listed message, and pswd must be asked anew in GUI; by design loads and deletes can't overlap with any other POP activity, but multiple fetches (for view/reply/fwd/rawtext) can be running in parallel both with each other and with the main GUI thread; UPDATE: reload failures no longer clear the password (this is moot); """ if self.popPassword is None: # Dec2015: allow '' pswds try: self.getPasswordFromFile() except: import sys self.trace('%s %s' % (sys.exc_info()[0], sys.exc_info()[1])) prompt = 'Password for %s on %s?' % (self.popUser, self.popServer) # [4.0] pass parent for transient+geometry on Linux and Mac OSX self.popPassword = askPasswordWindow(appname, prompt, parent) # on instance 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 still be called by MailFetcher superclass, but only if passwd is still empty string due to dialog close; user may have to Load or other again to reinput pswd; """ return self.popPassword