Skip to main content

No project description provided

Project description

mountaineer-email

Dependencies to easily format and send email with Mountaineer or FastAPI.

Getting Started

Since email deliverability is nearly zero if you send with local linux utilities, you'll almost always want to use a 3rd party service. This package is provider agnostic and delegates delivery integrations to mountaineer-cloud provider packages such as Resend.

The core flow is:

  1. Define an EmailControllerBase with a typed payload model.
  2. Register that controller on your AppController.
  3. Inject the mounted email template with Depends(get_email_template(...)).
  4. Call await template.render(...) with your payload to produce a FilledOutEmail.
from fastapi import Depends
from pydantic import BaseModel

from mountaineer import AppController
from mountaineer_cloud.primitives import EmailBody, EmailMessage, EmailRecipient
from mountaineer_cloud.providers.resend import ResendCore, ResendDependencies
from mountaineer_email import (
    EmailControllerBase,
    EmailMetadata,
    EmailRenderBase,
    get_email_template,
)


class WelcomeEmailPayload(BaseModel):
    first_name: str
    last_name: str


class WelcomeEmailRender(EmailRenderBase):
    name: str


class WelcomeEmailController(EmailControllerBase):
    view_path = "emails/welcome/page.tsx"

    async def render(
        self,
        payload: WelcomeEmailPayload,
    ) -> WelcomeEmailRender:
        name = f"{payload.first_name} {payload.last_name}"

        return WelcomeEmailRender(
            name=name,
            email_metadata=EmailMetadata(
                subject=f"Welcome {name}",
            ),
        )


controller = AppController()
controller.register(WelcomeEmailController())


async def send_welcome_email(
    template: WelcomeEmailController = Depends(
        get_email_template(WelcomeEmailController)
    ),
    resend: ResendCore = Depends(ResendDependencies.get_resend_core),
) -> str:
    filled_email = await template.render(
        WelcomeEmailPayload(
            first_name="Ada",
            last_name="Lovelace",
        )
    )

    message = EmailMessage[ResendCore](
        sender=EmailRecipient(
            email="noreply@example.com",
            display_name="Example App",
        ),
        recipient=EmailRecipient(email="ada@example.com"),
        subject=filled_email.subject,
        body=EmailBody(html=filled_email.html_body),
    )

    return await message.send(resend)

Designing

You want your emails to be beautiful, but email design is notoriously a headache. Email clients lag significantly in adoption of html features and only implement a subset of the CSS spec. If it gives you any sense of the current ecosystem, marking up with <table> still rules the day. See the currently supported css attributes, for reference.

For complex email templates, you'll probably want to use a dedicated designer app or plugin for something like Figma. For simpler email layouts we bundle basic Tailwind support by inlining the CSS markup that are usually defined in classes.

Usage

To setup a new email, you'll need both the view (equivalent to a Mountaineer frontend view) and a controller (similarly equivalent to a Mountaineer controller). A typical project layout looks like:

myproject/
├── controllers/
├── emails/
│   └── email1.py
└── views/
    ├── app/
    ├── emails/
    │   ├── email1/
    │   │   └── page.tsx
    │   └── template.tsx
    └── project.json

This layout mirrors the frontend views exactly - we support individual pages and the nesting of layouts to wrap your emails in a common design.

Unlike your conventional routes, emails aren't interactive. You can think of them as running without javascript within an email client. So the initial representation of your React component will be the permanent representation of the page.

We compile down your React components into raw html using Mountaineer's regular SSR renderer. We then perform some email-specific transformations that allow your styling to show up properly for browsers.

Define your view:

import React from "react";
import { useServer } from "./_server/useServer";

const Page = () => {
  const serverState = useServer();

  return (
      <div className="space-y-4">
        {serverState.user_name && <div>Hi {serverState.user_name}!</div>}
      </div>
  );
};

export default Page;

And then your associated controller:

from uuid import UUID

from fastapi import Depends
from mountaineer_email import EmailControllerBase, EmailMetadata, EmailRenderBase
from pydantic import BaseModel
from iceaxe import DBConnection

from mountaineer import CoreDependencies, LinkAttribute, ManagedViewPath, Metadata
from iceaxe.mountaineer import DatabaseDependencies

from myproject import models

class WelcomeEmailRequest(BaseModel):
    user_id: UUID


class WelcomeEmailRender(EmailRenderBase):
    user_name: str | None


class WelcomeEmailController(EmailControllerBase[WelcomeEmailRequest]):
    view_path = "emails/welcome/page.tsx"

    async def render(
        self,
        payload: WelcomeEmailRequest,
        db_session: DBConnection = Depends(DatabaseDependencies.get_db_connection),
    ) -> WelcomeEmailRender:
        user = await db_session.get(models.User, payload.user_id)
        if not user:
            raise ValueError(f"User not found: {payload.user_id}")

        return WelcomeEmailRender(
            user_name=user.name,
            email_metadata=EmailMetadata(
                subject="Welcome!",
            ),
            metadata=Metadata(
                links=[LinkAttribute(rel="stylesheet", href="/static/auth_main.css")]
            ),
        )

Dependencies declared on render() are resolved when you call template.render(...) or template.render_email(...). For example, you can combine the payload with another injected value:

from fastapi import Depends


def get_email_signature() -> str:
    return "Thanks for joining us!"


class WelcomeEmailController(EmailControllerBase[WelcomeEmailRequest]):
    view_path = "emails/welcome/page.tsx"

    async def render(
        self,
        payload: WelcomeEmailRequest,
        db_session: DBConnection = Depends(DatabaseDependencies.get_db_connection),
        signature: str = Depends(get_email_signature),
    ) -> WelcomeEmailRender:
        user = await db_session.get(models.User, payload.user_id)
        if not user:
            raise ValueError(f"User not found: {payload.user_id}")

        return WelcomeEmailRender(
            user_name=f"{user.name} {signature}",
            email_metadata=EmailMetadata(
                subject="Welcome!",
            ),
        )

Then add these controllers to your AppController:

from myproject import emails

controller = AppController(
    config=config,
    global_metadata=Metadata(
        links=[LinkAttribute(rel="stylesheet", href="/static/app_main.css")]
    ),
    custom_builders=[
        PostCSSBundler(),
    ],
)

controller.register(emails.WelcomeEmailController())

mountaineer-email only owns the rendering and preview flow. Provider-specific delivery settings should come from the matching mountaineer-cloud provider config, for example ResendConfig if you're sending through Resend.

To render a filled email from application code, resolve the mounted template instance from the registry and call render(...) with your request model:

from fastapi import Depends

from mountaineer_email import get_email_template


async def send_preview(
    template: WelcomeEmailController = Depends(
        get_email_template(WelcomeEmailController)
    ),
):
    filled_email = await template.render(
        WelcomeEmailRequest(user_id=user_id),
    )

Inliner

Since regular tailwind will render css to a 3rd party stylesheet - that can't be read by most email browsers - you'll want to inline the styles of your tailwind components so they show up as <div style=xyz>. We recommend you use @react-email/tailwind since it has a lot of helper utilities out of the box for tailwind's variables:

cd project/views && npm install @react-email/tailwind
import { Tailwind } from "@react-email/tailwind";

const Email = () => {
  return (
    <Tailwind>
      <button className="bg-blue-500">Click me!</button>
    </Tailwind>
  );
};

export default Email;

Admin Panel

We bundle an admin panel that allows you to preview your emails with different imports. You'll have to add these explicitly to your AppController. We suggest conditionally adding these to your webservice if you're running locally:

import mountaineer_email.controllers as email_admin_controllers

if ENV == "development":
    controller.register(email_admin_controllers.EmailHomeController())
    controller.register(email_admin_controllers.EmailDetailController())

Development

If you update the admin UI files, you'll need to build the artifacts for inclusion in the published library. We do this automatically when distributing through CI, so this is just when you're making changes and testing locally:

uv run build-email

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

mountaineer_email-0.1.1.tar.gz (579.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

mountaineer_email-0.1.1-py3-none-any.whl (388.7 kB view details)

Uploaded Python 3

File details

Details for the file mountaineer_email-0.1.1.tar.gz.

File metadata

  • Download URL: mountaineer_email-0.1.1.tar.gz
  • Upload date:
  • Size: 579.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for mountaineer_email-0.1.1.tar.gz
Algorithm Hash digest
SHA256 5f10bdc3dc784a392118c47a7cc3e3298afbc84654707b8c616e6d04a8eea7a4
MD5 cb20f2cfb10e71de76ff634e917c3820
BLAKE2b-256 9ced45b0b262d993767c80190a9d4cdeb94a6a8eb3a1b5821ba4e9f37cc9c1e5

See more details on using hashes here.

Provenance

The following attestation bundles were made for mountaineer_email-0.1.1.tar.gz:

Publisher: test.yml on piercefreeman/mountaineer-email

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file mountaineer_email-0.1.1-py3-none-any.whl.

File metadata

File hashes

Hashes for mountaineer_email-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 8042504873bbc7348184bb159802439dd1e3026cc8ec30f0eff852c6dfafbc31
MD5 cdb1399e2a73ccb9c599667f2fdfe10a
BLAKE2b-256 6b7eed6a905ef278637c5e3323efcc585d6cef9a962fb81c69ec18f3a2b5cc57

See more details on using hashes here.

Provenance

The following attestation bundles were made for mountaineer_email-0.1.1-py3-none-any.whl:

Publisher: test.yml on piercefreeman/mountaineer-email

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page