# @backendfree/inbox

> Sending a contact form's message to the website's inbox, past the spam checks, and reading them back.

A website's contact form, sent to its inbox on the dashboard, and the messages
read back. Built on [`@backendfree/core`](../core).

```sh
npm install @backendfree/core @backendfree/inbox
```

```ts
import { Client } from '@backendfree/core';
import { Inbox, formTimer } from '@backendfree/inbox';

const inbox = new Inbox(
  new Client({ origin: 'https://backendfree.com', key: 'pk_live_...' }),
);

const opened = formTimer();                    // when the form appears

form.addEventListener('submit', async (event) => {
  event.preventDefault();
  const data = new FormData(form);
  await inbox.send({
    name: String(data.get('name')),
    email: String(data.get('email')),
    message: String(data.get('message')),
    page: location.pathname,
    honeypot: String(data.get('website') ?? ''),
    filled_in_seconds: opened(),
  });
  showThanks();
});
```

The owner is emailed each message, with your visitor's address as Reply-To, and
finds them all under Inbox on the dashboard. Switch Inbox on for the website
first; until then `/v1/messages` answers 404.

## Keeping spam out

Every message passes these checks, on the platform's side, before anybody sees
it:

- **A hidden field.** Add an input people never see and send whatever it holds
  as `honeypot`. A script filling every field fills it too, and that message is
  answered as received and not kept.
- **How fast it was sent.** `formTimer()` measures how long the form was open;
  a form sent faster than a person can fill one in goes to spam.
- **Links.** More than a couple of web addresses, or a link written as code,
  goes to spam.
- **Limits.** One visitor address may send a few messages an hour to one
  website, and a website takes a set number a day. The
  [limits page](https://backendfree.com/docs/limits/) has the figures.

The hidden field, hidden from people and from screen readers but not from a
script reading the page:

```html
<div aria-hidden="true" style="position: absolute; left: -10000px">
  <label>Leave this empty <input name="website" tabindex="-1" autocomplete="off"></label>
</div>
```

Spam is kept for a while in the Spam folder, where the owner can move a message
back. It is never emailed and never sent to a webhook.

**`send` resolves the same whatever became of the message**, so a script learns
nothing from the answer. Two refusals throw, because a person can act on them:

| code | means |
|---|---|
| `bot_check_failed` | The website asks for Cloudflare's check and the token was missing, spent or refused. Show the check again. |
| `daily_limit_reached` | The website has had its messages for today. Suggest another way to get in touch. |

## Cloudflare's bot check

Optional, for a form a script keeps getting past. Make a Turnstile widget in
your own Cloudflare account, put its site key in your form, and paste its secret
key on the inbox's settings screen. From then on every message has to carry a
token Cloudflare accepts:

```ts
await inbox.send({
  name, email, message,
  bot_check: String(data.get('cf-turnstile-response') ?? ''),
});
```

A token works once, so after a refusal reset the widget before sending again.
Cloudflare's test keys, which always pass or always fail, try it without a real
widget.

## From your own server

`send` works with a secret key as well, from a server action or an API route,
for a form that posts to your own server first. The per-visitor limits then see
your server rather than the visitor, so apply your own; the daily limit still
holds.

## Reading messages

Reading needs a secret key, and `list` and `message` throw on a publishable one
before any request is made: a message is a stranger's name, address and words.

```ts
import { verifyWebhook } from '@backendfree/core';

const event = await verifyWebhook<{ message_id: string }>({ body: await request.text(), headers, secret });
if (event.name === 'message.received') {
  const message = await inbox.message(event.data.message_id);
  await crm.add(message.email, message.name, message.message);
}
```

`message.received` carries the message's id and nothing anybody wrote in it.
It is sent when a message arrives in the inbox, or is moved there out of spam.

```ts
const { results } = await inbox.list({ folder: 'inbox' });
```
