Blame


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