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"} }