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  }