Domain Model
Entity Hierarchy
Organization (from JWT `iss` claim)
└── Domain
└── Inbox
└── Thread
└── Message
├── Recipients (to/cc/bcc)
├── References (Message-ID chain)
├── Attachments
└── Delivery EventsThreading uses standard RFC 5322 headers. When a message arrives or is sent with an In-Reply-To header that matches a known MessageId, it is assigned to that thread. If no match is found, the References chain and subject fallback are tried before creating a new thread. The message_references table stores the full References chain for robust thread reconstruction.
Threads support soft-delete: DELETE /threads/:id sets deleted_at rather than removing the row. Messages remain accessible by direct ID lookup.
Core Types
Domain
A sending/receiving domain with SES identity and DNS verification.
| Field | Type | Description |
|---|---|---|
id | UUID | Primary key |
name | String | Domain name (e.g., mail.acme.com) |
ses_identity_arn | Option<String> | ARN after SES identity creation |
dkim_tokens | Vec<String> | DKIM CNAME tokens from SES |
from_name | Option<String> | Default sender display name |
reply_to_address | Option<String> | Default Reply-To address |
bounce_address | Option<String> | Bounce notification address |
verification | DnsVerification | MX, SPF, DKIM, DMARC status (all booleans) |
created_at | DateTime | Creation timestamp |
updated_at | DateTime | Last update timestamp |
deleted_at | Option<DateTime> | Soft-delete timestamp |
Inbox
A named receiving address within a domain.
| Field | Type | Description |
|---|---|---|
id | UUID | Primary key |
domain_id | UUID | Parent domain |
name | String | Display name |
local_part | String | Address prefix (e.g., support) |
is_catchall | bool | Accept mail for any unmatched address |
created_at | DateTime | Creation timestamp |
updated_at | DateTime | Last update timestamp |
deleted_at | Option<DateTime> | Soft-delete timestamp |
EmailMessage
Source:
crates/core/src/lib.rs—message::EmailMessage
| Field | Type | Description |
|---|---|---|
message_id | MessageId (String) | RFC 5322 Message-ID header (primary key) |
thread_id | ThreadId (UUID) | Resolved thread |
inbox_id | UUID | Owning inbox |
direction | Inbound | Outbound | Message direction |
from | EmailAddress | Sender |
to / cc / bcc | Vec<EmailAddress> | Recipients |
subject | String | Subject line |
sent_at | DateTime | Send timestamp |
in_reply_to | Option<MessageId> | Parent message reference |
references | Vec<MessageId> | Full reference chain |
body_text | Option<String> | Plain text body |
body_html | Option<String> | HTML body |
attachments | Vec<EmailAttachment> | Attachment metadata |
raw_storage_key | String | S3 key for raw MIME |
ses_message_id | Option<String> | SES-assigned ID (outbound only) |
Supporting Types
Source:
crates/core/src/lib.rs(message module) andcrates/core/src/threading.rs
MessageId— Newtype aroundString. RFC 5322 Message-ID header value (e.g.,<unique-id@domain>).ThreadId— Newtype aroundUuid. Identifies a conversation thread.EmailAddress—{ name: Option<String>, address: String }. Used for from/to/cc/bcc fields.EmailEnvelope— Headers and routing info without body content: message_id, from, to/cc/bcc, subject, sent_at, in_reply_to, references.EmailAttachment— Attachment metadata: filename, content_type, storage_key, size_bytes. Content stored in S3.EmailThread— Thread container: thread_id, inbox_id, subject, created_at, updated_at, message_count, deleted_at.
DeliveryStatus
Source:
crates/core/src/lib.rs—message::DeliveryStatus
Variants: Pending, Sent, Delivered, Bounced { reason }, Complained, Failed { reason }.
State transitions:
Pending ──► Sent ──► Delivered
│
├──► Bounced
├──► Complained
└──► FailedDomainError
Source:
crates/core/src/lib.rs—error::DomainError
| Variant | Fields | HTTP Status |
|---|---|---|
Validation | { field: &'static str, reason: String } | 400 Bad Request |
NotFound | { resource: &'static str, id: String } | 404 Not Found |
AttachmentTooLarge | { size_bytes: u64, limit_bytes: u64 } | 413 Payload Too Large |
Storage | Box<dyn Error + Send + Sync> | 500 Internal Server Error |
Delivery | Box<dyn Error + Send + Sync> | 500 Internal Server Error |
See crates/http/src/error.rs for the full HTTP mapping implementation.
Database Tables
| Table | Purpose |
|---|---|
threads | Conversation containers |
messages | Individual emails with envelope metadata |
message_recipients | To/CC/BCC addresses per message |
message_references | References header chain per message |
attachments | File metadata with S3 storage keys |
delivery_events | SES status tracking per message |
webhook_endpoints | Registered webhook URLs and config |
webhook_deliveries | Pending/completed webhook delivery records |
suppressed_addresses | Bounce/complaint suppression list |
domains | Sending domains with verification state |
inboxes | Receiving addresses within domains |
auth_keys | JWT public key registry |
See Schema Reference for full SQL DDL, column definitions, and indexes.
S3 Storage Layout
raw-email-bucket/
{yyyy}/{mm}/{dd}/{message-id-hash}.eml
attachments-bucket/
{yyyy}/{mm}/{dd}/{message-id-hash}/{attachment-uuid}.{ext}Key format:
- Date partitioning for lifecycle policies
- Message-ID hashed to avoid special characters in keys
- Original extension preserved for attachments