###################################################### # PyMailGui 1.0 - A Python/Tkinter email client. # Early/experimental version of this program. # # Adds a simple Tkinter 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. # # I like this script for two main reasons: # 1) It's scriptable--you control its evolution # from this point forward, and can easily customize # and program it by editing the Python code in this # file, unlike canned products like MS-Outlook. # 2) It's portable--this script can be used to # process your email on any machine with internet # sockets where Python and Tkinter are installed; # use the simple command-line based pymail.py if # you have sockets and Python but not Tkinter. # E.g., I use this to read my pop email account # from UNIX machines when I'm away from home PC. # # There is much room for improvement and new features # here--left as exercises. Example extensions: # - Add an automatic spam filter, which matches # from/to hdrs etc. with a regex and auto deletes # matching messages as they are being downloaded; # - Auto decoding/unpacking for multi-part emails: # see decode*.py scripts here for pointers; # - Fields in the main list box could be padded to # a standard length or put in distinct widgets. # - Make me a class to avoid global vars, and make # it easier to attach this GUI to another one; # that would also allow creation of more than one # mail client gui per process, but this won't work # as currently designed (deletes in one gui can # invalidate others, due to new pop msg numbers); # - Inherit from GuiMaker here to get menu/toolbars; # - In onViewMail, pop up a full-blown TextEditor; # - Handle wait states better (e.g., use threads); # - Strip some headers from displayed mail text; # - Only fetch new mail on 'Load' button--keep the # last mbox size, and retrieve oldsize+1..newsize; ###################################################### import pymail, mailconfig import rfc822, StringIO, string, sys from Tkinter import * from tkFileDialog import asksaveasfilename from tkMessageBox import showinfo, showerror, askyesno 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 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 modalinfobox(message): # popup wait message box new = Toplevel() new.title('PyMail Wait') Label(new, text=message+'...', height=10, width=40, cursor='watch').pack() new.focus() new.update() return new # new.focus/grab?, new.destroy to erase def onLoadMail(): # load all pop email getpassword() win = modalinfobox('Retrieving mail') global msgList try: msgList = pymail.loadmessages(mailserver, mailuser, mailpswd) except: excinfo = '\n' + str(sys.exc_type) + '\n' + str(sys.exc_value) showerror('PyMail', 'Error loading mail\n' + excinfo) fillIndex(msgList) win.destroy() def onViewMail(): # view selected message msgnum = selectedMsg() if not (1 <= msgnum <= len(msgList)): showerror('PyMail', 'No message selected') else: text = msgList[msgnum-1] # or put in a TextEditor from ScrolledText import ScrolledText window = Toplevel() window.title('PyMail message viewer #' + str(msgnum)) browser = ScrolledText(window) browser.insert('0.0', text) browser.pack(expand=YES, fill=BOTH) def onSaveMail(): # save selected message in file mailfile = asksaveasfilename(title='PyMail Save File', initialdir='.') if mailfile: if allModeVar.get(): for i in range(1, len(msgList)+1): pymail.savemessage(i, mailfile, msgList) else: msgnum = selectedMsg() if not (1 <= msgnum <= len(msgList)): showerror('PyMail', 'No message selected') else: pymail.savemessage(msgnum, mailfile, msgList) def onDeleteMail(): # delete selected message 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') else: toDelete.append(msgnum) def sendMail(From, To, Cc, Subj, text): # send completed email import smtplib, time from mailconfig import smtpservername 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: PyMail Version 1.0 (Python)\n' Tos = string.split(To, ',') + string.split(Cc, ',') win = modalinfobox('Sending mail') print 'Connecting...' server = smtplib.SMTP(smtpservername) errors = server.sendmail(From, Tos, hdrs + text) server.quit() win.destroy() if errors: showerror('PyMail', 'Error on send:\n' + str(errors)) 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) window.destroy() def editmail(mode, From, To='', Subj='', origtext=''): # create a mail edit window win = Toplevel() win.title('PyMail - '+ mode) win.iconname('PyMail') # 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), ('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 epatch = [None] sendit = (lambda w=win, e=epatch, h=hdrs: onWriteReplyFwdSend(w, e[0], h)) for (label, callback) in [('Cancel', win.destroy), ('Send', sendit)]: 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 - make,pack last=clip first from PP2E.Gui.TextEditor.textEditor import TextEditorComponentMinimal editor = epatch[0] = TextEditorComponentMinimal(win) editor.pack(side=BOTTOM) if mailconfig.mysignature: # add 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 = '\n-----Original Message-----\n' + msgList[msgnum-1] origtext = '\n' + string.replace(origtext, '\n', '\n> ') return origtext 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])) To = '%s <%s>' % hdrs.getaddr('From') 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 onQuitMail(): # exit mail tool, delete now if askyesno('PyMail Verify', 'Verify Quit?'): if toDelete: showinfo('PyMail', 'Deleting mail from server...') getpassword() pymail.deletemessages(mailserver, mailuser, mailpswd, toDelete, 0) showinfo('PyMail', 'Mail deleted from server...') rootWin.quit() def getpassword(): # prompt for pop password global mailpswd if mailpswd: # getpass.getpass uses stdin, not GUI return # tkSimpleDialog.askstring echos input else: win = Toplevel() win.title('PyMail Prompt') prompt = 'Password for %s on %s?' % (mailuser, mailserver) Label(win, text=prompt).pack(side=LEFT) entvar = StringVar() ent = Entry(win, textvariable=entvar, show='*') ent.pack(side=RIGHT) ent.bind('', lambda event, savewin=win: savewin.destroy()) win.focus_set(); win.grab_set(); win.wait_window() win.update() mailpswd = entvar.get() # ent widget is gone 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', onViewMail), ('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: onViewMail()) frame2.pack(side=TOP, expand=YES, fill=BOTH) vscroll.pack(side=RIGHT, fill=BOTH) listBox.pack(side=LEFT, expand=YES, fill=BOTH) return rootWin helptext = """ PyMail, version 1.0 February, 2000 Programming Python, 2nd Ed. Click buttons to process email: Load retrieves all your POP mail, Write sends new mail by SMTP. Click All to apply Save or Del to all retrieved messages. Click Del to delete selected (or all) mail from POP server on exit. Change mailconfig.py to reflect your email servers, address, and optional signature. """ 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=(lambda: showinfo('PyMail', helptext))) title.pack(fill=X) decorate(root) return root # 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 mailserver = mailconfig.popservername # where to read pop email from mailuser = mailconfig.popusername # smtp server in mailconfig too mailpswd = None # passwd input via a popup here #mailfile = mailconfig.savemailfile # from a file select dialog here if __name__ == '__main__': #run stand-alone or attached #rootWin = makemainwindow() rootWin = makemainwindow(container()) rootWin.mainloop()