Initial commit

This commit is contained in:
Neil Alexander
2021-07-07 18:15:07 +01:00
commit ceffe7612d
26 changed files with 2130 additions and 0 deletions

View File

@@ -0,0 +1,40 @@
package sqlite3
import (
"database/sql"
"fmt"
_ "github.com/mattn/go-sqlite3"
)
type SQLite3Storage struct {
*TableConfig
*TableUsers
*TableMailboxes
*TableMails
}
func NewSQLite3StorageStorage(filename string) (*SQLite3Storage, error) {
db, err := sql.Open("sqlite3", "file:"+filename+"?_foreign_keys=on")
if err != nil {
return nil, fmt.Errorf("sql.Open: %w", err)
}
s := &SQLite3Storage{}
s.TableConfig, err = NewTableConfig(db)
if err != nil {
return nil, fmt.Errorf("NewTableConfig: %w", err)
}
s.TableUsers, err = NewTableUsers(db)
if err != nil {
return nil, fmt.Errorf("NewTableUsers: %w", err)
}
s.TableMailboxes, err = NewTableMailboxes(db)
if err != nil {
return nil, fmt.Errorf("NewTableMailboxes: %w", err)
}
s.TableMails, err = NewTableMails(db)
if err != nil {
return nil, fmt.Errorf("NewTableMails: %w", err)
}
return s, nil
}

View File

@@ -0,0 +1,61 @@
package sqlite3
import (
"database/sql"
"fmt"
)
type TableConfig struct {
db *sql.DB
get *sql.Stmt
set *sql.Stmt
}
const configSchema = `
CREATE TABLE IF NOT EXISTS config (
key TEXT NOT NULL,
value TEXT NOT NULL,
PRIMARY KEY(key)
);
`
const configGet = `
SELECT value FROM config WHERE key = $1
`
const configSet = `
INSERT OR REPLACE INTO config (key, value) VALUES($1, $2)
`
func NewTableConfig(db *sql.DB) (*TableConfig, error) {
t := &TableConfig{
db: db,
}
_, err := db.Exec(configSchema)
if err != nil {
return nil, fmt.Errorf("db.Exec: %w", err)
}
t.get, err = db.Prepare(configGet)
if err != nil {
return nil, fmt.Errorf("db.Prepare(get): %w", err)
}
t.set, err = db.Prepare(configSet)
if err != nil {
return nil, fmt.Errorf("db.Prepare(set): %w", err)
}
return t, nil
}
func (t *TableConfig) ConfigGet(key string) (string, error) {
var value string
err := t.get.QueryRow(key).Scan(&value)
if err == sql.ErrNoRows {
return "", nil
}
return value, err
}
func (t *TableConfig) ConfigSet(key, value string) error {
_, err := t.set.Exec(key, value)
return err
}

View File

@@ -0,0 +1,152 @@
package sqlite3
import (
"database/sql"
"fmt"
)
type TableMailboxes struct {
db *sql.DB
selectMailboxes *sql.Stmt
listMailboxes *sql.Stmt
listMailboxesSubscribed *sql.Stmt
createMailbox *sql.Stmt
renameMailbox *sql.Stmt
deleteMailbox *sql.Stmt
subscribeMailbox *sql.Stmt
}
const mailboxesSchema = `
CREATE TABLE IF NOT EXISTS mailboxes (
username TEXT NOT NULL REFERENCES users(username) ON DELETE CASCADE ON UPDATE CASCADE,
mailbox TEXT NOT NULL DEFAULT('INBOX'),
subscribed BOOLEAN NOT NULL DEFAULT 1,
PRIMARY KEY(username, mailbox)
);
`
const mailboxesList = `
SELECT mailbox FROM mailboxes WHERE username = $1
`
const mailboxesListSubscribed = `
SELECT mailbox FROM mailboxes WHERE username = $1 AND subscribed = 1
`
const mailboxesSelect = `
SELECT mailbox FROM mailboxes WHERE username = $1 AND mailbox = $2
`
const mailboxesCreate = `
INSERT INTO mailboxes (username, mailbox) VALUES($1, $2)
`
const mailboxesRename = `
UPDATE mailboxes SET mailbox = $1 WHERE username = $2 AND mailbox = $3
`
const mailboxesDelete = `
DELETE FROM mailboxes WHERE username = $1 AND mailbox = $2
`
const mailboxesSubscribe = `
UPDATE mailboxes SET subscribed = $1 WHERE username = $2 AND mailbox = $3
`
func NewTableMailboxes(db *sql.DB) (*TableMailboxes, error) {
t := &TableMailboxes{
db: db,
}
_, err := db.Exec(mailboxesSchema)
if err != nil {
return nil, fmt.Errorf("db.Exec: %w", err)
}
t.listMailboxes, err = db.Prepare(mailboxesList)
if err != nil {
return nil, fmt.Errorf("db.Prepare(mailboxesCreate): %w", err)
}
t.listMailboxesSubscribed, err = db.Prepare(mailboxesListSubscribed)
if err != nil {
return nil, fmt.Errorf("db.Prepare(mailboxesCreate): %w", err)
}
t.selectMailboxes, err = db.Prepare(mailboxesSelect)
if err != nil {
return nil, fmt.Errorf("db.Prepare(mailboxesSelect): %w", err)
}
t.createMailbox, err = db.Prepare(mailboxesCreate)
if err != nil {
return nil, fmt.Errorf("db.Prepare(mailboxesCreate): %w", err)
}
t.deleteMailbox, err = db.Prepare(mailboxesDelete)
if err != nil {
return nil, fmt.Errorf("db.Prepare(mailboxesDelete): %w", err)
}
t.renameMailbox, err = db.Prepare(mailboxesRename)
if err != nil {
return nil, fmt.Errorf("db.Prepare(mailboxesRename): %w", err)
}
t.subscribeMailbox, err = db.Prepare(mailboxesSubscribe)
if err != nil {
return nil, fmt.Errorf("db.Prepare(mailboxesSubscribe): %w", err)
}
return t, nil
}
func (t *TableMailboxes) MailboxList(user string, onlySubscribed bool) ([]string, error) {
stmt := t.listMailboxes
if onlySubscribed {
stmt = t.listMailboxesSubscribed
}
rows, err := stmt.Query(user)
if err != nil {
return nil, fmt.Errorf("t.listMailboxes.Query: %w", err)
}
defer rows.Close()
var mailboxes []string
for rows.Next() {
var mailbox string
if err := rows.Scan(&mailbox); err != nil {
return nil, fmt.Errorf("rows.Scan: %w", err)
}
mailboxes = append(mailboxes, mailbox)
}
return mailboxes, nil
}
func (t *TableMailboxes) MailboxSelect(user, mailbox string) (bool, error) {
row := t.selectMailboxes.QueryRow(user, mailbox)
if err := row.Err(); err != nil && err != sql.ErrNoRows {
return false, fmt.Errorf("row.Err: %w", err)
} else if err == sql.ErrNoRows {
return false, nil
}
var got string
if err := row.Scan(&got); err != nil {
return false, fmt.Errorf("row.Scan: %w", err)
}
return mailbox == got, nil
}
func (t *TableMailboxes) MailboxCreate(user, name string) error {
_, err := t.createMailbox.Exec(user, name)
return err
}
func (t *TableMailboxes) MailboxRename(user, old, new string) error {
_, err := t.renameMailbox.Exec(new, user, old)
return err
}
func (t *TableMailboxes) MailboxDelete(user, name string) error {
_, err := t.deleteMailbox.Exec(user, name)
return err
}
func (t *TableMailboxes) MailboxSubscribe(user, name string, subscribed bool) error {
sn := 1
if !subscribed {
sn = 0
}
_, err := t.subscribeMailbox.Exec(sn, user, name)
return err
}

View File

@@ -0,0 +1,227 @@
package sqlite3
import (
"database/sql"
"fmt"
"time"
)
type TableMails struct {
db *sql.DB
selectMails *sql.Stmt
selectMail *sql.Stmt
selectMailNextID *sql.Stmt
selectIDForSeq *sql.Stmt
searchMail *sql.Stmt
createMail *sql.Stmt
countMails *sql.Stmt
countUnseenMails *sql.Stmt
updateMailFlags *sql.Stmt
deleteMail *sql.Stmt
expungeMail *sql.Stmt
}
const mailsSchema = `
CREATE TABLE IF NOT EXISTS mails (
username TEXT NOT NULL,
mailbox TEXT NOT NULL,
id INTEGER NOT NULL DEFAULT 1,
mail BLOB NOT NULL,
datetime INTEGER NOT NULL,
seen BOOLEAN NOT NULL DEFAULT 0, -- the mail has been read
answered BOOLEAN NOT NULL DEFAULT 0, -- the mail has been replied to
flagged BOOLEAN NOT NULL DEFAULT 0, -- the mail has been flagged for later attention
deleted BOOLEAN NOT NULL DEFAULT 0, -- the email is marked for deletion at next EXPUNGE
PRIMARY KEY(username, mailbox, id),
FOREIGN KEY (username, mailbox) REFERENCES mailboxes(username, mailbox) ON DELETE CASCADE ON UPDATE CASCADE
);
DROP VIEW IF EXISTS inboxes;
CREATE VIEW IF NOT EXISTS inboxes AS SELECT * FROM (
SELECT ROW_NUMBER() OVER (PARTITION BY username, mailbox) AS seq, * FROM mails
)
ORDER BY username, mailbox, id;
`
const selectMailsStmt = `
SELECT * FROM inboxes
ORDER BY username, mailbox, id
`
const selectMailStmt = `
SELECT seq, id, mail, datetime, seen, answered, flagged, deleted FROM inboxes
WHERE username = $1 AND mailbox = $2 AND id = $3
ORDER BY username, mailbox, id
`
const selectMailCountStmt = `
SELECT COUNT(*) FROM mails WHERE username = $1 AND mailbox = $2
`
const selectMailUnseenStmt = `
SELECT COUNT(*) FROM mails WHERE username = $1 AND mailbox = $2 AND seen = 0
`
const searchMailStmt = `
SELECT id FROM mails
WHERE username = $1 AND mailbox = $2
ORDER BY username, mailbox, id
`
const insertMailStmt = `
INSERT INTO mails (username, mailbox, id, mail, datetime) VALUES(
$1, $2, (
SELECT IFNULL(MAX(id)+1,1) AS id FROM mails
WHERE username = $1 AND mailbox = $2
), $3, $4
)
RETURNING id;
`
const selectIDForSeqStmt = `
SELECT id FROM inboxes
WHERE username = $1 AND mailbox = $2 AND seq = $3
`
const selectMailNextID = `
SELECT IFNULL(MAX(id)+1,1) AS id FROM mails
WHERE username = $1 AND mailbox = $2
`
const updateMailFlagsStmt = `
UPDATE mails SET seen = $1, answered = $2, flagged = $3, deleted = $4 WHERE username = $5 AND mailbox = $6 AND id = $7
`
const deleteMailStmt = `
UPDATE mails SET deleted = 1 WHERE username = $1 AND mailbox = $2 AND id = $3
`
const expungeMailStmt = `
DELETE FROM mails WHERE username = $1 AND mailbox = $2 AND deleted = 1
`
func NewTableMails(db *sql.DB) (*TableMails, error) {
t := &TableMails{
db: db,
}
_, err := db.Exec(mailsSchema)
if err != nil {
return nil, fmt.Errorf("db.Exec: %w", err)
}
t.selectMails, err = db.Prepare(selectMailsStmt)
if err != nil {
return nil, fmt.Errorf("db.Prepare(selectMailsStmt): %w", err)
}
t.selectMail, err = db.Prepare(selectMailStmt)
if err != nil {
return nil, fmt.Errorf("db.Prepare(selectMailStmt): %w", err)
}
t.selectMailNextID, err = db.Prepare(selectMailNextID)
if err != nil {
return nil, fmt.Errorf("db.Prepare(selectMailNextID): %w", err)
}
t.selectIDForSeq, err = db.Prepare(selectIDForSeqStmt)
if err != nil {
return nil, fmt.Errorf("db.Prepare(selectPIDForIDStmt): %w", err)
}
t.searchMail, err = db.Prepare(searchMailStmt)
if err != nil {
return nil, fmt.Errorf("db.Prepare(selectPIDForIDStmt): %w", err)
}
t.createMail, err = db.Prepare(insertMailStmt)
if err != nil {
return nil, fmt.Errorf("db.Prepare(insertMailStmt): %w", err)
}
t.updateMailFlags, err = db.Prepare(updateMailFlagsStmt)
if err != nil {
return nil, fmt.Errorf("db.Prepare(updateMailSeenStmt): %w", err)
}
t.deleteMail, err = db.Prepare(deleteMailStmt)
if err != nil {
return nil, fmt.Errorf("db.Prepare(deleteMailStmt): %w", err)
}
t.expungeMail, err = db.Prepare(expungeMailStmt)
if err != nil {
return nil, fmt.Errorf("db.Prepare(expungeMailStmt): %w", err)
}
t.countMails, err = db.Prepare(selectMailCountStmt)
if err != nil {
return nil, fmt.Errorf("db.Prepare(selectMailCountStmt): %w", err)
}
t.countUnseenMails, err = db.Prepare(selectMailUnseenStmt)
if err != nil {
return nil, fmt.Errorf("db.Prepare(selectMailUnseenStmt): %w", err)
}
return t, nil
}
func (t *TableMails) MailCreate(user, mailbox string, data []byte) (int, error) {
var id int
err := t.createMail.QueryRow(user, mailbox, data, time.Now().Unix()).Scan(&id)
return id, err
}
func (t *TableMails) MailSelect(user, mailbox string, id int) (int, int, []byte, bool, bool, bool, bool, time.Time, error) {
var data []byte
var seen, answered, flagged, deleted bool
var ts int64
var seq, pid int
err := t.selectMail.QueryRow(user, mailbox, id).Scan(&seq, &pid, &data, &ts, &seen, &answered, &flagged, &deleted)
return seq, pid, data, seen, answered, flagged, deleted, time.Unix(ts, 0), err
}
func (t *TableMails) MailSearch(user, mailbox string) ([]uint32, error) {
var ids []uint32
rows, err := t.searchMail.Query(user, mailbox)
if err != nil {
return nil, fmt.Errorf("t.searchMail.Query: %w", err)
}
defer rows.Close()
for rows.Next() {
var id uint32
if err := rows.Scan(&id); err != nil {
return nil, fmt.Errorf("rows.Scan: %w", err)
}
ids = append(ids, id)
}
return ids, nil
}
func (t *TableMails) MailNextID(user, mailbox string) (int, error) {
var id int
err := t.selectMailNextID.QueryRow(user, mailbox).Scan(&id)
return id, err
}
func (t *TableMails) MailIDForSeq(user, mailbox string, seq int) (int, error) {
var id int
err := t.selectIDForSeq.QueryRow(user, mailbox, seq).Scan(&id)
return id, err
}
func (t *TableMails) MailUnseen(user, mailbox string) (int, error) {
var unseen int
err := t.countUnseenMails.QueryRow(user, mailbox).Scan(&unseen)
return unseen, err
}
func (t *TableMails) MailUpdateFlags(user, mailbox string, id int, seen, answered, flagged, deleted bool) error {
_, err := t.updateMailFlags.Exec(seen, answered, flagged, deleted, user, mailbox, id)
return err
}
func (t *TableMails) MailDelete(user, mailbox, id string) error {
_, err := t.deleteMail.Exec(user, mailbox, id)
return err
}
func (t *TableMails) MailExpunge(user, mailbox string) error {
_, err := t.expungeMail.Exec(user, mailbox)
return err
}
func (t *TableMails) MailCount(user, mailbox string) (int, error) {
var count int
err := t.countMails.QueryRow(user, mailbox).Scan(&count)
return count, err
}

View File

@@ -0,0 +1,76 @@
package sqlite3
import (
"database/sql"
"fmt"
"golang.org/x/crypto/bcrypt"
)
type TableUsers struct {
db *sql.DB
getPassword *sql.Stmt
insertUser *sql.Stmt
}
const usersSchema = `
CREATE TABLE IF NOT EXISTS users (
username TEXT NOT NULL,
password TEXT NOT NULL,
PRIMARY KEY(username)
);
`
const usersGetPassword = `
SELECT password FROM users WHERE username = $1
`
const usersInsertUser = `
INSERT INTO users (username, password) VALUES($1, $2)
`
func NewTableUsers(db *sql.DB) (*TableUsers, error) {
t := &TableUsers{
db: db,
}
_, err := db.Exec(usersSchema)
if err != nil {
return nil, fmt.Errorf("db.Exec: %w", err)
}
t.getPassword, err = db.Prepare(usersGetPassword)
if err != nil {
return nil, fmt.Errorf("db.Prepare(getPassword): %w", err)
}
t.insertUser, err = db.Prepare(usersInsertUser)
if err != nil {
return nil, fmt.Errorf("db.Prepare(usersInsertUser): %w", err)
}
return t, nil
}
func (t *TableUsers) CreateUser(username, password string) error {
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return fmt.Errorf("bcrypt.GenerateFromPassword: %w", err)
}
if _, err := t.insertUser.Exec(username, hash); err != nil {
return fmt.Errorf("t.insertUser.Exec: %w", err)
}
return nil
}
func (t *TableUsers) TryAuthenticate(username, password string) (bool, error) {
var dbPasswordHash []byte
err := t.getPassword.QueryRow(username).Scan(&dbPasswordHash)
if err == sql.ErrNoRows {
return false, nil
}
if err != nil {
return false, err
}
err = bcrypt.CompareHashAndPassword(dbPasswordHash, []byte(password))
if err == nil {
return true, nil
}
return false, err
}

View File

@@ -0,0 +1,28 @@
package storage
import "time"
type Storage interface {
ConfigGet(key string) (string, error)
ConfigSet(key, value string) error
TryAuthenticate(username, password string) (bool, error)
MailboxSelect(user, mailbox string) (bool, error)
MailNextID(user, mailbox string) (int, error)
MailIDForSeq(user, mailbox string, id int) (int, error)
MailUnseen(user, mailbox string) (int, error)
MailboxList(user string, onlySubscribed bool) ([]string, error)
MailboxCreate(user, name string) error
MailboxRename(user, old, new string) error
MailboxDelete(user, name string) error
MailboxSubscribe(user, name string, subscribed bool) error
MailCreate(user, mailbox string, data []byte) (int, error)
MailSelect(user, mailbox string, id int) (int, int, []byte, bool, bool, bool, bool, time.Time, error)
MailSearch(user, mailbox string) ([]uint32, error)
MailUpdateFlags(user, mailbox string, id int, seen, answered, flagged, deleted bool) error
MailDelete(user, mailbox, id string) error
MailExpunge(user, mailbox string) error
MailCount(user, mailbox string) (int, error)
}