wxPyMail: Creating an Application to Send Emails

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:

wxPyMail - Emailing with Python

To follow along, you’ll need the following:

Creating the Interface

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.

Attaching an Email

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.

Editing Your Attachments

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.

Sending an Email

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.

Logging In

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.

Hacking the Registry

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.

Wrapping Up

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:

  • Store profile information (i.e. Your name, email, signature, etc)
  • Store email addresses in SQLite
  • Create a wizard for setting up the profile(s)
  • Add encryption to the emails.

Further Reading:

Download the Source:

4 thoughts on “wxPyMail: Creating an Application to Send Emails”

  1. Who knows where to download XRumer 5.0 Palladium?
    Help, please. All recommend this program to effectively advertise on the Internet, this is the best program!

  2. Pingback: How to Send Email with Python « The Mouse Vs. The Python

  3. “Well , the view of the passage is totally correct ,your details mbt shoes store is really reasonable and you guy give us mbt shoes valuable informative post, I totally agree the standpoint of upstairs. I often surfing on this forum when I m free and I find there are so much good information we can learn in this forum!

  4. Pingback: mafri.ws

Comments are closed.