diff --git a/.gitignore b/.gitignore index 66fd13c..485dee6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,15 +1 @@ -# Binaries for programs and plugins -*.exe -*.exe~ -*.dll -*.so -*.dylib - -# Test binary, built with `go test -c` -*.test - -# Output of the go coverage tool, specifically when used with LiteIDE -*.out - -# Dependency directories (remove the comment below to include it) -# vendor/ +.idea diff --git a/README.md b/README.md index 2644379..3f113ca 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,27 @@ -# ngr-emailer -Tiny lib for email (with attachments) sending +# emailer +Tiny lib for email (with attachments) sending (smtp) + +# import +```import "github.com/NGRsoftlab/ngr-emailer"``` + +# example +``` +s := NewSender( + "user@mail.com", + "password", + "user@mail.com", + fmt.Sprintf("%v:%v", "test.com", "587"), + ) + + err := s.NewMessage("topic", []string{"user@mail.com"}, "test", []AttachData{{ + fileName: "test.txt", + fileData: []byte("test"), + }}) + if err != nil { + log.Fatal(err) + } + err = s.Send() + if err != nil { + log.Fatal(err) + } +``` diff --git a/emailer.go b/emailer.go new file mode 100644 index 0000000..17d42b2 --- /dev/null +++ b/emailer.go @@ -0,0 +1,152 @@ +// Copyright 2020 NGR Softlab +// +package emailer + +import ( + "bytes" + "encoding/base64" + "errors" + "fmt" + "github.com/NGRsoftlab/ngr-logging" + "mime/multipart" + "net/smtp" + "strings" + "time" +) + +// Struct for auth. +type loginAuth struct { + username, password string +} + +// Struct for attachments. +type AttachData struct { + FileName string + FileData []byte +} + +// Auth with login and password. +func LoginAuth(username, password string) smtp.Auth { + return &loginAuth{username, password} +} + +// Login auth start. +func (a *loginAuth) Start(server *smtp.ServerInfo) (string, []byte, error) { + return "LOGIN", []byte{}, nil +} + +// Next loginAuth. +func (a *loginAuth) Next(fromServer []byte, more bool) ([]byte, error) { + if more { + switch string(fromServer) { + case "Username:": + return []byte(a.username), nil + case "Password:": + return []byte(a.password), nil + default: + return nil, errors.New("Unknown fromServer") + } + } + return nil, nil +} + +///////////////////////////////////////////// + +// Struct - sender for sending smtp packs. +type Sender struct { + Login string // user login + Email string // user email address + Password string // user password + ServerSMTP string // smtp server string + client *smtp.Client // smtp client pointer + message []byte // message text + to []string // receivers of email +} + +// Send smtp pack (mail). +func (s *Sender) Send() error { + err := smtp.SendMail(s.ServerSMTP, + LoginAuth(s.Login, s.Password), + s.Login, s.to, s.message) + + if err != nil { + logging.Logger.Error("SEND ERROR: ", err) + return err + } + return nil +} + +///////////////////////////////////////////// + +// Creating new *Sender obj. +func NewSender(login, password, email, server string) *Sender { + auth := Sender{ + Login: login, + Email: email, + Password: password, + ServerSMTP: server} + return &auth +} + +// Creating new email message. +func (s *Sender) NewMessage(subject string, to []string, body string, files []AttachData) error { + logging.Logger.Info("files: ", len(files)) + + attachments, err := attachFile(files) + if err != nil { + logging.Logger.Error(err) + return err + } + + withAttachments := len(attachments) > 0 + var headers = make(map[string]string) + headers["From"] = s.Email + headers["To"] = strings.Join(to, ";") + headers["Subject"] = subject + headers["MIME-Version"] = "1.0" + headers["Date"] = time.Now().Format(time.RFC1123Z) + var buf = bytes.NewBuffer(nil) + writer := multipart.NewWriter(buf) + boundary := writer.Boundary() + + for k, v := range headers { + buf.WriteString(fmt.Sprintf("%s: %s\r\n", k, v)) + } + + if withAttachments { + buf.WriteString(fmt.Sprintf(`Content-Type: multipart/mixed; boundary="%s"`, boundary)) + buf.WriteString("\r\n\r\n") + buf.WriteString(fmt.Sprintf("--%s\r\n", boundary)) + } + buf.WriteString("Content-Type: text/plain; charset=utf-8\r\n") + buf.WriteString("MIME-Version: 1.0\r\n") + buf.WriteString("\r\n" + body) + if withAttachments { + for k, v := range attachments { + buf.WriteString(fmt.Sprintf("\r\n--%s\r\n", boundary)) + buf.WriteString("Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\r\n") + buf.WriteString("Content-Transfer-Encoding: base64\r\n") + buf.WriteString("MIME-Version: 1.0\r\n") + buf.WriteString(fmt.Sprintf(`Content-Disposition: attachment; filename="%s"`, k)) + buf.WriteString("\r\n\r\n") + + var b = make([]byte, base64.StdEncoding.EncodedLen(len(v))) + base64.StdEncoding.Encode(b, v) + buf.Write(b) + } + buf.WriteString("--") + } + s.to = to + s.message = buf.Bytes() + + return nil +} + +// Attaching file to mail. Returns attachments map: "filename": filedata. +func attachFile(files []AttachData) (map[string][]byte, error) { + var attachments = make(map[string][]byte) + for _, f := range files { + attachments[f.FileName] = f.FileData + } + return attachments, nil +} diff --git a/emailer_test.go b/emailer_test.go new file mode 100644 index 0000000..e79ae95 --- /dev/null +++ b/emailer_test.go @@ -0,0 +1,31 @@ +// Copyright 2020 NGR Softlab +// +package emailer + +import ( + "fmt" + "testing" +) + +///////////////////////////////////////////////// +// Put correct here before testing. +func TestSimpleMail(t *testing.T) { + s := NewSender( + "user@mail.com", + "password", + "user@mail.com", + fmt.Sprintf("%v:%v", "test.com", "587"), + ) + + err := s.NewMessage("topic", []string{"user@mail.com"}, "test", []AttachData{{ + FileName: "test.txt", + FileData: []byte("test"), + }}) + if err != nil { + t.Fatal(err) + } + err = s.Send() + if err != nil { + t.Fatal(err) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..df1b34d --- /dev/null +++ b/go.mod @@ -0,0 +1,8 @@ +module github.com/NGRsoftlab/ngr-emailer + +go 1.13 + +require ( + github.com/NGRsoftlab/ngr-logging v1.0.0 + github.com/sirupsen/logrus v1.8.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..2f420b2 --- /dev/null +++ b/go.sum @@ -0,0 +1,12 @@ +github.com/NGRsoftlab/ngr-logging v1.0.0 h1:Yp42kvw/bofZ6xXC5jPlPx1HNabZQY9cvzGnB0earJY= +github.com/NGRsoftlab/ngr-logging v1.0.0/go.mod h1:99kZ+XwSK7rKRitmhZvqOdYnPf9Qepywt3zjJJcJDME= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= +github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=