package db import ( "context" "database/sql" "fmt" "os" "path/filepath" _ "modernc.org/sqlite" ) type Store struct { DB *sql.DB } func Open(path string) (*Store, error) { if path != ":memory:" { if err := os.MkdirAll(filepath.Dir(path), 0750); err != nil { return nil, fmt.Errorf("create database directory: %w", err) } } db, err := sql.Open("sqlite", path) if err != nil { return nil, fmt.Errorf("open sqlite: %w", err) } if _, err := db.Exec("PRAGMA foreign_keys = ON"); err != nil { _ = db.Close() return nil, fmt.Errorf("enable foreign keys: %w", err) } if path == ":memory:" { db.SetMaxOpenConns(1) } return &Store{DB: db}, nil } func (s *Store) Close() error { if s == nil || s.DB == nil { return nil } return s.DB.Close() } func (s *Store) Ping(ctx context.Context) error { return s.DB.PingContext(ctx) } func (s *Store) Migrate(ctx context.Context) error { _, err := s.DB.ExecContext(ctx, schemaSQL) if err != nil { return fmt.Errorf("migrate sqlite: %w", err) } return nil } const schemaSQL = ` CREATE TABLE IF NOT EXISTS mail_messages ( id INTEGER PRIMARY KEY AUTOINCREMENT, account_id TEXT NOT NULL, pop3_uid TEXT NOT NULL, message_id_header TEXT, in_reply_to TEXT, references_header TEXT, subject TEXT, sender_name TEXT, sender_email TEXT, to_json TEXT NOT NULL DEFAULT '[]', cc_json TEXT NOT NULL DEFAULT '[]', date_header TEXT, sent_at TEXT, received_at TEXT NOT NULL, body_text TEXT, body_html TEXT, raw_eml_path TEXT, has_attachment INTEGER NOT NULL DEFAULT 0, local_read INTEGER NOT NULL DEFAULT 0, local_processed INTEGER NOT NULL DEFAULT 0, local_archived INTEGER NOT NULL DEFAULT 0, local_deleted INTEGER NOT NULL DEFAULT 0, local_replied INTEGER NOT NULL DEFAULT 0, created_at TEXT NOT NULL, updated_at TEXT NOT NULL, UNIQUE(account_id, pop3_uid) ); CREATE INDEX IF NOT EXISTS idx_mail_messages_account_uid ON mail_messages(account_id, pop3_uid); CREATE INDEX IF NOT EXISTS idx_mail_messages_message_id ON mail_messages(message_id_header); CREATE INDEX IF NOT EXISTS idx_mail_messages_sender ON mail_messages(sender_email); CREATE INDEX IF NOT EXISTS idx_mail_messages_sent_at ON mail_messages(sent_at); CREATE INDEX IF NOT EXISTS idx_mail_messages_status ON mail_messages(local_read, local_processed, local_archived, local_deleted, local_replied); CREATE TABLE IF NOT EXISTS attachments ( id INTEGER PRIMARY KEY AUTOINCREMENT, message_id INTEGER NOT NULL, file_name TEXT NOT NULL, content_type TEXT, content_id TEXT, size_bytes INTEGER NOT NULL DEFAULT 0, storage_path TEXT NOT NULL, created_at TEXT NOT NULL, FOREIGN KEY(message_id) REFERENCES mail_messages(id) ON DELETE CASCADE ); CREATE INDEX IF NOT EXISTS idx_attachments_message ON attachments(message_id); CREATE TABLE IF NOT EXISTS labels ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL UNIQUE, color TEXT, created_at TEXT NOT NULL, updated_at TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS message_labels ( message_id INTEGER NOT NULL, label_id INTEGER NOT NULL, created_at TEXT NOT NULL, PRIMARY KEY(message_id, label_id), FOREIGN KEY(message_id) REFERENCES mail_messages(id) ON DELETE CASCADE, FOREIGN KEY(label_id) REFERENCES labels(id) ON DELETE CASCADE ); CREATE INDEX IF NOT EXISTS idx_message_labels_label ON message_labels(label_id); CREATE TABLE IF NOT EXISTS drafts ( id INTEGER PRIMARY KEY AUTOINCREMENT, to_json TEXT NOT NULL DEFAULT '[]', cc_json TEXT NOT NULL DEFAULT '[]', bcc_json TEXT NOT NULL DEFAULT '[]', subject TEXT, body TEXT, reply_to_email_id INTEGER, status TEXT NOT NULL DEFAULT 'draft', last_error TEXT, sent_message_id INTEGER, created_at TEXT NOT NULL, updated_at TEXT NOT NULL, FOREIGN KEY(reply_to_email_id) REFERENCES mail_messages(id) ON DELETE SET NULL ); CREATE INDEX IF NOT EXISTS idx_drafts_status ON drafts(status); CREATE TABLE IF NOT EXISTS sent_messages ( id INTEGER PRIMARY KEY AUTOINCREMENT, draft_id INTEGER, to_json TEXT NOT NULL DEFAULT '[]', cc_json TEXT NOT NULL DEFAULT '[]', bcc_json TEXT NOT NULL DEFAULT '[]', subject TEXT, body TEXT, message_id_header TEXT, in_reply_to TEXT, references_header TEXT, smtp_from TEXT, status TEXT NOT NULL, error_message TEXT, sent_at TEXT, created_at TEXT NOT NULL, FOREIGN KEY(draft_id) REFERENCES drafts(id) ON DELETE SET NULL ); CREATE INDEX IF NOT EXISTS idx_sent_messages_draft ON sent_messages(draft_id); CREATE INDEX IF NOT EXISTS idx_sent_messages_status ON sent_messages(status); `