Email

00 Markdown

Email templates in Elements are written in the same HTML as your pages, and sending one is a single function call. The build inlines the template's styles and makes its links absolute, so the email renders in mail clients the way it renders in your browser.

Create an email template

Terminal
elements create email welcome
app/emails/welcome/index.ehtml
import "./style.css"; /** @email */ <html (name: string)> <h1>Welcome, {name}</h1> <p>Thanks for signing up.</p> </html>

The @email build tag on <html> marks the template as an email. It has typed attributes like any template, and nothing in it is sent to a browser.

Send an email

app/jobs/send-welcome.ts
import { email } from "@elements/app"; import WelcomeEmail from "#app/emails/welcome"; email({ to: user.email, subject: "Welcome", body: new WelcomeEmail({ name: user.name }), });

email() works in any server code: a route, an RPC function, a job or a helper they call. It sends right away.

Send email from a job

email() talks to the mail server while your code waits. In a page's RPC function that wait holds up the request, and if the mail server is down, the email is lost. Sending from a job fixes both: the job runs in a separate worker process, and it retries until the mail server answers.

app/jobs/send-welcome.ts
import { Job, email } from "@elements/app"; import WelcomeEmail from "#app/emails/welcome"; export interface SendWelcomeJobFields { to: string; name: string; } export class SendWelcomeJob extends Job<SendWelcomeJobFields> { static maxAttempts = 5; run() { let { to, name } = this.fields; email({ to, subject: "Welcome", body: new WelcomeEmail({ name }), }); } }

Email and transactions

email() sends right away and is not part of a transaction. Inside tx(), a statement that throws before email() stops the code there, so the email is never sent, the same as any line after a throw. The case to watch is a statement that throws after email() has run: the transaction rolls back, but the email has already gone out.

Scheduling the job inside the transaction instead ties the email to the commit. The job is a row in the same transaction, so if anything after it throws, the job is rolled back with everything else and the email is never sent:

app/pages/signup/services.ts
import { tx } from "@elements/app"; import { SendWelcomeJob } from "#app/jobs/send-welcome"; /** @rpc */ export function signup(form: SignupForm): User { return tx(() => { let user = createUser(form); new SendWelcomeJob({ to: user.email, name: user.name, }).schedule(); createDefaultWorkspace(user); return user; }); }

If createDefaultWorkspace throws, the user, the workspace and the welcome email are all rolled back together.

In development

By default, nothing is sent while you build. Each email is written to the project server log instead, with its recipients, subject and body, so you need no mail account to get started. To look at an email as it will render, return the template from a route and open that route in your browser:

app/pages/email-preview/index.ts
import WelcomeEmail from "#app/emails/welcome"; export default function route() { return new WelcomeEmail({ name: "Ada" }); }

Register that route in development only. Then edit the template and reload the page to see each change.

Sending live emails

To deliver real email, your app needs an email sending service. Your app hands each email to the service, and the service delivers it to the recipient's inbox. Postmark, Amazon SES, Resend, SendGrid and Mailgun are common choices, and most offer a free tier to start with.

Apps hand email to these services over SMTP, the standard protocol for sending mail, and Elements works with any service that supports it, which they all do. When you sign up, the service gives you four SMTP settings: a host, a port, a username and a password. Most also ask you to verify the domain you send from, by adding a few DNS records they show you, so your email does not land in spam.

Put those settings in the environment file for each environment that should send real email, and set EMAIL_LIVE=true there:

config/env/production.env
EMAIL_LIVE=true SMTP_HOST=smtp.postmarkapp.com SMTP_PORT=587 SMTP_USER=your-username SMTP_PASSWORD=your-password

This works in any environment, development included, so you can send real email to yourself while you build. config/env/production.env is never committed, which keeps the password out of your repository. development.env is committed, so to send live email in development, set the password in your shell instead of in that file.

In production, an app that is not set to send live email refuses to start. An email that should go out never ends up silently written to a log.

See email in a demo

The manual: email (opens in a new tab), jobs (opens in a new tab), config (opens in a new tab).

Get a digest to your inbox once per week.

Comments · 0