diff --git a/services/tasks/alert.go b/services/tasks/alert.go
index 25f98199..9e27c83a 100644
--- a/services/tasks/alert.go
+++ b/services/tasks/alert.go
@@ -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":"{{ .Name }}
\n#{{ .TaskID }} {{ .TaskResult }} {{ .TaskVersion }}
{{ .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() {
diff --git a/services/tasks/templates/email.tmpl b/services/tasks/templates/email.tmpl
new file mode 100644
index 00000000..850c97a4
--- /dev/null
+++ b/services/tasks/templates/email.tmpl
@@ -0,0 +1,2 @@
+Task {{ .Task.ID }} with template '{{ .Name }}' has failed!
+Task Log: {{ .Task.URL }}
diff --git a/services/tasks/templates/slack.tmpl b/services/tasks/templates/slack.tmpl
new file mode 100644
index 00000000..18afc956
--- /dev/null
+++ b/services/tasks/templates/slack.tmpl
@@ -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
+ }
+ ]
+ }
+ ]
+}
diff --git a/services/tasks/templates/telegram.tmpl b/services/tasks/templates/telegram.tmpl
new file mode 100644
index 00000000..33d50d90
--- /dev/null
+++ b/services/tasks/templates/telegram.tmpl
@@ -0,0 +1,5 @@
+{
+ "chat_id": "{{ .Chat.ID }}",
+ "parse_mode": "HTML",
+ "text": "{{ .Name }}
\n#{{ .Task.ID }} {{ .Task.Result }} {{ .Task.Version }}
- {{ .Task.Desc }}\nby {{ .Author }}\n{{ .Task.URL }}"
+}
diff --git a/util/mail.go b/util/mail.go
deleted file mode 100644
index 64bb3447..00000000
--- a/util/mail.go
+++ /dev/null
@@ -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
-}
diff --git a/util/mailer/mailer.go b/util/mailer/mailer.go
new file mode 100644
index 00000000..03cc9886
--- /dev/null
+++ b/util/mailer/mailer.go
@@ -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
+}