commit - cb474497f0adffac4fd2f79500e62f32bff27ea3
commit + 71bdede3e2286e895508fbbfe745eef880db6b7b
blob - 3e185bc025d1f54a717d41a63664fda8de1571cf
blob + 472454650f5c9083a8eceaee4c9fc1c41b0a1f5a
--- alias.go
+++ alias.go
// 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
}
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)
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
}
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
package mailmux
import (
- "bytes"
"encoding/json"
"errors"
"fmt"
- "net/http"
+ "net"
)
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
-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
+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
Rlist
Tremove
Rremove
+ Tauth
+ Rauth
)
// Mcall represents a message passed between mailmux clients and servers.
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
+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
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
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
}
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
+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)
+}