Guides

Add a mail driver

Send Pluto emails through a provider other than Resend.

A mail driver is a small object. It sends one message through one provider. You register it on the server.

The contract

interface PlutoMailDriver {
  /** The value of NUXT_PLUTO_MAIL_DRIVER that selects this driver. */
  id: string
  /** False when the driver cannot send yet. The default is true. */
  isConfigured?: () => boolean
  send: (message: PlutoMailMessage, context: PlutoMailSendContext) => Promise<PlutoMailSendResult>
}
  • PlutoMailMessage has to, subject, html, text, and the optional from, replyTo, and idempotencyKey.
  • PlutoMailSendContext has the configured from and replyTo. They can be empty.
  • PlutoMailSendResult is { id, driver }.

Step 1: write the driver

server/mail/my-provider.ts
export const myProviderDriver: PlutoMailDriver = {
  id: 'my-provider',
  isConfigured: () => Boolean(useRuntimeConfig().myProvider?.apiKey),
  async send(message, context) {
    const response = await fetch('https://api.example.com/send', {
      method: 'POST',
      headers: { Authorization: `Bearer ${useRuntimeConfig().myProvider.apiKey}` },
      body: JSON.stringify({
        from: message.from || context.from,
        to: message.to,
        subject: message.subject,
        html: message.html,
        text: message.text
      })
    })
    if (!response.ok) {
      throw new PlutoMailError('provider_error', 'The provider failed to send the message.', { status: response.status })
    }
    return { id: null, driver: 'my-provider' }
  }
}

The example reads the key from a private runtimeConfig.myProvider.apiKey value. Declare it in the nuxt.config.ts of your layer with an empty default. Then the variable NUXT_MY_PROVIDER_API_KEY sets it at run time.

Follow these rules:

  • Throw a PlutoMailError on failure. It takes the error code, a fixed English message, and optional status and providerCode values. See Mail for the codes.
  • Never log a recipient, a subject, a link, or a key. Never put them in an error message.
  • Read a secret only on the server.
  • Keep secrets in the environment.

Step 2: register the driver

Register the driver when the server starts:

server/plugins/mail.ts
export default defineNitroPlugin(() => {
  registerMailDriver(myProviderDriver)
})

Step 3: select the driver

Set the driver id in the environment of the running server:

.env
NUXT_PLUTO_MAIL_DRIVER=my-provider

If the id is not registered, Pluto uses the none driver and logs one warning. In production, Pluto never falls back to the dev driver.

Step 4: test the driver

Tests never call a real provider. Pass a fake fetch, or stub globalThis.fetch. Add a test for each error code that your driver can throw.

Optional: a mail store

A PlutoMailStore is a source of secrets other than the environment. It has an id and a get(name) function. The only name today is 'resendApiKey', so a store cannot give the secrets of your own driver. A backend layer can register one with registerMailStore. The environment always wins over a store. Read the secrets of your own driver from the environment.

Optional: an Email page section

Explain your provider setup in Settings, Email with a mailPanels entry. The guided setup of that page covers Resend only. Register the panel component with defineAsyncComponent. See Mail.