Learning how to automate emails with Python can save time when you need to send reports, reminders, alerts, invoices, or welcome messages. Python includes tools for building an email message and sending it through an SMTP server, so a simple script can replace repetitive manual work.
This guide shows a secure, beginner-friendly workflow with EmailMessage, smtplib, environment variables, HTML content, attachments, multiple recipients, scheduling, logging, and practical error handling.
You should already understand basic Python functions, exceptions, and file handling.
How Email Automation Works
Your Python program does not deliver a message directly to every recipient. It normally connects to an SMTP server provided by an email service. The server authenticates your account, accepts the message, and handles delivery.
Python’s official smtplib documentation explains the SMTP client. The email package documentation covers message construction and MIME content.
Important Security Rules
Do not place a real password or API key directly in your source code. Credentials committed to Git or copied into a tutorial can be exposed. Use environment variables, an application-specific password, or a transactional email provider’s token.
- Enable encryption with TLS or SSL.
- Use the minimum account permissions required.
- Do not log passwords or tokens.
- Respect provider limits and anti-spam rules.
- Send only to recipients who expect the message.
For larger applications, read the environment variables guide.
Set Environment Variables
Set the following values in your operating system or deployment platform:
SMTP_HOST=smtp.example.com
SMTP_PORT=587
[email protected]
SMTP_PASSWORD=your-app-password
[email protected]The exact host and port depend on your provider. Port 587 commonly uses STARTTLS. Port 465 commonly uses an SSL connection from the beginning.
Create a Plain-Text Email
import os
from email.message import EmailMessage
def build_message(recipient: str) -> EmailMessage:
message = EmailMessage()
message["Subject"] = "Your weekly Python report"
message["From"] = os.environ["EMAIL_FROM"]
message["To"] = recipient
message.set_content(
"""Hello!
Your automated report is ready.
Regards,
Python Bot"""
)
return messageEmailMessage handles headers and body encoding. It is clearer and safer than assembling raw email text manually.
Send the Message with STARTTLS
import os
import smtplib
def send_message(message) -> None:
host = os.environ["SMTP_HOST"]
port = int(os.environ.get("SMTP_PORT", "587"))
username = os.environ["SMTP_USERNAME"]
password = os.environ["SMTP_PASSWORD"]
with smtplib.SMTP(host, port, timeout=20) as server:
server.ehlo()
server.starttls()
server.ehlo()
server.login(username, password)
server.send_message(message)The context manager closes the connection automatically. The timeout prevents the script from waiting forever when the server is unavailable.
Complete First Example
import os
import smtplib
from email.message import EmailMessage
def create_email(recipient: str) -> EmailMessage:
message = EmailMessage()
message["Subject"] = "Automated Python message"
message["From"] = os.environ["EMAIL_FROM"]
message["To"] = recipient
message.set_content(
"""Hello!
This message was sent automatically with Python."""
)
return message
def send_email(message: EmailMessage) -> None:
with smtplib.SMTP(
os.environ["SMTP_HOST"],
int(os.environ.get("SMTP_PORT", "587")),
timeout=20,
) as server:
server.starttls()
server.login(
os.environ["SMTP_USERNAME"],
os.environ["SMTP_PASSWORD"],
)
server.send_message(message)
if __name__ == "__main__":
email = create_email("[email protected]")
send_email(email)
print("Email sent successfully.")Add an HTML Version
Provide plain text first, then add HTML as an alternative. Email clients that cannot display HTML still have readable content.
message.set_content("Your report is ready. Open the HTML version for details.")
message.add_alternative(
"""
<html>
<body>
<h1>Weekly Report</h1>
<p>Your automated report is ready.</p>
<p><strong>Completed tasks:</strong> 18</p>
</body>
</html>
""",
subtype="html",
)Keep HTML simple because email clients support a smaller set of web features than modern browsers.
Attach a File
Read the file as bytes, detect or define its MIME type, and attach it:
from pathlib import Path
report_path = Path("weekly-report.csv")
with report_path.open("rb") as file:
message.add_attachment(
file.read(),
maintype="text",
subtype="csv",
filename=report_path.name,
)Use the pathlib guide for portable paths and the CSV guide to generate report files.
Send to Multiple Recipients
For a message where recipients may see each other, use a list in the To header:
recipients = [
"[email protected]",
"[email protected]",
]
message["To"] = ", ".join(recipients)For private bulk communication, send separate messages or use Bcc carefully. A transactional email service is usually a better choice for large recipient lists because it provides delivery monitoring, bounce handling, and unsubscribe controls.
Personalize Messages from Data
customers = [
{"name": "Alex", "email": "[email protected]"},
{"name": "Sam", "email": "[email protected]"},
]
for customer in customers:
message = EmailMessage()
message["Subject"] = "Your account update"
message["From"] = os.environ["EMAIL_FROM"]
message["To"] = customer["email"]
message.set_content(
f"""Hello {customer['name']},
Your account update is ready."""
)
send_email(message)When processing many records, validate every address and decide what should happen after an individual failure.
Handle SMTP Errors
import smtplib
try:
send_email(message)
except smtplib.SMTPAuthenticationError:
print("Authentication failed. Check the account and app password.")
except smtplib.SMTPRecipientsRefused:
print("The server refused the recipient address.")
except smtplib.SMTPException as error:
print(f"SMTP error: {error}")
except OSError as error:
print(f"Network or system error: {error}")
else:
print("Message accepted by the SMTP server.")Being accepted by an SMTP server does not guarantee that the message reached the inbox. It may still bounce or be classified as spam.
Add Logging
import logging
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(levelname)s %(message)s",
)
try:
send_email(message)
except Exception:
logging.exception("Email delivery failed")
else:
logging.info("Email submitted to the SMTP server")Do not include message secrets or credentials in logs. The Python logging guide explains handlers, levels, and file rotation.
Schedule the Script
Keep scheduling outside the email code. On Linux and macOS, cron can run a script at a chosen time. On Windows, use Task Scheduler. Cloud platforms also provide scheduled jobs.
A good design has one function that creates a report, another that creates the email, and another that sends it. This separation makes the program easier to test and reuse.
Practical Project: Send a Daily Summary
import os
import smtplib
from datetime import date
from email.message import EmailMessage
def build_summary(completed: int, pending: int) -> str:
return f"""Daily summary for {date.today():%Y-%m-%d}
Completed tasks: {completed}
Pending tasks: {pending}
"""
def create_summary_email(recipient: str) -> EmailMessage:
message = EmailMessage()
message["Subject"] = f"Daily summary - {date.today():%Y-%m-%d}"
message["From"] = os.environ["EMAIL_FROM"]
message["To"] = recipient
message.set_content(build_summary(completed=12, pending=4))
return message
def send_email(message: EmailMessage) -> None:
with smtplib.SMTP(
os.environ["SMTP_HOST"],
int(os.environ.get("SMTP_PORT", "587")),
timeout=20,
) as server:
server.starttls()
server.login(
os.environ["SMTP_USERNAME"],
os.environ["SMTP_PASSWORD"],
)
server.send_message(message)
if __name__ == "__main__":
message = create_summary_email("[email protected]")
send_email(message)The date formatting is covered in the Python datetime guide.
Testing Email Automation
Do not send a real message in every automated test. Test message construction separately and mock the function that communicates with SMTP.
- Verify the subject, sender, and recipient.
- Check that the body contains expected values.
- Confirm that attachments have the correct filename.
- Mock authentication and sending failures.
- Use a test inbox before enabling production delivery.
The Pytest beginner guide introduces assertions and mocks.
Common Mistakes
- Hardcoding passwords in the script.
- Using the wrong SMTP port or encryption mode.
- Sending bulk messages from a personal mailbox.
- Exposing all recipient addresses in the To field.
- Not using a timeout.
- Ignoring provider rate limits.
- Creating HTML-only messages without a text alternative.
- Retrying immediately and endlessly after a failure.
Frequently Asked Questions
Can Python send email without a Gmail account?
Yes. You can use any provider that offers SMTP access or a supported email API.
Should I use SMTP or an email API?
SMTP is convenient for small scripts. An API is often better for production systems, large volumes, analytics, templates, and bounce handling.
Can the script run every day?
Yes. Use cron, Task Scheduler, a cloud scheduler, or a workflow system.
Can I attach PDF and Excel files?
Yes. Read the file as bytes and use the correct MIME type. Avoid sending sensitive files without appropriate protection.
Conclusion
Python email automation combines message construction, secure credential handling, SMTP communication, scheduling, and error reporting. Begin with a single test recipient, use environment variables, enable encryption, and add a timeout.
As the workflow grows, separate report generation from delivery, log meaningful events, test without contacting the real server, and move high-volume communication to a dedicated email service.






