mirror of
https://github.com/semaphoreui/semaphore.git
synced 2024-11-23 12:30:41 +01:00
feat: refactoring of alerts and send correct email alerts
Previously the sent email alerts have been missing mandatory headers like `Date` and it was also missing content type, content transfer encoding and mime version. I have taken proper examples form the unmaintained gomail library to build right emails. Besides that I have refactored the calls for alerts, they git the same structure now and it should be prepared to inject custom templates for all altering methods at some later point. Generally it is prepared for a more flexible alert handling.
This commit is contained in:
parent
7c0fed0809
commit
5c8b87620e
@ -2,23 +2,20 @@ package tasks
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"github.com/ansible-semaphore/semaphore/lib"
|
||||
"github.com/ansible-semaphore/semaphore/util"
|
||||
"html/template"
|
||||
"embed"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"github.com/ansible-semaphore/semaphore/lib"
|
||||
"github.com/ansible-semaphore/semaphore/util"
|
||||
"github.com/ansible-semaphore/semaphore/util/mailer"
|
||||
)
|
||||
|
||||
const emailTemplate = "Subject: Task '{{ .Name }}' failed\r\n" +
|
||||
"From: {{ .From }}\r\n" +
|
||||
"\r\n" +
|
||||
"Task {{ .TaskID }} with template '{{ .Name }}' has failed!`\n" +
|
||||
"Task Log: {{ .TaskURL }}"
|
||||
|
||||
const telegramTemplate = `{"chat_id": "{{ .ChatID }}","parse_mode":"HTML","text":"<code>{{ .Name }}</code>\n#{{ .TaskID }} <b>{{ .TaskResult }}</b> <code>{{ .TaskVersion }}</code> {{ .TaskDescription }}\nby {{ .Author }}\n{{ .TaskURL }}"}`
|
||||
|
||||
const slackTemplate = `{ "attachments": [ { "title": "Task: {{ .Name }}", "title_link": "{{ .TaskURL }}", "text": "execution ID #{{ .TaskID }}, status: {{ .TaskResult }}!", "color": "{{ .Color }}", "mrkdwn_in": ["text"], "fields": [ { "title": "Author", "value": "{{ .Author }}", "short": true }] } ]}`
|
||||
//go:embed templates/*.tmpl
|
||||
var templates embed.FS
|
||||
|
||||
const microsoftTeamsTemplate = `{
|
||||
"type": "message",
|
||||
@ -75,16 +72,23 @@ const microsoftTeamsTemplate = `{
|
||||
|
||||
// Alert represents an alert that will be templated and sent to the appropriate service
|
||||
type Alert struct {
|
||||
TaskID string
|
||||
Name string
|
||||
TaskURL string
|
||||
ChatID string
|
||||
TaskResult string
|
||||
TaskDescription string
|
||||
TaskVersion string
|
||||
Author string
|
||||
Color string
|
||||
From string
|
||||
Name string
|
||||
Author string
|
||||
Color string
|
||||
Task alertTask
|
||||
Chat alertChat
|
||||
}
|
||||
|
||||
type alertTask struct {
|
||||
ID string
|
||||
URL string
|
||||
Result string
|
||||
Desc string
|
||||
Version string
|
||||
}
|
||||
|
||||
type alertChat struct {
|
||||
ID string
|
||||
}
|
||||
|
||||
func (t *TaskRunner) sendMailAlert() {
|
||||
@ -92,45 +96,58 @@ func (t *TaskRunner) sendMailAlert() {
|
||||
return
|
||||
}
|
||||
|
||||
mailHost := util.Config.EmailHost + ":" + util.Config.EmailPort
|
||||
body := bytes.NewBufferString("")
|
||||
author, version := t.alertInfos()
|
||||
|
||||
var mailBuffer bytes.Buffer
|
||||
alert := Alert{
|
||||
TaskID: strconv.Itoa(t.Task.ID),
|
||||
Name: t.Template.Name,
|
||||
TaskURL: util.Config.WebHost + "/project/" + strconv.Itoa(t.Template.ProjectID) +
|
||||
"/templates/" + strconv.Itoa(t.Template.ID) +
|
||||
"?t=" + strconv.Itoa(t.Task.ID),
|
||||
From: util.Config.EmailSender,
|
||||
Author: author,
|
||||
Color: t.alertColor("email"),
|
||||
Task: alertTask{
|
||||
ID: strconv.Itoa(t.Task.ID),
|
||||
URL: t.taskLink(),
|
||||
Result: strings.ToUpper(string(t.Task.Status)),
|
||||
Version: version,
|
||||
Desc: t.Task.Message,
|
||||
},
|
||||
}
|
||||
tpl := template.New("mail body template")
|
||||
tpl, err := tpl.Parse(emailTemplate)
|
||||
util.LogError(err)
|
||||
|
||||
t.panicOnError(tpl.Execute(&mailBuffer, alert), "Can't generate alert template!")
|
||||
tpl, err := template.ParseFS(templates, "templates/email.tmpl")
|
||||
|
||||
for _, user := range t.users {
|
||||
userObj, err2 := t.pool.store.GetUser(user)
|
||||
if err != nil {
|
||||
t.Log("Can't parse email alert template!")
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if !userObj.Alert {
|
||||
if err := tpl.Execute(body, alert); err != nil {
|
||||
t.Log("Can't generate email alert template!")
|
||||
panic(err)
|
||||
}
|
||||
|
||||
for _, uid := range t.users {
|
||||
user, err := t.pool.store.GetUser(uid)
|
||||
|
||||
if !user.Alert {
|
||||
continue
|
||||
}
|
||||
|
||||
if err2 != nil {
|
||||
util.LogError(err2)
|
||||
if err != nil {
|
||||
util.LogError(err)
|
||||
continue
|
||||
}
|
||||
|
||||
if util.Config.EmailSecure {
|
||||
err2 = util.SendSecureMail(util.Config.EmailHost, util.Config.EmailPort,
|
||||
util.Config.EmailSender, util.Config.EmailUsername, util.Config.EmailPassword,
|
||||
userObj.Email, mailBuffer)
|
||||
} else {
|
||||
err2 = util.SendMail(mailHost, util.Config.EmailSender, userObj.Email, mailBuffer)
|
||||
}
|
||||
|
||||
if err2 != nil {
|
||||
util.LogError(err2)
|
||||
if err := mailer.Send(
|
||||
util.Config.EmailSecure,
|
||||
util.Config.EmailHost,
|
||||
util.Config.EmailPort,
|
||||
util.Config.EmailUsername,
|
||||
util.Config.EmailPassword,
|
||||
util.Config.EmailSender,
|
||||
user.Email,
|
||||
fmt.Sprintf("Task '%s' failed", t.Template.Name),
|
||||
body.String(),
|
||||
); err != nil {
|
||||
util.LogError(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -153,60 +170,45 @@ func (t *TaskRunner) sendTelegramAlert() {
|
||||
return
|
||||
}
|
||||
|
||||
var telegramBuffer bytes.Buffer
|
||||
|
||||
var version string
|
||||
if t.Task.Version != nil {
|
||||
version = *t.Task.Version
|
||||
} else if t.Task.BuildTaskID != nil {
|
||||
buildVer := t.Task.GetIncomingVersion(t.pool.store)
|
||||
if buildVer != nil {
|
||||
version = *buildVer
|
||||
}
|
||||
} else {
|
||||
version = ""
|
||||
}
|
||||
|
||||
var message string
|
||||
if t.Task.Message != "" {
|
||||
message = "- " + t.Task.Message
|
||||
}
|
||||
|
||||
var author string
|
||||
if t.Task.UserID != nil {
|
||||
user, err := t.pool.store.GetUser(*t.Task.UserID)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
author = user.Name
|
||||
}
|
||||
body := bytes.NewBufferString("")
|
||||
author, version := t.alertInfos()
|
||||
|
||||
alert := Alert{
|
||||
TaskID: strconv.Itoa(t.Task.ID),
|
||||
Name: t.Template.Name,
|
||||
TaskURL: util.Config.WebHost + "/project/" + strconv.Itoa(t.Template.ProjectID) + "/templates/" + strconv.Itoa(t.Template.ID) + "?t=" + strconv.Itoa(t.Task.ID),
|
||||
ChatID: chatID,
|
||||
TaskResult: strings.ToUpper(string(t.Task.Status)),
|
||||
TaskVersion: version,
|
||||
TaskDescription: message,
|
||||
Author: author,
|
||||
Name: t.Template.Name,
|
||||
Author: author,
|
||||
Color: t.alertColor("telegram"),
|
||||
Task: alertTask{
|
||||
ID: strconv.Itoa(t.Task.ID),
|
||||
URL: t.taskLink(),
|
||||
Result: strings.ToUpper(string(t.Task.Status)),
|
||||
Version: version,
|
||||
Desc: t.Task.Message,
|
||||
},
|
||||
Chat: alertChat{
|
||||
ID: chatID,
|
||||
},
|
||||
}
|
||||
|
||||
tpl := template.New("telegram body template")
|
||||
tpl, err := template.ParseFS(templates, "templates/telegram.tmpl")
|
||||
|
||||
tpl, err := tpl.Parse(telegramTemplate)
|
||||
if err != nil {
|
||||
t.Log("Can't parse telegram template!")
|
||||
t.Log("Can't parse telegram alert template!")
|
||||
panic(err)
|
||||
}
|
||||
|
||||
err = tpl.Execute(&telegramBuffer, alert)
|
||||
if err != nil {
|
||||
t.Log("Can't generate alert template!")
|
||||
if err := tpl.Execute(body, alert); err != nil {
|
||||
t.Log("Can't generate telegram alert template!")
|
||||
panic(err)
|
||||
}
|
||||
|
||||
resp, err := http.Post("https://api.telegram.org/bot"+util.Config.TelegramToken+"/sendMessage", "application/json", &telegramBuffer)
|
||||
resp, err := http.Post(
|
||||
fmt.Sprintf(
|
||||
"https://api.telegram.org/bot%s/sendMessage",
|
||||
util.Config.TelegramToken,
|
||||
),
|
||||
"application/json",
|
||||
body,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
t.Log("Can't send telegram alert! Error: " + err.Error())
|
||||
@ -224,11 +226,50 @@ func (t *TaskRunner) sendSlackAlert() {
|
||||
return
|
||||
}
|
||||
|
||||
slackUrl := util.Config.SlackUrl
|
||||
body := bytes.NewBufferString("")
|
||||
author, version := t.alertInfos()
|
||||
|
||||
var slackBuffer bytes.Buffer
|
||||
alert := Alert{
|
||||
Name: t.Template.Name,
|
||||
Author: author,
|
||||
Color: t.alertColor("slack"),
|
||||
Task: alertTask{
|
||||
ID: strconv.Itoa(t.Task.ID),
|
||||
URL: t.taskLink(),
|
||||
Result: strings.ToUpper(string(t.Task.Status)),
|
||||
Version: version,
|
||||
Desc: t.Task.Message,
|
||||
},
|
||||
}
|
||||
|
||||
tpl, err := template.ParseFS(templates, "templates/slack.tmpl")
|
||||
|
||||
if err != nil {
|
||||
t.Log("Can't parse slack alert template!")
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if err := tpl.Execute(body, alert); err != nil {
|
||||
t.Log("Can't generate slack alert template!")
|
||||
panic(err)
|
||||
}
|
||||
|
||||
resp, err := http.Post(
|
||||
util.Config.SlackUrl,
|
||||
"application/json",
|
||||
body,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
t.Log("Can't send slack alert! Error: " + err.Error())
|
||||
} else if resp.StatusCode != 200 {
|
||||
t.Log("Can't send slack alert! Response code: " + strconv.Itoa(resp.StatusCode))
|
||||
}
|
||||
}
|
||||
|
||||
func (t *TaskRunner) alertInfos() (string, string) {
|
||||
version := ""
|
||||
|
||||
var version string
|
||||
if t.Task.Version != nil {
|
||||
version = *t.Task.Version
|
||||
} else if t.Task.BuildTaskID != nil {
|
||||
@ -237,65 +278,51 @@ func (t *TaskRunner) sendSlackAlert() {
|
||||
version = ""
|
||||
}
|
||||
|
||||
var message string
|
||||
if t.Task.Message != "" {
|
||||
message = "- " + t.Task.Message
|
||||
}
|
||||
author := ""
|
||||
|
||||
var author string
|
||||
if t.Task.UserID != nil {
|
||||
user, err := t.pool.store.GetUser(*t.Task.UserID)
|
||||
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
author = user.Name
|
||||
}
|
||||
|
||||
var color string
|
||||
if t.Task.Status == lib.TaskSuccessStatus {
|
||||
color = "good"
|
||||
} else if t.Task.Status == lib.TaskFailStatus {
|
||||
color = "danger"
|
||||
} else if t.Task.Status == lib.TaskRunningStatus {
|
||||
color = "#333CFF"
|
||||
} else if t.Task.Status == lib.TaskWaitingStatus {
|
||||
color = "#FFFC33"
|
||||
} else if t.Task.Status == lib.TaskStoppingStatus {
|
||||
color = "#BEBEBE"
|
||||
} else if t.Task.Status == lib.TaskStoppedStatus {
|
||||
color = "#5B5B5B"
|
||||
}
|
||||
alert := Alert{
|
||||
TaskID: strconv.Itoa(t.Task.ID),
|
||||
Name: t.Template.Name,
|
||||
TaskURL: util.Config.WebHost + "/project/" + strconv.Itoa(t.Template.ProjectID) + "/templates/" + strconv.Itoa(t.Template.ID) + "?t=" + strconv.Itoa(t.Task.ID),
|
||||
TaskResult: strings.ToUpper(string(t.Task.Status)),
|
||||
TaskVersion: version,
|
||||
TaskDescription: message,
|
||||
Author: author,
|
||||
Color: color,
|
||||
return version, author
|
||||
}
|
||||
|
||||
func (t *TaskRunner) alertColor(kind string) string {
|
||||
switch kind {
|
||||
case "slack":
|
||||
switch t.Task.Status {
|
||||
case lib.TaskSuccessStatus:
|
||||
return "good"
|
||||
case lib.TaskFailStatus:
|
||||
return "danger"
|
||||
case lib.TaskRunningStatus:
|
||||
return "#333CFF"
|
||||
case lib.TaskWaitingStatus:
|
||||
return "#FFFC33"
|
||||
case lib.TaskStoppingStatus:
|
||||
return "#BEBEBE"
|
||||
case lib.TaskStoppedStatus:
|
||||
return "#5B5B5B"
|
||||
}
|
||||
}
|
||||
|
||||
tpl := template.New("slack body template")
|
||||
return ""
|
||||
}
|
||||
|
||||
tpl, err := tpl.Parse(slackTemplate)
|
||||
if err != nil {
|
||||
t.Log("Can't parse slack template!")
|
||||
panic(err)
|
||||
}
|
||||
|
||||
err = tpl.Execute(&slackBuffer, alert)
|
||||
if err != nil {
|
||||
t.Log("Can't generate alert template!")
|
||||
panic(err)
|
||||
}
|
||||
resp, err := http.Post(slackUrl, "application/json", &slackBuffer)
|
||||
|
||||
if err != nil {
|
||||
t.Log("Can't send slack alert! Error: " + err.Error())
|
||||
} else if resp.StatusCode != 200 {
|
||||
t.Log("Can't send slack alert! Response code: " + strconv.Itoa(resp.StatusCode))
|
||||
}
|
||||
func (t *TaskRunner) taskLink() string {
|
||||
return fmt.Sprintf(
|
||||
"%s/project/%d/templates/%d?t=%d",
|
||||
util.Config.WebHost,
|
||||
t.Template.ProjectID,
|
||||
t.Template.ID,
|
||||
t.Task.ID,
|
||||
)
|
||||
}
|
||||
|
||||
func (t *TaskRunner) sendMicrosoftTeamsAlert() {
|
||||
|
2
services/tasks/templates/email.tmpl
Normal file
2
services/tasks/templates/email.tmpl
Normal file
@ -0,0 +1,2 @@
|
||||
Task {{ .Task.ID }} with template '{{ .Name }}' has failed!
|
||||
Task Log: {{ .Task.URL }}
|
20
services/tasks/templates/slack.tmpl
Normal file
20
services/tasks/templates/slack.tmpl
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"attachments": [
|
||||
{
|
||||
"title": "Task: {{ .Name }}",
|
||||
"title_link": "{{ .Task.URL }}",
|
||||
"text": "execution #{{ .Task.ID }}, status: {{ .Task.Result }}!",
|
||||
"color": "{{ .Color }}",
|
||||
"mrkdwn_in": [
|
||||
"text"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"title": "Author",
|
||||
"value": "{{ .Author }}",
|
||||
"short": true
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
5
services/tasks/templates/telegram.tmpl
Normal file
5
services/tasks/templates/telegram.tmpl
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"chat_id": "{{ .Chat.ID }}",
|
||||
"parse_mode": "HTML",
|
||||
"text": "<code>{{ .Name }}</code>\n#{{ .Task.ID }} <b>{{ .Task.Result }}</b> <code>{{ .Task.Version }}</code> - {{ .Task.Desc }}\nby {{ .Author }}\n{{ .Task.URL }}"
|
||||
}
|
67
util/mail.go
67
util/mail.go
@ -1,67 +0,0 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"io"
|
||||
"net/smtp"
|
||||
)
|
||||
|
||||
// SendMail dispatches a mail using smtp
|
||||
func SendMail(emailHost, mailSender, mailRecipient string, mail bytes.Buffer) error {
|
||||
c, err := smtp.Dial(emailHost)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer func(c *smtp.Client) {
|
||||
err = c.Close()
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
}(c)
|
||||
|
||||
// Set the sender and recipient.
|
||||
err = c.Mail(mailSender)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = c.Rcpt(mailRecipient)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Send the email body.
|
||||
wc, err := c.Data()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer func(wc io.WriteCloser) {
|
||||
err = wc.Close()
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
}(wc)
|
||||
_, err = mail.WriteTo(wc)
|
||||
return err
|
||||
}
|
||||
|
||||
// SendSecureMail dispatches a mail using smtp with authentication and StartTLS
|
||||
func SendSecureMail(emailHost, emailPort, mailSender, mailUsername, mailPassword, mailRecipient string, mail bytes.Buffer) error {
|
||||
|
||||
// Receiver email address.
|
||||
to := []string{
|
||||
mailRecipient,
|
||||
}
|
||||
|
||||
// Authentication.
|
||||
auth := smtp.PlainAuth("", mailUsername, mailPassword, emailHost)
|
||||
|
||||
// Sending email.
|
||||
err := smtp.SendMail(emailHost+":"+emailPort, auth, mailSender, to, mail.Bytes())
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
return err
|
||||
}
|
160
util/mailer/mailer.go
Normal file
160
util/mailer/mailer.go
Normal file
@ -0,0 +1,160 @@
|
||||
package mailer
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net"
|
||||
"net/smtp"
|
||||
"strings"
|
||||
"text/template"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
mailerBase = "MIME-version: 1.0\r\n" +
|
||||
"Content-Type: text/plain; charset=UTF-8\r\n" +
|
||||
"Content-Transfer-Encoding: quoted-printable\r\n" +
|
||||
"Date: {{ .Date }}\r\n" +
|
||||
"To: {{ .To }}\r\n" +
|
||||
"From: {{ .From }}\r\n" +
|
||||
"Subject: {{ .Subject }}\r\n\r\n" +
|
||||
"{{ .Body }}"
|
||||
)
|
||||
|
||||
var (
|
||||
r = strings.NewReplacer(
|
||||
"\r\n", "",
|
||||
"\r", "",
|
||||
"\n", "",
|
||||
"%0a", "",
|
||||
"%0d", "",
|
||||
)
|
||||
)
|
||||
|
||||
// Send simply sends the defined mail via SMTP.
|
||||
func Send(
|
||||
secure bool,
|
||||
host string,
|
||||
port string,
|
||||
username,
|
||||
password,
|
||||
from,
|
||||
to,
|
||||
subject string,
|
||||
content string,
|
||||
) error {
|
||||
body := bytes.NewBufferString("")
|
||||
tpl, err := template.New("").Parse(mailerBase)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := tpl.Execute(body, struct {
|
||||
Date string
|
||||
To string
|
||||
From string
|
||||
Subject string
|
||||
Body string
|
||||
}{
|
||||
Date: time.Now().UTC().Format(time.RFC1123),
|
||||
To: r.Replace(to),
|
||||
From: r.Replace(from),
|
||||
Subject: r.Replace(subject),
|
||||
Body: content,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if secure {
|
||||
return plainauth(
|
||||
host,
|
||||
port,
|
||||
username,
|
||||
password,
|
||||
from,
|
||||
to,
|
||||
body,
|
||||
)
|
||||
}
|
||||
|
||||
return anonymous(
|
||||
host,
|
||||
port,
|
||||
from,
|
||||
to,
|
||||
body,
|
||||
)
|
||||
}
|
||||
|
||||
func plainauth(
|
||||
host string,
|
||||
port string,
|
||||
username string,
|
||||
password string,
|
||||
from string,
|
||||
to string,
|
||||
body *bytes.Buffer,
|
||||
) error {
|
||||
return smtp.SendMail(
|
||||
net.JoinHostPort(
|
||||
host,
|
||||
port,
|
||||
),
|
||||
smtp.PlainAuth(
|
||||
"",
|
||||
username,
|
||||
password,
|
||||
host,
|
||||
),
|
||||
from,
|
||||
[]string{to},
|
||||
body.Bytes(),
|
||||
)
|
||||
}
|
||||
|
||||
func anonymous(
|
||||
host string,
|
||||
port string,
|
||||
from string,
|
||||
to string,
|
||||
body *bytes.Buffer,
|
||||
) error {
|
||||
c, err := smtp.Dial(
|
||||
net.JoinHostPort(
|
||||
host,
|
||||
port,
|
||||
),
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer c.Close()
|
||||
|
||||
if err := c.Mail(
|
||||
r.Replace(from),
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = c.Rcpt(
|
||||
r.Replace(to),
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
w, err := c.Data()
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer w.Close()
|
||||
|
||||
if _, err := body.WriteTo(w); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
Loading…
Reference in New Issue
Block a user