package store import ( "database/sql" "errors" "fmt" "git.dcglab.co.uk/steve/emcli/internal/crypto" ) var ErrAccountNotFound = errors.New("account not found") // Account is the decrypted, in-memory view of a configured account. type Account struct { ID int64 Name string Mode string // RO | RW IMAPHost string IMAPPort int IMAPSecurity string // tls | starttls AuthType string // password | oauth2 Username string Password string // decrypted; empty in ListAccounts WhitelistInEnabled bool WhitelistOutEnabled bool SubjectRegex string ProcessBacklog bool } func (s *Store) AddAccount(a Account) (int64, error) { var encPw []byte if a.Password != "" { b, err := crypto.Seal(s.key, []byte(a.Password)) if err != nil { return 0, err } encPw = b } res, err := s.db.Exec(` INSERT INTO accounts (name,mode,imap_host,imap_port,imap_security,auth_type,username, enc_password,whitelist_in_enabled,whitelist_out_enabled,subject_regex,process_backlog) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)`, a.Name, a.Mode, a.IMAPHost, a.IMAPPort, a.IMAPSecurity, a.AuthType, a.Username, encPw, b2i(a.WhitelistInEnabled), b2i(a.WhitelistOutEnabled), nullStr(a.SubjectRegex), b2i(a.ProcessBacklog)) if err != nil { return 0, fmt.Errorf("insert account: %w", err) } return res.LastInsertId() } func (s *Store) GetAccount(name string) (Account, error) { row := s.db.QueryRow(` SELECT id,name,mode,imap_host,imap_port,imap_security,auth_type,username, enc_password,whitelist_in_enabled,whitelist_out_enabled,subject_regex,process_backlog FROM accounts WHERE name = ?`, name) a, encPw, err := scanAccount(row) if errors.Is(err, sql.ErrNoRows) { return Account{}, ErrAccountNotFound } if err != nil { return Account{}, err } if len(encPw) > 0 { pw, err := crypto.Open(s.key, encPw) if err != nil { return Account{}, fmt.Errorf("decrypt password (wrong EMCLI_KEY?): %w", err) } a.Password = string(pw) } return a, nil } func (s *Store) ListAccounts() ([]Account, error) { rows, err := s.db.Query(` SELECT id,name,mode,imap_host,imap_port,imap_security,auth_type,username, enc_password,whitelist_in_enabled,whitelist_out_enabled,subject_regex,process_backlog FROM accounts ORDER BY name`) if err != nil { return nil, err } defer rows.Close() var out []Account for rows.Next() { a, _, err := scanAccount(rows) // secrets discarded if err != nil { return nil, err } out = append(out, a) } return out, rows.Err() } func (s *Store) DeleteAccount(name string) error { res, err := s.db.Exec("DELETE FROM accounts WHERE name = ?", name) if err != nil { return err } if n, _ := res.RowsAffected(); n == 0 { return ErrAccountNotFound } return nil } // scanner is satisfied by *sql.Row and *sql.Rows. type scanner interface{ Scan(dest ...any) error } func scanAccount(sc scanner) (Account, []byte, error) { var ( a Account encPw []byte subj sql.NullString wlIn, wlOut int backlog int ) err := sc.Scan(&a.ID, &a.Name, &a.Mode, &a.IMAPHost, &a.IMAPPort, &a.IMAPSecurity, &a.AuthType, &a.Username, &encPw, &wlIn, &wlOut, &subj, &backlog) if err != nil { return Account{}, nil, err } a.WhitelistInEnabled = wlIn != 0 a.WhitelistOutEnabled = wlOut != 0 a.ProcessBacklog = backlog != 0 a.SubjectRegex = subj.String return a, encPw, nil } func b2i(b bool) int { if b { return 1 } return 0 } func nullStr(s string) any { if s == "" { return nil } return s }