Blob


1 package mailmux
3 import (
4 "database/sql"
5 "errors"
6 "fmt"
7 "net/mail"
8 "os"
9 "path"
10 "time"
12 _ "github.com/mattn/go-sqlite3"
13 "golang.org/x/crypto/bcrypt"
14 )
16 // maxUsernameSize defines a reasonable maximum length for an email address.
17 const maxUsernameLength = 255
19 // UserDB is an implementation of UserStore backed by a SQLite3 database.
20 // Users are fully authenticated after confirming ownership of their
21 // email address by supplying a matching ticket to a generated ticket file.
22 type UserDB struct {
23 *sql.DB
24 TicketDir string
25 }
27 // CreateUserDB creates the named user database file and ticket directory.
28 // If the database file already exists, it is truncated. If successfullly created,
29 // the database's tables are initialised so the database is ready to use.
30 func CreateUserDB(name, ticketDir string) (*UserDB, error) {
31 _, err := os.Create(name)
32 if err != nil {
33 return nil, err
34 }
35 if err := os.MkdirAll(ticketDir, 0666); err != nil {
36 return nil, fmt.Errorf("create user db: %w", err)
37 }
38 db, err := OpenUserDB(name, ticketDir)
39 if err != nil {
40 return nil, fmt.Errorf("create user db: %w", err)
41 }
42 return db, initialiseUserDB(db.DB)
43 }
45 // OpenUserDB opens the named user database file and ticket directory.
46 // If the database does not exist, it is created and its tables are
47 // initialised ready for use.
48 func OpenUserDB(name, dir string) (*UserDB, error) {
49 db, err := sql.Open("sqlite3", name)
50 if err != nil {
51 return nil, err
52 }
53 if err := initialiseUserDB(db); err != nil {
54 return nil, err
55 }
56 return &UserDB{db, dir}, db.Ping()
57 }
59 func initialiseUserDB(db *sql.DB) error {
60 stmt := `CREATE TABLE IF NOT EXISTS users (
61 username TEXT PRIMARY KEY,
62 password BLOB NOT NULL,
63 modtime INTEGER NOT NULL
64 );`
65 _, err := db.Exec(stmt)
66 return err
67 }
69 func (db *UserDB) Lookup(name string) (User, error) {
70 var u User
71 row := db.QueryRow("SELECT username, password FROM users WHERE username = ?", name)
72 if err := row.Scan(&u.name, &u.password); err != nil {
73 if errors.Is(err, sql.ErrNoRows) {
74 return User{}, ErrUnknownUser
75 }
76 return User{}, err
77 }
78 return u, nil
79 }
81 func (db *UserDB) add(username string, pw Password) error {
82 if len(username) > maxUsernameLength {
83 return fmt.Errorf("add %s: invalid username: too long", username)
84 }
85 addr, err := mail.ParseAddress(username)
86 if err != nil {
87 return fmt.Errorf("add %s: invalid username: %w", username, err)
88 }
89 if addr.Name != "" {
90 return fmt.Errorf("add %s: proper name present", username)
91 }
93 hashed, err := bcrypt.GenerateFromPassword(pw, bcrypt.DefaultCost)
94 if err != nil {
95 return fmt.Errorf("add %s: %w", username, err)
96 }
97 _, err = db.Exec("INSERT INTO users (username, password, modtime) VALUES (?, ?, ?)", username, hashed, time.Now().Unix())
98 if err != nil {
99 return fmt.Errorf("add %s: %w", username, err)
101 if err := createTicket(db.TicketDir, username, pw); err != nil {
102 return fmt.Errorf("add %s: create ticket: %w", username, err)
104 return nil
107 func (db *UserDB) Change(name string, new Password) error {
108 _, err := db.Lookup(name)
109 if errors.Is(err, ErrUnknownUser) {
110 return db.add(name, new)
112 if err != nil {
113 return fmt.Errorf("lookup %s: %w", name, err)
116 hashed, err := bcrypt.GenerateFromPassword(new, bcrypt.DefaultCost)
117 if err != nil {
118 return fmt.Errorf("generate hash: %w", err)
120 _, err = db.Exec("UPDATE users SET password = ?, modtime = ? WHERE username = ?", hashed, time.Now().Unix(), name)
121 return err
124 func (db *UserDB) Delete(name string) error {
125 _, err := db.Lookup(name)
126 if err != nil {
127 return fmt.Errorf("delete %s: %w", name, err)
129 _, err = db.Exec("DELETE FROM users WHERE username = ?", name)
130 if err != nil {
131 return fmt.Errorf("delete %s: %w", name, err)
133 return nil
136 func (db *UserDB) Authenticate(name string, pw Password) error {
137 u, err := db.Lookup(name)
138 if err != nil {
139 return fmt.Errorf("authenticate %s: %w", name, err)
141 if err := bcrypt.CompareHashAndPassword(u.password, pw); err != nil {
142 return fmt.Errorf("authenticate %s: %w", name, err)
144 return nil
147 // TODO tickets aren't implemented yet
148 func createTicket(dir, username string, pw Password) error {
149 f, err := os.Create(path.Join(dir, username))
150 if err != nil {
151 return err
153 defer f.Close()
155 // TODO hash username, password, random bytes
156 // _, err := f.Write(b)
157 if _, err := f.Write(testTicket); err != nil {
158 return err
160 return nil