github.com/billybanfield/evergreen@v0.0.0-20170525200750-eeee692790f7/alerts/email.go (about) 1 package alerts 2 3 import ( 4 "bytes" 5 "crypto/tls" 6 "encoding/base64" 7 "fmt" 8 "net/mail" 9 "net/smtp" 10 "strings" 11 12 "github.com/evergreen-ci/evergreen" 13 "github.com/evergreen-ci/evergreen/model" 14 "github.com/evergreen-ci/evergreen/model/alertrecord" 15 "github.com/evergreen-ci/render" 16 "github.com/mongodb/grip" 17 "github.com/pkg/errors" 18 ) 19 20 const EmailSubjectPrologue = "[Evergreen]" 21 22 type SMTPSettings struct { 23 From string 24 Server string 25 Port int 26 UseSSL bool 27 Username string 28 Password string 29 } 30 31 // EmailDeliverer is an implementation of Deliverer that sends notifications to an SMTP server 32 type EmailDeliverer struct { 33 SMTPSettings 34 render *render.Render 35 } 36 37 func (es *EmailDeliverer) Deliver(alertCtx AlertContext, alertConf model.AlertConfig) error { 38 rcptRaw, ok := alertConf.Settings["recipient"] 39 if !ok { 40 return errors.New("missing email address") 41 } 42 grip.Infof("Sending email to %v", rcptRaw) 43 44 var rcpt string 45 if rcpt, ok = rcptRaw.(string); !ok { 46 return errors.New("email address must be a string") 47 } 48 49 var err error 50 subject := getSubject(alertCtx) 51 body, err := es.getBody(alertCtx) 52 if err != nil { 53 return err 54 } 55 56 var c *smtp.Client 57 var tlsCon *tls.Conn 58 if es.UseSSL { 59 tlsCon, err = tls.Dial("tcp", fmt.Sprintf("%v:%v", es.Server, es.Port), &tls.Config{}) 60 if err != nil { 61 return err 62 } 63 c, err = smtp.NewClient(tlsCon, es.Server) 64 if err != nil { 65 return err 66 } 67 } else { 68 c, err = smtp.Dial(fmt.Sprintf("%v:%v", es.Server, es.Port)) 69 } 70 71 if err != nil { 72 return err 73 } 74 75 if es.Username != "" { 76 err = c.Auth(smtp.PlainAuth("", es.Username, es.Password, es.Server)) 77 if err != nil { 78 return err 79 } 80 } 81 82 // Set the sender 83 from := mail.Address{ 84 Name: "Evergreen Alerts", 85 Address: es.From, 86 } 87 err = c.Mail(es.From) 88 if err != nil { 89 grip.Errorf("Error establishing mail sender (%s): %+v", es.From, err) 90 return errors.WithStack(err) 91 } 92 93 err = c.Rcpt(rcpt) 94 if err != nil { 95 grip.Errorf("Error establishing mail recipient (%s): %+v", rcpt, err) 96 return errors.WithStack(err) 97 } 98 99 // Send the email body. 100 wc, err := c.Data() 101 if err != nil { 102 return errors.WithStack(err) 103 } 104 defer wc.Close() 105 106 // set header information 107 header := make(map[string]string) 108 header["From"] = from.String() 109 header["To"] = rcpt 110 header["Subject"] = subject 111 header["MIME-Version"] = "1.0" 112 header["Content-Type"] = "text/html; charset=\"utf-8\"" 113 header["Content-Transfer-Encoding"] = "base64" 114 115 message := "" 116 for k, v := range header { 117 message += fmt.Sprintf("%s: %s\r\n", k, v) 118 } 119 120 message += "\r\n" + base64.StdEncoding.EncodeToString([]byte(body)) 121 122 // write the body 123 buf := bytes.NewBufferString(message) 124 if _, err = buf.WriteTo(wc); err != nil { 125 return errors.WithStack(err) 126 } 127 128 return nil 129 } 130 131 func getTemplate(alertCtx AlertContext) string { 132 switch alertCtx.AlertRequest.Trigger { 133 case alertrecord.SpawnHostTwoHourWarning: 134 fallthrough 135 case alertrecord.SpawnHostTwelveHourWarning: 136 return "email/host_spawn.html" 137 default: 138 return "email/task_fail.html" 139 } 140 } 141 142 // getBody executes a template with the alert data and returns the body of the notification 143 // e-mail to be sent as an HTML string. 144 func (es *EmailDeliverer) getBody(alertCtx AlertContext) (string, error) { 145 out := &bytes.Buffer{} 146 template := getTemplate(alertCtx) 147 err := es.render.HTML(out, alertCtx, "content", template) 148 if err != nil { 149 return "", errors.WithStack(err) 150 } 151 return out.String(), nil 152 } 153 154 // taskFailureSubject creates an email subject for a task failure in the style of 155 // Test Failures: Task_name on Variant (test1, test2) // ProjectName @ githash 156 // based on the given AlertContext. 157 func taskFailureSubject(ctx AlertContext) string { 158 subj := &bytes.Buffer{} 159 failed := []string{} 160 for _, test := range ctx.Task.TestResults { 161 if test.Status == evergreen.TestFailedStatus { 162 failed = append(failed, cleanTestName(test.TestFile)) 163 } 164 } 165 switch { 166 case ctx.Task.Details.TimedOut: 167 subj.WriteString("Task Timed Out: ") 168 case ctx.Task.Details.Type == model.SystemCommandType: 169 subj.WriteString("Task System Failure: ") 170 case len(failed) == 1: 171 subj.WriteString("Test Failure: ") 172 case len(failed) > 1: 173 subj.WriteString("Test Failures: ") 174 default: 175 subj.WriteString("Task Failed: ") 176 } 177 178 fmt.Fprintf(subj, "%s on %s ", ctx.Task.DisplayName, ctx.Build.DisplayName) 179 180 // include test names if <= 4 failed, otherwise print two plus the number remaining 181 if len(failed) > 0 { 182 subj.WriteString("(") 183 if len(failed) <= 4 { 184 subj.WriteString(strings.Join(failed, ", ")) 185 } else { 186 fmt.Fprintf(subj, "%s, %s, +%v more", failed[0], failed[1], len(failed)-2) 187 } 188 subj.WriteString(") ") 189 } 190 191 fmt.Fprintf(subj, "// %s @ %s", ctx.ProjectRef.DisplayName, ctx.Version.Revision[0:8]) 192 return subj.String() 193 } 194 195 // getSubject generates a subject line for an e-mail for the given alert. 196 func getSubject(alertCtx AlertContext) string { 197 switch alertCtx.AlertRequest.Trigger { 198 case alertrecord.FirstVersionFailureId: 199 return fmt.Sprintf("First Task Failure: %s on %s // %s @ %s", 200 alertCtx.Task.DisplayName, 201 alertCtx.Build.DisplayName, 202 alertCtx.ProjectRef.DisplayName, 203 alertCtx.Version.Revision[0:8]) 204 case alertrecord.FirstVariantFailureId: 205 return fmt.Sprintf("Variant Failure: %s // %s @ %s", 206 alertCtx.Build.DisplayName, 207 alertCtx.ProjectRef.DisplayName, 208 alertCtx.Version.Revision[0:8], 209 ) 210 case alertrecord.SpawnHostTwoHourWarning: 211 return fmt.Sprintf("Your %s host (%s) will expire in two hours.", 212 alertCtx.Host.Distro, alertCtx.Host.Id) 213 case alertrecord.SpawnHostTwelveHourWarning: 214 return fmt.Sprintf("Your %s host (%s) will expire in twelve hours.", 215 alertCtx.Host.Distro, alertCtx.Host.Id) 216 // TODO(EVG-224) alertrecord.SpawnHostExpired: 217 } 218 return taskFailureSubject(alertCtx) 219 } 220 221 // cleanTestName returns the last item of a test's path. 222 // TODO: stop accommodating this. 223 func cleanTestName(path string) string { 224 if unixIdx := strings.LastIndex(path, "/"); unixIdx != -1 { 225 // if the path ends in a slash, remove it and try again 226 if unixIdx == len(path)-1 { 227 return cleanTestName(path[:len(path)-1]) 228 } 229 return path[unixIdx+1:] 230 } 231 if windowsIdx := strings.LastIndex(path, `\`); windowsIdx != -1 { 232 // if the path ends in a slash, remove it and try again 233 if windowsIdx == len(path)-1 { 234 return cleanTestName(path[:len(path)-1]) 235 } 236 return path[windowsIdx+1:] 237 } 238 return path 239 }