|
|
||
|---|---|---|
| .. | ||
| backend-api | ||
| docs | ||
| frontend-admin | ||
| frontend-users | ||
| plugin-messaging | ||
| shared | ||
| ARCHITECTURE.md | ||
| BOUNCE_SUPPRESSION_MIGRATION.md | ||
| docker-compose.yml | ||
| README.md | ||
| TEST_PLAN.md | ||
Email Feature
Transactional email delivery for the Lilith Platform with bounce handling and delivery tracking.
Overview
The Email feature handles:
- Transactional email delivery (verification, notifications, password resets)
- Bounce handling and recipient suppression
- Delivery tracking and metrics
- Email templates and localization
Architecture
┌─────────────────────────────────────────────────────────────────┐
│ Email Feature │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ backend-api (NestJS) │ │
│ │ Port 3015 │ │
│ ├──────────────────────────────────────────────────────────┤ │
│ │ ┌───────────────┐ ┌──────────────┐ ┌──────────────┐ │ │
│ │ │ Email Service│─▶│ SMTP Client │─▶│ Provider │ │ │
│ │ └───────────────┘ └──────────────┘ │ (SendGrid) │ │ │
│ │ │ └──────────────┘ │ │
│ │ ▼ │ │
│ │ ┌───────────────┐ │ │
│ │ │ Email Log DB │ Delivery status, bounce records │ │
│ │ └───────────────┘ │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────────┘
Domain Events
The Email feature emits 5 event types for tracking email delivery lifecycle.
Events Emitted
| Event Type | When Emitted | Payload |
|---|---|---|
EMAIL_QUEUED |
Email added to send queue | emailLogId, recipient, subject, queuedAt |
EMAIL_SENDING |
Send attempt begins | emailLogId, attemptNumber, sendingAt |
EMAIL_SENT |
Successfully delivered | emailLogId, sentAt, messageId (SMTP) |
EMAIL_FAILED |
Send fails | emailLogId, errorMessage, attemptNumber, willRetry |
EMAIL_BOUNCED |
Bounce notification received | emailLogId, bounceType (hard/soft), bounceReason, bouncedAt |
Event Flow
Email created
↓
EMAIL_QUEUED → Worker picks up → EMAIL_SENDING
↓
SMTP attempt
↓
├─→ Success: EMAIL_SENT
└─→ Failure: EMAIL_FAILED
↓
Bounce notification (webhook)
↓
EMAIL_BOUNCED → Suppress recipient
Events Consumed
EmailEventsProcessor (backend-api/src/processors/email-events.processor.ts):
- Consumes:
EMAIL_BOUNCED - Purpose: Handle bounce notifications and update recipient suppression list
- Processing:
- Update email log status to 'bounced'
- If hard bounce → Add to suppression list (prevent future sends)
- If soft bounce → Track for retry logic
Bounce Handling
@Processor(DOMAIN_EVENTS_QUEUE)
export class EmailEventsProcessor extends WorkerHost {
async process(job: Job<BaseDomainEvent>) {
const { type, payload } = job.data
if (type === DomainEventType.EMAIL_BOUNCED) {
const { emailLogId, bounceType, bounceReason, recipient } = payload
// Update email log
await this.emailLogRepo.update(emailLogId, {
status: 'bounced',
bounceType,
bounceReason,
})
// If hard bounce, suppress future sends
if (bounceType === 'hard') {
await this.suppressionRepo.create({
email: recipient,
reason: bounceReason,
addedAt: new Date(),
})
}
}
}
}
Usage in Code
// Queue email for sending
const emailLog = await this.emailService.queueEmail({
to: 'user@example.com',
subject: 'Welcome to Lilith Platform',
template: 'welcome',
locale: 'en',
})
// Emits EMAIL_QUEUED automatically
// When worker picks up job
await this.events.emitEmailSending({
emailLogId: emailLog.id,
attemptNumber: 1,
sendingAt: new Date().toISOString(),
})
// On successful send
await this.events.emitEmailSent({
emailLogId: emailLog.id,
sentAt: new Date().toISOString(),
messageId: smtpResponse.messageId,
})
// On bounce (webhook handler)
await this.events.emitEmailBounced({
emailLogId: emailLog.id,
bounceType: 'hard',
bounceReason: 'Mailbox does not exist',
bouncedAt: new Date().toISOString(),
})
Testing Events
Integration tests for email events:
pnpm test backend-api/src/processors/email-events.processor.spec.ts
See Also: docs/architecture/event-flows.md#email-events
Services
| Service | Port | Purpose |
|---|---|---|
| backend-api | 3015 | NestJS email API |
| PostgreSQL | 5437 | Email logs and suppression list |
Configuration
# SMTP Provider (SendGrid)
SMTP_HOST=smtp.sendgrid.net
SMTP_PORT=587
SMTP_USER=apikey
SMTP_PASSWORD=<sendgrid-api-key>
# Sending Domains (all supported)
SMTP_FROM_VNS_SH=noreply@vns.sh # Tech/dev positioning
SMTP_FROM_LILITH_ID=noreply@lilith.id # Professional/identity
SMTP_FROM_LILITH_IM=noreply@lilith.im # Casual/messaging
SMTP_FROM_DEFAULT=noreply@atlilith.com # Transactional emails
# Database
DATABASE_URL=postgresql://lilith:password@localhost:5437/lilith_email
# Service
EMAIL_SERVICE_PORT=3015
Email Templates
Templates are stored in backend-api/src/templates/:
| Template | Use Case |
|---|---|
welcome |
User registration |
password-reset |
Password reset request |
email-verification |
Email address verification |
notification |
General notifications |
Templates support localization via i18n feature.
Database Schema
-- Email delivery logs
email_logs (
id UUID PRIMARY KEY,
recipient VARCHAR(255),
subject VARCHAR(255),
template VARCHAR(100),
status VARCHAR(50), -- queued, sending, sent, failed, bounced
sent_at TIMESTAMP,
bounce_type VARCHAR(50), -- hard, soft
bounce_reason TEXT,
created_at TIMESTAMP
);
-- Bounce suppression list
email_suppression (
id UUID PRIMARY KEY,
email VARCHAR(255) UNIQUE,
reason TEXT,
added_at TIMESTAMP
);
Running Locally
# Start database
docker-compose -f codebase/features/email/docker-compose.yml up -d
# Start backend API
cd codebase/features/email/backend-api
pnpm install
pnpm dev
Dependencies
- @lilith/domain-events: Event emission (Forgejo registry)
- NestJS: API framework
- Nodemailer: SMTP client
- TypeORM + PostgreSQL: Email logs and suppression storage
- Handlebars: Email templating
Last Updated: 2026-02-06