Commit Diff


commit - 5b8260a55717a51bd99ac6e148cd9bf179561126
commit + a7e7d1d5189113e16d1cd8eb7a6d044d3eca0aca
blob - d12434921090a07f23a8ab1eec3054d934401d8a
blob + 98a9f070192863dc80ac6d29db43f0aa03ec5064
--- client.go
+++ client.go
@@ -4,7 +4,10 @@ import (
 	"bytes"
 	"encoding/json"
 	"fmt"
+	"io"
 	"net/http"
+
+	"mailmux.net/aliases"
 )
 
 const apiurl = "https://mailmux.net/v1/aliases"
@@ -49,33 +52,33 @@ func (c *Client) Register(username, password string) e
 	return fmt.Errorf("register %s: %s", username, rerror.Error)
 }
 
-func (c *Client) NewAlias() (Alias, error) {
-	mcall := &Mcall{
+func (c *Client) NewAlias() (aliases.Aliases, error) {
+	tmsg := &Mcall{
 		Type:     Tnew,
 		Username: c.user,
 		Password: c.token,
 	}
 	buf := &bytes.Buffer{}
-	if err := json.NewEncoder(buf).Encode(mcall); err != nil {
-		return Alias{}, fmt.Errorf("new alias: %w", err)
+	if err := json.NewEncoder(buf).Encode(tmsg); err != nil {
+		return nil, fmt.Errorf("new alias: %w", err)
 	}
+
 	resp, err := http.Post(c.addr+"/aliases", jsonContentType, buf)
 	if err != nil {
-		return Alias{}, fmt.Errorf("new alias: %w", err)
+		return nil, fmt.Errorf("new alias: %w", err)
 	}
-
 	defer resp.Body.Close()
 	rmsg, err := ParseMcall(resp.Body)
 	if err != nil {
-		return Alias{}, fmt.Errorf("new alias: parse response: %w", err)
+		return nil, fmt.Errorf("new alias: parse response: %w", err)
 	}
 	if rmsg.Type == Rerror {
-		return Alias{}, fmt.Errorf("new alias: %v", rmsg.Error)
+		return nil, fmt.Errorf("new alias: %v", rmsg.Error)
 	}
-	return rmsg.Aliases[0], nil
+	return rmsg.Aliases, nil
 }
 
-func (c *Client) Aliases() ([]Alias, error) {
+func (c *Client) Aliases() (aliases.Aliases, error) {
 	tmsg := &Mcall{
 		Type:     Tlist,
 		Username: c.user,
@@ -91,7 +94,10 @@ func (c *Client) Aliases() ([]Alias, error) {
 	}
 
 	defer resp.Body.Close()
-	rmsg, err := ParseMcall(resp.Body)
+	buf.Reset()
+	io.Copy(buf, resp.Body)
+	fmt.Println(string(buf.Bytes()))
+	rmsg, err := ParseMcall(buf)
 	if err != nil {
 		return nil, fmt.Errorf("list aliases: parse response: %w", err)
 	}
blob - 95eb5dc5f0570ad7be1eab427a0896ff34a8f85c
blob + 51e0bf7797f62622a549f18aa0f43c9b2b4b2d2e
--- cmd/mailmux/mailmux.go
+++ cmd/mailmux/mailmux.go
@@ -15,77 +15,82 @@ import (
 )
 
 type server struct {
-	seedfile string
+	seedfile  string
 	aliaspath string
-	aliases aliases.Aliases
-	users mailmux.UserStore
+	aliases   aliases.Aliases
+	users     mailmux.UserStore
 }
 
 func (srv *server) aliasHandler(w http.ResponseWriter, req *http.Request) {
-	if err := req.ParseForm(); err != nil {
-		w.WriteHeader(http.StatusBadRequest)
+	tmsg, err := mailmux.ParseMcall(req.Body)
+	if err != nil {
+		rerror(w, err.Error(), http.StatusBadRequest)
 		return
 	}
-	user := req.Form.Get("username")
-	if user == "" {
-		http.Error(w, "empty user", http.StatusBadRequest)
-		return
-	}
-	token := req.Form.Get("token")
-	if token == "" {
-		http.Error(w, "empty token", http.StatusBadRequest)
-		return
-	}
-	_, err := srv.users.Lookup(user)
+	_, err = srv.users.Lookup(tmsg.Username)
 	if err != nil {
-		w.WriteHeader(http.StatusUnauthorized)
+		rerror(w, "unauthorised", http.StatusUnauthorized)
 		log.Println(err)
 		return
 	}
 
-	switch req.Method {
-	case http.MethodGet:
-		srv.listAliasHandler(w, req)
+	// TODO authenticate user
+
+	switch tmsg.Type {
+	case mailmux.Tnew:
+		srv.newAlias(w, tmsg)
 		return
-	case http.MethodPost:
-		srv.newAliasHandler(w, req)
+	case mailmux.Tlist:
+		srv.listAliasHandler(w, tmsg)
 		return
-	case http.MethodDelete:
-		srv.deleteAlias(w, req)
-		return
 	}
-	w.WriteHeader(http.StatusMethodNotAllowed)
+	rerror(w, "not implemented yet", http.StatusNotImplemented)
 }
 
-func (srv *server) listAliasHandler(w http.ResponseWriter, req *http.Request) {
-	username := req.Form.Get("username")
+func (srv *server) listAliasHandler(w http.ResponseWriter, tmsg *mailmux.Mcall) {
 	filtered := make(aliases.Aliases)
 	for rcpt, dest := range srv.aliases {
-		if dest == username {
+		if dest == tmsg.Username {
 			filtered[rcpt] = dest
 		}
 	}
-	if err := json.NewEncoder(w).Encode(filtered); err != nil {
-		http.Error(w, err.Error(), http.StatusInternalServerError)
+	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) newAliasHandler(w http.ResponseWriter, req *http.Request) {
-	username := req.Form.Get("username")
+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 {
-		http.Error(w, err.Error(), http.StatusInternalServerError)
+		s := fmt.Sprintf("random username: %v", err)
+		rerror(w, s, http.StatusInternalServerError)
 		return
 	}
-	srv.aliases[rcpt] = username
+	srv.aliases[rcpt] = tmsg.Username
 	if err := aliases.Put(srv.aliases, srv.aliaspath); err != nil {
-		http.Error(w, err.Error(), http.StatusInternalServerError)
+		s := fmt.Sprintf("put: %v", err)
+		rerror(w, s, http.StatusInternalServerError)
 		return
 	}
-	rcpt = rcpt + "@mailmux.net"
-	if err := json.NewEncoder(w).Encode(rcpt); err != nil {
-		http.Error(w, err.Error(), http.StatusInternalServerError)
+
+	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
 	}
 }
@@ -113,13 +118,12 @@ func (srv *server) deleteAlias(w http.ResponseWriter, 
 // 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) {
-	mcall := &mailmux.Mcall{Type: mailmux.Rerror, Error: errormsg}
-	if err := json.NewEncoder(w).Encode(mcall); err != nil {
-		http.Error(w, err.Error(), http.StatusInternalServerError)
-		return
-	}
 	w.WriteHeader(status)
-	return
+	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() {
blob - 52f9c160dcf7b2ab08ae17f9b0923353ce2d9b0f
blob + 26966c2e5dd6cb30b90898b3dedd30a52773a6a6
--- cmd/mailmux/mailmux_test.go
+++ cmd/mailmux/mailmux_test.go
@@ -1,6 +1,7 @@
 package main
 
 import (
+	"fmt"
 	"net"
 	"net/http"
 	"os"
@@ -8,6 +9,7 @@ import (
 	"testing"
 
 	mailmux "mailmux.net"
+	"mailmux.net/aliases"
 )
 
 func newTestServerClient(t *testing.T) *mailmux.Client {
@@ -21,6 +23,7 @@ func newTestServerClient(t *testing.T) *mailmux.Client
 	}
 	srv := &server{
 		users:     db,
+		aliases:   make(aliases.Aliases),
 		aliaspath: path.Join(dir, "aliases"),
 		seedfile:  "/usr/share/dict/words",
 	}
@@ -30,22 +33,27 @@ func newTestServerClient(t *testing.T) *mailmux.Client
 		t.Fatal(err)
 	}
 
-	http.HandleFunc("/register", srv.registerHandler)
-	http.HandleFunc("/aliases", srv.aliasHandler)
+	mux := http.NewServeMux()
+	mux.HandleFunc("/register", srv.registerHandler)
+	mux.HandleFunc("/aliases", srv.aliasHandler)
 	go func() {
-		t.Fatal(http.Serve(ln, nil))
+		t.Fatal(http.Serve(ln, mux))
 	}()
 
-	return mailmux.Dial("http://" + ln.Addr().String(), "test", "test")
+	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 client
 }
 
 func TestBadRegistration(t *testing.T) {
 	client := newTestServerClient(t)
 	registrations := map[string]string{
-		"djfkjskdjf": "dfjkdkfjsd",
-		"": "asdfgjkl",
-		"fjdklskjdsf": "",
-		"@@@@": "dfjksjkdf",
+		"djfkjskdjf":      "dfjkdkfjsd",
+		"":                "asdfgjkl",
+		"fjdklskjdsf":     "",
+		"@@@@":            "dfjksjkdf",
 		"foo@example.com": "",
 	}
 	for username, password := range registrations {
@@ -55,3 +63,19 @@ func TestBadRegistration(t *testing.T) {
 		}
 	}
 }
+
+func TestAliases(t *testing.T) {
+	client := newTestServerClient(t)
+	for i := 0; i <= 100; i++ {
+		a, err := client.NewAlias()
+		if err != nil {
+			t.Fatal(err)
+		}
+		fmt.Println(a)
+	}
+	a, err := client.Aliases()
+	if err != nil {
+		t.Fatal(err)
+	}
+	fmt.Println(a)
+}
blob - 46f5ee368b9a28a1cf6f475ad9dfa4a501d8d6e9
blob + 9428a3263c8c7a06be0274eedf32b489c0ca49f3
--- cmd/mailmux/register.go
+++ cmd/mailmux/register.go
@@ -11,12 +11,12 @@ import (
 
 func (srv *server) registerUser(name string, pw mailmux.Password) error {
 	_, err := srv.users.Lookup(name)
-	if err == nil {
-		return mailmux.ErrUserExist
-	}
 	if errors.Is(err, mailmux.ErrUnknownUser) {
 		return srv.users.Change(name, pw)
 	}
+	if err == nil {
+		return mailmux.ErrUserExist
+	}
 	return err
 }
 
@@ -26,28 +26,29 @@ func (srv *server) registerHandler(w http.ResponseWrit
 		return
 	}
 
-	var mcall mailmux.Mcall
-	if err := json.NewDecoder(req.Body).Decode(&mcall); err != nil {
+	var tmsg mailmux.Mcall
+	if err := json.NewDecoder(req.Body).Decode(&tmsg); err != nil {
 		rerror(w, err.Error(), http.StatusBadRequest)
 		return
 	}
 
-	if mcall.Type != mailmux.Tregister {
-		s := fmt.Sprintf("mcall type %d is not Tregister", mcall.Type)
+	if tmsg.Type != mailmux.Tregister {
+		s := fmt.Sprintf("mcall type %d is not Tregister", tmsg.Type)
 		rerror(w, s, http.StatusBadRequest)
 		return
 	}
-	if mcall.Username == "" {
+	if tmsg.Username == "" {
 		rerror(w, "empty username", http.StatusBadRequest)
 		return
 	}
-	if mcall.Password == "" {
+	if tmsg.Password == "" {
 		rerror(w, "empty password", http.StatusBadRequest)
 		return
 	}
 
-	err := srv.registerUser(mcall.Username, mailmux.Password(mcall.Password))
+	err := srv.registerUser(tmsg.Username, mailmux.Password(tmsg.Password))
 	if err != nil {
 		rerror(w, err.Error(), http.StatusInternalServerError)
+		return
 	}
 }
blob - b3008bfc6eabb6e3a0750bd60e55419257c9291e
blob + f147c4457cb40a47a4144343f9916eac478b7d86
--- mcall.go
+++ mcall.go
@@ -5,6 +5,8 @@ import (
 	"errors"
 	"io"
 	"time"
+
+	"mailmux.net/aliases"
 )
 
 const (
@@ -25,11 +27,11 @@ const (
 // This design is loosely based on the Plan 9 network file protocol 9P.
 type Mcall struct {
 	Type     uint
-	Username string    // Tregister, Rregister
-	Password string    // Tregister
-	Error    string    // Rerror
-	Aliases  []Alias   // Rnew, Rlist, Tremove
-	Expiry   time.Time // Tnew, Rnew
+	Username string          // Tregister, Rregister
+	Password string          // Tregister
+	Error    string          // Rerror
+	Aliases  aliases.Aliases // Rnew, Rlist, Tremove
+	Expiry   time.Time       // Tnew, Rnew
 }
 
 // ParseMcall parses and validates one Mcall from r.
@@ -38,17 +40,17 @@ func ParseMcall(r io.Reader) (*Mcall, error) {
 	if err := json.NewDecoder(r).Decode(&mc); err != nil {
 		return nil, err
 	}
-	if mc.Username == "" {
-		return nil, errors.New("empty username")
+	switch mc.Type {
+	case Rerror:
+		if mc.Error == "" {
+			return nil, errors.New("empty error message")
+		}
+	case Tregister, Tnew, Tlist, Tremove:
+		if mc.Username == "" {
+			return nil, errors.New("empty username")
+		} else if mc.Password == "" {
+			return nil, errors.New("empty password")
+		}
 	}
-	if mc.Password == "" {
-		return nil, errors.New("empty password")
-	}
-	if mc.Type != Rerror && mc.Error != "" {
-		return nil, errors.New("non-empty error field")
-	}
-	if mc.Type == Rerror && mc.Error == "" {
-		return nil, errors.New("empty error message")
-	}
 	return &mc, nil
 }