How to send E-mails with Python?

This blog introduces how to set up a fake SMTP server and send e-mails with python, and how to test sending e-mails with unittest.mock.

Imagine that we need to deliver some specific dashboards or report frequently, it’s more convenient to send them automatically rather than manually. In this blog I’ll talk about how to send an e-mail with Python in the following points:

  • Setting up a fake SMTP server with Python
  • Sending e-mails with Python
  • Test sending e-mails with unittest.mock

Setting up a fake SMTP server with Python

sudo python -m smtpd -n -c DebuggingServer localhost:25
  • -m mod : run library module as a script (terminates option list)
  • -c cmd : program passed in as string (terminates option list)

This command helps us to create a new debugging server, the messages will be discarded, and printed on stdout.

We use sudo in this case because we’re using port 25, if you don’t want that you can use a port higher than 1024.

Sending e-mails with Python

# sending_mail_by_python/sending_mail.py
from mailer import Message
import smtplib

def build_email(from_address, to_address, subject, content, attach_rpt):
    message = Message()
    message.From = from_address
    message.To = to_address
    message.Subject = subject
    message.Body = content
    message.attach(attach_rpt, mimetype='text/csv', charset='us-ascii')

    return message

def send_email(msg, host='', port=0):
    s = smtplib.SMTP(host, port, local_hostname="smtp.mydomain.com")
    result = s.sendmail(msg.From, msg.To, msg.as_string())
    s.quit()

    return result

For building mail, I created the build_email() function with mailer module to specify the sender, the destination, the subject, mail’s content, and attachment if you want. For sending mail by SMTP, I created the send_email() function with smtplib module. I firstly created an SMTP instance that encapsulates an SMTP connection with smtplib.SMTP, assigning host, port and local_hostname; then sending mail with this instance; exit when it finished.

If you want to send an E-mail from “from@domain.com” to “to@domain.com”, with “subject” as the subject, “message” as the content and “test_df.csv” as the attachment, using the SMTP instance “localhost:25”, you can do like:

msg = build_email('from@domain.com', ['to@domain.com'],
                  'subject', 'message', 'test_df.csv')
send_email(msg, 'localhost', 25)

Then you will get the following result in you terminal:

20200601-example

Test sending e-mails with unittest.mock

Mocking the SMTP instance means we replace the original SMTP instance with a mock, then we will test the functions above with it.

import unittest
from unittest.mock import patch, call
from sending_mail_by_python import sending_mail as target

class SendEmailTests(unittest.TestCase):
    def test_send_email(self):
        with patch("smtplib.SMTP") as smtp:
            from_address = "from@domain.com"
            to_address = ["to@domain.com"]

            msg = target.build_email(
                from_address, to_address, "subject", "message")
            target.send_email(msg)

            # Get instance of mocked SMTP object
            instance = smtp.return_value

At the beginning of the test, I applied unittest.mock.patch() as a context manager to mock the smtplib.SMTP class; then the elements for sending E-mail are specified. Since I’ve mocked the SMTP, there is no more need to detail the host and port. Before testing, I retrieved the instance of mocked SMTP object, which will be used in the following tests.

            # Checks the mock has been called at least one time
            self.assertTrue(instance.sendmail.called)

            # Check the mock has been called only once
            self.assertEqual(instance.sendmail.call_count, 1)

Now, let’s test! I first tested if the mocked SMTP instance is called and only called once.

            # Check built e-mail elements
            self.assertEqual(msg.From, from_address)
            self.assertEqual(msg.To, to_address)

            # Check sent e-mail elements
            self.assertEqual(instance.sendmail.mock_calls[0][1][0],
                             from_address)
            self.assertEqual(instance.sendmail.mock_calls[0][1][1], to_address)

Then check the E-mail’s elements are correspond with the assignment. If you want to test the functions with attachment, you can check like self.assertEqual(msg.attachments[0][0], 'test_df.csv').

            # Check the mock has been called ONLY once the given arguments and
            # keywords
            instance.sendmail.assert_called_once_with(msg.From,
                                                      msg.To,
                                                      msg.as_string())

Moreover, we can also check if sendmail() has been called only once with the given arguments by assert_called_once_with().

            # Check the mock' calls are equal to a specific list of calls in a
            # specific order
            self.assertEqual(instance.sendmail.mock_calls,
                             [call(msg.From, msg.To, msg.as_string())]

We can check the mock’ calls are equal to a specific list of calls in a specific order as well.

Reference