Skip to main content

Email Architecture

CAFH Platform uses a queue-based email system:
  1. Admin creates campaign → Emails added to queue
  2. Queue worker processes → Sends emails via SMTP
  3. Rate limiting → Respects hourly limits
  4. Logs and metrics → Track delivery and engagement

Server Implementation

From server.ts:
server.ts
import express from "express";
import nodemailer from "nodemailer";
import fs from "fs";

const app = express();
const DB_PATH = path.join(__dirname, "data.json");

// Email transporter
const transporter = nodemailer.createTransport({
  host: process.env.SMTP_HOST || "localhost",
  port: parseInt(process.env.SMTP_PORT || "465"),
  secure: process.env.SMTP_SECURE === "true",
  auth: {
    user: process.env.SMTP_USER,
    pass: process.env.SMTP_PASS,
  },
});

// Queue database
interface DB {
  queue: QueuedEmail[];
  sentCountThisHour: number;
  lastResetTime: string;
}

interface QueuedEmail {
  id: string;
  to: string;
  subject: string;
  content: string;
  status: 'pending' | 'sent' | 'failed';
  createdAt: string;
  sentAt?: string;
  error?: string;
}

API Endpoints

POST /api/email/queue

Add emails to the queue:
app.post("/api/email/queue", (req, res) => {
  const { recipients, subject, content } = req.body;
  
  const db = loadDB();
  const newEmails: QueuedEmail[] = recipients.map(email => ({
    id: Math.random().toString(36).substr(2, 9),
    to: email,
    subject,
    content,
    status: 'pending',
    createdAt: new Date().toISOString()
  }));

  db.queue.push(...newEmails);
  saveDB(db);

  res.json({ 
    message: `${newEmails.length} emails added to queue`,
    queueSize: db.queue.filter(e => e.status === 'pending').length
  });
});

GET /api/email/status

Get queue status:
app.get("/api/email/status", (req, res) => {
  const db = loadDB();
  const pending = db.queue.filter(e => e.status === 'pending').length;
  const sent = db.queue.filter(e => e.status === 'sent').length;
  const failed = db.queue.filter(e => e.status === 'failed').length;

  res.json({
    pending,
    sent,
    failed,
    sentCountThisHour: db.sentCountThisHour,
    limit: parseInt(process.env.MAX_EMAILS_PER_HOUR || "80")
  });
});

Queue Worker

Processes emails every 30 seconds:
server.ts
const processQueue = async () => {
  const db = loadDB();
  const now = new Date();
  const lastReset = new Date(db.lastResetTime);
  
  // Reset hourly counter if an hour has passed
  if (now.getTime() - lastReset.getTime() > 3600000) {
    db.sentCountThisHour = 0;
    db.lastResetTime = now.toISOString();
  }

  const limit = parseInt(process.env.MAX_EMAILS_PER_HOUR || "80");
  const availableSlots = limit - db.sentCountThisHour;

  if (availableSlots <= 0) {
    console.log("[Queue Worker] Hourly limit reached");
    saveDB(db);
    return;
  }

  const pendingEmails = db.queue
    .filter(e => e.status === 'pending')
    .slice(0, availableSlots);

  console.log(`[Queue Worker] Processing ${pendingEmails.length} emails...`);

  for (const email of pendingEmails) {
    try {
      await transporter.sendMail({
        from: process.env.SMTP_USER,
        to: email.to,
        subject: email.subject,
        html: email.content
      });
      
      email.status = 'sent';
      email.sentAt = new Date().toISOString();
      db.sentCountThisHour++;
    } catch (error: any) {
      console.error(`Failed to send to ${email.to}:`, error.message);
      email.status = 'failed';
      email.error = error.message;
    }
  }

  saveDB(db);
};

// Run every 30 seconds
setInterval(processQueue, 30000);

Rate Limiting

Controlled by MAX_EMAILS_PER_HOUR:
.env
MAX_EMAILS_PER_HOUR=80
The worker:
  • Tracks emails sent this hour
  • Resets counter after 60 minutes
  • Stops sending when limit reached
  • Resumes next hour automatically

SMTP Providers

Most web hosts provide cPanel email:
SMTP_HOST=mail.yourdomain.com
SMTP_PORT=465
SMTP_USER=[email protected]
SMTP_PASS=your_password
SMTP_SECURE=true
MAX_EMAILS_PER_HOUR=80
Typical limit: 80-100 emails/hour

Testing Email Locally

1

Start the server

npm run dev
2

Queue a test email

curl -X POST http://localhost:3000/api/email/queue \
  -H "Content-Type: application/json" \
  -d '{
    "recipients": ["[email protected]"],
    "subject": "Test Email",
    "content": "<h1>Hello</h1><p>This is a test.</p>"
  }'
3

Check queue status

curl http://localhost:3000/api/email/status
Response:
{
  "pending": 1,
  "sent": 0,
  "failed": 0,
  "sentCountThisHour": 0,
  "limit": 80
}
4

Wait for worker

The queue processes every 30 seconds. Check your inbox.

Monitoring

Check data.json for queue state:
data.json
{
  "queue": [
    {
      "id": "abc123",
      "to": "[email protected]",
      "subject": "Welcome to CAFH",
      "content": "<p>Welcome!</p>",
      "status": "sent",
      "createdAt": "2024-03-05T10:00:00Z",
      "sentAt": "2024-03-05T10:00:30Z"
    }
  ],
  "sentCountThisHour": 5,
  "lastResetTime": "2024-03-05T10:00:00Z"
}

Troubleshooting

  1. Check SMTP credentials
  2. Verify MAX_EMAILS_PER_HOUR not exceeded
  3. Check data.json for failed emails
  4. Review server logs for errors
  • Double-check username and password
  • For Gmail, use App Password
  • Verify SMTP host and port
  • Check firewall allows outbound SMTP
  • Ensure server is running (npm run dev)
  • Check console for worker logs:
    [Queue Worker] Processing 3 emails...
    
  • Verify data.json exists and is writable

Production Recommendations

Use a dedicated SMTP service

SendGrid, Mailgun, or AWS SES provide better deliverability and higher limits

Monitor bounce rates

Track failed deliveries and remove bounced emails from your list

Implement retry logic

Retry failed emails with exponential backoff

Add email templates

Create reusable HTML templates for campaigns

Next Steps

SMTP Configuration

Admin guide for SMTP setup

Email Campaigns

Creating and sending campaigns

Automation Workflows

Automated email sequences

Build docs developers (and LLMs) love