---
title: Mail
description: Send email from hooks, agents, jobs and server functions, through one mail server set up in the admin UI.
section: Server
order: 5
---

# Mail

<p class="lead">Set up a mail server once, then send from any server code with <code>mail()</code>. Sign-in links, invitations, password resets and notifications use the same one.</p>

## Setting it up

In the admin UI, under **Mail**:

| | |
|---|---|
| Host and port | Your provider's SMTP server: `smtp.example.com`, `587` |
| Username, password | The password is kept on the server and never sent back out |
| From, from name | The address mail comes from, and the name beside it: "Springfield Elementary" rather than a bare address |
| Base URL | Where links in mail point. Without one, the address the app is served at |
| TLS | STARTTLS, on unless a local relay doesn't speak it |

Without a host, mail is off. That is a fine way to run: nothing fails, nothing is sent.

## Sending

```ts title="agents/receipts.ts"
export async function act({ event, mail, link }) {
  const sent = await mail({
    to: event.data.email,
    subject: "Paid, thank you",
    text: `Your order is paid. See it at ${await link("/orders")}`,
  });
}
```

- `mail({ to, subject, text })` answers whether it went: `false` when mail isn't set up or the server refused, which is logged, never thrown.
- `link(path)` makes a path in the app into a full address for a letter, from the base URL.

Mail is plain text on purpose. A notification says there is something in the app and links to it; it doesn't try to be the conversation, so nobody replies to an address that nobody reads.

## What Sluurp sends by itself

Sign-in links, password resets and invitations, in the app's language. Threads in [conversations](/docs/client#and-the-rest) mail the people who weren't there when a message arrived; chat rooms never do, so the mail stays worth reading.
