first commit
This commit is contained in:
@@ -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;
|
||||
`,
|
||||
},
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user