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.
Sending emails via Gmail account using the Python module smtplib. |
Some posts related to emails I’ve written previously:
- GMail API: quick start with Python and NodeJs.
- GMail API: send emails with NodeJs.
- 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:
-
Gmail SMTP server address:
smtp.gmail.com
-
Gmail SMTP user name:
behai.van.nguyen@gmail.com
-
Gmail SMTP password:
gwrnafeanafjlgsj
-
SMTP ports:
465
and587
-- see also SMPT Port 465 and Port 587: What’s the Difference?
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
- Python module
smtplib and ports
587
,465
- HTML emails
- Emails with attachments
- Concluding remarks
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:
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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
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
andalternative
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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
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: