commit - 5e622b9bb50c661a19f5b6b32666e6f880776fca
commit + 925168e0252fd037ac6e89d98f75d613efdadef2
blob - /dev/null
blob + 3f859921e3e13b963781a960269da12b6986c579 (mode 644)
--- /dev/null
+++ alias.go
+package mailmux
+
+import (
+ "database/sql"
+ "fmt"
+ "time"
+)
+
+type Alias struct {
+ Recipient string
+ Destination string
+ Expiry time.Time
+ Note string
+}
+
+type AliasStore interface {
+ Create(dest string) (Alias, error)
+ Aliases(dest string) ([]Alias, error)
+ Delete(rcpt string) error
+}
+
+type AliasDB struct {
+ *sql.DB
+ dictpath string
+}
+
+func OpenAliasDB(name, dictpath string) (*AliasDB, error) {
+ db, err := sql.Open("sqlite3", name)
+ if err != nil {
+ return nil, err
+ }
+ stmt := `
+CREATE TABLE IF NOT EXISTS aliases (
+ recipient TEXT PRIMARY KEY,
+ destination TEXT NOT NULL,
+ expiry INTEGER,
+ note TEXT
+);`
+ _, err = db.Exec(stmt)
+ return &AliasDB{db, dictpath}, err
+}
+
+func (db *AliasDB) Create(dest string) (Alias, error) {
+ rcpt, err := RandomUsername(db.dictpath)
+ if err != nil {
+ return Alias{}, fmt.Errorf("create alias: %w", err)
+ }
+ _, err = db.Exec("INSERT INTO aliases (recipient, destination) VALUES (?, ?)", rcpt, dest)
+ if err != nil {
+ return Alias{}, fmt.Errorf("create alias: %w", err)
+ }
+ return Alias{Recipient: rcpt, Destination: dest}, nil
+}
+
+func (db *AliasDB) Aliases(dest string) ([]Alias, error) {
+ rows, err := db.Query("SELECT recipient, destination, expiry, note FROM aliases WHERE destination = ?", dest)
+ if err != nil {
+ return nil, fmt.Errorf("aliases for %s: %w", dest, err)
+ }
+ defer rows.Close()
+ var aliases []Alias
+ for rows.Next() {
+ var a Alias
+ var t sql.NullTime
+ var note sql.NullString
+ err := rows.Scan(&a.Recipient, &a.Destination, &t, ¬e)
+ if err != nil {
+ return aliases, err
+ }
+ if t.Valid {
+ a.Expiry = t.Time
+ }
+ if note.Valid {
+ a.Note = note.String
+ }
+ aliases = append(aliases, a)
+ }
+ return aliases, rows.Err()
+}
+
+func (db *AliasDB) Delete(rcpt string) error {
+ _, err := db.Exec("DELETE FROM aliases WHERE recipient = ?", rcpt)
+ if err != nil {
+ return fmt.Errorf("delete %s: %w", rcpt, err)
+ }
+ return nil
+}
blob - 81ef5523f4eb5c7954232f88156d459c9429bc3a
blob + 1e2d1a189ba11c0a2b3ceb7aaa37685ba5ee67e0
--- client.go
+++ client.go
"encoding/json"
"fmt"
"net/http"
-
- "mailmux.net/aliases"
)
const apiurl = "https://mailmux.net/v1/aliases"
type Client struct {
*http.Client
- addr string
+ url string
user string
token string
}
-func Dial(uri, user, token string) *Client {
- return &Client{http.DefaultClient, uri, user, token}
+func Dial(addr, user, token string, tls bool) *Client {
+ var url string
+ if tls {
+ url = "https://" + addr
+ } else {
+ url = "http://" + addr
+ }
+ return &Client{http.DefaultClient, url, user, token}
}
func (c *Client) Register(username, password string) error {
if err := json.NewEncoder(body).Encode(mcall); err != nil {
return fmt.Errorf("register %s: %v", username, err)
}
- resp, err := c.Post(c.addr+"/register", jsonContentType, body)
+ resp, err := c.Post(c.url+"/register", jsonContentType, body)
if err != nil {
return fmt.Errorf("register %s: %v", username, err)
}
return fmt.Errorf("register %s: %s", username, rerror.Error)
}
-func (c *Client) NewAlias() (aliases.Aliases, error) {
+func (c *Client) NewAlias() ([]Alias, error) {
tmsg := &Mcall{
Type: Tnew,
Username: c.user,
return nil, fmt.Errorf("new alias: %w", err)
}
- resp, err := http.Post(c.addr+"/aliases", jsonContentType, buf)
+ resp, err := http.Post(c.url+"/alias", jsonContentType, buf)
if err != nil {
- return nil, fmt.Errorf("new alias: %w", err)
+ return nil, fmt.Errorf("new alias: POST tmsg: %w", err)
}
defer resp.Body.Close()
rmsg, err := ParseMcall(resp.Body)
return rmsg.Aliases, nil
}
-func (c *Client) Aliases() (aliases.Aliases, error) {
+func (c *Client) Aliases() ([]Alias, error) {
tmsg := &Mcall{
Type: Tlist,
Username: c.user,
if err := json.NewEncoder(buf).Encode(tmsg); err != nil {
return nil, fmt.Errorf("list aliases: %w", err)
}
- resp, err := http.Post(c.addr+"/aliases", jsonContentType, buf)
+ resp, err := http.Post(c.url+"/alias", jsonContentType, buf)
if err != nil {
return nil, fmt.Errorf("list aliases: %w", err)
}
blob - 5373d46e3aabb175b56ac070c0b75feb61afa2ce
blob + 22fa8c2a63d6e2df882e06396a8ae9ba2b367294
--- cmd/mailmux/mailmux.go
+++ cmd/mailmux/mailmux.go
package main
import (
- "encoding/json"
- "errors"
"fmt"
- "io/fs"
"log"
"math/rand"
"net/http"
"time"
mailmux "mailmux.net"
- "mailmux.net/aliases"
)
-type server struct {
- seedfile string
- aliaspath string
- aliases aliases.Aliases
- users mailmux.UserStore
+func init() {
+ rand.Seed(time.Now().UnixNano())
}
-func (srv *server) aliasHandler(w http.ResponseWriter, req *http.Request) {
- tmsg, err := mailmux.ParseMcall(req.Body)
- if err != nil {
- rerror(w, err.Error(), http.StatusBadRequest)
- return
- }
-
- err = srv.users.Authenticate(tmsg.Username, mailmux.Password(tmsg.Password))
- if err != nil {
- rerror(w, "unauthorised", http.StatusUnauthorized)
- log.Println(err)
- return
- }
-
- switch tmsg.Type {
- case mailmux.Tnew:
- srv.newAlias(w, tmsg)
- return
- case mailmux.Tlist:
- srv.listAliasHandler(w, tmsg)
- return
- }
- rerror(w, "not implemented yet", http.StatusNotImplemented)
-}
-
-func (srv *server) listAliasHandler(w http.ResponseWriter, tmsg *mailmux.Mcall) {
- filtered := make(aliases.Aliases)
- for rcpt, dest := range srv.aliases {
- if dest == tmsg.Username {
- filtered[rcpt] = dest
- }
- }
- rmsg := &mailmux.Mcall{
- Type: mailmux.Rlist,
- Username: tmsg.Username,
- Aliases: filtered,
- }
- if err := json.NewEncoder(w).Encode(rmsg); err != nil {
- rerror(w, err.Error(), http.StatusInternalServerError)
- return
- }
-}
-
-func (srv *server) newAlias(w http.ResponseWriter, tmsg *mailmux.Mcall) {
- if tmsg.Type != mailmux.Tnew {
- rerror(w, "message type is not Tnew", http.StatusBadRequest)
- return
- }
-
- rcpt, err := mailmux.RandomUsername(srv.seedfile)
- if err != nil {
- s := fmt.Sprintf("random username: %v", err)
- rerror(w, s, http.StatusInternalServerError)
- return
- }
- srv.aliases[rcpt] = tmsg.Username
- if err := aliases.Put(srv.aliases, srv.aliaspath); err != nil {
- s := fmt.Sprintf("put: %v", err)
- rerror(w, s, http.StatusInternalServerError)
- return
- }
-
- rnew := &mailmux.Mcall{
- Type: mailmux.Rnew,
- Username: tmsg.Username,
- Aliases: aliases.Aliases{rcpt: tmsg.Username},
- }
- if err := json.NewEncoder(w).Encode(rnew); err != nil {
- rerror(w, err.Error(), http.StatusInternalServerError)
- return
- }
-}
-
-func (srv *server) deleteAlias(w http.ResponseWriter, req *http.Request) {
- username := req.Form.Get("username")
- recipient := req.Form.Get("recipient")
- if recipient == "" {
- http.Error(w, "empty recipient", http.StatusBadRequest)
- return
- }
-
- for rcpt, dest := range srv.aliases {
- if rcpt == recipient && dest == username {
- delete(srv.aliases, rcpt)
- w.WriteHeader(http.StatusNoContent)
- return
- }
- }
- msg := fmt.Sprintf("no alias for recpient %s with destination %s", recipient, username)
- http.Error(w, msg, http.StatusNotFound)
-}
-
-// rerror replies to the HTTP request with a JSON-encoded Rerror message
-// with its Error field set to errormsg.
-// Just like http.Error, callers should ensure no further writes are done to w.
-func rerror(w http.ResponseWriter, errormsg string, status int) {
- w.WriteHeader(status)
- rmsg := &mailmux.Mcall{Type: mailmux.Rerror, Error: errormsg}
- if err := json.NewEncoder(w).Encode(rmsg); err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
-}
-
func main() {
cdir, err := os.UserCacheDir()
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
-
- srv := &server{}
- srv.users, err = mailmux.OpenUserDB(path.Join(cdir, "/mailmux/db"), ticketDir)
- if errors.Is(err, fs.ErrNotExist) {
- srv.users, err = mailmux.CreateUserDB(path.Join(cdir, "/mailmux/db"), ticketDir)
- }
+ udb, err := mailmux.OpenUserDB(path.Join(cdir, "/mailmux/db"), ticketDir)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
-
- srv.aliaspath = path.Join(cdir, "/mailmux/aliases")
- srv.aliases, err = aliases.Load(srv.aliaspath)
- if errors.Is(err, fs.ErrNotExist) {
- _, err = os.Create(path.Join(cdir, "/mailmux/aliases"))
- }
+ astore, err := mailmux.OpenAliasDB(path.Join(cdir, "/mailmux/db"), "/usr/share/dict/words")
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
-
- rand.Seed(time.Now().UnixNano())
- srv.seedfile = "/usr/share/dict/words"
-
- http.HandleFunc("/register", srv.registerHandler)
- http.HandleFunc("/aliases", srv.aliasHandler)
-
- log.Fatal(http.ListenAndServe("[::1]:6969", nil))
+ log.Fatal(http.ListenAndServe("[::1]:6969", mailmux.NewWebServer(astore, udb)))
}
blob - 2050aa72e2ed4a6392495d6fed4365660326a41c
blob + 252dbce017446a93961926b7f8636ad5df4d5a4e
--- cmd/mailmux/mailmux_test.go
+++ cmd/mailmux/mailmux_test.go
"testing"
mailmux "mailmux.net"
- "mailmux.net/aliases"
)
-func newTestServerClient(t *testing.T) *mailmux.Client {
+func newTestServer(t *testing.T) (net.Listener, *http.Server) {
dir, err := os.MkdirTemp("", "")
if err != nil {
t.Fatal(err)
}
- db, err := mailmux.CreateUserDB(path.Join(dir, "db"), dir)
+ ticketDir := path.Join(dir, "/mailmux/ticket")
+ if err := os.MkdirAll(ticketDir, 0777); err != nil {
+ t.Fatal(err)
+ }
+ udb, err := mailmux.OpenUserDB(path.Join(dir, "/mailmux/db"), ticketDir)
if err != nil {
t.Fatal(err)
}
- srv := &server{
- users: db,
- aliases: make(aliases.Aliases),
- aliaspath: path.Join(dir, "aliases"),
- seedfile: "/usr/share/dict/words",
+ astore, err := mailmux.OpenAliasDB(path.Join(dir, "/mailmux/db"), "/usr/share/dict/words")
+ if err != nil {
+ t.Fatal(err)
}
-
ln, err := net.Listen("tcp", "[::1]:0")
if err != nil {
t.Fatal(err)
}
- mux := http.NewServeMux()
- mux.HandleFunc("/register", srv.registerHandler)
- mux.HandleFunc("/aliases", srv.aliasHandler)
- go func() {
- t.Fatal(http.Serve(ln, mux))
- }()
-
- client := mailmux.Dial("http://"+ln.Addr().String(), "test@example.com", "test")
- if err := client.Register("test@example.com", "test"); err != nil {
- t.Fatal(err)
+ return ln, &http.Server{
+ Addr: ln.Addr().String(),
+ Handler: mailmux.NewWebServer(astore, udb),
}
- return client
}
func TestBadRegistration(t *testing.T) {
- client := newTestServerClient(t)
+ ln, srv := newTestServer(t)
+ defer srv.Close()
+ go func() {
+ srv.Serve(ln)
+ }()
+ client := mailmux.Dial(srv.Addr, "", "", false)
registrations := map[string]string{
"djfkjskdjf": "dfjkdkfjsd",
"": "asdfgjkl",
}
func TestAliases(t *testing.T) {
- client := newTestServerClient(t)
+ ln, srv := newTestServer(t)
+ go func() {
+ srv.Serve(ln)
+ }()
+ defer srv.Close()
+ client := mailmux.Dial(srv.Addr, "test@example.com", "secret", false)
+ if err := client.Register("test@example.com", "secret"); err != nil {
+ t.Fatal(err)
+ }
for i := 0; i <= 100; i++ {
_, err := client.NewAlias()
if err != nil {
t.Fatal(err)
}
}
- _, err := client.Aliases()
+ a, err := client.Aliases()
if err != nil {
t.Fatal(err)
}
+ t.Log(a)
}
blob - 9428a3263c8c7a06be0274eedf32b489c0ca49f3 (mode 644)
blob + /dev/null
--- cmd/mailmux/register.go
+++ /dev/null
-package main
-
-import (
- "encoding/json"
- "errors"
- "fmt"
- "net/http"
-
- mailmux "mailmux.net"
-)
-
-func (srv *server) registerUser(name string, pw mailmux.Password) error {
- _, err := srv.users.Lookup(name)
- if errors.Is(err, mailmux.ErrUnknownUser) {
- return srv.users.Change(name, pw)
- }
- if err == nil {
- return mailmux.ErrUserExist
- }
- return err
-}
-
-func (srv *server) registerHandler(w http.ResponseWriter, req *http.Request) {
- if req.Method != http.MethodPost {
- rerror(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
- return
- }
-
- var tmsg mailmux.Mcall
- if err := json.NewDecoder(req.Body).Decode(&tmsg); err != nil {
- rerror(w, err.Error(), http.StatusBadRequest)
- return
- }
-
- if tmsg.Type != mailmux.Tregister {
- s := fmt.Sprintf("mcall type %d is not Tregister", tmsg.Type)
- rerror(w, s, http.StatusBadRequest)
- return
- }
- if tmsg.Username == "" {
- rerror(w, "empty username", http.StatusBadRequest)
- return
- }
- if tmsg.Password == "" {
- rerror(w, "empty password", http.StatusBadRequest)
- return
- }
-
- err := srv.registerUser(tmsg.Username, mailmux.Password(tmsg.Password))
- if err != nil {
- rerror(w, err.Error(), http.StatusInternalServerError)
- return
- }
-}
blob - d6a73244b2364976cbef16028536aa644d35fb67
blob + 8c144c4624fd750edac385562d6df627ece50404
--- mcall.go
+++ mcall.go
"errors"
"io"
"time"
-
- "mailmux.net/aliases"
)
const (
// This design is loosely based on the Plan 9 network file protocol 9P.
type Mcall struct {
Type uint
- Username string `json:",omitempty"` // Tregister, Rregister
- Password string `json:",omitempty"` // Tregister
- Error string `json:",omitempty"` // Rerror
- Aliases aliases.Aliases `json:",omitempty"` // Rnew, Rlist, Tremove
- Expiry time.Time `json:",omitempty"` // Tnew, Rnew
+ Username string `json:",omitempty"` // Tregister, Rregister
+ Password string `json:",omitempty"` // Tregister
+ Error string `json:",omitempty"` // Rerror
+ Aliases []Alias `json:",omitempty"` // Rnew, Rlist, Tremove
+ Expiry time.Time `json:",omitempty"` // Tnew, Rnew
}
// ParseMcall parses and validates a JSON-encoded Mcall from r.
blob - bb0b51d2b75a7b8148e02d22ae69b358e98d31db
blob + 0002ddd8efae18df9bafc950863b2331ca9e5d21
--- userdb.go
+++ userdb.go
"database/sql"
"errors"
"fmt"
- "io/fs"
"net/mail"
"os"
"path"
// OpenUserDB opens the named user database file and ticket directory.
func OpenUserDB(name, dir string) (*UserDB, error) {
- // sql.Open creates a blank file if it finds it doesn't exist.
- // Return early if the file doesn't exist so we can handle it ourselves.
- _, err := os.Stat(name)
- if errors.Is(err, fs.ErrNotExist) {
- return nil, err
- }
db, err := sql.Open("sqlite3", name)
if err != nil {
return nil, err
}
+ stmt := `CREATE TABLE IF NOT EXISTS users (
+ username TEXT PRIMARY KEY,
+ password BLOB NOT NULL
+);`
+ if _, err := db.Exec(stmt); err != nil {
+ return nil, err
+ }
return &UserDB{db, dir}, db.Ping()
}
blob - /dev/null
blob + d0d25d70ddf134d338c0dd36d983b041926a1a3c (mode 644)
--- /dev/null
+++ server.go
+package mailmux
+
+import (
+ "encoding/json"
+ "errors"
+ "fmt"
+ "log"
+ "net/http"
+)
+
+type Server struct {
+ aliases AliasStore
+ users UserStore
+}
+
+// rerror replies to the HTTP request with a JSON-encoded Rerror message
+// with its Error field set to errormsg.
+// Just like http.Error, callers should ensure no further writes are done to w.
+func rerror(w http.ResponseWriter, errormsg string, status int) {
+ w.WriteHeader(status)
+ rmsg := &Mcall{Type: Rerror, Error: errormsg}
+ if err := json.NewEncoder(w).Encode(rmsg); err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+}
+
+func NewWebServer(aliases AliasStore, users UserStore) http.Handler {
+ mux := http.NewServeMux()
+ srv := Server{aliases, users}
+ mux.HandleFunc("/register", srv.registerHandler)
+ mux.HandleFunc("/alias", srv.aliasHandler)
+ return mux
+}
+
+func (srv *Server) registerHandler(w http.ResponseWriter, req *http.Request) {
+ if req.Method != http.MethodPost {
+ rerror(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
+ return
+ }
+
+ defer req.Body.Close()
+ tmsg, err := ParseMcall(req.Body)
+ if err != nil {
+ rerror(w, err.Error(), http.StatusBadRequest)
+ return
+ }
+ if tmsg.Type != Tregister {
+ s := fmt.Sprintf("mcall type %d is not Tregister", tmsg.Type)
+ rerror(w, s, http.StatusBadRequest)
+ return
+ }
+
+ _, err = srv.users.Lookup(tmsg.Username)
+ if err == nil {
+ rerror(w, "user already exists", http.StatusBadRequest)
+ return
+ } else if !errors.Is(err, ErrUnknownUser) {
+ rerror(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+
+ if err := srv.users.Change(tmsg.Username, Password(tmsg.Password)); err != nil {
+ rerror(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ rmsg := &Mcall{
+ Type: Rregister,
+ Username: tmsg.Username,
+ }
+ b, err := json.Marshal(rmsg)
+ if err != nil {
+ rerror(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ w.Write(b)
+}
+
+func (srv *Server) aliasHandler(w http.ResponseWriter, req *http.Request) {
+ tmsg, err := ParseMcall(req.Body)
+ if err != nil {
+ rerror(w, err.Error(), http.StatusBadRequest)
+ return
+ }
+
+ err = srv.users.Authenticate(tmsg.Username, Password(tmsg.Password))
+ if err != nil {
+ rerror(w, "unauthorised", http.StatusUnauthorized)
+ log.Println(err)
+ return
+ }
+
+ var rmsg *Mcall
+ switch tmsg.Type {
+ case Tnew:
+ rmsg = srv.newAlias(tmsg)
+ case Tlist:
+ rmsg = srv.listAliasHandler(tmsg)
+ default:
+ rerror(w, "not implemented yet", http.StatusNotImplemented)
+ return
+ }
+ if rmsg.Type == Rerror {
+ w.WriteHeader(http.StatusInternalServerError)
+ }
+ if err := json.NewEncoder(w).Encode(rmsg); err != nil {
+ rerror(w, err.Error(), http.StatusInternalServerError)
+ }
+}
+
+func (srv *Server) newAlias(tmsg *Mcall) *Mcall {
+ alias, err := srv.aliases.Create(tmsg.Username)
+ if err != nil {
+ return &Mcall{Type: Rerror, Error: err.Error()}
+ }
+ return &Mcall{Type: Rnew, Username: tmsg.Username, Aliases: []Alias{alias}}
+}
+
+func (srv *Server) listAliasHandler(tmsg *Mcall) *Mcall {
+ a, err := srv.aliases.Aliases(tmsg.Username)
+ if err != nil {
+ return &Mcall{Type: Rerror, Error: err.Error()}
+ }
+ return &Mcall{Type: Rlist, Username: tmsg.Username, Aliases: a}
+}