v1.3.0These docs are for laramail v1.3.0. View changelog

Custom Providers

Register your own mail providers with Mail.extend(). Custom providers are checked before built-in drivers, so you can integrate any email service or override existing behavior.


Quick Start

import { Mail } from 'laramail';

// 1. Register the custom driver
Mail.extend('my-api', (config) => ({
  async send(options) {
    const response = await fetch('https://api.example.com/send', {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${config.apiKey}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        to: options.to,
        subject: options.subject,
        html: options.html,
      }),
    });

    const data = await response.json();
    return {
      success: response.ok,
      messageId: data.id,
    };
  },
}));

// 2. Configure to use it
Mail.configure({
  default: 'api',
  from: { address: 'noreply@example.com', name: 'My App' },
  mailers: {
    api: { driver: 'my-api', apiKey: process.env.MY_API_KEY },
  },
});

// 3. Send emails as usual
await Mail.to('user@example.com')
  .subject('Hello!')
  .html('<p>Hi there</p>')
  .send();

API

Mail.extend(driver, factory)

Mail.extend(
  driver: string,
  factory: (config: MailerConfig) => MailProvider
): void
ParameterTypeDescription
driverstringThe driver name to register (used in mailer config)
factory(config) => MailProviderFactory function that receives the mailer config and returns a provider

MailManager.extend(driver, factory)

Same API, available on the MailManager class directly:

import { MailManager } from 'laramail';

MailManager.extend('custom', (config) => myProvider);

MailManager.clearCustomProviders()

Removes all registered custom providers. Useful for test cleanup:

afterEach(() => {
  MailManager.clearCustomProviders();
});

Custom Config Properties

The factory function receives the full mailer configuration object, so you can add any custom properties:

Mail.extend('webhook', (config) => ({
  async send(options) {
    await fetch(config.webhookUrl as string, {
      method: 'POST',
      body: JSON.stringify(options),
    });
    return { success: true, messageId: `webhook-${Date.now()}` };
  },
}));

Mail.configure({
  default: 'hook',
  from: { address: 'noreply@example.com', name: 'App' },
  mailers: {
    hook: {
      driver: 'webhook',
      webhookUrl: 'https://hooks.example.com/email',
    },
  },
});

Validation

The factory must return an object with a send() method. If it doesn't, an error is thrown at send time:

// This will throw at send time:
// "Custom provider factory for "bad" must return an object with a send() method"
Mail.extend('bad', () => ({}) as any);

Overriding Built-in Drivers

Custom providers are checked before the built-in switch statement. This means you can override any built-in driver:

Mail.extend('smtp', (config) => {
  console.log('Using custom SMTP implementation');
  return new MyCustomSmtpProvider(config);
});

MailProvider Interface

Your custom provider must implement the MailProvider interface:

interface MailProvider {
  send(options: MailOptions): Promise<MailResponse>;
}

interface MailResponse {
  success: boolean;
  messageId?: string;
  accepted?: string[];
  rejected?: string[];
  response?: string;
  error?: string;
}

Testing Custom Providers

import { MailManager, Mail } from 'laramail';

afterEach(() => {
  MailManager.clearCustomProviders();
  Mail.restore();
});

it('sends via custom provider', async () => {
  const mockSend = jest.fn().mockResolvedValue({
    success: true,
    messageId: 'custom-123',
  });

  MailManager.extend('test-driver', () => ({ send: mockSend }));

  Mail.configure({
    default: 'test',
    from: { address: 'test@example.com', name: 'Test' },
    mailers: { test: { driver: 'test-driver' } },
  });

  await Mail.to('user@example.com')
    .subject('Hello')
    .html('<p>Hi</p>')
    .send();

  expect(mockSend).toHaveBeenCalledWith(
    expect.objectContaining({
      to: 'user@example.com',
      subject: 'Hello',
    })
  );
});

You can also use Mail.fake() alongside custom providers — fake mode intercepts sends before the provider is called, so your assertions work as usual.