Skip to content

Commit

Permalink
middlewares: mail
Browse files Browse the repository at this point in the history
  • Loading branch information
mcuadros committed Oct 12, 2015
1 parent b4b7032 commit a8fdd9b
Show file tree
Hide file tree
Showing 3 changed files with 242 additions and 0 deletions.
6 changes: 6 additions & 0 deletions cli/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ type Config struct {
Global struct {
middlewares.SlackConfig
middlewares.SaveConfig
middlewares.MailConfig
}
Jobs map[string]*ExecJobConfig `gcfg:"Job"`
}
Expand Down Expand Up @@ -53,6 +54,8 @@ func (c *Config) build() (*core.Scheduler, error) {
c.buildSchedulerMiddlewares(sh)

for name, j := range c.Jobs {
defaults.SetDefaults(j)

j.Client = d
j.Name = name
j.buildMiddlewares()
Expand Down Expand Up @@ -80,6 +83,7 @@ func (c *Config) buildLogger() core.Logger {
func (c *Config) buildSchedulerMiddlewares(sh *core.Scheduler) {
sh.Use(middlewares.NewSlack(&c.Global.SlackConfig))
sh.Use(middlewares.NewSave(&c.Global.SaveConfig))
sh.Use(middlewares.NewMail(&c.Global.MailConfig))
}

// ExecJobConfig contains all configuration params needed to build a ExecJob
Expand All @@ -88,10 +92,12 @@ type ExecJobConfig struct {
middlewares.OverlapConfig
middlewares.SlackConfig
middlewares.SaveConfig
middlewares.MailConfig
}

func (c *ExecJobConfig) buildMiddlewares() {
c.ExecJob.Use(middlewares.NewOverlap(&c.OverlapConfig))
c.ExecJob.Use(middlewares.NewSlack(&c.SlackConfig))
c.ExecJob.Use(middlewares.NewSave(&c.SaveConfig))
c.ExecJob.Use(middlewares.NewMail(&c.MailConfig))
}
157 changes: 157 additions & 0 deletions middlewares/mail.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
package middlewares

import (
"bytes"
"encoding/json"
"fmt"
"html/template"
"io"
"os"
"strings"

"gopkg.in/gomail.v2"

"github.com/mcuadros/ofelia/core"
)

// MailConfig configuration for the Mail middleware
type MailConfig struct {
SMTPHost string `gcfg:"smtp-host"`
SMTPPort int `gcfg:"smtp-port"`
SMTPUser string `gcfg:"smtp-user"`
SMTPPassword string `gcfg:"smtp-password"`
EmailTo string `gcfg:"email-to"`
EmailFrom string `gcfg:"email-from" default:"ofelia@%s"`
MailOnlyOnError bool `gcfg:"mail-only-on-error"`
}

// NewMail returns a Mail middleware if the given configuration is not empty
func NewMail(c *MailConfig) core.Middleware {
var m core.Middleware

if !IsEmpty(c) {
m = &Mail{*c}
}

return m
}

// Mail middleware delivers a email just after an execution finishes
type Mail struct {
MailConfig
}

// ContinueOnStop return allways true, we want always report the final status
func (m *Mail) ContinueOnStop() bool {
return true
}

// Run sents a email with the result of the execution
func (m *Mail) Run(ctx *core.Context) error {
err := ctx.Next()
ctx.Stop(err)

if ctx.Execution.Failed || !m.MailOnlyOnError {
err := m.sendMail(ctx)
if err != nil {
ctx.Logger.Error("Mail error: %q", err)
}
}

return err
}

func (m *Mail) sendMail(ctx *core.Context) error {
msg := gomail.NewMessage()
msg.SetHeader("From", m.from())
msg.SetHeader("To", strings.Split(m.EmailTo, ",")...)
msg.SetHeader("Subject", m.subject(ctx))
msg.SetBody("text/html", m.body(ctx))

base := fmt.Sprintf("%s_%s", ctx.Job.GetName(), ctx.Execution.ID)
msg.Attach(base+".stdout.log", gomail.SetCopyFunc(func(w io.Writer) error {
_, err := io.Copy(w, ctx.Execution.OutputStream)
return err
}))

msg.Attach(base+".stderr.log", gomail.SetCopyFunc(func(w io.Writer) error {
_, err := io.Copy(w, ctx.Execution.ErrorStream)
return err
}))

msg.Attach(base+".stderr.json", gomail.SetCopyFunc(func(w io.Writer) error {
js, _ := json.MarshalIndent(map[string]interface{}{
"Job": ctx.Job,
"Execution": ctx.Execution,
}, "", " ")

_, err := w.Write(js)
return err
}))

d := gomail.NewPlainDialer(m.SMTPHost, m.SMTPPort, m.SMTPUser, m.SMTPPassword)
if err := d.DialAndSend(msg); err != nil {
return err
}

return nil
}

func (m *Mail) from() string {
if strings.Index(m.EmailFrom, "%") == -1 {
return m.EmailFrom
}

hostname, _ := os.Hostname()
return fmt.Sprintf(m.EmailFrom, hostname)
}

func (m *Mail) subject(ctx *core.Context) string {
buf := bytes.NewBuffer(nil)
mailSubjectTemplate.Execute(buf, ctx)

return buf.String()
}

func (m *Mail) body(ctx *core.Context) string {
buf := bytes.NewBuffer(nil)
mailBodyTemplate.Execute(buf, ctx)

return buf.String()
}

var mailBodyTemplate, mailSubjectTemplate *template.Template

func init() {
f := map[string]interface{}{
"status": executionLabel,
}

mailBodyTemplate = template.New("mail-body")
mailSubjectTemplate = template.New("mail-subject")
mailBodyTemplate.Funcs(f)
mailSubjectTemplate.Funcs(f)

template.Must(mailBodyTemplate.Parse(`
<p>
Job ​<b>{{.Job.GetName}}</b>,
Execution <b>{{status .Execution}}</b> in ​<b>{{.Execution.Duration}}</b>​,
command: ​<pre>{{.Job.GetCommand}}</pre>​
</p>
`))

template.Must(mailSubjectTemplate.Parse(
"[Execution {{status .Execution}}] Job {{.Job.GetName}} finished in {{.Execution.Duration}}",
))
}

func executionLabel(e *core.Execution) string {
status := "successful"
if e.Skipped {
status = "skipped"
} else if e.Failed {
status = "failed"
}

return status
}
79 changes: 79 additions & 0 deletions middlewares/mail_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package middlewares

import (
"net"
"strconv"
"strings"
"sync"

"github.com/bradfitz/go-smtpd/smtpd"

. "gopkg.in/check.v1"
)

type MailSuite struct {
BaseSuite

l net.Listener
smtpd *smtpd.Server
smtpdHost string
smtpdPort int
}

var _ = Suite(&MailSuite{})

func (s *MailSuite) SetUpTest(c *C) {
s.BaseSuite.SetUpTest(c)

s.smtpd = &smtpd.Server{
Addr: ":0",
}

ln, err := net.Listen("tcp", "127.0.0.1:0")
c.Assert(err, IsNil)

s.l = ln
go func() {
err := s.smtpd.Serve(ln)
c.Assert(err, IsNil)
}()

p := strings.Split(s.l.Addr().String(), ":")
s.smtpdHost = p[0]
s.smtpdPort, _ = strconv.Atoi(p[1])
}

func (s *MailSuite) TearDownTest(c *C) {
s.l.Close()
}

func (s *MailSuite) TestNewSlackEmpty(c *C) {
c.Assert(NewMail(&MailConfig{}), IsNil)
}

func (s *MailSuite) TestRunSuccess(c *C) {
s.ctx.Start()
s.ctx.Stop(nil)

m := NewMail(&MailConfig{
SMTPHost: s.smtpdHost,
SMTPPort: s.smtpdPort,
EmailTo: "[email protected]",
EmailFrom: "[email protected]",
})

var wg sync.WaitGroup
s.smtpd.OnNewMail = func(_ smtpd.Connection, from smtpd.MailAddress) (smtpd.Envelope, error) {
c.Assert(from.Email(), Equals, "[email protected]")
wg.Done()

return nil, nil
}

wg.Add(1)
go func() {
c.Assert(m.Run(s.ctx), IsNil)
}()

wg.Wait()
}

0 comments on commit a8fdd9b

Please sign in to comment.