Email¶
Send emails via SMTP, console output, or in-memory backend for testing. Supports plain text, HTML with text fallback, CC/BCC, reply-to, custom headers, and bulk sending.
Quick Start¶
from hyperdjango.mail import send_mail, configure_mail
# Configure once at startup
configure_mail(
host="smtp.gmail.com",
port=587,
username="you@gmail.com",
password="app-password",
use_tls=True,
default_from="you@gmail.com",
)
# Send a simple email
await send_mail(
subject="Welcome!",
body="Thanks for signing up.",
recipients=["user@example.com"],
)
Configuration¶
configure_mail()¶
Configure the global email settings. Call once at application startup:
from hyperdjango.mail import configure_mail
configure_mail(
host="smtp.gmail.com",
port=587,
username="user@gmail.com",
password="app-password",
use_tls=True,
use_ssl=False,
default_from="noreply@myapp.com",
backend="smtp",
timeout=30,
)
Parameters¶
| Parameter | Type | Default | Description |
|---|---|---|---|
host |
str |
"localhost" |
SMTP server hostname |
port |
int |
587 |
SMTP server port |
username |
str |
"" |
SMTP authentication username |
password |
str |
"" |
SMTP authentication password |
use_tls |
bool |
True |
Use STARTTLS after connecting (port 587) |
use_ssl |
bool |
False |
Use implicit SSL connection (port 465) |
default_from |
str |
"noreply@localhost" |
Default sender address when from_email is omitted |
backend |
str |
"smtp" |
Backend type: "smtp", "console", or "memory" |
timeout |
int |
30 |
Connection timeout in seconds |
get_mail_config()¶
Retrieve the current configuration:
from hyperdjango.mail import get_mail_config
config = get_mail_config()
print(config.host) # "smtp.gmail.com"
print(config.backend) # "smtp"
Returns a MailConfig dataclass with all the fields above.
TLS vs SSL¶
use_tls=True(default): Connect to port 587 in plaintext, then upgrade to TLS viaSTARTTLS. This is the most common configuration for modern SMTP servers.use_ssl=True: Connect to port 465 with implicit SSL from the start. Used by some older servers.- Do not set both to
True. Ifuse_ssl=True, the TLS upgrade step is skipped since the connection is already encrypted.
Common Provider Configurations¶
Gmail (App Password):
configure_mail(
host="smtp.gmail.com",
port=587,
username="you@gmail.com",
password="xxxx-xxxx-xxxx-xxxx", # App password, not account password
use_tls=True,
)
Amazon SES:
configure_mail(
host="email-smtp.us-east-1.amazonaws.com",
port=587,
username="AKIAIOSFODNN7EXAMPLE",
password="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
use_tls=True,
default_from="noreply@yourdomain.com",
)
Mailgun:
configure_mail(
host="smtp.mailgun.org",
port=587,
username="postmaster@yourdomain.com",
password="your-mailgun-password",
use_tls=True,
)
SendGrid:
configure_mail(
host="smtp.sendgrid.net",
port=587,
username="apikey",
password="SG.your-api-key",
use_tls=True,
)
Backends¶
SMTP Backend (Production)¶
The default backend. Sends real emails via SMTP:
Connection flow:
- If
use_ssl: connect withSMTP_SSL(implicit SSL) - Else: connect with
SMTP, thenSTARTTLSifuse_tls - Login with
username/passwordif provided sendmail()to all recipients (To + CC + BCC)quit()to close connection
Console Backend (Development)¶
Prints emails to stdout instead of sending. Useful for development:
configure_mail(backend="console")
await send_mail(
subject="Test",
body="Hello",
recipients=["user@example.com"],
)
Output:
============================================================
EMAIL: Test
From: noreply@localhost
To: user@example.com
============================================================
Hello
============================================================
HTML body is printed separately if present:
Memory Backend (Testing)¶
Stores sent emails in an in-memory list. Perfect for automated tests:
configure_mail(backend="memory")
await send_mail(
subject="Test",
body="Hello",
recipients=["user@example.com"],
)
# Access sent messages
from hyperdjango.mail import get_outbox, clear_outbox
outbox = get_outbox()
assert len(outbox) == 1
assert outbox[0].subject == "Test"
assert "user@example.com" in outbox[0].recipients
# Clear between tests
clear_outbox()
The outbox is thread-safe (protected by a lock).
send_mail()¶
Convenience function for sending a single email:
from hyperdjango.mail import send_mail
success = await send_mail(
subject="Order Confirmation",
body="Your order #123 has been confirmed.",
recipients=["customer@example.com"],
from_email="orders@myapp.com",
html_body="<h1>Order Confirmed</h1><p>Order #123</p>",
cc=["accounting@myapp.com"],
bcc=["archive@myapp.com"],
)
Parameters¶
| Parameter | Type | Default | Description |
|---|---|---|---|
subject |
str |
required | Email subject line |
body |
str |
required | Plain text body |
recipients |
list[str] |
required | List of recipient email addresses (To field) |
from_email |
str |
"" |
Sender address. Falls back to default_from from config. |
html_body |
str |
"" |
Optional HTML body. Creates multipart/alternative message. |
cc |
list[str] \| None |
None |
CC recipients |
bcc |
list[str] \| None |
None |
BCC recipients (not visible in headers) |
Returns True on success, False on failure. Uses the globally configured backend.
EmailMessage¶
For full control over email construction:
from hyperdjango.mail import EmailMessage
msg = EmailMessage(
subject="Invoice #456",
body="Please find your invoice attached.",
recipients=["customer@example.com"],
from_email="billing@myapp.com",
cc=["accounting@myapp.com"],
bcc=["archive@myapp.com"],
reply_to="support@myapp.com",
headers={"X-Priority": "1", "X-Mailer": "HyperDjango"},
)
success = await msg.send()
Fields¶
| Field | Type | Default | Description |
|---|---|---|---|
subject |
str |
required | Subject line |
body |
str |
required | Plain text body |
recipients |
list[str] |
required | To addresses |
from_email |
str |
"" |
Sender address (falls back to config default) |
html_body |
str |
"" |
HTML body (creates multipart/alternative) |
cc |
list[str] |
[] |
CC addresses |
bcc |
list[str] |
[] |
BCC addresses (sent to but not in headers) |
reply_to |
str |
"" |
Reply-To address |
headers |
dict[str, str] |
{} |
Extra MIME headers |
send()¶
Send the email using the globally configured backend. Returns True on success.
MIME Construction¶
The EmailMessage builds the MIME message automatically:
- Plain text only (
html_bodyempty): SingleMIMEText("plain")message - HTML + plain text (
html_bodyset):MIMEMultipart("alternative")with bothtext/plainandtext/htmlparts. Email clients display whichever they prefer (HTML-capable clients show HTML, others show plain text).
Headers set automatically:
SubjectFromTo(comma-separated recipients)Cc(if cc provided)Reply-To(if reply_to provided)- Custom headers from
headersdict
BCC recipients are included in the sendmail call but not in the message headers.
HTML Email¶
Inline HTML¶
msg = EmailMessage(
subject="Newsletter",
body="This week's news in plain text.",
html_body="""
<html>
<body>
<h1>This Week's News</h1>
<p>Here are the highlights...</p>
</body>
</html>
""",
recipients=["subscriber@example.com"],
from_email="news@myapp.com",
)
await msg.send()
With Template Rendering¶
from hyperdjango.templating import render_template
html = render_template("emails/welcome.html", {
"user_name": "Alice",
"activation_url": "https://myapp.com/activate/abc123",
})
text = render_template("emails/welcome.txt", {
"user_name": "Alice",
"activation_url": "https://myapp.com/activate/abc123",
})
msg = EmailMessage(
subject="Welcome to MyApp!",
body=text,
html_body=html,
recipients=["alice@example.com"],
from_email="welcome@myapp.com",
)
await msg.send()
Bulk Email¶
send_mass_mail()¶
Send multiple emails over a single SMTP connection (more efficient than calling send_mail repeatedly):
from hyperdjango.mail import send_mass_mail
messages = [
("Subject 1", "Body 1", "from@example.com", ["to1@example.com"]),
("Subject 2", "Body 2", "from@example.com", ["to2@example.com"]),
("Subject 3", "Body 3", "from@example.com", ["to3@example.com"]),
]
await send_mass_mail(messages)
Each element is a tuple of (subject, body, from_email, recipients). A single SMTP connection is used for all messages.
Manual Bulk Sending¶
For more control (HTML, CC, headers), create EmailMessage instances and send them:
from hyperdjango.mail import EmailMessage
messages = []
for user in users:
msg = EmailMessage(
subject="Monthly Report",
body=f"Hi {user.name}, here is your report.",
html_body=render_template("emails/report.html", {"user": user}),
recipients=[user.email],
from_email="reports@myapp.com",
headers={"List-Unsubscribe": f"<mailto:unsub+{user.id}@myapp.com>"},
)
messages.append(msg)
for msg in messages:
await msg.send()
Password Reset Email¶
Built-in password reset flow with HMAC tokens:
from hyperdjango.auth.password_reset import send_password_reset_email
await send_password_reset_email(
user=user,
from_email="noreply@myapp.com",
subject="Reset Your Password",
reset_url_template="https://myapp.com/reset/{token}/",
)
This generates a time-limited HMAC token, constructs the reset URL, and sends an email. The token is validated when the user clicks the link.
Custom Reset Email¶
For custom styling or content:
from hyperdjango.auth.password_reset import generate_reset_token
token = generate_reset_token(user)
reset_url = f"https://myapp.com/reset/{token}/"
msg = EmailMessage(
subject="Reset Your Password",
body=f"Click here to reset: {reset_url}",
html_body=render_template("emails/reset.html", {
"user": user,
"reset_url": reset_url,
}),
recipients=[user.email],
from_email="noreply@myapp.com",
)
await msg.send()
Testing¶
Memory Backend¶
Use the memory backend to capture sent emails in tests:
from hyperdjango.mail import configure_mail, send_mail, get_outbox, clear_outbox
# Setup
configure_mail(backend="memory")
async def test_welcome_email():
clear_outbox()
await send_mail(
subject="Welcome!",
body="Hello",
recipients=["user@test.com"],
)
outbox = get_outbox()
assert len(outbox) == 1
msg = outbox[0]
assert msg.subject == "Welcome!"
assert msg.body == "Hello"
assert "user@test.com" in msg.recipients
assert msg.from_email == "" or msg.from_email # uses default_from
Testing HTML Content¶
async def test_html_email():
clear_outbox()
msg = EmailMessage(
subject="Invoice",
body="Your invoice.",
html_body="<h1>Invoice</h1>",
recipients=["user@test.com"],
cc=["copy@test.com"],
)
await msg.send()
outbox = get_outbox()
assert len(outbox) == 1
assert outbox[0].html_body == "<h1>Invoice</h1>"
assert outbox[0].cc == ["copy@test.com"]
Testing Password Reset¶
async def test_password_reset():
clear_outbox()
await send_password_reset_email(
user=user,
from_email="noreply@test.com",
subject="Reset",
reset_url_template="https://test.com/reset/{token}/",
)
outbox = get_outbox()
assert len(outbox) == 1
assert "reset" in outbox[0].body.lower()
Custom Headers¶
Add any MIME headers to the email:
msg = EmailMessage(
subject="Order #123",
body="Your order is confirmed.",
recipients=["customer@example.com"],
headers={
"X-Priority": "1",
"X-Mailer": "MyApp/1.0",
"Message-ID": "<order-123@myapp.com>",
"List-Unsubscribe": "<mailto:unsub@myapp.com>",
"References": "<original-msg-id@myapp.com>",
},
)
await msg.send()
Common headers:
| Header | Description |
|---|---|
X-Priority |
Email priority (1=high, 3=normal, 5=low) |
X-Mailer |
Identify your sending application |
Message-ID |
Unique message identifier |
In-Reply-To |
Message ID of the email being replied to |
References |
Chain of message IDs for threading |
List-Unsubscribe |
One-click unsubscribe support (required by some ESPs) |
Complete Example¶
from hyperdjango import HyperApp
from hyperdjango.mail import configure_mail, send_mail, EmailMessage
app = HyperApp("myapp")
# Configure at startup
configure_mail(
host="smtp.gmail.com",
port=587,
username="noreply@myapp.com",
password="app-password",
use_tls=True,
default_from="MyApp <noreply@myapp.com>",
)
@app.post("/contact")
async def contact(request):
data = request.json
msg = EmailMessage(
subject=f"Contact: {data['subject']}",
body=data["message"],
recipients=["support@myapp.com"],
from_email=data["email"],
reply_to=data["email"],
headers={"X-Contact-Form": "true"},
)
success = await msg.send()
if success:
return Response.json({"status": "sent"})
return Response.json({"status": "failed"}, status=500)
@app.post("/newsletter/subscribe")
async def subscribe(request):
data = request.json
await send_mail(
subject="Welcome to our newsletter!",
body="You're now subscribed.",
html_body="<h1>Welcome!</h1><p>You're now subscribed to our newsletter.</p>",
recipients=[data["email"]],
)
return Response.json({"status": "subscribed"})