Pluggable email transport architecture with multiple provider support
The Actinium Mailer system provides hook-driven email sending with pluggable transport backends:
- Core System:
Actinium.Mail.send()API withmailer-transporthook - Default Transport: Sendmail (Unix/Linux systems)
- Plugin Transports: SMTP, Mailgun, AWS SES
- Settings Integration: Database-backed configuration with ENV fallbacks
- Hook Extensibility: Transport selection via plugin activation
┌─────────────────────────────────────────────────────┐
│ Actinium.Mail.send(message) │
└───────────────────┬─────────────────────────────────┘
│
▼
Hook.run('mailer-transport')
│
┌──────────┴──────────┐
│ │
Priority 0 Priority 1+
(Default) (Plugins)
│ │
▼ ▼
Sendmail SMTP / Mailgun / SES
│ │
└──────────┬──────────┘
▼
nodemailer.createTransport()
│
▼
Email sent via configured transport
Key Mechanisms:
mailer-transporthook returnscontext.transport- Last registered hook (highest priority) wins
- Active plugin replaces default sendmail transport
- Falls back to sendmail if plugin not configured
Source: actinium-mailer/mailer-plugin.js:40-57
Actinium.Mail.send(message: nodemailer.MailOptions): Promise<nodemailer.SentMessageInfo>Parameters:
message: Standard nodemailer message object
Returns: Promise resolving to send info or rejecting with error
Example:
await Actinium.Mail.send({
from: 'noreply@example.com',
to: 'user@example.com',
subject: 'Welcome to Actinium',
text: 'Plain text body',
html: '<h1>HTML body</h1>',
attachments: [
{
filename: 'document.pdf',
path: '/path/to/file.pdf'
}
]
});Message Options (from nodemailer):
{
from: string | { name: string, address: string },
to: string | string[],
cc?: string | string[],
bcc?: string | string[],
subject: string,
text?: string, // Plain text body
html?: string, // HTML body
attachments?: Array<{
filename: string,
path?: string, // File path
content?: Buffer | string,
contentType?: string
}>,
replyTo?: string,
inReplyTo?: string,
headers?: object,
priority?: 'high' | 'normal' | 'low'
}Source: actinium-mailer/mailer-plugin.js:21-38
Configuration:
// Via Settings API
await Actinium.Setting.set('mailer', {
sendmail: true,
path: '/usr/sbin/sendmail',
newline: 'unix'
});
// Via Environment Variables (fallback)
ENV.SENDMAIL_BIN = '/usr/sbin/sendmail';
ENV.SENDMAIL_NEWLINE_STYLE = 'unix'; // 'unix' or 'windows'Priority: 0 (runs first, overridden by active plugins)
Use Case: Development on Linux/Mac, simple production deployments
Source: actinium-mailer/smtp-plugin.js:1-137
Activation: Enable SMTP-MAILER plugin in Actinium Admin
Via Settings API:
await Actinium.Setting.set('smtp', {
host: 'smtp.gmail.com',
port: 587,
user: 'your-email@gmail.com',
pass: 'your-password'
});Via Environment Variables:
ENV.SMTP_MAILER_HOST = 'smtp.gmail.com';
ENV.SMTP_MAILER_PORT = '587';
ENV.SMTP_MAILER_USER = 'your-email@gmail.com';
ENV.SMTP_MAILER_PASS = 'your-password';Via JSON File:
// ENV variable
ENV.SMTP_MAILER_SETTINGS_FILE = '/path/to/smtp-config.json';
// smtp-config.json
{
"host": "smtp.gmail.com",
"port": 587,
"user": "your-email@gmail.com",
"pass": "your-password"
}Settings Priority: JSON file > ENV variables > Settings API defaults
Gmail SMTP:
{
host: 'smtp.gmail.com',
port: 587,
user: 'your-email@gmail.com',
pass: 'app-specific-password' // Not regular password
}Office 365 SMTP:
{
host: 'smtp.office365.com',
port: 587,
user: 'your-email@company.com',
pass: 'your-password'
}Custom SMTP Server:
{
host: 'mail.example.com',
port: 465, // SSL
user: 'noreply@example.com',
pass: 'smtp-password'
}Hook priority: 1 (overrides sendmail default)
Plugin validates configuration on startup and logs warnings if incomplete:
- Missing
smtp.hostorENV.SMTP_MAILER_HOST - Missing
smtp.portorENV.SMTP_MAILER_PORT - Missing
smtp.userorENV.SMTP_MAILER_USER - Missing
smtp.passorENV.SMTP_MAILER_PASS
Falls back to sendmail if any required field missing.
Source: actinium-mailer/mailgun-plugin.js:1-106
Activation: Enable MAILGUN plugin in Actinium Admin
Via Settings API:
await Actinium.Setting.set('mailgun', {
api_key: 'key-xxxxxxxxxxxxxxxxxx',
domain: 'mg.example.com',
proxy: 'http://proxy.example.com:8080' // Optional
});Via Environment Variables:
ENV.MAILGUN_API_KEY = 'key-xxxxxxxxxxxxxxxxxx';
ENV.MAILGUN_DOMAIN = 'mg.example.com';
ENV.MAILGUN_PROXY = 'http://proxy.example.com:8080'; // Optional// Mailgun configuration
{
api_key: 'key-1234567890abcdef',
domain: 'mail.myapp.com'
}
// Send email (same API)
await Actinium.Mail.send({
from: 'MyApp <noreply@mail.myapp.com>',
to: 'user@example.com',
subject: 'Test Email',
html: '<p>Sent via Mailgun</p>'
});Hook priority: 1 (overrides sendmail, same as SMTP)
Note: If both SMTP and Mailgun plugins active, last registered wins (undefined behavior - activate only one).
Plugin validates configuration on startup:
- Missing
mailgun.api_keyorENV.MAILGUN_API_KEY - Missing
mailgun.domainorENV.MAILGUN_DOMAIN
Falls back to sendmail if required fields missing.
Source: Third-party plugin actinium-ses-mailer
Example Usage:
From actinium-ses-mailer/plugin.js:100:
// SES plugin provides transport, then calls core API
return Actinium.Mail.send(message);Configuration: Uses AWS SDK credentials (IAM role, ENV variables, or credentials file)
Purpose: Provide nodemailer transport to Actinium.Mail.send()
Signature:
Actinium.Hook.register(
'mailer-transport',
async (context) => {
context.transport = nodemailer.createTransport(config);
return Promise.resolve();
},
priority // 0 = default, 1+ = plugins
);Context Object:
{
transport: nodemailer.Transport | undefined
}Hook Flow:
Actinium.Mail.send()runsmailer-transporthook- Each registered hook can set
context.transport - Last hook (highest priority) wins
- Core uses
context.transportor rejects if undefined
// Custom transport plugin
Actinium.Hook.register('mailer-transport', async (context) => {
const customConfig = {
host: 'custom-smtp.example.com',
port: 2525,
auth: {
user: 'custom-user',
pass: 'custom-pass'
},
tls: {
rejectUnauthorized: false
}
};
context.transport = nodemailer.createTransport(customConfig);
return Promise.resolve();
}, 10); // Higher priority than built-in pluginsconst sendPasswordReset = async (user, resetToken) => {
const resetUrl = `https://app.example.com/reset-password?token=${resetToken}`;
await Actinium.Mail.send({
from: 'MyApp <noreply@example.com>',
to: user.get('email'),
subject: 'Reset Your Password',
html: `
<h2>Password Reset Request</h2>
<p>Click the link below to reset your password:</p>
<a href="${resetUrl}">${resetUrl}</a>
<p>This link expires in 1 hour.</p>
`
});
};const sendWelcomeEmail = async (user) => {
await Actinium.Mail.send({
from: 'MyApp Team <hello@example.com>',
to: user.get('email'),
subject: 'Welcome to MyApp!',
html: `
<h1>Welcome, ${user.get('username')}!</h1>
<p>Thanks for joining MyApp. Here's how to get started:</p>
<ul>
<li><a href="https://app.example.com/tutorial">Watch Tutorial</a></li>
<li><a href="https://app.example.com/docs">Read Docs</a></li>
</ul>
`
});
};const sendReportEmail = async (user, reportPath) => {
await Actinium.Mail.send({
from: 'Reports <reports@example.com>',
to: user.get('email'),
subject: 'Your Monthly Report',
text: 'Please find your monthly report attached.',
attachments: [
{
filename: 'monthly-report.pdf',
path: reportPath
}
]
});
};Actinium.Cloud.define('user-signup', async (req) => {
const { email, username } = req.params;
// Create user
const user = new Parse.User();
user.set('email', email);
user.set('username', username);
await user.save(null, Actinium.Utils.MasterOptions());
// Send welcome email
try {
await Actinium.Mail.send({
from: 'MyApp <noreply@example.com>',
to: email,
subject: 'Welcome!',
html: `<h1>Welcome, ${username}!</h1>`
});
} catch (emailError) {
console.error('Failed to send welcome email:', emailError);
// Don't fail signup if email fails
}
return { success: true, user };
});// .env.dev
# Use default sendmail
# No mailer config needed// .env.staging
SMTP_MAILER_HOST=smtp.mailtrap.io
SMTP_MAILER_PORT=2525
SMTP_MAILER_USER=your-mailtrap-user
SMTP_MAILER_PASS=your-mailtrap-passActivate SMTP-MAILER plugin in Actinium Admin.
// .env.production
MAILGUN_API_KEY=key-prod-xxxxxxxxxxxxxx
MAILGUN_DOMAIN=mg.myapp.comActivate MAILGUN plugin in Actinium Admin.
// Configure per-tenant via cloud function
Actinium.Cloud.define('admin-configure-mailer', async (req) => {
await Actinium.CloudHasCapabilities(req, ['admin.mailer.configure']);
const { provider, config } = req.params;
if (provider === 'smtp') {
await Actinium.Setting.set('smtp', config);
} else if (provider === 'mailgun') {
await Actinium.Setting.set('mailgun', config);
}
return { success: true };
});- Use ENV variables for secrets (never commit API keys)
- Settings API for runtime config (admin-configurable)
- JSON file for complex config (multiple SMTP servers)
- Enable only ONE transport plugin (avoid conflicts)
try {
await Actinium.Mail.send(message);
} catch (error) {
console.error('Email failed:', error);
// Log to monitoring service
// Don't fail critical operations (user signup, order creation)
}// Development: Use Mailtrap or MailHog
ENV.SMTP_MAILER_HOST = 'localhost';
ENV.SMTP_MAILER_PORT = '1025'; // MailHog// Use templating library
import Handlebars from 'handlebars';
const template = Handlebars.compile(`
<h1>Hello, {{username}}!</h1>
<p>{{message}}</p>
`);
await Actinium.Mail.send({
from: 'noreply@example.com',
to: user.get('email'),
subject: 'Notification',
html: template({ username: user.get('username'), message: 'Welcome!' })
});// Prevent email spam
const sendEmail = async (user, message) => {
const cacheKey = `email-sent:${user.id}`;
const lastSent = await Actinium.Cache.get(cacheKey);
if (lastSent && Date.now() - lastSent < 60000) {
throw new Error('Email sent too recently, try again in 1 minute');
}
await Actinium.Mail.send(message);
await Actinium.Cache.set(cacheKey, Date.now(), 60000); // 1 min TTL
};// ❌ PROBLEM: Both SMTP and Mailgun active
// Last registered hook wins (undefined which one)
// ✅ SOLUTION: Activate only one transport plugin// ❌ WRONG: Plugin active but not configured
// Falls back to sendmail silently
// ✅ CORRECT: Check startup warnings, verify config// ❌ WRONG: Gmail blocks login
{
host: 'smtp.gmail.com',
port: 587,
user: 'you@gmail.com',
pass: 'your-regular-password' // FAILS
}
// ✅ CORRECT: Use App Password
// 1. Enable 2FA on Google account
// 2. Generate App Password in Google Account settings
// 3. Use App Password in SMTP config// ❌ WRONG: User signup fails if email fails
const user = await createUser(params);
await Actinium.Mail.send(welcomeEmail); // Throws error, rolls back user
// ✅ CORRECT: Don't fail critical operations
const user = await createUser(params);
try {
await Actinium.Mail.send(welcomeEmail);
} catch (error) {
console.error('Email failed but user created:', error);
}// ❌ WRONG: Invalid email causes silent failure
await Actinium.Mail.send({
to: user.get('emailllll') // Typo in field name
});
// ✅ CORRECT: Validate email address
const email = user.get('email');
if (!email || !email.includes('@')) {
throw new Error('Invalid email address');
}
await Actinium.Mail.send({ to: email, ... });// ❌ WRONG: Hardcoded sender
await Actinium.Mail.send({
from: 'noreply@gmail.com', // Rejected by SMTP server
});
// ✅ CORRECT: Use configured domain
const fromAddress = await Actinium.Setting.get('mailer.from', 'noreply@myapp.com');
await Actinium.Mail.send({
from: fromAddress,
...
});// Custom transport with debug logging
Actinium.Hook.register('mailer-transport', async (context) => {
context.transport = nodemailer.createTransport({
host: 'smtp.example.com',
port: 587,
auth: { user: 'user', pass: 'pass' },
logger: true, // Enable logging
debug: true // Include SMTP traffic
});
}, 10);// Cloud function to test mailer
Actinium.Cloud.define('test-email', async (req) => {
await Actinium.CloudHasCapabilities(req, ['admin.test']);
const { to } = req.params;
try {
const info = await Actinium.Mail.send({
from: 'test@example.com',
to: to || 'test@example.com',
subject: 'Test Email',
text: 'This is a test email from Actinium Mailer.'
});
return { success: true, info };
} catch (error) {
return { success: false, error: error.message };
}
});// Cloud function to check mailer config
Actinium.Cloud.define('mailer-status', async (req) => {
await Actinium.CloudHasCapabilities(req, ['admin.test']);
const context = await Actinium.Hook.run('mailer-transport');
const hasTransport = !!context.transport;
const smtpActive = Actinium.Plugin.isActive('SMTP-MAILER');
const mailgunActive = Actinium.Plugin.isActive('MAILGUN');
return {
hasTransport,
smtpActive,
mailgunActive,
defaultSettings: await Actinium.Setting.get('mailer'),
smtpSettings: smtpActive ? await Actinium.Setting.get('smtp') : null,
mailgunSettings: mailgunActive ? await Actinium.Setting.get('mailgun') : null
};
});| Feature | Actinium Mailer | Parse Server | Express.js |
|---|---|---|---|
| API | Actinium.Mail.send() |
Parse.Cloud.sendEmail() |
Manual nodemailer |
| Transports | Pluggable via hooks | Fixed adapter pattern | Manual configuration |
| Configuration | Settings + ENV | ENV only | ENV or code |
| Extensibility | Hook-driven | Adapter replacement | Middleware pattern |
| Default | Sendmail | None (must configure) | None |
Advantages:
- Hook-based plugin system (swap transports without code changes)
- Settings API integration (admin-configurable, per-tenant)
- Automatic fallback to sendmail
- Multiple transport plugins available
Trade-offs:
- Plugin activation required (not automatic)
- Multiple active transports undefined behavior
- Less flexible than direct nodemailer usage
Actinium Mailer provides:
- Simple API:
Actinium.Mail.send(message) - Pluggable transports: Sendmail (default), SMTP, Mailgun, AWS SES
- Hook-driven architecture:
mailer-transporthook - Settings integration: Database + ENV configuration
- Multiple providers: Activate plugins as needed
Use cases:
- Transactional emails (password reset, signup confirmation)
- Notifications (alerts, reports)
- Marketing emails (newsletters, announcements)
- System emails (error reports, monitoring alerts)
Choose Actinium Mailer when:
- Need admin-configurable email provider
- Want plugin-based transport swapping
- Require settings-driven multi-tenant config
- Prefer hook extensibility over direct nodemailer
Use direct nodemailer when:
- Need full nodemailer feature set
- Require complex transport configuration
- Building custom email service
- Don't need plugin architecture