From 948bc2c80642412f68198589ab37641feeea6eb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vic=20Sh=C3=B3stak?= Date: Mon, 30 Aug 2021 20:34:47 +0300 Subject: [PATCH] Initial commit --- .editorconfig | 13 +++++++ .gitignore | 3 +- .prettierrc | 6 +++ LICENSE | 2 +- README.md | 5 ++- go.mod | 3 ++ parse_email_test.go | 57 +++++++++++++++++++++++++++ parse_template.go | 19 +++++++++ sender.go | 88 ++++++++++++++++++++++++++++++++++++++++++ templates/default.html | 71 ++++++++++++++++++++++++++++++++++ 10 files changed, 263 insertions(+), 4 deletions(-) create mode 100644 .editorconfig create mode 100755 .prettierrc create mode 100644 go.mod create mode 100644 parse_email_test.go create mode 100644 parse_template.go create mode 100644 sender.go create mode 100644 templates/default.html diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..98af5d3 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,13 @@ +root = true + +[*] +indent_style = space +indent_size = 2 +charset = utf-8 +max_line_length = 100 +trim_trailing_whitespace = true +insert_final_newline = true + +[{Dockerfile,*.yml,*.yaml,*.json}] +indent_style = tab +indent_size = 2 \ No newline at end of file diff --git a/.gitignore b/.gitignore index 66fd13c..337d116 100644 --- a/.gitignore +++ b/.gitignore @@ -6,10 +6,11 @@ *.dylib # Test binary, built with `go test -c` +tmp/ *.test # Output of the go coverage tool, specifically when used with LiteIDE *.out # Dependency directories (remove the comment below to include it) -# vendor/ +vendor/ diff --git a/.prettierrc b/.prettierrc new file mode 100755 index 0000000..0965e7e --- /dev/null +++ b/.prettierrc @@ -0,0 +1,6 @@ +{ + "semi": false, + "singleQuote": true, + "trailingComma": "all", + "printWidth": 100 +} diff --git a/LICENSE b/LICENSE index 261eeb9..503920a 100644 --- a/LICENSE +++ b/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright [yyyy] [name of copyright owner] + Copyright 2021 Vic Shóstak (https://shostak.dev) Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/README.md b/README.md index 4a51d08..20d7cb3 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,3 @@ -# go-email-sender -Simple (but useful) email sender for Go. Support HTML templates and attachments. +# 📮 Go Email Sender + +Simple (but useful) email sender written with pure Go `v1.17`. Support HTML templates and attachments. diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..63a3d0d --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/koddr/go-email-sender + +go 1.17 diff --git a/parse_email_test.go b/parse_email_test.go new file mode 100644 index 0000000..b28c625 --- /dev/null +++ b/parse_email_test.go @@ -0,0 +1,57 @@ +package sender + +import ( + "os" + "testing" +) + +func TestParseTemplate(t *testing.T) { + // Create folder and file for running tests. + _ = os.Mkdir("tmp", 0700) + file, err := os.OpenFile("tmp/test.html", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0700) + if err != nil { + t.Errorf("Sender.parseTemplate() error = %v", err) + return + } + _, err = file.Write([]byte("

Hello, World!

")) + if err != nil { + t.Errorf("Sender.parseTemplate() error = %v", err) + return + } + defer file.Close() + + // Test cases. + type args struct { + file string + data interface{} + } + tests := []struct { + name string + args args + want string + wantErr bool + }{ + { + name: "successfully parsing the template file", + args: args{ + file: "tmp/test.html", + data: nil, + }, + want: "

Hello, World!

", + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := ParseTemplate(tt.args.file, tt.args.data) + if (err != nil) != tt.wantErr { + t.Errorf("Sender.parseTemplate() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("Sender.parseTemplate() = %v, want %v", got, tt.want) + } + os.RemoveAll("tmp") // remove temp folder + }) + } +} diff --git a/parse_template.go b/parse_template.go new file mode 100644 index 0000000..b5b6c37 --- /dev/null +++ b/parse_template.go @@ -0,0 +1,19 @@ +package sender + +import ( + "bytes" + "html/template" +) + +// ParseTemplate ... +func ParseTemplate(file string, data interface{}) (string, error) { + tmpl, errParseFiles := template.ParseFiles(file) + if errParseFiles != nil { + return "", errParseFiles + } + buffer := new(bytes.Buffer) + if errExecute := tmpl.Execute(buffer, data); errExecute != nil { + return "", errExecute + } + return buffer.String(), nil +} diff --git a/sender.go b/sender.go new file mode 100644 index 0000000..a9c8532 --- /dev/null +++ b/sender.go @@ -0,0 +1,88 @@ +package sender + +import ( + "bytes" + "fmt" + "mime/quotedprintable" + "net/smtp" + "strings" +) + +// Sender struct for describe auth data for sending emails. +type Sender struct { + Login string + Password string + SMTPServer string + SMTPServerPort int +} + +// NewEmailSender func for create new email sender. +// Includes login, password, SMTP server and port. +func NewEmailSender(login, password, server string, port int) *Sender { + return &Sender{login, password, server, port} +} + +// SendHTMLEmail func for send email with given HTML template and data. +// If template is empty string, it will send with default template. +func (s *Sender) SendHTMLEmail(template string, dest []string, subject string, data interface{}) error { + if template == "" { + template = "templates/default.html" + } + tmpl, errParseTemplate := ParseTemplate(template, data) + if errParseTemplate != nil { + return errParseTemplate + } + body := s.writeEmail(dest, "text/html", subject, tmpl) + s.sendEmail(dest, subject, body) + return nil +} + +// SendPlainEmail func for send plain text email with data. +func (s *Sender) SendPlainEmail(dest []string, subject, data string) error { + body := s.writeEmail(dest, "text/plain", subject, data) + s.sendEmail(dest, subject, body) + return nil +} + +// writeEmail method for prepare email header and body to send. +func (s *Sender) writeEmail(dest []string, contentType, subject, body string) string { + // Define variables. + var message string + var header map[string]string + var encodedMessage bytes.Buffer + + // Create header. + header["From"] = s.Login + header["To"] = strings.Join(dest, ",") + header["Subject"] = subject + header["MIME-Version"] = "1.0" + header["Content-Type"] = fmt.Sprintf("%s; charset=\"utf-8\"", contentType) + header["Content-Transfer-Encoding"] = "quoted-printable" + header["Content-Disposition"] = "inline" + + // Add header values to the message. + for key, value := range header { + message += fmt.Sprintf("%s:%s\r\n", key, value) + } + + // Create writer for make encoding the message. + result := quotedprintable.NewWriter(&encodedMessage) + result.Write([]byte(body)) + result.Close() + + // Return the encoded message string. + message += "\r\n" + encodedMessage.String() + return message +} + +// sendEmail method for send email to destination addresses with subject and body. +func (s *Sender) sendEmail(dest []string, subject, body string) error { + if err := smtp.SendMail( + fmt.Sprintf("%s:%d", s.SMTPServer, s.SMTPServerPort), + smtp.PlainAuth("", s.Login, s.Password, s.SMTPServer), + s.Login, dest, []byte(body), + ); err != nil { + return err + } + return nil +} diff --git a/templates/default.html b/templates/default.html new file mode 100644 index 0000000..320511d --- /dev/null +++ b/templates/default.html @@ -0,0 +1,71 @@ + + + + + + + ✨ The default email from example.com + + + + + + + + + + + + + +
✨ The default email
+

+ Hi,
+ This is the default email from + koddr/go-email-sender +

+

Please specify your HTML template in the SendHTMLEmail method.

+
+ +