github.com/decred/politeia@v1.4.0/politeiawww/legacy/mail/client.go (about) 1 // Copyright (c) 2021 The Decred developers 2 // Use of this source code is governed by an ISC 3 // license that can be found in the LICENSE file. 4 5 package mail 6 7 import ( 8 "crypto/tls" 9 "crypto/x509" 10 "fmt" 11 "net/mail" 12 "net/url" 13 "os" 14 "time" 15 16 "github.com/dajohi/goemail" 17 "github.com/decred/politeia/politeiawww/legacy/user" 18 "github.com/google/uuid" 19 ) 20 21 const ( 22 // defaultRateLimitPeriod is the default rate limit period that is 23 // used when initializing a new client. This value is configurable 24 // so that it can be updated during tests. 25 defaultRateLimitPeriod = 24 * time.Hour 26 ) 27 28 // client provides an SMTP client for sending emails from a preset email 29 // address. 30 // 31 // client implements the Mailer interface. 32 type client struct { 33 smtp *goemail.SMTP // SMTP server 34 mailName string // From name 35 mailAddress string // From email address 36 mailerDB user.MailerDB // User mailer database in www 37 disabled bool // Has email been disabled 38 39 // rateLimit is the maximum number of emails that can be sent to 40 // any individual user during a single rateLimitPeriod. Once the 41 // rate limit is hit the user must wait one rateLimitPeriod before 42 // the will be sent any additional emails. The rate limit is only 43 // applied to certain client methods. 44 rateLimit int 45 rateLimitPeriod time.Duration 46 } 47 48 // IsEnabled returns whether the mail server is enabled. 49 // 50 // This function satisfies the Mailer interface. 51 func (c *client) IsEnabled() bool { 52 return !c.disabled 53 } 54 55 // SendTo sends an email to a list of recipient email addresses. 56 // This function does not rate limit emails and a recipient does 57 // does not need to correspond to a politeiawww user. This function 58 // can be used to send emails to sysadmins or similar cases. 59 // 60 // This function satisfies the Mailer interface. 61 func (c *client) SendTo(subject, body string, recipients []string) error { 62 if c.disabled || len(recipients) == 0 { 63 return nil 64 } 65 66 // Setup email 67 msg := goemail.NewMessage(c.mailAddress, subject, body) 68 msg.SetName(c.mailName) 69 70 // Add all recipients to BCC 71 for _, v := range recipients { 72 msg.AddBCC(v) 73 } 74 75 return c.smtp.Send(msg) 76 } 77 78 // SendToUsers sends an email to a list of recipient email 79 // addresses. The recipient MUST correspond to a politeiawww user 80 // in the database for the email to be sent. This function rate 81 // limits the number of emails that can be sent to any individual 82 // user over a 24 hour period. If a recipient is provided that does 83 // not correspond to a politeiawww user, the email is simply 84 // skipped. An error is not returned. 85 // 86 // This function satisfies the Mailer interface. 87 func (c *client) SendToUsers(subjects, body string, recipients map[uuid.UUID]string) error { 88 if c.disabled || len(recipients) == 0 { 89 return nil 90 } 91 92 filtered, err := c.filterRecipients(recipients) 93 if err != nil { 94 return err 95 } 96 97 // Handle valid recipients. 98 err = c.SendTo(subjects, body, filtered.valid) 99 if err != nil { 100 return err 101 } 102 103 // Handle warning email recipients. 104 err = c.SendTo(limitEmailSubject, limitEmailBody, filtered.warning) 105 if err != nil { 106 return err 107 } 108 109 // Update email histories in the db. 110 err = c.mailerDB.EmailHistoriesSave(filtered.histories) 111 if err != nil { 112 return err 113 } 114 115 return nil 116 } 117 118 // filteredRecipients is returned by the filteredRecipients function and 119 // contains the recipients that should receive some sort of email notification. 120 // 121 // Users that have already hit the email rate limit are not included in this 122 // reply. 123 // 124 // If a user has previously hit the rate limit, but a full rate limit period 125 // has passed, their email history is reset and they will be included in the 126 // reply. 127 type filteredRecipients struct { 128 // valid contains the email addresses of the users that have not 129 // hit the email rate limit and are eligible to receive an email. 130 valid []string 131 132 // warning contains the email addresses of the users that have hit 133 // the email rate limit during this invocation and should be sent 134 // the rate limit warning email. 135 warning []string 136 137 // histories contains the updated email histories of the users in 138 // the valid and warning lists. 139 histories map[uuid.UUID]user.EmailHistory 140 } 141 142 // filterRecipients filters the users map[userid]email argument into the 143 // filteredRecipients struct. 144 func (c *client) filterRecipients(users map[uuid.UUID]string) (*filteredRecipients, error) { 145 // Compile user IDs from recipients and get their email histories. 146 ids := make([]uuid.UUID, 0, len(users)) 147 for id := range users { 148 ids = append(ids, id) 149 } 150 hs, err := c.mailerDB.EmailHistoriesGet(ids) 151 if err != nil { 152 return nil, err 153 } 154 155 // Divide recipients into valid and warning recipients, and parse their 156 // new email history. 157 var ( 158 valid = make([]string, 0, len(users)) 159 warning = make([]string, 0, len(users)) 160 histories = make(map[uuid.UUID]user.EmailHistory, len(users)) 161 ) 162 for userID, email := range users { 163 history, ok := hs[userID] 164 if !ok { 165 // User does not have a mail history yet, add user to valid 166 // recipients and create his email history. 167 histories[userID] = user.EmailHistory{ 168 Timestamps: []int64{time.Now().Unix()}, 169 LimitWarningSent: false, 170 } 171 valid = append(valid, email) 172 continue 173 } 174 175 // Filter timestamps for the past rate limit period. 176 history.Timestamps = filterTimestamps(history.Timestamps, 177 c.rateLimitPeriod) 178 179 // Check if user can receive the email notification. 180 if len(history.Timestamps) < c.rateLimit { 181 // Rate limit has not been hit, add user to valid recipients and 182 // update email history. 183 valid = append(valid, email) 184 history.Timestamps = append(history.Timestamps, time.Now().Unix()) 185 history.LimitWarningSent = false 186 histories[userID] = history 187 } 188 189 // Check if user has hit the email rate limit and needs to be warned. 190 if len(history.Timestamps) == c.rateLimit && !history.LimitWarningSent { 191 // Rate limit has been hit with the last email notification above. 192 // If limit warning email has not yet been sent, add user to 193 // warning recipients and update email history. 194 warning = append(warning, email) 195 history.LimitWarningSent = true 196 histories[userID] = history 197 } 198 } 199 200 return &filteredRecipients{ 201 valid: valid, 202 warning: warning, 203 histories: histories, 204 }, nil 205 } 206 207 // filterTimestamps filters out timestamps from the passed in slice that comes 208 // before the specified delta time duration. 209 func filterTimestamps(in []int64, delta time.Duration) []int64 { 210 before := time.Now().Add(-delta) 211 out := make([]int64, 0, len(in)) 212 213 for _, ts := range in { 214 timestamp := time.Unix(ts, 0) 215 if timestamp.Before(before) { 216 continue 217 } 218 out = append(out, ts) 219 } 220 221 return out 222 } 223 224 // The limit email is sent to users as a warning when they hit the email rate 225 // limit. 226 const limitEmailSubject = "Email Rate Limit Hit" 227 const limitEmailBody = ` 228 Your email rate limit for the past 24 hours has been hit. This measure is used to avoid malicious users from spamming Politeia's email server. You will not receive any notification emails for 24 hours. 229 230 We apologize for any inconvenience. 231 ` 232 233 // NewClient returns a new client. 234 func NewClient(host, user, password, emailAddress, certPath string, skipVerify bool, rateLimit int, db user.MailerDB) (*client, error) { 235 // Email is considered disabled if any of the required user 236 // credentials are missing. 237 if host == "" || user == "" || password == "" { 238 log.Infof("Mail: DISABLED") 239 return &client{ 240 disabled: true, 241 }, nil 242 } 243 244 // Parse mail host 245 h := fmt.Sprintf("smtps://%v:%v@%v", user, password, host) 246 u, err := url.Parse(h) 247 if err != nil { 248 return nil, err 249 } 250 251 log.Infof("Mail host: smtps://%v:[password]@%v", user, host) 252 253 // Parse email address 254 a, err := mail.ParseAddress(emailAddress) 255 if err != nil { 256 return nil, err 257 } 258 259 log.Infof("Mail address: %v", a.String()) 260 261 // Setup tls config 262 tlsConfig := &tls.Config{ 263 InsecureSkipVerify: skipVerify, 264 } 265 if !skipVerify && certPath != "" { 266 cert, err := os.ReadFile(certPath) 267 if err != nil { 268 return nil, err 269 } 270 certPool, err := x509.SystemCertPool() 271 if err != nil { 272 certPool = x509.NewCertPool() 273 } 274 certPool.AppendCertsFromPEM(cert) 275 tlsConfig.RootCAs = certPool 276 } 277 278 // Setup smtp context 279 smtp, err := goemail.NewSMTP(u.String(), tlsConfig) 280 if err != nil { 281 return nil, err 282 } 283 284 return &client{ 285 smtp: smtp, 286 mailName: a.Name, 287 mailAddress: a.Address, 288 mailerDB: db, 289 disabled: false, 290 rateLimit: rateLimit, 291 rateLimitPeriod: defaultRateLimitPeriod, 292 }, nil 293 }