github.com/decred/politeia@v1.4.0/politeiawww/legacy/pi/mail.go (about)

     1  // Copyright (c) 2020-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 pi
     6  
     7  import (
     8  	"bytes"
     9  	"fmt"
    10  	"net/url"
    11  	"strconv"
    12  	"strings"
    13  	"text/template"
    14  
    15  	rcv1 "github.com/decred/politeia/politeiawww/api/records/v1"
    16  	"github.com/google/uuid"
    17  )
    18  
    19  const (
    20  	// The following routes are used in notification emails to direct
    21  	// the user to the correct GUI pages.
    22  	guiRouteRecordDetails = "/record/{token}"
    23  	guiRouteRecordComment = "/record/{token}/comments/{id}"
    24  )
    25  
    26  type proposalNew struct {
    27  	Username string // Author username
    28  	Name     string // Proposal name
    29  	Link     string // GUI proposal details URL
    30  }
    31  
    32  var proposalNewText = `
    33  A new proposal has been submitted on Politeia by {{.Username}}:
    34  
    35  {{.Name}}
    36  {{.Link}}
    37  `
    38  
    39  var proposalNewTmpl = template.Must(
    40  	template.New("proposalNew").Parse(proposalNewText))
    41  
    42  func (p *Pi) mailNtfnProposalNew(token, name, username string, recipients map[uuid.UUID]string) error {
    43  	route := strings.Replace(guiRouteRecordDetails, "{token}", token, 1)
    44  	u, err := url.Parse(p.cfg.WebServerAddress + route)
    45  	if err != nil {
    46  		return err
    47  	}
    48  
    49  	tmplData := proposalNew{
    50  		Username: username,
    51  		Name:     name,
    52  		Link:     u.String(),
    53  	}
    54  
    55  	subject := fmt.Sprintf(`New Proposal Submitted "%v"`, name)
    56  	body, err := populateTemplate(proposalNewTmpl, tmplData)
    57  	if err != nil {
    58  		return err
    59  	}
    60  
    61  	return p.mail.SendToUsers(subject, body, recipients)
    62  }
    63  
    64  type proposalEdit struct {
    65  	Name     string // Proposal name
    66  	Version  uint32 // Proposal version
    67  	Username string // Author username
    68  	Link     string // GUI proposal details URL
    69  }
    70  
    71  var proposalEditText = `
    72  A proposal by {{.Username}} has just been edited:
    73  
    74  {{.Name}} (Version {{.Version}})
    75  {{.Link}}
    76  `
    77  
    78  var proposalEditTmpl = template.Must(
    79  	template.New("proposalEdit").Parse(proposalEditText))
    80  
    81  func (p *Pi) mailNtfnProposalEdit(token string, version uint32, name, username string, recipients map[uuid.UUID]string) error {
    82  	route := strings.Replace(guiRouteRecordDetails, "{token}", token, 1)
    83  	u, err := url.Parse(p.cfg.WebServerAddress + route)
    84  	if err != nil {
    85  		return err
    86  	}
    87  
    88  	tmplData := proposalEdit{
    89  		Name:     name,
    90  		Version:  version,
    91  		Username: username,
    92  		Link:     u.String(),
    93  	}
    94  
    95  	subject := fmt.Sprintf(`Proposal Edited "%v"`, name)
    96  	body, err := populateTemplate(proposalEditTmpl, tmplData)
    97  	if err != nil {
    98  		return err
    99  	}
   100  
   101  	return p.mail.SendToUsers(subject, body, recipients)
   102  }
   103  
   104  type proposalPublished struct {
   105  	Name string // Proposal name
   106  	Link string // GUI proposal details URL
   107  }
   108  
   109  var proposalPublishedTmpl = template.Must(
   110  	template.New("proposalPublished").Parse(proposalPublishedText))
   111  
   112  var proposalPublishedText = `
   113  A new proposal has just been published on Politeia.
   114  
   115  {{.Name}}
   116  {{.Link}}
   117  `
   118  
   119  func (p *Pi) mailNtfnProposalSetStatus(token, name string, status rcv1.RecordStatusT, recipients map[uuid.UUID]string) error {
   120  	route := strings.Replace(guiRouteRecordDetails, "{token}", token, 1)
   121  	u, err := url.Parse(p.cfg.WebServerAddress + route)
   122  	if err != nil {
   123  		return err
   124  	}
   125  
   126  	var (
   127  		subject string
   128  		body    string
   129  	)
   130  	switch status {
   131  	case rcv1.RecordStatusPublic:
   132  		subject = fmt.Sprintf(`New Proposal Published "%v"`, name)
   133  		tmplData := proposalPublished{
   134  			Name: name,
   135  			Link: u.String(),
   136  		}
   137  		body, err = populateTemplate(proposalPublishedTmpl, tmplData)
   138  		if err != nil {
   139  			return err
   140  		}
   141  
   142  	default:
   143  		return fmt.Errorf("no mail ntfn for status %v", status)
   144  	}
   145  
   146  	return p.mail.SendToUsers(subject, body, recipients)
   147  }
   148  
   149  type proposalPublishedToAuthor struct {
   150  	Name string // Proposal name
   151  	Link string // GUI proposal details URL
   152  }
   153  
   154  var proposalPublishedToAuthorText = `
   155  Your proposal has just been made public on Politeia!
   156  
   157  Your proposal has now entered the discussion phase where the community can leave comments and provide feedback.  Be sure to keep an eye out for new comments and to answer any questions that the community may have.  You can edit your proposal at any point prior to the start of voting.
   158  
   159  Once you feel that enough time has been given for discussion you may authorize the vote to commence on your proposal.  An admin is not able to start the voting process until you explicitly authorize it.  You can authorize a proposal vote by opening the proposal page and clicking on the authorize vote button.
   160  
   161  {{.Name}}
   162  {{.Link}}
   163  
   164  If you have any questions, drop by the proposals channel on matrix.
   165  https://chat.decred.org/#/room/#proposals:decred.org
   166  `
   167  var proposalPublishedToAuthorTmpl = template.Must(
   168  	template.New("proposalPublishedToAuthor").
   169  		Parse(proposalPublishedToAuthorText))
   170  
   171  type proposalCensoredToAuthor struct {
   172  	Name   string // Proposal name
   173  	Reason string // Reason for censoring
   174  }
   175  
   176  var proposalCensoredToAuthorText = `
   177  Your proposal on Politeia has been censored.
   178  
   179  {{.Name}}
   180  Reason: {{.Reason}}
   181  `
   182  
   183  var proposalCensoredToAuthorTmpl = template.Must(
   184  	template.New("proposalCensoredToAuthor").
   185  		Parse(proposalCensoredToAuthorText))
   186  
   187  func (p *Pi) mailNtfnProposalSetStatusToAuthor(token, name string, status rcv1.RecordStatusT, reason string, recipient map[uuid.UUID]string) error {
   188  	route := strings.Replace(guiRouteRecordDetails, "{token}", token, 1)
   189  	u, err := url.Parse(p.cfg.WebServerAddress + route)
   190  	if err != nil {
   191  		return err
   192  	}
   193  
   194  	var (
   195  		subject string
   196  		body    string
   197  	)
   198  	switch status {
   199  	case rcv1.RecordStatusPublic:
   200  		subject = "Your Proposal Has Been Published " + token
   201  		tmplData := proposalPublishedToAuthor{
   202  			Name: name,
   203  			Link: u.String(),
   204  		}
   205  		body, err = populateTemplate(proposalPublishedToAuthorTmpl, tmplData)
   206  		if err != nil {
   207  			return err
   208  		}
   209  
   210  	case rcv1.RecordStatusCensored:
   211  		subject = fmt.Sprintf(`Your Proposal Has Been Censored "%v"`, name)
   212  		tmplData := proposalCensoredToAuthor{
   213  			Name:   name,
   214  			Reason: reason,
   215  		}
   216  		body, err = populateTemplate(proposalCensoredToAuthorTmpl, tmplData)
   217  		if err != nil {
   218  			return err
   219  		}
   220  
   221  	default:
   222  		return fmt.Errorf("no author notification for prop status %v", status)
   223  	}
   224  
   225  	return p.mail.SendToUsers(subject, body, recipient)
   226  }
   227  
   228  type commentNewToProposalAuthor struct {
   229  	Username string // Comment author username
   230  	Name     string // Proposal name
   231  	Link     string // Comment link
   232  }
   233  
   234  var commentNewToProposalAuthorText = `
   235  {{.Username}} has commented on your proposal "{{.Name}}".
   236  
   237  {{.Link}}
   238  `
   239  
   240  var commentNewToProposalAuthorTmpl = template.Must(
   241  	template.New("commentNewToProposalAuthor").
   242  		Parse(commentNewToProposalAuthorText))
   243  
   244  func (p *Pi) mailNtfnCommentNewToProposalAuthor(token string, commentID uint32, commentUsername, proposalName string, recipient map[uuid.UUID]string) error {
   245  	cid := strconv.FormatUint(uint64(commentID), 10)
   246  	route := strings.Replace(guiRouteRecordComment, "{token}", token, 1)
   247  	route = strings.Replace(route, "{id}", cid, 1)
   248  
   249  	u, err := url.Parse(p.cfg.WebServerAddress + route)
   250  	if err != nil {
   251  		return err
   252  	}
   253  
   254  	subject := fmt.Sprintf(`New Comment on Your Proposal "%v"`, proposalName)
   255  	tmplData := commentNewToProposalAuthor{
   256  		Username: commentUsername,
   257  		Name:     proposalName,
   258  		Link:     u.String(),
   259  	}
   260  	body, err := populateTemplate(commentNewToProposalAuthorTmpl, tmplData)
   261  	if err != nil {
   262  		return err
   263  	}
   264  
   265  	return p.mail.SendToUsers(subject, body, recipient)
   266  }
   267  
   268  type commentReply struct {
   269  	Username string // Comment author username
   270  	Name     string // Proposal name
   271  	Link     string // Comment link
   272  }
   273  
   274  var commentReplyText = `
   275  {{.Username}} has replied to your comment on "{{.Name}}".
   276  
   277  {{.Link}}
   278  `
   279  
   280  var commentReplyTmpl = template.Must(
   281  	template.New("commentReply").Parse(commentReplyText))
   282  
   283  func (p *Pi) mailNtfnCommentReply(token string, commentID uint32, commentUsername, proposalName string, recipient map[uuid.UUID]string) error {
   284  	cid := strconv.FormatUint(uint64(commentID), 10)
   285  	route := strings.Replace(guiRouteRecordComment, "{token}", token, 1)
   286  	route = strings.Replace(route, "{id}", cid, 1)
   287  
   288  	u, err := url.Parse(p.cfg.WebServerAddress + route)
   289  	if err != nil {
   290  		return err
   291  	}
   292  
   293  	subject := fmt.Sprintf(`New Reply to Your Comment on "%v"`, proposalName)
   294  	tmplData := commentReply{
   295  		Username: commentUsername,
   296  		Name:     proposalName,
   297  		Link:     u.String(),
   298  	}
   299  	body, err := populateTemplate(commentReplyTmpl, tmplData)
   300  	if err != nil {
   301  		return err
   302  	}
   303  
   304  	return p.mail.SendToUsers(subject, body, recipient)
   305  }
   306  
   307  type voteAuthorized struct {
   308  	Name string // Proposal name
   309  	Link string // GUI proposal details url
   310  }
   311  
   312  var voteAuthorizedText = `
   313  A proposal vote has been authorized.
   314  
   315  {{.Name}}
   316  {{.Link}}
   317  `
   318  
   319  var voteAuthorizedTmpl = template.Must(
   320  	template.New("voteAuthorized").Parse(voteAuthorizedText))
   321  
   322  func (p *Pi) mailNtfnVoteAuthorized(token, name string, recipients map[uuid.UUID]string) error {
   323  	route := strings.Replace(guiRouteRecordDetails, "{token}", token, 1)
   324  	u, err := url.Parse(p.cfg.WebServerAddress + route)
   325  	if err != nil {
   326  		return err
   327  	}
   328  
   329  	subject := fmt.Sprintf(`Voting Authorized for "%v"`, name)
   330  	tmplData := voteAuthorized{
   331  		Name: name,
   332  		Link: u.String(),
   333  	}
   334  	body, err := populateTemplate(voteAuthorizedTmpl, tmplData)
   335  	if err != nil {
   336  		return err
   337  	}
   338  
   339  	return p.mail.SendToUsers(subject, body, recipients)
   340  }
   341  
   342  type voteStarted struct {
   343  	Name string // Proposal name
   344  	Link string // GUI proposal details url
   345  }
   346  
   347  const voteStartedText = `
   348  Voting has started on a Politeia proposal.
   349  
   350  {{.Name}}
   351  {{.Link}}
   352  `
   353  
   354  var voteStartedTmpl = template.Must(
   355  	template.New("voteStarted").Parse(voteStartedText))
   356  
   357  func (p *Pi) mailNtfnVoteStarted(token, name string, recipients map[uuid.UUID]string) error {
   358  	route := strings.Replace(guiRouteRecordDetails, "{token}", token, 1)
   359  	u, err := url.Parse(p.cfg.WebServerAddress + route)
   360  	if err != nil {
   361  		return err
   362  	}
   363  
   364  	subject := fmt.Sprintf(`Voting Started for "%v"`, name)
   365  	tmplData := voteStarted{
   366  		Name: name,
   367  		Link: u.String(),
   368  	}
   369  	body, err := populateTemplate(voteStartedTmpl, tmplData)
   370  	if err != nil {
   371  		return err
   372  	}
   373  
   374  	return p.mail.SendToUsers(subject, body, recipients)
   375  }
   376  
   377  type voteStartedToAuthor struct {
   378  	Name string // Proposal name
   379  	Link string // GUI proposal details url
   380  }
   381  
   382  const voteStartedToAuthorText = `
   383  Voting has just started on your Politeia proposal.
   384  
   385  {{.Name}}
   386  {{.Link}}
   387  `
   388  
   389  var voteStartedToAuthorTmpl = template.Must(
   390  	template.New("voteStartedToAuthor").Parse(voteStartedToAuthorText))
   391  
   392  func (p *Pi) mailNtfnVoteStartedToAuthor(token, name string, recipient map[uuid.UUID]string) error {
   393  	route := strings.Replace(guiRouteRecordDetails, "{token}", token, 1)
   394  	u, err := url.Parse(p.cfg.WebServerAddress + route)
   395  	if err != nil {
   396  		return err
   397  	}
   398  
   399  	subject := fmt.Sprintf(`Voting Started on Your Proposal "%v"`, name)
   400  	tmplData := voteStartedToAuthor{
   401  		Name: name,
   402  		Link: u.String(),
   403  	}
   404  	body, err := populateTemplate(voteStartedToAuthorTmpl, tmplData)
   405  	if err != nil {
   406  		return err
   407  	}
   408  
   409  	return p.mail.SendToUsers(subject, body, recipient)
   410  }
   411  
   412  func populateTemplate(tmpl *template.Template, tmplData interface{}) (string, error) {
   413  	var b bytes.Buffer
   414  	err := tmpl.Execute(&b, tmplData)
   415  	if err != nil {
   416  		return "", err
   417  	}
   418  	return b.String(), nil
   419  }