A lightweight, self-hosted email relay for contact forms. Zero external dependencies — just a single Go binary.
- HTML form and API access with JSON or multipart form data
- Configurable CORS and origin restriction
- Per-target rate limiting
- Optional API key authentication
- File attachments
- Honeypot antispam (hidden
_gotchafield) - Optional Cloudflare Turnstile CAPTCHA verification
- Redirect support for form submissions
Create a targets/ directory and add a JSON file for each endpoint:
mkdir targets
cp targets/example.json targets/my-site.json
# Edit with your SMTP credentials and recipientsSee targets/example.json for reference.
A public image is available on GitHub Container Registry:
docker run -d \
-p 3000:3000 \
-v $(pwd)/targets:/targets \
-e TARGETS_DIR=/targets \
ghcr.io/devopsmitch/go-mail-form:mainOr build it yourself:
docker build -t go-mail-form .
docker run -d \
-p 3000:3000 \
-v $(pwd)/targets:/targets \
-e TARGETS_DIR=/targets \
go-mail-formcurl -X POST http://localhost:3000/my-site \
-d "[email protected]" \
-d "subject=Hello" \
-d "body=Test message"Each setting can be provided as a command-line flag or an environment variable. When both are set, the flag takes precedence. Precedence is: flag > environment variable > default.
| Flag | Environment variable | Default | Description |
|---|---|---|---|
--port |
PORT |
3000 |
Server port |
--targets-dir |
TARGETS_DIR |
targets |
Path to target JSON files |
--trusted-header |
TRUSTED_HEADER |
(empty) | HTTP header to trust for client IP (e.g. CF-Connecting-IP, X-Forwarded-For). If unset, uses the direct connection IP |
Other flags:
| Flag | Description |
|---|---|
--version |
Print the version and exit |
| Field | Required | Description |
|---|---|---|
transport |
no | Delivery transport: smtp (default) or ses |
smtp |
conditional | SMTP(S) URL, e.g. smtps://user:[email protected]. Required when transport is smtp |
ses.region |
conditional | AWS region for Amazon SES, e.g. us-east-1. Required when transport is ses |
from |
yes | Sender address. Must be a verified identity when using SES |
recipients |
yes | Array of recipient email addresses |
rateLimit.timespan |
yes | Rate limit window in seconds |
rateLimit.requests |
yes | Max requests per window per IP |
origin |
no | Allowed HTTP origin (CORS). Default * |
subjectPrefix |
no | Prefix prepended to all subjects |
key |
no | API key (sent as Authorization: Bearer <key>) |
redirect.success |
no | URL to redirect on success |
redirect.error |
no | URL to redirect on error |
turnstile.secretKey |
no | Cloudflare Turnstile secret key. Enables CAPTCHA verification when set |
Rate limiting IP detection: The client IP is resolved from the
TRUSTED_HEADERif configured, otherwise from the direct connection address. SetTRUSTED_HEADER=CF-Connecting-IPbehind Cloudflare, orTRUSTED_HEADER=X-Forwarded-Forbehind other reverse proxies. Leave unset if clients connect directly.
Note: The
origincheck relies on theOriginheader, which is only sent by browsers. Non-browser API clients (e.g.curl) won't send it, so requests will be rejected iforiginis set. If you need both browser form submissions and API access on the same target, usekeyfor API clients andoriginfor browser CORS — or create separate targets.
{
"smtp": "smtps://user:[email protected]",
"recipients": ["[email protected]"],
"from": "[email protected]",
"rateLimit": { "timespan": 60, "requests": 5 },
"turnstile": {
"secretKey": "0x4AAAAAAA..."
}
}Set transport to ses and provide a region. The message is delivered through
the SES SendEmail API over HTTPS instead of SMTP, so no SMTP credentials are
needed. The from address must be a verified SES identity (email or domain).
{
"transport": "ses",
"ses": { "region": "us-east-1" },
"recipients": ["[email protected]"],
"from": "[email protected]",
"rateLimit": { "timespan": 60, "requests": 5 }
}AWS credentials are resolved from the standard credential chain: environment
variables (AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY / AWS_SESSION_TOKEN),
shared config (~/.aws), or an attached IAM role / task role. The role needs
the ses:SendEmail permission. SES clients are created per distinct region only
when at least one target uses the SES transport, so SMTP-only deployments do not
require AWS credentials.
| Field | Required | Description |
|---|---|---|
from |
no | Visitor's email address. Used as the Reply-To so replies go to the visitor; the email's From is always the target's configured from |
name |
no | Visitor's name, combined with from in the Reply-To |
subject |
yes | Email subject (2-255 chars) |
body |
yes | Email body, supports HTML (5-32000 chars) |
subjectPrefix |
no | Per-request subject prefix |
_gotcha |
no | Honeypot field — leave empty. Bots that fill this in are silently rejected |
<form method="post" action="https://mailform.example.com/my-site">
<input type="email" name="from" placeholder="Your email" />
<input type="text" name="name" placeholder="Your name" />
<input type="text" name="subject" placeholder="Subject" />
<textarea name="body" placeholder="Your message"></textarea>
<!-- Honeypot: hide with CSS, invisible to screen readers and keyboard -->
<div class="hp-field" aria-hidden="true">
<input type="text" name="_gotcha" tabindex="-1" autocomplete="off" />
</div>
<!-- Optional: Cloudflare Turnstile CAPTCHA -->
<div class="cf-turnstile" data-sitekey="YOUR_SITE_KEY"></div>
<button type="submit">Send</button>
</form>
<!-- Include only if using Turnstile -->
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
<style>
.hp-field { position: absolute; left: -9999px; }
</style>| Code | Meaning |
|---|---|
200 |
Email sent |
400 |
Bad request |
401 |
Unauthorized (wrong or missing API key) |
403 |
Forbidden (origin mismatch) |
404 |
Target not found |
405 |
Method not allowed |
422 |
Validation error (details in JSON body) |
429 |
Rate limited |
500 |
Email sending failed |
GET /healthz → 200 OK
This project follows Semantic Versioning. Given a version MAJOR.MINOR.PATCH:
- MAJOR — Changes that may break existing deployments, such as changes to target-file fields, request fields, or the HTTP request/response contract. Review the release notes before upgrading.
- MINOR — Backwards-compatible new features (e.g. a new optional target field).
- PATCH — Backwards-compatible bug fixes.
While the version is 0.x, the target-file schema and API should be considered unstable and may change between minor releases. Version 1.0.0 marks the point where the target-file format and API contract are considered stable.
Released binaries and tagged Docker images report their version via --version and in the startup log. Docker images built from main (untagged) report the commit SHA instead.