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| Parameter | Type | Description |
|---|---|---|
driver | string | The driver name to register (used in mailer config) |
factory | (config) => MailProvider | Factory 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.