How to send an email with Python?
Stefan Bogdanescu
Founder & Senior Architect
How to Send an Email with Python: Mastering smtplib and Avoiding Connection Errors
Sending emails is a fundamental task for any application, whether you are building a simple script or a complex web service. In the Python ecosystem, the built-in smtplib module provides the necessary tools to interface with Simple Mail Transfer Protocol (SMTP) servers. However, as demonstrated by the provided examples, moving from a straightforward script to encapsulating that logic within a function can introduce subtle but critical errors related to connection management and resource handling.
This post will walk you through the correct ways to send emails in Python, analyze why functional wrappers sometimes fail, and establish best practices for robust network communication.
The Direct Approach: Simple and Effective Scripting
When sending an email directly from a script, the process is straightforward. You connect to the SMTP server, authenticate (if required), and use the sendmail command. This approach works perfectly for one-off tasks where the connection lifespan is short and explicitly managed.
Here is the canonical way to send an email using smtplib:
import smtplib
# Configuration details
SERVER = "smtp.example.com" # Replace with your actual server
FROM_EMAIL = 'monty@python.com'
TO_EMAILS = ["jon@mycompany.com"]
SUBJECT = "Hello from Python!"
BODY = "This message was sent successfully using smtplib."
# Prepare the message content
message = f"From: {FROM_EMAIL}\r\nTo: {', '.join(TO_EMAILS)}\r\nSubject: {SUBJECT}\r\n\r\n{BODY}"
try:
# Connect to the server
server = smtplib.SMTP(SERVER, 587) # Use port 587 for TLS
server.ehlo()
server.starttls() # Secure the connection
server.login(FROM_EMAIL, "your_password") # Authentication step
server.sendmail(FROM_EMAIL, TO_EMAILS, message)
server.quit()
print("Email sent successfully!")
except smtplib.SMTPAuthenticationError as e:
print(f"Authentication failed: {e}")
except Exception as e:
print(f"An error occurred: {e}")
This direct method is clear, but it requires the developer to manually manage the connection lifecycle (server.quit()).
The Pitfall: Why Function Wrappers Fail
The issue arises when you attempt to wrap this logic into a reusable function, as seen in your second example. The traceback you observed—smtplib.SMTPServerDisconnected: Connection unexpectedly closed—is not an error in the sendmail command itself; rather, it signals a failure in how the underlying network socket connection is maintained within the function's scope.
When you define a function that handles external resources like network sockets, you must ensure that these resources are properly opened and closed, even if errors occur mid-process. In your functional attempt, the disconnect likely happens because:
- Improper Context Management: The raw
serverobject is created and used, but there’s no guarantee that all network states (like establishing TLS or sending commands) are completed before the function exits or encounters an exception. - Resource Leakage: If an exception occurs, the connection might be left open or improperly terminated by the server side, leading to the "Connection unexpectedly closed" error upon the next operation attempt.
In modern Python development, especially when dealing with I/O operations, we rely heavily on context managers (with statements) to handle resource setup and teardown automatically. This practice mirrors robust design principles found in larger frameworks like those in the Laravel ecosystem where reliable session management is crucial.
Best Practice: Using Context Managers for Robust Connections
To fix the disconnection issue and ensure your email sending logic is resilient, we should use smtplib within a with block. This guarantees that the connection is properly initialized and, most importantly, automatically closed, regardless of whether the code inside the block succeeds or fails.
Here is the revised, robust way to structure your function:
import smtplib
def send_email_robust(server_address, sender_email, password, recipient_emails, subject, body):
"""Sends an email using a context manager for safe connection handling."""
try:
# Use 'with' statement to manage the SMTP session automatically
with smtplib.SMTP(server_address, 587) as server:
server.ehlo()
server.starttls()
server.login(sender_email, password)
message = f"From: {sender_email}\r\nTo: {', '.join(recipient_emails)}\r\nSubject: {subject}\r\n\r\n{body}"
server.sendmail(sender_email, recipient_emails, message.encode('utf-8'))
print("Email sent successfully via context manager!")
except smtplib.SMTPAuthenticationError:
print("Error: Authentication failed. Check your email and password.")
except smtplib.SMTPServerDisconnected:
# This catches the specific error if the server drops the connection unexpectedly
print("Error: Connection unexpectedly closed by the SMTP server.")
except Exception as e:
print(f"An unexpected error occurred: {e}")
# Example call (Note: Replace placeholders with real credentials)
# send_email_robust("smtp.example.com", "sender@python.com", "password", ["recipient@test.com"], "Test", "Test body.")
Conclusion
Sending emails in Python is entirely achievable using smtplib. However, the difference between a working script and a production-ready function lies in handling external resources gracefully. By shifting from manually managing connection states to leveraging context managers like with, you ensure that your network operations are atomic, secure, and resilient against transient network errors. Always prioritize robust resource management when dealing with I/O operations, making your code cleaner and significantly more reliable.