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
"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.
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
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
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.
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
┌─────────────────┐
│ send_email() │
└────────┬────────┘
│
┌────┴─────┐
│ Local or │
│ External?│
└────┬─────┘
│
┌────┴────────────────┐
│ │
┌───▼──────┐ ┌──────▼─────┐
│ Local │ │ External │
│ │ │ │
│ Append to│ │ Queue via │
│ mailbox │ │ dbbasic- │
│ │ │ queue │
└──────────┘ └──────┬─────┘
│
┌────▼────┐
│ Worker │
│ sends │
│ SMTP │
└─────────┘
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
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
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
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
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)
data/queue.tsv
id type payload status attempts
abc123 send_email {"to":"user@gmail.com",...} pending 0
Uses existing dbbasic-queue infrastructure.
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.
No external services required (can use local SMTP or configure external).
# 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)
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!
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')
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
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)
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
@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 %}
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
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
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
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
send_template('user@gmail.com', 'welcome', {
'app_name': 'MyBlog',
'username': 'john',
'app_url': 'https://myblog.com'
})
Simple string formatting - no template engine needed.
#!/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
| 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.
| 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.
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:
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
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
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)
# 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
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
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!
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.
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')
# 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.
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"
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)
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
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.
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"]
# 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
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)
Never commit credentials:
# .env (in .gitignore)
SMTP_PASSWORD=your-password
# Use environment variables
export SMTP_PASSWORD=your-password
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
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
# 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
)
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')
@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')
@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}')
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.
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
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)
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')
Old:
UserMailer.welcome_email(user).deliver_later
New:
from dbbasic_email import send_template
send_template(user.email, 'welcome', {'username': user.name})
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)
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
This implementation is successful if:
# 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.
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.