Email hosting is one of those features that seems simple — "just give users an email address" — but has a surprising amount of depth once you dig in. DNS verification, mailbox provisioning, quota management, spam filtering, DKIM signing, and the ever-present question of deliverability. Here's how we're building it into AskRobots.
Why Email Hosting?
Paid AskRobots accounts ($50/month) will include email hosting. Every paid user gets username@askrobots.com automatically. Users who bring their own domain can set up custom email (e.g., you@yourcompany.com) with full DNS verification.
The bigger play: AI-powered email processing. Incoming emails can automatically create tasks, notes, or forward to other addresses based on rules. Send an email to your AskRobots mailbox and it becomes a tracked task — no manual entry needed.
Choosing Mailcow
We evaluated six open-source mail servers:
| Server | Pros | Cons |
|---|---|---|
| Mailcow | Full REST API, Docker-native, active community, SOGo webmail | GPL v3 (fine for SaaS) |
| Postal | Good API, built for transactional | MariaDB + RabbitMQ dependency mess |
| Stalwart | Modern Rust, JMAP support | Too new, sustainability risk |
| Mailu | Simple setup | Limited API |
| Modoboa | Good admin UI | Smaller community |
| iRedMail | Battle-tested | Manual server install, no Docker |
Mailcow won because of its comprehensive REST API. Every operation we need — adding domains, creating mailboxes, managing aliases, rate limiting, DKIM generation — is a single API call. No shelling out, no config file manipulation.
GPL v3 is a non-issue for us: we run it as a service, we don't distribute modified Mailcow code.
Architecture: Feature-Flagged, Mailcow-Optional
The entire email hosting feature is behind a django-waffle feature flag called email_hosting. Until the flag is activated, all email routes return 404. This lets us build and test everything on the Django side before Mailcow is even installed.
┌─────────────────────┐ ┌──────────────────┐
│ AskRobots Django │────▶│ Mailcow Server │
│ email_hosting app │ API │ mail.askrobots │
│ │◀────│ .com │
│ - Models (Django) │ │ - Dovecot │
│ - Views (CRUD) │ │ - Postfix │
│ - Mailcow Client │ │ - SOGo │
│ - Waffle flag gate │ │ - ClamAV │
└─────────────────────┘ └──────────────────┘
The MailcowClient gracefully degrades: if MAILCOW_API_URL is empty, operations are saved locally in Django but not synced. Once Mailcow is live, a sync job can push everything.
The Data Model
Four Django models handle the domain:
EmailDomain
Tracks custom domains with DNS verification status for all four critical records:
- MX — Points mail to our server
- SPF — Authorizes us to send on behalf of the domain
- DKIM — Cryptographic email signing
- DMARC — Policy for handling authentication failures
Each domain tracks its Mailcow sync state independently.
EmailMailbox
The actual email accounts. Key fields:
- local_part + domain = full email address
- is_system flag for auto-provisioned @askrobots.com accounts
- quota / quota_used for storage limits (default 1GB)
EmailAlias
Simple forwarding: sales@example.com → info@example.com. Syncs alias IDs back from Mailcow for edit/delete operations.
EmailForwardingRule
This is where it gets interesting. Rules match incoming email by sender, subject, or recipient and trigger actions:
- Forward — Standard email forwarding
- Create Task — Parse the email and create an AskRobots task
- Create Note — Save email content as a note
- Ignore — Silently discard (spam from known sources)
Rules have priority ordering so you can build a processing pipeline.
The Mailcow API Client
A clean Python wrapper around Mailcow's REST API:
from email_hosting.mailcow_client import MailcowClient
client = MailcowClient()
# Domain management
client.add_domain("example.com")
client.generate_dkim("example.com", key_size=2048)
# Mailbox CRUD
client.add_mailbox("user", "example.com", "password", quota=2048)
client.get_mailboxes(domain="example.com")
# Rate limiting
client.set_domain_ratelimit("example.com", rate="100/h")
client.set_mailbox_ratelimit("user@example.com", rate="50/h")
# Quarantine management
quarantine = client.get_quarantine()
client.release_quarantine(quarantine_id)
All API calls go through a single _request() method with:
- 30-second timeout
- Automatic error detection (Mailcow returns errors inside success responses)
- Connection/timeout error handling with clear error messages
- Empty URL guard (raises immediately instead of hitting MissingSchema)
What's Built So Far
The Django side is complete and tested:
- 15 views — Dashboard, domain/mailbox/alias/rule CRUD with proper auth
- 13 templates — Matching AskRobots earth-tone theme, Bootstrap cards, breadcrumbs
- 4 forms — With user-scoped querysets (you only see your own domains/mailboxes)
- 13 unit tests — Model tests + mocked Mailcow client tests, all passing
- Admin interface — Full Django admin registration for all models
Everything is feature-flagged. Flip email_hosting on in Django admin when Mailcow is ready.
What's Next
- Mailcow installation on RackBarn VM (Ubuntu reinstall, Docker Compose)
- DNS verification service — Background Celery task using
dnspythonto verify MX/SPF/DKIM/DMARC records - Auto-provisioning — Create
username@askrobots.commailbox when user upgrades to paid - Billing integration — Gate email features behind subscription check
- AI email processing — Celery workers that poll Mailcow for new mail and apply forwarding rules
- MCP tools —
create_mailbox,list_mailboxes,add_aliasfor AI agent access - Email hosting REST API — For programmatic access
- Landing page — Public marketing page + sidebar link
Lessons
Feature flags are underrated. We wrote the entire email hosting UI, tested it, and can deploy it to production today — it just won't be visible until we flip the switch. No long-running branches, no merge conflicts.
Mock your external APIs from day one. Every Mailcow client test uses unittest.mock.patch. We caught an edge case (empty API URL causing MissingSchema) in tests before it could ever hit production.
Django's ecosystem still delivers. waffle for feature flags, crispy_forms for consistent UI, allauth for auth, django-tables2 for lists — the app came together in a single session because we're standing on solid libraries.
Comentarios
No comments yet. Be the first!