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 925168e0 2022-04-14 o }
28 925168e0 2022-04-14 o
29 925168e0 2022-04-14 o type AliasStore interface {
30 925168e0 2022-04-14 o Create(dest string) (Alias, error)
31 71bdede3 2022-04-21 o Put(alias Alias) error
32 925168e0 2022-04-14 o Aliases(dest string) ([]Alias, error)
33 925168e0 2022-04-14 o Delete(rcpt string) error
34 925168e0 2022-04-14 o }
35 925168e0 2022-04-14 o
36 cb474497 2022-04-19 o // AliasDB is an implementation of AliasStore backed by a sqlite3 database.
37 925168e0 2022-04-14 o type AliasDB struct {
38 925168e0 2022-04-14 o *sql.DB
39 925168e0 2022-04-14 o dictpath string
40 925168e0 2022-04-14 o }
41 925168e0 2022-04-14 o
42 cb474497 2022-04-19 o var errRecipientNotExist = errors.New("no such recipient")
43 cb474497 2022-04-19 o
44 cb474497 2022-04-19 o // OpenAliasDB opens the named database file, using the file at dictpath for
45 71bdede3 2022-04-21 o // generating recipient names for new aliases. The database is created and
46 cb474497 2022-04-19 o // initialised if it doesn't already exist.
47 925168e0 2022-04-14 o func OpenAliasDB(name, dictpath string) (*AliasDB, error) {
48 925168e0 2022-04-14 o db, err := sql.Open("sqlite3", name)
49 925168e0 2022-04-14 o if err != nil {
50 925168e0 2022-04-14 o return nil, err
51 925168e0 2022-04-14 o }
52 925168e0 2022-04-14 o stmt := `
53 925168e0 2022-04-14 o CREATE TABLE IF NOT EXISTS aliases (
54 925168e0 2022-04-14 o recipient TEXT PRIMARY KEY,
55 925168e0 2022-04-14 o destination TEXT NOT NULL,
56 cb474497 2022-04-19 o expiry INTEGER NOT NULL,
57 cb474497 2022-04-19 o note TEXT NOT NULL
58 925168e0 2022-04-14 o );`
59 925168e0 2022-04-14 o _, err = db.Exec(stmt)
60 052981c3 2022-04-16 o if err != nil {
61 052981c3 2022-04-16 o return nil, err
62 052981c3 2022-04-16 o }
63 052981c3 2022-04-16 o _, err = os.Stat(dictpath)
64 925168e0 2022-04-14 o return &AliasDB{db, dictpath}, err
65 925168e0 2022-04-14 o }
66 925168e0 2022-04-14 o
67 cb474497 2022-04-19 o // Create is a convenience method for creating a new random alias for destination.
68 cb474497 2022-04-19 o // To set more Alias attributes, such as setting an expiry date, use Put.
69 cb474497 2022-04-19 o func (db *AliasDB) Create(destination string) (Alias, error) {
70 925168e0 2022-04-14 o rcpt, err := RandomUsername(db.dictpath)
71 925168e0 2022-04-14 o if err != nil {
72 925168e0 2022-04-14 o return Alias{}, fmt.Errorf("create alias: %w", err)
73 925168e0 2022-04-14 o }
74 cb474497 2022-04-19 o a := Alias{
75 cb474497 2022-04-19 o Recipient: rcpt,
76 cb474497 2022-04-19 o Destination: destination,
77 925168e0 2022-04-14 o }
78 cb474497 2022-04-19 o if err := db.Put(a); err != nil {
79 cb474497 2022-04-19 o return Alias{}, fmt.Errorf("put %s: %w", a.Recipient, err)
80 cb474497 2022-04-19 o }
81 cb474497 2022-04-19 o return a, nil
82 925168e0 2022-04-14 o }
83 925168e0 2022-04-14 o
84 cb474497 2022-04-19 o // Put creates or updates the given alias in db.
85 cb474497 2022-04-19 o func (db *AliasDB) Put(a Alias) error {
86 cb474497 2022-04-19 o _, err := db.Lookup(a.Recipient)
87 cb474497 2022-04-19 o if err != nil && !errors.Is(err, errRecipientNotExist) {
88 cb474497 2022-04-19 o return fmt.Errorf("lookup %s: %w", a.Recipient, err)
89 cb474497 2022-04-19 o }
90 cb474497 2022-04-19 o var q string
91 cb474497 2022-04-19 o if errors.Is(err, errRecipientNotExist) {
92 cb474497 2022-04-19 o q = "INSERT INTO aliases (recipient, destination, expiry, note) VALUES (?, ?, ?, ?)"
93 71bdede3 2022-04-21 o _, err = db.Exec(q, a.Recipient, a.Destination, a.Expiry.Unix(), a.Note)
94 cb474497 2022-04-19 o } else if err == nil {
95 71bdede3 2022-04-21 o q = "UPDATE aliases SET recipient = ?, destination = ?, expiry = ?, note = ? WHERE recipient = ?"
96 71bdede3 2022-04-21 o _, err = db.Exec(q, a.Recipient, a.Destination, a.Expiry.Unix(), a.Note, a.Recipient)
97 cb474497 2022-04-19 o }
98 cb474497 2022-04-19 o return err
99 cb474497 2022-04-19 o }
100 cb474497 2022-04-19 o
101 cb474497 2022-04-19 o // Lookup returns the Alias with the recipient rcpt. If no alias exists, an error is returned.
102 cb474497 2022-04-19 o func (db *AliasDB) Lookup(rcpt string) (Alias, error) {
103 cb474497 2022-04-19 o var a Alias
104 cb474497 2022-04-19 o q := "SELECT recipient, destination, expiry, note FROM ALIASES WHERE recipient = ?"
105 71bdede3 2022-04-21 o var expiry int64
106 71bdede3 2022-04-21 o err := db.QueryRow(q, rcpt).Scan(&a.Recipient, &a.Destination, &expiry, &a.Note)
107 cb474497 2022-04-19 o if errors.Is(err, sql.ErrNoRows) {
108 cb474497 2022-04-19 o return Alias{}, errRecipientNotExist
109 cb474497 2022-04-19 o } else if err != nil {
110 cb474497 2022-04-19 o return Alias{}, err
111 cb474497 2022-04-19 o }
112 71bdede3 2022-04-21 o a.Expiry = time.Unix(expiry, 0)
113 cb474497 2022-04-19 o return a, nil
114 cb474497 2022-04-19 o }
115 cb474497 2022-04-19 o
116 cb474497 2022-04-19 o // Aliases returns all aliases who have their destination address as dest.
117 925168e0 2022-04-14 o func (db *AliasDB) Aliases(dest string) ([]Alias, error) {
118 925168e0 2022-04-14 o rows, err := db.Query("SELECT recipient, destination, expiry, note FROM aliases WHERE destination = ?", dest)
119 925168e0 2022-04-14 o if err != nil {
120 cb474497 2022-04-19 o return nil, err
121 925168e0 2022-04-14 o }
122 925168e0 2022-04-14 o defer rows.Close()
123 925168e0 2022-04-14 o var aliases []Alias
124 925168e0 2022-04-14 o for rows.Next() {
125 925168e0 2022-04-14 o var a Alias
126 cb474497 2022-04-19 o var sec int64
127 cb474497 2022-04-19 o if err := rows.Scan(&a.Recipient, &a.Destination, &sec, &a.Note); err != nil {
128 925168e0 2022-04-14 o return aliases, err
129 925168e0 2022-04-14 o }
130 cb474497 2022-04-19 o a.Expiry = time.Unix(sec, 0)
131 925168e0 2022-04-14 o aliases = append(aliases, a)
132 925168e0 2022-04-14 o }
133 925168e0 2022-04-14 o return aliases, rows.Err()
134 925168e0 2022-04-14 o }
135 925168e0 2022-04-14 o
136 cb474497 2022-04-19 o // Delete deletes any alias with recipient rcpt.
137 cb474497 2022-04-19 o // No error is returned if no alias exists with the given recipient.
138 925168e0 2022-04-14 o func (db *AliasDB) Delete(rcpt string) error {
139 925168e0 2022-04-14 o _, err := db.Exec("DELETE FROM aliases WHERE recipient = ?", rcpt)
140 925168e0 2022-04-14 o if err != nil {
141 925168e0 2022-04-14 o return fmt.Errorf("delete %s: %w", rcpt, err)
142 925168e0 2022-04-14 o }
143 925168e0 2022-04-14 o return nil
144 925168e0 2022-04-14 o }