How the forms module sends mail, with Resend in production and a log provider for development, and the settings and failure modes that go with it. For developers.
On this page
Every form sends one mail per valid submission to the recipient you configure. The forms module has two mail providers. resend sends the message through the Resend API. log prints it to the server output instead, which is what you want while you build. You choose between them with environment variables, so no code changes between your laptop and production.
The mail settings are the same for the Astro server (the server hosting model) and for both backend runtimes (Node and .NET) of a split site.
The settings
| Variable | Secret | Meaning |
|---|---|---|
MAIL_PROVIDER | No | resend or log. Case does not matter. Any other value is an error. |
RESEND_API_KEY | Yes | The Resend API key. |
MAIL_FROM | No | The sender of the mail, for example Website <forms@example.com>. It must be on a domain you verified in Resend. |
<RECIPIENT> | No | The form’s recipient variable, named by recipientEnv in the form definition (for example CONTACT_FORM_TO). One address, or several separated by commas or semicolons. |
Which provider is used
- If
MAIL_PROVIDERis set, it decides. - If not, the provider is
resendwhenRESEND_API_KEYis set, andlogotherwise.
resend needs both RESEND_API_KEY and MAIL_FROM. When either is missing, the form answers 500 mail-not-configured and the server log names the missing variable, never its value. The .NET backend is stricter: it refuses to start when MAIL_PROVIDER is not resend or log, or when resend has no key or no sender (ValidateOnStart).
Recipients
The recipient variable is split on commas and semicolons. Each part is trimmed and kept only if it looks like a plain address (name@host, no spaces, no < or >). That means Anna <anna@example.com> is dropped; write anna@example.com. With no valid address the form answers 500 recipient-not-configured. The log provider does not need a recipient.
Resend
Resend is a hosted mail API. You need an account and a verified sending domain.
- Create a Resend account and add your sending domain, for example
example.com. - Add the DNS records Resend shows you (they prove that you own the domain and sign the mail). Wait until Resend marks the domain as verified.
- Create an API key in Resend with permission to send.
- Set
MAIL_FROMto an address on that domain, such asWebsite <forms@example.com>. - Store the key as a secret: in Key Vault for a deployed site, and in your local
.envfor testing. See Azure and Bicep.
The kit calls POST https://api.resend.com/emails with a bearer key. The mail has a plain-text part and a simple HTML table. Labels come from the form definition, values are escaped, and a single value is cut at 10,000 characters. A field the visitor left empty shows as (not given), a checkbox as Yes or No. Line breaks are removed from the subject and the sender so a visitor cannot inject mail headers. If the form has replyTo and that field holds a plausible address, it becomes the Reply-To.
Tip
Use a separate Resend API key for the site. The CMS has its own mail setting (Email:Resend:ApiKey) and the two have nothing to do with each other.
When Resend fails
| What happened | Answer to the visitor | In the log |
|---|---|---|
| Resend could not be reached | 502 delivery-failed | form "contact": mail failed (Resend could not be reached) |
| Resend answered with an error | 502 delivery-failed | ... (Resend answered 403: <Resend's message>) |
The visitor sees “The message could not be sent. Please try again later.” The log line never contains the key.
The log provider
With MAIL_PROVIDER=log (or no key at all), a valid submission is printed instead of sent:
[mdlake mail:log] to=owner@example.com from=forms@localhost subject="New message from the website" reply-to=anna@example.com
Your name: Anna
Email address: anna@example.com
Message: Hello, I would like a quote.
[mdlake mail:log] endfrom is MAIL_FROM, or forms@localhost when that is not set. In the server tier the line appears in the Astro dev server output. In a split site it appears in the backend’s output ([api] lines in npm run dev:all).
Warning
In production, log means mail is lost. The server logs a warning the first time it happens: “MAIL_PROVIDER is “log” (or RESEND_API_KEY is not set): submissions are printed to the log and not mailed”. If you see it in the App Service log stream, a setting is missing.
Where to set the values
| Where | How |
|---|---|
| Local development | In .env, which Git ignores. npx mdlake add-form writes the names to .env.example. |
| Azure, plain settings | In the settings object of infra/parameters/production.bicepparam (MAIL_PROVIDER, MAIL_FROM and the recipient variable). They are not secret. |
| Azure, secrets | RESEND_API_KEY is listed in secretNames. Its value goes into Key Vault. See Azure and Bicep. |
The generated parameter file starts with MAIL_PROVIDER: 'resend' and empty MAIL_FROM and recipient values. Fill them in before the first deployment.
What can go wrong
| Symptom | Fix |
|---|---|
500 mail-not-configured | MAIL_PROVIDER=resend needs RESEND_API_KEY and MAIL_FROM. Set the missing one. |
500 recipient-not-configured | The recipient variable is empty, or has no valid address. Remove display names. |
502 delivery-failed and Resend answered 403 in the log | The sender domain is not verified in Resend, or the key may not send. Check the domain and the key. |
| No mail arrives and the response was a success | The form’s honeypot was filled (a script), or the provider is log. Check the log. |
| Mail goes to spam | Finish the DNS records Resend asks for (SPF and DKIM), and use a MAIL_FROM on your own domain. |
| The .NET backend does not start | The log says “MAIL_PROVIDER must be resend or log, and resend needs RESEND_API_KEY and MAIL_FROM.” Fix the settings. |
What’s next
- Turnstile to add a bot check in front of the mail.
- Azure and Bicep to put the settings in place.
