JS-Mailer is a simple webservice, that allows JavaScript-based websites to easily send form data, by providing a simple
API that can be accessed via JavaScript Fetch() or XMLHttpRequest.
- Single-binary webservice
- Multi-form support
- Multiple recipients per form
- Only display form-fields that are configured for the form in the resulting mail
- Check for required form fields
- Anti-SPAM functionality via built-in, auto-expiring and single-use security token feature
- Anti-SPAM functionality via honeypot fields
- Limit form access to specific domains
- Per-form mail server configuration
- hCaptcha support
- reCaptcha v2 (Checkbox) support
- Turnstile support
- Private Captcha support
- Form field type validation (text, email, number, boolean, matchvalue)
- Confirmation mail to poster
- Custom Reply-To header based on sending mail address
- Form body templates (possibly HTML)
There is a ready-to-use Docker image hosted on Github.
- Download the image:
$ docker pull ghcr.io/wneessen/js-mailer:main
- Get your config files in place
- Run the image:
$ docker run -p 8765:8765 -v /etc/js-mailer:/etc/js-mailer ghcr.io/wneessen/js-mailer:main
The service, by default, searches for its configuration file in $HOME/.config/js-mailer/. In there it
will look for a file named js-mailer.ext where ext is one of json, toml or yaml.
The config format is very simple and looks like this:
[log]
# Log level (slog): -4=DEBUG, 0=INFO, 4=WARN, 8=ERROR
level = 0
format = "json"
[forms]
# Directory where form definitions are stored
path = "/var/lib/app/forms"
# Default expiration for generated forms
default_expiration = "10m"
[server]
# Address and port the HTTP server binds to
address = "127.0.0.1"
port = "8765"
# Cache lifetime for captcha / form responses
cache_lifetime = "10m"
# Request timeout
timeout = "15s"Each form has its own configuration file. The configuration is searched for in the forms path that has been defined in
the configuration file. The form configuration file must be named <formid>.ext where <formid> is the form id and
ext is one of json, toml or yaml.
Equivalent to the server configuration, the form configuration file format is very simple and looks like this:
# Unique form identifier
id = "contact_form"
# Domains allowed to submit this form
domains = ["example.com", "www.example.com"]
# Email recipients for form submissions
recipients = ["[email protected]"]
# Sender address used for outgoing emails
sender = "[email protected]"
# Shared secret used for form token generation
secret = "super-secret-value"
# Mail content configuration
[content]
subject = "New contact form submission"
fields = ["name", "email", "message"]
# Confirmation mail configuration
[confirmation]
enabled = true
rcpt_field = "email"
subject = "We received your message"
content = "Thank you for contacting us. We will get back to you shortly."
# Form Reply-To address configuration
[reply_to]
field = "email"
# Mail server configuration
[server]
host = "smtp.example.com"
port = 587
username = "smtp-user"
password = "smtp-password"
force_tls = true
dry_run = false
# Form validation configuration
[validation]
honeypot = "company"
# Form field validation configuration
[[validation.fields]]
name = "name"
required = true
type = "string"
[[validation.fields]]
name = "email"
required = true
type = "email"
[[validation.fields]]
name = "message"
required = true
type = "string"
# Form captcha providers configuration
[validation.hcaptcha]
enabled = false
secret_key = ""
[validation.recaptcha]
enabled = true
secret_key = "recaptcha-secret-key"
[validation.turnstile]
enabled = false
secret_key = ""
[validation.private_captcha]
enabled = false
host = "captcha.internal.example"
api_key = "private-captcha-api-key"JS-Mailer follows a two-step workflow. First your JavaScript requests a token from the API using the /token
endpoint. If the request is valid and website is authorized to request a token, this endpoint returns all information
required by your HTML form and JavaScript to submit form data to the API.
Use the values from the response as follows:
- Set the form’s
actionattribute to the value ofdata.url - Set the form’s
methodattribute toPOST(fromdata.request_method) - Set the form’s
enctypeattribute to the value ofdata.encoding
Once the form is submitted, the API validates the sender token, checks all submitted fields against the configured form validation rules, and—if validation succeeds—delivers the form data to the configured recipients using the configured mail server.
The sender token is bound to the form (data.form_id) and is only valid within the time window defined by
data.create_time and data.expire_time. Submissions using expired or invalid tokens will be rejected.
{
"success": true,
"statusCode": 201,
"status": "Created",
"message": "sender token successfully created",
"timestamp": "2025-12-21T17:56:22.867942901Z",
"data": {
"token": "cb8620734dd48c81d843be9c70d32b546643e0aff64c79ba195aa90db0b55059",
"form_id": "test_form",
"create_time": 1766339782,
"expire_time": 1766340382,
"url": "https://jsmailer.example.internal/send/test_form/cb8620734dd48c81d843be9c70d32b546643e0aff64c79ba195aa90db0b55059",
"encoding": "multipart/form-data",
"request_method": "POST"
}
}All API endpoints return a JSON response that follows a consistent, envelope-based format. This ensures predictable handling of both successful and failed requests across the entire API.
| Field | Type | Description |
|---|---|---|
success |
boolean |
Indicates whether the request was processed successfully. |
statusCode |
number |
HTTP status code associated with the response. |
status |
string |
Human-readable HTTP status text (e.g. OK, Created, Bad Request). |
message |
string |
Optional short description of the result. |
timestamp |
string (RFC 3339) |
Server-side timestamp indicating when the response was generated. |
requestId |
string |
Optional unique identifier for request tracing and debugging. |
data |
object |
Optional endpoint-specific response payload. |
errors |
string[] |
Optional list of error messages describing why the request failed. |
A successful request has the following characteristics:
successistruestatusCodeis a 2xx HTTP status codedatacontains the endpoint-specific payloaderrorsis omitted
{
"success": true,
"statusCode": 200,
"status": "OK",
"message": "request processed successfully",
"timestamp": "2025-12-21T18:10:00Z",
"data": {
"result": "example"
}
}A failed request returns structured error information:
successisfalsestatusCodeis a 4xx or 5xx HTTP status codeerrorscontains one or more descriptive error messagesdatais omitted
{
"success": false,
"statusCode": 400,
"status": "Bad Request",
"message": "validation failed",
"timestamp": "2025-12-21T18:11:00Z",
"errors": [
"email is required",
"captcha verification failed"
]
}- Always check
successbefore processingdata. - Use
statusCodefor programmatic error handling. - Treat
messageas informational and not machine-readable. - Do not assume optional fields are present.
This unified response format enables consistent client-side handling and simplified API integrations.