commit 71bdede3e2286e895508fbbfe745eef880db6b7b from: Oliver Lowe date: Thu Apr 21 06:33:51 2022 UTC wip commit - cb474497f0adffac4fd2f79500e62f32bff27ea3 commit + 71bdede3e2286e895508fbbfe745eef880db6b7b blob - 3e185bc025d1f54a717d41a63664fda8de1571cf blob + 472454650f5c9083a8eceaee4c9fc1c41b0a1f5a --- alias.go +++ alias.go @@ -11,23 +11,24 @@ import ( // An Alias is used by a server to forward mail it receives. // Mail addressed to Recipient is forwarded to Destination. type Alias struct { - // Recipient is just the username part of a publicly visible email address. - // The domain(s) available for accepting mail depend on the SMTP server + // Recipient is just the username part of a publicly visible email address. + // The domain(s) available for accepting mail depend on the SMTP server // implementation. - Recipient string + Recipient string // Destination contains an email address, with domain suffix, // to which mail will be forwarded. Destination string // Expiry specifies a time after which the alias is considered inactive; // that is, mail addressed to Recipient should be bounced. - Expiry time.Time + Expiry time.Time // Note contains user-defined text that can be used to, // for example, identify the alias. - Note string + Note string } type AliasStore interface { Create(dest string) (Alias, error) + Put(alias Alias) error Aliases(dest string) ([]Alias, error) Delete(rcpt string) error } @@ -41,7 +42,7 @@ type AliasDB struct { var errRecipientNotExist = errors.New("no such recipient") // OpenAliasDB opens the named database file, using the file at dictpath for -// generating recipient names for new aliases. The database is created and +// generating recipient names for new aliases. The database is created and // initialised if it doesn't already exist. func OpenAliasDB(name, dictpath string) (*AliasDB, error) { db, err := sql.Open("sqlite3", name) @@ -89,10 +90,11 @@ func (db *AliasDB) Put(a Alias) error { var q string if errors.Is(err, errRecipientNotExist) { q = "INSERT INTO aliases (recipient, destination, expiry, note) VALUES (?, ?, ?, ?)" + _, err = db.Exec(q, a.Recipient, a.Destination, a.Expiry.Unix(), a.Note) } else if err == nil { - q = "UPDATE aliases (recipient, destination, expiry, note) VALUES(?, ?, ?, ?)" + q = "UPDATE aliases SET recipient = ?, destination = ?, expiry = ?, note = ? WHERE recipient = ?" + _, err = db.Exec(q, a.Recipient, a.Destination, a.Expiry.Unix(), a.Note, a.Recipient) } - _, err = db.Exec(q, a.Recipient, a.Destination, a.Expiry.Unix(), a.Note) return err } @@ -100,12 +102,14 @@ func (db *AliasDB) Put(a Alias) error { func (db *AliasDB) Lookup(rcpt string) (Alias, error) { var a Alias q := "SELECT recipient, destination, expiry, note FROM ALIASES WHERE recipient = ?" - err := db.QueryRow(q, rcpt).Scan(&a.Recipient, &a.Destination, &a.Expiry, &a.Note) + var expiry int64 + err := db.QueryRow(q, rcpt).Scan(&a.Recipient, &a.Destination, &expiry, &a.Note) if errors.Is(err, sql.ErrNoRows) { return Alias{}, errRecipientNotExist } else if err != nil { return Alias{}, err } + a.Expiry = time.Unix(expiry, 0) return a, nil } blob - ea0912002994073ff2e272d13fde8a00e8cd538e blob + 81a5c9082fd564ce701fcecde85b1b5b8ee760ce --- client.go +++ client.go @@ -1,11 +1,10 @@ package mailmux import ( - "bytes" "encoding/json" "errors" "fmt" - "net/http" + "net" ) const apiurl = "https://mailmux.net/v1/aliases" @@ -13,97 +12,82 @@ const apiurl = "https://mailmux.net/v1/aliases" const jsonContentType = "application/json" type Client struct { - *http.Client - url string - user string - token string + conn net.Conn // or io.ReadWriteCloser? } -func Dial(addr, user, token string, tls bool) *Client { - var url string - if tls { - url = "https://" + addr - } else { - url = "http://" + addr +func Dial(network, address string) (*Client, error) { + c, err := net.Dial(network, address) + if err != nil { + return nil, err } - return &Client{http.DefaultClient, url, user, token} + return &Client{conn: c}, nil } -func (c *Client) Register(username, password string) error { - mcall := &Mcall{ - Type: Tregister, - Username: username, - Password: password, +func (c *Client) exchange(tmsg *Mcall) (*Mcall, error) { + if err := c.tx(tmsg); err != nil { + return nil, fmt.Errorf("transmit tmsg: %w", err) } - - body := &bytes.Buffer{} - if err := json.NewEncoder(body).Encode(mcall); err != nil { - return fmt.Errorf("register %s: %v", username, err) - } - resp, err := c.Post(c.url+"/register", jsonContentType, body) + rmsg, err := c.rx() if err != nil { - return fmt.Errorf("register %s: %v", username, err) + return nil, fmt.Errorf("receive rmsg: %w", err) } - if resp.StatusCode == http.StatusOK { - return nil - } + // TODO sanity checks here. + // did we get back the message type we expected? + return rmsg, nil +} - defer resp.Body.Close() - rerror, err := ParseMcall(resp.Body) +func (c *Client) tx(tmsg *Mcall) error { + return json.NewEncoder(c.conn).Encode(tmsg) +} + +func (c *Client) rx() (*Mcall, error) { + return ParseMcall(c.conn) +} + +func (c *Client) Auth(username, password string) error { + tmsg := &Mcall{ + Type: Tauth, + Username: username, + Password: password, + } + rmsg, err := c.exchange(tmsg) if err != nil { - return fmt.Errorf("register %s: parse response: %v", username, err) + return err } - return fmt.Errorf("register %s: %s", username, rerror.Error) + if rmsg.Type == Rerror { + return errors.New(rmsg.Error) + } + return nil } func (c *Client) NewAlias() ([]Alias, error) { tmsg := &Mcall{ - Type: Tcreate, - Username: c.user, - Password: c.token, + Type: Tcreate, } - buf := &bytes.Buffer{} - if err := json.NewEncoder(buf).Encode(tmsg); err != nil { - return nil, fmt.Errorf("new alias: %w", err) - } - - resp, err := http.Post(c.url+"/alias", jsonContentType, buf) + rmsg, err := c.exchange(tmsg) if err != nil { - return nil, fmt.Errorf("new alias: POST tmsg: %w", err) + return nil, fmt.Errorf("exchange tmsg: %w", err) } - defer resp.Body.Close() - rmsg, err := ParseMcall(resp.Body) - if err != nil { - return nil, fmt.Errorf("new alias: parse response: %w", err) - } if rmsg.Type == Rerror { - return nil, fmt.Errorf("new alias: %v", rmsg.Error) + return nil, errors.New(rmsg.Error) } return rmsg.Aliases, nil } func (c *Client) Aliases() ([]Alias, error) { tmsg := &Mcall{ - Type: Tlist, - Username: c.user, - Password: c.token, + Type: Tlist, } - buf := &bytes.Buffer{} - if err := json.NewEncoder(buf).Encode(tmsg); err != nil { - return nil, fmt.Errorf("encode tmsg: %w", err) - } - resp, err := http.Post(c.url+"/alias", jsonContentType, buf) + rmsg, err := c.exchange(tmsg) if err != nil { - return nil, err + return nil, fmt.Errorf("exchange tmsg: %w", err) } - - defer resp.Body.Close() - rmsg, err := ParseMcall(resp.Body) - if err != nil { - return nil, fmt.Errorf("parse response: %w", err) - } if rmsg.Type == Rerror { return nil, errors.New(rmsg.Error) } return rmsg.Aliases, nil } + +func (c *Client) Close() error { + return c.conn.Close() +} blob - faa7d1977d89b20452eabe3d25e5d493f78795f4 (mode 644) blob + /dev/null --- cmd/mailmux/mailmux_test.go +++ /dev/null @@ -1,84 +0,0 @@ -package main - -import ( - "net" - "net/http" - "os" - "path" - "testing" - - mailmux "mailmux.net" -) - -func newTestServer(t *testing.T) (net.Listener, *http.Server) { - dir, err := os.MkdirTemp("", "") - if err != nil { - t.Fatal(err) - } - 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) - } - 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) - } - - return ln, &http.Server{ - Addr: ln.Addr().String(), - Handler: mailmux.NewWebServer(astore, udb), - } -} - -func TestBadRegistration(t *testing.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", - "fjdklskjdsf": "", - "@@@@": "dfjksjkdf", - "foo@example.com": "", - } - for username, password := range registrations { - err := client.Register(username, password) - if err == nil { - t.Errorf("nil error on bad registration username %q password %q", username, password) - } - } -} - -func TestAliases(t *testing.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) - } - } - a, err := client.Aliases() - if err != nil { - t.Fatal("list aliases:", err) - } - t.Log(a) -} blob - /dev/null blob + 60fbf28f01990ed5518b5a8ed2f90a83f5f8466a (mode 644) --- /dev/null +++ http.go @@ -0,0 +1,195 @@ +package mailmux + +import ( + "encoding/json" + "errors" + "fmt" + "net/http" + "path" + "strconv" + "strings" + "time" +) + +func jerror(w http.ResponseWriter, msg string, status int) { + w.WriteHeader(status) + m := map[string]string{"error": msg} + json.NewEncoder(w).Encode(m) +} + +func NewWebServer(aliases AliasStore, users UserStore) http.Handler { + mux := http.NewServeMux() + aliaseshandler := &aliasesHandler{aliases} + aliashandler := &aliasHandler{aliases} + authhandler := &authHandler{users} + mux.Handle("/v1/register", authhandler) + mux.Handle("/v1/aliases", authhandler.basicAuth(aliaseshandler)) + mux.Handle("/v1/aliases/", authhandler.basicAuth(aliashandler)) + return mux +} + +type aliasesHandler struct { + AliasStore +} + +func (h *aliasesHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { + username, _, ok := req.BasicAuth() + if !ok || len(username) == 0 { + jerror(w, "empty username", http.StatusForbidden) + return + } + + switch req.Method { + case http.MethodGet: + aliases, err := h.Aliases(username) + if err != nil { + jerror(w, err.Error(), http.StatusInternalServerError) + return + } + json.NewEncoder(w).Encode(aliases) + + case http.MethodPost: + alias, err := h.Create(username) + if err != nil { + jerror(w, err.Error(), http.StatusInternalServerError) + return + } + json.NewEncoder(w).Encode(alias) + default: + jerror(w, http.StatusText(http.StatusNotImplemented), http.StatusNotImplemented) + } +} + +type aliasHandler struct { + AliasStore +} + +func (h *aliasHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { + username, _, ok := req.BasicAuth() + if !ok || len(username) == 0 { + jerror(w, "empty username", http.StatusForbidden) + return + } + + recipient := path.Base(req.URL.Path) + aliases, err := h.Aliases(username) + if err != nil { + jerror(w, fmt.Sprintf("aliases for %s: %v", recipient, err), http.StatusInternalServerError) + return + } + var alias Alias + var found bool + for _, a := range aliases { + if a.Recipient == recipient { + alias = a + found = true + } + } + if !found { + jerror(w, "no such alias", http.StatusNotFound) + return + } + + switch req.Method { + case http.MethodDelete: + if err := h.Delete(recipient); err != nil { + jerror(w, fmt.Sprintf("delete %s: %v", recipient, err), http.StatusInternalServerError) + return + } + w.WriteHeader(http.StatusNoContent) + + case http.MethodGet: + json.NewEncoder(w).Encode(alias) + + case http.MethodPost: + if err := req.ParseForm(); err != nil { + jerror(w, fmt.Sprintf("parse form: %v", err), http.StatusBadRequest) + return + } + for param := range req.PostForm { + switch param { + case "expiry": + i, err := strconv.Atoi(req.PostForm.Get(param)) + if err != nil { + jerror(w, fmt.Sprintf("parse expiry: %v", err), http.StatusBadRequest) + return + } + alias.Expiry = time.Unix(int64(i), 0) + case "note": + alias.Note = req.PostForm.Get(param) + default: + jerror(w, fmt.Sprintf("invalid alias parameter %s", param), http.StatusBadRequest) + return + } + } + if err := h.Put(alias); err != nil { + jerror(w, fmt.Sprintf("update alias %s: %v", alias.Recipient, err), http.StatusInternalServerError) + return + } + json.NewEncoder(w).Encode(alias) + + default: + jerror(w, "not implemented yet", http.StatusMethodNotAllowed) + } +} + +type authHandler struct { + UserStore +} + +func (h *authHandler) basicAuth(next http.Handler) http.Handler { + fn := func(w http.ResponseWriter, req *http.Request) { + username, password, ok := req.BasicAuth() + if !ok || len(username) == 0 || len(password) == 0 { + jerror(w, "unauthorised", http.StatusUnauthorized) + return + } + err := h.Authenticate(username, Password(password)) + if err != nil { + jerror(w, "unauthorised", http.StatusUnauthorized) + return + } + next.ServeHTTP(w, req) + } + return http.HandlerFunc(fn) +} + +func (h *authHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { + if req.Method != http.MethodPost { + code := http.StatusMethodNotAllowed + http.Error(w, http.StatusText(code), code) + return + } + + if err := req.ParseForm(); err != nil { + jerror(w, err.Error(), http.StatusBadRequest) + return + } + username := req.PostForm.Get("username") + if username == "" { + jerror(w, "empty username", http.StatusBadRequest) + return + } + password := req.PostForm.Get("password") + if password == "" { + jerror(w, "empty password", http.StatusBadRequest) + return + } + + _, err := h.Lookup(username) + if err == nil { + jerror(w, "user already exists", http.StatusBadRequest) + return + } else if !errors.Is(err, ErrUnknownUser) { + jerror(w, fmt.Sprintf("lookup %s: %v", username, err), http.StatusInternalServerError) + return + } + + if err := h.Change(username, Password(password)); err != nil { + code := http.StatusInternalServerError + if strings.Contains(err.Error(), "invalid username") { + code = http.StatusBadRequest + } + jerror(w, fmt.Sprintf("change %s: %v", username, err), code) + } +} blob - 275add587c519278a2643e428b56045b6021b10a blob + 0e35e4f50d9c51bb7681dbef07f96d781d25bd58 --- mcall.go +++ mcall.go @@ -17,6 +17,8 @@ const ( Rlist Tremove Rremove + Tauth + Rauth ) // Mcall represents a message passed between mailmux clients and servers. @@ -45,12 +47,26 @@ func ParseMcall(r io.Reader) (*Mcall, error) { if mc.Error == "" { return nil, errors.New("empty error message") } - case Tregister, Tcreate, Tlist, Tremove: + case Tauth: if mc.Username == "" { return nil, errors.New("empty username") - } else if mc.Password == "" { + } + if mc.Password == "" { return nil, errors.New("empty password") } + case Tregister, Tcreate, Tlist, Tremove: + // check required params } return &mc, nil } + +func writeMcall(w io.Writer, mcall *Mcall) error { + return json.NewEncoder(w).Encode(mcall) +} + +// rerror writes a JSON-encoded Rerror message to the underlying writer +// with its Error field set to errormsg. +func rerror(w io.Writer, errormsg string) error { + rmsg := &Mcall{Type: Rerror, Error: errormsg} + return writeMcall(w, rmsg) +} blob - /dev/null blob + 59e531ef8bd789f0abf90ef96364a897fb031ec3 (mode 644) --- /dev/null +++ http_test.go @@ -0,0 +1,125 @@ +package mailmux + +import ( + "encoding/json" + "io" + "net/http" + "net/http/httptest" + "net/url" + "strconv" + "strings" + "testing" + "time" +) + +func TestBadRegister(t *testing.T) { + srv := newTestServer() + httpsrv := httptest.NewServer(NewWebServer(srv.Aliases, srv.Users)) + client := httpsrv.Client() + + registrations := []url.Values{ + // this user already exists; created in newTestServer. + url.Values{ + "username": []string{testUsername}, + "password": []string{testPassword}, + }, + // username isn't an email address + url.Values{ + "username": []string{"bla bla hello world!"}, + "password": []string{testPassword}, + }, + // empty password + url.Values{ + "username": []string{testUsername}, + "password": []string{}, + }, + } + for i, form := range registrations { + resp, err := client.PostForm(httpsrv.URL+"/v1/register", form) + if err != nil { + t.Error(err) + } + if resp.StatusCode != http.StatusBadRequest { + t.Errorf("bad registration case %d got HTTP status %s", i, resp.Status) + b, err := io.ReadAll(resp.Body) + if err != nil { + t.Fatal(err) + } + t.Log(string(b)) + } + } +} + +// TestAliasLifecycle tests creating, reading, updating then deleting an alias. +func TestAliasLifecycle(t *testing.T) { + srv := newTestServer() + httpsrv := httptest.NewServer(NewWebServer(srv.Aliases, srv.Users)) + client := httpsrv.Client() + + req, err := http.NewRequest(http.MethodPost, httpsrv.URL+"/v1/aliases", nil) + req.SetBasicAuth(testUsername, testPassword) + resp, err := client.Do(req) + if err != nil { + t.Fatalf("create alias: %v", err) + } + defer resp.Body.Close() + var alias Alias + if err := json.NewDecoder(resp.Body).Decode(&alias); err != nil { + t.Fatalf("decode new alias response: %v", err) + } + + req, err = http.NewRequest(http.MethodGet, httpsrv.URL+"/v1/aliases", nil) + req.SetBasicAuth(testUsername, testPassword) + resp, err = client.Do(req) + if err != nil { + t.Fatalf("list aliases: %v", err) + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + b, _ := io.ReadAll(resp.Body) + t.Log(string(b)) + t.Fatalf("list aliases: HTTP status %s", resp.Status) + } + var aliases []Alias + if err := json.NewDecoder(resp.Body).Decode(&aliases); err != nil { + t.Fatalf("decode list aliases response: %v", err) + } + + // update alias by adding a note and expiry time + v := make(url.Values) + v.Set("expiry", strconv.Itoa(int(time.Now().Add(time.Hour).Unix()))) + v.Set("note", "a test note to describe something") + req, err = http.NewRequest(http.MethodPost, httpsrv.URL+"/v1/aliases/"+alias.Recipient, strings.NewReader(v.Encode())) + if err != nil { + t.Fatalf("create update request: %v", err) + } + req.SetBasicAuth(testUsername, testPassword) + resp, err = client.Do(req) + if err != nil { + t.Fatalf("update alias: %v", err) + } + if resp.StatusCode != http.StatusOK { + b, _ := io.ReadAll(resp.Body) + t.Log(string(b)) + t.Fatalf("update alias: HTTP status %s", resp.Status) + } + defer resp.Body.Close() + if err := json.NewDecoder(resp.Body).Decode(&alias); err != nil { + t.Fatalf("decode updated alias response: %v", err) + } + + req, err = http.NewRequest(http.MethodDelete, httpsrv.URL+"/v1/aliases/"+alias.Recipient, nil) + if err != nil { + t.Fatalf("create delete request: %v", err) + } + req.SetBasicAuth(testUsername, testPassword) + resp, err = client.Do(req) + if err != nil { + t.Fatalf("delete alias: %v", err) + } + if resp.StatusCode != http.StatusNoContent { + b, _ := io.ReadAll(resp.Body) + t.Log(string(b)) + t.Fatalf("delete alias: HTTP status %s", resp.Status) + } +} blob - 50982ee30965a523551740de2e702c7df1bd933b blob + 9f38fd5a7b217257cdd6f38fc474ae7326696321 --- server.go +++ server.go @@ -1,125 +1,99 @@ package mailmux import ( - "encoding/json" "errors" "fmt" - "log" - "net/http" + "io" + "net" + "os" ) type Server struct { - aliases AliasStore - users UserStore + ln net.Listener + 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 (srv *Server) ListenAndServe() error { + if srv.ln == nil { + return errors.New("nil listener") } + return srv.Serve(srv.ln) } -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 +func (srv *Server) Serve(ln net.Listener) error { + if ln == nil { + return errors.New("nil listener") } - - defer req.Body.Close() - tmsg, err := ParseMcall(req.Body) - if err != nil { - rerror(w, err.Error(), http.StatusBadRequest) - return + for { + conn, err := ln.Accept() + if err != nil { + return err + } + go func() { + if err := srv.handleConn(conn); err != nil { + fmt.Fprintln(os.Stderr, err) + } + conn.Close() + }() } - 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) + return errors.New("unreachable?") } -func (srv *Server) aliasHandler(w http.ResponseWriter, req *http.Request) { - tmsg, err := ParseMcall(req.Body) +func (srv *Server) handleConn(conn net.Conn) error { + tmsg, err := ParseMcall(conn) if err != nil { - rerror(w, err.Error(), http.StatusBadRequest) - return + err = fmt.Errorf("parse mcall: %w", err) + rerror(conn, err.Error()) + return err } - - err = srv.users.Authenticate(tmsg.Username, Password(tmsg.Password)) - if err != nil { - rerror(w, "unauthorised", http.StatusUnauthorized) - log.Println(err) - return + if tmsg.Type != Tauth { + rerror(conn, "unauthorised") + return nil } + if err := srv.Users.Authenticate(tmsg.Username, Password(tmsg.Password)); err != nil { + rerror(conn, "authentication failed") + return err + } + rmsg := &Mcall{Type: Rauth, Username: tmsg.Username} + writeMcall(conn, rmsg) - var rmsg *Mcall - switch tmsg.Type { - case Tcreate: - rmsg = srv.newAlias(tmsg) - case Tlist: - rmsg = srv.listAliasHandler(tmsg) - default: - rerror(w, "not implemented yet", http.StatusNotImplemented) - return + user := tmsg.Username + for { + tmsg, err := ParseMcall(conn) + if errors.Is(err, io.EOF) { + return nil + } else if err != nil { + rerror(conn, err.Error()) + continue + } + rmsg := &Mcall{} + switch tmsg.Type { + case Tauth: + rmsg = &Mcall{Type: Rerror, Error: "already authenticated"} + case Tlist: + rmsg = srv.listAliases(user) + case Tcreate: + rmsg = srv.createAlias(user) + default: + rmsg = &Mcall{Type: Rerror, Error: "this tmessage not implemented yet"} + } + writeMcall(conn, rmsg) } - 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) +func (srv *Server) createAlias(username string) *Mcall { + alias, err := srv.Aliases.Create(username) if err != nil { return &Mcall{Type: Rerror, Error: err.Error()} } - return &Mcall{Type: Rcreate, Username: tmsg.Username, Aliases: []Alias{alias}} + return &Mcall{Type: Rcreate, Aliases: []Alias{alias}} } -func (srv *Server) listAliasHandler(tmsg *Mcall) *Mcall { - a, err := srv.aliases.Aliases(tmsg.Username) +func (srv *Server) listAliases(username string) *Mcall { + a, err := srv.Aliases.Aliases(username) if err != nil { - return &Mcall{Type: Rerror, Error: fmt.Sprintf("aliases for %s: %v", tmsg.Username, err)} + return &Mcall{Type: Rerror, Error: fmt.Sprintf("aliases for %s: %v", username, err)} } - return &Mcall{Type: Rlist, Username: tmsg.Username, Aliases: a} + return &Mcall{Type: Rlist, Aliases: a} } blob - 0002ddd8efae18df9bafc950863b2331ca9e5d21 blob + ec4e56c45297dc213a48d61aca6edde0232b8781 --- userdb.go +++ userdb.go @@ -68,9 +68,9 @@ func (db *UserDB) Lookup(name string) (User, error) { row := db.QueryRow("SELECT username, password FROM users WHERE username = ?", name) if err := row.Scan(&u.name, &u.password); err != nil { if errors.Is(err, sql.ErrNoRows) { - return User{}, fmt.Errorf("lookup %s: %w", name, ErrUnknownUser) + return User{}, ErrUnknownUser } - return User{}, fmt.Errorf("lookup %s: %w", name, err) + return User{}, err } return u, nil } @@ -104,18 +104,15 @@ func (db *UserDB) Change(name string, new Password) er return db.add(name, new) } if err != nil { - return fmt.Errorf("change %s: %w", name, err) + return fmt.Errorf("lookup %s: %w", name, err) } hashed, err := bcrypt.GenerateFromPassword(new, bcrypt.DefaultCost) if err != nil { - return fmt.Errorf("change %s: %w", name, err) + return fmt.Errorf("generate hash: %w", err) } _, err = db.Exec("UPDATE users SET password = ? WHERE username = ?", hashed, name) - if err != nil { - return fmt.Errorf("change %s: %w", name, err) - } - return nil + return err } func (db *UserDB) Delete(name string) error { blob - /dev/null blob + 5119d0d2235a5e52a418398ab4bc02496bf28174 (mode 644) --- /dev/null +++ server_test.go @@ -0,0 +1,68 @@ +package mailmux + +import ( + "errors" + "fmt" + "net" + "os" + "testing" +) + +const ( + testUsername = "test@example.com" + testPassword = "dummy" +) + +func newTestServer() *Server { + tmpdb, err := os.CreateTemp("", "") + if err != nil { + panic(err) + } + aliasdb, err := OpenAliasDB(tmpdb.Name(), "/usr/share/dict/words") + if err != nil { + panic(err) + } + userdb, err := OpenUserDB(tmpdb.Name(), os.TempDir()) + if err != nil { + panic(err) + } + if err := userdb.Change(testUsername, Password(testPassword)); err != nil { + panic(err) + } + return &Server{ + Aliases: aliasdb, + Users: userdb, + } +} + +func TestBasicList(t *testing.T) { + srv := newTestServer() + ln, err := net.Listen("unix", "/tmp/test.sock") + if err != nil { + t.Fatal(err) + } + defer ln.Close() + go func() { + err := srv.Serve(ln) + if err != nil && !errors.Is(err, net.ErrClosed) { + t.Fatal(err) + } + }() + client, err := Dial("unix", "/tmp/test.sock") + if err != nil { + t.Fatal(err) + } + err = client.Auth("test@example.com", "dummy") + if err != nil { + t.Fatal(err) + } + a, err := client.NewAlias() + if err != nil { + t.Error(err) + } + a, err = client.Aliases() + if err != nil { + t.Log(err) + } + fmt.Println(a) +}