Replace all ~1,086 occurrences of Wizamart/wizamart/WIZAMART/WizaMart with Orion/orion/ORION across 184 files. This includes database identifiers, email addresses, domain references, R2 bucket names, DNS prefixes, encryption salt, Celery app name, config defaults, Docker configs, CI configs, documentation, seed data, and templates. Renames homepage-wizamart.html template to homepage-orion.html. Fixes duplicate file_pattern key in api.yaml architecture rule. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
7.9 KiB
Email System
The email system provides multi-provider support with database-stored templates and comprehensive logging for the Orion platform.
Overview
The email system supports:
- Multiple Providers: SMTP, SendGrid, Mailgun, Amazon SES
- Multi-language Templates: EN, FR, DE, LB (stored in database)
- Jinja2 Templating: Variable interpolation in subjects and bodies
- Email Logging: Track all sent emails for debugging and compliance
- Debug Mode: Log emails instead of sending during development
Configuration
Environment Variables
Add these settings to your .env file:
# Provider: smtp, sendgrid, mailgun, ses
EMAIL_PROVIDER=smtp
EMAIL_FROM_ADDRESS=noreply@orion.lu
EMAIL_FROM_NAME=Orion
EMAIL_REPLY_TO=
# Behavior
EMAIL_ENABLED=true
EMAIL_DEBUG=false
# SMTP Settings (when EMAIL_PROVIDER=smtp)
SMTP_HOST=smtp.example.com
SMTP_PORT=587
SMTP_USER=
SMTP_PASSWORD=
SMTP_USE_TLS=true
SMTP_USE_SSL=false
# SendGrid (when EMAIL_PROVIDER=sendgrid)
# SENDGRID_API_KEY=SG.your_api_key_here
# Mailgun (when EMAIL_PROVIDER=mailgun)
# MAILGUN_API_KEY=your_api_key_here
# MAILGUN_DOMAIN=mg.yourdomain.com
# Amazon SES (when EMAIL_PROVIDER=ses)
# AWS_ACCESS_KEY_ID=your_access_key
# AWS_SECRET_ACCESS_KEY=your_secret_key
# AWS_REGION=eu-west-1
Debug Mode
Set EMAIL_DEBUG=true to log emails instead of sending them. This is useful during development:
EMAIL_DEBUG=true
Emails will be logged to the console with full details (recipient, subject, body preview).
Database Models
EmailTemplate
Stores multi-language email templates:
| Column | Type | Description |
|---|---|---|
| id | Integer | Primary key |
| code | String(100) | Template identifier (e.g., "signup_welcome") |
| language | String(5) | Language code (en, fr, de, lb) |
| name | String(255) | Human-readable name |
| description | Text | Template purpose |
| category | String(50) | AUTH, ORDERS, BILLING, SYSTEM, MARKETING |
| subject | String(500) | Email subject (supports Jinja2) |
| body_html | Text | HTML body |
| body_text | Text | Plain text fallback |
| variables | Text | JSON list of expected variables |
| is_active | Boolean | Enable/disable template |
EmailLog
Tracks all sent emails:
| Column | Type | Description |
|---|---|---|
| id | Integer | Primary key |
| template_code | String(100) | Template used (if any) |
| recipient_email | String(255) | Recipient address |
| subject | String(500) | Email subject |
| status | String(20) | PENDING, SENT, FAILED, DELIVERED, OPENED |
| sent_at | DateTime | When email was sent |
| error_message | Text | Error details if failed |
| provider | String(50) | Provider used (smtp, sendgrid, etc.) |
| store_id | Integer | Related store (optional) |
| user_id | Integer | Related user (optional) |
Usage
Using EmailService
from app.services.email_service import EmailService
def send_welcome_email(db, user, store):
email_service = EmailService(db)
email_service.send_template(
template_code="signup_welcome",
to_email=user.email,
to_name=f"{user.first_name} {user.last_name}",
language="fr", # Falls back to "en" if not found
variables={
"first_name": user.first_name,
"merchant_name": store.name,
"store_code": store.store_code,
"login_url": f"https://orion.lu/store/{store.store_code}/dashboard",
"trial_days": 30,
"tier_name": "Essential",
},
store_id=store.id,
user_id=user.id,
related_type="signup",
)
Convenience Function
from app.services.email_service import send_email
send_email(
db=db,
template_code="order_confirmation",
to_email="customer@example.com",
language="en",
variables={"order_number": "ORD-001"},
)
Sending Raw Emails
For one-off emails without templates:
email_service = EmailService(db)
email_service.send_raw(
to_email="user@example.com",
subject="Custom Subject",
body_html="<h1>Hello</h1><p>Custom message</p>",
body_text="Hello\n\nCustom message",
)
Email Templates
Creating Templates
Templates use Jinja2 syntax for variable interpolation:
<p>Hello {{ first_name }},</p>
<p>Welcome to {{ merchant_name }}!</p>
Seeding Templates
Run the seed script to populate default templates:
python scripts/seed/seed_email_templates.py
This creates templates for:
signup_welcome(en, fr, de, lb)
Available Variables
For signup_welcome:
| Variable | Description |
|---|---|
| first_name | User's first name |
| merchant_name | Store merchant name |
| User's email address | |
| store_code | Store code for dashboard URL |
| login_url | Direct link to dashboard |
| trial_days | Number of trial days |
| tier_name | Subscription tier name |
Provider Setup
SMTP
Standard SMTP configuration:
EMAIL_PROVIDER=smtp
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_USER=your-email@gmail.com
SMTP_PASSWORD=your-app-password
SMTP_USE_TLS=true
SendGrid
- Create account at sendgrid.com
- Generate API key in Settings > API Keys
- Configure:
EMAIL_PROVIDER=sendgrid
SENDGRID_API_KEY=SG.xxxxxxxxxxxxxxxxxxxx
- Install package:
pip install sendgrid
Mailgun
- Create account at mailgun.com
- Add and verify your domain
- Get API key from Domain Settings
- Configure:
EMAIL_PROVIDER=mailgun
MAILGUN_API_KEY=key-xxxxxxxxxxxxxxxx
MAILGUN_DOMAIN=mg.yourdomain.com
Amazon SES
- Set up SES in AWS Console
- Verify sender domain/email
- Create IAM user with SES permissions
- Configure:
EMAIL_PROVIDER=ses
AWS_ACCESS_KEY_ID=AKIAXXXXXXXXXXXXXXXX
AWS_SECRET_ACCESS_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
AWS_REGION=eu-west-1
- Install package:
pip install boto3
Email Logging
All emails are logged to the email_logs table. Query examples:
# Get failed emails
failed = db.query(EmailLog).filter(
EmailLog.status == EmailStatus.FAILED.value
).all()
# Get emails for a store
store_emails = db.query(EmailLog).filter(
EmailLog.store_id == store_id
).order_by(EmailLog.created_at.desc()).all()
# Get recent signup emails
signups = db.query(EmailLog).filter(
EmailLog.template_code == "signup_welcome",
EmailLog.created_at >= datetime.now() - timedelta(days=7)
).all()
Language Fallback
The system automatically falls back to English if a template isn't available in the requested language:
- Request template for "de" (German)
- If not found, try "en" (English)
- If still not found, return None (log error)
Testing
Run email service tests:
pytest tests/unit/services/test_email_service.py -v
Test coverage includes:
- Provider abstraction (Debug, SMTP, etc.)
- Template rendering with Jinja2
- Language fallback behavior
- Email sending success/failure
- EmailLog model methods
- Template variable handling
Architecture
app/services/email_service.py # Email service with provider abstraction
models/database/email.py # EmailTemplate and EmailLog models
app/core/config.py # Email configuration settings
scripts/seed/seed_email_templates.py # Template seeding script
Provider Abstraction
The system uses a strategy pattern for email providers:
EmailProvider (ABC)
├── SMTPProvider
├── SendGridProvider
├── MailgunProvider
├── SESProvider
└── DebugProvider
Each provider implements the send() method with the same signature, making it easy to switch providers via configuration.
Future Enhancements
Planned improvements:
- Email Queue: Background task queue for high-volume sending
- Webhook Tracking: Track deliveries, opens, clicks via provider webhooks
- Template Editor: Admin UI for editing templates
- A/B Testing: Test different email versions
- Scheduled Emails: Send emails at specific times