Contact form email: get website enquiries to your inbox without becoming a spam relay

Updated

A contact form takes text from the public internet and turns it into an email from your domain. That makes it a target, and the sending code is mostly about defending it.

Workflow

  1. The visitor submits name, email and message to your server.
  2. Your server validates and rate-limits the request.
  3. It sends one email to a fixed internal address.
  4. Optionally it sends a fixed acknowledgement to the visitor.

The recipient and sender are decided in your code, never by the form fields.

Implementation (Express with the Node SDK)

import express from "express";
import rateLimit from "express-rate-limit";
import { Inboxili } from "@inboxili/node";

const inboxili = new Inboxili({ apiKey: process.env.INBOXILI_API_KEY });
const app = express();
app.use(express.json({ limit: "20kb" }));

const esc = (s) => String(s).replace(/[&<>"']/g, (c) => `&#${c.charCodeAt(0)};`);

app.post(
  "/contact",
  rateLimit({ windowMs: 10 * 60_000, limit: 5 }), // 5 submissions per IP per 10 minutes
  async (req, res) => {
    const { name, email, message, website } = req.body ?? {};
    if (website) return res.json({ ok: true }); // honeypot field: bots fill it, people never see it
    if (typeof email !== "string" || !/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email) || typeof message !== "string" || message.length < 5 || message.length > 5000) {
      return res.status(400).json({ error: "Please check your email and message." });
    }

    try {
      await inboxili.emails.send({
        to: "hello@yourdomain.com", // fixed recipient
        fromEmail: "website@yourdomain.com", // fixed sender
        fromName: "Website form",
        subject: "New enquiry from {{name}}",
        htmlBody: "<p><strong>{{name}}</strong> &lt;{{email}}&gt; wrote:</p><p>{{message}}</p>",
        textBody: `Enquiry from ${String(name ?? "").slice(0, 100)} <${email}>:\n\n${message}`,
        templateData: { name: esc(String(name ?? "").slice(0, 100)), email: esc(email), message: esc(message).replace(/\n/g, "<br>") },
      });
      res.json({ ok: true });
    } catch (err) {
      console.error("contact form send failed", err.status, err.code);
      res.status(502).json({ error: "Could not send your message. Please email us directly." });
    }
  }
);

Things that go wrong

  • Unescaped input in the HTML body. Escape every field.
  • Open relay behaviour. If the form let the visitor choose the recipient, anyone could use your domain to email anyone. Keep the recipient fixed.
  • No rate limit. Bots will submit thousands of messages, burn your sending quota and hurt your sender reputation.
  • No reply-to. Hitting reply in your inbox goes to the sending address, not the visitor. Put the visitor's address first in the body, and consider a "Reply" link of the form mailto:.
  • Silent failure. Log failed sends and give the visitor a fallback address.

Stronger spam defence

A honeypot and rate limit stop the crudest bots. For a public form that gets attention, add a challenge such as a CAPTCHA, and reject submissions that contain several links.

Delivery events

Bounces matter little here because the recipient is your own mailbox. Watch for complained events on the sending address, which would mean your own mail provider is junking the notifications.

Deliverability notes

Use a sender like website@ on your verified domain, and make sure the destination mailbox is not filtering it into spam.

Frequently asked questions

Can I set the visitor's address as reply-to?
The Inboxili API has no reply-to field today. Put the visitor's address in the body, and optionally send them a separate acknowledgement.
Should I send the visitor a copy?
An acknowledgement that says you received the message is useful, but do not echo their text back to an address they typed, because that lets anyone make you email a stranger. Keep the acknowledgement fixed.

Official SDKs

Send your first form notification

Create a workspace, verify a domain, and make your first API call.

Related