Automating Email Sending with Python smtplib: Practical Experience and Useful Tips
In the field of information technology, especially system operations, automation is always the golden rule. I often use Python to automate many daily tasks, from deploying scripts to system monitoring alerts. Among these, sending automated emails to report status, warn of errors, or announce the completion of a specific task is an indispensable part.
Today, I will share my experience and best practices when using Python’s smtplib library for automated email sending. This is a “native” Python library, which helps me deeply understand the email mechanism and freely customize everything.
Why is automated email necessary in IT work?
Imagine you’re managing dozens of services that need monitoring. Whenever an issue occurs, you want to be notified immediately, without spending time checking manually. Or at the end of each day/week, you need to compile system performance reports to send to your boss and colleagues. Doing these tasks manually is both time-consuming and prone to errors.
- Automated reports: Send periodic reports on performance, system logs, and script execution results.
- Alerts: Immediate notification when the system encounters issues, resource thresholds are exceeded, or unusual activity is detected (e.g., CPU over 90% or service unresponsive 5 minutes ago).
- Status notifications: Confirm task completion (e.g., successful backup, deployment finished).
- User authentication: Send OTP codes, password reset links.
Clearly, automated email is a powerful tool that helps optimize work and ensures smooth system operation.
Methods for automated email sending with Python: Which option is right for you?
When you’re just starting, you’ll find many libraries and approaches for sending emails with Python. I’ll compare them to give you an overview.
Approach 1: smtplib – The Backbone of Everything
smtplib is a built-in Python library that provides an interface for interacting with SMTP (Simple Mail Transfer Protocol) servers. In other words, it’s the lowest-level tool for you to “communicate” directly with email sending servers.
- Pros:
- Maximum control: You control every aspect of the email sending process, from connection to structure.
- No third-party libraries needed:
smtplibis built into Python, no additional installation required. - Provides deep insight: This is the best way to understand the technical mechanism of how emails are sent.
- Cons:
- Quite “basic”: To send HTML emails or attachments, you need to manually build complex MIME (Multipurpose Internet Mail Extensions) structures.
- Requires more code: Compared to higher-level libraries, you will write more lines of code for the same task.
Approach 2: Higher-Level Libraries (Yagmail, SendGrid, Mailjet…)
These libraries are built upon smtplib or utilize the APIs of professional email services like SendGrid, Mailjet. They simplify email sending by abstracting many technical details.
- Pros:
- Easy to use, less code: With just a few lines, you can send HTML emails with attachments.
- Integrated diverse features: Some libraries/services also provide templates, statistics, and email list management.
- Cons:
- Additional dependencies: You need to install these additional library packages into your project.
- Reduced customization: Abstraction can sometimes make deep customization difficult.
- Third-party service dependency: If you use SendGrid/Mailjet APIs, you will depend on the stability and policies of the provider.
Analysis and Choice
In my opinion, to truly understand and “customize” everything, smtplib is an ideal starting point. It offers the highest level of control and the flexibility needed for many automation tasks. Once you’ve mastered smtplib, transitioning to higher-level libraries (if needed) will be very easy.
In this article, I will delve into smtplib so you can grasp its fundamental operating mechanism. Together, we will build email sending functions from basic to complex, incorporating tips I’ve gathered during my work.
Preparing Your Toolkit: Email Account Configuration
Before diving into coding, you’ll need an email account to send mail. I recommend using a separate email account specifically for automation purposes. More importantly, ABSOLUTELY DO NOT use the MAIN PASSWORD for this account.
If you use Gmail (which is very common), create an “App password” by following these steps:
- Log in to your Google account.
- Go to Google Security page.
- Find “How you sign in to Google” and select “App passwords”. If you don’t see this option, you might not have 2-Step Verification enabled. Please enable this feature first.
- Select the application (“Mail”) and device (“Other”) for which you want to generate the password.
- Click “Generate”. You will receive a 16-character string. This is the “app password” we will use in Python code. Save it carefully.
App passwords are more secure because they are independent of your main password. If an app password is compromised, you only need to revoke it without affecting your entire account.
For other email providers (Outlook, Yahoo, or your company’s private SMTP server), the process is similar. You need to find their SMTP server information (host name) and port. Typically, the server address will be smtp.server.com (e.g., smtp.gmail.com) and the port will be 587 (TLS) or 465 (SSL).
Hands-on with smtplib: From Basic to Advanced
1. Sending Plain Text Emails
This is the simplest way to send emails, containing only plain text. It’s very useful for quick notifications.
Code Example:
import smtplib
from email.mime.text import MIMEText
import os
def send_plain_email(sender_email, sender_password, receiver_email, subject, body):
"""
Sends a plain text email.
"""
try:
# Configure SMTP server
smtp_server = "smtp.gmail.com"
port = 465 # For SSL
# Create email object. MIMEText helps format the content.
msg = MIMEText(body, 'plain', 'utf-8')
msg['Subject'] = subject
msg['From'] = sender_email
msg['To'] = receiver_email
# Connect and send email
# With smtplib.SMTP_SSL, the connection is encrypted (SSL) from the start
with smtplib.SMTP_SSL(smtp_server, port) as server:
server.login(sender_email, sender_password)
server.sendmail(sender_email, receiver_email, msg.as_string())
print("Plain text email sent successfully!")
return True
except Exception as e:
print(f"Error sending plain text email: {e}")
return False
# --- Usage (example) ---
# Tip: Read sensitive information from environment variables or config files
# SENDER_EMAIL = os.getenv("EMAIL_USER")
# SENDER_PASSWORD = os.getenv("EMAIL_APP_PASSWORD")
# RECEIVER_EMAIL = "[email protected]"
# SUBJECT = "System Status Report: OK"
# BODY = "Hello,\nThe system is operating stably. No errors detected.\nHave a productive workday!"
# if SENDER_EMAIL and SENDER_PASSWORD:
# send_plain_email(SENDER_EMAIL, SENDER_PASSWORD, RECEIVER_EMAIL, SUBJECT, BODY)
# else:
# print("Please configure EMAIL_USER and EMAIL_APP_PASSWORD environment variables.")
In the code snippet above, MIMEText is an important class from the email.mime.text module. It helps us create an email object with standard formatted content, ensuring that your email displays correctly on all clients.
2. Sending Sophisticated HTML Emails
When you need to send reports with tables, charts, or want emails to look more professional with formatting and colors, HTML is the optimal choice. I often use this feature to send automated daily/weekly summaries. This allows recipients to easily read and grasp important information.
Code Example:
import smtplib
from email.mime.text import MIMEText
import os
def send_html_email(sender_email, sender_password, receiver_email, subject, html_body):
"""
Sends an email with HTML content.
"""
try:
smtp_server = "smtp.gmail.com"
port = 465
# Set _subtype='html' to specify this is HTML content
msg = MIMEText(html_body, 'html', 'utf-8')
msg['Subject'] = subject
msg['From'] = sender_email
msg['To'] = receiver_email
with smtplib.SMTP_SSL(smtp_server, port) as server:
server.login(sender_email, sender_password)
server.sendmail(sender_email, receiver_email, msg.as_string())
print("HTML email sent successfully!")
return True
except Exception as e:
print(f"Error sending HTML email: {e}")
return False
# --- Usage (example) ---
# HTML_BODY = """
# <html>
# <body>
# <h2>Service Status Report</h2>
# <p>Hello,</p>
# <p>This is the system's daily <b>report</b>.</p>
# <table border="1" style="width:100%; border-collapse: collapse;">
# <tr style="background-color:#f2f2f2;">
# <th style="padding: 8px; text-align: left;">Service</th>
# <th style="padding: 8px; text-align: left;">Status</th>
# <th style="padding: 8px; text-align: left;">Notes</th>
# </tr>
# <tr>
# <td style="padding: 8px;">Web Server</td>
# <td style="padding: 8px; color: green; font-weight: bold;">Active</td>
# <td style="padding: 8px;">CPU < 50%</td>
# </tr>
# <tr>
# <td style="padding: 8px;">Database Server</td>
# <td style="padding: 8px; color: green; font-weight: bold;">Active</td>
# <td style="padding: 8px;">Stable connection</td>
# </tr>
# <tr>
# <td style="padding: 8px;">Monitoring Service</td>
# <td style="padding: 8px; color: red; font-weight: bold;">Error</td>
# <td style="padding: 8px;">Unresponsive 5 minutes ago</td>
# </tr>
# </table>
# <p>Sincerely,</p>
# <p>ITFromZero Automated System</p>
# </body>
# </html>
# """
# if SENDER_EMAIL and SENDER_PASSWORD:
# send_html_email(SENDER_EMAIL, SENDER_PASSWORD, RECEIVER_EMAIL, "Daily HTML Report", HTML_BODY)
The only difference is the _subtype='html' parameter in MIMEText. It tells the email client that the content is HTML code and should be rendered accordingly.
3. Sending Attachments: From Reports to Images
Many times, the email content alone is not enough. You need to attach log files, Excel reports, error screenshots, or any other documents. This is where MIMEMultipart and MIMEBase come into play.
Code Example:
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from email.mime.base import MIMEBase
from email import encoders
import os
def send_email_with_attachment(sender_email, sender_password, receiver_email, subject, body, attachment_path):
"""
Sends an email with an attachment.
"""
try:
smtp_server = "smtp.gmail.com"
port = 465
msg = MIMEMultipart() # Use MIMEMultipart to contain both text and attachments
msg['From'] = sender_email
msg['To'] = receiver_email
msg['Subject'] = subject
# Attach the text/HTML part of the email
msg.attach(MIMEText(body, 'plain', 'utf-8')) # Can be replaced with 'html' if sending HTML
# Handle attachments
if os.path.exists(attachment_path):
attachment = open(attachment_path, "rb") # Open file in binary mode (read binary)
part = MIMEBase('application', 'octet-stream') # Generic content type for binary files
part.set_payload(attachment.read())
encoders.encode_base64(part) # Base64 encode the attachment
# Add header so the email client knows this is an attachment and its filename
part.add_header('Content-Disposition',
f"attachment; filename= {os.path.basename(attachment_path)}")
msg.attach(part)
attachment.close()
print(f"Attached file: {os.path.basename(attachment_path)}")
else:
print(f"Warning: Attachment file does not exist: {attachment_path}")
with smtplib.SMTP_SSL(smtp_server, port) as server:
server.login(sender_email, sender_password)
server.sendmail(sender_email, receiver_email, msg.as_string())
print(f"Email with attachment sent successfully!")
return True
except Exception as e:
print(f"Error sending email with attachment: {e}")
return False
# --- Usage (example) ---
# Create a temporary file to attach
# temp_file_name = "report_2026_03_18.txt"
# with open(temp_file_name, "w", encoding="utf-8") as f:
# f.write("This is today's report content.\n")
# f.write("System monitoring data:\n")
# f.write("CPU Usage: 70%\n")
# f.write("Memory Usage: 85%\n")
#
# ATTACHMENT_PATH = temp_file_name
# if SENDER_EMAIL and SENDER_PASSWORD:
# send_email_with_attachment(SENDER_EMAIL, SENDER_PASSWORD, RECEIVER_EMAIL,
# "Attachment Report from System",
# "Please see the attached report regarding system status.",
# ATTACHMENT_PATH)
# os.remove(ATTACHMENT_PATH) # Delete temporary file after sending
Here, we use MIMEMultipart() to create an email containing multiple parts (e.g., text and attachments). Each attachment will be handled by MIMEBase, read in binary form, Base64 encoded, and necessary headers added for the email client to recognize it correctly.
Hard-earned Experience from Real-world Deployment
After many sleepless nights fixing bugs and optimizing automated scripts, I have some advice to help you avoid common mistakes:
1. Secure Login Information: ABSOLUTELY NO hardcoding!
This is a critically important principle. Placing email and password directly in the code creates a serious security vulnerability. Instead, use:
- Environment Variables: This is the method I use most often for scripts running on servers. For example:
import os
SENDER_EMAIL = os.getenv("EMAIL_USER")
SENDER_PASSWORD = os.getenv("EMAIL_APP_PASSWORD")
# Before running the script, you need to set environment variables
# export EMAIL_USER="[email protected]"
# export EMAIL_APP_PASSWORD="your_app_password"
.env file (with the python-dotenv library) or a YAML file (e.g., config.yaml in my project) to store sensitive information. Ensure this file is not committed to git.Secure Login Information: This is a critically important principle. Placing email and password directly in the code creates a serious security vulnerability. Instead, use:
2. Error Handling: Always!
Email sending systems are not always stable. The server might be busy, login information could be incorrect, or there might be network errors. Always wrap email sending code in a try-except block to catch and log error details. This helps you debug easily and makes your automated system more “resilient.”.
# Example of try-except already integrated into the functions above
# ...
except smtplib.SMTPAuthenticationError:
print("Authentication error: Incorrect username or password.")
except smtplib.SMTPConnectError:
print("SMTP connection error: Could not connect to server.")
except Exception as e:
print(f"An unexpected error occurred: {e}")
3. Rate Limits and Anti-Spam
Email service providers (Gmail, Outlook…) all impose limits on the number of emails you can send within a certain period, for example, 500 emails/day for a regular Gmail account. If you send too many, your account may be temporarily blocked or marked as spam. To avoid this, consider:
- Adding delays: If you need to send a large quantity, add a small pause between sends.
- Using professional email services: For enterprise applications that need to send thousands of emails, use services like SendGrid, Mailjet, Amazon SES.
4. Optimizing HTML Email Structure
Designing HTML emails is significantly different from designing websites. Many email applications (old Outlook, Gmail on mobile) do not fully support modern CSS. Keep HTML simple, use inline CSS, and always test rendering across various email clients before official use.
5. Thorough Testing
Before running email sending scripts in a production environment, always send a test email to your test address. Check if the email arrives, if the formatting is correct, and if attachments can be opened. Nobody wants an important report email to have formatting errors or fail to send!
Conclusion
smtplib may not be the “hottest” library, but it is a solid foundation for you to build any automated email sending system with Python. From simple notifications to complex reports with attachments, smtplib performs well, especially when you need high control and flexibility.
For me, Python and smtplib are always reliable tools for automation tasks, from small to large. Hopefully, through this article, you have gained enough knowledge and experience to start building your own automated email sending system. Don’t hesitate to experiment and customize to create the most suitable solution for your work!
