Mail Component
MailComponent wires a pluggable email transport (Nodemailer, Mailgun, or your own) into MailService, adding template rendering, batch sending, and an independent queue executor for verification-code/token flows.
In one example
Bind MailKeys.MAIL_OPTIONS, register MailComponent, then inject IMailService anywhere to send:
import { BaseApplication, ValueOrPromise } from '@venizia/ignis';
import { MailComponent, MailKeys, MailProviders } from '@venizia/ignis/mail';
export class Application extends BaseApplication {
preConfigure(): ValueOrPromise<void> {
// MAIL_OPTIONS is the only binding MailComponent requires
this.bind({ key: MailKeys.MAIL_OPTIONS }).toValue({
provider: MailProviders.NODEMAILER,
from: 'noreply@example.com',
config: {
host: 'smtp.gmail.com',
port: 465,
secure: true,
auth: { user: process.env.APP_ENV_MAIL_USER, pass: process.env.APP_ENV_MAIL_PASS },
},
});
this.component(MailComponent);
}
}import { BaseService, inject } from '@venizia/ignis';
import { MailKeys, type IMailService } from '@venizia/ignis/mail';
export class UserService extends BaseService {
constructor(@inject({ key: MailKeys.MAIL_SERVICE }) private mailService: IMailService) {
super({ scope: UserService.name });
}
async sendWelcomeEmail(email: string) {
return this.mailService.send({ to: email, subject: 'Welcome!', html: '<h1>Welcome!</h1>' });
}
}MailKeys.MAIL_QUEUE_EXECUTOR_CONFIG is optional -- omit it and MailComponent binds a direct executor (no queue) by default.
How it works
- One required binding.
MailComponent.binding()throwsMail options not configuredifMailKeys.MAIL_OPTIONSis not bound before registration. Every other binding -- queue executor config, verification generators -- is optional with a working default. - Transport is a discriminated union.
TMailOptions.providerselectsNodemailerTransportHelper,MailgunTransportHelper, or acustomobject you supply that implementsIMailTransport(send()+verify()).MailTransportProvideris the factory that switches on it and throws for an unsupported provider string. MailServiceis the one sending entry point.send(),sendBatch(),sendTemplate(), andverify()all validate first, then call the transport directly and synchronously, then normalize failures intoMailErrorCodes. A validation error (400) passes through unchanged; only a throwing transport gets wrapped asSEND_FAILED(500) -- the built-in Nodemailer and Mailgun transports never throw, they return{ success: false, error }instead.- The queue executor is a separate subsystem, not a mail queue.
IMailQueueExecutor(direct/internal-queue/bullmq) only exposesenqueueVerificationEmail()andsetProcessor()-- it never touchesMailService. You must callsetProcessor()with your own function (typically one that callsmailService.send()internally) beforeenqueueVerificationEmail()does anything. - Templates are a simple substitution engine.
TemplateEngineServicestores templates in an in-memoryMapand replaces{{variable}}placeholders (dot-notation for nested values). A missing value is logged and left as the literal placeholder text, not blanked out. - Startup logging never leaks credentials.
MailComponentlogs onlymailOptions.providerandqueueExecutorConfig.type-- never the SMTP password, OAuth2 secret, API key, or Redis password nested inside them.
Common tasks
Send an email
Inject IMailService via MailKeys.MAIL_SERVICE and call send().
const result = await this.mailService.send({
to: 'user@example.com',
subject: 'Welcome!',
html: '<h1>Welcome!</h1>',
text: 'Welcome!',
});Send a batch of emails
sendBatch() runs each message through send() with bounded concurrency (default 5).
const results = await this.mailService.sendBatch(messages, { concurrency: 5 });Send a registered template
Register a template on IMailTemplateEngine, then send it through IMailService.
this.templateEngine.registerTemplate({
name: 'welcome-email',
content: '<h1>Welcome {{userName}}!</h1>',
options: { subject: 'Welcome to {{appName}}' },
});
await this.mailService.sendTemplate({
templateName: 'welcome-email',
data: { userName: 'Jane', appName: 'My App' },
recipients: 'user@example.com',
});Switch to Mailgun
config must carry username, key, and domain -- the transport validates them eagerly, on construction.
{
provider: MailProviders.MAILGUN,
from: 'noreply@example.com',
config: { username: 'api', key: process.env.MAILGUN_API_KEY, domain: 'mg.example.com' },
}Queue verification emails
Get the queue executor instance, register a processor, then enqueue.
const executor = this.application.get<IMailQueueExecutor>({
key: MailKeys.MAIL_QUEUE_EXECUTOR_INSTANCE,
});
executor.setProcessor(async email => {
await this.mailService.send({ to: email, subject: 'Verify', html: '...' });
return { success: true, message: 'Sent', expiresInMinutes: 10 };
});
await executor.enqueueVerificationEmail('user@example.com');Generate a verification code and token
MAIL_VERIFICATION_DATA_GENERATOR composes a numeric code and a base64url token in one call.
const data = this.verificationGenerator.generateVerificationData({
codeLength: 6,
tokenBytes: 32,
codeExpiryMinutes: 10,
tokenExpiryHours: 24,
});See also
- Usage & Examples -- sending, templates, queue executors, and verification generators
- API Reference -- architecture, binding keys, interfaces, and internals
- Error Reference -- error codes and troubleshooting
- Components Overview -- component system basics
- Queue Helper -- the in-memory/BullMQ primitives the queue executors are built on
- Redis Helper --
RedisSingleHelper, required by the BullMQ queue executor
Files:
packages/core/src/components/mail/component.ts--MailComponentpackages/core/src/components/mail/services/mail.service.ts--MailServicepackages/core/src/components/mail/services/template.service.ts--TemplateEngineServicepackages/core/src/components/mail/services/generator.service.ts-- verification generatorspackages/core/src/components/mail/common/keys.ts--MailKeyspackages/core/src/components/mail/common/types.ts--TMailOptions,IMailMessage, and every mail interface