This document covers local development, local configuration, and API debugging for ZSend.
bun- Cloudflare account
- Wrangler
- At least one working SMTP account
- A Cloudflare D1 database binding named
DB
Install dependencies:
bun installCreate your local environment file:
cp .dev.vars.example .dev.varsThen edit .dev.vars and fill in your local values for:
TOKENSMTP_CONFIGS
Start the local Worker dev server:
bun run devNotes:
- Wrangler loads local variables from
.dev.vars .dev.varsis ignored by git and should stay local only.dev.vars.exampleis the template for other users and new environments- Local dev port is fixed at
8000inwrangler.jsonc - The Worker entry is
src/index.ts - The project has no
test,lint, ortypecheckscript configured right now
Current bindings used by the Worker:
| Binding | Type | Required | Description |
|---|---|---|---|
TOKEN |
string | Yes | Bearer token required by POST /api/v1/send |
SMTP_CONFIGS |
array or JSON string | Yes | SMTP account list used to match the from address |
DB |
D1 binding | Yes | Stores mail delivery logs in mail_logs |
Each SMTP item should look like this:
[
{
"host": "smtp.example.com",
"port": 587,
"username": "smtp-login@example.com",
"password": "your-password",
"fromEmail": "no-reply@example.com",
"protocol": "tls",
"senderName": "ZSend"
}
]Fields:
host: SMTP server hostnameport: SMTP server portusername: SMTP login accountpassword: SMTP passwordfromEmail: optional actual sender email address; falls back tousernamewhen missing or emptyprotocol: usesslfor implicit TLS, otherwise the code treats it as STARTTLSsenderName: default display name for that SMTP account
The current code expects SMTP_CONFIGS as a JSON string. Use .dev.vars locally and Cloudflare secrets for production.
TOKEN=your-local-token
SMTP_CONFIGS=[{"host":"smtp.example.com","port":587,"username":"smtp-login@example.com","password":"your-smtp-password","fromEmail":"no-reply@example.com","protocol":"tls","senderName":"ZSend"}]Files:
.dev.vars: local-only real values.dev.vars.example: committed template without real secrets
GET /Example response:
{
"code": 200,
"msg": "Hello World!",
"data": null
}POST /api/v1/send
Authorization: Bearer <TOKEN>
Content-Type: application/jsonRequest body:
{
"from": "no-reply@example.com",
"to": "user@example.com",
"title": "Welcome",
"content": "# Hello\nThis message was sent by ZSend.",
"type": "markdown",
"sender_name": "ZSend Notifications"
}Request fields:
from: required, the server first tries to matchSMTP_CONFIGS[].fromEmail; if no match is found, it falls back toSMTP_CONFIGS[].usernameto: required, recipient email addresstitle: required, mail subjectcontent: required, mail bodytype: optional, one oftext,html,markdown; defaults totextsender_name: optional, overrides the configured display name for this request
Example curl:
curl -X POST "http://127.0.0.1:8000/api/v1/send" \
-H "Authorization: Bearer your-token" \
-H "Content-Type: application/json" \
-d '{
"from": "no-reply@example.com",
"to": "user@example.com",
"title": "Welcome",
"content": "# Hello\nThis message was sent by ZSend.",
"type": "markdown",
"sender_name": "ZSend Notifications"
}'Accepted response example:
{
"code": 200,
"msg": "Email request accepted",
"data": {
"from": "no-reply@example.com",
"to": "user@example.com",
"title": "Welcome"
}
}Important behavior:
- Invalid or missing Bearer token returns
401 - Invalid JSON body returns
400 - Missing required fields returns
400 - Unsupported
typereturns400 - If no SMTP config matches
from, the API returns400 - The HTTP API returns after the request is accepted; sending and log writing continue in
waitUntil - SMTP sending is retried once after a 10 second delay if the first attempt fails
bun run dev
bun run deploy
bun run cf-typegen