diff --git a/docker/blocks/smtp/Dockerfile b/docker/blocks/smtp/Dockerfile new file mode 100644 index 00000000000..c1a3adba7c8 --- /dev/null +++ b/docker/blocks/smtp/Dockerfile @@ -0,0 +1,13 @@ +FROM centos:centos7 +MAINTAINER Przemyslaw Ozgo + +RUN \ + yum update -y && \ + yum install -y net-snmp net-snmp-utils && \ + yum clean all + +COPY bootstrap.sh /tmp/bootstrap.sh + +EXPOSE 161 + +ENTRYPOINT ["/tmp/bootstrap.sh"] diff --git a/docker/blocks/smtp/bootstrap.sh b/docker/blocks/smtp/bootstrap.sh new file mode 100755 index 00000000000..a78f9d6dc16 --- /dev/null +++ b/docker/blocks/smtp/bootstrap.sh @@ -0,0 +1,27 @@ +#!/bin/sh + +set -u + +# User params +USER_PARAMS=$@ + +# Internal params +RUN_CMD="snmpd -f ${USER_PARAMS}" + +####################################### +# Echo/log function +# Arguments: +# String: value to log +####################################### +log() { + if [[ "$@" ]]; then echo "[`date +'%Y-%m-%d %T'`] $@"; + else echo; fi +} + +# Launch +log $RUN_CMD +$RUN_CMD + +# Exit immidiately in case of any errors or when we have interactive terminal +if [[ $? != 0 ]] || test -t 0; then exit $?; fi +log diff --git a/docker/blocks/smtp/fig b/docker/blocks/smtp/fig new file mode 100644 index 00000000000..c2d37e01c21 --- /dev/null +++ b/docker/blocks/smtp/fig @@ -0,0 +1,4 @@ +snmpd: + build: blocks/snmpd + ports: + - "161:161" diff --git a/main.go b/main.go index 5d5bddb7273..97f9262eb52 100644 --- a/main.go +++ b/main.go @@ -16,6 +16,7 @@ import ( "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/search" "github.com/grafana/grafana/pkg/services/eventpublisher" + "github.com/grafana/grafana/pkg/services/mailer" "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/social" @@ -56,6 +57,7 @@ func main() { social.NewOAuthService() eventpublisher.Init() plugins.Init() + mailer.Init() if setting.ReportingEnabled { go metrics.StartUsageReportLoop() diff --git a/pkg/api/apikey.go b/pkg/api/apikey.go index b2097104aba..9c2210f11d2 100644 --- a/pkg/api/apikey.go +++ b/pkg/api/apikey.go @@ -24,6 +24,13 @@ func GetApiKeys(c *middleware.Context) Response { } } + bus.Dispatch(&m.SendEmailCommand{ + To: []string{"torkel@raintank.io"}, + From: "grafana@test.com", + Subject: "Test from Grafana2", + Body: "Body! hej hoppas allt är bra", + }) + return Json(200, result) } diff --git a/pkg/models/emails.go b/pkg/models/emails.go new file mode 100644 index 00000000000..342d220d2f5 --- /dev/null +++ b/pkg/models/emails.go @@ -0,0 +1,24 @@ +package models + +type SendEmailCommand struct { + To []string + From string + Subject string + Body string + Type string + Massive bool + Info string +} + +// create mail content +func (m *SendEmailCommand) Content() string { + // set mail type + contentType := "text/plain; charset=UTF-8" + if m.Type == "html" { + contentType = "text/html; charset=UTF-8" + } + + // create mail content + content := "From: " + m.From + "\r\nSubject: " + m.Subject + "\r\nContent-Type: " + contentType + "\r\n\r\n" + m.Body + return content +} diff --git a/pkg/services/mailer/mailer.go b/pkg/services/mailer/mailer.go new file mode 100644 index 00000000000..27e4dc36d87 --- /dev/null +++ b/pkg/services/mailer/mailer.go @@ -0,0 +1,243 @@ +package mailer + +import ( + "crypto/tls" + "fmt" + "net" + "net/mail" + "net/smtp" + "os" + "strings" + + "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/log" + m "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/setting" +) + +var mailQueue chan *m.SendEmailCommand + +func Init() { + bus.AddHandler("email", handleEmailCommand) + + mailQueue = make(chan *m.SendEmailCommand, 10) + + setting.Smtp = setting.SmtpSettings{ + Host: "smtp.gmail.com:587", + User: "torkel.odegaard@gmail.com", + Password: "peslpwstnnloiksq", + FromAddress: "grafana@grafana.org", + } + + go processMailQueue() +} + +func processMailQueue() { + for { + select { + case msg := <-mailQueue: + num, err := buildAndSend(msg) + tos := strings.Join(msg.To, "; ") + info := "" + if err != nil { + if len(msg.Info) > 0 { + info = ", info: " + msg.Info + } + log.Error(4, fmt.Sprintf("Async sent email %d succeed, not send emails: %s%s err: %s", num, tos, info, err)) + } else { + log.Trace(fmt.Sprintf("Async sent email %d succeed, sent emails: %s%s", num, tos, info)) + } + } + } +} + +func encodeRFC2047(text string) string { + // use mail's rfc2047 to encode any string + addr := mail.Address{Address: text} + return strings.Trim(addr.String(), " <>") +} + +func handleEmailCommand(cmd *m.SendEmailCommand) error { + log.Info("Sending on queue") + mailQueue <- cmd + return nil +} + +func sendToSmtpServer(recipients []string, msgContent []byte) error { + host, port, err := net.SplitHostPort(setting.Smtp.Host) + if err != nil { + return err + } + + tlsconfig := &tls.Config{ + InsecureSkipVerify: setting.Smtp.SkipVerify, + ServerName: host, + } + + if setting.Smtp.CertFile != "" { + cert, err := tls.LoadX509KeyPair(setting.Smtp.CertFile, setting.Smtp.KeyFile) + if err != nil { + return err + } + tlsconfig.Certificates = []tls.Certificate{cert} + } + + conn, err := net.Dial("tcp", net.JoinHostPort(host, port)) + if err != nil { + return err + } + defer conn.Close() + + isSecureConn := false + // Start TLS directly if the port ends with 465 (SMTPS protocol) + if strings.HasSuffix(port, "465") { + conn = tls.Client(conn, tlsconfig) + isSecureConn = true + } + + client, err := smtp.NewClient(conn, host) + if err != nil { + return err + } + + hostname, err := os.Hostname() + if err != nil { + return err + } + + if err = client.Hello(hostname); err != nil { + return err + } + + // If not using SMTPS, alway use STARTTLS if available + hasStartTLS, _ := client.Extension("STARTTLS") + if !isSecureConn && hasStartTLS { + if err = client.StartTLS(tlsconfig); err != nil { + return err + } + } + + canAuth, options := client.Extension("AUTH") + + if canAuth && len(setting.Smtp.User) > 0 { + var auth smtp.Auth + + if strings.Contains(options, "CRAM-MD5") { + auth = smtp.CRAMMD5Auth(setting.Smtp.User, setting.Smtp.Password) + } else if strings.Contains(options, "PLAIN") { + auth = smtp.PlainAuth("", setting.Smtp.User, setting.Smtp.Password, host) + } + + if auth != nil { + if err = client.Auth(auth); err != nil { + return err + } + } + } + + if fromAddress, err := mail.ParseAddress(setting.Smtp.FromAddress); err != nil { + return err + } else { + if err = client.Mail(fromAddress.Address); err != nil { + return err + } + } + + for _, rec := range recipients { + if err = client.Rcpt(rec); err != nil { + return err + } + } + + w, err := client.Data() + if err != nil { + return err + } + if _, err = w.Write([]byte(msgContent)); err != nil { + return err + } + + if err = w.Close(); err != nil { + return err + } + + return client.Quit() + // smtpServer := "smtp.gmail.com" + // auth := smtp.PlainAuth( + // "", + // "torkel.odegaard@gmail.com", + // "peslpwstnnloiksq", + // smtpServer, + // ) + // + // from := mail.Address{Name: "test", Address: "torkel@test.com"} + // to := mail.Address{Name: "Torkel Ödegaard", Address: "torkel@raintank.io"} + // title := "Message from Grafana" + // + // body := "Testing email sending" + // + // header := make(map[string]string) + // header["From"] = from.String() + // header["To"] = to.String() + // header["Subject"] = encodeRFC2047(title) + // header["MIME-Version"] = "1.0" + // header["Content-Type"] = "text/plain; charset=\"utf-8\"" + // header["Content-Transfer-Encoding"] = "base64" + // + // message := "" + // for k, v := range header { + // message += fmt.Sprintf("%s: %s\r\n", k, v) + // } + // message += "\r\n" + base64.StdEncoding.EncodeToString([]byte(body)) + // + // // Connect to the server, authenticate, set the sender and recipient, + // // and send the email all in one step. + // err := smtp.SendMail( + // smtpServer+":587", + // auth, + // from.Address, + // []string{to.Address}, + // []byte(message), + // ) + // if err != nil { + // log.Info("Failed to send email: %v", err) + // } + // kkkk +} + +func buildAndSend(msg *m.SendEmailCommand) (int, error) { + log.Trace("Sending mails to: %s", strings.Join(msg.To, "; ")) + + // get message body + content := msg.Content() + + if len(msg.To) == 0 { + return 0, fmt.Errorf("empty receive emails") + } else if len(msg.Body) == 0 { + return 0, fmt.Errorf("empty email body") + } + + if msg.Massive { + // send mail to multiple emails one by one + num := 0 + for _, to := range msg.To { + body := []byte("To: " + to + "\r\n" + content) + err := sendToSmtpServer([]string{to}, body) + if err != nil { + return num, err + } + num++ + } + return num, nil + } else { + body := []byte("To: " + strings.Join(msg.To, ";") + "\r\n" + content) + + // send to multiple emails in one message + err := sendToSmtpServer(msg.To, body) + if err != nil { + return 0, err + } else { + return 1, nil + } + } +} diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index 6768f9aabd9..2a8ae91941c 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -114,6 +114,9 @@ var ( ReportingEnabled bool GoogleAnalyticsId string + + // SMTP email settings + Smtp SmtpSettings ) type CommandLineArgs struct { diff --git a/pkg/setting/setting_smtp.go b/pkg/setting/setting_smtp.go new file mode 100644 index 00000000000..e392c164ee3 --- /dev/null +++ b/pkg/setting/setting_smtp.go @@ -0,0 +1,11 @@ +package setting + +type SmtpSettings struct { + Host string + User string + Password string + CertFile string + KeyFile string + FromAddress string + SkipVerify bool +}