smtp_mailer
A production-ready Python library for sending HTML emails over SMTP with a clean, intuitive API. Supports STARTTLS and SSL, CC/BCC, file attachments, exponential-backoff retries, batch sending, and a context-manager interface.
Table of Contents
- Features
- Installation
- Quick Start
- Gmail Example
- Outlook Example
- SSL Example (port 465)
- Inline HTML Content
- CC and BCC
- Attachments Example
- Batch Sending
- Context Manager
- Error Handling
- API Reference
- Configuration Reference
- Security Notes
- Publishing to PyPI
Features
- ✅ HTML email body — via file path or inline string
- ✅ Multiple recipients (To, CC, BCC)
- ✅ File attachments (PDF, PNG, JPG, DOCX, TXT, ZIP, CSV, XLSX)
- ✅ STARTTLS (port 587) and SSL (port 465)
- ✅ Exponential-backoff retries for transient errors
- ✅ Configurable timeout
- ✅ Batch sending with per-recipient error tracking
- ✅ Context-manager interface
- ✅ Structured dict return value with
message_idandtimestamp - ✅ RFC-compliant email validation via
email-validator - ✅ Credentials never logged or exposed in exceptions
- ✅ Full type annotations (passes
mypy --strict) - ✅ >90% test coverage
Installation
pip install smtp_mailer
Requirements: Python 3.11+
Quick Start
from smtp_mailer import send_email
result = send_email(
html_file="welcome.html",
subject="Hello from smtp_mailer!",
sender_email="you@gmail.com",
app_password="xxxx xxxx xxxx xxxx",
receiver_email="friend@example.com",
)
print(result["message_id"]) # RFC 2822 Message-ID
print(result["timestamp"]) # ISO 8601 UTC timestamp
Gmail Example
Gmail requires an App Password when 2-Step Verification is enabled.
from smtp_mailer import send_email
send_email(
html_file="templates/welcome.html",
subject="Welcome to our service",
sender_email="you@gmail.com",
app_password="xxxx xxxx xxxx xxxx", # App Password, not your login password
receiver_email="customer@example.com",
smtp_server="smtp.gmail.com", # default
port=587, # default (STARTTLS)
)
Outlook Example
send_email(
html_file="newsletter.html",
subject="Monthly Newsletter",
sender_email="you@outlook.com",
app_password="your-app-password",
receiver_email="subscriber@example.com",
smtp_server="smtp-mail.outlook.com",
port=587,
)
SSL Example (port 465)
send_email(
html_file="notice.html",
subject="Secure Notice",
sender_email="you@example.com",
app_password="secret",
receiver_email="them@example.com",
smtp_server="mail.example.com",
port=465,
use_ssl=True,
)
Inline HTML Content
Use html_content instead of html_file to pass the email body as a string:
send_email(
html_content="<h1>Hello!</h1><p>This is an inline body.</p>",
subject="Inline HTML",
sender_email="you@gmail.com",
app_password="secret",
receiver_email="them@example.com",
)
Note:
html_fileandhtml_contentare mutually exclusive — providing both raises aValueError.
CC and BCC
send_email(
html_file="report.html",
subject="Q3 Report",
sender_email="analytics@company.com",
app_password="secret",
receiver_email="ceo@company.com",
cc=["cfo@company.com", "cto@company.com"],
bcc=["audit@company.com"],
)
BCC addresses are passed in the SMTP envelope only and never appear in email headers.
Attachments Example
send_email(
html_file="invoice_email.html",
subject="Your Invoice #1042",
sender_email="billing@company.com",
app_password="secret",
receiver_email="client@example.com",
attachments=[
"invoices/invoice_1042.pdf",
"assets/logo.png",
],
)
Allowed extensions: .pdf, .png, .jpg, .jpeg, .docx, .doc,
.txt, .zip, .csv, .xlsx
Max size: 25 MB per file.
Batch Sending
Send the same email to many recipients, tracking individual successes and failures:
from smtp_mailer import send_batch_email
results = send_batch_email(
html_file="promo.html",
subject="Exclusive Offer",
sender_email="marketing@company.com",
app_password="secret",
receiver_emails=[
"alice@example.com",
"bob@example.com",
"carol@example.com",
],
per_message_delay=0.5, # seconds between sends (avoids rate-limiting)
)
for recipient, outcome in results.items():
if isinstance(outcome, dict):
print(f"✓ {recipient} — message_id: {outcome['message_id']}")
else:
print(f"✗ {recipient} — error: {outcome}")
Context Manager
The SMTPMailer class stores credentials once and reuses them across sends:
from smtp_mailer import SMTPMailer
with SMTPMailer(
sender_email="you@gmail.com",
app_password="xxxx xxxx xxxx xxxx",
) as mailer:
# Single send
mailer.send(
html_file="welcome.html",
subject="Welcome!",
receiver_email="alice@example.com",
)
# Batch send
results = mailer.send_batch(
html_content="<p>Your weekly digest</p>",
subject="Weekly Digest",
receiver_emails=["bob@example.com", "carol@example.com"],
)
Error Handling
All package exceptions inherit from SMTPMailerError:
from smtp_mailer import send_email
from smtp_mailer.exceptions import (
SMTPMailerError,
InvalidEmailError,
AttachmentError,
AuthenticationError,
ConnectionError,
EmailSendError,
)
try:
send_email(
html_file="email.html",
subject="Hello",
sender_email="you@gmail.com",
app_password="secret",
receiver_email="friend@example.com",
)
except InvalidEmailError as exc:
print(f"Bad address: {exc.address}")
except AttachmentError as exc:
print(f"Attachment issue: {exc.path} — {exc.reason}")
except AuthenticationError:
print("Wrong credentials. Check your App Password.")
except ConnectionError as exc:
print(f"Cannot reach {exc.server}:{exc.port}")
except EmailSendError as exc:
print(f"Failed after {exc.attempts} attempt(s): {exc.reason}")
except SMTPMailerError as exc:
# Catch-all for any other package error
print(f"Email error: {exc}")
API Reference
send_email(**kwargs) -> dict
Send a single HTML email.
| Parameter | Type | Default | Description |
|---|---|---|---|
html_file |
str | None |
None |
Path to .html file (mutually exclusive with html_content) |
html_content |
str | None |
None |
Raw HTML string (mutually exclusive with html_file) |
subject |
str |
"" |
Email subject line |
sender_email |
str |
"" |
From address and SMTP login |
app_password |
str |
"" |
SMTP password / App Password |
receiver_email |
str | list[str] |
"" |
One or more To addresses |
smtp_server |
str |
"smtp.gmail.com" |
SMTP relay hostname |
port |
int |
587 |
TCP port |
use_ssl |
bool |
False |
Use SMTP_SSL (port 465) |
timeout |
int |
30 |
Socket timeout (seconds) |
cc |
list[str] | None |
None |
CC addresses |
bcc |
list[str] | None |
None |
BCC addresses (envelope only) |
attachments |
list[str] | None |
None |
File paths to attach |
retries |
int |
3 |
Total send attempts |
Returns:
{
"success": True,
"recipients": ["user@example.com"],
"subject": "Hello",
"timestamp": "2024-01-01T12:00:00+00:00",
"message_id": "550e8400-e29b-41d4-a716-446655440000",
}
send_batch_email(**kwargs) -> dict
Same parameters as send_email but accepts receiver_emails: list[str] and
per_message_delay: float (default 0.5). Returns a dict keyed by recipient address.
SMTPMailer(sender_email, app_password, ...)
Class-based interface. Constructor accepts sender_email, app_password, smtp_server,
port, use_ssl, timeout, and retries. Exposes .send(**kwargs) and
.send_batch(**kwargs) with the same signatures as the functional API.
Configuration Reference
Retry backoff: delays follow 1 × 2^(attempt-1) seconds — 1 s, 2 s, 4 s, etc.
Attachment limits:
- Allowed:
.pdf,.png,.jpg,.jpeg,.docx,.doc,.txt,.zip,.csv,.xlsx - Max size: 25 MB per file
Security Notes
- Never hard-code credentials. Use environment variables or a secrets manager.
- Passwords are never logged or included in exception messages.
- Sender addresses in logs are masked (
ab***@gmail.com). - BCC addresses are kept out of message headers.
import os
from smtp_mailer import send_email
send_email(
html_file="email.html",
subject="Hello",
sender_email=os.environ["SENDER_EMAIL"],
app_password=os.environ["APP_PASSWORD"],
receiver_email=os.environ["RECEIVER_EMAIL"],
)
Publishing to PyPI
1. Install build tools
pip install build twine
2. Build the distribution
python -m build
This creates dist/smtp_mailer-1.0.0.tar.gz and dist/smtp_mailer-1.0.0-py3-none-any.whl.
3. Upload to TestPyPI first
twine upload --repository testpypi dist/*
Verify the install:
pip install --index-url https://test.pypi.org/simple/ smtp_mailer
4. Upload to PyPI
twine upload dist/*
Tip: Use a PyPI API token (not your password) and store it in
~/.pypircor as theTWINE_PASSWORDenvironment variable.
License
MIT © smtp_mailer contributors
Release files for smtp-mailer-ezra 1.0.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| smtp_mailer_ezra-1.0.0.tar.gz | 24.7 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| smtp_mailer_ezra-1.0.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 44.6 kB
Release files / smtp_mailer_ezra-1.0.0.tar.gz
| Download URL | smtp_mailer_ezra-1.0.0.tar.gz |
|---|---|
| Size | 24.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
63abb06096266ebaa9ad75d7be7764906cb04928f83af08e2d76e7204579f93c
|
|
BLAKE2b-256 checksum How to use checksums |
eb3814aeff1cdd1cb42b4ed64bdddd71b1290ae94a9662bd12cd7712f708895e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.13.13
|
Release files / smtp_mailer_ezra-1.0.0-py3-none-any.whl
| Download URL | smtp_mailer_ezra-1.0.0-py3-none-any.whl |
|---|---|
| Size | 19.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
8008c43bd2c0c6ab407a6cc6c240575b14151ee558b3e9a4f56aa0bcc12710b7
|
|
BLAKE2b-256 checksum How to use checksums |
dbef27ed3fdcdce852b742d9cc8001534aa04e1e1fc3c244228598a19cc68e1b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.13.13
|