Transactional Email Service
EmailEngine can function as a self-hosted transactional email service, allowing you to convert any email account into a reliable email delivery system. You can submit messages for delivery, schedule future sends, track delivery status, and receive bounce notifications.
Overview
EmailEngine provides transactional email capabilities through:
- Multiple Submission Methods: Send via REST API or SMTP
- Message Queuing: Reliable delivery with automatic retry
- Scheduled Sending: Delay delivery to a specific future time
- Bounce Detection: Automatic bounce tracking and webhooks
- Sent Mail Tracking: Automatic upload to "Sent Mail" folder
- Reply Threading: Automatic "Answered" flag on replied messages
Delivery via REST API
Submit Endpoint
Submit emails using the /v1/account/{account}/submit endpoint. EmailEngine converts your structured JSON into a valid RFC822 MIME message.
Endpoint: POST /v1/account/{account}/submit
Benefits:
- No MIME knowledge required
- Unicode strings and base64 attachments
- Automatic header generation
- Reply threading support
Basic Example
curl -XPOST "https://ee.example.com/v1/account/example/submit" \
-H "Authorization: Bearer TOKEN" \
-H "Content-Type: application/json" \
-d '{
"from": {
"name": "Example Sender",
"address": "sender@example.com"
},
"to": [{
"name": "John Doe",
"address": "john@example.com"
}],
"subject": "Hello from EmailEngine",
"text": "Plain text message",
"html": "<p>HTML message</p>",
"attachments": [
{
"filename": "document.pdf",
"content": "BASE64_ENCODED_CONTENT"
}
]
}'
Response:
{
"response": "Queued for delivery",
"messageId": "<188db4df-3abb-806c-94c8-7a9303652c50@example.com>",
"sendAt": "2025-10-15T10:30:00.000Z",
"queueId": "24279fb3e0dff64e"
}
Reply to Existing Message
When replying to a message, EmailEngine automatically handles threading headers:
curl -XPOST "https://ee.example.com/v1/account/example/submit" \
-H "Authorization: Bearer TOKEN" \
-H "Content-Type: application/json" \
-d '{
"reference": {
"message": "AAAAAQAAP1w",
"action": "reply"
},
"from": {
"name": "Support Team",
"address": "support@example.com"
},
"to": [{
"name": "Customer",
"address": "customer@example.com"
}],
"text": "Thank you for your message. We will review and get back to you.",
"html": "<p>Thank you for your message. We will review and get back to you.</p>"
}'
Automatic Handling:
- Subject derived from original (with "Re:" prefix)
In-Reply-Toheader set correctlyReferencesheader populated- Original message marked as "Answered"
You can override the subject if needed:
{
"reference": {
"message": "AAAAAQAAP1w",
"action": "reply"
},
"subject": "Custom reply subject",
"text": "Reply content"
}
Attachments
Include attachments with base64-encoded content:
{
"from": { "address": "sender@example.com" },
"to": [{ "address": "recipient@example.com" }],
"subject": "File attached",
"text": "Please find the file attached.",
"attachments": [
{
"filename": "report.pdf",
"content": "JVBERi0xLjQKJeLjz9MKMSAwIG9iago8PAovVHlwZSAvQ2F0YWxvZwovUGFnZXMgMiAwI...",
"contentType": "application/pdf"
},
{
"filename": "image.png",
"content": "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAABlBMVEUAAAD///+l2Z...",
"contentType": "image/png",
"cid": "unique-cid-123"
}
]
}
Attachment Properties:
filename: Name of the filecontent: Base64-encoded file contentcontentType(optional): MIME type (auto-detected if omitted)cid(optional): Content-ID for inline images
Inline Images
Reference inline images in HTML using CID:
{
"from": { "address": "sender@example.com" },
"to": [{ "address": "recipient@example.com" }],
"subject": "Image email",
"html": "<p>Check out this image:</p><img src=\"cid:logo-image\" />",
"attachments": [
{
"filename": "logo.png",
"content": "BASE64_ENCODED_IMAGE",
"contentType": "image/png",
"cid": "logo-image"
}
]
}
Delivery via SMTP
EmailEngine includes an optional SMTP server for standard email client integration.
Enable SMTP Server
- Navigate to Configuration → SMTP Interface
- Check Enable SMTP Server
- Configure port (default: 2525)
- Set authentication password
- Save settings
Important Notes:
- TLS/STARTTLS support available via
smtpServerTLSEnabledsetting - Can enable HAProxy PROXY protocol support
- Authentication optional but recommended
Authentication
SMTP uses PLAIN authentication. Generate auth string:
# Format: \0{account_id}\0{password}
echo -ne "\0example\0your_password" | base64
# Output: AGV4YW1wbGUAeW91cl9wYXNzd29yZA==
Manual SMTP Session
Test SMTP with telnet or netcat:
# Connect
telnet localhost 2525
# or
nc -c localhost 2525
SMTP Commands:
EHLO client.example.com
AUTH PLAIN AGV4YW1wbGUAeW91cl9wYXNzd29yZA==
MAIL FROM:<sender@example.com>
RCPT TO:<recipient@example.com>
DATA
From: sender@example.com
To: recipient@example.com
Subject: Test Email
X-EE-Send-At: 2025-10-16T14:00:00.000Z
This is the email body.
.
QUIT
Response: 250 Message queued for delivery as {queueId} ({timestamp})
SMTP Headers
EmailEngine recognizes special headers:
X-EE-Send-At
Schedule delivery for future time:
X-EE-Send-At: 2025-10-16T14:00:00.000Z
This header is removed before delivery.
X-EE-Account
Specify account when authentication disabled:
X-EE-Account: example
Required only if SMTP authentication is disabled.
SMTP Client Example
Node.js (nodemailer):
const nodemailer = require('nodemailer');
const transporter = nodemailer.createTransport({
host: 'localhost',
port: 2525,
secure: false, // No TLS
auth: {
user: 'example', // Account ID
pass: 'your_password'
}
});
const message = {
from: 'sender@example.com',
to: 'recipient@example.com',
subject: 'Test Email',
text: 'Plain text content',
html: '<p>HTML content</p>',
// Schedule for future delivery
headers: {
'X-EE-Send-At': '2025-10-16T14:00:00.000Z'
}
};
const info = await transporter.sendMail(message);
console.log('Message queued:', info.messageId);
Python (smtplib):
import smtplib
from email.message import EmailMessage
msg = EmailMessage()
msg['From'] = 'sender@example.com'
msg['To'] = 'recipient@example.com'
msg['Subject'] = 'Test Email'
msg['X-EE-Send-At'] = '2025-10-16T14:00:00.000Z'
msg.set_content('Plain text content')
with smtplib.SMTP('localhost', 2525) as smtp:
smtp.login('example', 'your_password')
smtp.send_message(msg)
print('Message queued')
Important SMTP Notes
- Recipient addresses: Only addresses in
RCPT TOcommands receive email - Header addresses:
To,Cc,Bccheaders are informational only - Bcc header: Automatically removed from messages
- Mandatory headers:
Message-ID,MIME-Version,Dateadded if missing - TLS: Enable via
smtpServerTLSEnabledsetting, or use HAProxy for TLS termination
Scheduled Sending
Delay message delivery to a specific future time.
API Scheduling
Use the sendAt property with ISO timestamp:
curl -XPOST "https://ee.example.com/v1/account/example/submit" \
-H "Authorization: Bearer TOKEN" \
-H "Content-Type: application/json" \
-d '{
"from": {
"address": "sender@example.com"
},
"to": [{
"address": "recipient@example.com"
}],
"subject": "Scheduled Email",
"text": "This email was scheduled for delivery.",
"sendAt": "2025-10-18T08:00:00.000Z"
}'
Response includes scheduled time:
{
"response": "Queued for delivery",
"messageId": "<uuid@example.com>",
"sendAt": "2025-10-18T08:00:00.000Z",
"queueId": "abc123"
}
SMTP Scheduling
Add X-EE-Send-At header:
From: sender@example.com
To: recipient@example.com
Subject: Scheduled Email
X-EE-Send-At: 2025-10-18T08:00:00.000Z
This email will be sent at the scheduled time.
Time Format
Use ISO 8601 format with timezone:
2025-10-18T08:00:00.000Z # UTC
2025-10-18T08:00:00+02:00 # UTC+2
2025-10-18T08:00:00-05:00 # UTC-5
Scheduling Limits
- Maximum schedule time: Configurable (default: no limit)
- Minimum schedule time: None (if
sendAtis in the past, message sends immediately) - Queue retention: Messages remain queued until
sendAttime
Webhook Notifications
EmailEngine sends webhook notifications for delivery events.
messageSent
Triggered when SMTP server accepts the message:
{
"account": "example",
"date": "2025-10-15T10:30:05.000Z",
"event": "messageSent",
"data": {
"messageId": "<188db4df-3abb-806c-94c8-7a9303652c50@example.com>",
"response": "250 2.0.0 OK queued as 1234ABCD",
"queueId": "24279fb3e0dff64e",
"envelope": {
"from": "sender@example.com",
"to": ["recipient@example.com"]
}
}
}
messageDeliveryError
Triggered when delivery fails temporarily (will retry):
{
"account": "example",
"date": "2025-10-15T10:30:05.000Z",
"event": "messageDeliveryError",
"data": {
"queueId": "24279fb3e0dff64e",
"messageId": "<188db4df-3abb-806c-94c8-7a9303652c50@example.com>",
"envelope": {
"from": "sender@example.com",
"to": ["recipient@example.com"]
},
"error": "Connection timeout",
"errorCode": "ETIMEDOUT",
"smtpResponse": "421 4.4.2 Connection timed out",
"smtpResponseCode": 421,
"job": {
"attemptsMade": 1,
"nextAttempt": "2025-10-15T10:30:15.000Z"
}
}
}
messageFailed
Triggered when delivery permanently fails (no more retries):
{
"account": "example",
"date": "2025-10-15T10:30:05.000Z",
"event": "messageFailed",
"data": {
"queueId": "24279fb3e0dff64e",
"messageId": "<188db4df-3abb-806c-94c8-7a9303652c50@example.com>",
"error": "Recipient address rejected",
"response": "550 5.1.1 User unknown"
}
}
messageBounce
Triggered when a bounce message is detected in the mailbox:
{
"account": "example",
"date": "2025-10-15T11:00:00.000Z",
"event": "messageBounce",
"data": {
"bounceMessage": "AAAAAgAAxxk",
"recipient": "invalid@example.com",
"action": "failed",
"response": {
"source": "smtp",
"message": "550 5.1.1 No such user",
"status": "5.1.1"
},
"mta": "mx.example.com (192.168.1.1)",
"messageId": "<19f1157c-d72b-50eb-74d5-d30f9ec816d3@example.com>"
}
}
Note: Bounce notification includes both messageId and queueId, which you can use to correlate bounces with sent messages.
Bounce Detection
EmailEngine automatically monitors IMAP accounts for bounce messages and parses delivery status notifications (DSN).
How It Works
- Message submitted and sent to SMTP server
- SMTP server accepts message (
messageSentwebhook) - If delivery later fails, MTA sends bounce email to sender
- EmailEngine detects bounce message in IMAP account
- EmailEngine parses bounce and extracts details
messageBouncewebhook sent with failure information
Bounce Types
Hard Bounce (action: "failed"):
- Permanent delivery failure
- Invalid email address
- Domain doesn't exist
- Mailbox disabled
Soft Bounce (action: "delayed"):
- Temporary failure
- Mailbox full
- Server temporarily unavailable
- Will be retried by MTA
Bounce Information
Bounce webhooks include:
- recipient: Failed recipient address
- action:
failed(permanent) ordelayed(temporary) - response.status: SMTP status code (e.g., "5.1.1")
- response.message: Error message from server
- mta: Mail server that reported failure
- messageId: Original message ID
- bounceMessage: EmailEngine ID of the bounce email
Tracking Bounces
To correlate bounces with sent messages:
1. Store messageId when sending:
const response = await fetch('https://ee.example.com/v1/account/example/submit', {
method: 'POST',
headers: {
'Authorization': 'Bearer TOKEN',
'Content-Type': 'application/json'
},
body: JSON.stringify({
from: { address: 'sender@example.com' },
to: [{ address: 'recipient@example.com' }],
subject: 'Test',
text: 'Content'
})
});
const data = await response.json();
// Store in database
await db.messages.insert({
queueId: data.queueId,
messageId: data.messageId,
recipient: 'recipient@example.com',
status: 'queued'
});
2. Match bounce webhook to original:
// Webhook handler
app.post('/webhooks', async (req, res) => {
const event = req.body;
if (event.event === 'messageBounce') {
const messageId = event.data.messageId;
// Find original message
const original = await db.messages.findOne({ messageId });
if (original) {
// Update status
await db.messages.update(
{ messageId },
{
status: 'bounced',
bounceReason: event.data.response.message,
bounceStatus: event.data.response.status
}
);
// Handle bounce (unsubscribe, notify, etc.)
await handleBounce(original, event.data);
}
}
res.json({ success: true });
});
Queue Management
EmailEngine uses BullMQ for reliable message queuing.
Queue Monitoring
View queue status in Bull Board:
- Navigate to Tools → Bull Board
- Select Submit queue
- View job statuses:
- Waiting: Ready to send immediately
- Delayed: Scheduled for future or retry after failure
- Active: Currently being sent
- Completed: Successfully delivered
- Failed: Permanently failed
Retry Behavior
Default Retry Strategy: EmailEngine uses exponential backoff with a base delay of 5 seconds:
- Initial attempt: Immediate
- Retry 1: ~5 seconds later (5s x 2^0)
- Retry 2: ~10 seconds later (5s x 2^1)
- Retry 3: ~20 seconds later (5s x 2^2)
- Retry 4: ~40 seconds later (5s x 2^3)
- And so on, doubling each time
Default maximum attempts: 10
Configure retry attempts in Configuration → General Settings → Retry Attempts.
Manual Queue Management
Retry a failed job:
- Go to Bull Board → Submit queue → Failed
- Find the job
- Click Retry
Remove a job:
- Go to Bull Board → Submit queue
- Find the job in any status
- Click Delete
Pause queue:
- Go to Bull Board → Submit queue
- Click Pause
- All new jobs go to "Paused" status
- Click Resume to continue
Queue Performance
For high-volume sending:
- Monitor Waiting queue size
- If growing, increase worker concurrency
- Check SMTP server rate limits
- Review delivery errors in Failed tab