Emailο
The openviper.core.email package provides async-native email delivery with
Jinja2 templates, Markdown-to-HTML rendering, file and URL attachments, and
pluggable backends. Emails can be sent immediately or routed to a background
worker queue when the Background Tasks system is enabled.
The single public entry-point is send_email().
from openviper.core.email import send_email
await send_email(
recipients="alice@example.com",
subject="Welcome!",
text="Thanks for signing up.",
)
Settingsο
Email behaviour is configured through the EMAIL dictionary in your
settings module (settings.py):
# settings.py
EMAIL = {
# ββ SMTP connection βββββββββββββββββββββββββββββββββββββββββββ
"host": "smtp.example.com", # SMTP server hostname (default: "localhost")
"port": 587, # SMTP port (default: 25)
"username": "apikey", # SMTP auth username (optional)
"password": "SG.xxxx", # SMTP auth password (optional)
"use_tls": True, # STARTTLS after connect (default: False)
"use_ssl": False, # Implicit TLS / SMTPS (default: False)
"timeout": 10, # Connection timeout in seconds (default: 10)
# ββ Sender ββββββββββββββββββββββββββββββββββββββββββββββββββββ
"default_sender": "noreply@example.com", # Fallback From address
# "from" is accepted as an alias for "default_sender"
# ββ Backend βββββββββββββββββββββββββββββββββββββββββββββββββββ
"backend": "SMTPBackend", # "SMTPBackend" (default) or "ConsoleBackend"
# ββ Error handling ββββββββββββββββββββββββββββββββββββββββββββ
"fail_silently": False, # Swallow send errors (default: False)
# ββ Background delivery βββββββββββββββββββββββββββββββββββββββ
"background": None, # True / False / None (auto-detect)
}
Key |
Default |
Description |
|---|---|---|
|
|
SMTP server hostname. |
|
|
SMTP server port. Use |
|
|
SMTP authentication username. Also accepts the alias |
|
|
SMTP authentication password. |
|
|
Issue a |
|
|
Connect using implicit TLS ( |
|
|
Socket timeout in seconds. |
|
|
|
|
|
Delivery backend class name. |
|
|
When |
|
|
Controls background delivery. See Background Delivery below. |
Sending Emailο
See also
Working project that sends email:
examples/ecommerce_clone/ -
send_emailinOrder.after_insertlifecycle hook, full SMTPEMAILsettings
- send_email(recipients, subject, text=None, html=None, cc=None, bcc=None, attachments=None, template=None, context=None, fail_silently=None, background=None, sender=None) Awaitable[bool]ο
Send an email. Returns
Trueon success orFalsewhenfail_silentlyis enabled and delivery fails.- Parameters:
recipients β One or more recipient addresses. Accepts a single string or a list of strings.
subject β Email subject line.
text β Plain-text body. If omitted and
htmlis provided, a plain-text version is auto-generated by stripping HTML tags.html β HTML body.
cc β Carbon-copy addresses (string or list).
bcc β Blind carbon-copy addresses (string or list).
attachments β List of attachments. See Attachments.
template β Jinja2 template path relative to your templates directory. See Templates.
context β Template context dictionary.
fail_silently β Override the global
EMAIL["fail_silently"]setting for this call.background β Override background delivery for this call.
Trueforces queueing,Falseforces synchronous delivery.sender β Override the
Fromaddress for this call.
Plain textο
await send_email(
recipients="user@example.com",
subject="Order shipped",
text="Your order #1234 has been shipped.",
)
HTMLο
When both text and html are provided the message is sent as
multipart/alternative. When only html is given, a plain-text
fallback is auto-generated by stripping HTML tags:
await send_email(
recipients="user@example.com",
subject="Weekly digest",
html="<h1>Your weekly digest</h1><p>3 new items this week.</p>",
)
Multiple recipients, CC, and BCCο
await send_email(
recipients=["alice@example.com", "bob@example.com"],
subject="Team update",
text="Sprint review tomorrow at 10 AM.",
cc="manager@example.com",
bcc=["hr@example.com", "archive@example.com"],
)
Duplicate addresses across recipients, cc, and bcc are
automatically deduplicated at the SMTP transport level.
Custom senderο
await send_email(
recipients="support@example.com",
subject="Feedback",
text="Great product!",
sender="feedback@myapp.com",
)
Error handlingο
By default, delivery errors raise. Use fail_silently to suppress them:
success = await send_email(
recipients="user@example.com",
subject="Notification",
text="Hello",
fail_silently=True,
)
if not success:
logger.warning("Email delivery failed silently")
Templatesο
Pass a template path (relative to your TEMPLATES_DIR) along with a
context dictionary. The template is rendered with Jinja2.
Plain text template (.txt)ο
# templates/emails/welcome.txt
# Hello {{ name }}, welcome to {{ site_name }}!
await send_email(
recipients="user@example.com",
subject="Welcome",
template="emails/welcome.txt",
context={"name": "Alice", "site_name": "MyApp"},
)
For .txt templates the rendered output becomes the text body and
html is left as None.
HTML template (.html)ο
await send_email(
recipients="user@example.com",
subject="Invoice",
template="emails/invoice.html",
context={"order_id": 1234, "total": "$99.00"},
)
For .html / .htm templates the rendered output becomes the html
body. A plain-text fallback is auto-generated.
Markdown template (.md)ο
Markdown templates are rendered to HTML using
python-markdown (with extra and
sane_lists extensions). Both the raw Markdown (as text) and the
rendered HTML (as html) are included in the message:
# templates/emails/release_notes.md
# # Release {{ version }}
#
# **New features:**
# - {{ feature_1 }}
# - {{ feature_2 }}
await send_email(
recipients="subscribers@example.com",
subject="Release notes",
template="emails/release_notes.md",
context={"version": "2.0", "feature_1": "Dark mode", "feature_2": "Email support"},
)
If python-markdown is not installed, a minimal built-in fallback converts
headings and paragraphs.
Template + explicit bodyο
When you pass both template and an explicit text or html, the
explicit value takes precedence. This lets you override just one part:
await send_email(
recipients="user@example.com",
subject="Welcome",
template="emails/welcome.md",
context={"name": "Alice"},
text="Plain override", # overrides the Markdown text body
)
Attachmentsο
The attachments parameter accepts a list of items in several formats.
All attachments are resolved concurrently for performance.
File path (string or Path)ο
from pathlib import Path
await send_email(
recipients="user@example.com",
subject="Report",
text="See the attached report.",
attachments=[
"/tmp/report.pdf",
Path("data/export.csv"),
],
)
MIME type is guessed from the file extension.
Tuple - (filename, content) or (filename, content, mimetype)ο
await send_email(
recipients="user@example.com",
subject="Data",
text="Attached.",
attachments=[
("hello.txt", b"Hello, world!", "text/plain"),
("data.bin", b"\x00\x01\x02"),
],
)
content can be bytes, a file path string, a Path, or an HTTP
URL.
Raw bytesο
await send_email(
recipients="user@example.com",
subject="Binary",
text="See attachment.",
attachments=[b"\x89PNG\r\n\x1a\n..."],
)
Named attachment-{index}.bin with application/octet-stream.
Dict - path, url, content, or content_b64ο
await send_email(
recipients="user@example.com",
subject="Attachments",
text="Multiple formats.",
attachments=[
{"path": "/tmp/invoice.pdf"},
{"url": "https://example.com/logo.png", "filename": "logo.png"},
{"content": "CSV header\nrow1\nrow2", "filename": "data.csv", "mimetype": "text/csv"},
{"content_b64": "SGVsbG8=", "filename": "hello.txt"},
],
)
AttachmentData objectsο
For programmatic use, pass pre-built AttachmentData instances:
from openviper.core.email import AttachmentData
await send_email(
recipients="user@example.com",
subject="Attachment",
text="See attached.",
attachments=[
AttachmentData(filename="notes.txt", content=b"Notes here", mimetype="text/plain"),
],
)
Attachment limitsο
Individual attachments (file or URL) are capped at 25 MB by default.
Attachments that exceed the limit raise ValueError at send time.
URL attachments are restricted to http:// and https:// schemes.
Attempts to use other schemes (e.g. file://, ftp://) raise
ValueError.
Background Deliveryο
When the Background Tasks system is enabled (i.e. a real message broker like Redis or RabbitMQ is configured), emails can be routed to a background worker queue instead of being sent synchronously in the request cycle.
Auto-detectionο
By default (background absent or None), send_email
auto-detects whether a background worker is available:
It calls
worker_available(), which checks if the configured Dramatiq broker is a real broker (Redis/RabbitMQ) rather than theStubBrokerused in tests.If a real broker is detected, the email is enqueued via
enqueue_email_job()and delivered by the worker.If no real broker is available, the email falls back to synchronous delivery via the configured backend.
This means zero configuration is needed - if OpenViper can enqueue the message to a configured task broker, emails are queued automatically. If the broker is unavailable while auto-detection is in use, OpenViper falls back to inline delivery instead of dropping the message.
# settings.py - tasks enabled with Redis
TASKS = {
"enabled": 1,
"broker": "redis",
"broker_url": "redis://localhost:6379/0",
}
EMAIL = {
"host": "smtp.example.com",
"port": 587,
"use_tls": True,
"username": "apikey",
"password": "SG.xxxx",
"default_sender": "noreply@example.com",
# background is omitted - auto-detects worker availability
}
With this configuration, calling await send_email(...) automatically
queues the email for background delivery.
Explicit controlο
Set background in settings to force behaviour:
EMAIL = {
...
"background": True, # Always attempt background delivery
}
Or override per call:
# Force synchronous delivery for this email
await send_email(
recipients="admin@example.com",
subject="Critical alert",
text="Server is down!",
background=False,
)
# Force background delivery
await send_email(
recipients="user@example.com",
subject="Newsletter",
text="Weekly update...",
background=True,
)
Safety fallbackο
Even when background is True (or background=True),
if no real worker is available at runtime (e.g. the broker is down), the email
falls back to synchronous delivery rather than silently dropping.
Queue configurationο
Background emails are routed to the emails queue. To run a dedicated
email worker:
openviper viperctl start-worker . --queues emails
Or process all queues in a single worker:
openviper viperctl start-worker .
Backendsο
SMTPBackendο
The default backend. Connects to the SMTP server configured in EMAIL
settings and delivers via smtplib. TLS certificate validation uses
Pythonβs default SSL context.
ConsoleBackendο
A development backend that prints the full rendered email (headers + body) to stdout. Useful for local development without an SMTP server:
EMAIL = {
"backend": "ConsoleBackend",
}
API Referenceο
openviper.core.email.messageο
- class EmailMessageDataο
Normalized email payload dataclass.
- Parameters:
recipients β List of recipient addresses.
subject β Email subject.
text β Plain-text body (optional).
html β HTML body (optional).
cc β CC addresses (default:
[]).bcc β BCC addresses (default:
[]).attachments β List of
AttachmentData(default:[]).sender β From address.
- build_message(data: EmailMessageData) email.message.EmailMessageο
Build a stdlib
EmailMessagefrom anEmailMessageDatainstance. Handles multipart/alternative assembly and attachment encoding.
openviper.core.email.attachmentsο
- class AttachmentDataο
@dataclass(slots=True) class AttachmentData: filename: str content: bytes mimetype: str = "application/octet-stream"
- resolve_attachments(attachments) Awaitable[list[AttachmentData]]ο
Resolve a list of mixed attachment inputs (paths, URLs, dicts, tuples, bytes,
AttachmentData) into normalizedAttachmentDataobjects. Inputs are resolved concurrently.
openviper.core.email.backendsο
- class EmailSettingsο
SMTP configuration parsed from
settings.EMAIL.- classmethod from_settings() EmailSettingsο
Parse and return settings from the current configuration.
- get_backend(name=None) EmailBackendο
Return an email backend instance. Falls back to
EMAIL["backend"]from settings if name isNone.
openviper.core.email.queueο
- enqueue_email_job(data: EmailMessageData) Anyο
Serialize data and enqueue a background delivery job on the
emailsqueue.