Surat is a lightweight email notification service with support for MJML (email templates), Handlebars (variable injection), and built-in Swagger API docs.
It is packaged as a Docker image for easy deployment with SMTP integration.
- 📩 Send emails via any SMTP provider
- 🎨 Templating with MJML + Handlebars
- 🛠️ Auto-generated Swagger docs (
/api) - 🐳 Docker image ready to use
- 🔑 Easy configuration via environment variables
- 🔒 Optional API key authentication
- 📎 Attachments (plain text or base64), including inline images
docker pull ariefsn/surat:latestdocker run -d \
-p 3000:3000 \
-e PORT=3000 \
-e SMTP_HOST=smtp.example.com \
-e SMTP_PORT=587 \
-e SMTP_USER=myuser \
-e SMTP_PASSWORD=mypassword \
-e SMTP_SECURE=false \
-e DEFAULT_SENDER_EMAIL=no-reply@example.com \
-e DEFAULT_SENDER_NAME="Surat" \
-e SMTP_POOL=true \
-e SMTP_MAX_CONNECTIONS=10 \
-e SMTP_MAX_MESSAGES=100 \
-e BODY_LIMIT=10mb \
-e API_KEY=change-me \
--name surat \
ariefsn/surat:latestversion: "3.8"
services:
surat:
image: ariefsn/surat:latest
container_name: surat
restart: unless-stopped
ports:
- "3000:3000"
environment:
PORT: 3000
SMTP_HOST: smtp.example.com
SMTP_PORT: 587
SMTP_USER: myuser
SMTP_PASSWORD: mypassword
SMTP_SECURE: "false"
SMTP_POOL: "true"
SMTP_MAX_CONNECTIONS: "10"
SMTP_MAX_MESSAGES: "100"
DEFAULT_SENDER_EMAIL: no-reply@example.com
DEFAULT_SENDER_NAME: "Surat"
BODY_LIMIT: "10mb"
API_KEY: change-me| Variable | Required | Default | Description |
|---|---|---|---|
PORT |
✅ | 3000 |
Port the app listens on |
SMTP_HOST |
✅ | – | SMTP server host |
SMTP_PORT |
✅ | – | SMTP server port |
SMTP_USER |
✅ | – | SMTP username |
SMTP_PASSWORD |
✅ | – | SMTP password |
SMTP_SECURE |
❌ | false |
Enable TLS/SSL (true / false) |
SMTP_POOL |
❌ | true |
Enable connection pooling (true / false) |
SMTP_MAX_CONNECTIONS |
❌ | 10 |
Max SMTP connections per pool |
SMTP_MAX_MESSAGES |
❌ | 100 |
Max messages per connection |
DEFAULT_SENDER_EMAIL |
✅ | – | Default sender email address |
DEFAULT_SENDER_NAME |
✅ | – | Default sender display name |
BODY_LIMIT |
❌ | 10mb |
Body limit for requests |
API_KEY |
❌ | – | When set, /email and /mjml require a matching X-API-Key header. Leave empty to disable auth |
💡 You can store them in a .env file and run with:
docker compose --env-file .env up -dExample .env:
PORT=3000
SMTP_HOST=smtp.example.com
SMTP_PORT=587
SMTP_USER=myuser
SMTP_PASSWORD=mypassword
SMTP_SECURE=false
DEFAULT_SENDER_EMAIL=no-reply@example.com
DEFAULT_SENDER_NAME=Surat
BODY_LIMIT=10mb
API_KEY=change-meSwagger UI is available at:
👉 http://localhost:3000/api
Endpoints:
GET /→ Health checkPOST /email→ Send an emailPOST /mjml→ Convert MJML to HTML
Authentication is off by default. Set API_KEY to turn it on. Requests to POST /email and POST /mjml must then send the same value in the X-API-Key header, or they get 401 Unauthorized. GET / (health check) and the Swagger UI (/api) stay public.
curl -X POST http://localhost:3000/email \
-H "Content-Type: application/json" \
-H "X-API-Key: change-me" \
-d @payload.jsonIn Swagger UI, click Authorize and enter the key.
POST /email accepts an attachments array. Each item has these fields:
| Field | Required | Description |
|---|---|---|
filename |
✅ | File name shown to the recipient |
content |
✅ | File content, as plain text or encoded (see encoding) |
encoding |
❌ | Encoding of content, e.g. base64. Omit it to send content as plain text |
type |
❌ | MIME type, e.g. application/pdf, image/png. Guessed from filename when omitted |
disposition |
❌ | attachment (default) or inline |
contentId |
❌ | Content ID for inline images, referenced in the body as cid:<contentId> |
Surat never guesses the encoding. Binary files such as PDFs and images must be sent base64-encoded with "encoding": "base64", otherwise the recipient gets the base64 text instead of the file.
{
"attachments": [
{
"filename": "invoice.pdf",
"content": "JVBERi0xLjQK...",
"encoding": "base64",
"type": "application/pdf"
},
{
"filename": "note.txt",
"content": "Hello from Surat"
},
{
"filename": "logo.png",
"content": "iVBORw0KGgo...",
"encoding": "base64",
"type": "image/png",
"disposition": "inline",
"contentId": "logo"
}
]
}The inline image above is shown in the body with <img src="cid:logo">.
Attachment size counts toward
BODY_LIMIT. Base64 makes a file about 33% larger.
The service provides several custom Handlebars helpers out of the box, so you can use them directly inside your email templates:
| Helper | Example (Template) | Result |
|---|---|---|
add |
{{add 2 3}} |
5 |
gt |
{{gt 5 3}} |
true |
gte |
{{gte 5 5}} |
true |
lt |
{{lt 2 5}} |
true |
lte |
{{lte 2 2}} |
true |
eq |
{{eq "a" "a"}} |
true |
ne |
{{ne "a" "b"}} |
true |
or |
{{or true false}} |
true |
and |
{{and true false}} |
false |
not |
{{not true}} |
false |
contains |
{{contains "hello world" "wor"}} |
true |
startWith |
{{startWith "hello" "he"}} |
true |
endWith |
{{endWith "hello" "lo"}} |
true |
replace |
{{replace "foo bar" "bar" "baz"}} |
foo baz |
json |
{{json myObject}} |
{"key":"value"} |
👉 Example MJML + Handlebars usage:
<mj-text>
Hello {{name}},
You have {{add unreadCount 1}} unread messages.
</mj-text>- Node.js 24.22.1 or newer
- NestJS
- MJML
- Handlebars
- Nodemailer
- Swagger
This project is licensed under the MIT License.