Self-hosting
Clone, set a few env vars, run. The app works on your laptop with SQLite, and on Railway / any Python host with Postgres.
Local setup
git clone https://github.com/scripts-and-tables/emails-manager.git
cd emails-manager
python -m venv .venv
.venv\Scripts\activate # Windows
# source .venv/bin/activate # macOS / Linux
pip install -r requirements.txt
copy .env.example .env # Windows
# cp .env.example .env # macOS / Linux
Fill in .env (see the env vars table below), then:
python manage.py migrate
python manage.py runserver
The app runs at http://127.0.0.1:8000.
Generating keys
Both SECRET_KEY and FIELD_ENCRYPTION_KEY need to be
set to real random values. Easy way:
# SECRET_KEY โ any long random string
python -c "import secrets; print(secrets.token_urlsafe(64))"
# FIELD_ENCRYPTION_KEY โ must be a Fernet key
python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"
FIELD_ENCRYPTION_KEY invalidates every stored IMAP
password. Keep it stable, or be prepared to re-enter every password.
Environment variables
| Variable | Required | Notes |
|---|---|---|
SECRET_KEY |
yes | Django signing key. Long random string. |
DEBUG |
no | Defaults to False. Set True locally. |
ALLOWED_HOSTS |
no | Comma-separated. Defaults to localhost,127.0.0.1. |
CSRF_TRUSTED_ORIGINS |
no | Comma-separated origins for CSRF protection. |
FIELD_ENCRYPTION_KEY |
yes | Fernet key for IMAP password encryption. Rotating invalidates all stored passwords. |
RESEND_API_KEY |
yes (for email) | Without this, OTP and verification emails won't send. |
RESEND_FROM_EMAIL |
yes (for email) | Must use a verified domain in Resend. RFC 5322 format. |
DATABASE_URL |
no | Postgres URL. Falls back to SQLite if unset. |
Project layout
emailsmanager/ # Django project (settings, root urls, wsgi)
core/ # Single app โ models, views, urls, templates, encryption, middleware
docs/ # This Pages site (you're looking at it)
requirements.txt
.env.example
manage.py
Deployment notes
Railway / RAILPACK gotcha
Static files in this project are served by WhiteNoise with
WHITENOISE_USE_FINDERS = True, on purpose. Railway's
RAILPACK builder runs collectstatic in a
preDeploy phase whose filesystem doesn't propagate into the
runtime container, so STATIC_ROOT ends up empty in
production and WhiteNoise 404s every /static/ URL
(including /static/admin/...). Finders mode works because
both Django's contrib/admin/static and the app's
core/static/ ship inside the runtime image as installed
packages and repo source.
Database
Production deploys go through DATABASE_URL (parsed by
dj-database-url). Anything without it falls back to a
SQLite file in the project directory, which is fine for local dev and
small one-user deploys.
Outbound email
Transactional email goes out through Resend. Verify your
sender domain there, then point RESEND_FROM_EMAIL at any
address on that domain (the app uses a dedicated verified-domain
sender per a recent rebrand).
First admin user
Once the app is up:
python manage.py createsuperuser
Continuous integration
The repo ships a single GitHub Actions workflow at
.github/workflows/ci.yml that runs on every push to
main and every pull request:
- Python 3.13, pip cache enabled.
ruff check .against the whole repo.python manage.py test --verbosity=2.- Test-only env values are injected inline; no real secrets are used.