I thought it would be a good idea to write a sample application in wxPython to show how to put all the pieces together and make something useful. At my day job, I created a little program to send emails because we had a lot of users that missed the mailto functionality that we lost when we switched from Exchange/Outlook to Zimbra. It should be noted that this is a Windows only application currently, but it shouldn't be too hard to make it more OS-agnostic.
I'll split this article into three pieces: First is creating the interface; second is setting up the data handling and third will be creating a Windows executable and connecting it to the mailto handler.When we're done, the GUI will look something like this:
To follow along, you'll need the following:
Let's go over the code below. As you can see, I am basing this application on the wx.Frame object and an instance of wx.PySimpleApp to make the application run.
import os
import sys
import urllib
import wx
import mail_ico
class SendMailWx(wx.Frame):
def __init__(self):
wx.Frame.__init__(self, None, -1, 'New Email Message (Plain Text)',
size=(600,400))
self.panel = wx.Panel(self, wx.ID_ANY)
# set your email address here
self.email = 'myEmail@email.com'
self.filepaths = []
self.currentDir = os.path.abspath(os.path.dirname(sys.argv[0]))
self.createMenu()
self.createToolbar()
self.createWidgets()
try:
print sys.argv
self.parseURL(sys.argv[1])
except Exception, e:
print 'Unable to execute parseURL...'
print e
self.layoutWidgets()
self.attachTxt.Hide()
self.editAttachBtn.Hide()
def createMenu(self):
menubar = wx.MenuBar()
fileMenu = wx.Menu()
send_menu_item = fileMenu.Append(wx.NewId(), '&Send', 'Sends the email')
close_menu_item = fileMenu.Append(wx.NewId(), '&Close', 'Closes the window')
menubar.Append(fileMenu, '&File')
self.SetMenuBar(menubar)
# bind events to the menu items
self.Bind(wx.EVT_MENU, self.onSend, send_menu_item)
self.Bind(wx.EVT_MENU, self.onClose, close_menu_item)
def createToolbar(self):
toolbar = self.CreateToolBar(wx.TB_3DBUTTONS|wx.TB_TEXT)
toolbar.SetToolBitmapSize((31,31))
bmp = mail_ico.getBitmap()
sendTool = toolbar.AddSimpleTool(-1, bmp, 'Send', 'Sends Email')
self.Bind(wx.EVT_MENU, self.onSend, sendTool)
toolbar.Realize()
def createWidgets(self):
p = self.panel
font = wx.Font(10, wx.SWISS, wx.NORMAL, wx.BOLD)
self.fromLbl = wx.StaticText(p, wx.ID_ANY, 'From', size=(60,-1))
self.fromTxt = wx.TextCtrl(p, wx.ID_ANY, self.email)
self.toLbl = wx.StaticText(p, wx.ID_ANY, 'To:', size=(60,-1))
self.toTxt = wx.TextCtrl(p, wx.ID_ANY, '')
self.subjectLbl = wx.StaticText(p, wx.ID_ANY, ' Subject:', size=(60,-1))
self.subjectTxt = wx.TextCtrl(p, wx.ID_ANY, '')
self.attachBtn = wx.Button(p, wx.ID_ANY, 'Attachments')
self.attachTxt = wx.TextCtrl(p, wx.ID_ANY, '', style=wx.TE_MULTILINE)
self.attachTxt.Disable()
self.editAttachBtn = wx.Button(p, wx.ID_ANY, 'Edit Attachments')
self.messageTxt = wx.TextCtrl(p, wx.ID_ANY, '', style=wx.TE_MULTILINE)
self.Bind(wx.EVT_BUTTON, self.onAttach, self.attachBtn)
self.Bind(wx.EVT_BUTTON, self.onAttachEdit, self.editAttachBtn)
self.fromLbl.SetFont(font)
self.toLbl.SetFont(font)
self.subjectLbl.SetFont(font)
def layoutWidgets(self):
mainSizer = wx.BoxSizer(wx.VERTICAL)
fromSizer = wx.BoxSizer(wx.HORIZONTAL)
toSizer = wx.BoxSizer(wx.HORIZONTAL)
subjSizer = wx.BoxSizer(wx.HORIZONTAL)
attachSizer = wx.BoxSizer(wx.HORIZONTAL)
fromSizer.Add(self.fromLbl, 0)
fromSizer.Add(self.fromTxt, 1, wx.EXPAND)
toSizer.Add(self.toLbl, 0)
toSizer.Add(self.toTxt, 1, wx.EXPAND)
subjSizer.Add(self.subjectLbl, 0)
subjSizer.Add(self.subjectTxt, 1, wx.EXPAND)
attachSizer.Add(self.attachBtn, 0, wx.ALL, 5)
attachSizer.Add(self.attachTxt, 1, wx.ALL|wx.EXPAND, 5)
attachSizer.Add(self.editAttachBtn, 0, wx.ALL, 5)
mainSizer.Add(fromSizer, 0, wx.ALL|wx.EXPAND, 5)
mainSizer.Add(toSizer, 0, wx.ALL|wx.EXPAND, 5)
mainSizer.Add(subjSizer, 0, wx.ALL|wx.EXPAND, 5)
mainSizer.Add(attachSizer, 0, wx.ALL|wx.EXPAND, 5)
mainSizer.Add(self.messageTxt, 1, wx.ALL|wx.EXPAND, 5)
self.panel.SetSizer(mainSizer)
self.panel.Layout()
def parseURL(self, url):
''' Parse the URL passed from the mailto link '''
sections = 1
mailto_string = url.split(':')[1]
if '?' in mailto_string:
sections = mailto_string.split('?')
else:
address = mailto_string
if len(sections) > 1:
address = sections[0]
new_sections = urllib.unquote(sections[1]).split('&')
for item in new_sections:
if 'subject' in item.lower():
Subject = item.split('=')[1]
self.subjectTxt.SetValue(Subject)
if 'body' in item.lower():
Body = item.split('=')[1]
self.messageTxt.SetValue(Body)
self.toTxt.SetValue(address)
def onAttach(self, event):
'''
Displays a File Dialog to allow the user to choose a file
and then attach it to the email.
'''
print "in onAttach method..."
def onAttachEdit(self, event):
''' Allow the editing of the attached files list '''
print "in onAttachEdit method..."
def onSend(self, event):
''' Send the email using the filled out textboxes.
Warn the user if they forget to fill part
of it out.
'''
print "in onSend event handler..."
def onClose(self, event):
self.Close()
#######################
# Start program
if __name__ == '__main__':
app = wx.PySimpleApp()
frame = SendMailWx()
frame.Show()
app.MainLoop()
I've already explained how to create toolbars, menus and sizers in previous posts, so I'm going to focus on the new stuff here. I import the urllib module to help in parsing the data sent from the mailto link on a web page. I currently support the To, Subject and Body fields of the mailto protocol. The respective textboxes are set depending on the number of sections that are passed into the parseURL() method. You could easily extend this is need be. I also grab the directory where the script is running from by using this line of code:
self.currentDir = os.path.abspath(os.path.dirname(sys.argv[0]))
Finally, there are three event handler stubs: "onAttach", "onAttachEdit", and "onSend". Let's go ahead and flesh these out a bit.
The first method, onAttach(), allows the user to attach files to their email message. I use the wx.FileDialog to get the user's choice. Here is where the "filepaths" property comes in. I also call the new method,getFileSize, which will calculate the file's size. See the code below:
def onAttach(self, event):
'''
Displays a File Dialog to allow the user to choose a file
and then attach it to the email.
'''
attachments = self.attachTxt.GetLabel()
filepath = ''
# create a file dialog
wildcard = "All files (*.*)|*.*"
dialog = wx.FileDialog(None, 'Choose a file', self.currentDir,
'', wildcard, wx.OPEN)
# if the user presses OK, get the path
if dialog.ShowModal() == wx.ID_OK:
self.attachTxt.Show()
self.editAttachBtn.Show()
filepath = dialog.GetPath()
print filepath
# Change the current directory to reflect the last dir opened
os.chdir(os.path.dirname(filepath))
self.currentDir = os.getcwd()
# add the user's file to the filepath list
if filepath != '':
self.filepaths.append(filepath)
# get file size
fSize = self.getFileSize(filepath)
# modify the attachment's label based on it's current contents
if attachments == '':
attachments = '%s (%s)' % (os.path.basename(filepath), fSize)
else:
temp = '%s (%s)' % (os.path.basename(filepath), fSize)
attachments = attachments + '; ' + temp
self.attachTxt.SetLabel(attachments)
dialog.Destroy()
def getFileSize(self, f):
''' Get the file's approx. size '''
fSize = os.stat(f).st_size
if fSize >= 1073741824: # gigabyte
fSize = int(math.ceil(fSize/1073741824.0))
size = '%s GB' % fSize
elif fSize >= 1048576: # megabyte
fSize = int(math.ceil(fSize/1048576.0))
size = '%s MB' % fSize
elif fSize >= 1024: # kilobyte
fSize = int(math.ceil(fSize/1024.0))
size = '%s KB' % fSize
else:
size = '%s bytes' % fSize
return size
You'll also notice that I save the last directory the user goes into. I still come across programs that don't do this or don't do it consistently. Hopefully my implementation will work in the majority of cases. The getFileSize() method is supposed to calculate the size of the attached file. This only displays the nearest size and doesn't show fractions. Other than that, I think it's pretty self-explanatory.
The onAttachEdit() method is pretty similar, except that it calls a custom dialog to allow the user to edit what files are included in case they chose one erroneously.
def onAttachEdit(self, event):
''' Allow the editing of the attached files list '''
print 'in onAttachEdit...'
attachments = ''
dialog = EditDialog(self.filepaths)
dialog.ShowModal()
self.filepaths = dialog.filepaths
print 'Edited paths:\n', self.filepaths
dialog.Destroy()
if self.filepaths == []:
# hide the attachment controls
self.attachTxt.Hide()
self.editAttachBtn.Hide()
else:
for path in self.filepaths:
# get file size
fSize = self.getFileSize(path)
# Edit the attachments listed
if attachments == '':
attachments = '%s (%s)' % (os.path.basename(path), fSize)
else:
temp = '%s (%s)' % (os.path.basename(path), fSize)
attachments = attachments + '; ' + temp
self.attachTxt.SetLabel(attachments)
class EditDialog(wx.Dialog):
def __init__(self, filepaths):
wx.Dialog.__init__(self, None, -1, 'Edit Attachments', size=(190,150))
self.filepaths = filepaths
instructions = 'Check the items below that you no longer wish to attach to the email'
lbl = wx.StaticText(self, wx.ID_ANY, instructions)
deleteBtn = wx.Button(self, wx.ID_ANY, 'Delete Items')
cancelBtn = wx.Button(self, wx.ID_ANY, 'Cancel')
self.Bind(wx.EVT_BUTTON, self.onDelete, deleteBtn)
self.Bind(wx.EVT_BUTTON, self.onCancel, cancelBtn)
mainSizer = wx.BoxSizer(wx.VERTICAL)
btnSizer = wx.BoxSizer(wx.HORIZONTAL)
mainSizer.Add(lbl, 0, wx.ALL, 5)
self.chkList = wx.CheckListBox(self, wx.ID_ANY, choices=self.filepaths)
mainSizer.Add(self.chkList, 0, wx.ALL, 5)
btnSizer.Add(deleteBtn, 0, wx.ALL|wx.CENTER, 5)
btnSizer.Add(cancelBtn, 0, wx.ALL|wx.CENTER, 5)
mainSizer.Add(btnSizer, 0, wx.ALL|wx.CENTER, 5)
self.SetSizer(mainSizer)
self.Fit()
self.Layout()
def onCancel(self, event):
self.Close()
def onDelete(self, event):
print 'in onDelete'
numberOfPaths = len(self.filepaths)
for item in range(numberOfPaths):
val = self.chkList.IsChecked(item)
if val == True:
path = self.chkList.GetString(item)
print path
for i in range(len(self.filepaths)-1,-1,-1):
if path in self.filepaths[i]:
del self.filepaths[i]
print 'new list => ', self.filepaths
self.Close()
The main thing to notice in the code above is that the EditDialog is sub-classing wx.Dialog. The reason I chose this over a wx.Frame is because I wanted my dialog to be non-modal and I think using the wx.Dialog class makes the most sense for this. Probably the most interesting part of this class is my onDelete method, in which I loop over the paths backwards. I do this so I can delete the items in any order without comprising the integrity of the list. For example, if I had deleted element 2 repeatedly, I would probably end up deleting an element I didn't mean to.
My last method is the onSend() one. I think it is probably the most complex and the one that will need refactoring the most. In this implementation, all the SMTP elements are hard coded. Let's take a look and see how it works:
def OnSend(self, event):
''' Send the email using the filled out textboxes.
Warn the user if they forget to fill part
of it out.
'''
From = self.fromTxt.GetValue()
To = self.toTxt.GetValue()
Subject = self.subjectTxt.GetValue()
text = self.messageTxt.GetValue()
colon = To.find(';')
period = To.find(',')
if colon != -1:
temp = To.split(';')
To = self.sendStrip(temp) #';'.join(temp)
elif period != -1:
temp = To.split(',')
To = self.sendStrip(temp) #';'.join(temp)
else:
pass
if To == '':
print 'add an address to the "To" field!'
dlg = wx.MessageDialog(None, 'Please add an address to the "To" field and try again', 'Error', wx.OK|wx.ICON_EXCLAMATION)
dlg.ShowModal()
dlg.Destroy()
elif Subject == '':
dlg = wx.MessageDialog(None, 'Please add a "Subject" and try again', 'Error', wx.OK|wx.ICON_EXCLAMATION)
dlg.ShowModal()
dlg.Destroy()
elif From == '':
lg = wx.MessageDialog(None, 'Please add an address to the "From" field and try again',
'Error', wx.OK|wx.ICON_EXCLAMATION)
dlg.ShowModal()
dlg.Destroy()
else:
msg = MIMEMultipart()
msg['From'] = From
msg['To'] = To
msg['Subject'] = Subject
msg['Date'] = formatdate(localtime=True)
msg.attach( MIMEText(text) )
if self.filepaths != []:
print 'attaching file(s)...'
for path in self.filepaths:
part = MIMEBase('application', "octet-stream")
part.set_payload( open(path,"rb").read() )
Encoders.encode_base64(part)
part.add_header('Content-Disposition', 'attachment; filename="%s"' % os.path.basename(path))
msg.attach(part)
# edit this to match your mail server (i.e. mail.myserver.com)
server = smtplib.SMTP('mail.myserver.org')
# open login dialog
dlg = LoginDlg(server)
res = dlg.ShowModal()
if dlg.loggedIn:
dlg.Destroy() # destroy the dialog
try:
failed = server.sendmail(From, To, msg.as_string())
server.quit()
self.Close() # close the program
except Exception, e:
print 'Error - send failed!'
print e
else:
if failed: print 'Failed:', failed
else:
dlg.Destroy()
Most of this you've seen before, so I'm only going to talk about the email module calls. Tha main part to noice is that to create an email message with attachments, you'll want to use the MIMEMultipart czll. I used it to add the "From", "To", "Subject" and "Date" fields. To attach files, you'll need to use MIMEBase. Finally, to send the email, you'll need to set the SMTP server, which I did using the smptlib library and login, which is what the LoginDlg is for. I'll go over that next, but before I do I would like to recommend reading both module's respective documentation for full details as they much more functionality that I do not use in this example.
I noticed that my code didn't work outside my organization and it took me a while to figure out why. It turns out that when I'm logged in at work, I'm also logged into our webmail system, so I don't need to authenticate with it. When I'm outside, I do. Since this is actually pretty normal procedure for SMTP servers, I included a fairly simple login dialog. Let's take a look at the code:
class LoginDlg(wx.Dialog):
def __init__(self, server):
wx.Dialog.__init__(self, None, -1, 'Login', size=(190,150))
self.server = server
self.loggedIn = False
# widgets
userLbl = wx.StaticText(self, wx.ID_ANY, 'Username:', size=(50, -1))
self.userTxt = wx.TextCtrl(self, wx.ID_ANY, '')
passwordLbl = wx.StaticText(self, wx.ID_ANY, 'Password:', size=(50, -1))
self.passwordTxt = wx.TextCtrl(self, wx.ID_ANY, '', size=(150, -1),
style=wx.TE_PROCESS_ENTER|wx.TE_PASSWORD)
loginBtn = wx.Button(self, wx.ID_YES, 'Login')
cancelBtn = wx.Button(self, wx.ID_ANY, 'Cancel')
self.Bind(wx.EVT_BUTTON, self.OnLogin, loginBtn)
self.Bind(wx.EVT_TEXT_ENTER, self.OnTextEnter, self.passwordTxt)
self.Bind(wx.EVT_BUTTON, self.OnClose, cancelBtn)
# sizer / layout
userSizer = wx.BoxSizer(wx.HORIZONTAL)
passwordSizer = wx.BoxSizer(wx.HORIZONTAL)
btnSizer = wx.BoxSizer(wx.HORIZONTAL)
mainSizer = wx.BoxSizer(wx.VERTICAL)
userSizer.Add(userLbl, 0, wx.ALL, 5)
userSizer.Add(self.userTxt, 0, wx.ALL, 5)
passwordSizer.Add(passwordLbl, 0, wx.LEFT|wx.RIGHT, 5)
passwordSizer.Add(self.passwordTxt, 0, wx.LEFT, 5)
btnSizer.Add(loginBtn, 0, wx.ALL, 5)
btnSizer.Add(cancelBtn, 0, wx.ALL, 5)
mainSizer.Add(userSizer, 0, wx.ALL, 0)
mainSizer.Add(passwordSizer, 0, wx.ALL, 0)
mainSizer.Add(btnSizer, 0, wx.ALL|wx.CENTER, 5)
self.SetSizer(mainSizer)
self.Fit()
self.Layout()
def OnTextEnter(self, event):
''' When enter is pressed, login method is run. '''
self.OnLogin('event')
def OnLogin(self, event):
'''
When the "Login" button is pressed, the credentials are authenticated.
If correct, the email will attempt to be sent. If incorrect, the user
will be notified.
'''
try:
user = self.userTxt.GetValue()
pw = self.passwordTxt.GetValue()
res = self.server.login(user, pw)
self.loggedIn = True
self.OnClose('')
except:
message = 'Your username or password is incorrect. Please try again.'
dlg = wx.MessageDialog(None, message, 'Login Error', wx.OK|wx.ICON_EXCLAMATION)
dlg.ShowModal()
dlg.Destroy()
def OnClose(self, event):
self.Close()
For the most part, we've seen this before. The primary part to notice is that I have added two styles to my password TextCtrl: wx.TE_PROCESS_ENTER and wx.TE_PASSWORD. The first will allow you to press enter to login rather than pressing the Login button explicitly. The TE_PASSWORD style obscures the text typed into the TextCtrl with black circles or asterisks.
Also you should note that your username may include your email's url too. For example, rather than just username, it may be username@hotmail.com. Fortunately, if the login is incorrect, the program will throw an error and display a dialog letting the user know.
The final thing to do on Windows is to set it to use this script when the user clicks on a mailto link. To do this, you'll need to mess with the Windows Registry. Before you do anything with the registry, be sure to back it up as there's always a chance that you may break something, including the OS.
To begin, go to Start, Run and type regedit. Now navigate to the following location:
HKEY_CLASSES_ROOT\mailto\shell\open\command
Just expand the tree on the right to navigate the tree. One there, you'll need to edit the (Default) key on the right. It should be of type REG_SZ. Just double-click it to edit it. Here's what you'll want to put in there:
cmd /C "SET PYTHONHOME=c:\path\to\Python24&&c:\path\to\python24\python.exe c:\path\to\wxPyMail.py %1"
Basically, this tells Windows to set Python's home directory and the path to the python.exe as an environmental variable, which I think is only temporary. It then passes the wxPyMail.py script we created to the python.exe specified. The "%1" are the arguments passed by the mailto link. Once you hit the OK button, it's saved and should just start working.
Now you know how to make a fully functional application with wxPython. In my next post, I will show how to package it up as an executable so you can distribute it.
Some possible improvements that you could add:
Further Reading:
Download the Source:
Copyright © 2025 Mouse Vs Python | Powered by Pythonlibrary