Why your form's auto-reply shouldn't come from your form
A short engineering story about how letting users control auto-reply email content turns your service into a free SMTP relay, and the boring fix.
When we built auto-reply for SaveForm, the obvious API was the one everyone asks for: hidden form fields. Add _autoReply=true and _autoReplyMessage=“Hi, we got it” to the form payload, and the service sends an acknowledgement email back to the submitter. Five minutes to specify, ten minutes to implement. We almost shipped it. Then we noticed it was a free email relay.
Anything that lets the form payload control both the recipient and the email body is, by definition, an open SMTP relay.
The attack, in three lines
Imagine the API we almost built. Someone finds your public contact form. They submit:
{
"email": "victim@example.com",
"_autoReply": "true",
"_autoReplySubject": "Your invoice is overdue",
"_autoReplyMessage": "Pay this link or we'll send to collections..."
}Our service dutifully sends a phishing email to the victim, signed by our verified sender domain. From the victim's perspective, the email passed SPF, DKIM, and DMARC, because it really did come from us. We just told it to.
The damage spreads
- Recipients mark the email as spam. Our sender reputation degrades. Now every legitimate notification email from every other customer starts landing in promotions tabs.
- Resend (or any sane email provider) shuts us down. Their ToS explicitly prohibits user-generated email content with arbitrary recipients. We don't blame them.
- Domain blocklists pile on. Spamhaus, SURBL, the works. Cleanup is a multi-week ordeal.
- The form owner gets the call. Even though they did nothing wrong, the abusive emails went out under their form's name. They're the one explaining it to the recipient.
The bug isn't that we sent an email. The bug is that we sent an email whose subject and body were authored by a stranger to a recipient also chosen by that stranger.
The fix is boring. That's the point.
The whole problem disappears the moment you constrain who controls what:
- Subject and body live on the form record. The form owner authors them once, in the dashboard. The HTTP payload can't override them.
- Personalisation via variables, not raw text. The owner's template can include
{{name}}and it'll substitute the submitter's name. The submitter never gets to inject prose, only fill in the blanks the owner predefined. - Variable values are HTML-escaped. A mischievous “name” like
<script>ends up rendered as text, not executed. - Recipient hardcoded to the form's email field. The submitter can only get the auto-reply at the email they're already submitting from. They can't target someone else.
- Spam-flagged submissions skip auto-reply. Disposable email domains, gibberish names, the Gmail dot trick are all already caught by the existing spam scoring. The auto-reply path piggybacks on that.
What the API actually looks like
From the form owner's side:
They open Form settings → Auto-reply and write something like:
Subject: Thanks for reaching out, {{name}}
Hi {{name}},
We got your message and someone from the team will reply within
one business day. In the meantime, if it helps, our docs cover
the most common questions: https://example.com/docs.
Cheers,
The teamFrom the submitter's side: they fill out the form with name, email, and message like always. Nothing in the form's markup tells them an auto-reply will fire. It just shows up in their inbox a few seconds later.
No Reply-To. Yes, on purpose.
The other tempting feature is to set the Reply-To header on the auto-reply to the form owner's email, so the submitter can reply directly to a human. We thought about it and chose not to.
- It exposes the form owner's email to every submitter, which for a public contact form is most of the internet.
- It implies a conversation can happen, when most acknowledgement emails are intentionally fire-and-forget.
- If the form owner wants a path back, they can put a support email or contact link directly in the message body. That's way more discoverable than a header most users never see.
The takeaway
When designing a feature that sends email on someone's behalf, map every input's control plane to who's allowed to set it:
- Recipient: derived from form payload, but only from a specific field, not arbitrary.
- Subject + body: form owner only, server-side.
- Variables in the body: from form payload, but escaped.
- Sender domain: ours, never controllable.
Anything more permissive turns your service into a vector. Anything less permissive shuts down legitimate use cases. This is the line we settled on, and so far we haven't had to walk it back.
For the boring details, the auto-reply docs cover the limits, the variable syntax, and the exact conditions under which the email fires.
Auto-reply that won’t torch your domain
Server-controlled templates, payload-controlled variables, spam-aware delivery. The boring design that doesn't make the news.