Add missing newline in README
[cacert-boardvoting.git] / notifications.go
1 /*
2 Copyright 2017-2019 Jan Dittberner
3
4 Licensed under the Apache License, Version 2.0 (the "License");
5 you may not use this program except in compliance with the License.
6 You may obtain a copy of the License at
7
8 http://www.apache.org/licenses/LICENSE-2.0
9
10 Unless required by applicable law or agreed to in writing, software
11 distributed under the License is distributed on an "AS IS" BASIS,
12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 See the License for the specific language governing permissions and
14 limitations under the License.
15 */
16 package main
17
18 import (
19 "bytes"
20 "fmt"
21 "git.cacert.org/cacert-boardvoting/boardvoting"
22 "github.com/Masterminds/sprig"
23 "gopkg.in/gomail.v2"
24 "text/template"
25
26 log "github.com/sirupsen/logrus"
27 )
28
29 type headerData struct {
30 name string
31 value []string
32 }
33
34 type headerList []headerData
35
36 type recipientData struct {
37 field, address, name string
38 }
39
40 type notificationContent struct {
41 template string
42 data interface{}
43 subject string
44 headers headerList
45 recipients []recipientData
46 }
47
48 type NotificationMail interface {
49 GetNotificationContent() *notificationContent
50 }
51
52 var NotifyMailChannel = make(chan NotificationMail, 1)
53
54 func MailNotifier(quitMailNotifier chan int) {
55 log.Info("Launched mail notifier")
56 for {
57 select {
58 case notification := <-NotifyMailChannel:
59 content := notification.GetNotificationContent()
60 mailText, err := buildMail(content.template, content.data)
61 if err != nil {
62 log.Errorf("building mail failed: %v", err)
63 continue
64 }
65
66 m := gomail.NewMessage()
67 m.SetAddressHeader("From", config.NotificationSenderAddress, "CAcert board voting system")
68 for _, recipient := range content.recipients {
69 m.SetAddressHeader(recipient.field, recipient.address, recipient.name)
70 }
71 m.SetHeader("Subject", content.subject)
72 for _, header := range content.headers {
73 m.SetHeader(header.name, header.value...)
74 }
75 m.SetBody("text/plain", mailText.String())
76
77 d := gomail.NewDialer(config.MailServer.Host, config.MailServer.Port, "", "")
78 if err := d.DialAndSend(m); err != nil {
79 log.Errorf("sending mail failed: %v", err)
80 }
81 case <-quitMailNotifier:
82 log.Info("Ending mail notifier")
83 return
84 }
85 }
86 }
87
88 func buildMail(templateName string, context interface{}) (mailText *bytes.Buffer, err error) {
89 b, err := boardvoting.Asset(fmt.Sprintf("templates/%s", templateName))
90 if err != nil {
91 return
92 }
93 t, err := template.New(templateName).Funcs(sprig.GenericFuncMap()).Parse(string(b))
94 if err != nil {
95 return
96 }
97
98 mailText = bytes.NewBufferString("")
99 if err := t.Execute(mailText, context); err != nil {
100 log.Errorf("Failed to execute template %s with context %+v: %v", templateName, context, err)
101 return nil, err
102 }
103
104 return
105 }
106
107 type notificationBase struct{}
108
109 func (n *notificationBase) getRecipient() recipientData {
110 return recipientData{field: "To", address: config.NoticeMailAddress, name: "CAcert board mailing list"}
111 }
112
113 type decisionReplyBase struct {
114 decision Decision
115 }
116
117 func (n *decisionReplyBase) getHeaders() headerList {
118 headers := make(headerList, 0)
119 headers = append(headers, headerData{
120 name: "References", value: []string{fmt.Sprintf("<%s>", n.decision.Tag)},
121 })
122 headers = append(headers, headerData{
123 name: "In-Reply-To", value: []string{fmt.Sprintf("<%s>", n.decision.Tag)},
124 })
125 return headers
126 }
127
128 func (n *decisionReplyBase) getSubject() string {
129 return fmt.Sprintf("Re: %s - %s", n.decision.Tag, n.decision.Title)
130 }
131
132 type notificationClosedDecision struct {
133 notificationBase
134 decisionReplyBase
135 voteSums VoteSums
136 reasoning string
137 }
138
139 func NewNotificationClosedDecision(decision *Decision, voteSums *VoteSums, reasoning string) NotificationMail {
140 notification := &notificationClosedDecision{voteSums: *voteSums, reasoning: reasoning}
141 notification.decision = *decision
142 return notification
143 }
144
145 func (n *notificationClosedDecision) GetNotificationContent() *notificationContent {
146 return &notificationContent{
147 template: "closed_motion_mail.txt",
148 data: struct {
149 *Decision
150 *VoteSums
151 Reasoning string
152 }{&n.decision, &n.voteSums, n.reasoning},
153 subject: fmt.Sprintf("Re: %s - %s - finalised", n.decision.Tag, n.decision.Title),
154 headers: n.decisionReplyBase.getHeaders(),
155 recipients: []recipientData{n.notificationBase.getRecipient()},
156 }
157 }
158
159 type NotificationCreateMotion struct {
160 notificationBase
161 decision Decision
162 voter Voter
163 }
164
165 func (n *NotificationCreateMotion) GetNotificationContent() *notificationContent {
166 voteURL := fmt.Sprintf("%s/vote/%s", config.BaseURL, n.decision.Tag)
167 unvotedURL := fmt.Sprintf("%s/motions/?unvoted=1", config.BaseURL)
168 return &notificationContent{
169 template: "create_motion_mail.txt",
170 data: struct {
171 *Decision
172 Name string
173 VoteURL string
174 UnvotedURL string
175 }{&n.decision, n.voter.Name, voteURL, unvotedURL},
176 subject: fmt.Sprintf("%s - %s", n.decision.Tag, n.decision.Title),
177 headers: headerList{headerData{"Message-ID", []string{fmt.Sprintf("<%s>", n.decision.Tag)}}},
178 recipients: []recipientData{n.notificationBase.getRecipient()},
179 }
180 }
181
182 type notificationUpdateMotion struct {
183 notificationBase
184 decisionReplyBase
185 voter Voter
186 }
187
188 func NewNotificationUpdateMotion(decision Decision, voter Voter) NotificationMail {
189 notification := notificationUpdateMotion{voter: voter}
190 notification.decision = decision
191 return &notification
192 }
193
194 func (n *notificationUpdateMotion) GetNotificationContent() *notificationContent {
195 voteURL := fmt.Sprintf("%s/vote/%s", config.BaseURL, n.decision.Tag)
196 unvotedURL := fmt.Sprintf("%s/motions/?unvoted=1", config.BaseURL)
197 return &notificationContent{
198 template: "update_motion_mail.txt",
199 data: struct {
200 *Decision
201 Name string
202 VoteURL string
203 UnvotedURL string
204 }{&n.decision, n.voter.Name, voteURL, unvotedURL},
205 subject: n.decisionReplyBase.getSubject(),
206 headers: n.decisionReplyBase.getHeaders(),
207 recipients: []recipientData{n.notificationBase.getRecipient()},
208 }
209 }
210
211 type notificationWithDrawMotion struct {
212 notificationBase
213 decisionReplyBase
214 voter Voter
215 }
216
217 func NewNotificationWithDrawMotion(decision *Decision, voter *Voter) NotificationMail {
218 notification := &notificationWithDrawMotion{voter: *voter}
219 notification.decision = *decision
220 return notification
221 }
222
223 func (n *notificationWithDrawMotion) GetNotificationContent() *notificationContent {
224 return &notificationContent{
225 template: "withdraw_motion_mail.txt",
226 data: struct {
227 *Decision
228 Name string
229 }{&n.decision, n.voter.Name},
230 subject: fmt.Sprintf("Re: %s - %s - withdrawn", n.decision.Tag, n.decision.Title),
231 headers: n.decisionReplyBase.getHeaders(),
232 recipients: []recipientData{n.notificationBase.getRecipient()},
233 }
234 }
235
236 type RemindVoterNotification struct {
237 voter Voter
238 decisions []Decision
239 }
240
241 func (n *RemindVoterNotification) GetNotificationContent() *notificationContent {
242 return &notificationContent{
243 template: "remind_voter_mail.txt",
244 data: struct {
245 Decisions []Decision
246 Name string
247 BaseURL string
248 }{n.decisions, n.voter.Name, config.BaseURL},
249 subject: "Outstanding CAcert board votes",
250 recipients: []recipientData{{"To", n.voter.Reminder, n.voter.Name}},
251 }
252 }
253
254 type voteNotificationBase struct{}
255
256 func (n *voteNotificationBase) getRecipient() recipientData {
257 return recipientData{"To", config.VoteNoticeMailAddress, "CAcert board votes mailing list"}
258 }
259
260 type notificationProxyVote struct {
261 voteNotificationBase
262 decisionReplyBase
263 proxy Voter
264 voter Voter
265 vote Vote
266 justification string
267 }
268
269 func NewNotificationProxyVote(decision *Decision, proxy *Voter, voter *Voter, vote *Vote, justification string) NotificationMail {
270 notification := &notificationProxyVote{proxy: *proxy, voter: *voter, vote: *vote, justification: justification}
271 notification.decision = *decision
272 return notification
273 }
274
275 func (n *notificationProxyVote) GetNotificationContent() *notificationContent {
276 return &notificationContent{
277 template: "proxy_vote_mail.txt",
278 data: struct {
279 Proxy string
280 Vote VoteChoice
281 Voter string
282 Decision *Decision
283 Justification string
284 }{n.proxy.Name, n.vote.Vote, n.voter.Name, &n.decision, n.justification},
285 subject: n.decisionReplyBase.getSubject(),
286 headers: n.decisionReplyBase.getHeaders(),
287 recipients: []recipientData{n.voteNotificationBase.getRecipient()},
288 }
289 }
290
291 type notificationDirectVote struct {
292 voteNotificationBase
293 decisionReplyBase
294 voter Voter
295 vote Vote
296 }
297
298 func NewNotificationDirectVote(decision *Decision, voter *Voter, vote *Vote) NotificationMail {
299 notification := &notificationDirectVote{voter: *voter, vote: *vote}
300 notification.decision = *decision
301 return notification
302 }
303
304 func (n *notificationDirectVote) GetNotificationContent() *notificationContent {
305 return &notificationContent{
306 template: "direct_vote_mail.txt",
307 data: struct {
308 Vote VoteChoice
309 Voter string
310 Decision *Decision
311 }{n.vote.Vote, n.voter.Name, &n.decision},
312 subject: n.decisionReplyBase.getSubject(),
313 headers: n.decisionReplyBase.getHeaders(),
314 recipients: []recipientData{n.voteNotificationBase.getRecipient()},
315 }
316 }