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 }