56.5 smtplib: Sending Email
The smtplib module provides a client session object that can be used to send mail to any Internet machine running a Simple Mail Transfer Protocol (SMTP) or Extended SMTP (ESMTP) listener daemon. It abstracts the complexities of the SMTP protocol, allowing developers to focus on the content and structure of their email messages rather than the low-level network communication.
Establishing a Connection and Logging In
The first step in sending an email is to create an SMTP object and establish a connection to your mail server. The smtplib.SMTP constructor takes the server’s hostname and, optionally, a port number. For security, it is crucial to initiate a Transport Layer Security (TLS) encrypted connection using the .starttls() method. This encrypts all subsequent commands, protecting your login credentials and the email content from eavesdropping. Without this step, your authentication is sent in plaintext, which is a significant security vulnerability on any network.
import smtplib
# Establish a connection to the SMTP server
smtp_server = "smtp.gmail.com"
port = 587 # For STARTTLS
sender_email = "your_email@gmail.com"
password = "your_app_password" # Use an App Password, not your main password!
try:
server = smtplib.SMTP(smtp_server, port)
server.ehlo() # Identify yourself to the server
server.starttls() # Secure the connection
server.ehlo() # Re-identify after encryption
server.login(sender_email, password)
print("Login successful!")
except Exception as e:
print(f"Connection or login failed: {e}")
finally:
server.quit() # Ensure connection is always closed
Why it works this way: The ehlo() command is an ESMTP greeting that informs the server of the client’s capabilities. Calling it after starttls() is necessary because the encryption process resets the connection state, and the server needs to be made aware of the client’s capabilities again in the new encrypted context.
Crafting the Email Message with email
While smtplib handles the transmission, the structure of the email itself—including headers like From, To, Subject, and the message body—is built using the email package. The email.mime submodules provide classes to create MIME (Multipurpose Internet Mail Extensions) messages, which can handle plain text, HTML, and attachments. Using string formatting to create a message is error-prone and can lead to issues with encoding and missing headers; the email package ensures standards compliance.
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
# Create a multipart message container
message = MIMEMultipart("alternative")
message["Subject"] = "Test Email from Python"
message["From"] = sender_email
message["To"] = "recipient@example.com"
# Create the plain-text and HTML version of your message
text = """\
Hi,
This is a test email sent from Python.
"""
html = """\
<html>
<body>
<p>Hi,<br>
This is a <b>test email</b> sent from <a href="https://www.python.org">Python</a>.
</p>
</body>
</html>
"""
# Turn these into MIMEText objects
part1 = MIMEText(text, "plain")
part2 = MIMEText(html, "html")
# Attach parts into message container
message.attach(part1)
message.attach(part2)
Sending the Mail and Handling Errors
The actual sending of the email is done with the sendmail() method. It is critical to understand its parameters: the sender address, a list of recipient addresses, and the string representation of the entire MIME message. A common pitfall is passing the email.message.Message object directly; it must be converted to a string using .as_string(). The sendmail() method may raise exceptions for a variety of reasons, such as a rejected recipient or a network failure. These must be caught and handled gracefully. Furthermore, the quit() method must be called to properly close the connection to the server, which is why it’s often placed in a finally block.
try:
# ... connection and login code from above ...
# Send the composed message
server.sendmail(sender_email, "recipient@example.com", message.as_string())
print("Email sent successfully!")
except smtplib.SMTPRecipientsRefused as e:
print(f"All recipients were refused: {e}")
except smtplib.SMTPException as e:
print(f"SMTP error occurred: {e}")
except Exception as e:
print(f"An unexpected error occurred: {e}")
finally:
server.quit()
Common Pitfalls and Best Practices
- Authentication Errors: Modern email providers (Gmail, Outlook, etc.) often require an “App Password” for programmatic access instead of your primary account password. Using the main password will likely result in an
smtplib.SMTPAuthenticationError. - Ports and Security: The common ports are 587 (for STARTTLS) and 465 (for implicit SSL/TLS, using
SMTP_SSL). Always use an encrypted connection. Ifstarttls()fails, your connection is insecure. - Rate Limiting: Sending a large volume of emails quickly can trigger spam filters or cause your provider to temporarily suspend your account. Introduce delays between messages for bulk sending.
- Headers and Spam: Incorrectly formatted headers (especially
From,To,Subject) are a primary reason emails get marked as spam. Theemailpackage helps avoid these formatting errors. - Error Handling: Network operations are inherently unreliable. Your code must be robust enough to handle connection timeouts, server errors, and invalid recipient addresses without crashing. Use try-except blocks to catch specific
smtplibexceptions. - Context Manager (
withstatement): For cleaner code, theSMTPobject can be used as a context manager, which automatically callsquit()upon exit, even if an error occurs.
# Best practice: Using a context manager
with smtplib.SMTP(smtp_server, port) as server:
server.ehlo()
server.starttls()
server.ehlo()
server.login(sender_email, password)
server.sendmail(sender_email, "recipient@example.com", message.as_string())
print("Email sent and connection closed automatically.")