Email System¶
Merchello's email system handles all transactional emails -- order confirmations, shipment notifications, abandoned cart recovery, password resets, and more. It is built around three ideas: topic-based routing, MJML templates, and a reliable delivery queue with retry support.
How It Works¶
The email system connects internal Merchello notifications to email templates through a topic registry. When something happens in the system (an order is created, a shipment ships, a cart is abandoned), a notification fires. The EmailNotificationHandler picks it up, finds all email configurations for that topic, and queues a delivery for each one.
Notification fires -> EmailNotificationHandler (priority 2100) -> Find configs for topic -> Queue deliveries
This means you can have multiple email configurations for the same topic. For example, you might send an order confirmation to the customer and a different notification to your warehouse team -- both triggered by the same order.created topic.
CLAUDE.md invariant: The handler swallows (catch/log) all dispatch errors and never rethrows. An email delivery failure must never break the business operation that triggered it. See EmailNotificationHandler.cs:ProcessEmailsAsync.
Email Configurations¶
An email configuration maps a topic to a template with recipient rules. You create these in the backoffice under Settings > Email.
Each configuration has:
| Field | Description |
|---|---|
Name |
Display name (e.g., "Order Confirmation to Customer") |
Topic |
The notification topic that triggers this email |
TemplatePath |
Relative path to the Razor/MJML template |
ToExpression |
Recipient address, supports {{token}} syntax |
CcExpression |
CC recipients (optional) |
BccExpression |
BCC recipients (optional) |
FromExpression |
Sender address (falls back to default settings) |
SubjectExpression |
Email subject, supports {{token}} syntax |
AttachmentAliases |
Which attachments to include (e.g., invoice-saved-pdf) |
Enabled |
Toggle on/off without deleting |
Token Replacement¶
Subject and recipient fields support token syntax. Tokens come from the notification that triggered the email:
Available tokens vary by topic and are listed in the backoffice when you edit an email configuration.
Email Topics¶
Topics are registered in IEmailTopicRegistry and defined as constants in Constants.cs:EmailTopics. Each topic maps to a notification type and lists its available tokens. Here are the built-in topics by category:
Orders¶
order.created-- New order placedorder.status_changed-- Order status updatedorder.cancelled-- Order cancelled (dispatched fromInvoiceCancelledNotification)
Invoices¶
invoice.created-- Invoice generatedinvoice.paid-- Invoice fully paid (dispatched alongsidepayment.created)invoice.refunded-- Refund processed (dispatched alongsidepayment.refunded)invoice.deleted-- Invoice deletedinvoice.reminder-- Payment reminder (fromInvoiceReminderJob)invoice.overdue-- Invoice overdue (fromInvoiceReminderJob)
Payments¶
payment.created-- Payment receivedpayment.refunded-- Payment refunded
Customers¶
customer.created-- New customer registeredcustomer.updated-- Customer profile changedcustomer.password_reset-- Password reset requested
Shipments¶
shipment.created-- Shipment createdshipment.preparing-- Shipment being prepared (bridged fromShipmentCreatedNotification)shipment.updated-- Shipment details changedshipment.shipped-- Shipment dispatched with trackingshipment.delivered-- Shipment deliveredshipment.cancelled-- Shipment cancelled
Checkout Recovery¶
checkout.abandoned-- Cart abandoned (general)checkout.abandoned.first-- First recovery email duecheckout.abandoned.reminder-- Reminder email duecheckout.abandoned.final-- Final recovery email duecheckout.recovered-- Abandoned cart recoveredcheckout.converted-- Recovery converted to order
Inventory¶
inventory.low_stock-- Stock below threshold
Digital Products¶
digital.delivered-- Download links ready
Fulfilment¶
fulfilment.supplier_order-- Order sent to supplier (Supplier Direct)
Topic naming convention: Topic keys use dots as category separators and underscores within a single word (
order.status_changed,inventory.low_stock). Avoid inventing your own format; always use the constants inConstants.EmailTopics.
MJML Templates¶
Merchello uses MJML for email templates. MJML is a markup language that compiles to responsive HTML email. You write simple markup and it generates the complex table-based HTML that email clients need.
Templates are Razor (.cshtml) files whose rendered output is compiled to HTML by the IMjmlCompiler (MjmlCompiler.cs) and support theme settings (colors, fonts, logo) from EmailThemeSettings (EmailThemeSettings.cs). The provided HTML helpers live in Merchello.Email.Extensions so you can mix helpers (@Html.Mjml().EmailStart(...)) with raw MJML elements.
Sample templates ship in the example site at src/Merchello.Site/Views/Emails/ (OrderConfirmation.cshtml, AbandonedCartFirst.cshtml, AbandonedCartReminder.cshtml, AbandonedCartFinal.cshtml, DigitalProductDelivered.cshtml, PasswordReset.cshtml, SupplierOrder.cshtml). Copy one as a starting point — they already demonstrate the strongly-typed EmailModel<TNotification> pattern.
Template Locations¶
Templates are resolved from the view locations configured in EmailSettings.TemplateViewLocations (in order):
/App_Plugins/Merchello/Views/Emails/{template}.cshtml/Views/Emails/{template}.cshtml
You can customize these paths in settings:
{
"Merchello": {
"Email": {
"TemplateViewLocations": [
"/Views/Emails/{0}.cshtml",
"/App_Plugins/Merchello/Views/Emails/{0}.cshtml"
]
}
}
}
Attachments¶
Email configurations can include attachments by specifying attachment aliases. Built-in attachment types include:
invoice-saved-pdf-- PDF of the invoiceorder-invoice-pdf-- Generated invoice PDF for the orderorder-line-items-csv-- CSV of order line items
Attachments have size limits:
- Single attachment: 10 MB (configurable via MaxAttachmentSizeBytes)
- Total attachments per email: 25 MB (configurable via MaxTotalAttachmentSizeBytes)
The EmailAttachmentCleanupJob removes orphaned attachment files after 72 hours (configurable).
Delivery Queue and Retries¶
Emails are not sent immediately. They are queued as OutboundDelivery records (OutboundDelivery.cs) and processed by the OutboundDeliveryJob background service (OutboundDeliveryJob.cs). Email and webhook deliveries share the same queue and job; the DeliveryType column distinguishes them (Webhook = 0, Email = 1).
This means:
- Emails do not block the main request
- Failed emails are automatically retried
- You get a full delivery log in the backoffice
- Payloads render lazily at send time, so template tokens always reflect the latest entity state
Retry Policy¶
{
"Merchello": {
"Email": {
"MaxRetries": 3,
"RetryDelaysSeconds": [60, 300, 900],
"DeliveryRetentionDays": 30
}
}
}
Failed deliveries retry at 1 minute, 5 minutes, and 15 minutes. After all attempts are exhausted, the delivery is marked as Abandoned.
Delivery Statuses¶
| Status | Description |
|---|---|
Pending |
Queued, waiting to be sent |
Sending |
Currently being sent |
Succeeded |
Delivered successfully |
Failed |
Last attempt failed, will retry |
Retrying |
Waiting for next retry attempt |
Abandoned |
All retries exhausted |
Configuration¶
Full email settings in appsettings.json:
{
"Merchello": {
"Email": {
"DefaultFromAddress": "store@example.com",
"DefaultFromName": "My Store",
"MaxRetries": 3,
"RetryDelaysSeconds": [60, 300, 900],
"DeliveryRetentionDays": 30,
"MaxAttachmentSizeBytes": 10485760,
"MaxTotalAttachmentSizeBytes": 26214400,
"AttachmentStoragePath": "App_Data/Email_Attachments",
"AttachmentRetentionHours": 72,
"Theme": {
"PrimaryColor": "#2563eb",
"LogoUrl": "/img/logo.png"
}
}
}
}
Tip: If
DefaultFromAddressis not set, Merchello falls back to the SMTP sender configured in Umbraco's settings.
Backoffice API¶
Routes are relative to the Umbraco management API prefix. Source: EmailConfigurationApiController.cs, EmailMetadataApiController.cs.
| Endpoint | Method | Description |
|---|---|---|
/api/v1/emails |
GET | List email configurations (paginated, filterable) |
/api/v1/emails/{id} |
GET | Get configuration detail |
/api/v1/emails |
POST | Create configuration |
/api/v1/emails/{id} |
PUT | Update configuration |
/api/v1/emails/{id} |
DELETE | Delete configuration |
/api/v1/emails/{id}/toggle |
POST | Enable/disable |
/api/v1/emails/{id}/test |
POST | Send a test email |
/api/v1/emails/{id}/preview |
GET | Preview rendered email |
/api/v1/emails/topics |
GET | List available topics |
/api/v1/emails/topics/categories |
GET | Topics grouped by category |
/api/v1/emails/topics/{topic}/tokens |
GET | List tokens for a topic |
/api/v1/emails/topics/{topic}/attachments |
GET | Attachments compatible with a topic |
/api/v1/emails/templates |
GET | Discover available .cshtml templates |
/api/v1/emails/templates/exists |
GET | Check whether a template path resolves |
/api/v1/emails/attachments |
GET | List all registered attachment aliases |
Handler Priority¶
The EmailNotificationHandler runs at priority 2100, which means it executes after business logic handlers (1000), timeline logging (2000), and upsell email enrichment (2050) but before outbound webhooks (2200). This ordering ensures all data is finalized before the email template is rendered. See Notification System - Priority Ranges.
Related Topics¶
- Notification System
- Outbound Webhooks
- Background Jobs
- Abandoned Cart Recovery
- Developer reference - docs/EmailSystem.md (internal guide, not shipped as documentation)