###################################################### # PyMailGui 1.0 - A Python/Tkinter email client. # Adds a Tkinter-based GUI interface to the pymail # script's functionality. Works for POP/SMTP based # email accounts using sockets on the machine on # which this script is run. Uses threads if # installed to run loads, sends, and deletes with # no blocking; threads are standard on Windows. # GUI updates done in main thread only (Windows). # Reuses and attaches TextEditor class object. # Run from command-line to see status messages. # See use notes in help text in PyMailGuiHelp.py. # To do: support attachments, shade deletions. ###################################################### # get services import pymail, mailconfig import rfc822, StringIO, string, sys from Tkinter import * from tkFileDialog import asksaveasfilename, SaveAs from tkMessageBox import showinfo, showerror, askyesno from PP2E.Gui.TextEditor.textEditor import TextEditorComponentMinimal # run if no threads try: # raise ImportError to import thread # run with gui blocking except ImportError: # no wait popups appear class fakeThread: def start_new_thread(self, func, args): apply(func, args) thread = fakeThread() # init global/module vars msgList = [] # list of retrieved emails text toDelete = [] # msgnums to be deleted on exit listBox = None # main window's scrolled msg list rootWin = None # the main window of this program allModeVar = None # for All mode checkbox value threadExitVar = 0 # used to signal child thread exit debugme = 0 # enable extra status messages mailserver = mailconfig.popservername # where to read pop email from mailuser = mailconfig.popusername # smtp server in mailconfig too mailpswd = None # pop passwd via file or popup here #mailfile = mailconfig.savemailfile # from a file select dialog here def fillIndex(msgList): # fill all of main listbox listBox.delete(0, END) count = 1 for msg in msgList: hdrs = rfc822.Message(StringIO.StringIO(msg)) msginfo = '%02d' % count for key in ('Subject', 'From', 'Date'): if hdrs.has_key(key): msginfo = msginfo + ' | ' + hdrs[key][:30] listBox.insert(END, msginfo) count = count+1 listBox.see(END) # show most recent mail=last line def selectedMsg(): # get msg selected in main listbox # print listBox.curselection() if listBox.curselection() == (): return 0 # empty tuple:no selection else: # else zero-based index return eval(listBox.curselection()[0]) + 1 # in a 1-item tuple of str def waitForThreadExit(win): import time global threadExitVar # in main thread, watch shared global var delay = 0.0 # 0.0=no sleep needed on Win98 (but hogs cpu) while not threadExitVar: win.update() # dispatch any new GUI events during wait time.sleep(delay) # if needed, sleep so other thread can run threadExitVar = 0 # at most one child thread active at once def busyInfoBoxWait(message): # popup wait message box, wait for a thread exit # main gui event thread stays alive during wait # as coded returns only after thread has finished # popup.wait_variable(threadExitVar) may work too popup = Toplevel() popup.title('PyMail Wait') popup.protocol('WM_DELETE_WINDOW', lambda:0) # ignore deletes label = Label(popup, text=message+'...') label.config(height=10, width=40, cursor='watch') # busy cursor label.pack() popup.focus_set() # grab application popup.grab_set() # wait for thread exit waitForThreadExit(popup) # gui alive during wait print 'thread exit caught' popup.destroy() def loadMailThread(): # load mail while main thread handles gui events global msgList, errInfo, threadExitVar print 'load start' errInfo = '' try: nextnum = len(msgList) + 1 newmail = pymail.loadmessages(mailserver, mailuser, mailpswd, nextnum) msgList = msgList + newmail except: exc_type, exc_value = sys.exc_info()[:2] # thread exc errInfo = '\n' + str(exc_type) + '\n' + str(exc_value) print 'load exit' threadExitVar = 1 # signal main thread def onLoadMail(): # load all (or new) pop email getpassword() thread.start_new_thread(loadMailThread, ()) busyInfoBoxWait('Retrieving mail') if errInfo: global mailpswd # zap pswd so can reinput mailpswd = None showerror('PyMail', 'Error loading mail\n' + errInfo) fillIndex(msgList) def onViewRawMail(): # view selected message - raw mail text with header lines msgnum = selectedMsg() if not (1 <= msgnum <= len(msgList)): showerror('PyMail', 'No message selected') else: text = msgList[msgnum-1] # put in ScrolledText from ScrolledText import ScrolledText window = Toplevel() window.title('PyMail raw message viewer #' + str(msgnum)) browser = ScrolledText(window) browser.insert('0.0', text) browser.pack(expand=YES, fill=BOTH) def onViewFormatMail(): # view selected message - popup formatted display msgnum = selectedMsg() if not (1 <= msgnum <= len(msgList)): showerror('PyMail', 'No message selected') else: mailtext = msgList[msgnum-1] # put in a TextEditor form textfile = StringIO.StringIO(mailtext) headers = rfc822.Message(textfile) # strips header lines bodytext = textfile.read() # rest is message body editmail('View #%d' % msgnum, headers.get('From', '?'), headers.get('To', '?'), headers.get('Subject', '?'), bodytext, headers.get('Cc', '?')) # use objects that retain prior directory for the next # select, instead of simple asksaveasfilename() dialog saveOneDialog = saveAllDialog = None def myasksaveasfilename_one(): global saveOneDialog if not saveOneDialog: saveOneDialog = SaveAs(title='PyMail Save File') return saveOneDialog.show() def myasksaveasfilename_all(): global saveAllDialog if not saveAllDialog: saveAllDialog = SaveAs(title='PyMail Save All File') return saveAllDialog.show() def onSaveMail(): # save selected message in file if allModeVar.get(): mailfile = myasksaveasfilename_all() if mailfile: try: # maybe this should be a thread for i in range(1, len(msgList)+1): pymail.savemessage(i, mailfile, msgList) except: showerror('PyMail', 'Error during save') else: msgnum = selectedMsg() if not (1 <= msgnum <= len(msgList)): showerror('PyMail', 'No message selected') else: mailfile = myasksaveasfilename_one() if mailfile: try: pymail.savemessage(msgnum, mailfile, msgList) except: showerror('PyMail', 'Error during save') def onDeleteMail(): # mark selected message for deletion on exit global toDelete if allModeVar.get(): toDelete = range(1, len(msgList)+1) else: msgnum = selectedMsg() if not (1 <= msgnum <= len(msgList)): showerror('PyMail', 'No message selected') elif msgnum not in toDelete: toDelete.append(msgnum) # fails if in list twice def sendMailThread(From, To, Cc, Subj, text): # send mail while main thread handles gui events global errInfo, threadExitVar import smtplib, time from mailconfig import smtpservername print 'send start' date = time.ctime(time.time()) Cchdr = (Cc and 'Cc: %s\n' % Cc) or '' hdrs = ('From: %s\nTo: %s\n%sDate: %s\nSubject: %s\n' % (From, To, Cchdr, date, Subj)) hdrs = hdrs + 'X-Mailer: PyMailGui Version 1.0 (Python)\n' Ccs = (Cc and string.split(Cc, ';')) or [] # some servers reject [''] Tos = string.split(To, ';') + Ccs # cc: hdr line, and To list Tos = map(string.strip, Tos) # some addrs can have ','s print 'Connecting to mail...', Tos # strip spaces around addrs errInfo = '' failed = {} # smtplib may raise except try: # or return failed Tos dict server = smtplib.SMTP(smtpservername) failed = server.sendmail(From, Tos, hdrs + text) server.quit() except: exc_type, exc_value = sys.exc_info()[:2] # thread exc excinfo = '\n' + str(exc_type) + '\n' + str(exc_value) errInfo = 'Error sending mail\n' + excinfo else: if failed: errInfo = 'Failed recipients:\n' + str(failed) print 'send exit' threadExitVar = 1 # signal main thread def sendMail(From, To, Cc, Subj, text): # send completed email thread.start_new_thread(sendMailThread, (From, To, Cc, Subj, text)) busyInfoBoxWait('Sending mail') if errInfo: showerror('PyMail', errInfo) def onWriteReplyFwdSend(window, editor, hdrs): # mail edit window send button press From, To, Cc, Subj = hdrs sendtext = editor.getAllText() sendMail(From.get(), To.get(), Cc.get(), Subj.get(), sendtext) if not errInfo: window.destroy() # else keep to retry or save def editmail(mode, From, To='', Subj='', origtext='', Cc=''): # create a new mail edit/view window win = Toplevel() win.title('PyMail - '+ mode) win.iconname('PyMail') viewOnly = (mode[:4] == 'View') # header entry fields frm = Frame(win); frm.pack( side=TOP, fill=X) lfrm = Frame(frm); lfrm.pack(side=LEFT, expand=NO, fill=BOTH) mfrm = Frame(frm); mfrm.pack(side=LEFT, expand=NO, fill=NONE) rfrm = Frame(frm); rfrm.pack(side=RIGHT, expand=YES, fill=BOTH) hdrs = [] for (label, start) in [('From:', From), ('To:', To), # order matters on send ('Cc:', Cc), ('Subj:', Subj)]: lab = Label(mfrm, text=label, justify=LEFT) ent = Entry(rfrm) lab.pack(side=TOP, expand=YES, fill=X) ent.pack(side=TOP, expand=YES, fill=X) ent.insert('0', start) hdrs.append(ent) # send, cancel buttons (need new editor) editor = TextEditorComponentMinimal(win) sendit = (lambda w=win, e=editor, h=hdrs: onWriteReplyFwdSend(w, e, h)) for (label, callback) in [('Cancel', win.destroy), ('Send', sendit)]: if not (viewOnly and label == 'Send'): b = Button(lfrm, text=label, command=callback) b.config(bg='beige', relief=RIDGE, bd=2) b.pack(side=TOP, expand=YES, fill=BOTH) # body text editor: pack last=clip first editor.pack(side=BOTTOM) # may be multiple editors if (not viewOnly) and mailconfig.mysignature: # add auto signature text? origtext = ('\n%s\n' % mailconfig.mysignature) + origtext editor.setAllText(origtext) def onWriteMail(): # compose new email editmail('Write', From=mailconfig.myaddress) def quoteorigtext(msgnum): origtext = msgList[msgnum-1] textfile = StringIO.StringIO(origtext) headers = rfc822.Message(textfile) # strips header lines bodytext = textfile.read() # rest is message body quoted = '\n-----Original Message-----\n' for hdr in ('From', 'To', 'Subject', 'Date'): quoted = quoted + ( '%s: %s\n' % (hdr, headers.get(hdr, '?')) ) quoted = quoted + '\n' + bodytext quoted = '\n' + string.replace(quoted, '\n', '\n> ') return quoted def onReplyMail(): # reply to selected email msgnum = selectedMsg() if not (1 <= msgnum <= len(msgList)): showerror('PyMail', 'No message selected') else: text = quoteorigtext(msgnum) hdrs = rfc822.Message(StringIO.StringIO(msgList[msgnum-1])) toname, toaddr = hdrs.getaddr('From') if toname and ',' in toname: toname = '"%s"' % toname To = '%s <%s>' % (toname, toaddr) From = mailconfig.myaddress or ('%s <%s>' % hdrs.getaddr('To')) Subj = 'Re: ' + hdrs.get('Subject', '(no subject)') editmail('Reply', From, To, Subj, text) def onFwdMail(): # forward selected email msgnum = selectedMsg() if not (1 <= msgnum <= len(msgList)): showerror('PyMail', 'No message selected') else: text = quoteorigtext(msgnum) hdrs = rfc822.Message(StringIO.StringIO(msgList[msgnum-1])) From = mailconfig.myaddress or ('%s <%s>' % hdrs.getaddr('To')) Subj = 'Fwd: ' + hdrs.get('Subject', '(no subject)') editmail('Forward', From, '', Subj, text) def deleteMailThread(toDelete): # delete mail while main thread handles gui events global errInfo, threadExitVar print 'delete start' try: pymail.deletemessages(mailserver, mailuser, mailpswd, toDelete, 0) except: exc_type, exc_value = sys.exc_info()[:2] errInfo = '\n' + str(exc_type) + '\n' + str(exc_value) else: errInfo = '' print 'delete exit' threadExitVar = 1 # signal main thread def onQuitMail(): # exit mail tool, delete now if askyesno('PyMail', 'Verify Quit?'): if toDelete and askyesno('PyMail', 'Really Delete Mail?'): getpassword() thread.start_new_thread(deleteMailThread, (toDelete,)) busyInfoBoxWait('Deleting mail') if errInfo: showerror('PyMail', 'Error while deleting:\n' + errInfo) else: showinfo('PyMail', 'Mail deleted from server') rootWin.quit() def askpassword(prompt, app='PyMail'): # getpass.getpass uses stdin, not GUI win = Toplevel() # tkSimpleDialog.askstring echos input win.title(app + ' Prompt') Label(win, text=prompt).pack(side=LEFT) entvar = StringVar() ent = Entry(win, textvariable=entvar, show='*') ent.pack(side=RIGHT, expand=YES, fill=X) ent.bind('', lambda event, savewin=win: savewin.destroy()) ent.focus_set(); win.grab_set(); win.wait_window() win.update() return entvar.get() # ent widget is now gone def getpassword(): # unless known, set global pop password # from client-side file or popup dialog global mailpswd if mailpswd: return else: try: localfile = open(mailconfig.poppasswdfile) mailpswd = localfile.readline()[:-1] if debugme: print 'local file password', repr(mailpswd) except: prompt = 'Password for %s on %s?' % (mailuser, mailserver) mailpswd = askpassword(prompt) if debugme: print 'user input password', repr(mailpswd) def decorate(rootWin): # window manager stuff for main window rootWin.title('PyMail 1.0') rootWin.iconname('PyMail') rootWin.protocol('WM_DELETE_WINDOW', onQuitMail) def makemainwindow(parent=None): # make the main window global rootWin, listBox, allModeVar if parent: rootWin = Frame(parent) # attach to a parent rootWin.pack(expand=YES, fill=BOTH) else: rootWin = Tk() # assume I'm standalone decorate(rootWin) # add main buttons at bottom frame1 = Frame(rootWin) frame1.pack(side=BOTTOM, fill=X) allModeVar = IntVar() Checkbutton(frame1, text="All", variable=allModeVar).pack(side=RIGHT) actions = [ ('Load', onLoadMail), ('View', onViewFormatMail), ('Save', onSaveMail), ('Del', onDeleteMail), ('Write', onWriteMail), ('Reply', onReplyMail), ('Fwd', onFwdMail), ('Quit', onQuitMail) ] for (title, callback) in actions: Button(frame1, text=title, command=callback).pack(side=LEFT, fill=X) # add main listbox and scrollbar frame2 = Frame(rootWin) vscroll = Scrollbar(frame2) fontsz = (sys.platform[:3] == 'win' and 8) or 10 listBox = Listbox(frame2, bg='white', font=('courier', fontsz)) # crosslink listbox and scrollbar vscroll.config(command=listBox.yview, relief=SUNKEN) listBox.config(yscrollcommand=vscroll.set, relief=SUNKEN, selectmode=SINGLE) listBox.bind('', lambda event: onViewRawMail()) frame2.pack(side=TOP, expand=YES, fill=BOTH) vscroll.pack(side=RIGHT, fill=BOTH) listBox.pack(side=LEFT, expand=YES, fill=BOTH) return rootWin # load text block string from PyMailGuiHelp import helptext def showhelp(helptext=helptext, appname='PyMail'): # show helptext in from ScrolledText import ScrolledText # a non-modal dialog new = Toplevel() # make new popup window bar = Frame(new) # pack first=clip last bar.pack(side=BOTTOM, fill=X) code = Button(bar, bg='beige', text="Source", command=showsource) quit = Button(bar, bg='beige', text="Cancel", command=new.destroy) code.pack(pady=1, side=LEFT) quit.pack(pady=1, side=LEFT) text = ScrolledText(new) # add Text + scrollbar text.config(font='system', width=70) # too big for showinfo text.config(bg='steelblue', fg='white') # erase on btn or return text.insert('0.0', helptext) text.pack(expand=YES, fill=BOTH) new.title(appname + " Help") new.bind("", (lambda event, new=new: new.destroy())) def showsource(): # tricky, but open try: # like web getfile.cgi source = open('PyMailGui.py').read() # in cwd or below it? except: try: # or use find.find(f)[0], import os # $PP2EHOME, guessLocation from PP2E.Launcher import findFirst # or spawn pyedit with arg here = os.curdir source = open(findFirst(here, 'PyMailGui.py')).read() except: source = 'Sorry - cannot find my source file' subject = 'Main script [see also: PyMailGuiHelp, pymail, mailconfig]' editmail('View Source Code', 'PyMailGui', 'User', subject, source) def container(): # use attachment to add help button # this is a bit easier with classes root = Tk() title = Button(root, text='PyMail - a Python/Tkinter email client') title.config(bg='steelblue', fg='white', relief=RIDGE) title.config(command=showhelp) title.pack(fill=X) decorate(root) return root if __name__ == '__main__': # run stand-alone or attach rootWin = makemainwindow(container()) # or makemainwindow() rootWin.mainloop()