Skip to content
mdlake

Mail

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

VariableSecretMeaning
MAIL_PROVIDERNoresend or log. Case does not matter. Any other value is an error.
RESEND_API_KEYYesThe Resend API key.
MAIL_FROMNoThe sender of the mail, for example Website <forms@example.com>. It must be on a domain you verified in Resend.
<RECIPIENT>NoThe 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

  1. If MAIL_PROVIDER is set, it decides.
  2. If not, the provider is resend when RESEND_API_KEY is set, and log otherwise.

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.

  1. Create a Resend account and add your sending domain, for example example.com.
  2. 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.
  3. Create an API key in Resend with permission to send.
  4. Set MAIL_FROM to an address on that domain, such as Website <forms@example.com>.
  5. Store the key as a secret: in Key Vault for a deployed site, and in your local .env for 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 happenedAnswer to the visitorIn the log
Resend could not be reached502 delivery-failedform "contact": mail failed (Resend could not be reached)
Resend answered with an error502 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] end

from 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

WhereHow
Local developmentIn .env, which Git ignores. npx mdlake add-form writes the names to .env.example.
Azure, plain settingsIn the settings object of infra/parameters/production.bicepparam (MAIL_PROVIDER, MAIL_FROM and the recipient variable). They are not secret.
Azure, secretsRESEND_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

SymptomFix
500 mail-not-configuredMAIL_PROVIDER=resend needs RESEND_API_KEY and MAIL_FROM. Set the missing one.
500 recipient-not-configuredThe recipient variable is empty, or has no valid address. Remove display names.
502 delivery-failed and Resend answered 403 in the logThe 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 successThe form’s honeypot was filled (a script), or the provider is log. Check the log.
Mail goes to spamFinish the DNS records Resend asks for (SPF and DKIM), and use a MAIL_FROM on your own domain.
The .NET backend does not startThe log says “MAIL_PROVIDER must be resend or log, and resend needs RESEND_API_KEY and MAIL_FROM.” Fix the settings.

What’s next