191 lines
6.2 KiB
Go
191 lines
6.2 KiB
Go
// Package db is the SQLite data layer, mirroring email_server/models.py. It uses plain
|
|
// database/sql + hand-written SQL rather than an ORM — the schema is small and fixed,
|
|
// so an ORM would be an unrequested abstraction.
|
|
package db
|
|
|
|
import (
|
|
"database/sql"
|
|
"fmt"
|
|
|
|
_ "modernc.org/sqlite"
|
|
)
|
|
|
|
// schema creates all esrv_* tables if missing. There is no migration framework here,
|
|
// matching the Python precedent (its own migrations/ directory is a single manual SQL
|
|
// patch file, never auto-applied) — CREATE TABLE IF NOT EXISTS covers the whole surface.
|
|
const schema = `
|
|
CREATE TABLE IF NOT EXISTS esrv_domains (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
domain_name TEXT NOT NULL UNIQUE,
|
|
is_active INTEGER NOT NULL DEFAULT 1,
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
verification_token TEXT NOT NULL DEFAULT '',
|
|
is_verified INTEGER NOT NULL DEFAULT 0,
|
|
verified_at DATETIME
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS esrv_senders (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
email TEXT NOT NULL UNIQUE,
|
|
password_hash TEXT NOT NULL,
|
|
domain_id INTEGER NOT NULL REFERENCES esrv_domains(id),
|
|
can_send_as_domain INTEGER NOT NULL DEFAULT 0,
|
|
is_active INTEGER NOT NULL DEFAULT 1,
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
store_message_content INTEGER NOT NULL DEFAULT 0
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS esrv_whitelisted_ips (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
ip_address TEXT NOT NULL,
|
|
domain_id INTEGER NOT NULL REFERENCES esrv_domains(id),
|
|
is_active INTEGER NOT NULL DEFAULT 1,
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
store_message_content INTEGER NOT NULL DEFAULT 0
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS esrv_email_logs (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
message_id TEXT NOT NULL UNIQUE,
|
|
timestamp DATETIME NOT NULL,
|
|
peer_ip TEXT NOT NULL,
|
|
mail_from TEXT NOT NULL,
|
|
to_address TEXT NOT NULL DEFAULT '',
|
|
cc_addresses TEXT DEFAULT '',
|
|
bcc_addresses TEXT DEFAULT '',
|
|
subject TEXT,
|
|
email_headers TEXT NOT NULL,
|
|
message_body TEXT,
|
|
status TEXT NOT NULL,
|
|
dkim_signed INTEGER NOT NULL DEFAULT 0,
|
|
username TEXT,
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS esrv_email_recipient_logs (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
email_log_id INTEGER NOT NULL REFERENCES esrv_email_logs(id),
|
|
recipient TEXT NOT NULL,
|
|
recipient_type TEXT NOT NULL,
|
|
status TEXT NOT NULL,
|
|
error_code TEXT,
|
|
error_message TEXT,
|
|
server_response TEXT
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS esrv_auth_logs (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
auth_type TEXT NOT NULL,
|
|
identifier TEXT NOT NULL,
|
|
ip_address TEXT,
|
|
success INTEGER NOT NULL,
|
|
message TEXT,
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS esrv_dkim_keys (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
domain_id INTEGER NOT NULL REFERENCES esrv_domains(id),
|
|
selector TEXT NOT NULL DEFAULT 'default',
|
|
private_key TEXT NOT NULL,
|
|
public_key TEXT NOT NULL,
|
|
is_active INTEGER NOT NULL DEFAULT 1,
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
replaced_at DATETIME
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS esrv_custom_headers (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
domain_id INTEGER NOT NULL REFERENCES esrv_domains(id),
|
|
header_name TEXT NOT NULL,
|
|
header_value TEXT NOT NULL,
|
|
is_active INTEGER NOT NULL DEFAULT 1,
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS esrv_email_attachments (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
email_log_id INTEGER NOT NULL REFERENCES esrv_email_logs(id),
|
|
filename TEXT NOT NULL,
|
|
content_type TEXT,
|
|
file_path TEXT NOT NULL,
|
|
size INTEGER,
|
|
uploaded_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS esrv_admin_users (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
username TEXT NOT NULL UNIQUE,
|
|
password_hash TEXT NOT NULL,
|
|
must_change_password INTEGER NOT NULL DEFAULT 0,
|
|
totp_secret TEXT NOT NULL DEFAULT '',
|
|
totp_enabled INTEGER NOT NULL DEFAULT 0,
|
|
is_global_admin INTEGER NOT NULL DEFAULT 0,
|
|
created_by INTEGER,
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
|
|
-- Which domains a non-global admin is allowed to see/manage. Global admins have no
|
|
-- rows here at all — their access is implicit (AdminUser.IsGlobalAdmin).
|
|
CREATE TABLE IF NOT EXISTS esrv_admin_domain_access (
|
|
admin_user_id INTEGER NOT NULL REFERENCES esrv_admin_users(id),
|
|
domain_id INTEGER NOT NULL REFERENCES esrv_domains(id),
|
|
PRIMARY KEY (admin_user_id, domain_id)
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS esrv_admin_sessions (
|
|
token TEXT PRIMARY KEY,
|
|
user_id INTEGER NOT NULL REFERENCES esrv_admin_users(id),
|
|
mfa_verified INTEGER NOT NULL DEFAULT 0,
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
expires_at DATETIME NOT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS esrv_webauthn_credentials (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
user_id INTEGER NOT NULL REFERENCES esrv_admin_users(id),
|
|
name TEXT NOT NULL DEFAULT '',
|
|
credential_id TEXT NOT NULL UNIQUE,
|
|
credential_data TEXT NOT NULL,
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
`
|
|
|
|
// migrateAddedColumns best-effort ALTER TABLEs the columns added to esrv_domains
|
|
// after its first release, for dev DBs created before this feature existed.
|
|
// CREATE TABLE IF NOT EXISTS doesn't retrofit columns onto an existing table, and
|
|
// there's no migration framework here (see the schema comment above) — errors are
|
|
// ignored since SQLite has no "ADD COLUMN IF NOT EXISTS" and a duplicate-column
|
|
// error just means the column is already there.
|
|
func migrateAddedColumns(db *sql.DB) {
|
|
stmts := []string{
|
|
`ALTER TABLE esrv_domains ADD COLUMN verification_token TEXT NOT NULL DEFAULT ''`,
|
|
`ALTER TABLE esrv_domains ADD COLUMN is_verified INTEGER NOT NULL DEFAULT 0`,
|
|
`ALTER TABLE esrv_domains ADD COLUMN verified_at DATETIME`,
|
|
`ALTER TABLE esrv_admin_users ADD COLUMN is_global_admin INTEGER NOT NULL DEFAULT 0`,
|
|
`ALTER TABLE esrv_admin_users ADD COLUMN created_by INTEGER`,
|
|
}
|
|
for _, stmt := range stmts {
|
|
db.Exec(stmt)
|
|
}
|
|
}
|
|
|
|
// DB wraps *sql.DB with the query helpers below.
|
|
type DB struct {
|
|
*sql.DB
|
|
}
|
|
|
|
// Open opens (creating if needed) the SQLite file at path and ensures the schema exists.
|
|
func Open(path string) (*DB, error) {
|
|
sqlDB, err := sql.Open("sqlite", path)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("open sqlite: %w", err)
|
|
}
|
|
if _, err := sqlDB.Exec(schema); err != nil {
|
|
sqlDB.Close()
|
|
return nil, fmt.Errorf("create tables: %w", err)
|
|
}
|
|
migrateAddedColumns(sqlDB)
|
|
return &DB{sqlDB}, nil
|
|
}
|