11 // An Alias is used by a server to forward mail it receives.
12 // Mail addressed to Recipient is forwarded to Destination.
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
18 // Destination contains an email address, with domain suffix,
19 // to which mail will be forwarded.
21 // Expiry specifies a time after which the alias is considered inactive;
22 // that is, mail addressed to Recipient should be bounced.
24 // Note contains user-defined text that can be used to,
25 // for example, identify the alias.
27 // modTime contains the time the alias was last modified.
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 {
36 case a.Recipient != b.Recipient:
38 case a.Destination != b.Destination:
40 case !a.Expiry.Equal(b.Expiry):
42 case a.Note != b.Note:
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
55 // AliasDB is an implementation of AliasStore backed by a sqlite3 database.
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)
72 CREATE TABLE IF NOT EXISTS aliases (
73 recipient TEXT PRIMARY KEY,
74 destination TEXT NOT NULL,
75 expiry INTEGER NOT NULL,
77 modtime INTEGER NOT NULL DEFAULT (unixepoch())
79 _, err = db.Exec(stmt)
83 _, err = os.Stat(dictpath)
84 return &AliasDB{db, dictpath}, err
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)
92 return Alias{}, fmt.Errorf("create alias: %w", err)
96 Destination: destination,
98 if err := db.Put(a); err != nil {
99 return Alias{}, fmt.Errorf("put %s: %w", a.Recipient, err)
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)
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)
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) {
124 q := "SELECT recipient, destination, expiry, note, modtime FROM ALIASES WHERE recipient = ?"
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 {
133 a.Expiry = time.Unix(expiry, 0)
134 a.modTime = time.Unix(modtime, 0)
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)
150 if err := rows.Scan(&a.Recipient, &a.Destination, &sec, &a.Note); err != nil {
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)
164 return fmt.Errorf("delete %s: %w", rcpt, err)