@backendfree/inbox
v0.1.0
Published
A website's contact form, sent to its BackendFree inbox past the spam checks, and its messages read back.
Maintainers
Readme
@backendfree/inbox
A website's contact form, sent to its inbox on the dashboard, and the messages
read back. Built on @backendfree/core.
npm install @backendfree/core @backendfree/inboximport { 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 has the figures.
The hidden field, hidden from people and from screen readers but not from a script reading the page:
<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:
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.
import { verifyWebhook } from '@backendfree/core';
const event = await verifyWebhook({ 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.
const { results } = await inbox.list({ folder: 'inbox' });