← Back to Modules

dbbasic-email Specification

Version: 1.0 Status: Specification Author: DBBasic Project Date: October 2025

Links: - PyPI: https://pypi.org/project/dbbasic-email/ - GitHub: https://github.com/askrobots/dbbasic-email - Specification: http://dbbasic.com/email-spec


Philosophy

"Email is Unix mail. Queuing is sendmail. Templates are heredocs."

Email is not a cloud service. Unix had local mail working in 1971. The web made it complicated. Let's return to simplicity.

Design Principles

  1. Unix Mail First: Local delivery to var/mail/{user}, just like Unix
  2. SMTP When Needed: External email via SMTP (like sendmail)
  3. Queue Integration: Uses dbbasic-queue for async delivery
  4. Plain Text First: Templates are just string formatting
  5. No Service Required: Works without external email service

The Email Fork: How We Got Here

1971-1990s: Unix Mail Works

Local mail (same system):

# Send mail to user on same system
echo "Your report is ready" | mail john

# John reads it
mail
# Shows inbox from /var/mail/john

sendmail (external email):

# Send to external address
echo "Hello" | mail john@example.com
# sendmail delivers via SMTP

How it worked: - Local mail: Append to /var/mail/{user} - External mail: sendmail queues and delivers via SMTP - Simple, integrated, worked

1990s-2000s: Web Apps Fork

The problem: Shared hosting again

Can't use system sendmail (shared server)
Can't write to /var/mail (permissions)
Need to send from web app, not Unix user

The "solution": Everyone reinvents email

WordPress: wp_mail() → PHPMailer → SMTP
Rails: ActionMailer → Net::SMTP
Django: django.core.mail → smtplib
Custom: Everyone builds their own

Also: Email became a service - SendGrid, Mailgun, AWS SES - API calls instead of SMTP - Pay per email - Vendor lock-in

2025: Docker + dbbasic = Return to Unix

Each container has its own mail system:

myapp/
├── var/mail/           # Local mail (user inboxes)
│   ├── john
│   └── jane
└── var/spool/mail/     # Outgoing queue (like sendmail)
    ├── pending/
    └── sent/

Local delivery (internal notifications):

# Send to user on same app
mail('john', 'New comment on your post')
# → Appends to var/mail/john

External delivery (SMTP):

# Send to external email
send_email('user@gmail.com', 'Welcome!', 'Thanks for signing up')
# → Queues in var/spool/mail/pending/
# → Worker delivers via SMTP

Back to Unix patterns.


Architecture

Two Types of Email

1. Internal Mail (Local Delivery)

User to user on same app
Like Unix mail command
Fast, no SMTP needed

2. External Mail (SMTP Delivery)

App to outside world
Requires SMTP server
Queued for async delivery

The Flow

┌─────────────────┐
│  send_email()   │
└────────┬────────┘
         │
    ┌────┴─────┐
    │ Local or │
    │ External?│
    └────┬─────┘
         │
    ┌────┴────────────────┐
    │                     │
┌───▼──────┐      ┌──────▼─────┐
│  Local   │      │  External  │
│          │      │            │
│ Append to│      │ Queue via  │
│ mailbox  │      │ dbbasic-   │
│          │      │ queue      │
└──────────┘      └──────┬─────┘
                         │
                    ┌────▼────┐
                    │  Worker │
                    │  sends  │
                    │  SMTP   │
                    └─────────┘

API Specification

Function: mail(username, subject, body)

Purpose: Send local mail to user (Unix mail)

Parameters: - username (str): Local username - subject (str): Email subject - body (str): Email body (plain text)

Returns: - message_id (str): Unique message identifier

Behavior: 1. Append to var/mail/{username} in mbox format 2. Return message_id 3. Instant delivery (no queue)

Example:

from dbbasic_email import mail

# Send notification to user
mail('john',
     'New comment on your post',
     'Jane commented: "Great article!"')

# Appends to var/mail/john

When to use: - Notifications between users on same app - Internal messages - System alerts to users - No external email needed


Function: send_email(to, subject, body, from_addr=None)

Purpose: Send external email via SMTP (queued)

Parameters: - to (str): Email address (external) - subject (str): Email subject - body (str): Email body (plain text or HTML) - from_addr (str, optional): From address (defaults to config)

Returns: - job_id (str): Queue job ID

Behavior: 1. Queue email via dbbasic-queue 2. Worker picks up and sends via SMTP 3. Retries on failure (3 attempts) 4. Logs delivery status

Example:

from dbbasic_email import send_email

# Send welcome email
job_id = send_email(
    to='user@gmail.com',
    subject='Welcome to MyApp',
    body='Thanks for signing up!'
)

# Returns immediately (queued for delivery)

When to use: - Password resets - Welcome emails - Notifications to external addresses - Transactional emails


Function: send_template(to, template, context)

Purpose: Send email using template

Parameters: - to (str): Email address - template (str): Template name - context (dict): Template variables

Returns: - job_id (str): Queue job ID

Behavior: 1. Load template from templates/email/{template}.txt 2. Render with context (simple string formatting) 3. Queue via send_email()

Example:

from dbbasic_email import send_template

# Send password reset
send_template(
    to='user@gmail.com',
    template='password_reset',
    context={
        'username': 'john',
        'reset_link': 'https://myapp.com/reset/abc123'
    }
)

Template file (templates/email/password_reset.txt):

Subject: Password Reset Request

Hi {username},

Click here to reset your password:
{reset_link}

This link expires in 24 hours.

- MyApp Team

Function: read_mail(username, limit=50)

Purpose: Read user's mailbox (local mail)

Parameters: - username (str): Username - limit (int): Max messages to return

Returns: - List of messages (dicts)

Example:

from dbbasic_email import read_mail

# Get user's mail
messages = read_mail('john', limit=10)

for msg in messages:
    print(f"{msg['date']}: {msg['subject']}")
    print(f"  {msg['body']}")

When to use: - Show user's notifications - Internal messaging inbox - System alerts


Storage Format

Local Mail: var/mail/{username} (mbox format)

var/mail/john

Format (mbox - Unix standard):

From system Wed Oct 09 10:30:00 2025
Subject: New comment on your post
Message-ID: <abc123@myapp.com>

Jane commented: "Great article!"

From system Wed Oct 09 11:00:00 2025
Subject: Your report is ready
Message-ID: <def456@myapp.com>

The monthly report has been generated.

Why mbox: - Unix standard (1971) - Simple append - One file per user - Standard tools work (mail, mutt)

External Email Queue: dbbasic-queue

data/queue.tsv
id  type    payload status  attempts
abc123  send_email  {"to":"user@gmail.com",...} pending 0

Uses existing dbbasic-queue infrastructure.


Implementation

Core Implementation (~200 lines)

import os
import time
import smtplib
from email.message import EmailMessage
from pathlib import Path
from dbbasic_queue import enqueue

MAIL_DIR = 'var/mail'
SMTP_HOST = os.getenv('SMTP_HOST', 'localhost')
SMTP_PORT = int(os.getenv('SMTP_PORT', '587'))
SMTP_USER = os.getenv('SMTP_USER', '')
SMTP_PASSWORD = os.getenv('SMTP_PASSWORD', '')
FROM_ADDR = os.getenv('FROM_ADDR', 'noreply@example.com')

def mail(username, subject, body):
    """Send local mail to user (Unix mail)"""
    mailbox = Path(MAIL_DIR) / username
    mailbox.parent.mkdir(parents=True, exist_ok=True)

    # Generate message ID
    message_id = f"<{int(time.time())}@localhost>"

    # Append to mailbox (mbox format)
    with mailbox.open('a') as f:
        f.write(f"From system {time.ctime()}\n")
        f.write(f"Subject: {subject}\n")
        f.write(f"Message-ID: {message_id}\n")
        f.write(f"\n{body}\n\n")

    return message_id

def send_email(to, subject, body, from_addr=None):
    """Queue external email for SMTP delivery"""
    from_addr = from_addr or FROM_ADDR

    # Queue via dbbasic-queue
    job_id = enqueue('send_email', {
        'to': to,
        'from': from_addr,
        'subject': subject,
        'body': body
    })

    return job_id

def send_template(to, template, context):
    """Send email using template"""
    # Load template
    template_path = Path(f'templates/email/{template}.txt')
    if not template_path.exists():
        raise FileNotFoundError(f"Template not found: {template}")

    template_text = template_path.read_text()

    # Parse subject and body
    lines = template_text.split('\n')
    subject = ''
    body_lines = []
    in_body = False

    for line in lines:
        if line.startswith('Subject:'):
            subject = line.replace('Subject:', '').strip()
        elif line == '':
            in_body = True
        elif in_body:
            body_lines.append(line)

    body = '\n'.join(body_lines)

    # Render template (simple string formatting)
    subject = subject.format(**context)
    body = body.format(**context)

    return send_email(to, subject, body)

def read_mail(username, limit=50):
    """Read user's mailbox (local mail)"""
    mailbox = Path(MAIL_DIR) / username
    if not mailbox.exists():
        return []

    # Parse mbox format
    messages = []
    current_msg = None

    with mailbox.open('r') as f:
        for line in f:
            if line.startswith('From '):
                # Start of new message
                if current_msg:
                    messages.append(current_msg)
                current_msg = {
                    'date': line.replace('From system ', '').strip(),
                    'subject': '',
                    'message_id': '',
                    'body': ''
                }
            elif current_msg:
                if line.startswith('Subject:'):
                    current_msg['subject'] = line.replace('Subject:', '').strip()
                elif line.startswith('Message-ID:'):
                    current_msg['message_id'] = line.replace('Message-ID:', '').strip()
                elif line.strip():
                    current_msg['body'] += line

        # Add last message
        if current_msg:
            messages.append(current_msg)

    # Return most recent first, limited
    return list(reversed(messages))[:limit]

def _smtp_send_handler(payload):
    """
    Queue worker handler for sending SMTP email.

    Called by dbbasic-queue worker.
    """
    msg = EmailMessage()
    msg['From'] = payload['from']
    msg['To'] = payload['to']
    msg['Subject'] = payload['subject']
    msg.set_content(payload['body'])

    # Send via SMTP
    if SMTP_USER:
        # Authenticated SMTP
        with smtplib.SMTP(SMTP_HOST, SMTP_PORT) as smtp:
            smtp.starttls()
            smtp.login(SMTP_USER, SMTP_PASSWORD)
            smtp.send_message(msg)
    else:
        # Local SMTP (no auth)
        with smtplib.SMTP(SMTP_HOST, SMTP_PORT) as smtp:
            smtp.send_message(msg)

    return {'sent_at': time.time(), 'to': payload['to']}

# Register handler with queue
QUEUE_HANDLERS = {
    'send_email': _smtp_send_handler
}

That's it. ~200 lines.


Dependencies

No external services required (can use local SMTP or configure external).


Configuration

Environment Variables

# SMTP settings (for external email)
SMTP_HOST=smtp.gmail.com        # SMTP server
SMTP_PORT=587                   # SMTP port (587 for TLS)
SMTP_USER=your@gmail.com        # SMTP username
SMTP_PASSWORD=your-app-password # SMTP password
FROM_ADDR=noreply@myapp.com     # Default from address

# Or use local sendmail
SMTP_HOST=localhost
SMTP_PORT=25
# No auth needed for localhost

For development:

# Use local debugging SMTP
python -m smtpd -n -c DebuggingServer localhost:1025
# Set: SMTP_HOST=localhost SMTP_PORT=1025

For production: - Use Gmail SMTP (free for low volume) - Use your own mail server - Use SendGrid/Mailgun (if really needed)


Usage Examples

Internal Notifications (Local Mail)

from dbbasic_email import mail
from dbbasic_accounts import Accounts

accounts = Accounts('./etc')

# User posts comment
@app.route('/comment', methods=['POST'])
def add_comment():
    # ... save comment ...

    # Notify post author (local mail)
    post_author = get_post_author(post_id)
    mail(post_author.username,
         'New comment on your post',
         f'{commenter} commented: "{comment_text}"')

    return 'Comment added'

# User views their notifications
@app.route('/notifications')
def notifications():
    username = session['username']
    messages = read_mail(username, limit=20)
    return render('notifications.html', messages=messages)

No SMTP needed for internal notifications!

External Email (SMTP)

from dbbasic_email import send_email

# User registration
@app.route('/register', methods=['POST'])
def register():
    user = accounts.register(
        email=request.form['email'],
        password=request.form['password']
    )

    # Send welcome email (external SMTP)
    send_email(
        to=user.email,
        subject='Welcome to MyApp!',
        body=f'Hi {user.name}, thanks for signing up!'
    )

    return redirect('/login')

Email Templates

from dbbasic_email import send_template

# Password reset
@app.route('/forgot-password', methods=['POST'])
def forgot_password():
    email = request.form['email']
    user = accounts.get_user(email=email)

    if user:
        # Generate reset token
        token = generate_reset_token(user.uid)

        # Send using template
        send_template(
            to=email,
            template='password_reset',
            context={
                'username': user.username,
                'reset_link': f'https://myapp.com/reset/{token}'
            }
        )

    return 'If email exists, reset link sent'

Template (templates/email/password_reset.txt):

Subject: Password Reset Request

Hi {username},

Someone requested a password reset for your account.

Click here to reset your password:
{reset_link}

This link expires in 24 hours.

If you didn't request this, ignore this email.

- MyApp Team

Local Mail System

Mailbox Format (mbox)

Standard Unix format (used since 1971):

From system Wed Oct 09 10:30:00 2025
Subject: New comment
Message-ID: <abc123@localhost>

John commented on your post.

From system Wed Oct 09 11:00:00 2025
Subject: Report ready
Message-ID: <def456@localhost>

Your monthly report is ready.

Why mbox: - Unix standard - One file per user - Simple append - Tools exist (mail, mutt, mailx)

Reading Mailbox

Python API:

messages = read_mail('john', limit=10)

Unix tools:

# View mailbox
cat var/mail/john

# Count messages
grep "^From " var/mail/john | wc -l

# Search messages
grep "comment" var/mail/john

Web Interface

@app.route('/inbox')
def inbox():
    username = session['username']
    messages = read_mail(username, limit=50)
    return render('inbox.html', messages=messages)

templates/inbox.html:

<h1>Inbox</h1>
{% for msg in messages %}
  <div class="message">
    <strong>{{ msg.subject }}</strong>
    <span>{{ msg.date }}</span>
    <p>{{ msg.body }}</p>
  </div>
{% endfor %}

External Email via SMTP

Queue Integration

Sending external email:

send_email('user@gmail.com', 'Hello', 'Message body')
# → Queues job in data/queue.tsv

Worker processes queue:

# workers/email_worker.py
from dbbasic_queue import process_jobs
from dbbasic_email import QUEUE_HANDLERS

if __name__ == '__main__':
    process_jobs(QUEUE_HANDLERS, max_attempts=3)

Cron runs worker:

# /etc/cron.d/email-worker
* * * * * cd /app && python3 workers/email_worker.py >> /var/log/email.log 2>&1

SMTP Configuration

Gmail (free for low volume):

export SMTP_HOST=smtp.gmail.com
export SMTP_PORT=587
export SMTP_USER=your@gmail.com
export SMTP_PASSWORD=your-app-password  # Generate in Google Account settings
export FROM_ADDR=noreply@myapp.com

Your own mail server:

export SMTP_HOST=mail.yourserver.com
export SMTP_PORT=587
export SMTP_USER=noreply@yourserver.com
export SMTP_PASSWORD=your-password
export FROM_ADDR=noreply@yourserver.com

Local sendmail (Unix):

export SMTP_HOST=localhost
export SMTP_PORT=25
# No auth needed for localhost

Email Templates

Template Structure

templates/email/
├── welcome.txt
├── password_reset.txt
├── comment_notification.txt
└── weekly_digest.txt

Template format:

Subject: {subject_here}

{body_here}
Can use {variables}
Multiple lines
Plain text

Example Templates

welcome.txt:

Subject: Welcome to {app_name}!

Hi {username},

Thanks for signing up for {app_name}.

Your account is ready. You can now:
- Create posts
- Comment on articles
- Upload files

Get started: {app_url}

Questions? Reply to this email.

- {app_name} Team

password_reset.txt:

Subject: Password Reset Request

Hi {username},

Someone requested a password reset for your account.

Reset your password: {reset_link}

This link expires in {expiry_hours} hours.

If you didn't request this, ignore this email.

- {app_name} Team

comment_notification.txt:

Subject: New comment on "{post_title}"

Hi {author},

{commenter} commented on your post "{post_title}":

"{comment_text}"

View and reply: {post_url}

- {app_name} Team

Rendering Templates

send_template('user@gmail.com', 'welcome', {
    'app_name': 'MyBlog',
    'username': 'john',
    'app_url': 'https://myblog.com'
})

Simple string formatting - no template engine needed.


Worker Script

Email Worker (Cron Job)

#!/usr/bin/env python3
# workers/email_worker.py

import os
from dbbasic_queue import process_jobs
from dbbasic_email import QUEUE_HANDLERS
from dbbasic_logs import log

if __name__ == '__main__':
    log.info("Email worker starting")

    try:
        process_jobs(QUEUE_HANDLERS, max_attempts=3)
        log.info("Email worker completed")
    except Exception as e:
        log.exception("Email worker error")
        raise

Cron setup:

# Run every minute
* * * * * cd /app && python3 workers/email_worker.py

Performance Characteristics

Local Mail (Instant)

Operation Time Notes
Send local mail 0.1ms Append to file
Read mailbox 1ms Read and parse mbox

Local mail is instant - no network, no queue.

External Email (Queued)

Operation Time Notes
Queue email 0.1ms dbbasic-queue
Worker sends 100-500ms SMTP delivery
Retry on failure Exponential backoff 3 attempts

External email is async - app returns immediately.


Comparison to Alternatives

SendGrid/Mailgun (SaaS)

SaaS:

import sendgrid
sg = sendgrid.SendGridAPIClient(api_key='...')
sg.send(message)

Pros: - Managed service - High deliverability - Analytics

Cons: - Costs money ($15-200/month) - API dependency - Vendor lock-in - No local mail

dbbasic-email

dbbasic:

from dbbasic_email import send_email
send_email('user@gmail.com', 'Hello', 'Body')

Pros: - Free (use your SMTP) - Local mail built-in - No vendor lock-in - Simple queue integration

Cons: - Need SMTP server - Manage deliverability yourself

When to use SendGrid: - High volume (10,000+ emails/day) - Need analytics - Deliverability critical

When to use dbbasic-email: - Low-medium volume - Have SMTP access - Want simplicity - Need local mail


Django Comparison

Django

from django.core.mail import send_mail

send_mail(
    subject='Welcome',
    message='Thanks for signing up',
    from_email='noreply@myapp.com',
    recipient_list=['user@gmail.com'],
    fail_silently=False,
)

Requires: - SMTP configuration in settings.py - No queuing (blocking) - No local mail - No templates built-in

dbbasic-email

from dbbasic_email import send_email, mail, send_template

# External email (queued)
send_email('user@gmail.com', 'Welcome', 'Thanks for signing up')

# Local mail (instant)
mail('john', 'New comment', 'Someone commented on your post')

# Templates
send_template('user@gmail.com', 'welcome', {'username': 'john'})

Includes: - Queuing built-in (dbbasic-queue) - Local mail (Unix-style) - Simple templates - No configuration needed (works with localhost SMTP)


Rails Comparison

Rails (ActionMailer)

# app/mailers/user_mailer.rb
class UserMailer < ApplicationMailer
  def welcome_email(user)
    @user = user
    mail(to: @user.email, subject: 'Welcome')
  end
end

# Call it
UserMailer.welcome_email(@user).deliver_later

Requires: - Mailer classes - View templates - Background job system (Sidekiq/etc) - Complex configuration

dbbasic-email

from dbbasic_email import send_template

send_template(
    to=user.email,
    template='welcome',
    context={'username': user.name}
)

Simpler: - No classes needed - Text templates (not views) - Uses dbbasic-queue (built-in) - Minimal configuration


Unix Mail Integration

The Vision

Your web app = Unix system with mail:

# User signs up
python app.py
# → User 'john' created

# Send internal notification
python -c "from dbbasic_email import mail; mail('john', 'Welcome', 'Thanks!')"

# Read mail
cat var/mail/john

# Use Unix mail command (if installed)
echo "Test" | mail -s "Subject" john
# Appends to var/mail/john

Unix tools work with web app mail!

Internal Messaging System

from dbbasic_email import mail, read_mail

# Send message
@app.route('/message/<username>', methods=['POST'])
def send_message(username):
    sender = session['username']
    message = request.form['message']

    mail(username,
         f'Message from {sender}',
         message)

    return redirect(f'/user/{username}')

# Read messages
@app.route('/messages')
def inbox():
    username = session['username']
    messages = read_mail(username)
    return render('inbox.html', messages=messages)

Built-in messaging - no database needed.


Integration with Other Modules

dbbasic-accounts Integration

from dbbasic_accounts import Accounts
from dbbasic_email import send_template, mail

accounts = Accounts('./etc')

# User registers
@app.route('/register', methods=['POST'])
def register():
    user = accounts.register(
        email=request.form['email'],
        password=request.form['password'],
        name=request.form['name']
    )

    # Send welcome email (external SMTP)
    send_template(user.email, 'welcome', {
        'username': user.username,
        'name': user.name
    })

    # Send local notification
    mail(user.username,
         'Welcome to MyApp',
         'Your account is ready!')

    return redirect('/login')

dbbasic-queue Integration

# Email sending automatically uses queue
send_email('user@gmail.com', 'Subject', 'Body')
# → Creates job in data/queue.tsv
# → Worker picks up and sends
# → Retries on failure

Queue worker includes email handler automatically.

dbbasic-logs Integration

from dbbasic_email import send_email
from dbbasic_logs import log

# All email sending is logged
send_email('user@gmail.com', 'Welcome', 'Hello')
# Logs: "Email queued", to="user@gmail.com", job_id="abc123"

# Worker logs delivery
# Logs: "Email sent", to="user@gmail.com", duration=0.5
# Or: "Email failed", to="user@gmail.com", error="SMTP timeout"

Testing

Development Mode

Use debugging SMTP server:

# Terminal 1: Start debug SMTP
python -m smtpd -n -c DebuggingServer localhost:1025

# Terminal 2: Configure and run app
export SMTP_HOST=localhost
export SMTP_PORT=1025
python app.py

# All emails print to Terminal 1 (not actually sent)

Unit Tests

from dbbasic_email import mail, read_mail, send_email

def test_local_mail():
    # Send local mail
    msg_id = mail('john', 'Test Subject', 'Test body')
    assert msg_id

    # Read it back
    messages = read_mail('john')
    assert len(messages) > 0
    assert messages[0]['subject'] == 'Test Subject'
    assert 'Test body' in messages[0]['body']

def test_queue_email():
    # Queue external email
    job_id = send_email('test@example.com', 'Test', 'Body')
    assert job_id

    # Verify queued
    from dbbasic_queue import get_job
    job = get_job(job_id)
    assert job['type'] == 'send_email'
    assert job['payload']['to'] == 'test@example.com'

def test_template():
    # Test template rendering
    job_id = send_template('test@example.com', 'welcome', {
        'username': 'john',
        'app_name': 'MyApp'
    })
    assert job_id

Deployment

Production Setup

1. Configure SMTP:

# .env
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_USER=your@gmail.com
SMTP_PASSWORD=your-app-password
FROM_ADDR=noreply@myapp.com

2. Create email worker:

mkdir -p workers
cat > workers/email_worker.py << 'EOF'
from dbbasic_queue import process_jobs
from dbbasic_email import QUEUE_HANDLERS

if __name__ == '__main__':
    process_jobs(QUEUE_HANDLERS, max_attempts=3)
EOF

3. Set up cron:

# Run worker every minute
* * * * * cd /app && python3 workers/email_worker.py >> /var/log/email.log 2>&1

4. Use in app:

from dbbasic_email import send_email, mail

# Just use it
send_email('user@gmail.com', 'Subject', 'Body')
mail('john', 'Notification', 'You have a new message')

That's it. No email service signup, no credit card.

Docker Deployment

FROM python:3.11-slim

# Environment
ENV SMTP_HOST=smtp.gmail.com
ENV SMTP_PORT=587
ENV SMTP_USER=your@gmail.com
ENV SMTP_PASSWORD=your-app-password
ENV FROM_ADDR=noreply@myapp.com

# Install
RUN pip install dbbasic-email dbbasic-queue dbbasic-logs

# Copy app
COPY . /app
WORKDIR /app

# Run app + cron for worker
CMD ["python", "app.py"]

Monitoring

Check Email Queue

# Pending emails
grep "send_email.*pending" data/queue.tsv | wc -l

# Failed emails
grep "send_email.*failed" data/queue.tsv

# Recent deliveries
dbbasic logs:search "Email sent" --days 1

Admin Dashboard

from dbbasic_queue import query

@app.route('/admin/email-stats')
def email_stats():
    QUEUE_FILE = 'data/queue.tsv'

    stats = {
        'pending': len(query(QUEUE_FILE,
            lambda r: r[1] == 'send_email' and r[3] == 'pending')),
        'sent': len(query(QUEUE_FILE,
            lambda r: r[1] == 'send_email' and r[3] == 'completed')),
        'failed': len(query(QUEUE_FILE,
            lambda r: r[1] == 'send_email' and r[3] == 'failed')),
    }

    return jsonify(stats)

Security Considerations

SMTP Credentials

Never commit credentials:

# .env (in .gitignore)
SMTP_PASSWORD=your-password

# Use environment variables
export SMTP_PASSWORD=your-password

Email Validation

import re

def send_email(to, subject, body):
    # Validate email format
    if not re.match(r'^[^@]+@[^@]+\.[^@]+$', to):
        raise ValueError(f"Invalid email: {to}")

    # ... send email

Rate Limiting

from dbbasic_logs import log

def send_email(to, subject, body, user_id=None):
    # Check rate limit
    if user_id:
        recent = log.search(f"user_id.*{user_id}.*send_email", days=1)
        if len(recent) > 10:
            raise ValueError("Rate limit exceeded: 10 emails per day")

    # ... send email

Spam Prevention

# Don't allow user-controlled from address
send_email(
    to=user_input_email,
    subject=safe_subject,
    body=safe_body,
    from_addr=FROM_ADDR  # Always use configured address
)

Common Use Cases

Password Reset Flow

from dbbasic_email import send_template
from dbbasic_sessions import create_session
import secrets

@app.route('/forgot-password', methods=['POST'])
def forgot_password():
    email = request.form['email']
    user = accounts.get_user(email=email)

    if user:
        # Generate reset token (signed, time-limited)
        token = create_session(user.uid, ttl=3600)  # 1 hour

        # Send reset email
        send_template(user.email, 'password_reset', {
            'username': user.username,
            'reset_link': f'https://myapp.com/reset/{token}',
            'expiry_hours': '1'
        })

    return 'If email exists, reset link sent'

@app.route('/reset/<token>', methods=['GET', 'POST'])
def reset_password(token):
    from dbbasic_sessions import get_session

    # Verify token
    user_id = get_session(token)
    if not user_id:
        return 'Invalid or expired reset link', 400

    if request.method == 'POST':
        new_password = request.form['password']
        user = accounts.get_user(user_id=user_id)
        accounts.passwd.passwd(user.username, new_password)

        return redirect('/login')

    return render('reset_password.html')

Email Verification

@app.route('/register', methods=['POST'])
def register():
    email = request.form['email']
    password = request.form['password']

    # Create user (not verified yet)
    user = accounts.register(email, password)

    # Generate verification token
    token = create_session(user.uid, ttl=86400)  # 24 hours

    # Send verification email
    send_template(email, 'verify_email', {
        'username': user.username,
        'verify_link': f'https://myapp.com/verify/{token}'
    })

    return 'Check your email to verify account'

@app.route('/verify/<token>')
def verify_email(token):
    user_id = get_session(token)
    if not user_id:
        return 'Invalid or expired link', 400

    # Mark user as verified
    accounts.passwd.usermod_custom(user_id, verified=True)

    return redirect('/login')

Comment Notifications

@app.route('/comment', methods=['POST'])
def add_comment():
    post_id = request.form['post_id']
    comment_text = request.form['comment']
    commenter = session['username']

    # Save comment
    comment = save_comment(post_id, commenter, comment_text)

    # Get post author
    post = get_post(post_id)
    author = accounts.get_user(user_id=post.author_id)

    # Send local notification (instant)
    mail(author.username,
         f'New comment on "{post.title}"',
         f'{commenter} commented: "{comment_text}"')

    # Also send email if user wants it
    if author.email_notifications:
        send_template(author.email, 'comment_notification', {
            'author': author.username,
            'commenter': commenter,
            'post_title': post.title,
            'comment_text': comment_text,
            'post_url': f'https://myapp.com/post/{post_id}'
        })

    return redirect(f'/post/{post_id}')

Architectural Decisions

Why Local Mail + SMTP?

Local mail (var/mail/): - Internal notifications - User to user messages - System alerts - Instant delivery - No SMTP needed

External SMTP: - Emails to outside world - Password resets - Transactional emails - Queued delivery

Best of both worlds.

Why Not Just Use SendGrid?

SendGrid is great for: - High volume - Need analytics - Don't want to manage SMTP

But most apps: - < 1000 emails/day - Have SMTP access (Gmail, ISP, own server) - Don't need fancy analytics

dbbasic-email gives you the choice: - Start with free SMTP - Upgrade to SendGrid later if needed

Why mbox Format?

Alternatives: - Maildir (one file per message) - Database table - JSON files

mbox wins: - Unix standard (1971) - Simple append - Tools exist (mail, mutt) - One file per user (not thousands)


Migration Guide

From Django

Old:

from django.core.mail import send_mail

send_mail(
    'Subject',
    'Body',
    'from@example.com',
    ['to@example.com']
)

New:

from dbbasic_email import send_email

send_email('to@example.com', 'Subject', 'Body')

From Rails

Old:

UserMailer.welcome_email(user).deliver_later

New:

from dbbasic_email import send_template

send_template(user.email, 'welcome', {'username': user.name})

From SendGrid

Old:

from sendgrid import SendGridAPIClient

sg = SendGridAPIClient(api_key=SENDGRID_API_KEY)
sg.send(message)

New:

from dbbasic_email import send_email

send_email(to, subject, body)

Data migration: None needed (no stored emails)


Package Structure

dbbasic-email/
├── dbbasic_email/
│   ├── __init__.py         # Main implementation
│   ├── smtp.py             # SMTP delivery
│   ├── templates.py        # Template rendering
│   └── cli.py              # CLI commands
├── templates/
│   └── email/              # Example templates
│       ├── welcome.txt
│       └── password_reset.txt
├── tests/
│   ├── test_mail.py
│   ├── test_smtp.py
│   └── test_templates.py
├── workers/
│   └── email_worker.py     # Example worker
├── setup.py
├── README.md
├── LICENSE
└── CHANGELOG.md

Success Criteria

This implementation is successful if:

  1. Unix Mail: Local delivery to var/mail/{user}
  2. SMTP: External delivery via SMTP
  3. Queued: Uses dbbasic-queue for async
  4. Templates: Simple text templates
  5. Simple: < 300 lines of code
  6. No Service Required: Works with any SMTP
  7. Integrated: Works with accounts, queue, logs
  8. Standard: mbox format, Unix-compatible

CLI Commands

Module provides these commands:

# Send test email
dbbasic email:send user@example.com "Test" "Test body"

# View local mail
dbbasic email:inbox john

# Check queue status
dbbasic email:queue

# Send from template
dbbasic email:template user@example.com welcome username=john

Auto-discovered by dbbasic-cli.


References


Summary

dbbasic-email brings Unix mail to web apps:

What it provides: - Local mail (var/mail/) for internal notifications - SMTP delivery for external email - Queue integration for async sending - Simple text templates - Unix-compatible (mbox format)

How it works: - Local mail: Append to var/mail/{user} (instant) - External mail: Queue → worker → SMTP (async) - Templates: Plain text with {variables} - ~200 lines of code

Use it when: - Building web apps with email - Want Unix integration - Have SMTP access (or use Gmail) - Need internal messaging

Upgrade to SendGrid when: - > 10,000 emails/day - Need advanced analytics - Deliverability critical

Until then, use simple SMTP. It's the Unix way.


Next Steps: Implement, test, deploy, ship.

No SendGrid. No Mailgun. No vendor lock-in.

Just Unix mail + SMTP + queue.

~200 lines of code.