commit - 052981c3e4c54235c8bf047902fb6646eb57dde7
commit + cb474497f0adffac4fd2f79500e62f32bff27ea3
blob - ed8f12e75215abdcf7aebf7505255a9dede42fc5
blob + 3e185bc025d1f54a717d41a63664fda8de1571cf
--- alias.go
+++ alias.go
import (
"database/sql"
+ "errors"
"fmt"
"os"
"time"
)
+// 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
+ // implementation.
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
+ // Note contains user-defined text that can be used to,
+ // for example, identify the alias.
Note string
}
Delete(rcpt string) error
}
+// AliasDB is an implementation of AliasStore backed by a sqlite3 database.
type AliasDB struct {
*sql.DB
dictpath string
}
+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
+// initialised if it doesn't already exist.
func OpenAliasDB(name, dictpath string) (*AliasDB, error) {
db, err := sql.Open("sqlite3", name)
if err != nil {
CREATE TABLE IF NOT EXISTS aliases (
recipient TEXT PRIMARY KEY,
destination TEXT NOT NULL,
- expiry INTEGER,
- note TEXT
+ expiry INTEGER NOT NULL,
+ note TEXT NOT NULL
);`
_, err = db.Exec(stmt)
if err != nil {
return &AliasDB{db, dictpath}, err
}
-func (db *AliasDB) Create(dest string) (Alias, error) {
+// Create is a convenience method for creating a new random alias for destination.
+// To set more Alias attributes, such as setting an expiry date, use Put.
+func (db *AliasDB) Create(destination 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)
+ a := Alias{
+ Recipient: rcpt,
+ Destination: destination,
}
- return Alias{Recipient: rcpt, Destination: dest}, nil
+ if err := db.Put(a); err != nil {
+ return Alias{}, fmt.Errorf("put %s: %w", a.Recipient, err)
+ }
+ return a, nil
}
+// Put creates or updates the given alias in db.
+func (db *AliasDB) Put(a Alias) error {
+ _, err := db.Lookup(a.Recipient)
+ if err != nil && !errors.Is(err, errRecipientNotExist) {
+ return fmt.Errorf("lookup %s: %w", a.Recipient, err)
+ }
+ var q string
+ if errors.Is(err, errRecipientNotExist) {
+ q = "INSERT INTO aliases (recipient, destination, expiry, note) VALUES (?, ?, ?, ?)"
+ } else if err == nil {
+ q = "UPDATE aliases (recipient, destination, expiry, note) VALUES(?, ?, ?, ?)"
+ }
+ _, err = db.Exec(q, a.Recipient, a.Destination, a.Expiry.Unix(), a.Note)
+ return err
+}
+
+// Lookup returns the Alias with the recipient rcpt. If no alias exists, an error is returned.
+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)
+ if errors.Is(err, sql.ErrNoRows) {
+ return Alias{}, errRecipientNotExist
+ } else if err != nil {
+ return Alias{}, err
+ }
+ return a, nil
+}
+
+// Aliases returns all aliases who have their destination address as dest.
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)
+ return nil, 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 {
+ var sec int64
+ if err := rows.Scan(&a.Recipient, &a.Destination, &sec, &a.Note); err != nil {
return aliases, err
}
- if t.Valid {
- a.Expiry = t.Time
- }
- if note.Valid {
- a.Note = note.String
- }
+ a.Expiry = time.Unix(sec, 0)
aliases = append(aliases, a)
}
return aliases, rows.Err()
}
+// Delete deletes any alias with recipient rcpt.
+// No error is returned if no alias exists with the given recipient.
func (db *AliasDB) Delete(rcpt string) error {
_, err := db.Exec("DELETE FROM aliases WHERE recipient = ?", rcpt)
if err != nil {
blob - 1e2d1a189ba11c0a2b3ceb7aaa37685ba5ee67e0
blob + ea0912002994073ff2e272d13fde8a00e8cd538e
--- client.go
+++ client.go
import (
"bytes"
"encoding/json"
+ "errors"
"fmt"
"net/http"
)
func (c *Client) NewAlias() ([]Alias, error) {
tmsg := &Mcall{
- Type: Tnew,
+ Type: Tcreate,
Username: c.user,
Password: c.token,
}
}
buf := &bytes.Buffer{}
if err := json.NewEncoder(buf).Encode(tmsg); err != nil {
- return nil, fmt.Errorf("list aliases: %w", err)
+ return nil, fmt.Errorf("encode tmsg: %w", err)
}
resp, err := http.Post(c.url+"/alias", jsonContentType, buf)
if err != nil {
- return nil, fmt.Errorf("list aliases: %w", err)
+ return nil, err
}
defer resp.Body.Close()
rmsg, err := ParseMcall(resp.Body)
if err != nil {
- return nil, fmt.Errorf("list aliases: parse response: %w", err)
+ return nil, fmt.Errorf("parse response: %w", err)
}
if rmsg.Type == Rerror {
- return nil, fmt.Errorf("list aliases: %v", rmsg.Error)
+ return nil, errors.New(rmsg.Error)
}
return rmsg.Aliases, nil
}
blob - 252dbce017446a93961926b7f8636ad5df4d5a4e
blob + faa7d1977d89b20452eabe3d25e5d493f78795f4
--- cmd/mailmux/mailmux_test.go
+++ cmd/mailmux/mailmux_test.go
}
a, err := client.Aliases()
if err != nil {
- t.Fatal(err)
+ t.Fatal("list aliases:", err)
}
t.Log(a)
}
blob - 6b7bc2b7ecae600709724f269936478d9658cf2b
blob + 460d3d27b5cd3607ac2b6e52be7b3eb9091d73f1
--- go.mod
+++ go.mod
go 1.17
require (
- github.com/mattn/go-sqlite3 v1.14.12 // indirect
- golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4 // indirect
+ github.com/mattn/go-sqlite3 v1.14.12
+ golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4
)
blob - 8c144c4624fd750edac385562d6df627ece50404
blob + 275add587c519278a2643e428b56045b6021b10a
--- mcall.go
+++ mcall.go
Tregister = 1 + iota
Rregister
Rerror
- Tnew
- Rnew
+ Tcreate
+ Rcreate
Tlist
Rlist
Tremove
+ Rremove
)
// Mcall represents a message passed between mailmux clients and servers.
Password string `json:",omitempty"` // Tregister
Error string `json:",omitempty"` // Rerror
Aliases []Alias `json:",omitempty"` // Rnew, Rlist, Tremove
- Expiry time.Time `json:",omitempty"` // Tnew, Rnew
+ Expiry time.Time `json:",omitempty"` // Tcreate, Rnew
}
// ParseMcall parses and validates a JSON-encoded Mcall from r.
if mc.Error == "" {
return nil, errors.New("empty error message")
}
- case Tregister, Tnew, Tlist, Tremove:
+ case Tregister, Tcreate, Tlist, Tremove:
if mc.Username == "" {
return nil, errors.New("empty username")
} else if mc.Password == "" {
blob - d0d25d70ddf134d338c0dd36d983b041926a1a3c
blob + 50982ee30965a523551740de2e702c7df1bd933b
--- server.go
+++ server.go
var rmsg *Mcall
switch tmsg.Type {
- case Tnew:
+ case Tcreate:
rmsg = srv.newAlias(tmsg)
case Tlist:
rmsg = srv.listAliasHandler(tmsg)
if err != nil {
return &Mcall{Type: Rerror, Error: err.Error()}
}
- return &Mcall{Type: Rnew, Username: tmsg.Username, Aliases: []Alias{alias}}
+ return &Mcall{Type: Rcreate, 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: Rerror, Error: fmt.Sprintf("aliases for %s: %v", tmsg.Username, err)}
}
return &Mcall{Type: Rlist, Username: tmsg.Username, Aliases: a}
}