
Security News
New Study Identifies 53 Slopsquatting Targets Across 5 Frontier LLMs
Five frontier LLMs generated the same nonexistent package names, leaving 53 available for potential slopsquatting across PyPI and npm.
@fluojs/email
Advanced tools
Transport-agnostic email delivery core for Fluo with notifications and queue integration seams.
English 한국어
Transport-agnostic email delivery core for fluo. It provides a Nest-like module API, an injectable EmailService for standalone usage, and a first-party channel/queue adapter pair for @fluojs/notifications integration without hard-coding any runtime-specific transport.
npm install @fluojs/email
Install @fluojs/notifications and @fluojs/queue only when you want the built-in notifications channel and queue worker integration.
npm install @fluojs/notifications @fluojs/queue
Install nodemailer only when you use the explicit @fluojs/email/node subpath for Node-only SMTP delivery.
npm install @fluojs/email nodemailer
Node-specific SMTP delivery is available from the explicit @fluojs/email/node subpath. Queue-backed notifications integration is available from @fluojs/email/queue, and @fluojs/queue is declared as an optional peer for that subpath. The root @fluojs/email entrypoint stays transport-agnostic so Bun, Deno, Cloudflare, and custom HTTP transports do not inherit Node-only or queue-specific behavior.
@fluojs/notifications.@fluojs/queue instead of blocking request paths.import { Module } from '@fluojs/core';
import { EmailModule, type EmailTransport } from '@fluojs/email';
class ExampleTransport implements EmailTransport {
async send(message) {
return {
accepted: message.to.map((entry) => entry.address),
messageId: crypto.randomUUID(),
pending: [],
rejected: [],
};
}
}
@Module({
imports: [
EmailModule.forRoot({
defaultFrom: 'noreply@example.com',
transport: {
kind: 'example-http-transport',
create: async () => new ExampleTransport(),
},
}),
],
})
export class AppModule {}
import { Inject } from '@fluojs/core';
import { EmailService } from '@fluojs/email';
@Inject(EmailService)
export class WelcomeService {
constructor(private readonly email: EmailService) {}
async sendWelcome(address: string) {
await this.email.send({
to: [address],
subject: 'Welcome to fluo',
text: 'Your account is ready.',
});
}
}
The root @fluojs/email surface is intentionally module-first. Register email delivery through EmailModule.forRoot(...) or EmailModule.forRootAsync(...).
EmailModule.forRoot(...) and EmailModule.forRootAsync(...) return a global module by default. After one import, the exported EmailService, EmailChannel, EMAIL, and EMAIL_CHANNEL providers are visible to the application module graph. Pass global: false only when email providers should stay visible to modules that explicitly import the returned module.
Async registration intentionally uses fluo's explicit factory shape:
EmailModule.forRootAsync({
global: false,
inject: [ConfigService],
useFactory: (config) => ({
defaultFrom: config.mail.from,
transport: {
kind: config.mail.transportKind,
create: () => config.mail.transport,
ownsResources: false,
},
}),
});
global belongs on the top-level forRootAsync(...) options object, not in the factory result. The supported async registration shape is inject plus useFactory; NestJS dynamic-module forms such as imports, useClass, and useExisting are not part of the @fluojs/email contract. Register dependencies in the surrounding application module graph first, then list the tokens the factory needs in inject.
@fluojs/email/nodeUse the dedicated Node subpath when you want first-party Nodemailer/SMTP delivery without weakening the runtime-portable root package contract.
import { Module } from '@fluojs/core';
import { EmailModule } from '@fluojs/email';
import { createNodemailerEmailTransportFactory } from '@fluojs/email/node';
@Module({
imports: [
EmailModule.forRoot({
defaultFrom: 'noreply@example.com',
transport: createNodemailerEmailTransportFactory({
smtp: {
auth: {
pass: 'smtp-password',
user: 'smtp-user',
},
host: 'smtp.example.com',
port: 587,
secure: false,
},
}),
verifyOnModuleInit: true,
}),
],
})
export class AppModule {}
Behavioral contract notes:
createNodemailerEmailTransportFactory(...) is Node-only and is exported exclusively from @fluojs/email/node.EmailService can verify it on bootstrap and close it during shutdown.createNodemailerEmailTransport(...) wraps an existing Nodemailer transporter without transferring resource ownership.process.env directly.EmailServiceUse EmailService when your application wants direct email delivery without going through the notifications foundation.
EmailModule.forRootAsync({
inject: [ConfigService],
useFactory: (config) => ({
defaultFrom: config.mail.from,
transport: {
kind: config.mail.transportKind,
create: () => config.mail.transport,
ownsResources: false,
},
}),
});
Behavioral contract notes:
EmailService.send(...) resolves defaultFrom and defaultReplyTo before delivery.EmailService.send(...) rejects blank to recipients before handoff so transports never receive an empty delivery target.EmailService.send(...) and EmailService.sendNotification(...) honor an already-aborted AbortSignal before template rendering or transport handoff.EmailService.send(...) preserves accepted, pending, and rejected recipients separately so partial provider failures stay caller-visible.EmailService.sendMany(...) is fail-fast by default; pass continueOnError: true to collect failures in a batch result.EmailService.createPlatformStatusSnapshot() exposes lifecycle, readiness, health, and transport ownership details for diagnostics.verifyOnModuleInit: true, delivery waits until bootstrap verification has completed successfully before transport handoff.forRootAsync(...) option factories are not memoized permanently; the next provider resolution can retry configuration lookup.EmailService.send(...) and EmailService.sendNotification(...) fail with EmailLifecycleError instead of reusing or lazily creating transports; any in-flight factory-owned transport creation is awaited, active transport verify() / send() calls are drained, and then owned transports are closed by shutdown. Concurrent and repeated shutdown calls share the same completion promise, so an owned transport is closed at most once.verify() and close() provider errors are preserved as the cause of lifecycle failures for diagnostics.EmailModule.forRoot(...) and EmailModule.forRootAsync(...) are global by default. Use global: false to opt into module-local visibility.EmailModule.forRootAsync(...) supports inject plus useFactory only; NestJS imports, useClass, and useExisting registration shapes must be resolved at the application module boundary before calling the factory.process.env directly. All configuration must enter through explicit options or DI.@fluojs/notificationsInject EMAIL_CHANNEL into NotificationsModule.forRootAsync(...) so the email package remains the only place that understands email-specific payload fields and template rendering.
import { Module } from '@fluojs/core';
import { EmailModule, EMAIL_CHANNEL } from '@fluojs/email';
import { NotificationsModule } from '@fluojs/notifications';
@Module({
imports: [
EmailModule.forRoot({
defaultFrom: 'noreply@example.com',
transport: {
kind: 'transactional-http',
create: () => transactionalTransport,
ownsResources: false,
},
}),
NotificationsModule.forRootAsync({
inject: [EMAIL_CHANNEL],
useFactory: (channel) => ({
channels: [channel],
}),
}),
],
})
export class AppModule {}
Supported notification payload fields:
to, cc, bcc, from, replyTotext, html, attachments, headers, metadatatemplateData when a renderer is configured on the moduleBehavioral contract notes:
EmailChannel treats zero accepted recipients (accepted.length === 0) or any pending/rejected recipients as a failed notification dispatch instead of reporting the delivery as successful.EmailService.sendNotification(...) merges rendered template output with payload and notification metadata; payload fields override notification fallbacks.payload, metadata, locale, subject, and template; payload text, html, and notification subject override rendered fallbacks.Pass an EmailTemplateRenderer together with the required transport when registering EmailModule. This complete Node.js example uses the first-party SMTP factory; on another runtime, keep the renderer and replace only transport with an application-owned EmailTransport or EmailTransportFactory.
import { Module } from '@fluojs/core';
import { EmailModule, type EmailTemplateRenderer } from '@fluojs/email';
import { createNodemailerEmailTransportFactory } from '@fluojs/email/node';
const renderer: EmailTemplateRenderer = {
render({ payload, template }) {
const name = payload.templateData?.name;
const displayName = typeof name === 'string' ? name : 'customer';
return {
html: '<h1>Welcome</h1>',
subject: template === 'welcome' ? `Welcome, ${displayName}` : template,
text: `Hello, ${displayName}`,
};
},
};
@Module({
imports: [
EmailModule.forRoot({
defaultFrom: 'noreply@example.com',
renderer,
transport: createNodemailerEmailTransportFactory({
smtp: {
auth: {
pass: 'smtp-password',
user: 'smtp-user',
},
host: 'smtp.example.com',
port: 587,
secure: false,
},
}),
verifyOnModuleInit: true,
}),
],
})
export class AppModule {}
Call EmailService.sendNotification(...) with a template key and put renderer-specific values under payload.templateData. The same request shape works through NotificationsService.dispatch(...) after registering EMAIL_CHANNEL as shown above.
import { Inject } from '@fluojs/core';
import { EmailService } from '@fluojs/email';
@Inject(EmailService)
export class WelcomeEmailService {
constructor(private readonly email: EmailService) {}
async sendRenderedWelcome(address: string, name: string) {
await this.email.sendNotification({
channel: 'email',
recipients: [address],
template: 'welcome',
payload: {
templateData: { name },
},
});
}
async sendWelcomeWithOverrides(address: string, name: string) {
await this.email.sendNotification({
channel: 'email',
recipients: [address],
subject: 'Your account is ready',
template: 'welcome',
payload: {
html: '<p>Your account is ready.</p>',
templateData: { name },
text: 'Use this exact welcome message.',
},
});
}
}
The renderer runs only when both template and renderer are present. Its subject, text, and html are fallbacks: an explicit notification subject overrides the rendered subject, while explicit payload.text and payload.html override rendered bodies. payload.to also overrides the notification recipients fallback. templateData remains inside the opaque payload and is available to the renderer as payload.templateData; the email package does not interpret its keys.
When @fluojs/notifications should offload bulk email delivery to the background, import QueueModule, inject QueueLifecycleService, call createEmailNotificationsQueueAdapter(queue), and register EmailNotificationsQueueWorker as an application provider. The root EmailModule does not register the worker automatically, so applications that never import @fluojs/email/queue do not need @fluojs/queue at runtime.
import { Module } from '@fluojs/core';
import {
EmailModule,
EMAIL_CHANNEL,
} from '@fluojs/email';
import { createEmailNotificationsQueueAdapter, EmailNotificationsQueueWorker } from '@fluojs/email/queue';
import { NotificationsModule } from '@fluojs/notifications';
import { QueueLifecycleService, QueueModule } from '@fluojs/queue';
@Module({
imports: [
QueueModule.forRoot(),
EmailModule.forRoot({
defaultFrom: 'noreply@example.com',
transport: {
kind: 'bulk-email-api',
create: () => bulkEmailTransport,
ownsResources: false,
},
}),
NotificationsModule.forRootAsync({
inject: [EMAIL_CHANNEL, QueueLifecycleService],
useFactory: (channel, queue) => ({
channels: [channel],
queue: {
adapter: createEmailNotificationsQueueAdapter(queue),
bulkThreshold: 25,
},
}),
}),
],
providers: [EmailNotificationsQueueWorker],
})
export class AppModule {}
The built-in queue worker contract uses these defaults:
attempts: 3backoff: { type: 'exponential', delayMs: 1000 }concurrency: 5rateLimiter: { max: 50, duration: 1000 }jobName: 'fluo.email.notification'These defaults are exported from @fluojs/email/queue as DEFAULT_EMAIL_QUEUE_WORKER_OPTIONS so callers can document or mirror them when they build custom queue adapters/workers.
Behavioral contract notes:
@fluojs/email entrypoint and EmailModule do not import @fluojs/queue, register EmailNotificationsQueueWorker, or require queue peer installation.EmailNotificationsQueueWorker is exported from @fluojs/email/queue and must be registered by applications that enable queue-backed delivery.EmailChannel.channel. A mismatch fails with EmailMessageValidationError, so non-email work cannot reach the email transport.EmailChannel delivery semantics, so a queued job fails when the underlying transport reports zero accepted recipients or any pending/rejected recipients. This lets @fluojs/queue retry and dead-letter incomplete deliveries instead of acknowledging them as successful jobs.The email package intentionally does not:
process.envQueueModule or register queue workers automatically@fluojs/notificationsThese limitations are part of the package contract so transport selection, template strategy, and queue rollout stay explicit at the application boundary.
EmailModule.forRoot(options) / EmailModule.forRootAsync(options)EmailServiceEmailService.sendMany(messages, options)EmailService.sendNotification(notification, options)EmailService.createPlatformStatusSnapshot()EmailChannelEMAILEMAIL_CHANNELEmail: Application-facing sending facade exposed by the EMAIL compatibility token, not an address value; it provides send(...), sendMany(...), and sendNotification(...) methods backed by EmailService.EmailAddress / EmailAddressLike: Structured or shorthand recipient values accepted by EmailService before normalization.EmailAttachment: File attachment payload accepted on EmailMessage.attachments and forwarded to the configured transport with filename, content, and optional contentType fields.EmailModuleOptions / EmailAsyncModuleOptions: Synchronous and async module registration contracts, including sender defaults, renderer, lifecycle verification, transport factory wiring, top-level global visibility control, and the async inject + useFactory shape.EmailMessageEmailNotificationDispatchRequest / EmailNotificationPayload: Notification channel payload contracts consumed by EmailChannel.EmailSendOptions / EmailSendManyOptions: Per-send controls such as abort signals and batch failure collection.EmailSendResult / EmailSendBatchResult / EmailSendFailure: Direct and batch delivery result contracts that preserve accepted, pending, rejected, and failed messages.EmailTransportReceipt: Transport-level provider receipt preserved by EmailSendResult.EmailTransportEmailTransportContextEmailTransportFactoryEmailTemplateRenderInputEmailTemplateRendererEmailTemplateRenderResultNormalizedEmailAddressList / NormalizedEmailMessage: Internal-normalized message shapes exposed for typed integrations and tests.@fluojs/email/queue: createEmailNotificationsQueueAdapter(queue), EmailNotificationQueueJob, EmailNotificationsQueueWorker, DEFAULT_EMAIL_QUEUE_WORKER_OPTIONS, EmailQueueWorkerOptionscreateEmailPlatformStatusSnapshot(...)EmailLifecycleStateEmailPlatformStatusSnapshotEmailStatusAdapterInputEmailConfigurationErrorEmailLifecycleError: thrown by lifecycle-gated delivery, transport initialization or verification, and owned-resource shutdown failures. Catch this error when sends can race with application teardown.EmailMessageValidationErrorcreateNodemailerEmailTransport(...)createNodemailerEmailTransportFactory(...)NodemailerEmailTransportNodemailerTransporterNodemailerEmailTransportOptionsNodemailerEmailTransportFactoryOptions| Runtime | Subpath | Exports |
|---|---|---|
| Node.js | @fluojs/email/node | createNodemailerEmailTransport(...), createNodemailerEmailTransportFactory(...), NodemailerEmailTransport, NodemailerTransporter, NodemailerEmailTransportOptions, NodemailerEmailTransportFactoryOptions |
| Concern | Subpath | Exports |
|---|---|---|
| Queue-backed notifications integration | @fluojs/email/queue | createEmailNotificationsQueueAdapter(queue), EmailNotificationQueueJob, EmailNotificationsQueueWorker, DEFAULT_EMAIL_QUEUE_WORKER_OPTIONS, EmailQueueWorkerOptions |
@fluojs/notifications: Shared orchestration layer that consumes EMAIL_CHANNEL.@fluojs/queue: Recommended when bulk email delivery should run in the background.@fluojs/config: Recommended for resolving transport credentials and sender defaults without direct environment access.nodemailer: The Node-only SMTP implementation consumed by @fluojs/email/node.packages/email/src/module.test.ts: Module registration, option normalization, async wiring, lifecycle, and queue-backed notifications examples.packages/email/src/public-surface.test.ts: Public export and TypeScript contract verification.packages/email/src/node/node.test.ts: Node-only Nodemailer adapter mapping and lifecycle examples.packages/email/src/status.test.ts: Health/readiness contract examples.FAQs
Transport-agnostic email delivery core for Fluo with notifications and queue integration seams.
The npm package @fluojs/email receives a total of 24 weekly downloads. As such, @fluojs/email popularity was classified as not popular.
We found that @fluojs/email demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
Five frontier LLMs generated the same nonexistent package names, leaving 53 available for potential slopsquatting across PyPI and npm.

Security News
The White House’s Gold Eagle Initiative aims to coordinate AI-discovered vulnerabilities, validate findings, and accelerate patching across critical software.

Security News
A Shai-Hulud infection exposed Suno's source code, which shows the AI music startup stream-ripped tracks to train its models.