diff --git a/go.mod b/go.mod index c38a249..861b9a0 100644 --- a/go.mod +++ b/go.mod @@ -69,6 +69,7 @@ require ( github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 github.com/improbable-eng/grpc-web v0.15.0 github.com/joho/godotenv v1.5.1 + github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible github.com/lib/pq v1.10.9 github.com/mssola/useragent v1.0.0 github.com/nyaruka/phonenumbers v1.4.0 diff --git a/go.sum b/go.sum index 3ac9bc5..56f41f6 100644 --- a/go.sum +++ b/go.sum @@ -183,6 +183,8 @@ github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible h1:jdpOPRN1zP63Td1hDQbZW73xKmzDvZHzVdNYxhnTMDA= +github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible/go.mod h1:1c7szIrayyPPB/987hsnvNzLushdWf4o/79s3P08L8A= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= diff --git a/internal/util/util.go b/internal/util/util.go index 8a61393..1e7999d 100644 --- a/internal/util/util.go +++ b/internal/util/util.go @@ -5,6 +5,7 @@ import ( "fmt" "math/big" "net/mail" + "net/url" "strconv" "strings" "unicode/utf8" @@ -146,11 +147,17 @@ func TruncateString(str string, limit int) (string, bool) { return str, false } -// TruncateStringWithDescription tries to truncate the string and append "... (view details in Bytebase)" if truncated. +// TruncateStringWithDescription tries to truncate the string and append "..." if truncated. func TruncateStringWithDescription(str string) string { const limit = 450 if truncatedStr, truncated := TruncateString(str, limit); truncated { - return fmt.Sprintf("%s... (view details in Bytebase)", truncatedStr) + return fmt.Sprintf("%s...", truncatedStr) } return str } + +// ValidateURI validates the URI. +func ValidateURI(uri string) bool { + u, err := url.Parse(uri) + return err == nil && u.Scheme != "" && u.Host != "" +} diff --git a/plugin/mail/login_auth.go b/plugin/mail/login_auth.go new file mode 100644 index 0000000..23a08e6 --- /dev/null +++ b/plugin/mail/login_auth.go @@ -0,0 +1,34 @@ +package mail + +import ( + "errors" + "net/smtp" +) + +type loginAuth struct { + username string + password string +} + +// LoginAuth returns an Auth that implements the LOGIN authentication. +func LoginAuth(username, password string) smtp.Auth { + return &loginAuth{username, password} +} + +func (*loginAuth) Start(*smtp.ServerInfo) (string, []byte, error) { + return "LOGIN", []byte{}, nil +} + +func (la *loginAuth) Next(fromServer []byte, more bool) ([]byte, error) { + if more { + switch string(fromServer) { + case "Username:": + return []byte(la.username), nil + case "Password:": + return []byte(la.password), nil + default: + return nil, errors.New("Unknown fromServer") + } + } + return nil, nil +} diff --git a/plugin/mail/mail.go b/plugin/mail/mail.go new file mode 100644 index 0000000..d3d55e4 --- /dev/null +++ b/plugin/mail/mail.go @@ -0,0 +1,230 @@ +// Package mail is a mail delivery plugin based on SMTP. +package mail + +// Usage: +// email := NewEmailMsg().SetFrom("yourselfhosted ").AddTo("Customer ").SetSubject("Test Email Subject").SetBody(` +// +// +// +// HTML Test +// +// +//

This is a mail delivery test.

+// +// +// `) +// fmt.Printf("email: %+v\n", email) +// client := NewSMTPClient("smtp.gmail.com", 587) +// client.SetAuthType(SMTPAuthTypePlain) +// client.SetAuthCredentials("from@yourselfhosted.com", "nqxxxxxxxxxxxxxx") +// client.SetEncryptionType(SMTPEncryptionTypeSTARTTLS) +// if err := client.SendMail(email); err != nil { +// t.Fatalf("SendMail failed: %v", err) +// } + +import ( + "crypto/tls" + "fmt" + "io" + "net/mail" + "net/smtp" + + "github.com/jordan-wright/email" + "github.com/pkg/errors" +) + +// Email is the email to be sent. +type Email struct { + err error + from string + subject string + + e *email.Email +} + +// NewEmailMsg returns a new email message. +func NewEmailMsg() *Email { + e := &Email{ + e: email.NewEmail(), + } + return e +} + +// SetFrom sets the from address of the SMTP client. +// Only accept the valid RFC 5322 address, e.g. "yourselfhosted ". +func (e *Email) SetFrom(from string) *Email { + if e.err != nil { + return e + } + if e.from != "" { + e.err = errors.New("From address already set") + return e + } + + parsedAddr, err := mail.ParseAddress(from) + if err != nil { + e.err = errors.Wrapf(err, "Invalid from address: %s", from) + } + e.from = parsedAddr.Address + e.e.From = parsedAddr.String() + return e +} + +// AddTo adds the to address of the SMTP client. +// Only accept the valid RFC 5322 address, e.g. "yourselfhosted ". +func (e *Email) AddTo(to ...string) *Email { + if e.err != nil { + return e + } + var buf []*mail.Address + for _, toAddress := range to { + parsedAddr, err := mail.ParseAddress(toAddress) + if err != nil { + e.err = errors.Wrapf(err, "Invalid to address: %s", toAddress) + return e + } + buf = append(buf, parsedAddr) + } + for _, addr := range buf { + e.e.To = append(e.e.To, addr.String()) + } + return e +} + +// SetSubject sets the subject of the SMTP client. +func (e *Email) SetSubject(subject string) *Email { + if e.err != nil { + return e + } + if e.subject != "" { + e.err = errors.New("Subject already set") + return e + } + e.subject = subject + e.e.Subject = subject + return e +} + +// SetBody sets the body of the SMTP client. It must be html formatted. +func (e *Email) SetBody(body string) *Email { + e.e.HTML = []byte(body) + return e +} + +// The ContentType is the type of the content. +// https://cloud.google.com/appengine/docs/legacy/standard/php/mail/mail-with-headers-attachments. +type ContentType string + +const ( + // ContentTypeImagePNG is the content type of the file with png extension. + ContentTypeImagePNG ContentType = "image/png" +) + +// Attach attaches the file to the email, and returns the filename of the attachment. +// Caller can use filename as content id to reference the attachment in the email body. +func (e *Email) Attach(reader io.Reader, filename string, contentType ContentType) (string, error) { + attachment, err := e.e.Attach(reader, filename, string(contentType)) + if err != nil { + return "", err + } + return attachment.Filename, nil +} + +// SMTPAuthType is the type of SMTP authentication. +type SMTPAuthType uint + +const ( + // SMTPAuthTypeNone is the NONE auth type of SMTP. + SMTPAuthTypeNone = iota + // SMTPAuthTypePlain is the PLAIN auth type of SMTP. + SMTPAuthTypePlain + // SMTPAuthTypeLogin is the LOGIN auth type of SMTP. + SMTPAuthTypeLogin + // SMTPAuthTypeCRAMMD5 is the CRAM-MD5 auth type of SMTP. + SMTPAuthTypeCRAMMD5 +) + +// SMTPEncryptionType is the type of SMTP encryption. +type SMTPEncryptionType uint + +const ( + // SMTPEncryptionTypeNone is the NONE encrypt type of SMTP. + SMTPEncryptionTypeNone = iota + // SMTPEncryptionTypeSSLTLS is the SSL/TLS encrypt type of SMTP. + SMTPEncryptionTypeSSLTLS + // SMTPEncryptionTypeSTARTTLS is the STARTTLS encrypt type of SMTP. + SMTPEncryptionTypeSTARTTLS +) + +// SMTPClient is the client of SMTP. +type SMTPClient struct { + host string + port int + authType SMTPAuthType + username string + password string + encryptionType SMTPEncryptionType +} + +// NewSMTPClient returns a new SMTP client. +func NewSMTPClient(host string, port int) *SMTPClient { + return &SMTPClient{ + host: host, + port: port, + authType: SMTPAuthTypeNone, + username: "", + password: "", + encryptionType: SMTPEncryptionTypeNone, + } +} + +// SendMail sends the email. +func (c *SMTPClient) SendMail(e *Email) error { + if e.err != nil { + return e.err + } + + switch c.encryptionType { + case SMTPEncryptionTypeNone: + return e.e.Send(fmt.Sprintf("%s:%d", c.host, c.port), c.getAuth()) + case SMTPEncryptionTypeSSLTLS: + return e.e.SendWithTLS(fmt.Sprintf("%s:%d", c.host, c.port), c.getAuth(), &tls.Config{ServerName: c.host}) + case SMTPEncryptionTypeSTARTTLS: + return e.e.SendWithStartTLS(fmt.Sprintf("%s:%d", c.host, c.port), c.getAuth(), &tls.Config{InsecureSkipVerify: true}) + default: + return errors.Errorf("Unknown SMTP encryption type: %d", c.encryptionType) + } +} + +// SetAuthType sets the auth type of the SMTP client. +func (c *SMTPClient) SetAuthType(authType SMTPAuthType) *SMTPClient { + c.authType = authType + return c +} + +// SetAuthCredentials sets the auth credentials of the SMTP client. +func (c *SMTPClient) SetAuthCredentials(username, password string) *SMTPClient { + c.username = username + c.password = password + return c +} + +func (c *SMTPClient) getAuth() smtp.Auth { + switch c.authType { + case SMTPAuthTypeNone: + return nil + case SMTPAuthTypePlain: + return smtp.PlainAuth("", c.username, c.password, c.host) + case SMTPAuthTypeLogin: + return LoginAuth(c.username, c.password) + case SMTPAuthTypeCRAMMD5: + return smtp.CRAMMD5Auth(c.username, c.password) + default: + return nil + } +} + +// SetEncryptionType sets the encryption type of the SMTP client. +func (c *SMTPClient) SetEncryptionType(encryptionType SMTPEncryptionType) { + c.encryptionType = encryptionType +}