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
SettingDefaultDescription
enabledfalseActivate the SMTP service
host(required)SMTP server hostname
port587SMTP server port
usernameAuthentication username
password""Authentication password (prefer env var)
from(required)Default sender address
tlsstarttlsTLS mode: starttls, implicit, or none
rate_limit10Maximum emails per minute

TLS Modes

ModePortDescription
starttls587Connect plain, upgrade to TLS. Default for most providers.
implicit465Connect directly over TLS. Older style, still used by some providers.
none1025No 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:

ProviderHostPortNotes
Gmailsmtp.gmail.com587Requires App Password
SendGridsmtp.sendgrid.net587API key as password
Mailgunsmtp.mailgun.org587Domain-specific credentials
AWS SESemail-smtp.{region}.amazonaws.com587IAM SMTP credentials
Self-hosted (Postfix)localhost25No 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.