A production-grade email system built using Node.js, featuring complete SMTP infrastructure, custom DNS resolution, security protocols, and real-time notifications. This project demonstrates deep understanding of email protocols, distributed systems architecture, and asynchronous processing.
- Multi-domain support - Send and receive emails across own domain and external domains
- Optimized delivery - Emails to self processed directly by backend; outbound worker stores emails directly in recipient mailboxes for local domain users, delivers to recipents SMTP server for external domains
- Email threading - Automatic conversation detection for replies
- Attachment handling - Upload and receive file attachments
- Bounce management - Automatic bounce emails for undeliverable messages
- Delivery reliability - 3 retry attempt mechanism for temporary failures
- Real-time notifications - Instant new emails sent to frontend via Server-Sent Events (SSE)
- Search - Quickly find emails across your mailbox
- Email management - Star, read/unread, delete, trash, and restore
- Smart composition - Auto-suggestions for frequently contacted recipients
- Reply & Forward - Full conversation threading support
- Cursor-based pagination - Efficient loading for large mailboxes
- Modern UI - Responsive design with dark mode support
The system follows a distributed, event-driven architecture with asynchronous communication for fault tolerance and scalability.
┌─────────────────────┐
│ Nginx │
│ • Serves Frontend │
│ • Proxies API calls │
│ • Rate Limiting │
└──────────┬──────────┘
│
Store ┌──────────▼──────────┐
┌─────────────────────┤ Backend (Fastify) |
│ │ • Business Logic │
│ │ • Server Sent Events│
│ └──────┬───────▲──────┘
│ │ │
│ Enqueue │ │ Subscribe
│ outbound | │ to events
│ │ |
┌──────▼─────────────┐ ┌──────▼───────────┐
│ MongoDB │ │ Redis + BullMQ│
│ • User mailboxes │ ┌───►│ • Job Queues │
│ • Email metadata │ | │ • Pub/Sub Events │──────┐
│ • Thread │ | └──────┬───────▲───┘ │
│ • Attachments │ | │ Publish Event │
└──────▲───────▲─────┘ | ┌──────▼─────┐ │ ┌────────▼────────┐
│ │ | │ Inbound │ │ │ Outbound │
│ │ | │ Queue │ │ │ Queue │
│ │ | └──────┬─────┘ │ └────────┬────────┘
│ │ | │ │ │
│ │ | ┌──────▼─────┐_│ ┌────────▼────────────┐
│ │ Store | │ Inbound │ └─┤ Outbound Worker │
│ └────────|────┤ Worker │ │ • Internal Delivery |
│ | │ • Process │ │ • MX resolution │
| | | • Store │ │ • Delivery │
| | │ • Notify │ | • Retry │
| Store | └────────────┘ └────┬────┬──┬────────┘
└────────────────|──────────────────────────┘ │ | Deliver
| │ | ┌───────────────┐
Enqueue | ┌────────────┐ Resolve MX | │ | Remote │
inbound | | SMTP Server│ | └───►| Mail Transfer │
| │ • Port 25 │ │ │ Agent(MTA) │
└────│ • Validate │Inbound Mail │ └─┬────┬────────┘
│ • Greylist |◄────────────|─────────┘ │
└──────┬─────┘ │ │
│ │ │
┌──────▼───────────────────▼──────────────▼─┐
│ Custom DNS Server │
│ • MX records / SPF / DKIM / DMARC │
│ • Resolves all type of DNS request │
└───────────────────────────────────────────┘
Incoming Email (External Domain):
- Remote MTA → Custom SMTP Server (Port 25)
- SMTP checks if the IP is greylisted, validates email security (SPF, DKIM, DMARC via DNS Server)
- SMTP adds email to Inbound Queue (BullMQ)
- Inbound Worker picks job: processes attachments, detects threads, saves to MongoDB
- Inbound Worker publishes new mail arrival event via Redis Pub/Sub
- Backend receives event and pushes SSE notification to recipient's browser
Outgoing Email (External Domain):
- Frontend sends email data to Backend via REST API
- Backend immediately stores in sender's mailbox (MongoDB)
- Backend adds job to Outbound Queue and responds to frontend (non-blocking)
- Outbound Worker picks job from queue
- Worker resolves recipient's MX record via DNS Server
- Worker sends email via Nodemailer to recipent's SMTP server.
- On failure: retries up to 3 times, generates bounce email if permanently failed
- Worker publishes delivery failure event via Redis Pub/Sub
- Backend receives event and sends delivery failure mail to frontend via SSE
Email within your domain(Different Users):
- Frontend sends email data to Backend
- Backend stores in sender's mailbox (MongoDB)
- Backend adds the job to Outbound Queue
- Outbound Worker picks job and saves to recipient's mailbox.
- Generates bounce mail if some recipient's doesn't exist.
- Worker publishes event via Redis Pub/Sub
- Backend receives event and pushes new mails to users using SSE
Email to Self (Same User):
- Frontend sends email data to Backend
- Backend directly stores in user's mailbox (MongoDB) - no queue needed
- Backend immediately pushes new mail to frontend via SSE
| Component | Technology |
|---|---|
| Frontend | React |
| Backend | Node.js, Fastify |
| SMTP Server | Node.js |
| Workers | Node.js (Inbound & Outbound) |
| Queue System | BullMQ + Redis |
| Database | MongoDB |
| Pub/Sub | Redis |
| Web Server | Nginx (serves frontend + reverse proxy to backend) |
| DNS Resolution | Custom Node.js based DNS Server |
| Containerization | Docker + Docker Compose |
- Email Authentication - SPF, DKIM, and DMARC verification
- Greylisting - Temporary rejection senders with multiple failed Email Authentication to prevent spam
- Rate Limiting - Multiple layers at Nginx, Backend and SMTP levels
- Private Backend - Backend isolated on private network, accessible only via Nginx
- Input Validation - Comprehensive validation of email headers and content
- Docker & Docker Compose
- Custom DNS Server (https://github.com/neerajann/dns-server) - Only required for external domain emails. Emails between users within your domain work without DNS server.
-
Clone the repository
git clone https://github.com/neerajann/email-system.git cd email-system -
Configure environment
cp .env.example .env
# Edit .env with your configuration
# Required: Configure backend and frontend
cd backend && cp .env.example .env
cd ../frontend && cp .env.example .env
# Optional: Other components use shared .env by default
# In development, you may need to configure individual component .env files-
Start all services
docker compose up -d
-
Verify DNS server (Optional - for external domains only)
- You can skip this step for emails that belong to your domain
- For sending/receiving external domain emails:
- Ensure your custom DNS server is accessible
- Verify MX record resolution is working
-
Access the application
- Frontend:
http://localhost(or configured domain) - SMTP Server: Port 25
- Backend API: Proxied through Nginx
- Frontend:
email-system/
├── backend/ # Fastify API server
│ ├── src/
│ ├── .env.example
│ ├── Dockerfile
│ └── server.js # Entry point
│
├── core/ # Shared utilities (used by backend, smtp-server, workers)
│
├── frontend/ # React application
│ ├── public/
│ ├── src/
│ │ └── main.jsx # Entry point
│ ├── nginx.conf
│ ├── .env.example
│ ├── Dockerfile
│ └── index.html # HTML entry point
│
├── inbound-worker/ # Incoming email processor
│ ├── attachments/
│ ├── handlers/
│ ├── threading/
│ ├── .env.example
│ ├── Dockerfile
│ └── index.js # Entry point
│
├── outbound-worker/ # Outgoing email processor
│ ├── assembly/
│ ├── storage/
| ├── transport/
│ ├── .env.example
│ ├── Dockerfile
│ └── index.js # Entry point
│
├── smtp-server/ # Custom SMTP server
│ ├── config/
│ ├── .env.example
│ ├── Dockerfile
│ └── server.js # Entry point
│
├── compose.yaml # Service orchestration
└── README.md
- Custom SMTP Implementation - Full-featured SMTP server built using Node.js
- Custom DNS Server Integration - Full control over domain resolution, MX records, and email security verification
- Internal vs External Handling - Emails to domain owned by the system are processed directly; external domains sent via nodemailer.
- Event-Driven Architecture - Redis Pub/Sub for real-time inter-service communication
- Asynchronous Processing - BullMQ job queues with retry logic and failure handling
- Real-Time Updates - Server-Sent Events (SSE) pushes new emails to frontend in real-time
- Email Threading - Automatic conversation detection and grouping
- Production-Ready Architecture - Fault-tolerant, scalable, and containerized
For testing and demonstration purposes, a multi-server setup was created using Docker Macvlan networking, allowing two complete mail server replicas with different domain names to run on the same port (25). This setup is not included in the repository but was used to validate:
- Internal domain handling
- External domain delivery
- Cross-domain communication
- Bounce and retry mechanisms
- Security protocol validation
Nirajan Paudel
- LinkedIn: www.linkedin.com/in/nirajan-paudel-a9b052265
- GitHub: https://github.com/neerajann