Pass a FormData object as the body and Postboi converts it into a tidy HTML table. This
is what powers the SvelteKit form actions, but you can pass FormData to any mail() call.
body also accepts a plain object of fields (like Express/multer’s req.body), which
is normalised and parsed the same way:
And a promise resolving to any of these, so you can hand a framework’s request.formData() straight through without awaiting it yourself:
Special fields
These keys are extracted from the body and applied to the send options instead of appearing in the table:
_to,_from,_subject,_reply_to_cc,_bcc: comma-separated or repeated
Values can be base64-encoded; they’ll be decoded automatically.
Set _reply_to to the submitter’s email so replies go back to them instead of your from address. See the SvelteKit form for the hidden-field
pattern. Typically that’s all a contact form needs: a reply-to is enough for replies to
reach the right person, with no per-message from or verified domain required.
Addresses may include a display name: "ACME Inc <hello@example.com>" arrives as ACME
Inc. That works anywhere an address does: _from, _reply_to, default.from, and the
CLI’s Default from prompt.
Two more fields are stripped before rendering: the _honey honeypot (the classic 🍯 name too) and Turnstile’s token (cf-turnstile-response, or _captcha on SvelteKit
remote forms). They drive the built-in spam protection and never appear in your
emails.
Grouped fields
Use the fieldset→field syntax to group related fields into sections:
This produces sectioned tables in the email body: one section per fieldset (contact, order), with a row per field.
Escaping
Field names and values are HTML-escaped before they reach the table, so a submission
can’t inject markup into the email. This matters because the form is usually public:
without escaping, anyone could put a <a href> or a tracking pixel into the
notification you read, arriving from your own sending domain.
Escape nothing yourself on the way in — you’d get visible < entities. The
derived plain-text body decodes back to exactly what the sender typed.
Multi-line values
Line breaks in a value become <br>, so a textarea arrives laid out the way it was
typed rather than collapsed into one run-on line. Browsers submit textareas with CRLF; \r\n, lone \r and lone \n are all handled, and blank lines survive as
consecutive breaks. A <br> someone submits is still escaped — only the breaks
Postboi adds are real markup.
Hand-rolled bodies
Escaping applies to the table renderer only. An HTML string passed as body is
yours and is sent verbatim, so sanitise that yourself if any part of it came from a
user. The same two helpers are exported for that:
escape_html for single-line values and attributes, escape_lines when the value may
contain newlines.
Attachments
Attachments work from file inputs (details→files) or via attachments: File | File[] on mail().
Customising the labels
The formatter option on mail() controls how fieldset and field
labels are rendered. Pass null or false to a part to leave those labels untouched.