first commit

This commit is contained in:
2026-08-09 18:03:09 +01:00
commit d7ca591b76
169 changed files with 51272 additions and 0 deletions
+374
View File
@@ -0,0 +1,374 @@
package db
// migration is a single named schema change with driver-specific SQL variants.
// SQLite is the reference dialect (required); postgres/mysql variants are
// filled in as those build-tagged drivers are added — until then the sqlite
// SQL is close enough to run in most cases (TEXT/BLOB/DATETIME map cleanly).
type migration struct {
name string
sql map[string]string
}
var migrations = []migration{
{
name: "0001_tenants_domains",
sql: map[string]string{
"sqlite": `
CREATE TABLE tenants (
id TEXT PRIMARY KEY,
name TEXT UNIQUE NOT NULL,
display_name TEXT,
digest_interval_mins INTEGER NOT NULL DEFAULT 60,
max_accounts INTEGER NOT NULL DEFAULT 0,
quota_mb_per_user INTEGER NOT NULL DEFAULT 2048,
settings_json TEXT NOT NULL DEFAULT '{}',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE domains (
id TEXT PRIMARY KEY,
tenant_id TEXT NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
domain TEXT UNIQUE NOT NULL,
active INTEGER NOT NULL DEFAULT 1,
dkim_selector TEXT,
dkim_private_key_enc BLOB,
accept_all INTEGER NOT NULL DEFAULT 1,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_domains_tenant ON domains(tenant_id);
`,
},
},
{
name: "0002_users_auth",
sql: map[string]string{
"sqlite": `
CREATE TABLE users (
id TEXT PRIMARY KEY,
tenant_id TEXT NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
domain_id TEXT NOT NULL REFERENCES domains(id) ON DELETE CASCADE,
email TEXT UNIQUE NOT NULL,
password_hash TEXT NOT NULL,
display_name TEXT,
role TEXT NOT NULL DEFAULT 'user',
active INTEGER NOT NULL DEFAULT 1,
mfa_enabled INTEGER NOT NULL DEFAULT 0,
totp_secret_enc BLOB,
passkey_credentials_json TEXT NOT NULL DEFAULT '[]',
quota_mb INTEGER NOT NULL DEFAULT 2048,
used_bytes INTEGER NOT NULL DEFAULT 0,
digest_enabled INTEGER NOT NULL DEFAULT 1,
digest_interval_mins INTEGER NOT NULL DEFAULT 0,
last_digest_at DATETIME,
last_login_at DATETIME,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_users_tenant ON users(tenant_id);
CREATE INDEX idx_users_domain ON users(domain_id);
CREATE TABLE app_passwords (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
label TEXT NOT NULL,
password_hash TEXT NOT NULL,
scopes TEXT NOT NULL DEFAULT 'smtp,imap',
last_used_at DATETIME,
expires_at DATETIME,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_app_passwords_user ON app_passwords(user_id);
CREATE TABLE sessions (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
jti TEXT UNIQUE NOT NULL,
user_agent TEXT,
ip TEXT,
expires_at DATETIME NOT NULL,
revoked_at DATETIME
);
CREATE INDEX idx_sessions_user ON sessions(user_id);
CREATE INDEX idx_sessions_jti ON sessions(jti);
CREATE TABLE aliases (
id TEXT PRIMARY KEY,
tenant_id TEXT NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
from_address TEXT UNIQUE NOT NULL,
to_user_id TEXT REFERENCES users(id) ON DELETE CASCADE,
to_external TEXT,
active INTEGER NOT NULL DEFAULT 1
);
CREATE INDEX idx_aliases_tenant ON aliases(tenant_id);
`,
},
},
{
name: "0003_list_rules",
sql: map[string]string{
"sqlite": `
CREATE TABLE list_rules (
id TEXT PRIMARY KEY,
tenant_id TEXT NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
list_type TEXT NOT NULL,
match_type TEXT NOT NULL DEFAULT 'email',
value TEXT NOT NULL,
note TEXT,
active INTEGER NOT NULL DEFAULT 1,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_list_rules_tenant ON list_rules(tenant_id);
CREATE INDEX idx_list_rules_value ON list_rules(value);
`,
},
},
{
name: "0004_messages_mailbox",
sql: map[string]string{
"sqlite": `
CREATE TABLE messages (
id TEXT PRIMARY KEY,
tenant_id TEXT REFERENCES tenants(id) ON DELETE CASCADE,
from_address TEXT NOT NULL,
to_address TEXT NOT NULL,
subject TEXT,
message_id_hdr TEXT,
size_bytes INTEGER NOT NULL DEFAULT 0,
verdict TEXT NOT NULL DEFAULT 'clean',
total_score REAL NOT NULL DEFAULT 0,
sender_ip TEXT,
relayed_at DATETIME,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_messages_tenant ON messages(tenant_id);
CREATE INDEX idx_messages_to ON messages(to_address);
CREATE INDEX idx_messages_created ON messages(created_at);
CREATE TABLE message_checks (
id TEXT PRIMARY KEY,
message_id TEXT NOT NULL REFERENCES messages(id) ON DELETE CASCADE,
stage TEXT NOT NULL,
result TEXT NOT NULL,
score REAL NOT NULL DEFAULT 0,
detail TEXT,
duration_ms INTEGER NOT NULL DEFAULT 0
);
CREATE INDEX idx_message_checks_message ON message_checks(message_id);
CREATE TABLE mailbox_index (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
mailbox TEXT NOT NULL DEFAULT 'INBOX',
uid INTEGER NOT NULL,
eml_path TEXT NOT NULL,
flags TEXT NOT NULL DEFAULT '',
size_bytes INTEGER NOT NULL DEFAULT 0,
received_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
internal_date DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE(user_id, mailbox, uid)
);
CREATE INDEX idx_mailbox_index_user ON mailbox_index(user_id, mailbox);
CREATE TABLE mailbox_uid_counters (
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
mailbox TEXT NOT NULL,
next_uid INTEGER NOT NULL DEFAULT 1,
PRIMARY KEY (user_id, mailbox)
);
`,
},
},
{
name: "0005_outbound_queue",
sql: map[string]string{
"sqlite": `
CREATE TABLE outbound_queue (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
from_address TEXT NOT NULL,
to_address TEXT NOT NULL,
eml_path TEXT NOT NULL,
priority INTEGER NOT NULL DEFAULT 0,
attempts INTEGER NOT NULL DEFAULT 0,
last_error TEXT,
next_attempt_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_outbound_queue_next ON outbound_queue(next_attempt_at);
CREATE INDEX idx_outbound_queue_user ON outbound_queue(user_id);
`,
},
},
{
name: "0006_quarantine",
sql: map[string]string{
"sqlite": `
CREATE TABLE quarantine (
id TEXT PRIMARY KEY,
message_id TEXT NOT NULL REFERENCES messages(id) ON DELETE CASCADE,
eml_path TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'held',
reason TEXT,
released_by TEXT,
released_at DATETIME,
expires_at DATETIME NOT NULL,
notified_at DATETIME,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_quarantine_status ON quarantine(status);
CREATE INDEX idx_quarantine_message ON quarantine(message_id);
CREATE TABLE release_tokens (
id TEXT PRIMARY KEY,
quarantine_id TEXT NOT NULL REFERENCES quarantine(id) ON DELETE CASCADE,
token TEXT UNIQUE NOT NULL,
email TEXT,
used_at DATETIME,
expires_at DATETIME NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_release_tokens_token ON release_tokens(token);
`,
},
},
{
name: "0007_linked_accounts",
sql: map[string]string{
"sqlite": `
CREATE TABLE linked_accounts (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
provider TEXT NOT NULL,
display_name TEXT,
email_address TEXT NOT NULL,
auth_type TEXT NOT NULL,
imap_host TEXT,
imap_port INTEGER,
imap_tls TEXT,
smtp_host TEXT,
smtp_port INTEGER,
smtp_tls TEXT,
credential_enc BLOB,
oauth_expires_at DATETIME,
sync_state TEXT,
cache_retention_days INTEGER,
last_sync_at DATETIME,
last_sync_error TEXT,
active INTEGER NOT NULL DEFAULT 1,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_linked_accounts_user ON linked_accounts(user_id);
`,
},
},
{
name: "0008_dav_contacts_calendars",
sql: map[string]string{
"sqlite": `
CREATE TABLE addressbooks (
id TEXT PRIMARY KEY,
owner_type TEXT NOT NULL,
owner_id TEXT NOT NULL,
display_name TEXT,
description TEXT,
sync_token TEXT NOT NULL DEFAULT '1',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_addressbooks_owner ON addressbooks(owner_type, owner_id);
CREATE TABLE contacts (
id TEXT PRIMARY KEY,
addressbook_id TEXT NOT NULL REFERENCES addressbooks(id) ON DELETE CASCADE,
uid TEXT NOT NULL,
vcard_enc BLOB NOT NULL,
etag TEXT NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE(addressbook_id, uid)
);
CREATE INDEX idx_contacts_addressbook ON contacts(addressbook_id);
CREATE TABLE calendars (
id TEXT PRIMARY KEY,
owner_type TEXT NOT NULL,
owner_id TEXT NOT NULL,
display_name TEXT,
description TEXT,
color TEXT,
timezone TEXT NOT NULL DEFAULT 'UTC',
sync_token TEXT NOT NULL DEFAULT '1',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_calendars_owner ON calendars(owner_type, owner_id);
CREATE TABLE calendar_objects (
id TEXT PRIMARY KEY,
calendar_id TEXT NOT NULL REFERENCES calendars(id) ON DELETE CASCADE,
uid TEXT NOT NULL,
ical_enc BLOB NOT NULL,
component_type TEXT,
summary TEXT,
dtstart DATETIME,
dtend DATETIME,
etag TEXT NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE(calendar_id, uid)
);
CREATE INDEX idx_calendar_objects_calendar ON calendar_objects(calendar_id);
`,
},
},
{
name: "0009_sieve_scripts",
sql: map[string]string{
"sqlite": `
CREATE TABLE sieve_scripts (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
name TEXT NOT NULL,
script_text TEXT NOT NULL,
active INTEGER NOT NULL DEFAULT 0,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE(user_id, name)
);
CREATE INDEX idx_sieve_scripts_user ON sieve_scripts(user_id);
`,
},
},
{
name: "0010_tls_certs",
sql: map[string]string{
"sqlite": `
CREATE TABLE tls_certs (
id TEXT PRIMARY KEY,
domain TEXT UNIQUE NOT NULL,
cert_pem_enc BLOB,
key_pem_enc BLOB,
expires_at DATETIME,
acme_account_key_enc BLOB,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_tls_certs_domain ON tls_certs(domain);
`,
},
},
{
name: "0011_mfa_and_recovery",
sql: map[string]string{
"sqlite": `
CREATE TABLE mfa_backup_codes (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
code_hash TEXT NOT NULL,
used_at DATETIME,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_mfa_backup_codes_user ON mfa_backup_codes(user_id);
ALTER TABLE users ADD COLUMN recovery_email TEXT;
`,
},
},
}