Email Notifications (SMTP)
Overview
Accent CMS includes a built-in SMTP notification service that plugins and core features can use to send email. Typical use cases include contact form submissions, comment notifications, build reports, and health alerts.
The service is disabled by default. It is included in the Standard and Pro binaries; serving in production requires a Standard or Pro license.
Configuration
Add an smtp section to your config.yaml:
smtp: enabled: true host: smtp.example.com port: 587 username: "your-smtp-user" password: "" # prefer env var (see below) from: "noreply@example.com" tls: starttls # starttls, implicit, or none rate_limit: 10 # max emails per minute
| Setting | Default | Description |
|---|---|---|
enabled | false | Activate the SMTP service |
host | (required) | SMTP server hostname |
port | 587 | SMTP server port |
username | Authentication username | |
password | "" | Authentication password (prefer env var) |
from | (required) | Default sender address |
tls | starttls | TLS mode: starttls, implicit, or none |
rate_limit | 10 | Maximum emails per minute |
TLS Modes
| Mode | Port | Description |
|---|---|---|
starttls | 587 | Connect plain, upgrade to TLS. Default for most providers. |
implicit | 465 | Connect directly over TLS. Older style, still used by some providers. |
none | 1025 | No encryption. Only for local dev servers (Mailpit, MailHog). Do not use in production. |
Password Security
Store the SMTP password in an environment variable rather than config.yaml:
export ACCENTCMS_SMTP_PASSWORD="your-smtp-password" accent serve --config config.yaml
The environment variable takes priority over the password field in config. This keeps credentials out of version control.
How It Works
The SMTP service provides two interfaces:
For plugins: The typed host-services capability. A plugin’s WIT world imports host-services and its manifest opts in with host_services = ["mail"]; the plugin then calls send-mail directly – no HTTP, no port, no JSON envelope. See Plugin Usage below.
For core features: A Rust API (SmtpService::send()) used by built-in features like build reports and health alerts.
Both call the same service instance, so the configuration, sender address, and rate limit apply identically.
Rate Limiting
The rate_limit setting caps the number of emails per minute (default: 10). When exceeded, a plugin’s send-mail call fails with rate-limited and a core-feature send returns an error. The counter resets every 60 seconds.
This prevents accidental email floods from misconfigured plugins or form spam.
Edition Requirements
The SMTP service is compiled into the Standard and Pro binaries; serving in production requires a Standard or Pro license. When smtp.enabled is false (the default), no SMTP connections are made and no resources are consumed.
Supported SMTP Providers
Any standard SMTP server works:
| Provider | Host | Port | Notes |
|---|---|---|---|
| Gmail | smtp.gmail.com | 587 | Requires App Password |
| SendGrid | smtp.sendgrid.net | 587 | API key as password |
| Mailgun | smtp.mailgun.org | 587 | Domain-specific credentials |
| AWS SES | email-smtp.{region}.amazonaws.com | 587 | IAM SMTP credentials |
| Self-hosted (Postfix) | localhost | 25 | No auth needed on loopback |
Plugin Usage
Plugins send email through the typed host-services capability. Two declarations are needed – the WIT world import and the manifest opt-in:
[plugin]
host_services = ["mail"]
The plugin’s WIT world imports host-services (for example the host-services-route-plugin world from the plugin contract), and the code calls send-mail with a typed record:
use bindings::accent::plugin::host_services::{self, Mail}; host_services::send_mail(&Mail { to: vec!["recipient@example.com".to_string()], subject: "Contact form submission".to_string(), body_text: "Plain text version".to_string(), body_html: Some("<p>HTML version</p>".to_string()), })?;
Failures come back as a typed host-error (not-permitted, not-configured, invalid-request, rate-limited, internal). Because no bound listener is involved, send-mail also works during accent build. See the bundled contact-form example plugin for a complete implementation.
Migrating from the removed HTTP endpoint
Earlier releases also exposed POST /_internal/smtp/send, which plugins reached by adding 127.0.0.1 to allowed_hosts and POSTing a JSON body to their own server. That endpoint is gone, along with the address-guard exemption that made it reachable and the server_port value the host injected into plugin config.
A plugin still written that way loads, but its send is refused by the outbound address guard, and the server warns at load time about the loopback allowed_hosts entry. Replace the HTTP call with host_services = ["mail"] and the send-mail call above, and drop the allowed_hosts entry – the capability needs no allowlist, no port, and no JSON envelope, and unlike the endpoint it cannot be reached by other local processes.