@sututu/transporter
v0.1.1
Published
Transport manager for sending messages through different transports
Readme
@sututu/transporter
Transport manager for sending messages through different services/protocols.
Installation
Add the package to your app:
npm install @sututu/transporterFor SMTP email transport, also install optional peer dependency:
npm install nodemailerMailjet transport uses HTTP API and does not require additional dependencies.
Configuration
Define a top-level transporter section in sututu.config.js:
export default {
transporter: {
default: 'email',
transports: {
email: {
transport: 'email',
from: 'Sututu Notifications <[email protected]>',
templatePath: './mail/templates',
layoutPath: './mail/layouts',
smtp: {
host: 'localhost',
port: 1025,
secure: false
}
},
echo: {
transport: 'echo',
from: 'Sututu Notifications <[email protected]>',
templatePath: './mail/templates',
layoutPath: './mail/layouts'
},
mailjet: {
transport: 'mailjet',
from: 'Sututu Notifications <[email protected]>',
templatePath: './mail/templates',
layoutPath: './mail/layouts',
apiKey: process.env.MAILJET_API_KEY,
apiSecret: process.env.MAILJET_API_SECRET,
sandbox: true
}
}
}
}The from field supports mailbox format with display name, for example: John Doe <[email protected]>.
If templatePath is not set, the transporter reads templates from ./mail/templates by convention.
If layoutPath is not set, the transporter reads layouts from ./mail/layouts by convention.
For template default-email, expected files are:
default-email.html(required)default-email.txt(optional; if missing, email is sent without multipart text body)
For layout default-layout, expected files are:
default-layout.html(required)default-layout.txt(optional)
Layout files must include exactly one outlet: {{content}}.
Usage
import { createTransporter } from '@sututu/transporter'
const transporter = await createTransporter()
await transporter.send({
to: '[email protected]',
subject: 'Welcome Ana',
template: 'default-email',
layout: 'default-layout',
variables: {
name: 'Ana',
content: 'Your account is ready'
}
})
// pick transport explicitly with chainable API
await transporter.with('email').send({
to: '[email protected]',
subject: 'Welcome Ana',
template: 'default-email',
layout: 'default-layout',
variables: {
name: 'Ana',
content: 'Your account is ready'
}
})
// Echo transport prints the fully rendered email in server logs
await transporter.with('echo').send({
to: '[email protected]',
subject: 'Debug email',
template: 'default-email',
layout: 'default-layout',
variables: {
name: 'Ana',
content: 'This is a debug echo message'
}
})
// Mailjet transport sends rendered template content using Mailjet Send API
await transporter.with('mailjet').send({
to: '[email protected]',
subject: 'Welcome Ana',
template: 'default-email',
layout: 'default-layout',
variables: {
name: 'Ana',
content: 'Your account is ready'
}
})Custom transports
Built-in transport types are email, echo, and mailjet. To use a transport type that isn't one of these (SMS, Slack, WhatsApp, a different email API), add a custom transport from your own app — no fork of @sututu/transporter required.
Folder convention
src/
transports/ # each file exports { type, transport }Every .js file directly inside src/transports/ (non-recursive) is loaded. A file that default-exports { type: string, transport: Function } is registered as a new transport type; any other file in that folder is silently skipped, not an error.
Example
// src/transports/slack.js
import { BaseTransport } from '@sututu/transporter/transports/BaseTransport'
class SlackTransport extends BaseTransport {
constructor(config = {}, dependencies = {}) {
super(config)
this.fetchImpl = dependencies.fetchImpl || fetch
}
async connect() {
// No persistent connection needed for a webhook-based transport.
}
async send(message = {}) {
if (!message.text) throw new Error('Slack transport requires `text`')
const response = await this.fetchImpl(this.config.webhookUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text: message.text, channel: message.channel || this.config.defaultChannel })
})
if (!response.ok) throw new Error(`Slack transport failed: ${response.status}`)
return { delivered: true }
}
async disconnect() {}
}
export default { type: 'slack', transport: SlackTransport }export default {
transporter: {
default: 'slack',
transports: {
slack: { transport: 'slack', webhookUrl: process.env.SLACK_WEBHOOK_URL }
}
}
}await transporter.send({ text: 'Deploy finished ✅' })Extend EmailTransport instead of BaseTransport to reuse the built-in template/layout rendering for a new provider.
How discovery works
TransporterManager scans src/transports/ as the first step of connectAll(), before any configured transport connects:
- Built-in types (
email,echo,mailjet) are registered. - The app's
src/transports/folder is scanned; each valid{ type, transport }export is registered on top of the built-ins. - Configured transports are created, resolving
transport: '<type>'against the now-complete set of types.
A custom type that collides with a built-in name overrides the built-in — pick a distinct name unless that's intentional.
The folder location is fixed — src/transports is not configurable — so every app using @sututu/transporter follows the same convention.
API
createTransporter(options)createTransporterManager(config)TransporterManager#send(message, transportName?)TransporterManager#with(transportName).send(message)TransporterManager#registerAdapter(type, TransportClass)TransporterManager#registerTransport(name, transport)TransporterManager#getTransport(name?)
