From 425c585e479866946fca87006333fe3cfcaf24b7 Mon Sep 17 00:00:00 2001 From: shopper <39241071+DaikiYamakawa@users.noreply.github.com> Date: Fri, 4 Sep 2020 15:45:29 +0900 Subject: [PATCH] Support for smtp LOGIN authentication (#1048) * finished to implement new mail client * delete email_test.go --- go.mod | 2 + go.sum | 4 ++ report/email.go | 104 +++++++++++++++++++------------------- report/email_test.go | 115 ------------------------------------------- 4 files changed, 59 insertions(+), 166 deletions(-) delete mode 100644 report/email_test.go diff --git a/go.mod b/go.mod index 09eea959..e54bfd4b 100644 --- a/go.mod +++ b/go.mod @@ -19,6 +19,8 @@ require ( github.com/boltdb/bolt v1.3.1 github.com/cenkalti/backoff v2.2.1+incompatible github.com/d4l3k/messagediff v1.2.2-0.20190829033028-7e0a312ae40b + github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 + github.com/emersion/go-smtp v0.13.0 github.com/google/subcommands v1.2.0 github.com/gosuri/uitable v0.0.4 github.com/hashicorp/go-uuid v1.0.2 diff --git a/go.sum b/go.sum index ed1e1707..fab8a7a5 100644 --- a/go.sum +++ b/go.sum @@ -187,6 +187,10 @@ github.com/elazarl/goproxy v0.0.0-20190711103511-473e67f1d7d2/go.mod h1:/Zj4wYkg github.com/elazarl/goproxy/ext v0.0.0-20190421051319-9d40249d3c2f/go.mod h1:gNh8nYJoAm43RfaxurUnxr+N1PwuFV3ZMl/efxlIlY8= github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2 h1:dWB6v3RcOy03t/bUadywsbyrQwCqZeNIEX6M1OtSZOM= github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2/go.mod h1:gNh8nYJoAm43RfaxurUnxr+N1PwuFV3ZMl/efxlIlY8= +github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 h1:OJyUGMJTzHTd1XQp98QTaHernxMYzRaOasRir9hUlFQ= +github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ= +github.com/emersion/go-smtp v0.13.0 h1:aC3Kc21TdfvXnuJXCQXuhnDXUldhc12qME/S7Y3Y94g= +github.com/emersion/go-smtp v0.13.0/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ= github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= github.com/emirpasic/gods v1.9.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o= diff --git a/report/email.go b/report/email.go index b3b161bf..9ee6b7a4 100644 --- a/report/email.go +++ b/report/email.go @@ -5,10 +5,11 @@ import ( "fmt" "net" "net/mail" - "net/smtp" "strings" "time" + sasl "github.com/emersion/go-sasl" + smtp "github.com/emersion/go-smtp" "github.com/future-architect/vuls/config" "github.com/future-architect/vuls/models" "golang.org/x/xerrors" @@ -21,7 +22,6 @@ func (w EMailWriter) Write(rs ...models.ScanResult) (err error) { conf := config.Conf var message string sender := NewEMailSender() - m := map[string]int{} for _, r := range rs { if conf.FormatOneEMail { @@ -85,37 +85,50 @@ type EMailSender interface { type emailSender struct { conf config.SMTPConf - send func(string, smtp.Auth, string, []string, []byte) error } -func smtps(emailConf config.SMTPConf, message string) (err error) { - auth := smtp.PlainAuth("", - emailConf.User, - emailConf.Password, - emailConf.SMTPAddr, - ) - +func (e *emailSender) sendMail(smtpServerAddr, message string) (err error) { + var c *smtp.Client + var auth sasl.Client + emailConf := e.conf //TLS Config tlsConfig := &tls.Config{ ServerName: emailConf.SMTPAddr, } - - smtpServer := net.JoinHostPort(emailConf.SMTPAddr, emailConf.SMTPPort) - //New TLS connection - con, err := tls.Dial("tcp", smtpServer, tlsConfig) - if err != nil { - return xerrors.Errorf("Failed to create TLS connection: %w", err) + switch emailConf.SMTPPort { + case "465": + //New TLS connection + c, err = smtp.DialTLS(smtpServerAddr, tlsConfig) + if err != nil { + return xerrors.Errorf("Failed to create TLS connection to SMTP server: %w", err) + } + default: + c, err = smtp.Dial(smtpServerAddr) + if err != nil { + return xerrors.Errorf("Failed to create connection to SMTP server: %w", err) + } } - defer con.Close() + defer c.Close() - c, err := smtp.NewClient(con, emailConf.SMTPAddr) - if err != nil { - return xerrors.Errorf("Failed to create new client: %w", err) + if err = c.Hello("localhost"); err != nil { + return xerrors.Errorf("Failed to send Hello command: %w", err) } + + if ok, _ := c.Extension("STARTTLS"); ok { + if err := c.StartTLS(tlsConfig); err != nil { + return xerrors.Errorf("Failed to STARTTLS: %w", err) + } + } + + if ok, param := c.Extension("AUTH"); ok { + authList := strings.Split(param, " ") + auth = e.newSaslClient(authList) + } + if err = c.Auth(auth); err != nil { return xerrors.Errorf("Failed to authenticate: %w", err) } - if err = c.Mail(emailConf.From); err != nil { + if err = c.Mail(emailConf.From, nil); err != nil { return xerrors.Errorf("Failed to send Mail command: %w", err) } for _, to := range emailConf.To { @@ -169,38 +182,13 @@ func (e *emailSender) Send(subject, body string) (err error) { smtpServer := net.JoinHostPort(emailConf.SMTPAddr, emailConf.SMTPPort) if emailConf.User != "" && emailConf.Password != "" { - switch emailConf.SMTPPort { - case "465": - err := smtps(emailConf, message) - if err != nil { - return xerrors.Errorf("Failed to send emails: %w", err) - } - default: - err = e.send( - smtpServer, - smtp.PlainAuth( - "", - emailConf.User, - emailConf.Password, - emailConf.SMTPAddr, - ), - emailConf.From, - mailAddresses, - []byte(message), - ) - if err != nil { - return xerrors.Errorf("Failed to send emails: %w", err) - } + err = e.sendMail(smtpServer, message) + if err != nil { + return xerrors.Errorf("Failed to send emails: %w", err) } return nil } - err = e.send( - smtpServer, - nil, - emailConf.From, - mailAddresses, - []byte(message), - ) + err = e.sendMail(smtpServer, message) if err != nil { return xerrors.Errorf("Failed to send emails: %w", err) } @@ -209,5 +197,19 @@ func (e *emailSender) Send(subject, body string) (err error) { // NewEMailSender creates emailSender func NewEMailSender() EMailSender { - return &emailSender{config.Conf.EMail, smtp.SendMail} + return &emailSender{config.Conf.EMail} +} + +func (e *emailSender) newSaslClient(authList []string) sasl.Client { + for _, v := range authList { + switch v { + case "PLAIN": + auth := sasl.NewPlainClient("", e.conf.User, e.conf.Password) + return auth + case "LOGIN": + auth := sasl.NewLoginClient(e.conf.User, e.conf.Password) + return auth + } + } + return nil } diff --git a/report/email_test.go b/report/email_test.go deleted file mode 100644 index 6ad25468..00000000 --- a/report/email_test.go +++ /dev/null @@ -1,115 +0,0 @@ -package report - -import ( - "net/smtp" - "reflect" - "strings" - "testing" - - "github.com/future-architect/vuls/config" -) - -type emailRecorder struct { - addr string - auth smtp.Auth - from string - to []string - body string -} - -type mailTest struct { - in config.SMTPConf - out emailRecorder -} - -var mailTests = []mailTest{ - { - config.SMTPConf{ - SMTPAddr: "127.0.0.1", - SMTPPort: "25", - - From: "from@address.com", - To: []string{"to@address.com"}, - Cc: []string{"cc@address.com"}, - }, - emailRecorder{ - addr: "127.0.0.1:25", - auth: smtp.PlainAuth("", "", "", "127.0.0.1"), - from: "from@address.com", - to: []string{"to@address.com", "cc@address.com"}, - body: "body", - }, - }, - { - config.SMTPConf{ - SMTPAddr: "127.0.0.1", - SMTPPort: "25", - - User: "vuls", - Password: "password", - - From: "from@address.com", - To: []string{"to1@address.com", "to2@address.com"}, - Cc: []string{"cc1@address.com", "cc2@address.com"}, - }, - emailRecorder{ - addr: "127.0.0.1:25", - auth: smtp.PlainAuth( - "", - "vuls", - "password", - "127.0.0.1", - ), - from: "from@address.com", - to: []string{"to1@address.com", "to2@address.com", - "cc1@address.com", "cc2@address.com"}, - body: "body", - }, - }, -} - -func TestSend(t *testing.T) { - for i, test := range mailTests { - f, r := mockSend(nil) - sender := &emailSender{conf: test.in, send: f} - - subject := "subject" - body := "body" - if err := sender.Send(subject, body); err != nil { - t.Errorf("unexpected error: %s", err) - } - - if r.addr != test.out.addr { - t.Errorf("#%d: wrong 'addr' field.\r\nexpected: %s\n got: %s", i, test.out.addr, r.addr) - } - - if !reflect.DeepEqual(r.auth, test.out.auth) && r.auth != nil { - t.Errorf("#%d: wrong 'auth' field.\r\nexpected: %v\n got: %v", i, test.out.auth, r.auth) - } - - if r.from != test.out.from { - t.Errorf("#%d: wrong 'from' field.\r\nexpected: %v\n got: %v", i, test.out.from, r.from) - } - - if !reflect.DeepEqual(r.to, test.out.to) { - t.Errorf("#%d: wrong 'to' field.\r\nexpected: %v\n got: %v", i, test.out.to, r.to) - } - - if r.body != test.out.body { - t.Errorf("#%d: wrong 'body' field.\r\nexpected: %v\n got: %v", i, test.out.body, r.body) - } - - } - -} - -func mockSend(errToReturn error) (func(string, smtp.Auth, string, []string, []byte) error, *emailRecorder) { - r := new(emailRecorder) - return func(addr string, a smtp.Auth, from string, to []string, msg []byte) error { - // Split into header and body - messages := strings.Split(string(msg), "\r\n\r\n") - body := messages[1] - *r = emailRecorder{addr, a, from, to, body} - return errToReturn - }, r -}