You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
cacert-boardvoting/cmd/boardvoting/notifications.go

152 lines
4.0 KiB
Go

package main
import (
"bytes"
"fmt"
"path"
"text/template"
"git.cacert.org/cacert-boardvoting/internal"
"git.cacert.org/cacert-boardvoting/internal/models"
"github.com/Masterminds/sprig/v3"
"gopkg.in/mail.v2"
)
type recipientData struct {
field, address, name string
}
type NotificationContent struct {
template string
data interface{}
subject string
headers map[string][]string
recipients []recipientData
}
type NotificationMail interface {
GetNotificationContent(*mailConfig) *NotificationContent
}
type MailNotifier struct {
notifyChannel chan NotificationMail
senderAddress string
dialer *mail.Dialer
}
func (app *application) NewMailNotifier() {
app.mailNotifier = &MailNotifier{
notifyChannel: make(chan NotificationMail, 1),
senderAddress: app.mailConfig.NotificationSenderAddress,
dialer: mail.NewDialer(app.mailConfig.SMTPHost, app.mailConfig.SMTPPort, "", ""),
}
}
func (app *application) StartMailNotifier() {
app.infoLog.Print("Launching mail notifier")
for {
select {
case notification := <-app.mailNotifier.notifyChannel:
content := notification.GetNotificationContent(app.mailConfig)
mailText, err := content.buildMail(app.baseURL)
if err != nil {
app.errorLog.Printf("building mail failed: %v", err)
continue
}
m := mail.NewMessage()
m.SetHeaders(content.headers)
m.SetAddressHeader("From", app.mailNotifier.senderAddress, "CAcert board voting system")
for _, recipient := range content.recipients {
m.SetAddressHeader(recipient.field, recipient.address, recipient.name)
}
m.SetHeader("Subject", content.subject)
m.SetBody("text/plain", mailText.String())
if err = app.mailNotifier.dialer.DialAndSend(m); err != nil {
app.errorLog.Printf("sending mail failed: %v", err)
}
case <-app.ctx.Done():
app.infoLog.Print("ending mail notifier")
return
}
}
}
func (n *NotificationContent) buildMail(baseUrl string) (fmt.Stringer, error) {
b, err := internal.MailTemplates.ReadFile(
fmt.Sprintf(path.Join("mailtemplates", n.template)),
)
if err != nil {
return nil, fmt.Errorf("could not read mail template %s: %w", n.template, err)
}
t, err := template.New(n.template).Funcs(sprig.GenericFuncMap()).Parse(string(b))
if err != nil {
return nil, fmt.Errorf("could not parse mail template %s: %w", n.template, err)
}
data := struct {
Data any
BaseURL string
}{Data: n.data, BaseURL: baseUrl}
mailText := bytes.NewBuffer(make([]byte, 0))
if err = t.Execute(mailText, data); err != nil {
return nil, fmt.Errorf(
"failed to execute template %s with context %v: %w", n.template, n.data, err)
}
return mailText, nil
}
type RemindVoterNotification struct {
voter models.Voter
decisions []models.Decision
}
func (r RemindVoterNotification) GetNotificationContent(*mailConfig) *NotificationContent {
return &NotificationContent{
template: "remind_voter_mail.txt",
data: struct {
Decisions []models.Decision
Name string
}{Decisions: r.decisions, Name: r.voter.Name},
subject: "Outstanding CAcert board votes",
recipients: []recipientData{{"To", r.voter.Reminder, r.voter.Name}},
}
}
type ClosedDecisionNotification struct {
decision *models.ClosedDecision
}
func (c *ClosedDecisionNotification) GetNotificationContent(mc *mailConfig) *NotificationContent {
return &NotificationContent{
template: "closed_motion_mail.txt",
data: c.decision,
subject: fmt.Sprintf("Re: %s - %s - finalised", c.decision.Decision.Tag, c.decision.Decision.Title),
headers: c.getHeaders(),
recipients: []recipientData{c.getRecipient(mc)},
}
}
func (c *ClosedDecisionNotification) getHeaders() map[string][]string {
return map[string][]string{
"References": {fmt.Sprintf("<%s>", c.decision.Decision.Tag)},
"In-Reply-To": {fmt.Sprintf("<%s>", c.decision.Decision.Tag)},
}
}
func (c *ClosedDecisionNotification) getRecipient(mc *mailConfig) recipientData {
return recipientData{field: "To", address: mc.NoticeMailAddress, name: "CAcert board mailing list"}
}