Python 102: How to Send an Email Using smtplib + email

I wrote an article on this topic several years ago, but I think it is time for me to revisit it. Why? Well, lately I've been doing a lot of work on a program for sending emails and I've been looking at my old article and thinking I missed a few things when I first wrote it. So in this article we will learn the following:

  • The basics of emailing - kind of a rehash of the original article
  • How to send email using the TO, CC and BCC lines
  • How to send to multiple addresses at once
  • How to add an attachment and a body using the email module

Let's get started!

How to Send an Email with Python with smtplib

We will begin with a slightly modified version of the code from the original article. I noticed that I had forgotten to set up the HOST variable in the original, so this example will be a little more complete:

import smtplib
import string
 
HOST = "mySMTP.server.com"
SUBJECT = "Test email from Python"
TO = "mike@someAddress.org"
FROM = "python@mydomain.com"
text = "Python rules them all!"
BODY = string.join((
        "From: %s" % FROM,
        "To: %s" % TO,
        "Subject: %s" % SUBJECT ,
        "",
        text
        ), "\r\n")
server = smtplib.SMTP(HOST)
server.sendmail(FROM, [TO], BODY)
server.quit()

You will note that this code doesn't have a username or password in it. If your server requires authentication, then you'll need to add the following code:

server.login(username, password)

This should be added right after you create the server object. Normally, you would want to put this code into a function and call it with some of these parameters. You might even want to put some of this information into a config file. Let's do that next.

#----------------------------------------------------------------------
def send_email(host, subject, to_addr, from_addr, body_text):
    """
    Send an email
    """
    BODY = string.join((
            "From: %s" % from_addr,
            "To: %s" % to_addr,
            "Subject: %s" % subject ,
            "",
            body_text
            ), "\r\n")
    server = smtplib.SMTP(host)
    server.sendmail(from_addr, [to_addr], BODY)
    server.quit()
    
if __name__ == "__main__":
    host = "mySMTP.server.com"
    subject = "Test email from Python"
    to_addr = "mike@someAddress.org"
    from_addr = "python@mydomain.com"
    body_text = "Python rules them all!"
    send_email(host, subject, to_addr, from_addr, body_text)

Now you can see how small the actual code is by just looking at the function itself. That's 13 lines! And we could make it shorter if we didn't put every item in the BODY on its own line, but it wouldn't be as readable. Now we'll add a config file to hold the server information and the from address. Why? Well in the work I do, we might use different email servers to send email or if the email server gets upgraded and the name changes, then we only need to change the config file rather than the code. The same thing could apply to the from address if our company was bought and merged into another. We'll be using the configObj package instead of Python's ConfigParser as I find configObj simpler. You should go on over to the Python Package Index (PyPI) and download a copy if you don't already have it.

Let's take a look at the config file:

[smtp]
server = some.server.com
from_addr = python@mydomain.com

That is a very simple config file. In it we have a section labeled smtp in which we have two items: server and from_addr. We'll use configObj to read this file and turn it into a Python dictionary. Here's the updated version of the code:

import os
import smtplib
import string
import sys

from configobj import ConfigObj

#----------------------------------------------------------------------
def send_email(subject, to_addr, body_text):
    """
    Send an email
    """
    base_path = os.path.dirname(os.path.abspath(__file__))
    config_path = os.path.join(base_path, "config.ini")
    
    if os.path.exists(config_path):
        cfg = ConfigObj(config_path)
        cfg_dict = cfg.dict()
    else:
        print "Config not found! Exiting!"
        sys.exit(1)
        
    host = cfg_dict["smtp"]["server"]
    from_addr = cfg_dict["smtp"]["from_addr"]
    
    BODY = string.join((
            "From: %s" % from_addr,
            "To: %s" % to_addr,
            "Subject: %s" % subject ,
            "",
            body_text
            ), "\r\n")
    server = smtplib.SMTP(host)
    server.sendmail(from_addr, [to_addr], BODY)
    server.quit()
    
if __name__ == "__main__":
    subject = "Test email from Python"
    to_addr = "mike@someAddress.org"
    body_text = "Python rules them all!"
    send_email(subject, to_addr, body_text)

We've added a little check to this code. We want to first grab the path that the script itself is in, which is what base_path represents. Next we combine that path with the file name to get a fully qualified path to the config file. We then check for the existence of that file. If it's there, we create a dictionary and if it's not, we print a message and exit the script. We should add an exception handler around the ConfigObj call just to be on the safe side though as the file could exist, but be corrupt or we might not have permission to open it and that will throw an exception. That will be a little project that you can attempt on your own. Anyway, let's say that everything goes well and we get our dictionary. Now we can extract the host and from_addr information using normal dictionary syntax.

Now we're ready to learn how to send multiple emails at the same time!

How to Send Multiple Emails at Once

If you do a search on the web on this topic, you'll likely come across this StackOverflow question where we can learn for sending multiple emails via the smtplib module. Let's modify our last example a little so we send multiple emails!

import os
import smtplib
import string
import sys

from configobj import ConfigObj

#----------------------------------------------------------------------
def send_email(subject, body_text, emails):
    """
    Send an email
    """
    base_path = os.path.dirname(os.path.abspath(__file__))
    config_path = os.path.join(base_path, "config.ini")
    
    if os.path.exists(config_path):
        cfg = ConfigObj(config_path)
        cfg_dict = cfg.dict()
    else:
        print "Config not found! Exiting!"
        sys.exit(1)
        
    host = cfg_dict["smtp"]["server"]
    from_addr = cfg_dict["smtp"]["from_addr"]
    
    BODY = string.join((
            "From: %s" % from_addr,
            "To: %s" % ', '.join(emails),
            "Subject: %s" % subject ,
            "",
            body_text
            ), "\r\n")
    server = smtplib.SMTP(host)
    server.sendmail(from_addr, emails, BODY)
    server.quit()
    
if __name__ == "__main__":
    emails = ["mike@example.org", "someone@gmail.com"]
    subject = "Test email from Python"
    body_text = "Python rules them all!"
    send_email(subject, body_text, emails)

You'll notice that in this example, we removed the to_addr parameter and added an emails parameter, which is a list of email addresses. To make this work, we need to create a comma-separated string in the To: portion of the BODY and also pass the email list to the sendmail method. Thus we do the following to create a simple comma separated string: ', '.join(emails). Simple, huh?

Now we just need to figure out how to send using the CC and BCC fields. Let's create a new version of this code that supports that functionality!

import os
import smtplib
import string
import sys

from configobj import ConfigObj

#----------------------------------------------------------------------
def send_email(subject, body_text, to_emails, cc_emails, bcc_emails):
    """
    Send an email
    """
    base_path = os.path.dirname(os.path.abspath(__file__))
    config_path = os.path.join(base_path, "config.ini")
    
    if os.path.exists(config_path):
        cfg = ConfigObj(config_path)
        cfg_dict = cfg.dict()
    else:
        print "Config not found! Exiting!"
        sys.exit(1)
        
    host = cfg_dict["smtp"]["server"]
    from_addr = cfg_dict["smtp"]["from_addr"]
    
    BODY = string.join((
            "From: %s" % from_addr,
            "To: %s" % ', '.join(to_emails),
            "CC: %s" % ', '.join(cc_emails),
            "BCC: %s" % ', '.join(bcc_emails),
            "Subject: %s" % subject ,
            "",
            body_text
            ), "\r\n")
    emails = to_emails + cc_emails + bcc_emails
    
    server = smtplib.SMTP(host)
    server.sendmail(from_addr, emails, BODY)
    server.quit()
    
if __name__ == "__main__":
    emails = ["mike@somewhere.org"]
    cc_emails = ["someone@gmail.com"]
    bcc_emails = ["schmuck@newtel.net"]
    
    subject = "Test email from Python"
    body_text = "Python rules them all!"
    send_email(subject, body_text, emails, cc_emails, bcc_emails)

In this code, we pass in 3 lists, each with one email address a piece. We create the CC and BCC fields exactly the same as before, but we also need to combine the 3 lists into one so we can pass the combined list to the sendmail() method. There is some talk on StackOverflow that some email clients may handle the BCC field in odd ways that allow the recipient to see the BCC list via the email headers. I am unable to confirm this behavior, but I do know that Gmail successfully strips the BCC information from the email header. I haven't found a client that doesn't, but if you have, feel free to let us know in the comments.

Now we're ready to move on to using Python's email module!

Sending Email Attachments with Python

Now we'll take what we learned from the previous section and mix it together with the Python email module. The email module makes adding attachments extremely easy. Here's the code:

import os
import smtplib
import string
import sys

from configobj import ConfigObj
from email import Encoders
from email.mime.text import MIMEText
from email.MIMEBase import MIMEBase
from email.MIMEMultipart import MIMEMultipart
from email.Utils import formatdate

#----------------------------------------------------------------------
def send_email_with_attachment(subject, body_text, to_emails,
                               cc_emails, bcc_emails, file_to_attach):
    """
    Send an email with an attachment
    """
    base_path = os.path.dirname(os.path.abspath(__file__))
    config_path = os.path.join(base_path, "config.ini")
    header = 'Content-Disposition', 'attachment; filename="%s"' % file_to_attach
    
    # get the config
    if os.path.exists(config_path):
        cfg = ConfigObj(config_path)
        cfg_dict = cfg.dict()
    else:
        print "Config not found! Exiting!"
        sys.exit(1)
        
    # extract server and from_addr from config
    host = cfg_dict["smtp"]["server"]
    from_addr = cfg_dict["smtp"]["from_addr"]
    
    # create the message
    msg = MIMEMultipart()
    msg["From"] = from_addr
    msg["Subject"] = subject
    msg["Date"] = formatdate(localtime=True)
    if body_text:
        msg.attach( MIMEText(body_text) )
    
    msg["To"] = ', '.join(to_emails)
    msg["cc"] = ', '.join(cc_emails)
    
    attachment = MIMEBase('application', "octet-stream")
    try:
        with open(file_to_attach, "rb") as fh:
            data = fh.read()
        attachment.set_payload( data )
        Encoders.encode_base64(attachment)
        attachment.add_header(*header)
        msg.attach(attachment)
    except IOError:
        msg = "Error opening attachment file %s" % file_to_attach
        print msg
        sys.exit(1)
    
    emails = to_emails + cc_emails
    
    server = smtplib.SMTP(host)
    server.sendmail(from_addr, emails, msg.as_string())
    server.quit()
    
if __name__ == "__main__":
    emails = ["mike@somewhere.org", "nedry@jp.net"]
    cc_emails = ["someone@gmail.com"]
    bcc_emails = ["anonymous@circe.org"]
    
    subject = "Test email with attachment from Python"
    body_text = "This email contains an attachment!"
    path = "/path/to/some/file"
    send_email_with_attachment(subject, body_text, emails, 
                               cc_emails, bcc_emails, path)

Here we have renamed our function and added a new argument, file_to_attach. We also need to add a header and create a MIMEMultipart object. The header could be created any time before we add the attachment. We add elements to the MIMEMultipart object (msg) like we would keys to a dictionary. You'll note that we have to use the email module's formatdate method to insert the properly formatted date. To add the body of the message, we need to create an instance of MIMEText. If you're paying attention, you'll see that we didn't add the BCC information, but you could easily do so by following the conventions in the code above. Next we add the attachment. We wrap it in an exception handler and use the with statement to extract the file and place it in our MIMEBase object. Finally we add it to the msg variable and we send it out. Notice that we have to convert the msg to a string in the sendmail method.

Wrapping Up

Now you know how to send out emails with Python. For those of you that like mini projects, you should go back and add additional error handling around the server.sendmail portion of the code in case something odd happens during the process, such as an SMTPAuthenticationError or SMTPConnectError. We could also beef up the error handling during the attachment of the file to catch other errors. Finally, we may want take those various lists of emails and create one normalized list that has removed duplicates. This is especially important if we are reading a list of email addresses from a file.

Also note that our from address is fake. We can spoof emails using Python and other programming languages, but that is very bad etiquette and possibly illegal depending on where you live. You have been warned! Use your knowledge wisely and enjoy Python for fun and profit!

Note: All email addresses in code above are fake. This code was tested using Python 2.6.6 on Windows 7.

Additional Reading

Download the Source

Copyright © 2024 Mouse Vs Python | Powered by Pythonlibrary