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 ───────────────────────────────────────
    "use_background_worker": None,      # True / False / None (auto-detect)
}
EMAIL keys reference

Key

Default

Description

host

"localhost"

SMTP server hostname.

port

25

SMTP server port. Use 587 for STARTTLS or 465 for implicit TLS.

username

""

SMTP authentication username. Also accepts the alias user.

password

None

SMTP authentication password.

use_tls

False

Issue a STARTTLS command after connecting. Mutually exclusive with use_ssl.

use_ssl

False

Connect using implicit TLS (SMTP_SSL).

timeout

10

Socket timeout in seconds.

default_sender

"noreply@example.com"

From address used when no explicit sender is passed to send_email(). The alias from is also accepted.

backend

"SMTPBackend"

Delivery backend class name. "ConsoleBackend" prints emails to stdout — useful during development.

fail_silently

False

When True, delivery errors are silently swallowed and send_email() returns False. When False, exceptions propagate to the caller.

use_background_worker

None

Controls background delivery. See Background Delivery below.

Sending Email

See also

Working project that sends email:

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 True on success or False when fail_silently is 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 html is 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. True forces queueing, False forces synchronous delivery.

  • sender – Override the From address 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 (use_background_worker absent or None), send_email auto-detects whether a background worker is available:

  1. It calls worker_available(), which checks if the configured Dramatiq broker is a real broker (Redis/RabbitMQ) rather than the StubBroker used in tests.

  2. If a real broker is detected, the email is enqueued via enqueue_email_job() and delivered by the worker.

  3. If no real broker is available, the email falls back to synchronous delivery via the configured backend.

This means zero configuration is needed — if you have a task worker running, emails are automatically queued. If not, they are sent inline.

# settings.py — tasks enabled with Redis
TASKS = {
    "enabled": True,
    "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",
    # use_background_worker is omitted — auto-detects worker availability
}

With this configuration, calling await send_email(...) automatically queues the email for background delivery.

Explicit control

Set use_background_worker in settings to force behaviour:

EMAIL = {
    ...
    "use_background_worker": 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 use_background_worker is True (or background=True), if no real worker is available at runtime (e.g. Redis 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 runworker . --queues emails

Or process all queues in a single worker:

openviper viperctl runworker .

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.

delivery_recipients() list[str]

Return all SMTP recipients (to + cc + bcc), deduplicated.

build_message(data: EmailMessageData) email.message.EmailMessage

Build a stdlib EmailMessage from an EmailMessageData instance. 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 normalized AttachmentData objects. 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 is None.

openviper.core.email.queue

enqueue_email_job(data: EmailMessageData) Any

Serialize data and enqueue a background delivery job on the emails queue.

worker_available() bool

Return True when a real message-queue broker (Redis/RabbitMQ) is active. Returns False for the test StubBroker. Result is cached after the first call.

openviper.core.email.templates

render_template_content(template, context=None, template_dir='templates') tuple[str | None, str | None]

Render a Jinja2 template and return (text, html).

  • .txt(rendered, None)

  • .html / .htm(None, rendered)

  • .md / .markdown(rendered, markdown_to_html(rendered))

render_markdown(markdown_text) str

Convert Markdown to HTML. Uses python-markdown with extra and sane_lists extensions when available, otherwise a minimal built-in fallback.