A self-hosted, full-featured email platform with React frontend, Express backend, custom SMTP server, and real-time notifications
Table of Contents
- Project Overview
- The Problem It Solves
- Key Features
- Architecture
- Technology Stack
- System Components
- Email Flow
- Deployment
- Security Implementation
- Interview Q&A
Project Overview
NovaEmail is a complete, self-hosted email platform you can deploy on your own server. Instead of relying on Gmail or Outlook, you host a full-featured email system with your own domain.
It’s like building your own Gmail alternative with:
- Modern, responsive React web interface
- Express.js RESTful API backend
- Custom Node.js SMTP server for receiving emails
- MongoDB database for scalability
- SendGrid integration for reliable external email delivery
- Real-time WebSocket notifications
- Full email functionality (send, receive, search, organize)
Quick Stats
- Architecture: Full-Stack (Frontend + Backend + SMTP + Infrastructure)
- Language: JavaScript (Node.js)
- Frontend: React 18 + TailwindCSS
- Backend: Express.js
- Database: MongoDB
- Infrastructure: Nginx + PM2 + Let’s Encrypt SSL
- Hosting: Cloud (Azure Ubuntu Server)
- Domain: novaemail.me
The Problem It Solves
Why Build Your Own Email Platform?
Problem 1: Privacy Concerns
❌ Gmail/Outlook:
├─ Your emails stored on their servers
├─ They can scan content for ads
├─ Data breaches affect millions
└─ Government can request data
✅ NovaEmail:
├─ Your emails on YOUR server
├─ You control access completely
├─ Only you have encryption keys
└─ Complete privacy over your data
Problem 2: No Control
❌ Gmail:
├─ Can't modify features
├─ Can't control API
├─ Limited to their UI
└─ Dependent on their ToS
✅ NovaEmail:
├─ Full source code control
├─ Build custom features
├─ Modify UI exactly as needed
└─ Your rules, not theirs
Problem 3: Cost
❌ Enterprise Email Solutions:
├─ $20-30 per user per month
├─ 100 users = $2,400/month
└─ Additional features = additional cost
✅ NovaEmail:
├─ Single server = $5-20/month
├─ 1,000 users possible
├─ All features included
└─ One-time setup cost
Problem 4: Lock-in
❌ Switching from Gmail is painful:
├─ Exporting all emails takes hours
├─ Account recovery complex
├─ 3rd party tools required
└─ Data portability issues
✅ NovaEmail:
├─ Full data access (MongoDB)
├─ Standard formats
├─ Easy backup/restore
└─ Complete data ownership
Real-World Use Cases
-
Organizations Needing Privacy
- Law firms (client confidentiality)
- Healthcare (HIPAA compliance)
- Financial services (data security)
-
Startups with Custom Needs
- Integration with existing systems
- Custom workflows
- Branded experience
-
Government/Military
- Air-gapped deployments
- Complete control over infrastructure
- No 3rd party dependencies
Key Features
Email Functionality
Sending Emails
User composes in browser
↓
Attaches file (up to 25MB)
↓
Adds multiple recipients (chips UI)
↓
Sends via SendGrid SMTP
↓
External email delivered
- ✉️ Send to internal users (same domain)
- ✉️ Send to external addresses (Gmail, Outlook, etc.)
- 👥 Multiple recipients with Gmail-style chips
- 📎 Attachments (up to 25MB)
- 📝 Auto-save drafts
- ✅ Delivery confirmation
Receiving Emails
External sender (Gmail, etc.)
↓
Sends to your server (port 25)
↓
Custom SMTP server receives
↓
Parses email + attachments
↓
Scans with ClamAV (optional)
↓
Stores in MongoDB
↓
WebSocket notifies browser
↓
User sees notification
- 📧 Receive from any external email
- 🛡️ Virus scanning on attachments
- 📦 Automatic attachment storage
- ⏱️ Real-time notifications
- ✅ Delivery verification
Email Organization
Inbox (primary)
├─ Unread messages
├─ Auto-organized by date
└─ Real-time updates
Sent (outgoing)
├─ All emails sent by user
└─ Includes drafts that were sent
Drafts (in progress)
├─ Unsent messages
├─ Auto-saved every 30 seconds
└─ Auto-delete if empty for 7 days
Archive (kept but out of way)
├─ Intentionally archived emails
├─ Searchable
└─ Not deleted (recoverable)
Trash (deleted)
├─ 30-day auto-delete
├─ Can be recovered
└─ Free up space
Email Search
Search across:
- Subject line
- Sender/Recipient
- Email body content
- Attachment filenames
Example: "invoice from john 2024"
Result: All emails matching criteria
Email Management
- 🌟 Star important emails
- ✅ Mark as read/unread
- 📂 Move between folders
- 🔄 Bulk operations (select all, delete multiple)
- 🗑️ Soft delete (recoverable)
- 🔒 Secure deletion option
User Interface Features
Modern Design
- 🎨 Dark theme (easy on eyes, professional)
- 📱 Mobile responsive (works on all devices)
- ⚡ Snappy performance (React optimization)
- 🎯 Intuitive (Gmail-like familiar UX)
User Experience
- 🖱️ Right-click context menu
- Mark as read/unread
- Star/unstar
- Archive
- Delete
- ✅ Bulk selection (checkboxes)
- 🔄 Keyboard shortcuts
- 🏃 Smooth animations
Real-Time Features
- 📬 Notification when new email arrives
- 🔔 Unread count badge
- ⚡ Live update without refresh
- 💬 WebSocket connection stays open
Security Features
Authentication
Registration:
├─ Username (unique)
├─ Password (min 8 chars, bcrypt hash)
├─ Email (verified)
├─ Profile (name, DOB)
└─ Account created
Login:
├─ Username + Password
├─ Rate-limited (50 attempts per 15 min)
├─ JWT token issued
├─ Token stored in browser (secure cookie)
└─ Token used for API calls
Password Security
- 🔒 bcrypt hashing (not reversible)
- 🔐 Salt rounds: 10 (2^10 iterations)
- 🚫 No password storage in logs
- 🔄 Password reset via email
- 📋 Password strength indicator on registration
Communication Security
- 🔐 HTTPS (SSL certificate, auto-renewed)
- 🔒 Email encryption with SPF/DKIM/DMARC
- 🛡️ Helmet.js security headers
- XSS Protection
- CSRF Protection
- Clickjacking Protection
- 🔐 JWT tokens with expiration
Data Security
- 🗄️ MongoDB password protected
- 🔑 API keys stored securely (environment variables)
- ✅ Input validation on all endpoints
- 🚫 SQL injection prevention
- 🛡️ Command injection prevention
Email Authentication
SPF (Sender Policy Framework):
├─ Tells receivers which servers can send email for your domain
├─ Prevents spoofing
└─ Record: v=spf1 ip4:YOUR_IP mx ~all
DKIM (DomainKeys Identified Mail):
├─ Digitally signs emails
├─ Receiver verifies signature
├─ Records from SendGrid configuration
└─ Proves email wasn't tampered
DMARC (Domain-based Message Authentication):
├─ Policy for how to handle failures
├─ Collects reporting from receivers
├─ Record: v=DMARC1; p=none; rua=mailto:admin@novaemail.me
└─ Monitor spoofing attempts
Optional Virus Scanning
ClamAV Integration:
├─ Scans attachment before storage
├─ Detects viruses/malware
├─ Quarantine infected files
└─ Alert administrator
Architecture
High-Level System Design
┌─────────────────────────────────────────────────────────┐
│ User's Browser │
│ (React 18 + TailwindCSS Frontend) │
│ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ Compose │ Inbox │ Archive │ Trash │ Settings│ │
│ │────────────────────────────────────────────────│ │
│ │ Email List │ Email Content │ │
│ │ Real-time │ Attachment View │ │
│ │ Updates │ Reply Interface │ │
│ └─────────────────────────────────────────────────┘ │
└────────────────────┬──────────────────────────────────┘
│ HTTPS + WebSocket
┌────────────▼─────────────┐
│ Nginx Reverse Proxy │
│ (Port 80/443, Load Bal) │
└────────────┬─────────────┘
│
┌────────────────┼────────────────┐
│ │ │
│ ┌───────▼────────┐ │ ┌──────────────┐
│ │ Express API │ │ │ SMTP Server │
│ │ (Port 5000) │ │ │ (Port 25) │
│ │ │ │ │ │
│ │ Routes: │ │ │ Listens for │
│ │ /api/send │ │ │ incoming │
│ │ /api/emails │ │ │ emails │
│ │ /api/auth │ │ │ │
│ │ /api/register │ │ │ MailParser │
│ │ /api/login │ │ │ ClamAV │
│ │ /api/upload │ │ │ MongoDB │
│ └───────┬────────┘ │ └──────┬───────┘
│ │ │ │
└────────────────┼────────────────┼───────────────┘
│ │
┌──────▼────────────────▼──────┐
│ MongoDB Database │
│ (Collections) │
│ ├─ users │
│ ├─ emails │
│ ├─ attachments │
│ └─ sessions │
└──────────────────────────────┘
│
│
┌────────────▼───────────┐
│ SendGrid API │
│ (External email) │
│ (For outgoing emails) │
└────────────────────────┘
Component Breakdown
Frontend (React)
App.js (Main Component)
├── Auth Module
│ ├── Register Component
│ ├── Login Component
│ └── Profile Component
│
├── Email Module
│ ├── Inbox View
│ ├── Email Reader
│ ├── Compose Window
│ └── Email List
│
├── Folder Navigation
│ ├── Inbox
│ ├── Sent
│ ├── Drafts
│ ├── Archive
│ └── Trash
│
├── Features
│ ├── Search
│ ├── Star/Unstar
│ ├── Mark Read/Unread
│ ├── Move to Folder
│ └── Delete
│
└── Real-time Updates (WebSocket)
└── New email notifications
Backend (Express)
server.js (Main Server)
├── Authentication
│ ├── POST /api/register (create account)
│ ├── POST /api/login (get JWT token)
│ └── Middleware: verify JWT
│
├── Email Operations
│ ├── GET /api/emails (list emails)
│ ├── GET /api/emails/:id (read single)
│ ├── POST /api/send (send new)
│ ├── POST /api/drafts (save draft)
│ ├── PUT /api/emails/:id/read (mark read)
│ ├── PUT /api/emails/:id/star (star email)
│ ├── PUT /api/emails/:id/move (move folder)
│ ├── DELETE /api/emails/:id (delete)
│ └── GET /api/stats (statistics)
│
├── File Upload
│ ├── POST /api/upload (attach file)
│ ├── Storage: /uploads/attachments/
│ └── Max size: 25MB
│
├── WebSocket
│ └── Socket.io for real-time notifications
│
├── Rate Limiting
│ ├── API: 100 req/15 min
│ └── Login: 50 attempts/15 min
│
└── Health & Monitoring
└── GET /api/health (server status)
SMTP Server (Custom Node.js)
smtp-server.js (Independent Process)
├── Listens on Port 25
│
├── Incoming Email Flow
│ ├── Receives SMTP connection
│ ├── Accepts email for your domain
│ ├── MailParser extracts:
│ │ ├─ From (sender)
│ │ ├─ To (recipient)
│ │ ├─ Subject
│ │ ├─ Body
│ │ └─ Attachments
│ │
│ ├── Optional: ClamAV scan
│ │ ├─ Check for viruses
│ │ └─ Quarantine if infected
│ │
│ ├── Store in MongoDB
│ │ ├─ Create email record
│ │ ├─ Store metadata
│ │ └─ Link to recipient
│ │
│ └── Notify via WebSocket
│ └─ User sees notification
│
└── Process Management: PM2
└─ Auto-restart on crash
Technology Stack
Frontend
// React 18 - UI Framework
import React from 'react'
// TailwindCSS - Utility-first styling
<div className="flex gap-4 p-6 dark:bg-gray-900">
// Lucide React - Icon library
import { Mail, Send, Trash } from 'lucide-react'
// Fetch API - HTTP requests (no axios dependency)
const response = await fetch('/api/emails')
// WebSocket - Real-time updates
const socket = io(SERVER_URL)
socket.on('new-email', handleNewEmail)
Backend
// Node.js + Express - Server framework
const express = require('express')
const app = express()
// MongoDB + Mongoose - Database & ODM
const mongoose = require('mongoose')
const emailSchema = new Schema({
from: String,
to: [String],
subject: String,
body: String,
attachments: [String],
sentAt: Date
})
// JWT - Authentication tokens
const jwt = require('jsonwebtoken')
const token = jwt.sign({ userId }, JWT_SECRET, { expiresIn: '7d' })
// bcrypt - Password hashing
const hashedPassword = await bcrypt.hash(password, 10)
// Multer - File uploads
const upload = multer({ storage, limits: { fileSize: 25MB } })
// Nodemailer - Email sending
const transporter = nodemailer.createTransport(sendgridConfig)
await transporter.sendMail({ from, to, subject, html })
// Socket.io - Real-time WebSocket
const io = require('socket.io')(server)
io.on('connection', (socket) => { /* ... */ })
// Helmet - Security headers
const helmet = require('helmet')
app.use(helmet())
// Rate limiting
const rateLimit = require('express-rate-limit')
const limiter = rateLimit({ windowMs: 15 * 60 * 1000, max: 100 })
SMTP Server
// Node Mailer SMTP Server - Listen for incoming
const SMTPServer = require('smtp-server').SMTPServer
const server = new SMTPServer({
disableReverseLookup: true,
onData: async (stream, session) => {
// Parse incoming email
}
})
server.listen(25)
// MailParser - Parse email structure
const simpleParser = require('mailparser').simpleParser
const parsed = await simpleParser(stream)
Infrastructure
# Nginx - Reverse proxy
upstream api_backend {
server 127.0.0.1:5000;
}
server {
listen 443 ssl http2;
server_name novaemail.me;
location / {
proxy_pass http://api_backend;
}
}
# PM2 - Process manager
pm2 start server.js --name novaemail-backend
pm2 start smtp-server.js --name novaemail-smtp
pm2 save
# Let's Encrypt - SSL certificates
certbot --nginx -d novaemail.me -d www.novaemail.me
# Firewall
ufw allow 22 (SSH)
ufw allow 80 (HTTP)
ufw allow 443 (HTTPS)
ufw allow 25 (SMTP)
System Components
1. Frontend (React Application)
Main Components:
- AuthPage: Login/Register forms with validation
- MainApp: Email interface after login
- EmailList: Inbox with filters and search
- EmailReader: Single email view with reply
- Composer: Compose new email with attachments
- FolderNav: Switch between Inbox, Sent, Drafts, Archive, Trash
Features:
- Real-time email list updates
- Drag-and-drop file upload
- Keyboard shortcuts
- Auto-save drafts every 30 seconds
- Search across emails
- Responsive mobile view
Performance Optimizations:
- React lazy loading for components
- Virtual scrolling for large email lists
- Debounced search
- Cached API responses
2. Backend (Express API)
Authentication Layer:
// Middleware verifies JWT on each request
router.use((req, res, next) => {
const token = req.headers.authorization
try {
req.user = jwt.verify(token, JWT_SECRET)
next()
} catch (err) {
res.status(401).json({ error: 'Invalid token' })
}
})
// Routes require authentication
router.get('/api/emails', authenticate, getEmails)
Core Routes:
/api/auth/*- User authentication/api/emails/*- Email operations/api/upload- File attachment upload/api/stats- User statistics/api/health- Server status
Database Models (Mongoose):
// User schema
userSchema = {
username: String (unique),
passwordHash: String,
email: String,
profile: {
firstName: String,
lastName: String,
dateOfBirth: Date
}
}
// Email schema
emailSchema = {
userId: ObjectId (indexed),
from: String,
to: [String],
subject: String,
body: String (HTML),
attachments: [String],
folder: String (inbox/sent/drafts/archive/trash),
isRead: Boolean,
isStarred: Boolean,
sentAt: Date (indexed),
createdAt: Date
}
// Attachment schema
attachmentSchema = {
emailId: ObjectId,
fileName: String,
mimeType: String,
size: Number,
storagePath: String,
uploadedAt: Date
}
Indexes for Performance:
// Compound index: userId + folder + date
emailSchema.index({ userId: 1, folder: 1, sentAt: -1 })
// Unique email addresses per user
userSchema.index({ email: 1 }, { unique: true })
// Fast lookups by userId
emailSchema.index({ userId: 1 })
3. SMTP Server (Custom Implementation)
How It Works:
SMTP Connection from external sender
↓
Validate domain matches our server
↓
Accept MAIL FROM command
↓
Accept RCPT TO (if recipient exists)
↓
Receive DATA (email content)
↓
Parse with MailParser
├─ Extract From
├─ Extract To
├─ Extract Subject
├─ Extract Body (HTML + Text)
└─ Extract Attachments
↓
Optional: ClamAV virus scan
├─ If infected → Quarantine
└─ If clean → Continue
↓
Store in MongoDB
├─ Create email document
├─ Save attachments
├─ Set folder: inbox
└─ Set isRead: false
↓
Notify via WebSocket
├─ Connect to Socket.io
├─ Find user by recipient
└─ Send notification
↓
Return SMTP 250 OK (accepted)
Port 25 Configuration:
- Listens on all interfaces (0.0.0.0)
- Accepts connections for your domain
- Rejects external relaying (security)
- Processes emails concurrently
Email Flow
Sending Email (User → External)
Step 1: User composes in browser
│
├─ Fill fields:
│ ├─ To: (can add multiple)
│ ├─ Subject:
│ ├─ Body: (HTML editor)
│ └─ Attach: (files)
│
└─ Frontend validates
├─ Check To field not empty
├─ Check at least 1 recipient
└─ Check Subject not empty
Step 2: Submit to backend
│
├─ POST /api/send
│ ├─ Headers: Authorization: Bearer JWT_TOKEN
│ ├─ Body: {
│ │ to: ['user@gmail.com'],
│ │ subject: 'Hello',
│ │ body: '<p>Email body</p>',
│ │ attachments: ['id1', 'id2']
│ │ }
│ └─ Verify JWT token
│
└─ Backend processes
├─ Validate recipient emails
├─ Fetch attachments from storage
├─ Prepare email object
└─ Send via SendGrid
Step 3: SendGrid SMTP relay
│
├─ SendGrid receives email
├─ Validates domain reputation
├─ Adds DKIM signature
├─ Adds SPF headers
└─ Sends to recipient's mail server
Step 4: Recipient receives
│
├─ Email arrives at Gmail/Outlook
├─ Their server verifies:
│ ├─ SPF check (passes)
│ ├─ DKIM signature (passes)
│ └─ DMARC policy (passes)
└─ Email delivered to inbox
Step 5: Store in MongoDB
│
├─ Create email record:
│ ├─ from: novaemail.me
│ ├─ to: ['user@gmail.com']
│ ├─ status: 'sent'
│ ├─ folder: 'sent'
│ ├─ sentAt: timestamp
│ └─ messageId: unique_id
│
└─ User sees in Sent folder
Step 6: Return confirmation
│
└─ Frontend shows "Email sent"
Receiving Email (External → User)
Step 1: External sender sends
│
├─ User at Gmail composes
├─ Recipient: test@novaemail.me
├─ Sends to Gmail SMTP
└─ Gmail forwards to novaemail.me
Step 2: Gmail SMTP Server
│
├─ DNS lookup: novaemail.me
├─ Gets MX record: mail.novaemail.me
├─ Connects to our server:25
└─ Sends SMTP DATA
Step 3: Our SMTP Server receives
│
├─ SMTPServer listens on port 25
├─ Accepts MAIL FROM: sender@gmail.com
├─ Accepts RCPT TO: test@novaemail.me
├─ Accepts DATA: email content
└─ Parses with MailParser:
│ ├─ from: sender@gmail.com
│ ├─ to: test@novaemail.me
│ ├─ subject: 'Hello'
│ ├─ body: '<p>Email content</p>'
│ └─ attachments: [...]
│
└─ Returns 250 OK (accepted)
Step 4: Optional virus scan
│
├─ ClamAV scans attachments
│ ├─ If clean → Continue
│ └─ If infected → Quarantine & alert
│
└─ Process continues
Step 5: Store in MongoDB
│
├─ Create email document:
│ ├─ from: sender@gmail.com
│ ├─ to: test@novaemail.me
│ ├─ subject: 'Hello'
│ ├─ body: HTML content
│ ├─ attachments: [file_refs]
│ ├─ folder: 'inbox'
│ ├─ isRead: false
│ ├─ isStarred: false
│ ├─ receivedAt: timestamp
│ └─ messageId: unique_id
│
└─ Document linked to user by email
Step 6: Real-time notification
│
├─ Connect to Socket.io
├─ Find logged-in user sessions
├─ Emit 'new-email' event:
│ ├─ from: sender@gmail.com
│ ├─ subject: 'Hello'
│ └─ preview: 'Email content...'
│
└─ Frontend shows notification
Step 7: User sees in browser
│
├─ Browser receives WebSocket message
├─ Notification pops up
├─ Inbox list refreshes
└─ New email appears at top
Deployment
Prerequisites
✓ Ubuntu 20.04 or later
✓ Node.js 18+
✓ MongoDB Atlas account (free tier)
✓ SendGrid account (100 emails/day free)
✓ Domain name (registered)
✓ Server ports open (22, 25, 80, 443)
Deployment Steps
Step 1: Get server access
ssh ubuntu@YOUR_SERVER_IP
Step 2: Clone code
git clone https://github.com/YOUR_REPO/novaemail
cd novaemail
Step 3: Install dependencies
# Backend
cd backend
npm install
# Frontend
cd ../frontend
npm install
npm run build # Creates production build
# SMTP
cd ../smtp-server
npm install
Step 4: Configure environment
# Create .env file
cat > backend/.env << EOF
PORT=5000
NODE_ENV=production
DOMAIN=novaemail.me
JWT_SECRET=$(openssl rand -base64 32)
MONGODB_URI=mongodb+srv://user:pass@cluster.mongodb.net/novaemail
FRONTEND_URL=https://novaemail.me
SENDGRID_API_KEY=SG.xxxxxxxxxxxxx
EOF
Step 5: Start services with PM2
# Backend API
pm2 start backend/server.js --name novaemail-backend
# SMTP Server
pm2 start smtp-server/smtp-server.js --name novaemail-smtp
# Save PM2 config
pm2 save
pm2 startup
Step 6: Configure Nginx
# Copy config
sudo cp nginx/novaemail.conf /etc/nginx/sites-available/
# Enable site
sudo ln -s /etc/nginx/sites-available/novaemail /etc/nginx/sites-enabled/
# Remove default
sudo rm /etc/nginx/sites-enabled/default
# Test & restart
sudo nginx -t
sudo systemctl restart nginx
Step 7: SSL Certificate
sudo apt-get install certbot python3-certbot-nginx
sudo certbot --nginx -d novaemail.me -d www.novaemail.me
Step 8: Configure DNS
A Record: novaemail.me → YOUR_SERVER_IP
MX Record: novaemail.me → mail.novaemail.me (priority 10)
SPF Record: novaemail.me → v=spf1 ip4:YOUR_IP mx ~all
DMARC Record: _dmarc.novaemail.me → v=DMARC1; p=none; rua=mailto:admin@novaemail.me
DKIM Records: (from SendGrid configuration)
Step 9: Firewall
sudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow 25/tcp
sudo ufw enable
Step 10: Verify
# Check services running
pm2 status
# Check logs
pm2 logs novaemail-backend
pm2 logs novaemail-smtp
# Test HTTP endpoint
curl http://localhost:5000/api/health
# Test SMTP
telnet localhost 25
Security Implementation
1. Password Security
Registration:
// User submits password
const password = req.body.password
// Validate strength
if (password.length < 8) throw new Error('Too short')
if (!/[A-Z]/.test(password)) throw new Error('Need uppercase')
if (!/[0-9]/.test(password)) throw new Error('Need number')
// Hash password (not reversible)
const salt = await bcrypt.genSalt(10) // 2^10 iterations
const hashedPassword = await bcrypt.hash(password, salt)
// Store hash (not password)
user.password = hashedPassword
await user.save()
Login:
// User submits password
const password = req.body.password
// Compare with stored hash
const match = await bcrypt.compare(password, user.password)
// No way to reverse hash - must do actual comparison
if (!match) throw new Error('Invalid password')
2. Authentication (JWT Tokens)
Generate Token:
// After successful login
const token = jwt.sign(
{ userId: user._id, email: user.email },
JWT_SECRET,
{ expiresIn: '7d' } // Token expires in 7 days
)
// Send to client
res.json({ token })
Use Token:
// Client sends token in headers
Authorization: Bearer eyJhbGc...
// Server verifies
const decoded = jwt.verify(token, JWT_SECRET)
req.user = decoded // Attach to request
// If invalid or expired → 401 Unauthorized
3. Rate Limiting
API Rate Limiting:
const apiLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // 100 requests per window
message: 'Too many requests, try again later'
})
app.use('/api/', apiLimiter)
Login Rate Limiting (stricter):
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 50, // Only 50 login attempts
skipSuccessfulRequests: true // Don't count successful logins
})
app.post('/api/login', loginLimiter, handleLogin)
4. Email Authentication
SPF (Sender Policy Framework):
DNS Record:
v=spf1 ip4:YOUR_SERVER_IP mx ~all
When Gmail receives email from your domain:
├─ Checks SPF record
├─ Verifies sending IP in list
├─ If match → SPF passes
└─ If no match → Possible spoofing
DKIM (DomainKeys Identified Mail):
Your server signs emails with private key
Recipient verifies signature with public key
├─ Signature can't be forged (cryptography)
├─ Email hasn't been modified
├─ Proves your server sent it
└─ Gmail marks as authenticated
DMARC (Domain-based Message Authentication, Reporting & Conformance):
Policy: What to do if SPF/DKIM fail
├─ p=none: Monitor only (don't block)
├─ p=quarantine: Send to spam
└─ p=reject: Block email completely
Reporting:
├─ Receivers send reports about authentication
├─ You can see SPF/DKIM failures
└─ Helps identify issues
5. HTTPS/TLS
Client sends request to novaemail.me:443
↓
Server presents SSL certificate (from Let's Encrypt)
↓
Client verifies certificate:
├─ Signed by trusted CA
├─ Matches domain
└─ Not expired
↓
TLS handshake completes
↓
All communication encrypted with AES-256
↓
No one can intercept requests/responses
(Even if they intercept packets, encrypted)
6. Input Validation
Email Field Validation:
// Validate email format
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
if (!emailRegex.test(email)) {
throw new Error('Invalid email format')
}
// Prevent NoSQL injection
if (typeof email !== 'string') {
throw new Error('Email must be string')
}
// Prevent XSS in email content
const sanitized = DOMPurify.sanitize(emailBody)
File Upload Validation:
// Check file size
if (file.size > 25 * 1024 * 1024) {
throw new Error('File too large (max 25MB)')
}
// Check file type
const allowedTypes = ['image/*', 'application/pdf', 'text/*']
if (!allowedTypes.includes(file.type)) {
throw new Error('File type not allowed')
}
// Store with random name (prevent directory traversal)
const filename = crypto.randomBytes(16).toString('hex') + '.bin'
7. Optional: ClamAV Virus Scanning
// When attachment uploaded
const { exec } = require('child_process')
// Scan file
exec(`clamscan ${filePath}`, (error, stdout) => {
if (error) {
// Virus detected
logger.error('Virus detected in file')
quarantineFile(filePath)
notifyAdmin()
} else {
// File clean
allowDownload(filePath)
}
})
Interview Q&A
Basic Questions
Q1: Why did you build this project?
A: I wanted to learn full-stack web development by building something real. Email systems are complex (SMTP, authentication, real-time updates, file uploads), so it was a great learning opportunity. Plus, there are legitimate use cases for self-hosted email - privacy-conscious organizations, teams needing integration with legacy systems, cost savings for large deployments.
Q2: What’s the most complex part of this project?
A: Building the SMTP server to receive emails. It’s not just receiving data - you need to:
- Implement the SMTP protocol correctly
- Parse complex email formats (MIME multipart, attachments, HTML)
- Handle different encodings
- Store efficiently in MongoDB
- Notify the browser in real-time with WebSocket
- Integrate optional ClamAV scanning
Most developers use Sendmail or Postfix (C-based servers). Doing it in Node.js required careful attention to the RFC 5321 spec.
Q3: How do you handle sending emails to external servers (like Gmail)?
A: I use SendGrid’s SMTP gateway:
- Backend connects to SendGrid SMTP server
- Provides SendGrid API key for authentication
- SendGrid handles actual delivery to Gmail/Outlook/etc
- SendGrid provides DKIM/SPF authentication
- Handles bounces and complaints
This outsources the hard parts - mail reputation, deliverability, authentication.
Q4: How do you ensure emails aren’t lost?
A: Three layers:
- Receipt Confirmation: SMTP server confirms email received
- Database Storage: Immediately saved to MongoDB (replicated)
- Duplicate Detection: messageId prevents storing same email twice
- Backups: MongoDB Atlas handles backups automatically
Q5: How do you prevent spam/abuse?
A: Multiple approaches:
- Rate Limiting: Max 100 API requests/15 min per user
- Login Throttling: Max 50 login attempts/15 min
- Email Authentication: SPF/DKIM/DMARC prevent spoofing
- ClamAV: Optional virus/malware scanning
- IP Reputation: SendGrid monitors sender reputation
- Bounce Handling: Remove invalid addresses
Q6: How many users can this support?
A: Depends on hardware:
Single Server (~5-20/month cost):
├─ Capacity: 5,000-10,000 users
├─ 100 MB/s bandwidth
├─ MongoDB Atlas free tier: 512MB → 5GB
└─ Nginx load balancer: 10,000+ concurrent
Scale to millions:
├─ Load balance frontend (multiple instances)
├─ Load balance backend (multiple API servers)
├─ Load balance SMTP (multiple SMTP servers)
├─ Scale MongoDB (sharding)
└─ Use CDN for static assets
The architecture supports scaling by adding more servers.
Technical Questions
Q7: How do you handle real-time email notifications?
A: WebSocket (Socket.io):
// Backend
io.on('connection', (socket) => {
socket.on('join-room', (userId) => {
socket.join(`user_${userId}`)
})
})
// When email arrives via SMTP
io.to(`user_${recipientId}`).emit('new-email', {
from: sender,
subject: subject,
preview: snippet
})
// Frontend
socket.on('new-email', (data) => {
// Show notification
showNotification(data)
// Refresh email list
fetchEmails()
})
Benefits over polling:
- Polling: Refresh every 30 seconds (30s latency, wasteful)
- WebSocket: Instant notification (< 100ms latency, efficient)
Q8: How do you design the email schema?
A: MongoDB schema with proper indexing:
{
_id: ObjectId,
userId: ObjectId, // Owner of email
from: 'sender@gmail.com',
to: ['recipient@novaemail.me'],
cc: [],
bcc: [],
subject: 'Hello',
body: '<p>Email HTML content</p>',
textBody: 'Email plain text', // For plain text clients
// Metadata
messageId: 'unique@sender', // For deduplication
folder: 'inbox', // inbox|sent|drafts|archive|trash
isRead: false,
isStarred: false,
// File references
attachments: [
{
originalName: 'invoice.pdf',
mimeType: 'application/pdf',
size: 1024000,
storagePath: '/uploads/abc123.bin',
uploadedAt: Date
}
],
// Timestamps
sentAt: Date,
receivedAt: Date,
createdAt: Date,
updatedAt: Date
}
// Indexes
db.emails.createIndex({ userId: 1, folder: 1, sentAt: -1 })
db.emails.createIndex({ userId: 1, isRead: 1 })
db.emails.createIndex({ messageId: 1 }, { unique: true })
Index strategy:
(userId, folder, sentAt)compound - Fast for inbox queries(userId, isRead)- Fast for unread countmessageIdunique - Prevent duplicates
Q9: How do you handle file uploads securely?
A: Multiple validations:
const upload = multer({
storage: diskStorage({
destination: (req, file, cb) => {
cb(null, '/uploads/attachments/')
},
filename: (req, file, cb) => {
// Use random name, not original filename
const name = crypto.randomBytes(16).toString('hex')
cb(null, name + '.bin')
}
}),
limits: {
fileSize: 25 * 1024 * 1024 // 25MB max
},
fileFilter: (req, file, cb) => {
// Allowed types
const allowed = ['image/*', 'application/pdf', 'text/*']
if (allowed.includes(file.mimetype)) {
cb(null, true)
} else {
cb(new Error('File type not allowed'))
}
}
})
// Use middleware
router.post('/api/upload', authenticate, upload.single('file'))
Security measures:
- Store with random name (not user input)
- Validate file size
- Validate MIME type
- Store outside web root (not directly accessible)
- Scan with ClamAV (optional)
Q10: How do you prevent XSS attacks in email content?
A:
// Option 1: DOMPurify (recommended)
const DOMPurify = require('isomorphic-dompurify')
const cleanEmail = DOMPurify.sanitize(emailBody, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'p', 'br', 'a'],
ALLOWED_ATTR: ['href', 'target'],
KEEP_CONTENT: true
})
// Option 2: Escape HTML
const escape = (html) => {
return html
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''')
}
// Frontend: Use innerHTML with caution
// UNSAFE: innerHTML = userInput
// SAFE: textContent = userInput (doesn't parse HTML)
// SAFE: dangerouslySetInnerHTML with sanitized content (React)
The goal: Allow safe HTML (links, formatting) but prevent <script> tags.
Deployment Questions
Q11: How do you handle SSL certificate renewal?
A: Certbot auto-renewal:
# Initial certificate
sudo certbot --nginx -d novaemail.me
# Certbot sets up auto-renewal
sudo systemctl enable certbot.timer
# Manual renewal (if needed)
sudo certbot renew
# Dry run (test without making changes)
sudo certbot renew --dry-run
# Monitor renewal
sudo systemctl list-timers certbot.timer
Certbot automatically:
- Checks 30 days before expiration
- Renews if expiring soon
- Reloads Nginx
- No downtime required
Q12: How do you monitor the running services?
A: PM2 monitoring:
# View status
pm2 status
# View logs
pm2 logs novaemail-backend
pm2 logs novaemail-smtp
# Monitor in real-time
pm2 monit
# Save memory/CPU snapshots
pm2 save
# Get diagnostics
pm2 diagnose
Monitor key metrics:
- CPU usage (should be <20% idle)
- Memory (should not increase over time)
- Restart count (should be 0)
- Error logs (check for repeating errors)
Problem-Solving Questions
Q13: An email gets stuck in “Drafts” and can’t be sent. How do you debug?
A: Systematic troubleshooting:
// 1. Check if email exists in database
db.emails.findOne({ _id: emailId })
// If not found → Database connection issue
// 2. Check email status
const email = await Email.findById(emailId)
console.log(email) // Check all fields
// Missing required fields → Frontend validation
// 3. Try sending via API directly
POST /api/send
{
to: ['test@gmail.com'],
subject: 'Test',
body: '<p>Test</p>'
}
// If fails → Backend/SendGrid issue
// 4. Check SendGrid API
curl https://api.sendgrid.com/v3/mail/send \
-X POST \
-H "Authorization: Bearer SENDGRID_KEY" \
-H "Content-Type: application/json" \
-d '{...}'
// Check response code
// 5. Review logs
pm2 logs novaemail-backend | grep error
// Check for exceptions
// 6. Check MongoDB connection
// If MongoDB down → Can't save/retrieve emails
// 7. Check SendGrid quota
// If over limit → Can't send more emails
Q14: Why is email taking 30 seconds to appear after I send it?
A: Multiple potential causes:
Sending takes 3 seconds
├─ API call overhead
├─ SendGrid SMTP handshake
└─ MongoDB write
Receiving takes 5-10 seconds
├─ External server's processing
├─ Network propagation
├─ DNS resolution (MX lookup)
└─ SMTP server queueing
Display takes 2-5 seconds
├─ Email list polling (30s interval)
├─ WebSocket broadcasting
├─ React re-render
Total: Up to 40+ seconds is normal
To optimize:
├─ Reduce polling interval (but more wasteful)
├─ Use WebSocket for real-time (already done)
├─ Use SendGrid webhooks for delivery confirmation
└─ Add optimistic updates in UI (show immediately)
Interview Story
Tell this when asked about your biggest project:
“I built NovaEmail - a complete self-hosted email platform. This taught me full-stack web development from database to deployment.
The Challenge: Email systems are complicated. You need:
- A web interface (React)
- An API (Express)
- A custom SMTP server to RECEIVE emails (most complex part)
- Database design (MongoDB)
- Real-time notifications (WebSocket)
- Security (authentication, encryption, email validation)
- Infrastructure (Nginx, SSL, firewall)
The Solution: I built each component step by step:
- Frontend: React UI with TailwindCSS
- Backend: Express API with JWT auth
- SMTP: Custom Node.js SMTP server (implementing RFC 5321)
- Infrastructure: Nginx reverse proxy + SSL + PM2 process management
Key Technical Decisions:
- Used Socket.io for real-time notifications instead of polling (much more efficient)
- Implemented compound indexes in MongoDB for fast queries
- Outsourced outgoing email to SendGrid (focus on receiving)
- Used bcrypt for password hashing (non-reversible)
- Implemented proper rate limiting on login
Results:
- Complete working email platform
- Supports multiple users
- Real-time notifications
- Secure authentication
- All features of a production email system
This project gave me deep understanding of how email actually works.”
Numbers to Remember
- Attachment limit: 25MB
- Rate limit API: 100 requests per 15 minutes
- Rate limit login: 50 attempts per 15 minutes
- JWT expiration: 7 days
- bcrypt salt rounds: 10 (2^10 iterations)
- Default polling interval: 30 seconds
- Auto-save drafts: Every 30 seconds
- Database storage: MongoDB Atlas (5GB free)
- Email delivery: SendGrid (100/day free)
- Supported users: 5,000-10,000 per single server
Good luck with your interview! 🚀