npm package: @maxrent/strapi-provider-email-multi
Description: Multi-provider email for Strapi (Brevo API + SMTP with failover)
Latest version: 5.36.0

# Strapi Provider Email Multi

Multi-provider email for Strapi v5 with support for Brevo API and SMTP, featuring automatic failover, per-provider defaults, retry logic, and end-user SMTP sending.

## Features

- **Multi-Provider Support**: Brevo API + SMTP with priority-based failover
- **Single or Multiple Providers**: Works with one provider or many
- **Per-Provider Defaults**: Each provider can have its own sender settings
- **Automatic Failover**: Switch to backup provider on failures
- **Retry Logic**: Exponential backoff with configurable retries
- **Send As User**: Send emails using end-user's own SMTP credentials
- **Health Monitoring**: Check status of all providers
- **Statistics Tracking**: Monitor sent/failed counts per provider
- **Debug Logging**: Detailed logs for troubleshooting

## Installation

This is a local provider. Located at `src/providers/strapi-provider-email-multi/`.

---

## Quick Start - Single Provider

For a simple setup with just one email provider:

```typescript
// config/plugins.ts
export default ({ env }) => ({
  email: {
    config: {
      provider: "strapi-provider-email-multi",
      providerOptions: {
        providers: [
          {
            id: "brevo-main",
            type: "brevo",
            priority: 1,
            apiKey: env("BREVO_API_KEY"),
          },
        ],
      },
      settings: {
        defaultFrom: env("EMAIL_USERNAME", "info@yourdomain.com"),
        defaultSenderName: "Your Company",
      },
    },
  },
});
```

---

## Full Configuration Example

```typescript
// config/plugins.ts
export default ({ env }) => ({
  email: {
    config: {
      // Provider name (must match folder name in src/providers/)
      provider: "strapi-provider-email-multi",

      providerOptions: {
        // ═══════════════════════════════════════════════════════════════════
        // PROVIDERS - Define multiple email providers with failover
        // ═══════════════════════════════════════════════════════════════════
        providers: [
          // ─────────────────────────────────────────────────────────────────
          // PRIMARY PROVIDER: Brevo API
          // ─────────────────────────────────────────────────────────────────
          {
            // Unique identifier for this provider (used in logs/stats)
            id: "brevo-primary",

            // Provider type: 'brevo' for Brevo API, 'smtp' for SMTP server
            type: "brevo",

            // Priority order: lower number = higher priority (tried first)
            // Example: priority 1 is tried before priority 2
            priority: 1,

            // Brevo API key from https://app.brevo.com/settings/keys/api
            apiKey: env("BREVO_API_KEY"),

            // Request timeout in milliseconds (default: 30000)
            // Increase if experiencing timeout errors
            timeout: 30000,

            // Enable/disable this provider (default: true)
            // Set to false to temporarily disable without removing config
            enabled: true,

            // ── Per-Provider Sender Defaults ──
            // These override the global settings for this provider only

            // Default sender email address
            defaultFrom: env("BREVO_SENDER_EMAIL", "hello@yourdomain.com"),

            // Default sender display name (shown in email clients)
            defaultSenderName: env("BREVO_SENDER_NAME", "Your Company"),

            // Default reply-to address (where replies are sent)
            defaultReplyTo: env("BREVO_REPLY_EMAIL", "info@yourdomain.com"),
          },

          // ─────────────────────────────────────────────────────────────────
          // BACKUP PROVIDER: SMTP Server
          // Used when primary provider fails (rate limit, timeout, etc.)
          // ─────────────────────────────────────────────────────────────────
          {
            id: "smtp-backup",
            type: "smtp",
            priority: 2, // Lower priority = used as backup

            // SMTP server hostname
            host: env("EMAIL_HOST"),

            // SMTP port (common: 25, 465 (SSL), 587 (STARTTLS))
            port: parseInt(env("EMAIL_PORT", "465"), 10),

            // Use SSL/TLS connection
            // true = port 465 (implicit SSL)
            // false = port 587 (STARTTLS) or port 25 (plain)
            secure: true,

            // SMTP authentication credentials
            auth: {
              user: env("EMAIL_USERNAME"),
              pass: env("EMAIL_PASSWORD"),
            },

            // ── Optional SMTP Settings ──

            // Use connection pooling for multiple emails (default: false)
            // Enable for high-volume sending
            pool: false,

            // Maximum concurrent connections when pooling (default: 5)
            maxConnections: 5,

            // TLS options
            tls: {
              // Reject connections with invalid SSL certificates (default: true)
              // Set to false only for self-signed certs in development
              rejectUnauthorized: true,
            },

            // ── Per-Provider Sender Defaults ──
            defaultFrom: env("EMAIL_USERNAME", "hello@yourdomain.com"),
            defaultSenderName: env("EMAIL_SENDER_NAME", "Your Company Notifications"),
            defaultReplyTo: env("EMAIL_REPLYTO_EMAIL", "support@yourdomain.com"),
          },
        ],

        // ═══════════════════════════════════════════════════════════════════
        // RETRY CONFIGURATION - How to handle transient failures
        // ═══════════════════════════════════════════════════════════════════
        retry: {
          // Maximum number of retry attempts per provider (default: 3)
          // Total attempts = maxRetries (1 initial + retries)
          maxRetries: 3,

          // Initial delay between retries in milliseconds (default: 1000)
          // First retry waits baseDelay, second waits baseDelay * 2, etc.
          baseDelay: 1000,

          // Maximum delay with exponential backoff (default: 10000)
          // Prevents retry delays from growing too large
          maxDelay: 10000,

          // Retry when rate limited (HTTP 429) (default: true)
          // Set to false if you prefer immediate failover on rate limits
          retryOnRateLimit: true,
        },

        // ═══════════════════════════════════════════════════════════════════
        // FAILOVER SETTINGS - When to switch to backup provider
        // ═══════════════════════════════════════════════════════════════════

        // Enable automatic failover to next priority provider (default: true)
        failoverEnabled: true,

        // Error codes that trigger failover to backup provider
        // Other errors will throw immediately without trying backup
        failoverOnCodes: [
          "RATE_LIMIT", // HTTP 429 - Rate limited by provider
          "TIMEOUT", // Request took too long
          "NETWORK_ERROR", // Connection refused, DNS failed, etc.
          "HTTP_5XX", // Server error (500, 502, 503, etc.)
        ],

        // Remember failed providers temporarily (default: true)
        // When true, skips providers that recently failed
        rememberFailedProvider: true,

        // Reset failed provider status after this many milliseconds (default: 300000 = 5 min)
        // After this time, failed provider will be tried again
        resetFailedAfter: 300000,

        // ═══════════════════════════════════════════════════════════════════
        // DEBUG - Enable detailed logging
        // ═══════════════════════════════════════════════════════════════════

        // Enable debug logging (default: false)
        // Shows detailed logs for sends, retries, failovers, health checks
        debug: env("NODE_ENV") === "development",
      },

      // ═══════════════════════════════════════════════════════════════════
      // GLOBAL SETTINGS - Fallback defaults if provider doesn't specify
      // ═══════════════════════════════════════════════════════════════════
      settings: {
        // Global default sender email (used if provider has no defaultFrom)
        defaultFrom: env("EMAIL_USERNAME", "info@yourdomain.com"),

        // Global default sender name
        defaultSenderName: env("EMAIL_SENDER_NAME", "Your Company"),

        // Global default reply-to address
        defaultReplyTo: env("EMAIL_REPLYTO_EMAIL", "info@yourdomain.com"),
      },
    },
  },
});
```

---

## Configuration Reference

### Provider Options

| Option                   | Type     | Default      | Description                      |
| ------------------------ | -------- | ------------ | -------------------------------- |
| `providers`              | array    | **required** | Array of provider configurations |
| `retry`                  | object   | see below    | Retry configuration              |
| `failoverEnabled`        | boolean  | `true`       | Enable automatic failover        |
| `failoverOnCodes`        | string[] | see above    | Error codes triggering failover  |
| `rememberFailedProvider` | boolean  | `true`       | Skip recently failed providers   |
| `resetFailedAfter`       | number   | `300000`     | Reset failed status after ms     |
| `debug`                  | boolean  | `false`      | Enable debug logging             |

### Brevo Provider Config

| Option              | Type      | Required | Default | Description                     |
| ------------------- | --------- | -------- | ------- | ------------------------------- |
| `id`                | string    | ✅       | -       | Unique provider identifier      |
| `type`              | `'brevo'` | ✅       | -       | Must be `'brevo'`               |
| `priority`          | number    | ✅       | -       | Priority order (lower = higher) |
| `apiKey`            | string    | ✅       | -       | Brevo API key                   |
| `timeout`           | number    |          | `30000` | Request timeout in ms           |
| `enabled`           | boolean   |          | `true`  | Enable/disable provider         |
| `defaultFrom`       | string    |          | -       | Default sender email            |
| `defaultSenderName` | string    |          | -       | Default sender name             |
| `defaultReplyTo`    | string    |          | -       | Default reply-to address        |

### SMTP Provider Config

| Option                   | Type     | Required | Default | Description                |
| ------------------------ | -------- | -------- | ------- | -------------------------- |
| `id`                     | string   | ✅       | -       | Unique provider identifier |
| `type`                   | `'smtp'` | ✅       | -       | Must be `'smtp'`           |
| `priority`               | number   | ✅       | -       | Priority order             |
| `host`                   | string   | ✅       | -       | SMTP server hostname       |
| `port`                   | number   |          | `587`   | SMTP port                  |
| `secure`                 | boolean  |          | `false` | Use SSL (true for 465)     |
| `auth.user`              | string   | ✅       | -       | SMTP username              |
| `auth.pass`              | string   | ✅       | -       | SMTP password              |
| `pool`                   | boolean  |          | `false` | Use connection pooling     |
| `maxConnections`         | number   |          | `5`     | Max pooled connections     |
| `tls.rejectUnauthorized` | boolean  |          | `true`  | Reject invalid SSL certs   |
| `defaultFrom`            | string   |          | -       | Default sender email       |
| `defaultSenderName`      | string   |          | -       | Default sender name        |
| `defaultReplyTo`         | string   |          | -       | Default reply-to address   |

### Retry Config

| Option             | Type    | Default | Description            |
| ------------------ | ------- | ------- | ---------------------- |
| `maxRetries`       | number  | `3`     | Max retry attempts     |
| `baseDelay`        | number  | `1000`  | Initial delay in ms    |
| `maxDelay`         | number  | `10000` | Max delay with backoff |
| `retryOnRateLimit` | boolean | `true`  | Retry on HTTP 429      |

---

## Usage Examples

### Basic Email

```typescript
await strapi.plugin("email").service("email").send({
  to: "recipient@example.com",
  subject: "Hello from Strapi",
  text: "Plain text content",
  html: "<h1>HTML content</h1>",
});
```

### Email with Sender Name

```typescript
await strapi
  .plugin("email")
  .service("email")
  .send({
    to: { email: "recipient@example.com", name: "John Doe" },
    from: { email: "sender@yourdomain.com", name: "My Company" },
    subject: "Welcome!",
    html: "<p>Welcome to our platform!</p>",
  });
```

### Multiple Recipients with CC/BCC

```typescript
await strapi
  .plugin("email")
  .service("email")
  .send({
    to: ["user1@example.com", { email: "user2@example.com", name: "Jane" }],
    cc: ["manager@example.com"],
    bcc: ["archive@example.com"],
    subject: "Team Update",
    html: "<p>Important announcement</p>",
  });
```

### With Attachments

```typescript
await strapi
  .plugin("email")
  .service("email")
  .send({
    to: "recipient@example.com",
    subject: "Document attached",
    html: "<p>Please find the document attached.</p>",
    attachment: [
      {
        name: "document.pdf",
        content: "base64-encoded-content-here",
        contentType: "application/pdf",
      },
    ],
  });
```

### Templated Email (with Lodash interpolation)

Use Strapi's built-in `sendTemplatedEmail` for dynamic content with variable substitution:

```typescript
// Define template with lodash-style interpolation
const emailTemplate = {
  subject: "Welcome to <%= company %>, <%= user.name %>!",
  text: `Hello <%= user.name %>,

Thank you for joining <%= company %>. Your account is ready.

Best regards,
The <%= company %> Team`,
  html: `
    <h1>Welcome, <%= user.name %>!</h1>
    <p>Thank you for joining <strong><%= company %></strong>.</p>
    <p>Your account is ready to use.</p>
    <p>Best regards,<br/>The <%= company %> Team</p>
  `,
};

// Data to populate the template
const templateData = {
  user: {
    name: "John Doe",
    email: "john@example.com",
  },
  company: "Your Company",
  confirmationUrl: "https://example.com/confirm/abc123",
};

// Send templated email
await strapi.plugin("email").service("email").sendTemplatedEmail(
  {
    // Email options
    to: templateData.user.email,
    from: "noreply@yourdomain.com",
    replyTo: "support@yourdomain.com",
  },
  emailTemplate,
  templateData,
);
```

### Templated Email with sendAs (User SMTP)

Combine templated emails with user's own SMTP server using `sendTemplatedEmail`:

```typescript
// Get user's email config from database
const emailAccount = await strapi.documents("api::email-account.email-account").findFirst({
  filters: { owner: ownerId },
});

// Define the template
const emailTemplate = {
  subject: "Booking Confirmation - <%= propertyName %>",
  text: `Dear <%= guest.name %>,\n\nYour booking at <%= propertyName %> is confirmed.\n\nCheck-in: <%= checkIn %>\nCheck-out: <%= checkOut %>\n\nBest regards,\n<%= host.name %>`,
  html: `
    <h1>Booking Confirmed!</h1>
    <p>Dear <strong><%= guest.name %></strong>,</p>
    <p>Your booking at <strong><%= propertyName %></strong> is confirmed.</p>
    <ul>
      <li>Check-in: <%= checkIn %></li>
      <li>Check-out: <%= checkOut %></li>
    </ul>
    <p>Best regards,<br/><%= host.name %></p>
  `,
};

// Template data
const templateData = {
  guest: { name: "John Doe", email: guestEmail },
  propertyName: "Beachfront Villa",
  checkIn: "2026-03-15",
  checkOut: "2026-03-20",
  host: { name: emailAccount.senderName },
};

// sendAs is passed through emailOptions to the provider
await strapi
  .plugin("email")
  .service("email")
  .sendTemplatedEmail(
    {
      to: guestEmail,
      // sendAs works with sendTemplatedEmail - passed to provider.send()
      sendAs: {
        host: emailAccount.smtpHost,
        port: emailAccount.smtpPort,
        secure: emailAccount.smtpPort === 465,
        auth: {
          user: emailAccount.email,
          pass: emailAccount.password,
        },
        from: emailAccount.email,
        senderName: emailAccount.senderName,
        replyTo: emailAccount.email,
      },
    },
    emailTemplate,
    templateData,
  );
```

---

## Send As User (End-User SMTP)

Send emails using the end-user's own SMTP credentials (e.g., property owner sending to guests from their own domain).

### Basic Usage

```typescript
await strapi
  .plugin("email")
  .service("email")
  .send({
    to: "guest@example.com",
    subject: "Message from your host",
    html: "<p>Hello from the property owner!</p>",

    // Send using owner's own email server
    sendAs: {
      // ── SMTP Server Settings ──
      host: "smtp.owner-domain.com", // Owner's SMTP hostname
      port: 465, // SMTP port (465=SSL, 587=STARTTLS)
      secure: true, // true for port 465

      // ── Authentication ──
      auth: {
        user: "owner@their-domain.com",
        pass: "their-password",
      },

      // ── Sender Details ──
      from: "owner@their-domain.com", // Sender email address
      senderName: "Property Owner", // Display name
      replyTo: "owner@their-domain.com", // Reply-to address
    },
  });
```

### sendAs Configuration

| Option       | Type          | Required | Description                   |
| ------------ | ------------- | -------- | ----------------------------- |
| `host`       | string        | ✅       | User's SMTP server hostname   |
| `port`       | number        |          | SMTP port (default: 587)      |
| `secure`     | boolean       |          | Use SSL (true=465, false=587) |
| `auth.user`  | string        | ✅       | SMTP username                 |
| `auth.pass`  | string        | ✅       | SMTP password                 |
| `from`       | string/object | ✅       | Sender email address          |
| `senderName` | string        |          | Sender display name           |
| `replyTo`    | string/object |          | Reply-to address              |

### Retry and Fallback Behavior

When using `sendAs`, the provider:

1. **Attempts user SMTP** with the configured retry settings (`maxRetries`, `baseDelay`, `maxDelay`)
2. **Retries on transient errors**: `ECONNREFUSED`, `ENOTFOUND`, `ETIMEDOUT`, `ECONNRESET`
3. **Falls back to system providers** if all retries fail
4. **Sets user's email as `replyTo`** on fallback so replies go to the owner

```
Debug log example:
[Orchestrator] 👤 Sending as user via smtp.owner.com:587
[Orchestrator] 👤 ⚠️ User SMTP attempt 1/3 failed: Connection refused
[Orchestrator] 👤 🔄 Retrying in 1000ms...
[Orchestrator] 👤 ❌ User SMTP failed after 3 attempts, falling back to system providers
[Orchestrator] 👤 ✅ Fallback successful via brevo-primary
```

### Real-World Example: Property Owner Messaging Guest

```typescript
// Get owner's email settings from database
const emailAccount = await strapi.documents("api::email-account.email-account").findFirst({
  filters: { owner: ownerId },
});

// Send using owner's SMTP
const result = await strapi
  .plugin("email")
  .service("email")
  .send({
    to: guestEmail,
    subject: `Message from ${propertyName}`,
    html: messageTemplate,

    sendAs: {
      host: emailAccount.smtpHost,
      port: emailAccount.smtpPort,
      secure: emailAccount.smtpPort === 465,
      auth: {
        user: emailAccount.email,
        pass: emailAccount.password, // Encrypted in DB, decrypted here
      },
      from: emailAccount.email,
      senderName: emailAccount.senderName || propertyName,
      replyTo: emailAccount.email,
    },
  });

if (result.failedOver) {
  // Email was sent via system provider (Brevo/SMTP)
  // Guest will see replies go to owner (via replyTo)
  console.log("Used fallback provider:", result.provider);
}
```

---

## Advanced Features

### Health Check

```typescript
const emailProvider = strapi.plugin("email").provider;
const health = await emailProvider.healthCheck();
console.log(health);
// [
//   { id: 'brevo-primary', type: 'brevo', status: 'healthy', details: {...} },
//   { id: 'smtp-backup', type: 'smtp', status: 'healthy' }
// ]
```

### Provider Statistics

```typescript
const stats = strapi.plugin("email").provider.getStats();
console.log(stats);
// {
//   providers: [
//     { id: 'brevo-primary', type: 'brevo', enabled: true, available: true, sent: 150, failed: 2 },
//     { id: 'smtp-backup', type: 'smtp', enabled: true, available: true, sent: 3, failed: 0 }
//   ],
//   failedOver: 3
// }
```

### Dynamic Provider Control

```typescript
const provider = strapi.plugin("email").provider;

// Disable a provider at runtime
provider.setProviderEnabled("brevo-primary", false);

// Re-enable it
provider.setProviderEnabled("brevo-primary", true);

// Reset failed provider status (allow retrying failed providers)
provider.resetFailedProviders();
```

### Check Send Result

```typescript
const result = await strapi.plugin("email").service("email").send({
  to: "user@example.com",
  subject: "Test",
  html: "<p>Hello</p>",
});

console.log(result);
// {
//   messageId: '<unique-id@provider.com>',
//   provider: 'brevo-primary',
//   providerType: 'brevo',
//   failedOver: false,
//   attemptedProviders: []
// }
```

---

## Error Handling

### Error Codes

| Code              | Description               | Retryable | Triggers Failover |
| ----------------- | ------------------------- | --------- | ----------------- |
| `INVALID_EMAIL`   | Invalid email format      | ❌        | ❌                |
| `MISSING_CONTENT` | No content provided       | ❌        | ❌                |
| `RATE_LIMIT`      | Rate limit exceeded (429) | ✅        | ✅                |
| `TIMEOUT`         | Request timeout           | ✅        | ✅                |
| `NETWORK_ERROR`   | Connection failed         | ✅        | ✅                |
| `HTTP_5XX`        | Server error              | ✅        | ✅                |
| `INVALID_API_KEY` | Invalid API key           | ❌        | ❌                |
| `SMTP_ERROR`      | SMTP error                | Depends   | Depends           |

### Example Error Handling

```typescript
try {
  const result = await strapi.plugin("email").service("email").send({
    to: "user@example.com",
    subject: "Test",
    html: "<p>Hello</p>",
  });
  console.log("Sent via:", result.provider);
} catch (error) {
  console.error("Error code:", error.code);
  console.error("Message:", error.message);
  console.error("Attempted providers:", error.attemptedProviders);

  if (error.code === "RATE_LIMIT") {
    // All providers rate limited - queue for later
  }
}
```

---

## Debug Logging

Enable with `debug: true` in providerOptions.

| Event              | Log Prefix          | Example                                       |
| ------------------ | ------------------- | --------------------------------------------- |
| Brevo send success | `[Brevo] ✅`        | `Email sent successfully to user@example.com` |
| Brevo retry        | `[Brevo] 🔄`        | `Retrying in 1000ms... (attempt 2/3)`         |
| SMTP send success  | `[SMTP] ✅`         | `Email sent successfully to user@example.com` |
| SMTP retry         | `[SMTP] 🔄`         | `Retrying in 1000ms...`                       |
| Orchestrator init  | `[Orchestrator] 📧` | `Initialized with 2 provider(s)`              |
| Provider failover  | `[Orchestrator] 🔄` | `Failing over to smtp-backup (smtp)`          |
| User SMTP send     | `[Orchestrator] 👤` | `Sending as user via smtp.owner.com:587`      |
| User SMTP fallback | `[Orchestrator] 👤` | `Fallback successful via brevo-primary`       |

---

## Environment Variables

```env
# Brevo API
BREVO_API_KEY=your-brevo-api-key

# SMTP Backup
EMAIL_HOST=smtp.example.com
EMAIL_PORT=465
EMAIL_USERNAME=your@email.com
EMAIL_PASSWORD=your-password

# Sender Defaults
EMAIL_SENDER_NAME=Your Company
EMAIL_REPLYTO_EMAIL=info@yourdomain.com
```

---

## License

MIT