A microservice you can use to send emails on your behalf, written in Rust on top of lettre and axum.
Mailman follows the same pattern as geodude:
- an HTTP microservice that owns your SMTP credentials and sends mail, and
- a Rust client crate (SDK) that other services use to ask it to send mail.
The pattern is: deploy one mailman server somewhere, then
cargo add mailman in every other Rust service that needs to send email. The
server keeps the SMTP credentials in one place; clients just make HTTP calls.
[dependencies]
mailman = "0.2" # client by default; no need to disable anything
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }Point at your server once and send mail from anywhere:
mailman::setup("https://mailman.example.com", "super-secret-token")?;
let email = mailman::SendEmailRequest::new(
"Hello from mailman",
"This one's plain text.",
["alice@example.com", "Bob <bob@example.com>"],
);
mailman::send(&email).await?;Or let it read the URL and token from the environment (MAILMAN_URL and
MAILMAN_API_TOKEN):
let email = mailman::SendEmailRequest::new("Hi", "<h1>Hello!</h1>", ["bob@example.com"])
.html(true)
.cc(["carol@example.com"])
.attach(mailman::Attachment::new(
"invoice.pdf",
"application/pdf",
base64_encoded_bytes,
));
mailman::send(&email).await?; // reads MAILMAN_URL / MAILMAN_API_TOKENsend lazily falls back to MAILMAN_URL / MAILMAN_API_TOKEN on first call,
so for the env-only path you can skip setup() entirely. For multiple servers
or finer control over the reqwest::Client, use mailman::Client::new(url, token)
directly.
Bearer-token authentication is enabled by default. Send the shared secret from
MAILMAN_API_TOKEN as Authorization: Bearer <token>. Set
MAILMAN_REQUIRE_API_TOKEN=false to make the endpoint unauthenticated.
Request body:
{
"subject": "Hello from mailman",
"body": "<h1>Hi!</h1><p>This one is HTML.</p>",
"html": true,
"to": ["alice@example.com", "Bob <bob@example.com>"],
"cc": ["carol@example.com"],
"from": "Mailman <noreply@example.com>",
"attachments": [
{
"filename": "invoice.pdf",
"content_type": "application/pdf",
"content": "JVBERi0xLjQKJ..."
}
]
}| Field | Required | Notes |
|---|---|---|
subject |
yes | The subject line. |
body |
yes | The message body. Plain text unless html is true. |
to |
yes | One or more recipients. Bare (a@b.com) or named (Name <a@b.com>). |
cc |
no | Carbon-copy recipients. Defaults to empty. |
from |
no | Overrides the server's SMTP_FROM for this message. |
html |
no | Send body as text/html instead of text/plain. Defaults to false. |
attachments |
no | Files to attach. content is the file's bytes, base64-encoded. |
Example:
curl -X POST http://localhost:8080/send \
-H "Authorization: Bearer $MAILMAN_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"subject": "Hello",
"body": "Sent by mailman.",
"to": ["alice@example.com", "bob@example.com"]
}'Success returns 200:
{ "status": "sent", "message": "delivered to 2 recipient(s)" }Errors return { "error": "..." } with an appropriate status (400 for bad
input, 401 for a bad/missing token, 502 if the upstream SMTP send fails).
Unauthenticated liveness check; returns ok.
When the server is running, interactive Swagger documentation is available at http://localhost:8080/swagger-ui/. The raw OpenAPI document is served at http://localhost:8080/api-docs/openapi.json.
Use Swagger's Authorize button to enter MAILMAN_API_TOKEN when token
authentication is enabled.
All configuration is via environment variables. For local development, copy
them into a .env file — dev.sh generates a template on first run, and .env
is gitignored so credentials never get committed.
| Variable | Required | Default | Purpose |
|---|---|---|---|
MAILMAN_REQUIRE_API_TOKEN |
no | true |
Whether /send requires bearer-token authentication. |
MAILMAN_API_TOKEN |
conditional | — | Shared secret; required when MAILMAN_REQUIRE_API_TOKEN is true. |
SMTP_HOST |
yes | — | SMTP server hostname. |
SMTP_PORT |
no | 587 |
SMTP server port. |
SMTP_USERNAME |
no | — | SMTP username. Omit (with SMTP_PASSWORD) to send unauthenticated. |
SMTP_PASSWORD |
no | — | SMTP password. |
SMTP_FROM |
no* | — | Default From address. *Required unless every request supplies from. |
SMTP_ENCRYPTION |
no | starttls |
starttls (587), tls (465), or none (local relay only). |
PORT |
no | 8080 |
Port the HTTP server listens on. |
| Feature | Default | Purpose |
|---|---|---|
client |
yes | Builds the HTTP client SDK (reqwest + serde_json). |
server |
no | Builds the mailman server binary; pulls in axum, tokio, tracing, dotenvy, lettre. |
Run the server locally with cargo run --no-default-features --features server.
The Dockerfile and dev.sh already pass these flags.
./dev.shThis generates a .env template (if missing) and runs the server under
cargo watch, rebuilding on changes.
cargo build --release --no-default-features --features serverMailman ships as a Docker image. Build it and set the environment variables above in your platform's variables/secrets configuration:
docker build -t mailman .
docker run -p 8080:8080 --env-file .env mailmanThe client SDK is published to crates.io so
other services can cargo add mailman. See PUBLISHING.md for
the full release checklist; in short, push a v* tag and the
release workflow runs cargo publish for you
(it needs a CARGO_REGISTRY_TOKEN repository secret).