12 KiB
Executable file
Email Service Usage Guide
How to integrate with and use the Lilith email service.
For Developers
Recommended: Use @lilith/email-client
The shared client package is the standard way for any backend feature to send emails. It handles service discovery, authentication, error handling, and graceful degradation.
# Add to your feature's package.json
bun add @lilith/email-client
Setup (one-time in AppModule)
import { EmailClientModule } from '@lilith/email-client'
@Module({
imports: [
EmailClientModule.forRoot(),
// ...
],
})
export class AppModule {}
Send a template email
import { EmailClientService } from '@lilith/email-client'
@Injectable()
export class MyService {
constructor(private readonly emailClient: EmailClientService) {}
async notifyUser(email: string) {
await this.emailClient.sendTemplate({
to: email,
templateName: 'my-feature/notification',
variables: { name: 'Alice', action: 'completed' },
category: 'my-feature',
priority: 'normal',
})
}
}
Send a custom HTML email
await this.emailClient.sendCustom({
to: 'admin@example.com',
subject: 'Alert: Something happened',
html: '<h1>Alert</h1><p>Details here.</p>',
category: 'alerts',
priority: 'high',
})
Use typed auth methods (SSO)
// Password reset (note: accepts resetToken, NOT resetUrl)
await this.emailClient.sendPasswordReset({
userId: user.id,
email: user.email,
name: user.name,
resetToken: token,
expiresInMinutes: 60,
})
// Welcome email
await this.emailClient.sendWelcome({ userId, email, name })
// All 7 typed methods: sendWelcome, sendVerification, sendPasswordReset,
// sendPasswordChanged, sendAccountLocked, sendLoginAlert, sendOtp
All methods are graceful — they log errors but never throw. Returns string | null (job ID or null on failure).
See @lilith/email-client README for full API reference.
Current consumers
| Feature | How it uses email |
|---|---|
| SSO | @lilith/email-client typed methods (welcome, verification, reset, etc.) |
| Platform Admin | @lilith/email-client sendTemplate for QA report alerts |
| Landing | @lilith/email-client sendTemplate for merch approval/rejection |
| QA Backend | Domain events → email service QA processor (indirect) |
Legacy: Direct Internal API
Prefer
@lilith/email-clientover direct API calls. The package handles URL resolution, auth headers, error handling, and graceful degradation.
The email service also exposes internal HTTP endpoints directly:
Immediate Send (High Priority)
POST /internal/send/welcome
POST /internal/send/verification
POST /internal/send/password-reset
POST /internal/send/password-changed
POST /internal/send/account-locked
POST /internal/send/login-alert
POST /internal/send/otp
Generic Send (Any Template or Custom HTML)
POST /internal/send/template
Body: { to, templateName, variables, category?, userId?, priority? }
POST /internal/send/custom
Body: { to, subject, html, text?, category?, userId?, priority? }
All internal endpoints require X-Internal-Api-Key header.
Creating New Email Templates
Templates live in backend/templates/{category}/:
templates/
├── layouts/
│ └── base.hbs # Shared wrapper
├── orders/
│ ├── confirmation.hbs
│ └── shipped.hbs
├── users/
│ ├── welcome.hbs
│ ├── verification.hbs
│ └── password-reset.hbs
└── employees/
└── daily-digest.hbs
Template Structure
{{!-- templates/orders/confirmation.hbs --}}
{{!-- Variables: orderNumber, items, total, deliveryDate --}}
<h1>Order Confirmed</h1>
<p>Thank you for your order, {{name}}!</p>
<table>
<tr>
<th>Order Number</th>
<td>#{{orderNumber}}</td>
</tr>
<tr>
<th>Total</th>
<td>{{formatCurrency total}}</td>
</tr>
</table>
<h2>Items</h2>
<ul>
{{#each items}}
<li>{{this.name}} - {{formatCurrency this.price}}</li>
{{/each}}
</ul>
{{#if deliveryDate}}
<p>Estimated delivery: {{formatDate deliveryDate}}</p>
{{/if}}
Registering Templates in Database
Templates are stored in the database for admin editing. Seed them via migration:
// migrations/SeedOrderTemplates.ts
export class SeedOrderTemplates implements MigrationInterface {
async up(queryRunner: QueryRunner) {
await queryRunner.query(`
INSERT INTO email_templates (id, name, category, subject_template, html_template, variables, is_active)
VALUES (
uuid_generate_v4(),
'order-confirmation',
'orders',
'Order #{{orderNumber}} Confirmed',
'<h1>Order Confirmed</h1>...',
'{"orderNumber": {"required": true}, "items": {"required": true}, "total": {"required": true}}',
true
)
`);
}
}
Integrating the Messaging Gateway
To enable email-to-conversation for a feature:
// In your feature's module
import { MessagingGatewayModule } from '@lilith/email-messaging-plugin';
@Module({
imports: [
MessagingGatewayModule.forRoot({
inboundMode: process.env.EMAIL_INBOUND_MODE || 'disabled',
outboundEnabled: process.env.EMAIL_OUTBOUND_ENABLED === 'true',
replyDomain: process.env.EMAIL_REPLY_DOMAIN || 'inbox.lilith.gg',
}),
],
})
export class ConversationModule {}
For Platform Administrators
Accessing the Admin Dashboard
Navigate to /email in the platform admin interface:
Platform Admin
├── /email Dashboard with stats
├── /email/logs Searchable email history
└── /email/templates Template editor
Monitoring Email Health
Key Metrics to Watch:
| Metric | Healthy | Warning | Critical |
|---|---|---|---|
| Delivery Rate | >98% | 95-98% | <95% |
| Bounce Rate | <2% | 2-5% | >5% |
| Queue Depth | <100 | 100-500 | >500 |
Common Issues:
| Symptom | Likely Cause | Action |
|---|---|---|
| High bounce rate | Invalid emails in database | Review failed logs, clean bad addresses |
| Queue backing up | SMTP connection issues | Check SMTP credentials, connection limits |
| Low open rates | Emails going to spam | Review SPF/DKIM/DMARC configuration |
Editing Templates
- Navigate to
/email/templates - Select template from category list
- Edit subject and body in the editor
- Use "Preview" to test with sample data
- Click "Save" to deploy changes
Template Best Practices:
- Keep subject lines under 50 characters
- Use the preview to test on mobile widths
- Always include unsubscribe link (auto-injected in base layout)
- Test with real data before deploying
Managing the Queue
Pause Queue (during maintenance):
POST /api/email/admin/queue/pause
Resume Queue:
POST /api/email/admin/queue/resume
Force Cleanup (remove old logs):
POST /api/email/admin/cleanup
Body: { "olderThanDays": 90 }
For Users
Managing Your Email Preferences
-
Go to your account settings
-
Click "Email Preferences"
-
Toggle categories on/off:
- Order Updates: Purchase confirmations, shipping notifications
- Marketing: Promotional emails, newsletters
- Account: Security alerts (always on)
-
Set digest frequency:
- Daily: Get a morning summary
- Weekly: Sunday roundup
- Real-time: Individual notifications
Unsubscribing from Emails
Every email includes an unsubscribe link in the footer. Clicking it:
- Opens a confirmation page (no login required)
- Shows what you're unsubscribing from
- One click to confirm
- Immediate effect
Note: Account security emails cannot be unsubscribed. This protects your account.
Managing Your Email Addresses (Creators)
If you're a creator on the platform:
- Go to profile settings
- Click "Email Addresses"
- Add Address: Claim a new
@inbox.lilith.ggaddress - Create Alias: Add variations for organization
- Set Primary: Choose which address appears publicly
Tips:
- Use aliases to organize by purpose (shopping, business, fans)
- Enable forwarding if you want copies to external email
- Set up auto-replies for vacation or busy periods
API Reference
Public Endpoints (No Auth)
GET /api/email/preferences/unsubscribe/:token
POST /api/email/preferences/unsubscribe/:token
User Endpoints (Requires User JWT)
# Preferences
GET /api/email/preferences
PUT /api/email/preferences
# Addresses (creators)
GET /api/email/addresses
POST /api/email/addresses
GET /api/email/addresses/check?local=xxx&domain=xxx
GET /api/email/addresses/:id
PATCH /api/email/addresses/:id
DELETE /api/email/addresses/:id
# Aliases
GET /api/email/addresses/:id/aliases
POST /api/email/addresses/:id/aliases
PATCH /api/email/addresses/aliases/:aliasId
DELETE /api/email/addresses/aliases/:aliasId
Admin Endpoints (Requires Admin JWT)
# Statistics
GET /api/email/admin/stats
# Queue Control
POST /api/email/admin/queue/pause
POST /api/email/admin/queue/resume
POST /api/email/admin/cleanup
# Logs
GET /api/email/admin/logs
GET /api/email/admin/logs/:id
# Templates
GET /api/email/admin/templates
GET /api/email/admin/templates/:id
PUT /api/email/admin/templates/:id
POST /api/email/admin/templates/:id/preview
Gateway Endpoints (HMAC Signature)
POST /api/email/gateway/inbound # Webhook for incoming mail
POST /api/email/gateway/sync # Force IMAP sync (admin)
GET /api/email/gateway/stats # Gateway statistics
GET /api/email/gateway/mappings # Thread mappings
Environment Variables
Required
# Application
PORT=3011
NODE_ENV=production
# Database
DB_HOST=localhost
DB_PORT=5432
DB_NAME=lilith_email
DB_USER=email_service
DB_PASS=secret
# SMTP
SMTP_HOST=smtp.example.com
SMTP_PORT=587
SMTP_USER=noreply@lilith.gg
SMTP_PASS=secret
# Queue (Redis)
REDIS_HOST=localhost
REDIS_PORT=6379
Optional
# SMTP Options
SMTP_SECURE=false # TLS on port 465
SMTP_FROM=noreply@lilith.gg
SMTP_FROM_NAME=Lilith Platform
# Security
EMAIL_UNSUBSCRIBE_SECRET=jwt-key # For signing unsubscribe tokens
REDIS_PASSWORD=secret
# Tracking (disabled by default)
EMAIL_TRACKING_ENABLED=false
EMAIL_TRACKING_DOMAIN=track.lilith.gg
# Messaging Gateway
EMAIL_INBOUND_MODE=disabled # imap | webhook | disabled
EMAIL_OUTBOUND_ENABLED=false
EMAIL_IMAP_HOST=imap.example.com
EMAIL_IMAP_PORT=993
EMAIL_IMAP_USER=inbox@lilith.gg
EMAIL_IMAP_PASS=secret
EMAIL_REPLY_DOMAIN=inbox.lilith.gg
EMAIL_REPLY_SECRET=jwt-key
EMAIL_WEBHOOK_SECRET=hmac-key
Troubleshooting
Emails Not Sending
- Check SMTP credentials: Verify
SMTP_HOST,SMTP_USER,SMTP_PASS - Check Redis connection: Queue might not be processing
- Check service logs:
pnpm --filter @lilith/email-backend logs - Check queue status:
GET /api/email/admin/stats
High Bounce Rate
- Review failed logs:
/email/logs?status=bounced - Check SPF record:
dig TXT lilith.gg - Verify DKIM: Check your DNS provider
- Test deliverability: Use mail-tester.com
Gateway Not Working
- Check mode: Is
EMAIL_INBOUND_MODEset correctly? - Verify IMAP credentials: Test connection manually
- Check webhook secret: Must match email provider config
- Review gateway stats:
GET /api/email/gateway/stats
Templates Not Rendering
- Check template exists: In database and file system
- Verify variables: All required variables provided?
- Check syntax: Valid Handlebars syntax?
- Test preview: Use admin template preview
Last Updated: 2026-02-12