Summary
Provider API tokens (server tokens and account API keys used to talk to email providers like Postmark and ForwardEmail) are currently stored as a JSON blob in the EMAIL_SERVER_TOKENS environment variable and looked up via app/as_email/provider_tokens.py. Moving them into encrypted model fields makes token management easier operationally (editable via the Django admin UI rather than requiring env var changes and restarts) and consistent with how IMAP credentials are already handled via django-fernet-encrypted-fields.
Current Structure
settings.EMAIL_SERVER_TOKENS — a parsed JSON dict loaded from an env var:
{
"postmark": {
"example.com": "pm-server-token",
"account_api_key": "pm-account-key"
},
"forwardemail": {
"account_api_key": "fe-account-key"
}
}
app/as_email/provider_tokens.py — get_provider_token() and has_provider_token() look up tokens from the settings dict.
- Provider backends (
postmark.py, forwardemail.py) call get_provider_token() to obtain credentials at runtime.
Proposed Design
New model: ServerProviderToken
A Server can have multiple providers (send_provider FK + receive_providers M2M), so the per-server token is stored on the (Server, Provider) pair. This is the only design that allows tokens to be individually encrypted at rest via django-fernet-encrypted-fields — a JSONField approach cannot encrypt individual values nested inside JSON.
class ServerProviderToken(models.Model):
server = models.ForeignKey(Server, on_delete=models.CASCADE, related_name="provider_tokens")
provider = models.ForeignKey(Provider, on_delete=models.CASCADE, related_name="server_tokens")
token = EncryptedCharField(max_length=1024)
class Meta:
unique_together = [("server", "provider")]
Account-level keys (not tied to a specific server domain) go on the Provider model itself:
class Provider(models.Model):
# ... existing fields ...
account_api_key = EncryptedCharField(max_length=1024, blank=True, default="")
Removal
- Remove
EMAIL_SERVER_TOKENS from settings.py, .env, docker-compose, and Drone CI
- Remove
app/as_email/provider_tokens.py
- Update provider backends (
postmark.py, forwardemail.py) to read tokens from ServerProviderToken / Provider.account_api_key
- Update all tests that set
settings.EMAIL_SERVER_TOKENS[...] directly
Benefits
- Tokens encrypted at rest using the same
SALT_KEY/django-fernet-encrypted-fields mechanism already in use for IMAP passwords — consistent storage pattern for all secrets
- Eliminates a large JSON blob from environment / container configuration
- Tokens editable via Django admin UI without requiring a service restart
- Referential integrity and cascade deletes enforced at the DB level
- Clearer data model: each (Server, Provider) pair carries its own encrypted token
Acceptance Criteria
Summary
Provider API tokens (server tokens and account API keys used to talk to email providers like Postmark and ForwardEmail) are currently stored as a JSON blob in the
EMAIL_SERVER_TOKENSenvironment variable and looked up viaapp/as_email/provider_tokens.py. Moving them into encrypted model fields makes token management easier operationally (editable via the Django admin UI rather than requiring env var changes and restarts) and consistent with how IMAP credentials are already handled viadjango-fernet-encrypted-fields.Current Structure
settings.EMAIL_SERVER_TOKENS— a parsed JSON dict loaded from an env var:{ "postmark": { "example.com": "pm-server-token", "account_api_key": "pm-account-key" }, "forwardemail": { "account_api_key": "fe-account-key" } }app/as_email/provider_tokens.py—get_provider_token()andhas_provider_token()look up tokens from the settings dict.postmark.py,forwardemail.py) callget_provider_token()to obtain credentials at runtime.Proposed Design
New model:
ServerProviderTokenA
Servercan have multiple providers (send_providerFK +receive_providersM2M), so the per-server token is stored on the (Server, Provider) pair. This is the only design that allows tokens to be individually encrypted at rest viadjango-fernet-encrypted-fields— aJSONFieldapproach cannot encrypt individual values nested inside JSON.Account-level keys (not tied to a specific server domain) go on the
Providermodel itself:Removal
EMAIL_SERVER_TOKENSfromsettings.py,.env,docker-compose, and Drone CIapp/as_email/provider_tokens.pypostmark.py,forwardemail.py) to read tokens fromServerProviderToken/Provider.account_api_keysettings.EMAIL_SERVER_TOKENS[...]directlyBenefits
SALT_KEY/django-fernet-encrypted-fieldsmechanism already in use for IMAP passwords — consistent storage pattern for all secretsAcceptance Criteria
EMAIL_SERVER_TOKENSenv var removed everywhereapp/as_email/provider_tokens.pydeletedServerProviderTokenmodel added with encryptedtokenfieldProvider.account_api_keyencrypted field added