Blob


1 package mailmux
3 import (
4 "database/sql"
5 "errors"
6 "fmt"
7 "os"
8 "time"
9 )
11 // An Alias is used by a server to forward mail it receives.
12 // Mail addressed to Recipient is forwarded to Destination.
13 type Alias struct {
14 // Recipient is just the username part of a publicly visible email address.
15 // The domain(s) available for accepting mail depend on the SMTP server
16 // implementation.
17 Recipient string
18 // Destination contains an email address, with domain suffix,
19 // to which mail will be forwarded.
20 Destination string
21 // Expiry specifies a time after which the alias is considered inactive;
22 // that is, mail addressed to Recipient should be bounced.
23 Expiry time.Time
24 // Note contains user-defined text that can be used to,
25 // for example, identify the alias.
26 Note string
27 // modTime contains the time the alias was last modified.
28 modTime time.Time
29 }
31 // Equal reports whether a and b represent the same Alias.
32 // This is required since an alias' expiry is a time.Time value which has pitfalls
33 // when doing comparisons using reflect.DeepEqual or the == operator.
34 func (a Alias) Equal(b Alias) bool {
35 switch {
36 case a.Recipient != b.Recipient:
37 return false
38 case a.Destination != b.Destination:
39 return false
40 case !a.Expiry.Equal(b.Expiry):
41 return false
42 case a.Note != b.Note:
43 return false
44 }
45 return true
46 }
48 type AliasStore interface {
49 Create(dest string) (Alias, error)
50 Put(alias Alias) error
51 Aliases(dest string) ([]Alias, error)
52 Delete(rcpt string) error
53 }
55 // AliasDB is an implementation of AliasStore backed by a sqlite3 database.
56 type AliasDB struct {
57 *sql.DB
58 dictpath string
59 }
61 var errRecipientNotExist = errors.New("no such recipient")
63 // OpenAliasDB opens the named database file, using the file at dictpath for
64 // generating recipient names for new aliases. The database is created and
65 // initialised if it doesn't already exist.
66 func OpenAliasDB(name, dictpath string) (*AliasDB, error) {
67 db, err := sql.Open("sqlite3", name)
68 if err != nil {
69 return nil, err
70 }
71 stmt := `
72 CREATE TABLE IF NOT EXISTS aliases (
73 recipient TEXT PRIMARY KEY,
74 destination TEXT NOT NULL,
75 expiry INTEGER NOT NULL,
76 note TEXT NOT NULL,
77 modtime INTEGER NOT NULL DEFAULT (unixepoch())
78 );`
79 _, err = db.Exec(stmt)
80 if err != nil {
81 return nil, err
82 }
83 _, err = os.Stat(dictpath)
84 return &AliasDB{db, dictpath}, err
85 }
87 // Create is a convenience method for creating a new random alias for destination.
88 // To set more Alias attributes, such as setting an expiry date, use Put.
89 func (db *AliasDB) Create(destination string) (Alias, error) {
90 rcpt, err := RandomUsername(db.dictpath)
91 if err != nil {
92 return Alias{}, fmt.Errorf("create alias: %w", err)
93 }
94 a := Alias{
95 Recipient: rcpt,
96 Destination: destination,
97 }
98 if err := db.Put(a); err != nil {
99 return Alias{}, fmt.Errorf("put %s: %w", a.Recipient, err)
101 return a, nil
104 // Put creates or updates the given alias in db.
105 func (db *AliasDB) Put(a Alias) error {
106 _, err := db.Lookup(a.Recipient)
107 if err != nil && !errors.Is(err, errRecipientNotExist) {
108 return fmt.Errorf("lookup %s: %w", a.Recipient, err)
110 var q string
111 if errors.Is(err, errRecipientNotExist) {
112 q = "INSERT INTO aliases (recipient, destination, expiry, note) VALUES (?, ?, ?, ?)"
113 _, err = db.Exec(q, a.Recipient, a.Destination, a.Expiry.Unix(), a.Note)
114 } else if err == nil {
115 q = "UPDATE aliases SET recipient = ?, destination = ?, expiry = ?, note = ?, modtime = unixepoch() WHERE recipient = ?"
116 _, err = db.Exec(q, a.Recipient, a.Destination, a.Expiry.Unix(), a.Note, a.Recipient)
118 return err
121 // Lookup returns the Alias with the recipient rcpt. If no alias exists, an error is returned.
122 func (db *AliasDB) Lookup(rcpt string) (Alias, error) {
123 var a Alias
124 q := "SELECT recipient, destination, expiry, note, modtime FROM ALIASES WHERE recipient = ?"
125 var expiry int64
126 var modtime int64
127 err := db.QueryRow(q, rcpt).Scan(&a.Recipient, &a.Destination, &expiry, &a.Note, &modtime)
128 if errors.Is(err, sql.ErrNoRows) {
129 return Alias{}, errRecipientNotExist
130 } else if err != nil {
131 return Alias{}, err
133 a.Expiry = time.Unix(expiry, 0)
134 a.modTime = time.Unix(modtime, 0)
135 return a, nil
138 // Aliases returns all aliases who have their destination address as dest.
139 func (db *AliasDB) Aliases(dest string) ([]Alias, error) {
140 q := "SELECT recipient, destination, expiry, note FROM aliases WHERE destination = ?"
141 rows, err := db.Query(q, dest)
142 if err != nil {
143 return nil, err
145 defer rows.Close()
146 var aliases []Alias
147 for rows.Next() {
148 var a Alias
149 var sec int64
150 if err := rows.Scan(&a.Recipient, &a.Destination, &sec, &a.Note); err != nil {
151 return aliases, err
153 a.Expiry = time.Unix(sec, 0)
154 aliases = append(aliases, a)
156 return aliases, rows.Err()
159 // Delete deletes any alias with recipient rcpt.
160 // No error is returned if no alias exists with the given recipient.
161 func (db *AliasDB) Delete(rcpt string) error {
162 _, err := db.Exec("DELETE FROM aliases WHERE recipient = ?", rcpt)
163 if err != nil {
164 return fmt.Errorf("delete %s: %w", rcpt, err)
166 return nil