File: ViewWindows.py

File: ViewWindows.py

"""
###############################################################################
Implementation of View, Write, Reply, Forward windows: one class per kind.
Code is factored here for reuse: a Write window is a customized View window,
and Reply and Forward are custom Write windows. Windows defined in this
file are created by the list windows, in response to user actions.
Caveat:'split' pop ups for opening parts/attachments feel nonintuitive.
2.1: this caveat was addressed, by adding quick-access attachment buttons.
New in 3.0: platform-neutral grid() for mail headers, not packed col frames.
New in 3.0: supports Unicode encodings for main text + text attachments sent.
New in 3.0: PyEdit supports arbitrary Unicode for message parts viewed.
New in 3.0: supports Unicode/mail encodings for headers in mails sent.
TBD: could avoid verifying quits unless text area modified (like PyEdit2.0),
but these windows are larger, and would not catch headers already changed.
TBD: should Open dialog in write windows be program-wide? (per-window now).
###############################################################################
"""
from SharedNames import * # program-wide global objects
###############################################################################
# message view window - also a superclass of write, reply, forward
###############################################################################
class ViewWindow(windows.PopupWindow, mailtools.MailParser):
 """
 a Toplevel, with extra protocol and embedded TextEditor;
 inherits saveParts,partsList from mailtools.MailParser;
 mixes in custom subclass logic by direct inheritance here;
 """
 # class attributes
 modelabel = 'View' # used in window titles
 from mailconfig import okayToOpenParts # open any attachments at all?
 from mailconfig import verifyPartOpens # ask before open each part?
 from mailconfig import maxPartButtons # show up to this many + '...'
 from mailconfig import skipTextOnHtmlPart # 3.0: just browser, not PyEdit?
 tempPartDir = 'TempParts' # where 1 selected part saved
 # all view windows use same dialog: remembers last dir
 partsDialog = Directory(title=appname + ': Select parts save directory')
 def __init__(self, headermap, showtext, origmessage=None):
 """
 header map is origmessage, or custom hdr dict for writing;
 showtext is main text part of the message: parsed or custom;
 origmessage is parsed email.message.Message for view mail windows
 """
 windows.PopupWindow.__init__(self, appname, self.modelabel)
 self.origMessage = origmessage 
 self.makeWidgets(headermap, showtext)
 def makeWidgets(self, headermap, showtext):
 """
 add headers, actions, attachments, text editor
 3.0: showtext is assumed to be decoded Unicode str here;
 it will be encoded on sends and saves as directed/needed;
 """
 actionsframe = self.makeHeaders(headermap)
 if self.origMessage and self.okayToOpenParts:
 self.makePartButtons()
 self.editor = textEditor.TextEditorComponentMinimal(self)
 myactions = self.actionButtons()
 for (label, callback) in myactions:
 b = Button(actionsframe, text=label, command=callback)
 b.config(bg='beige', relief=RIDGE, bd=2)
 b.pack(side=TOP, expand=YES, fill=BOTH)
 # body text, pack last=clip first
 self.editor.pack(side=BOTTOM) # may be multiple editors
 self.update() # 3.0: else may be @ line2
 #------------------------------------------------------------------
 # Jan 2014, 1.5: setAllText failed once in 3.3 (after a decade of
 # daily use!), leaving thread-busy lock locked. Looks like a Tk
 # limitation -- it couldn't handle a "speak-no-evil monkey" Unicode
 # character > 16 bits in the text part of a text+html alternative
 # message (the html part uses a link instead) -- but this shouldn't
 # leave the GUI crippled due to an uncaught exception and busy lock
 # (it could no longer Load, and had to be closed in TaskManager on
 # Windows). The exception's traceback in the console window:
 #
 # File "..\ViewWindows.py", line 75, in makeWidgets
 # self.editor.setAllText(showtext) # each has own content
 # File "C:\PP4E\...\Gui\TextEditor\textEditor.py", line 873, in setAllText
 # self.text.insert(END, text) # or '1.0'; text=bytes or str
 # File "C:\Python33\lib\tkinter\__init__.py", line 3095, in insert
 # self.tk.call((self._w, 'insert', index, chars) + args)
 # ValueError: character U+1f64a is above the range (U+0000-U+FFFF) allowed by Tcl
 #------------------------------------------------------------------
 #self.editor.setAllText(showtext) # each has own content
 try:
 self.editor.setAllText(showtext) # each has own content
 except:
 showerror(appname, 'Error setting text in view window')
 printStack(sys.exc_info())
 
 lines = len(showtext.splitlines())
 lines = min(lines + 3, mailconfig.viewheight or 20)
 self.editor.setHeight(lines) # else height=24, width=80
 self.editor.setWidth(80) # or from PyEdit textConfig
 if mailconfig.viewbg:
 self.editor.setBg(mailconfig.viewbg) # colors, font in mailconfig
 if mailconfig.viewfg:
 self.editor.setFg(mailconfig.viewfg)
 if mailconfig.viewfont: # also via editor Tools menu
 self.editor.setFont(mailconfig.viewfont)
 def makeHeaders(self, headermap):
 """
 add header entry fields, return action buttons frame;
 3.0: uses grid for platform-neutral layout of label/entry rows;
 packed row frames with fixed-width labels would work well too;
 3.0: decoding of i18n headers (and email names in address headers)
 is performed here if still required as they are added to the GUI;
 some may have been decoded already for reply/forward windows that 
 need to use decoded text, but the extra decode here is harmless for
 these, and is required for other headers and cases such as fetched 
 mail views; always, headers are in decoded form when displayed in
 the GUI, and will be encoded within mailtools on Sends if they are 
 non-ASCII (see Write); i18n header decoding also occurs in list 
 window mail indexes, and for headers added to quoted mail text;
 text payloads in the mail body are also decoded for display and 
 encoded for sends elsewhere in the system (list windows, Write);
 3.0: creators of edit windows prefill Bcc header with sender email
 address to be picked up here, as a convenience for common usages if
 this header is enabled in mailconfig; Reply also now prefills the
 Cc header with all unique original recipients less From, if enabled;
 """
 top = Frame(self); top.pack (side=TOP, fill=X)
 left = Frame(top); left.pack (side=LEFT, expand=NO, fill=BOTH)
 middle = Frame(top); middle.pack(side=LEFT, expand=YES, fill=X)
 # headers set may be extended in mailconfig (Bcc, others?)
 self.userHdrs = ()
 showhdrs = ('From', 'To', 'Cc', 'Subject')
 if hasattr(mailconfig, 'viewheaders') and mailconfig.viewheaders:
 self.userHdrs = mailconfig.viewheaders
 showhdrs += self.userHdrs
 addrhdrs = ('From', 'To', 'Cc', 'Bcc') # 3.0: decode i18n specially
 
 self.hdrFields = []
 for (i, header) in enumerate(showhdrs):
 lab = Label(middle, text=header+':', justify=LEFT)
 ent = Entry(middle)
 lab.grid(row=i, column=0, sticky=EW)
 ent.grid(row=i, column=1, sticky=EW)
 middle.rowconfigure(i, weight=1)
 hdrvalue = headermap.get(header, '?') # might be empty
 # 3.0: if encoded, decode per email+mime+unicode
 if header not in addrhdrs:
 hdrvalue = self.decodeHeader(hdrvalue)
 else:
 hdrvalue = self.decodeAddrHeader(hdrvalue)
 ent.insert('0', hdrvalue)
 self.hdrFields.append(ent) # order matters in onSend
 middle.columnconfigure(1, weight=1)
 return left
 def actionButtons(self): # must be method for self
 return [('Cancel', self.destroy), # close view window silently
 ('Parts', self.onParts), # multiparts list or the body
 ('Split', self.onSplit)]
 def makePartButtons(self):
 """
 add up to N buttons that open attachments/parts
 when clicked; alternative to Parts/Split (2.1);
 okay that temp dir is shared by all open messages:
 part file not saved till later selected and opened;
 partname=partname is required in lambda in Py2.4;
 caveat: we could try to skip the main text part;
 """
 def makeButton(parent, text, callback):
 link = Button(parent, text=text, command=callback, relief=SUNKEN)
 if mailconfig.partfg: link.config(fg=mailconfig.partfg)
 if mailconfig.partbg: link.config(bg=mailconfig.partbg)
 link.pack(side=LEFT, fill=X, expand=YES)
 parts = Frame(self)
 parts.pack(side=TOP, expand=NO, fill=X)
 for (count, partname) in enumerate(self.partsList(self.origMessage)):
 if count == self.maxPartButtons:
 makeButton(parts, '...', self.onSplit)
 break
 openpart = (lambda partname=partname: self.onOnePart(partname))
 makeButton(parts, partname, openpart)
 def onOnePart(self, partname):
 """
 locate selected part for button and save and open;
 okay if multiple mails open: resaves each time selected;
 we could probably just use web browser directly here;
 caveat: tempPartDir is relative to cwd - poss anywhere;
 caveat: tempPartDir is never cleaned up: might be large,
 could use tempfile module (just like the HTML main text 
 part display code in onView of the list window class);
 """
 try:
 savedir = self.tempPartDir
 message = self.origMessage
 (contype, savepath) = self.saveOnePart(savedir, partname, message)
 except:
 showerror(appname, 'Error while writing part file')
 printStack(sys.exc_info())
 else:
 self.openParts([(contype, os.path.abspath(savepath))]) # reuse
 def onParts(self):
 """
 show message part/attachments in pop-up window;
 uses same file naming scheme as save on Split;
 if non-multipart, single part = full body text
 """
 partnames = self.partsList(self.origMessage)
 msg = '\n'.join(['Message parts:\n'] + partnames)
 showinfo(appname, msg)
 def onSplit(self):
 """
 pop up save dir dialog and save all parts/attachments there;
 if desired, pop up HTML and multimedia parts in web browser,
 text in TextEditor, and well-known doc types on windows;
 could show parts in View windows where embedded text editor
 would provide a save button, but most are not readable text;
 """
 savedir = self.partsDialog.show() # class attr: at prior dir
 if savedir: # tk dir chooser, not file
 try:
 partfiles = self.saveParts(savedir, self.origMessage)
 except:
 showerror(appname, 'Error while writing part files')
 printStack(sys.exc_info())
 else:
 if self.okayToOpenParts: self.openParts(partfiles)
 def askOpen(self, appname, prompt):
 if not self.verifyPartOpens:
 return True
 else:
 return askyesno(appname, prompt) # pop-up dialog
 def openParts(self, partfiles):
 """
 auto-open well known and safe file types, but only if verified 
 by the user in a pop up; other types must be opened manually 
 from save dir; at this point, the named parts have been already
 MIME-decoded and saved as raw bytes in binary-mode files, but text 
 parts may be in any Unicode encoding; PyEdit needs to know the
 encoding to decode, webbrowsers may have to guess or be told;
 caveat: punts for type application/octet-stream even if it has 
 safe filename extension such as .html; caveat: image/audio/video
 could be opened with the book's playfile.py; could also do that 
 if text viewer fails: would start notepad on Windows via startfile;
 webbrowser may handle most cases here too, but specific is better;
 """
 def textPartEncoding(fullfilename):
 """
 3.0: map a text part filename back to charset param in content-type 
 header of part's Message, so we can pass this on to the PyEdit 
 constructor for proper text display; we could return the charset
 along with content-type from mailtools for text parts, but fewer
 changes are needed if this is handled as a special case here;
 part content is saved in binary mode files by mailtools to avoid 
 encoding issues, but here the original part Message is not directly 
 available; we need this mapping step to extract a Unicode encoding 
 name if present; 4E's PyEdit now allows an explicit encoding name for 
 file opens, and resolves encoding on saves; see Chapter 11 for PyEdit
 policies: it may ask user for an encoding if charset absent or fails;
 caveat: move to mailtools.mailParser to reuse for <meta> in PyMailCGI?
 """
 partname = os.path.basename(fullfilename)
 for (filename, contype, part) in self.walkNamedParts(self.origMessage):
 if filename == partname:
 return part.get_content_charset() # None if not in header
 assert False, 'Text part not found' # should never happen
 for (contype, fullfilename) in partfiles:
 maintype = contype.split('/')[0] # left side
 extension = os.path.splitext(fullfilename)[1] # not [-4:]
 basename = os.path.basename(fullfilename) # strip dir
 # HTML and XML text, web pages, some media
 if contype in ['text/html', 'text/xml']:
 browserOpened = False
 if self.askOpen(appname, 'Open "%s" in browser?' % basename):
 try:
 webbrowser.open_new('file://' + fullfilename)
 browserOpened = True
 except:
 showerror(appname, 'Browser failed: trying editor')
 printStack(sys.exc_info()) # 1.5
 if not browserOpened or not self.skipTextOnHtmlPart:
 try: 
 # try PyEdit to see encoding name and effect
 encoding = textPartEncoding(fullfilename)
 textEditor.TextEditorMainPopup(parent=self,
 winTitle=' - %s email part' % (encoding or '?'),
 loadFirst=fullfilename, loadEncode=encoding)
 except:
 showerror(appname, 'Error opening text viewer')
 printStack(sys.exc_info()) # 1.5
 # text/plain, text/x-python, etc.; 4E: encoding, may fail
 elif maintype == 'text':
 if self.askOpen(appname, 'Open text part "%s"?' % basename):
 try:
 encoding = textPartEncoding(fullfilename)
 textEditor.TextEditorMainPopup(parent=self,
 winTitle=' - %s email part' % (encoding or '?'),
 loadFirst=fullfilename, loadEncode=encoding)
 except:
 # Jan 1014, 1.5: may also fail, but doesn't keep busy lock
 showerror(appname, 'Error opening text viewer')
 printStack(sys.exc_info()) # 1.5
 # multimedia types: Windows opens mediaplayer, imageviewer, etc.
 elif maintype in ['image', 'audio', 'video']:
 if self.askOpen(appname, 'Open media part "%s"?' % basename):
 try:
 webbrowser.open_new('file://' + fullfilename)
 except:
 showerror(appname, 'Error opening browser')
 printStack(sys.exc_info()) # 1.5
 # common Windows documents: Word, Excel, Adobe, archives, etc.
 elif (sys.platform[:3] == 'win' and
 maintype == 'application' and # 3.0: +x types
 extension in ['.doc', '.docx', '.xls', '.xlsx', # generalize me
 '.pdf', '.zip', '.tar', '.wmv']):
 if self.askOpen(appname, 'Open part "%s"?' % basename):
 os.startfile(fullfilename)
 else: # punt!
 msg = 'Cannot open part: "%s"\nOpen manually in: "%s"'
 msg = msg % (basename, os.path.dirname(fullfilename))
 showinfo(appname, msg)
###############################################################################
# message edit windows - write, reply, forward
###############################################################################
if mailconfig.smtpuser: # user set in mailconfig?
 MailSenderClass = mailtools.MailSenderAuth # login/password required
else:
 MailSenderClass = mailtools.MailSender
class WriteWindow(ViewWindow, MailSenderClass):
 """
 customize view display for composing new mail
 inherits sendMessage from mailtools.MailSender
 """
 modelabel = 'Write'
 def __init__(self, headermap, starttext):
 ViewWindow.__init__(self, headermap, starttext)
 MailSenderClass.__init__(self)
 self.attaches = [] # each win has own open dialog
 self.openDialog = None # dialog remembers last dir
 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 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'



AltStyle によって変換されたページ (->オリジナル) /