> ## Documentation Index
> Fetch the complete documentation index at: https://docs.riggery.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Send email

> Send one email through your mailbox server.

Sends one email. The host, login, and **From** stay on the node. Prefer Secrets via `{{secret:mailbox-password}}`. The password never goes to the model and is not in the result.

This does not read the inbox. [IMAP fetch](/graph/tools/imap-fetch) does.

<h2 id="input">
  Input
</h2>

Inspector **Settings**. Empty-field rules for the Graph: [Previous nodes](/graph/previous-nodes). **Insert value** is on **Host**, **User**, **Password**, **From**, **To**, **Subject**, **Text**, **HTML**, and **Message**.

On an Agent Tool, **To**, **Subject**, **Text**, and **HTML** can be **Agent decides** or **Fixed**. **Host**, **Port**, **Encryption**, **User**, **Password**, **From**, and **Message** stay on the node.

| Field | Required | Empty | Notes |
| - | - | - | - |
| **Host** | Yes | Fail | Outgoing mail server, for example `smtp.example.com`. A private address is rejected. |
| **Port** | Yes | Default: `587` | |
| **Encryption** | Yes | Default: **STARTTLS** | **STARTTLS** or **TLS**. There is no unencrypted option. |
| **User** | Yes | Fail | |
| **Password** | Yes | Fail | Secret placeholder. Never a real password on the Graph. |
| **From** | Yes | Fail | One mailbox, for example `news@example.com`. |
| **To** | Unless **Message** is set | Fail | One mailbox. A second address, or a display name, fails the step. |
| **Subject** | Unless **Message** supplies it | Fail | |
| **Text** | One of **Text** or **HTML** | — | Plain text. |
| **HTML** | One of **Text** or **HTML** | — | HTML body. |
| **Message** | Unless **To** is set | — | JSON from a previous node. Keys `to`, `subject`, `text`, and `html`. A filled field on this node overrides the same key. |

A line break in **To**, **From**, or **Subject** is removed. If that leaves more than one mailbox, the step fails.

<h2 id="output">
  Output
</h2>

On the next node, **Previous nodes** lists **`Send email`: Result** (`{{Send email.text}}` while the name is unique).

| Output | In menu | Type | Next node gets |
| - | - | - | - |
| Result | Yes | JSON text | `{ "ok": true, "to": "person@example.com", "messageId": "<id@example.com>", "error": null }` |

A refused recipient does not fail the run:

```json theme={null}
{ "ok": false, "to": "person@example.com", "messageId": null, "error": "Recipient rejected (550)" }
```

A missing host, a blocked host, a TLS failure, or a failed login fails the run. The body and the password are not in the result.

<h2 id="many">
  Many recipients
</h2>

One run sends one page of rows. [Read records](/graph/tools/state-query) and [Loop](/graph/loop) cap that page. A [Cron](/graph/triggers/cron) trigger starts the next run. Import the list on [State](/instance/state). A list that does not fit in one file is a second import into the same collection. The address is a column. The record key cannot contain `@`.

Put this inside the Loop, in order. Name the first Code node **Prepare**. Its `input` is the current row (`input.data` is your columns).

```javascript theme={null}
const row = input.data;
return {
  collection: "recipients",
  where: [{ path: "email", eq: row.email }],
  set: [{ path: "status", value: "sending" }],
  to: row.email,
  subject: "Hello",
  text: "Hello " + row.name,
};
```

`email`, `status`, and `name` are columns in your file. **Update records** reads this JSON. Leave **Collection** empty on that node when `collection` is in the JSON. See [Update records](/graph/tools/state-update).

**Send email** comes next. **Message** is `{{Prepare.text}}`. **Host**, **User**, **Password**, and **From** use `{{secret:Name}}`.

Name the next Code node **Finish**. Its `input` is the Result above.

```javascript theme={null}
return {
  collection: "recipients",
  where: [{ path: "email", eq: input.to }],
  set: [{ path: "status", value: input.ok ? "sent" : "error" }],
};
```

A second **Update records** reads that JSON.

Outside the Loop, **Read records** filters the status you have not marked yet, and **Limit** is `100`. Rows left at `sending` stay out of the next run, including when the run stops after the server accepts the message and before **Finish**.

<h2 id="limits">
  Limits
</h2>

One recipient. No attachments. **Subject** up to 998 characters. **Text** and **HTML** up to 256000 characters each. A larger body fails the step.
