Sending emails via Gmail account using the Python module smtplib.

We look at using our own Gmail account to send emails via the Python module smtplib. We’ll cover issues such as port 587 and port 465; plain text and HTML emails; emails with attachments; different classes used to create an email message.

Some posts related to emails I’ve written previously:

  1. GMail API: quick start with Python and NodeJs.
  2. GMail API: send emails with NodeJs.
  3. CI/CD #02. Jenkins: basic email using your Gmail account.

This post will continue on with some of the information presented in the last post in the above list: we’re using the same Gmail credential which we use in Jenkins. This time, we’re using this credential to send emails using the Python module smtplib — SMTP protocol client.

Since February, 2023, the time the above mentioned last post was written, there appear to be some changes in the Account page area: the Security screen no longer has the link App passwords.

However, as of June, 2023, this option is still available via the following link:

https://myaccount.google.com/apppasswords

We’ll get the App passwords screen:

We can delete existing ones and create new ones. The following screen from the above mentioned last post, where we generated a new one:

Following are the required information for the Gmail SMTP server:

Using the above Gmail SMTP information, we’ll demonstrate sending emails using the Python module smtplib. We’ll look at both ports 587 (TLS or Transport Layer Security mode), and 465 (SSL or Secure Socket Layer); plain text and HTML emails; emails with attachments; classes EmailMessage and MIMEMultipart (Multipurpose Internet Mail Extensions).

Table of contents

Script organisation

There’s no virtual environment, we’re not using any third party packages in this post, all scripts, and test emails’ attachment files are in the same directory. The Python executable is the global installation one.

The commands to run any script:

python <script_name.py>
python3 <script_name.py>

On Windows 10, all scripts have been tested with Python version 3.11.0. On Ubuntu 22.10, all scripts have been tested with Python version 3.10.7.

Common constants used across all scripts are defined in constants.py.

Content of constants.py:
host = 'smtp.gmail.com'
port_ssl = 465
port_tls = 587
# Use your own Gmail account.
user_name = 'YOUR_USER_NAME@gmail.com'
# Use your own Gmail account password: this password is no longer valid.
# I test the scripts with a different password.
password = 'gwrnafeanafjlgsj'
# Use your own test email account, which you have access to.
receiver_email = 'YOUR_USER_NAME@hotmail.com'

local_host = 'localhost'
local_port = 8500

text_email = ("""\
    Hello!

    Test email sent from Python SMTP Library, using {0}, port {1}.

    Official documentation: https://docs.python.org/3/library/smtplib.html

    Sent by script: {2}.

    ...behai
    """)

html_email = """\
    <html>
    <head></head>
    <body>
        <p>Hello!</p>

        <p>Test email sent from Python SMTP Library, using {0}, port {1}.</p>
        
        <p>
        Official documentation: <a href="https://docs.python.org/3/library/smtplib.html">
            smtplib</a> — SMTP protocol client.
        </p>

        <p>Sent by script: {2}.</p>

        <p>...behai</p>
    </body>
    </html>
"""

# Attachment files: they are in the same directory as the script files.
#
# happy_cat.jpg's URL: https://behainguyen.wordpress.com/wp-content/uploads/2023/06/happy_cat.jpg
jpg_filename = 'happy_cat.jpg'
# Use a small PDF as 'optus_payment.pdf', or change this value to some PDF name you
# have available.
pdf_filename = 'optus_payment.pdf'

def guess_mimetypes(file_name: str) -> tuple:
    """Guessing MIME maintype and subtype from a file name.

    See also `mimetypes <https://docs.python.org/3/library/mimetypes.html>`_. 

    :param str file_name: absolute file name whose MIME maintype and subtype 
        are to be determined.

    :return: MIME maintype and subtype as a tuple.
    :rtype: tuple.
    """
    import mimetypes

    mimetype, _ = mimetypes.guess_type(file_name)
    types = mimetype.split('/')
    
    return types[0], types[1]

Please read comments in module constants.py, you will need to substitute your own values for the noted constants.

Python module smtplib and ports 587, 465

In the context of the smtplib module, to use port 587 or port 465, requires (only) how the SMTP protocol client is created and initialised, from thence on, everything should be the same.

Let’s have a look some examples, we start with an example on port 587 (TLS), following by another one for port 465 (SSL).

Port 587 — TLS or Transport Layer Security mode

Content of tls_01.py:
from smtplib import (
    SMTP,
    SMTPHeloError,
    SMTPAuthenticationError,
    SMTPNotSupportedError,
    SMTPException,
    SMTPRecipientsRefused,
    SMTPSenderRefused,
    SMTPDataError,
)
import ssl
from email.message import EmailMessage

from constants import (
    host,
    port_tls,
    user_name,
    password,
    receiver_email,
    text_email,
)

server = SMTP(host, port_tls)
try:
    # server.ehlo() call can be omitted.
    server.ehlo()
    
    # Put the SMTP connection in TLS (Transport Layer Security) mode.
    # ssl.create_default_context(): secure SSL context.
    server.starttls(context=ssl.create_default_context())
    
    # server.ehlo() call can be omitted.
    server.ehlo()
    # SMTP server authentication.
    server.login(user_name, password)

    # Create and populate the email to be sent.
    msg = EmailMessage()

    msg['Subject'] = f'Test email: TLS/{port_tls}.'
    msg['From'] = user_name
    msg['To'] = receiver_email
    msg.set_content(text_email.format('TLS', port_tls, __file__))

    # Both send_message(...) and sendmail(...) work.
    # send_message(...) will eventually call to sendmail(...).
    #
    # server.send_message(msg)

    server.sendmail(user_name, receiver_email, msg.as_string())

    server.quit()

except SMTPHeloError as e:
    print("The server didn’t reply properly to the HELO greeting.")
    print(str(e))

except SMTPAuthenticationError as e:
    print("The server didn’t accept the username/password combination.")
    print(str(e))

except SMTPNotSupportedError as e:
    print("The AUTH command is not supported by the server.")
    print(str(e))

except SMTPException as e:
    print("No suitable authentication method was found.")
    print(str(e))

except SMTPRecipientsRefused as e:
    print("All recipients were refused. Nobody got the mail.")
    print(str(e))

except SMTPSenderRefused as e:
    print("The server didn’t accept the from_addr.")
    print(str(e))

except SMTPDataError as e:
    print("The server replied with an unexpected error code (other than a refusal of a recipient).")
    print(str(e))
  • Line 23: we create an instance of the SMTP protocol client via the class SMTP, using the Gmail host name, and port 587.
  • Lines 26 and 33: please see SMTP.ehlo(name=”). Leave them out, and this script still works.
  • Line 30: compulsory, we must call starttls(…) to put our SMTP connection to TLS (Transport Layer Security) mode, which uses port 587.
  • Line 35: Gmail requires authentication. We must call login(…).
  • Lines 38-43: we create a simple plain text email, using class EmailMessage.
  • Line 43: call method set_content(…) to set the actual plain text email message.
  • Line 50: send the email out. Please note, both sendmail(…) and send_message(…) work. I would actually prefer the latter.
  • Line 52: quit(), as per documentation, terminate the SMTP session and close the connection.
  • Lines 54-80: both login(…). and sendmail(…) can potentially raise exceptions. For illustration purposes, we’re listing out all exceptions which these two can potentially raise.

Port 465 — SSL or Secure Socket Layer

The SSL script, which uses port 465 follows.

Content of ssl_01.py:
from smtplib import (
    SMTP_SSL,
    SMTPHeloError,
    SMTPAuthenticationError,
    SMTPNotSupportedError,
    SMTPException,
    SMTPRecipientsRefused,
    SMTPSenderRefused,
    SMTPDataError,
)
import ssl
from email.message import EmailMessage

from constants import (
    host,
    port_ssl,
    user_name,
    password,
    receiver_email,
    text_email,
)

# ssl.create_default_context(): secure SSL context.
server = SMTP_SSL(host, port_ssl, context=ssl.create_default_context())
try:
    # SMTP server authentication.
    server.login(user_name, password)

    # Create and populate the email to be sent.
    msg = EmailMessage()
    
    msg['Subject'] = f'Test email: SSL/{port_ssl}.'
    msg['From'] = user_name
    msg['To'] = receiver_email
    msg.set_content(text_email.format('SSL', port_ssl, __file__))

    # Both send_message(...) and sendmail(...) work.
    # send_message(...) will eventually call to sendmail(...).
    #
    # server.send_message(msg)

    server.sendmail(user_name, receiver_email, msg.as_string())

    server.quit()

except SMTPHeloError as e:
    print("The server didn’t reply properly to the HELO greeting.")
    print(str(e))

except SMTPAuthenticationError as e:
    print("The server didn’t accept the username/password combination.")
    print(str(e))

except SMTPNotSupportedError as e:
    print("The AUTH command is not supported by the server.")
    print(str(e))

except SMTPException as e:
    print("No suitable authentication method was found.")
    print(str(e))

except SMTPRecipientsRefused as e:
    print("All recipients were refused. Nobody got the mail.")
    print(str(e))

except SMTPSenderRefused as e:
    print("The server didn’t accept the from_addr.")
    print(str(e))

except SMTPDataError as e:
    print("The server replied with an unexpected error code (other than a refusal of a recipient).")
    print(str(e))
  • Line 24: we create an instance of the SMTP protocol client via the class SMTP_SSL, using the Gmail host name, and port 465. This is the only difference to the above TLS script. The rest is pretty much identical.

From this point on, we will use TLS or port 587; we’ll also cut down the exception block.

🚀 We’ve covered plain text emails, and also the EmailMessage class.

HTML emails

We create and send emails in HTML using both classes EmailMessage and MIMEMultipart (Multipurpose Internet Mail Extensions).

For MINE type and MINE subtype, see this MDM Web Docs’ page MIME types (IANA media types).

HTML emails are “multipart” emails. They have a plain text version alongside the HTML version. For explanations, see this article Why You Shouldn’t Dismiss Plain Text Emails (And How to Make Them Engaging).

Using EmailMessage class

Almost identical to creating and sending plain text emails, we just need to add in the HTML content.

Content of tls_html_02.py:
from smtplib import SMTP
import ssl
from email.message import EmailMessage

from constants import (
    host,
    port_tls,
    user_name,
    password,
    receiver_email,
    text_email,
    html_email,
)

server = SMTP(host, port_tls)
try:
    # Put the SMTP connection in TLS (Transport Layer Security) mode.
    # ssl.create_default_context(): secure SSL context.
    server.starttls(context=ssl.create_default_context())
    # SMTP server authentication.
    server.login(user_name, password)

    msg = EmailMessage()

    msg['Subject'] = f'Test email: TLS/{port_tls}.'
    msg['From'] = user_name
    msg['To'] = receiver_email

    msg.set_content(text_email.format('TLS', port_tls, __file__), subtype='plain')
    msg.add_alternative(html_email.format('TLS', port_tls, __file__), subtype='html')

    # send_message(...) will eventually call to sendmail(...).
    # server.send_message(msg)

    server.sendmail(user_name, receiver_email, msg.as_string())

    server.quit()

except Exception as e:
    print("Some exception has occurred...")
    print(str(e))
  • Line 29: we pass in an additional named argument subtype='plain' to method set_content(…). This is optional, without this named argument, Hotmail still displays it as HTML.
  • Line 30: use method add_alternative(…) to set the HTML content. The named argument subtype='html' is required, without it, most likely mail clients would just display the plain text version. Hotmail does.

Using MIMEMultipart class

From the documentation, it seems that the MIMEMultipart class is older than the EmailMessage class, which has been introduced only in Python version 3.6. However, there is no mention of deprecation.

Content of tls_html_03.py:
from smtplib import SMTP
import ssl
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText

from constants import (
    host,
    port_tls,
    user_name,
    password,
    receiver_email,
    text_email,
    html_email,
)

server = SMTP(host, port_tls)
try:
    # Put the SMTP connection in TLS (Transport Layer Security) mode.
    # ssl.create_default_context(): secure SSL context.
    server.starttls(context=ssl.create_default_context())
    # SMTP server authentication.
    server.login(user_name, password)

    msg = MIMEMultipart('mixed')

    msg['Subject'] = f'Test email: TLS/{port_tls}.'
    msg['From'] = user_name
    msg['To'] = receiver_email

    msg_related = MIMEMultipart('related')
    msg_alternative = MIMEMultipart('alternative')

    # Attach parts into message container.
    # According to RFC 2046, the last part of a multipart message, in this case
    # the HTML message, is best and preferred.
    msg_alternative.attach(MIMEText(text_email.format('TLS', port_tls, __file__), 'plain'))
    msg_alternative.attach(MIMEText(html_email.format('TLS', port_tls, __file__), 'html'))

    msg_related.attach(msg_alternative)
    msg.attach(msg_related)

    # send_message(...) will eventually call to sendmail(...).
    server.send_message(msg)

    # server.sendmail(user_name, receiver_email, msg.as_string())

    server.quit()

except Exception as e:
    print("Some exception has occurred...")
    print(str(e))
  • Lines 24, 30, 31: we create the message instances using the MIMEMultipart class. On mixed, related and alternative values for _subtype — see this Stackoverflow answer, particularly the “MIME Hierarchies of Body Parts” chart.
  • Lines 36 and 37: we use the class MIMEText to create plain text and HTML content; as per the documentation, this class is used to create MIME objects of major type text.
  • Line 43: we switch to method send_message(…) to demonstrate that it works also.

🚀 We’ve covered HTML emails, using both EmailMessage and MIMEMultipart classes to create email messages to be sent.

Emails with attachments

We attach an image file and a PDF file to email messages. The process for other file types should be similar.

Using EmailMessage class

We also use an HTML email. Most of the code remains the same as the previous example.

Content of tls_html_04.py:
from smtplib import SMTP
import ssl
from email.message import EmailMessage

from constants import (
    host,
    port_tls,
    user_name,
    password,
    receiver_email,
    text_email,
    html_email,
    jpg_filename,
    pdf_filename,
    guess_mimetypes,
)

server = SMTP(host, port_tls)
try:
    # Put the SMTP connection in TLS (Transport Layer Security) mode.
    # ssl.create_default_context(): secure SSL context.
    server.starttls(context=ssl.create_default_context())
    # SMTP server authentication.
    server.login(user_name, password)

    msg = EmailMessage()

    msg['Subject'] = f'Test email: TLS/{port_tls}.'
    msg['From'] = user_name
    msg['To'] = receiver_email

    msg.set_content(text_email.format('TLS', port_tls, __file__), subtype='plain')
    msg.add_alternative(html_email.format('TLS', port_tls, __file__), subtype='html')

    with open(jpg_filename, 'rb') as fp:
        img_data = fp.read()
    mtype, stype = guess_mimetypes(jpg_filename)
    msg.add_attachment(img_data, maintype=mtype, subtype=stype, filename=jpg_filename)

    with open(pdf_filename, 'rb') as fp:
        pdf_data = fp.read()
    mtype, stype = guess_mimetypes(jpg_filename)
    msg.add_attachment(pdf_data, maintype=mtype, subtype=stype, filename=pdf_filename)

    # send_message(...) will eventually call to sendmail(...).
    server.send_message(msg)

    # server.sendmail(user_name, receiver_email, msg.as_string())

    server.quit()

except Exception as e:
    print("Some exception has occurred...")
    print(str(e))
  • Lines 35-43: call method add_attachment(…) to attach the image and the PDF files. The rest of the code we’ve gone through before.

It seems that we can get away with not having to worry about the message Content-Type property, which should be multipart/mixed in this case. Hotmail shows the correct Content-Type:

Using MIMEMultipart class

The script below is also an extension of the previous script in the section HTML email using MIMEMultipart class.

We’re using the class MIMEApplication to create email attachments.

Content of tls_html_05.py:
from os.path import basename
from smtplib import SMTP
import ssl
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.mime.application import MIMEApplication

from constants import (
    host,
    port_tls,
    user_name,
    password,
    receiver_email,
    text_email,
    html_email,
    jpg_filename,
    pdf_filename,
)

server = SMTP(host, port_tls)
try:
    # Put the SMTP connection in TLS (Transport Layer Security) mode.
    # ssl.create_default_context(): secure SSL context.
    server.starttls(context=ssl.create_default_context())
    # SMTP server authentication.
    server.login(user_name, password)

    msg = MIMEMultipart('mixed')

    msg['Subject'] = f'Test email: TLS/{port_tls}.'
    msg['From'] = user_name
    msg['To'] = receiver_email

    msg_related = MIMEMultipart('related')
    msg_alternative = MIMEMultipart('alternative')

    # Attach parts into message container.
    # According to RFC 2046, the last part of a multipart message, in this case
    # the HTML message, is best and preferred.
    msg_alternative.attach(MIMEText(text_email.format('TLS', port_tls, __file__), 'plain'))
    msg_alternative.attach(MIMEText(html_email.format('TLS', port_tls, __file__), 'html'))

    msg_related.attach(msg_alternative)

    with open(jpg_filename, 'rb') as fp:
        img_data = MIMEApplication(fp.read(), Name=basename(jpg_filename))
        img_data['Content-Disposition'] = f'attachment; filename="{basename(jpg_filename)}"'
    msg_related.attach(img_data)

    with open(pdf_filename, 'rb') as fp:
        pdf_data = MIMEApplication(fp.read(), Name=basename(pdf_filename))
        pdf_data['Content-Disposition'] = f'attachment; filename="{basename(pdf_filename)}"'
    msg_related.attach(pdf_data)
    
    msg.attach(msg_related)    

    # send_message(...) will eventually call to sendmail(...).
    server.send_message(msg)

    # server.sendmail(user_name, receiver_email, msg.as_string())

    server.quit()

except Exception as e:
    print("Some exception has occurred...")
    print(str(e))
  • Lines 45-53: we create attachments and attach them to the email message. For an explanation on Content-Disposition header, see this MDM Web Docs’ page Content-Disposition.

Content-Type as shown by Hotmail:

🚀 We’ve covered emails with attachments, using both EmailMessage and MIMEMultipart classes to create the email messages to be sent.

Concluding remarks

I hope I’ve not made any mistakes in this post. There’re a vast number of other methods in this area to study, we’ve only covered what I think is the most use case scenarios.

I think EmailMessage class requires less work. I would opt to use this class rather than the MIMEMultipart class.

Years ago, I’ve implemented a DLL in Delphi which pulls internal mail boxes at regular intervals and processes the emails. It would be interesting to look at the email reading functionalities of the smtplib.

Thank you for reading. I hope the information in this post is useful. Stay safe as always.

✿✿✿

Feature image sources:

One thought on “Sending emails via Gmail account using the Python module smtplib.”

Leave a comment

Design a site like this with WordPress.com
Get started