How to Send Emails with Python

Python provides a couple of really nice modules that you can use to craft emails with. They are the email and smtplib modules. Instead of going over various methods in these two modules, you'll spend some time learning how to actually use these modules.

Specifically, you'll be covering the following:

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

Let's get started!

Email Basics - How to Send an Email with smtplib

The smtplib module is very intuitive to use. You will write a quick example that shows how to send an email.

Open up your favorite Python IDE or text editor and create a new Python file. Add the following code to a that file and save it:

import smtplib

HOST = "mySMTP.server.com"
SUBJECT = "Test email from Python"
TO = "mike@someAddress.org"
FROM = "python@mydomain.com"
text = "Python 3.4 rules them all!"

BODY = "\r\n".join((
"From: %s" % FROM,
"To: %s" % TO,
"Subject: %s" % SUBJECT ,
"",
text
))

server = smtplib.SMTP(HOST)
server.sendmail(FROM, [TO], BODY)
server.quit()

Here you import only the smtplib module. Two-thirds of this code is used for setting up the email. Most of the variables are pretty self-explanatory, so you'll focus on the odd one only, which is BODY.

Here you use the string's join() method to combine all the previous variables into a single string where each line ends with a carriage return ("/r") plus new line ("/n"). If you print BODY out, it would look like this:

'From: python@mydomain.com\r\nTo: mike@mydomain.com\r\nSubject: Test email from Python\r\n\r\nblah blah blah'

After that, you set up a server connection to your host and then you call the smtplib module's sendmail method to send the email. Then you disconnect from the server. 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 put this code into a function.

import smtplib

def send_email(host, subject, to_addr, from_addr, body_text):
    """
    Send an email
    """
    BODY = "\r\n".join((
            "From: %s" % from_addr,
            "To: %s" % to_addr,
            "Subject: %s" % subject ,
            "",
            body_text
            ))
    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 you could make it shorter if you didn't put every item in the BODY on its own line, but it wouldn't be as readable. Now you'll add a config file to hold the server information and the from address.

Why would you do that? Many organizations use different email servers to send email or if the email server gets upgraded and the name changes, then you only need to change the config file rather than the code. The same thing could apply to the from address if your company was bought and merged into another.

Let's take a look at the config file (save it as email.ini):

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

That is a very simple config file. In it, you have a section labeled smtp in which you have two items: server and from_addr. you'll use ConfigParser to read this file and turn it into a Python dictionary. Here's the updated version of the code (save it as smtp_config.py)

import os
import smtplib
import sys

from configparser import ConfigParser

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, "email.ini")

    if os.path.exists(config_path):
        cfg = ConfigParser()
        cfg.read(config_path)
    else:
        print("Config not found! Exiting!")
        sys.exit(1)

    host = cfg.get("smtp", "server")
    from_addr = cfg.get("smtp", "from_addr")

    BODY = "\r\n".join((
        "From: %s" % from_addr,
        "To: %s" % to_addr,
        "Subject: %s" % subject ,
        "",
        body_text
    ))
    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)

You have added a little check to this code. You want to first grab the path that the script itself is in, which is what base_path represents. Next, you combine that path with the file name to get a fully qualified path to the config file. You then check for the existence of that file.

If it's there, you create a ConfigParser and if it's not, you print a message and exit the script. you should add an exception handler around the ConfigParser.read() call just to be on the safe side though as the file could exist, but be corrupt or you 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 you'll and the ConfigParser object is created successfully. Now you can extract the host and from_addr information using the usual ConfigParser syntax.

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

Sending Multiple Emails at Once

Being able to send multiple emails at once is a nice feature to have.

Go ahead and modify your last example a little so you can send multiple emails!

import os
import smtplib
import sys

from configparser import ConfigParser

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, "email.ini")

    if os.path.exists(config_path):
        cfg = ConfigParser()
        cfg.read(config_path)
    else:
        print("Config not found! Exiting!")
        sys.exit(1)

    host = cfg.get("smtp", "server")
    from_addr = cfg.get("smtp", "from_addr")

    BODY = "\r\n".join((
            "From: %s" % from_addr,
            "To: %s" % ', '.join(emails),
            "Subject: %s" % subject ,
            "",
            body_text
            ))
    server = smtplib.SMTP(host)
    server.sendmail(from_addr, emails, BODY)
    server.quit()

if __name__ == "__main__":
    emails = ["mike@someAddress.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, you removed the to_addr parameter and added an emails parameter, which is a list of email addresses. To make this work, you 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 you do the following to create a simple comma-separated string: ', '.join(emails). Simple, huh?

Send email using the TO, CC and BCC lines

Now you 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 sys

from configparser import ConfigParser

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, "email.ini")

    if os.path.exists(config_path):
        cfg = ConfigParser()
        cfg.read(config_path)
    else:
        print("Config not found! Exiting!")
        sys.exit(1)

    host = cfg.get("smtp", "server")
    from_addr = cfg.get("smtp", "from_addr")

    BODY = "\r\n".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
            ))
    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, you pass in 3 lists, each with one email address a piece. you create the CC and BCC fields exactly the same as before, but you also need to combine the 3 lists into one so you can pass the combined list to the sendmail() method.

There is some talk on forums like 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.

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

Add an attachment / body using the email module

Now you'll take what you learned from the previous section and mix it together with the Python email module so that you can send attachments.

The email module makes adding attachments extremely easy. Here's the code:

import os
import smtplib
import sys

from configparser import ConfigParser
from email import encoders
from email.mime.text import MIMEText
from email.mime.base import MIMEBase
from email.mime.multipart 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, "email.ini")
    header = 'Content-Disposition', 'attachment; filename="%s"' % file_to_attach

    # get the config
    if os.path.exists(config_path):
        cfg = ConfigParser()
        cfg.read(config_path)
    else:
        print("Config not found! Exiting!")
        sys.exit(1)

    # extract server and from_addr from config
    host = cfg.get("smtp", "server")
    from_addr = cfg.get("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@someAddress.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 you have renamed your function and added a new argument, file_to_attach. You also need to add a header and create a MIMEMultipart object. The header could be created any time before you add the attachment.

You add elements to the MIMEMultipart object (msg) like you would keys to a dictionary. You'll note that you have to use the email module's formatdate method to insert the properly formatted date.

To add the body of the message, you need to create an instance of MIMEText. If you're paying attention, you'll see that you didn't add the BCC information, but you could easily do so by following the conventions in the code above.

Next, you add the attachment. You wrap it in an exception handler and use the with statement to extract the file and place it in your MIMEBase object. Finally, you add it to the msg variable and you send it out. Notice that you 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.

One example would be a SMTPAuthenticationError or SMTPConnectError. You could also beef up the error handling during the attachment of the file to catch other errors. Finally, you may want to take those various lists of emails and create one normalized list that has removed duplicates. This is especially important if you are reading a list of email addresses from a file.

Also, note that your from address is fake. You 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!

Related Reading

Want to learn more Python basics? Then check out the following tutorials:

 

Copyright © 2021 Mouse Vs Python | Powered by Pythonlibrary