The smallest watchdog on earth. Tiny, monitoring-plugins compatible monitoring with a status page. https://cloud.docker.com/repository/docker/momar/chihuahua/general
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

247 lines
7.5 KiB

package notifiers
import (
"bytes"
"codeberg.org/momar/chihuahua"
"encoding/base64"
"github.com/flosch/pongo2"
"github.com/markbates/pkger"
"github.com/matcornic/hermes/v2"
"github.com/rs/zerolog/log"
"io/ioutil"
"net"
"net/mail"
"net/smtp"
"regexp"
"strconv"
"strings"
"time"
)
type SMTPNotifier struct {
queue *BatchQueue
From string `hcl:"from"`
To []string `hcl:"to"`
Server *string `hcl:"server"`
UseCRAMMD5 *bool `hcl:"crammd5"`
TimeoutStr *string `hcl:"delay"`
happyHermes hermes.Hermes
sadHermes hermes.Hermes
}
var SMTPAddressExpression = regexp.MustCompile(`^(?:([^:]*)(?::(.*))?@)?([^:@]*)(?::(.*))?$`)
// TODO: implement sendmail method
var MailSubjectTemplate, MailHTMLTemplate, MailPlaintextTemplate *pongo2.Template
func init() {
chihuahua.Notifiers["smtp"] = &SMTPNotifier{}
pongo2.RegisterFilter("timeFormat", func(in *pongo2.Value, param *pongo2.Value) (out *pongo2.Value, err *pongo2.Error) {
return pongo2.AsValue(time.Unix(int64(in.Integer()), 0).Format("15:04:05")), nil
})
assertNil := func(err error) {
if err != nil {
panic(err)
}
}
res, err := pkger.Open("codeberg.org/momar/chihuahua:/resources/notification-email.subject")
assertNil(err)
src, err := ioutil.ReadAll(res)
assertNil(err)
MailSubjectTemplate, err = pongo2.FromBytes(src)
assertNil(err)
res, err = pkger.Open("codeberg.org/momar/chihuahua:/resources/notification-email.html")
assertNil(err)
src, err = ioutil.ReadAll(res)
assertNil(err)
MailHTMLTemplate, err = pongo2.FromBytes(src)
assertNil(err)
res, err = pkger.Open("codeberg.org/momar/chihuahua:/resources/notification-email.txt")
assertNil(err)
src, err = ioutil.ReadAll(res)
assertNil(err)
MailPlaintextTemplate, err = pongo2.FromBytes(src)
assertNil(err)
}
func (n *SMTPNotifier) Notify(cfg *chihuahua.Config, check chihuahua.Check, previous chihuahua.CheckResult) {
if n.queue == nil {
n.queue = &BatchQueue{
Execute: n.SendBatch,
Timeout: 5 * time.Minute,
}
}
if n.TimeoutStr != nil {
// TODO: also do this in the config function
if delay, err := time.ParseDuration(*n.TimeoutStr); err == nil {
n.queue.Timeout = delay
} else {
log.Error().Str("delay", *n.TimeoutStr).Err(err).Msg("an email notifier has an unparsable \"delay\" attribute")
}
}
n.queue.Enqueue(cfg, check, previous)
}
func (n *SMTPNotifier) SendBatch(cfg *chihuahua.Config, notifications []BatchNotification, commonPrefix []BatchPrefix) {
serverStr := ""
if n.Server != nil {
serverStr = *n.Server
}
server := SMTPAddressExpression.FindStringSubmatch(serverStr)
if server == nil {
log.Error().Msg("an email notifier has an unparsable \"server\" attribute - this should not happen at all and should be reported to the developer!")
return
}
username, password, host, portStr := server[1], server[2], server[3], server[4]
port, err := strconv.ParseUint(portStr, 10, 64)
if err != nil && portStr != "" {
log.Warn().Err(err).Str("port", portStr).Msg("an email notifier specified an invalid port in the \"server\" attribute - will try the default port instead")
}
if port == 0 {
if host == "" {
port = 25
} else {
port = 587
}
}
log.Trace().Uint64("port", port).Str("host", host).Str("username", username).Str("password", password).Msg("preparing to send email")
var auth smtp.Auth
if n.UseCRAMMD5 != nil && *n.UseCRAMMD5 == true {
smtp.CRAMMD5Auth(username, password)
// PlainAuth requires a hostname, so it will be set below
}
// Prepare mail context
count := map[string]uint{
"OK": 0,
"WARNING": 0,
"CRITICAL": 0,
"UNKNOWN": 0,
}
for _, notification := range notifications {
count[notification.Result.Status.String()]++
}
countAll := map[string]uint{
"OK": 0,
"WARNING": 0,
"CRITICAL": 0,
"UNKNOWN": 0,
}
cfg.Walk(func(element *chihuahua.ServerOrGroup) {
if element.Checks != nil {
for _, check := range element.Checks {
if !check.Disable {
countAll[check.Result.Status.String()]++
}
}
}
})
context := pongo2.Context{
"CommonPrefix": commonPrefix,
"Notifications": notifications,
"Count": count,
"CountAll": countAll,
"RootURL": cfg.RootURL,
"From": n.From,
}
// Send mail
for _, receiver := range n.To {
context["To"] = receiver
delete(context, "Subject")
subject, err := MailSubjectTemplate.Execute(context)
if err != nil {
log.Error().Err(err).Msg("couldn't render email subject")
continue
}
subject = strings.TrimSpace(strings.Join(strings.Fields(subject), " "))
context["Subject"] = subject
message := bytes.NewBuffer([]byte{})
message.WriteString("MIME-Version: 1.0\r\n")
message.WriteString("From: " + n.From + "\r\n")
//message.WriteString("To: " + receiver + "\r\n")
message.WriteString("Subject: =?utf-8?B?" + base64.StdEncoding.EncodeToString([]byte(subject)) + "?=\r\n")
message.WriteString("X-Sender: " + n.From + "\r\n")
message.WriteString("X-Mailer: Chihuahua\r\n")
message.WriteString("X-Priority: 1\r\n")
message.WriteString("Content-Type: multipart/alternative;boundary=!WOOF!WOOF!--\r\n")
message.WriteString("\r\n")
message.WriteString("--!WOOF!WOOF!--\r\n")
message.WriteString("Content-type: text/html;charset=utf-8\r\n")
htmlBody, err := MailHTMLTemplate.Execute(context)
if err != nil {
log.Error().Err(err).Msg("couldn't render HTML email body")
continue
}
message.WriteString(htmlBody) // TODO: wrap lines!
message.WriteString("\r\n")
message.WriteString("--!WOOF!WOOF!--\r\n")
message.WriteString("Content-type: text/plain;charset=utf-8\r\n")
plaintextBody, err := MailPlaintextTemplate.Execute(context)
if err != nil {
log.Error().Err(err).Msg("couldn't render plaintext email body")
continue
}
message.WriteString(plaintextBody)
targetedHost := host
if targetedHost == "" {
// send email directly to the target server (unreliable)
// todo: only print once on setup (we really need a CheckConfig() method...
receiverAddress, err := mail.ParseAddress(receiver)
if err != nil {
log.Error().Err(err).Msg("couldn't send notification through smtp (error when parsing receiver address)")
continue
}
receiverHost := strings.Split(receiverAddress.Address, "@")[strings.Count(receiverAddress.Address, "@")]
mx, err := net.LookupMX(strings.TrimRight(receiverHost, "> "))
if err != nil {
log.Error().Err(err).Msg("an email notifier is missing the SMTP host in the \"server\" attribute, and the target host's MX record couldn't be found - the email will not be delivered")
continue
}
log.Warn().Msg("an email notifier is missing the SMTP host in the \"server\" attribute - it will do its best to deliver the email directly, but you should set an SMTP server for improved reliability")
targetedHost = mx[0].Host
}
targetedAuth := auth
if targetedAuth == nil {
targetedAuth = smtp.PlainAuth("", username, password, targetedHost)
}
if username == "" && password == "" {
targetedAuth = nil
}
fromAddress, err := mail.ParseAddress(n.From)
if err != nil {
log.Error().Err(err).Msg("couldn't send notification through smtp (error when parsing sender address)")
continue
}
err = smtp.SendMail(
targetedHost+":"+strconv.FormatUint(port, 10),
targetedAuth,
fromAddress.Address,
[]string{receiver},
append([]byte("To: "+receiver+"\r\n"), message.Bytes()...),
)
if err != nil {
log.Error().Err(err).Msg("couldn't send notification through smtp")
} else {
log.Trace().Str("to", receiver).Msg("an email has been sent successfully")
}
}
}