# [Email] — mail rendered from data

> An `[Email]` component is a mail template. It is rendered on the server — from any function, a workflow step included — into HTML every mail client lays out (tables, every style inline, no script, no web font, absolute image addresses) and a plain-text twin from the same tree, in the app's own theme. With a `subject =>` it is a whole mail and calling it answers a `RenderedEmail`; without one it is a part other mails compose.

<!-- id: ui-email · area: ui · stability: stable · html: https://osysharp.com/reference/ui/email/ -->

## Summary        {#summary}
A mail about something a person did — an order, a shipment, a refund — is written like a component and filled from
the data it is about. Calling it renders it:

```osy syntax
[Email]
component OrderConfirmation(Order order) {
  subject => $"Your order {order.Reference}";
  render {
    Box(bg: Colors.Bg, p: 8) {
      Text($"Thank you — order {order.Reference} is reserved for you.", fontSize: 20, fontWeight: 700);
      Link($"https://shop.example.com/orders/{order.Reference}", text: "See your order", bg: Colors.Primary, color: "#FFFFFF");
    }
  }
}

void Confirm(Order order) {
  var mail = OrderConfirmation(order);   // mail.Subject · mail.Html · mail.Text
}
```

It renders; it does not send. Hand `mail.Html` and `mail.Text` to whatever sends your mail — the `Osysharp.Resend`
kit takes both. The [`Osysharp.EmailTemplates`](https://osysharp.com/reference/ui/email-templates-kit/) kit has the parts a designed mail is built
from — a layout with a logo and a footer, headings, buttons, a receipt's lines.

## Signature      {#signature}
```osy syntax
[Email]
component Name(<parameters>) {
  subject => <string expression>;   // present: a MAIL — Name(...) answers a RenderedEmail
                                    // absent:  a PART  — used inside other [Email] components
  render {
    <email atoms, other [Email] components, if / foreach / var>
  }
}

[Email]
component Name(<parameters>) {
  subject => <string expression>;
  placeholders { <a.b> => <expression>; … }   // the CLOSED list of {placeholders} words written into it may use
  render {
    … Region("name", allow: [<mail blocks>], require: [<blocks it keeps>], sample: F) { <default arrangement> } …
  }
}
Name(<arguments>, language: "sv")   // any mail, rendered in a chosen language instead of its recipient's

public sealed class RenderedEmail { string Subject; string Html; string Text; List<EmailInlineImage> Inline;
                                   List<string> Placeholders; bool IsPreview; }
public sealed class EmailInlineImage { string ContentId; string Path; string FileName; string ContentType; Guid? Asset; }
```

## Description    {#description}

### A mail, or a part of one   {#mail-or-part}
With `subject =>` the component is a **mail**: `OrderConfirmation(order)` in any function answers a `RenderedEmail`
— its `Subject`, the `Html` document and its plain-text twin `Text`. It is called like a function because on the
server that is what it is: its render block runs with a server function's powers. It navigates the entity it was
handed (`order.Lines`, including rows written in the same unit of work and not yet saved), calls the app's own
functions (`Money(order.Total)`), reads data, and runs inside a workflow step as anywhere else.

Without a subject it is a **part**, used inside another `[Email]` component the way a component is used inside a page.
A part may have one `Slot` — where the caller's children go — and its own children render when the caller gives
none. A mail cannot contain another mail, and a page cannot contain either: a mail is sent, not shown.

### What a mail is built from   {#atoms}
The email atoms carry the names and the settings a page's atoms have, so what you know from a page carries over. Each
takes exactly the settings in its row, and anything else is refused by name.

| atom | what it draws | settings |
|---|---|---|
| `Stack { … }` | children one above the other | `gap` `align` + the panel settings |
| `Row { … }` | children side by side, in table cells | `gap` `justify` `align` + the panel settings |
| `Box { … }` | a panel | `align` + the panel settings |
| `Text(text)` | a paragraph (a line break is kept) | `color` `fontSize` `fontWeight` `fontStyle` `lineHeight` `letterSpacing` `fontFamily` `textAlign` `textTransform` `textDecoration` `p` `px` `py` `pt` `pr` `pb` `pl` `placeholders` |
| `Link(href) { … }` | a link — with a `bg`, a button | `text` `color` `fontSize` `fontWeight` `fontFamily` `textDecoration` `bg` `border` `borderW` `rounded` `px` `py` `w` `align` |
| `Image(src)` · `Image(picture: p)` | a picture — by address, carried inside the mail, or a `Picture` (one the app ships, or one staff uploaded) | `alt` `w` `h` `maxW` `rounded` `align` `inline` `picture` |
| `Divider()` | a horizontal rule | `color` `py` |
| `Spacer()` | empty vertical space | `h` |
| `Markdown(text)` | formatted text | `color` `fontSize` `lineHeight` `fontFamily` `textAlign` `linkColor` `placeholders` |
| `Region("name", allow: [...]) { … }` | the part of a mail staff arrange — see [below](#region) | `allow` `require` `sample` |
| `Preview(text)` | the line an inbox shows beside the subject — hidden in the mail itself | — |
| `Slot` | in a part: where the caller's children go | — |

The **panel settings** are `bg` `color` `border` `borderW` `borderTW` `borderBW` `borderLW` `borderRW` `rounded`
`p` `px` `py` `pt` `pr` `pb` `pl` `w` `maxW` `textAlign` `fontFamily` `fontSize` `fontWeight` `lineHeight`
`letterSpacing`.

A value is written as on a page: a theme token (`Colors.Primary`, `Colors.Primary.Hover`, `Radius.Md`,
`FontSize.Title`), a keyword (`Justify.Between`, `Align.Center`, `TextAlign.Center`), a number, or a string. Spacing
(`p`, `gap`) counts in the page's 0.25rem steps — `p: 4` is 16 pixels; a size (`w`, `h`, `fontSize`) counts in pixels.
`justify: Justify.Between` on a `Row` puts the first child against the left edge and the last against the right —
how a receipt's line and its amount sit.

### It wears the app's theme   {#theme}
A token names the theme's value, and the renderer writes the **app's** value into the mail — the one its own `theme`
declares, or the kit's where it declares none. So a shop whose brand colour is orange sends orange buttons, and a
theme change reaches every mail at the next compile. Light values are used: a mail cannot know which scheme its
reader's client will choose. A token the app does not have writes nothing at all rather than something no mail client
understands. No font is loaded — the theme's font stack is written as it is, so the reader's own system face is used.

### What makes the HTML safe in every mail client   {#email-safe}
Mail clients drop most of what a browser understands, so the HTML is written the way that survives all of them:
layout is **tables** (each `role="presentation"`), every style is **inline** on the element it styles — font and
colour on every piece of text, because inheritance through a table cell is not reliable — and there is no stylesheet,
no `class`, no script and no web font. A picture's address is **absolute**: a path the template wrote (`/files/logo.png`)
is sent as the app's own address, because a mail client has nothing to resolve a path against. A `Link` with a
background is drawn as a padded link inside a coloured cell — the one shape that is a button in every client, Outlook
included.

A mail is read on phones, so what must be seen whole is never broken across two lines: a code — letters and digits
joined by hyphens, like an order reference `GG-1041` — stays on one line with its characters unchanged (so it still
copies and searches as itself), and a number keeps the short word or sign after it (`165 kr`, `250 g`, `12 %`). A
theme's fluid sizes (`clamp(26px, 2.6vw, 36px)`) are settled for the 600-pixel column, because many clients drop
`clamp()` and `vw` and would show the client's own default size instead.

Everything that comes from data is text: it is escaped, a setting's value cannot end its style declaration, a
`javascript:` address never becomes a link, and HTML inside `Markdown` is shown as text rather than run.

### Pictures: by address, or inside the mail   {#pictures}
A picture is sent **by its address** unless the template says otherwise. That keeps a mail small, which is right for
photographs — a product, a campaign picture — and a path is made absolute against the app's own address. Its cost
is that many clients show nothing until the reader allows remote pictures; Outlook asks by default.

`Image(path, inline: true)` carries the picture **inside the mail** instead, as an attachment the HTML names by
`cid:`, so it shows at once, everywhere. Use it for the small pictures a reader needs: a logo, a QR code to show at a
counter. It takes a file the app stores — a store path (`public/logo.png`) or the app's own file address
(`File.Url(path)`); a picture on another host stays an address, because the renderer fetches nothing.

A rendered mail lists those pictures in `Inline` — each with its `ContentId`, the `Path` to read its bytes from, its
`FileName` and `ContentType`. Whatever sends the mail attaches each file under its `ContentId`; with
[the Resend kit](https://osysharp.com/reference/ui/email-templates-kit/) that is `new EmailAttachment { …, ContentId = image.ContentId }`. The
plain-text twin does not mention them.

A picture **pasted into a Markdown editor** is an upload — a `FileAsset`, private to whoever uploaded it, written into
the words as `/api/files/<id>`. That address only answers with a short-lived signature, which must never go out in a
mail, so the renderer never sends it: a `Markdown(...)` in a mail carries each uploaded picture **inside the mail**,
listed once in `Inline` with `Asset` set to the upload and no `Path`. Read its bytes with
[[storage-file-versions#read|`File.ReadAllBytes(asset)`]], which answers only what the caller may read — so read it as
the person sending, and refuse a picture they cannot open (otherwise a file id pasted from elsewhere would mail
somebody's private file out). Its name and type are the asset's `Name` and `MimeType`.

```osy syntax
Image("public/logo.png", alt: "Acme", h: 40, inline: true);    // inside the mail: shows even where pictures are blocked
Image(product.PhotoUrl, alt: product.Name, w: 552);            // by address: the default, and right for a photograph

foreach (var image in mail.Inline) {
  var bytes = image.Asset != null ? File.ReadAllBytes(FileAsset.Single(a => a.Id == image.Asset))   // an upload
                                  : File.ReadAllBytes(image.Path);                                  // a stored file
  attachments.Add(new EmailAttachment { Filename = image.FileName, Content = bytes,
                                        ContentType = image.ContentType, ContentId = image.ContentId });
}
```

### Words staff own: a region of blocks   {#region}
The layout is code, and it changes with a deploy. The WORDS of a mail — how an order confirmation greets, what a refund
mail says first — are often somebody else's, and they change on a Tuesday; and sometimes they want more than words: a
photograph before Christmas, a quote from a regular, a button to the new collection. A mail says where that goes with a
`Region`, exactly as a page does ([Blocks and regions — pages the staff arrange](https://osysharp.com/reference/ui/blocks/)): what is written inside it is the mail until staff arrange it
differently at the desk ([The PageBuilder kit — the desk staff arrange the site at](https://osysharp.com/reference/ui/pages-kit/)) — a draft, published, in each language, with no deploy.

```osy title="a region in a mail — staff arrange what is inside it" syntax
[Email]
component Welcome(Member member) {
  subject => $"Welcome, {member.Name}";
  placeholders { member.name => member.Name; }
  render {
    EmailLayout("The Rowing Club") {
      Region("welcome", allow: [EmailWords, EmailPicture, EmailQuote], sample: WelcomeSample) {
        EmailWords(words: "Welcome aboard, **{member.name}**.");
      }
    }
  }
}
```

**A mail block is an `[Email]` part marked `[Block]`.** Its parameters are the fields staff fill in, and their types
choose the editor, as for a page's block. [Osysharp.EmailTemplates](https://osysharp.com/reference/ui/email-templates-kit/) ships the everyday ones
(words, a heading with words, a picture, a button, a quote, small print); writing your own is writing a part:

```osy title="a mail block of your own — an [Email] part marked [Block]" syntax
[Block("The order", Hint = "What was ordered and what it costs.")]
[Email]
component OrderLines(Order order, [Label("Heading"), MaxLength(60)] string heading = "Your order") {
  render { Text(heading, placeholders: true); foreach (var l in order.Lines) { Text($"{l.Quantity} × {l.Name}"); } }
}
```

- **An entity a mail block takes is handed to it from the mail** — `order` above is the confirmation's own `Order`.
  Staff never pick it. The mail must have exactly one parameter of that type; with none, or two, the region is refused
  by name.
- **`require: [OrderLines]`** keeps a block in the region: staff may move it, never take the last one out. Use it for
  what the reader must be told — an order confirmation's lines are what the customer bought.
- **`sample: F`**, with `RenderedEmail? F(string language)`, renders the mail around the region for something real —
  the latest order — which is what the desk shows beside the blocks, as the mail itself.

**Placeholders.** A block's words may use the mail's placeholders: a mail block writes its text with
`placeholders: true` (`Markdown(words, placeholders: true)`, `Text(heading, placeholders: true)`), and they are filled
from whichever mail the block is placed in. Two rules keep words staff write from changing a mail into something else:

- **A value is data, never markup.** A placeholder's value is escaped before the words are read as Markdown, so a
  customer whose name is `[Win](https://evil.example)` is greeted with those characters, not a link — and `**` or `# `
  in a value cannot restyle the words around it. In a `Text` it is shown as text. The words themselves render as
  `Markdown(text)` does: raw HTML is shown as text and a `javascript:` link is never a link.
- **Only a declared placeholder is filled — and a word naming another is refused when staff save it.** Staff who write
  `{order.referense}` are told, by name, which placeholders the mail offers; a customer is never sent a token nobody
  filled.

**Words are per language.** A mail renders in its reader's language (`language => order.Language;`), and a region's
words are kept per language like any block's: the code's default arrives in every language the app speaks, as its
locale file translates it, and staff change each language's words separately — words written in English never reach a
Swedish reader.

**Versions and dates, as the site's.** A mail's region is arranged in the site's versions ([[ui-pages-kit#versions]]),
so the confirmation a customer is sent while the Christmas version is live is the Christmas one, and a block dated for
the week before Christmas is in it only that week.

**A preview is never sent.** A member of staff previewing a version (or its drafts, or a date) sees the mail as the
preview shows it — and a mail rendered then says so, `IsPreview`, and is not to be sent: the shop's outbox refuses one.
Every real send — a checkout, a workflow step, a scheduled job — is rendered with what is live.

`Markdown(text, placeholders: true)` fills the mail's placeholders in a text the template did not write itself — the
body of a campaign staff wrote — under the same two rules.

⚑ `Editable("Intro", default: "…")` and `[Email(Words = F)]` were the first way a mail's words were staff's, and they
are gone: each is refused, naming the region that replaced it.

### How the plain-text twin is written from the same tree   {#text}
`Text` is read from the same tree as `Html`, so the two say the same words: each paragraph on its own, a tight stack's
lines kept together (a receipt), a row's cells on one line, a button as `label: address`, a link as
`label (address)`. The `Preview` line is not in it — an inbox shows that beside the subject.

### What a mail cannot hold   {#refused}
An inbox runs no code, keeps no state, has no screen the app can measure, and drops stylesheets. So the compiler
refuses — naming the construct and what to write instead — an `action`, a field or `live var` (compute the value with
`var x = …;` in the render block), a lifecycle hook, an event setting (`onClick:`), the page atoms that act or draw in
the browser (`Button`, `Input`, `Icon`, `Svg`, `Canvas`, …), a page component, the browser's own surfaces
(`Layout`, `Navigation`, `Keyboard`, `Theme`, …), a `variants { }` or `skeleton { }` block, type parameters, and a
setting an atom does not take.

### Photograph it in a test   {#photograph}
`Ui.Shot(label, mail)` photographs a rendered mail. Under `osy test --pixels` a real browser lays the HTML out as a
mail client does — a 720-pixel reading pane, with no script — and the picture lands in `osy-shots/` beside the page
shots. It asserts nothing, and it needs no page open. Under `osy test` it says why there is no image.

```osy syntax
[Test]
void TheConfirmationLooksRight() {
  Ui.Shot("order confirmation", OrderConfirmation(AnOrder()));
}
```

## Examples       {#examples}

### A receipt, called from a function   {#example-receipt}
```osy title="a mail with a part, filled from an entity and its lines, in the app's own colours" test app=ui-email
theme Brand { Colors { Brand = "#C2410C"; Ink = "#20242B"; Canvas = "#F6F7F9"; } }

entity Order {
  [MaxLength(40)] string Reference;
  decimal Total;
  [ForeignKey(Order)] OrderLine[] Lines;
  security { allow read, create when IsAuthenticated; }
}

entity OrderLine {
  [Required] Order Order;
  [MaxLength(80)] string Name;
  int Quantity;
  security { allow read, create when IsAuthenticated; }
}

string Money(decimal amount) { return $"{amount:0.00} kr"; }

// A PART — no subject. Its children arrive at `Slot`.
[Email]
component Section(string title) {
  render {
    Stack(gap: 2) {
      Text(title, fontSize: 20, fontWeight: 700, color: Colors.Ink);
      Slot;
    }
  }
}

// A MAIL — it has a subject, so calling it answers a RenderedEmail.
[Email]
component Receipt(Order order) {
  subject => $"Your order {order.Reference}";
  render {
    Box(bg: Colors.Canvas, p: 6) {
      Preview($"Order {order.Reference} is reserved for you");
      Section("Thank you") {
        foreach (var line in order.Lines) {
          Text($"{line.Quantity} × {line.Name}");
        }
        Row(justify: Justify.Between) {
          Text("Total");
          Text(Money(order.Total), fontWeight: 600);
        }
      }
      Link($"https://shop.example.com/orders/{order.Reference}", text: "See your order",
           bg: Colors.Brand, color: "#FFFFFF", px: 6, py: 3, rounded: 6);
    }
  }
}

string ReceiptSubject(Order order) {
  var mail = Receipt(order);
  return mail.Subject;
}
```

### A mail whose words staff arrange   {#example-region}
```osy title="a mail with a region: blocks of its own, one handed the mail's data, the words filled from its placeholders" test app=ui-email-words
app RowingClub {
  use Osysharp.Ui;
  use Osysharp.Blocks;
  model "model/**/*.osy";
}

using Osysharp.Blocks;

[Principal]
entity Member {
  [MaxLength(80)] string Name;
  [MaxLength(200)] string Email;
  security { allow read when IsAuthenticated; }
}

/// A paragraph in the club's own voice.
[Block("Words")]
[Email]
component Words([Label("Words")] Markdown words) {
  render { Markdown(words, placeholders: true, lineHeight: 1.6); }
}

/// The member's own details — handed the member from the mail it is placed in.
[Block("Your details")]
[Email]
component Details(Member member, [Label("Heading"), MaxLength(60)] string heading = "Your details") {
  render {
    Stack(gap: 1) {
      Text(heading, fontWeight: 700);
      Text(member.Email);
    }
  }
}

[Email]
component Welcome(Member member) {
  subject => $"Welcome, {member.Name}";
  placeholders { member.name => member.Name; }
  render {
    Box(p: 6) {
      Region("welcome", allow: [Words, Details], require: [Details], sample: WelcomeSample) {
        Words(words: "Welcome aboard, **{member.name}**.");
        Details();
      }
    }
  }
}

/// The welcome mail for the newest member — what the desk previews the region with.
RenderedEmail? WelcomeSample(string language) {
  var member = Member.FirstOrDefault();
  return member == null ? null : Welcome(member, language: language);
}
```

## See also       {#see-also}
- [The EmailTemplates kit — mail that looks designed](https://osysharp.com/reference/ui/email-templates-kit/) — the parts a designed mail is built from: layout, headings, buttons, receipt lines — and
  the mail blocks staff arrange in a region.
- [Blocks and regions — pages the staff arrange](https://osysharp.com/reference/ui/blocks/) — blocks and regions: the same mechanism a page's arrangeable parts use.
- [The PageBuilder kit — the desk staff arrange the site at](https://osysharp.com/reference/ui/pages-kit/) — the desk staff arrange a mail's region at, with the mail itself beside it.
- [theme tokens](https://osysharp.com/reference/ui/theming/) — the tokens a mail's colours and sizes come from.
- [style props](https://osysharp.com/reference/ui/styling/) — the settings the email atoms share with a page's.
- [Markdown — rendering markdown text](https://osysharp.com/reference/ui/markdown/) — the Markdown a `Markdown(text)` atom understands.
- [Frame](https://osysharp.com/reference/ui/frame/) — show a rendered mail on a page, sandboxed, as a preview.
- [Ui — drive the app's UI from a test](https://osysharp.com/reference/testing/ui/) — `Ui.Shot`, and the pixel tier that photographs.
