X Tutup
Skip to content

Commit ecd6d01

Browse files
notify-mailer: Improve error checking during template execution (letsencrypt#5932)
- Break message body construction out into a testable method. - Ensure that in the event of a missing key, an informative error is returned instead of allowing the message to be populated with the zero value of the key. - Add message body construction tests for success, empty map, and missing key. - Comment the `recipient` struct and it's `Data` field to make it clear that SRE must be informed of any modifications. Fixes letsencrypt#5921
1 parent f69f524 commit ecd6d01

File tree

2 files changed

+82
-11
lines changed

2 files changed

+82
-11
lines changed

cmd/notify-mailer/main.go

Lines changed: 35 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,25 @@ func sortAddresses(input addressToRecipientMap) []string {
102102
return addresses
103103
}
104104

105+
// makeMessageBody is a helper for mailer.run() that's split out for the
106+
// purposes of testing.
107+
func (m *mailer) makeMessageBody(recipients []recipient) (string, error) {
108+
var messageBody strings.Builder
109+
110+
// Ensure that in the event of a missing key, an informative error is
111+
// returned.
112+
m.emailTemplate.Option("missingkey=error")
113+
err := m.emailTemplate.Execute(&messageBody, recipients)
114+
if err != nil {
115+
return "", err
116+
}
117+
118+
if messageBody.Len() == 0 {
119+
return "", errors.New("templating resulted in an empty message body")
120+
}
121+
return messageBody.String(), nil
122+
}
123+
105124
func (m *mailer) run() error {
106125
err := m.ok()
107126
if err != nil {
@@ -161,17 +180,13 @@ func (m *mailer) run() error {
161180
recipients := addressToRecipient[address]
162181
m.logStatus(address, i+1, totalAddresses, startTime)
163182

164-
var messageBody strings.Builder
165-
err = m.emailTemplate.Execute(&messageBody, recipients)
183+
messageBody, err := m.makeMessageBody(recipients)
166184
if err != nil {
167-
return err
168-
}
169-
170-
if messageBody.Len() == 0 {
171-
return errors.New("message body was empty after interpolation")
185+
m.log.Errf("Skipping %q due to templating error: %s", address, err)
186+
continue
172187
}
173188

174-
err = m.mailer.SendMail([]string{address}, m.subject, messageBody.String())
189+
err = m.mailer.SendMail([]string{address}, m.subject, messageBody)
175190
if err != nil {
176191
var badAddrErr bmail.BadAddressSMTPError
177192
if errors.As(err, &badAddrErr) {
@@ -253,11 +268,20 @@ func getAddressForID(id int64, dbMap dbSelector) ([]string, error) {
253268
return addresses, nil
254269
}
255270

256-
// recipient represents a single record from the recipient list file.
271+
// recipient represents a single record from the recipient list file. The 'id'
272+
// column is parsed to the 'id' field, all additional data will be parsed to a
273+
// mapping of column name to value in the 'Data' field. Please inform SRE if you
274+
// make any changes to the exported fields of this struct. These fields are
275+
// referenced in operationally critical e-mail templates used to notify
276+
// subscribers during incident response.
257277
type recipient struct {
278+
// id is the subscriber's ID.
258279
id int64
259280

260-
// Data is exported so the contents can be referenced from message template.
281+
// Data is a mapping of column name to value parsed from a single record in
282+
// the provided recipient list file. It's exported so the contents can be
283+
// accessed by the the template package. Please inform SRE if you make any
284+
// changes to this field.
261285
Data map[string]string
262286
}
263287

@@ -384,7 +408,7 @@ fields to be interpolated into the email template:
384408
The additional fields will be interpolated with Golang templating, e.g.:
385409
386410
Your last issuance on each account was:
387-
{{ range . }} {{ .Extra.lastIssuance }}
411+
{{ range . }} {{ .Data.lastIssuance }}
388412
{{ end }}
389413
390414
To help the operator gain confidence in the mailing run before committing fully

cmd/notify-mailer/main_test.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,53 @@ func TestReadRecipientListWithNoHeaderOrRecords(t *testing.T) {
225225
test.AssertErrorIs(t, err, io.EOF)
226226
}
227227

228+
func TestMakeMessageBody(t *testing.T) {
229+
emailTemplate := `{{range . }}
230+
{{ .Data.date }}
231+
{{ .Data.domainName }}
232+
{{end}}`
233+
234+
m := &mailer{
235+
log: blog.UseMock(),
236+
mailer: &mocks.Mailer{},
237+
emailTemplate: template.Must(template.New("email").Parse(emailTemplate)),
238+
sleepInterval: 0,
239+
targetRange: interval{end: "\xFF"},
240+
clk: newFakeClock(t),
241+
recipients: nil,
242+
dbMap: mockEmailResolver{},
243+
}
244+
245+
recipients := []recipient{
246+
{id: 10, Data: map[string]string{"date": "2018-11-21", "domainName": "example.com"}},
247+
{id: 23, Data: map[string]string{"date": "2018-11-22", "domainName": "example.net"}},
248+
}
249+
250+
expectedMessageBody := `
251+
2018-11-21
252+
example.com
253+
254+
2018-11-22
255+
example.net
256+
`
257+
258+
// Ensure that a very basic template with 2 recipients can be successfully
259+
// executed.
260+
messageBody, err := m.makeMessageBody(recipients)
261+
test.AssertNotError(t, err, "failed to execute a valid template")
262+
test.AssertEquals(t, messageBody, expectedMessageBody)
263+
264+
// With no recipients we should get an empty body error.
265+
recipients = []recipient{}
266+
_, err = m.makeMessageBody(recipients)
267+
test.AssertError(t, err, "should have errored on empty body")
268+
269+
// With a missing key we should get an informative templating error.
270+
recipients = []recipient{{id: 10, Data: map[string]string{"domainName": "example.com"}}}
271+
_, err = m.makeMessageBody(recipients)
272+
test.AssertEquals(t, err.Error(), "template: email:2:8: executing \"email\" at <.Data.date>: map has no entry for key \"date\"")
273+
}
274+
228275
func TestSleepInterval(t *testing.T) {
229276
const sleepLen = 10
230277
mc := &mocks.Mailer{}

0 commit comments

Comments
 (0)
X Tutup