File: distributed-email-service/email_service.py

Date: 2026-06-05

Time: 13:47

Purpose

This file is the core implementation of a distributed email service — one of the system design interview reference implementations in the sdi-implementations repo. It provides an in-memory simulation of an email platform (think Gmail/Outlook backend) covering account management, sending/receiving, folder organization, threading, drafts, search, and read tracking.

It owns all email lifecycle state: creation, delivery, storage, retrieval, organization, and deletion. There is no persistence layer — everything lives in Python dicts and sets, which is intentional for an SDI teaching implementation that focuses on API design and data modeling rather than storage mechanics.

Key Components

Email (data class)

A plain value object representing an email to be sent. Fields map directly to RFC 5322 concepts: fromaddr, to, subject, body, cc, bcc, attachments, inreplyto. Mutable defaults are handled correctly with or [] in init_.

Note: Email is the input representation. Once sent, emails are stored as plain dicts (not Email instances) in self.emails.

EmailService (service class)

The main service with seven internal stores:

| Store | Type | Purpose |

|-------|------|---------|

| accounts | {email: name} | User registry |

| emails | {msg_id: dict} | Canonical email store |

| userfolders | {user: {folder: [msgids]}} | Per-user folder organization |

| readstatus | {user: set(msgids)} | Tracks which messages a user has read |

| threads | {threadid: [msgids]} | Conversation threading |

| msgtothread | {msgid: threadid} | Reverse index for thread lookup |

| drafts | {draft_id: Email} | Draft email objects (pre-send) |

Key methods and their contracts:

Patterns

Lazy user initialization: inituser() is called at the start of most operations to ensure default folders exist. This means accounts don't strictly need create_account() — any operation will bootstrap the user. This is a defensive pattern common in SDI implementations where you don't want operations to fail just because setup was skipped.

Separate input/storage representations: Email objects go in, plain dicts come out. The storeemail method handles the conversion. This decouples the send API from the storage/query format.

Thread assignment by reply chain: assignthread uses inreplyto to find an existing thread. If the replied-to message has a thread, the new message joins it. Otherwise, a new thread is created with threadid = msgid (the first message's ID becomes the thread ID). This is a simplified version of how Gmail threading works.

Cursor-based pagination: list_folder uses integer offsets serialized as strings. This is a teaching simplification — real systems use opaque cursors to avoid issues with concurrent inserts shifting offsets.

Dependencies

Minimal — only stdlib:

Imported by two test files (testemail.py, testemail_service.py), meaning this module is tested from two angles, likely unit vs. integration or different test authors.

Flow

Send path: send()storeemail() (Email → dict) → assignthread() (thread management) → addto_folder() for sender's "sent" + each recipient's "inbox". BCC recipients get the message in their inbox but BCC addresses are stored in the email dict — a real system would strip them from the stored record for non-sender views.

Draft path: savedraft() stores both the Email object and a dict representation → updatedraft() mutates both in place → send_draft() pops from draft stores, removes from drafts folder, calls send() for normal delivery.

Read path: listfolder() returns paginated email dicts. getemail() returns a single email and marks it read. get_thread() returns all emails in a thread.

Delete path: delete() checks if message is already in trash. If yes, removes permanently. If no, calls movetofolder(user, message_id, "trash").

Invariants

1. Every sent message gets a UUID and UTC timestampsend() always generates both before any storage.

2. Sender's copy is auto-marked read — in send(), the sender's message ID is added to their read_status immediately.

3. Thread ID equals the first message's ID — when no inreplyto exists or the replied-to message has no thread, threadid = msgid.

4. movetofolder removes from exactly one source folder — it iterates all folders and breaks on the first match, so a message can only exist in one folder per user.

5. Two-phase delete — first delete moves to trash, second delete from trash permanently removes. There's no "empty trash" bulk operation.

6. get_email always marks read — there's no way to retrieve an email without the read side-effect.

Error Handling

Minimal and deliberate:

Topics to Explore

Beliefs