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
}