Edit Page

Emails Plugin

RESTHeart

The emails plugin (restheart-emails module) provides SMTP email sending capabilities to any RESTHeart plugin. It wraps the ermes-mail library and exposes a simple EmailSender interface via dependency injection.

Tip
Available since version 9.6.0.

The plugin is used by restheart-accounts for transactional emails (registration verification, team invitations, password reset) and can be used by any custom plugin that needs to send emails.

Note
The restheart-emails module is bundled with RESTHeart. No separate installation is required — just enable and configure it below.

Configuration

The emails block in restheart.yml configures the SMTP connection. The plugin is disabled by default — set enabled: true to activate it.

emails:
  enabled: true
  # Display name used in the email "From" header.
  app-name: "My App"
  # From email address.
  sender-email: noreply@example.com
  # SMTP connection settings.
  smtp-hostname: email-smtp.eu-central-1.amazonaws.com
  smtp-port: 465            # 465 = SMTPS (implicit TLS); 587 = STARTTLS
  smtp-username: AKIAxxxxxxxx
  smtp-password: secret
  # Optional: explicit SSL port (defaults to 465).
  # ssl-port: 465

When enabled is false or the configuration block is absent, the plugin is inert: sendEmail() and sendEmailAsync() log a warning and return without sending. This ensures services continue to operate even when SMTP is not configured.

Usage in plugins

Inject the EmailSender provider using @Inject("emails"):

@RegisterPlugin(name = "myPlugin", description = "...")
public class MyPlugin implements JsonService {

    @Inject("emails")
    private EmailSender emails;

    @Override
    public void handle(JsonRequest req, JsonResponse res) {
        if (emails.isEnabled()) {
            // Non-blocking send — the response is not delayed by the SMTP round trip
            emails.sendEmailAsync(
                "user@example.com",
                "John",
                "Welcome!",
                "<h1>Hello John</h1><p>Welcome to our app.</p>"
            );

            // Non-blocking send with per-request SMTP overrides
            emails.sendEmailAsync(
                req,
                "user@example.com",
                "John",
                "Welcome!",
                "<h1>Hello John</h1><p>Welcome to our app.</p>"
            );
        }
    }
}

The EmailSender interface is defined in restheart-commons (org.restheart.emails.EmailSender), so your plugin only needs restheart-commons as a compile-time dependency — no dependency on restheart-emails or ermes-mail is required.

Interface

public interface EmailSender {
    // Blocking — runs the SMTP transaction on the calling thread
    void sendEmail(String to, String recipientName, String subject, String htmlBody);

    // Blocking, reads per-request SMTP overrides from attached parameters
    void sendEmail(Request<?> request, String to, String recipientName, String subject, String htmlBody);

    // Non-blocking (default method) — dispatches the send on the shared virtual threads executor
    void sendEmailAsync(String to, String recipientName, String subject, String htmlBody);

    // Non-blocking (default method), reads per-request SMTP overrides from attached parameters
    void sendEmailAsync(Request<?> request, String to, String recipientName, String subject, String htmlBody);

    boolean isEnabled();
}

Execution model

Tip
The sendEmailAsync() methods are available since version 9.7.0.

sendEmail() is blocking: the whole SMTP transaction — TCP connect, TLS handshake, AUTH, DATA, QUIT — runs on the calling thread. A remote SMTP provider typically takes a few hundred milliseconds to over a second, and up to ermes-mail’s connection (10s) and socket (60s) timeouts when it is unresponsive.

sendEmailAsync() dispatches that work on RESTHeart’s shared virtual threads executor and returns immediately.

Choose between them based on where your code runs:

Use When

sendEmailAsync()

Inside a service or a request-phase interceptor, where the SMTP round trip would otherwise be added to the response latency

sendEmail()

Inside a RESPONSE_ASYNC interceptor, a scheduled job, or an initializer — the caller is already off the request path, so the extra dispatch buys nothing and blocking gives you natural backpressure

Delivery is best effort in both cases: EmailSender returns void and never throws. SMTP failures are logged by the plugin, so a failed email never breaks the calling request.

Note
The plugin configures ermes-mail with no internal thread pool, and EmailService holds no persistent SMTP connection: every send opens and closes its own connection. This makes the sender a plain value object, which is why per-request SMTP overrides are built on the fly rather than pooled or cached.

Per-request SMTP overrides

In multi-tenant deployments, SMTP settings can be overridden per request by attaching parameters to the request via request.attachParam(). The SmtpEmailSender reads these attached parameters before falling back to the static YAML configuration.

The following attached parameters are supported:

Attached parameter Description

override-emails-sender-email

Override the "From" email address

override-emails-sender-name

Override the "From" display name

override-emails-smtp-hostname

Override the SMTP hostname

override-emails-smtp-port

Override the SMTP port

override-emails-smtp-username

Override the SMTP username

override-emails-smtp-password

Override the SMTP password

If at least one override parameter is present, the send uses the overridden values. Parameters not specified fall back to the static YAML configuration. If no override parameters are present, the static configuration is used as-is.

Example from an interceptor:

req.attachParam("override-emails-sender-email", "noreply@tenant-a.com");
req.attachParam("override-emails-smtp-hostname", "smtp.tenant-a.com");
emails.sendEmailAsync(req, "user@tenant-a.com", "User", "Subject", "<p>Body</p>");

sendEmailAsync(Request, …​) reads the attached parameters on the calling thread before dispatching, so it is safe to use even though the exchange may be completed and recycled by the time the send actually runs.