/* * Copyright (c) 2024 Your Name / Yggmail Contributor * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ package sqlite3 import ( "database/sql" "fmt" "github.com/neilalexander/yggmail/internal/storage/types" ) type TableAccounts struct { db *sql.DB writer *Writer selectAccount *sql.Stmt listAccounts *sql.Stmt createAccount *sql.Stmt updateAccount *sql.Stmt deleteAccount *sql.Stmt } const accountsSchema = ` CREATE TABLE IF NOT EXISTS accounts ( pubkey TEXT NOT NULL, aliases TEXT, passwd_hash TEXT, private_key TEXT, PRIMARY KEY(pubkey) ); ` const accountsSelect = ` SELECT pubkey, aliases, passwd_hash, private_key FROM accounts WHERE pubkey = $1 ` const accountsList = ` SELECT pubkey, aliases, passwd_hash, private_key FROM accounts ` const accountsCreate = ` INSERT OR IGNORE INTO accounts (pubkey, aliases, passwd_hash, private_key) VALUES($1, $2, $3, $4) ` const accountsUpdate = ` UPDATE accounts SET aliases = $1, passwd_hash = $2, private_key = $3 WHERE pubkey = $4 ` const accountsDelete = ` DELETE FROM accounts WHERE pubkey = $1 ` func NewTableAccounts(db *sql.DB, writer *Writer) (*TableAccounts, error) { t := &TableAccounts{ db: db, writer: writer, } _, err := db.Exec(accountsSchema) if err != nil { return nil, fmt.Errorf("db.Exec: %w", err) } t.selectAccount, err = db.Prepare(accountsSelect) if err != nil { return nil, fmt.Errorf("db.Prepare(accountsSelect): %w", err) } t.listAccounts, err = db.Prepare(accountsList) if err != nil { return nil, fmt.Errorf("db.Prepare(accountsList): %w", err) } t.createAccount, err = db.Prepare(accountsCreate) if err != nil { return nil, fmt.Errorf("db.Prepare(accountsCreate): %w", err) } t.updateAccount, err = db.Prepare(accountsUpdate) if err != nil { return nil, fmt.Errorf("db.Prepare(accountsUpdate): %w", err) } t.deleteAccount, err = db.Prepare(accountsDelete) if err != nil { return nil, fmt.Errorf("db.Prepare(accountsDelete): %w", err) } return t, nil } func (t *TableAccounts) AccountGet(pubkey string) (*types.Account, error) { row := t.selectAccount.QueryRow(pubkey) acc := &types.Account{} err := row.Scan(&acc.PublicKey, &acc.Aliases, &acc.PasswdHash, &acc.Privatekey) if err != nil { if err == sql.ErrNoRows { return nil, nil // Аккаунт не найден } return nil, fmt.Errorf("row.Scan: %w", err) } return acc, nil } func (t *TableAccounts) AccountList() ([]*types.Account, error) { rows, err := t.listAccounts.Query() if err != nil { return nil, fmt.Errorf("t.listAccounts.Query: %w", err) } defer rows.Close() var accounts []*types.Account for rows.Next() { acc := &types.Account{} if err := rows.Scan(&acc.PublicKey, &acc.Aliases, &acc.PasswdHash, &acc.Privatekey); err != nil { return nil, fmt.Errorf("rows.Scan: %w", err) } accounts = append(accounts, acc) } return accounts, nil } func (t *TableAccounts) AccountCreate(acc *types.Account) error { // TODO: Validate that PublicKey is a valid hex before storing return t.writer.Do(t.db, nil, func(txn *sql.Tx) error { _, err := t.createAccount.Exec(acc.PublicKey, acc.Aliases, acc.PasswdHash, acc.Privatekey) return err }) } func (t *TableAccounts) AccountUpdate(acc *types.Account) error { // TODO: Ensure we don't accidentally overwrite PrivateKey with empty string if not provided in update return t.writer.Do(t.db, nil, func(txn *sql.Tx) error { _, err := t.updateAccount.Exec(acc.Aliases, acc.PasswdHash, acc.Privatekey, acc.PublicKey) return err }) } func (t *TableAccounts) AccountDelete(pubkey string) error { // TODO: Delete associated mails in TableMails when an account is deleted? // Or use ON DELETE CASCADE in future schema migrations. return t.writer.Do(t.db, nil, func(txn *sql.Tx) error { _, err := t.deleteAccount.Exec(pubkey) return err }) }