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.
27 f7343271 2022-04-25 o // modTime contains the time the alias was last modified.
28 f7343271 2022-04-25 o modTime time.Time
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 {
36 c6eb8eaf 2022-04-25 o case a.Recipient != b.Recipient:
38 c6eb8eaf 2022-04-25 o case a.Destination != b.Destination:
40 c6eb8eaf 2022-04-25 o case !a.Expiry.Equal(b.Expiry):
42 c6eb8eaf 2022-04-25 o case a.Note != b.Note:
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
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 {
58 925168e0 2022-04-14 o dictpath string
61 cb474497 2022-04-19 o var errRecipientNotExist = errors.New("no such recipient")
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
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())
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
83 052981c3 2022-04-16 o _, err = os.Stat(dictpath)
84 925168e0 2022-04-14 o return &AliasDB{db, dictpath}, err
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)
95 cb474497 2022-04-19 o Recipient: rcpt,
96 cb474497 2022-04-19 o Destination: destination,
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)
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)
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)
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) {
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
133 71bdede3 2022-04-21 o a.Expiry = time.Unix(expiry, 0)
134 f7343271 2022-04-25 o a.modTime = time.Unix(modtime, 0)
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
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() {
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
153 cb474497 2022-04-19 o a.Expiry = time.Unix(sec, 0)
154 925168e0 2022-04-14 o aliases = append(aliases, a)
156 925168e0 2022-04-14 o return aliases, rows.Err()
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)