Magic-Mail — Examples & Recipes
Welcome email on user creation
typescript
// src/extensions/users-permissions/strapi-server.ts
module.exports = (plugin) => {
const originalCallback = plugin.controllers.auth.callback;
plugin.controllers.auth.callback = async (ctx) => {
await originalCallback(ctx);
const user = ctx.state.user;
if (user && ctx.request.body.provider === 'email') {
await strapi.plugin('email').service('email').send({
to: user.email,
subject: `Welcome to ${strapi.config.get('server.name', 'our app')}, ${user.username}!`,
html: `
<h1>Welcome ${user.username}</h1>
<p>Thanks for signing up. Your account is ready to use.</p>
`,
type: 'transactional',
priority: 'high',
});
}
};
return plugin;
};Order confirmation with attachment
typescript
await strapi.plugin('email').service('email').send({
to: order.customerEmail,
subject: `Order #${order.number} confirmed`,
html: `
<h1>Thank you for your order</h1>
<p>Your order total: <strong>${order.total}</strong></p>
<p>Invoice PDF attached.</p>
`,
type: 'transactional',
priority: 'high',
attachments: [
{
filename: `invoice-${order.number}.pdf`,
path: `./uploads/invoices/${order.number}.pdf`,
},
],
});Marketing newsletter (GDPR-compliant)
typescript
for (const subscriber of subscribers) {
await strapi.plugin('email').service('email').send({
to: subscriber.email,
subject: 'Weekly digest — top stories this week',
html: renderTemplate('newsletter', { name: subscriber.name, posts }),
type: 'marketing', // triggers List-Unsubscribe header
unsubscribeUrl: `https://example.com/unsubscribe?token=${subscriber.unsubscribeToken}`,
});
}The type: 'marketing' classification makes Magic-Mail:
- Add
List-UnsubscribeandList-Unsubscribe-Postheaders automatically. - Route through your marketing-configured account (e.g. SendGrid).
- Apply any rate-limit policies you set for marketing.
Multi-tenant SaaS — each tenant has its own email
typescript
async function sendTenantEmail(tenantId: string, payload) {
await strapi.plugin('email').service('email').send({
...payload,
customField: `tenant-${tenantId}`,
});
}
// Then set up a routing rule per tenant in the admin UI:
// Rule: Tenant ACME -> condition: customField === 'tenant-acme' -> Account: acme-gmail
// Rule: Tenant WIDGETS -> condition: customField === 'tenant-widgets' -> Account: widgets-smtpPassword reset via Magic-Link (integrated)
If you install Magic-Link, it already uses Magic-Mail under the hood. You do not need custom code — just configure the template.
typescript
// config/plugins.ts
export default () => ({
'magic-mail': { enabled: true },
'magic-link': { enabled: true },
});Magic-Link picks the routing rule matching type === 'notification', so you can keep auth emails on their own dedicated account.
Conditional from-address by brand
typescript
// Multi-brand e-commerce
const brand = await strapi.db.query('api::brand.brand').findOne({ where: { id: order.brandId } });
await strapi.plugin('email').service('email').send({
to: order.email,
from: `${brand.name} <noreply@${brand.domain}>`,
subject: `Order from ${brand.name}`,
html: renderTemplate('order-confirmation', { order, brand }),
type: 'transactional',
});Retry with exponential backoff
Magic-Mail's built-in failover handles provider outages. For application-level retries (e.g., transient logic errors):
typescript
import { setTimeout as sleep } from 'node:timers/promises';
async function sendWithRetry(options, maxAttempts = 3) {
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await strapi.plugin('email').service('email').send(options);
} catch (err) {
if (err.code === 'RATE_LIMITED' && attempt < maxAttempts) {
const delayMs = Math.min(30_000, 1000 * 2 ** attempt);
strapi.log.warn(`Rate-limited, retrying in ${delayMs}ms`);
await sleep(delayMs);
continue;
}
throw err;
}
}
}Batch sending with concurrency limit
typescript
import pLimit from 'p-limit';
const limit = pLimit(5); // 5 concurrent sends
const results = await Promise.allSettled(
subscribers.map(s =>
limit(() =>
strapi.plugin('email').service('email').send({
to: s.email,
subject: 'Big announcement',
html: template,
type: 'marketing',
})
)
)
);
const failed = results.filter(r => r.status === 'rejected');
strapi.log.info(`Sent ${results.length - failed.length}/${results.length}`);Using a Magic-Mail template
typescript
await strapi.plugin('email').service('email').send({
to: user.email,
templateId: 12, // template created in admin UI
templateData: {
firstName: user.firstName,
orderNumber: order.number,
items: order.items,
total: order.total,
},
});The template has its own subject, HTML, and text — all variables interpolated via Mustache.
Preview a template from code
typescript
const html = await strapi.plugin('magic-mail').service('templates').render(12, {
firstName: 'Alice',
orderNumber: 'DEMO-123',
items: [{ name: 'Widget', qty: 2 }],
total: '$42',
});
// Save or display `html` for designer previewInspect account utilization
typescript
// Admin dashboard widget example
const accounts = await strapi.plugin('magic-mail').service('accounts').list();
return accounts.map(a => ({
name: a.name,
provider: a.provider,
utilizationPercent: (a.stats.sentToday / a.limits.perDay) * 100,
remaining: a.limits.perDay - a.stats.sentToday,
}));Emergency override — bypass routing
For a one-off critical alert where you don't care about rules:
typescript
await strapi.plugin('magic-mail').service('email-router').send({
to: 'admin@example.com',
subject: '🚨 Production alert',
html: '<p>Database connection lost at 2026-04-20 14:23 UTC</p>',
accountName: 'Emergency SMTP', // Force this account
skipRouting: true, // No rule evaluation
priority: 'high',
});Next: Troubleshooting →