# Contact form & email

> How the Astro Actions contact flow works, and the Resend or SMTP env vars that make it deliver mail in production.

The starter ships a working contact form — validated, spam-screened, and wired
to an email provider. In dev it logs to the console; in production it sends real
mail once you set a few env vars. Nothing is mocked.

## The flow

The form is an **Astro Action**, defined in `src/actions/index.ts`:

<CodeBlock filename="src/actions/index.ts">{`export const server = {
  contact: defineAction({
    accept: 'form',
    input: z.object({
      name: z.string().min(1).max(100),
      email: z.email().max(254),
      message: z.string().min(10).max(5000),
      _website: z.string().max(0).optional(), // honeypot
    }),
    handler: async (input) => {
      if (input._website) return { ok: true }; // bot — silently drop
      await sendContactEmail(input);
      return { ok: true };
    },
  }),
};`}</CodeBlock>

What happens on submit:

1. The page posts straight to the action: `<form method="post" action={actions.contact}>`. Astro handles progressive enhancement.
2. **Zod validates** name / email / message; invalid input returns a typed error the page renders inline.
3. The **honeypot** `_website` field (hidden from humans, irresistible to bots) short-circuits with a silent success.
4. Valid input is handed to `sendContactEmail` — the provider seam in `src/lib/email/`.
5. The page reads the outcome with `Astro.getActionResult(actions.contact)` and shows success or error.

<Callout variant="warn" title="Actions need a server">
  The action runs server-side, so the starter pins the `@astrojs/node` adapter.
  On Cloudflare / Netlify / Vercel, swap the adapter so it deploys as a function
  — see [Deploying](/deploying).
</Callout>

## The email seam

`src/lib/email/index.ts` re-exports one provider's `sendContactEmail`. The CLI
writes the right import based on the provider you pick at scaffold time:

- **Resend** (default) → `./resend.ts`
- **SMTP** → `./smtp.ts`
- **None** → a console logger that prints submissions with a "configure your
  provider" warning

Switch providers any time by editing that one import. Because the action only
knows the seam, nothing downstream changes.

## Environment variables

Copy `.env.example` to `.env` and fill in what your provider needs. **Local dev
works with none of these set** — submissions log to the console.

### Resend

<CodeBlock filename=".env">{`RESEND_API_KEY=re_xxxxxxxx     # from resend.com
CONTACT_TO_EMAIL=you@example.com`}</CodeBlock>

### SMTP

<CodeBlock filename=".env">{`SMTP_HOST=smtp.example.com
SMTP_PORT=587
SMTP_USER=postmaster@example.com
SMTP_PASS=••••••••
SMTP_FROM="Acme <hello@example.com>"
CONTACT_TO_EMAIL=you@example.com`}</CodeBlock>

`CONTACT_TO_EMAIL` is where submissions are delivered, regardless of provider.

<Callout variant="note" title="Set them where the server runs">
  Env vars belong in your host's dashboard (or the runtime environment for a
  Node deploy), never committed to the repo. On static hosts they go on the
  function that runs the action.
</Callout>

## Customizing

- **Add a field** → extend the Zod `input` schema and add the matching
  `<input>` to `src/pages/contact.astro`. The type flows through automatically.
- **Stronger spam protection** is intentionally left out — the honeypot covers
  the basics; add Turnstile or hCaptcha if you need more.
- **Change the email body** → edit `src/lib/email/resend.ts` (or `smtp.ts`).
