package mail import ( "crypto/tls" "fmt" "math/rand" "net/mail" "strconv" "strings" "time" "github.com/pkg/errors" gomail "gopkg.in/mail.v2" ) var ( ErrUnexpectedEmailAddressFormat = errors.New("unexpected email address format") ) type SendFunc func(*SendOption) type SendOption struct { Charset string AddressHeaders []AddressHeader Headers []Header Body Body AlternativeBodies []Body } type AddressHeader struct { Field string Address string Name string } type Header struct { Field string Values []string } type Body struct { Type string Content string PartSetting gomail.PartSetting } func WithCharset(charset string) func(*SendOption) { return func(opt *SendOption) { opt.Charset = charset } } func WithSender(address string, name string) func(*SendOption) { return WithAddressHeader("From", address, name) } func WithSubject(subject string) func(*SendOption) { return WithHeader("Subject", subject) } func WithAddressHeader(field, address, name string) func(*SendOption) { return func(opt *SendOption) { opt.AddressHeaders = append(opt.AddressHeaders, AddressHeader{field, address, name}) } } func WithHeader(field string, values ...string) func(*SendOption) { return func(opt *SendOption) { opt.Headers = append(opt.Headers, Header{field, values}) } } func WithRecipients(addresses ...string) func(*SendOption) { return WithHeader("To", addresses...) } func WithCopies(addresses ...string) func(*SendOption) { return WithHeader("Cc", addresses...) } func WithInvisibleCopies(addresses ...string) func(*SendOption) { return WithHeader("Cci", addresses...) } func WithBody(contentType string, content string, setting gomail.PartSetting) func(*SendOption) { return func(opt *SendOption) { if setting == nil { setting = gomail.SetPartEncoding(gomail.Unencoded) } opt.Body = Body{contentType, content, setting} } } func WithAlternativeBody(contentType string, content string, setting gomail.PartSetting) func(*SendOption) { return func(opt *SendOption) { if setting == nil { setting = gomail.SetPartEncoding(gomail.Unencoded) } opt.AlternativeBodies = append(opt.AlternativeBodies, Body{contentType, content, setting}) } } func (m *Mailer) Send(funcs ...SendFunc) error { opt := &SendOption{ Charset: "UTF-8", Body: Body{ Type: "text/plain", Content: "", PartSetting: gomail.SetPartEncoding(gomail.Unencoded), }, AddressHeaders: make([]AddressHeader, 0), Headers: make([]Header, 0), AlternativeBodies: make([]Body, 0), } for _, f := range funcs { f(opt) } conn, err := m.openConnection() if err != nil { return errors.Wrap(err, "could not open connection") } defer conn.Close() message := gomail.NewMessage(gomail.SetCharset(opt.Charset)) for _, h := range opt.AddressHeaders { message.SetAddressHeader(h.Field, h.Address, h.Name) } for _, h := range opt.Headers { message.SetHeader(h.Field, h.Values...) } froms := message.GetHeader("From") var sendDomain string if len(froms) > 0 { sendDomain, err = extractEmailDomain(froms[0]) if err != nil { return err } } messageID := generateMessageID(sendDomain) message.SetHeader("Message-Id", messageID) message.SetBody(opt.Body.Type, opt.Body.Content, opt.Body.PartSetting) for _, b := range opt.AlternativeBodies { message.AddAlternative(b.Type, b.Content, b.PartSetting) } if err := gomail.Send(conn, message); err != nil { return errors.Wrap(err, "could not send message") } return nil } func (m *Mailer) openConnection() (gomail.SendCloser, error) { dialer := gomail.NewDialer( m.opt.Host, m.opt.Port, m.opt.User, m.opt.Password, ) if m.opt.InsecureSkipVerify { dialer.TLSConfig = &tls.Config{ InsecureSkipVerify: true, } } conn, err := dialer.Dial() if err != nil { return nil, errors.Wrap(err, "could not dial smtp server") } return conn, nil } func extractEmailDomain(email string) (string, error) { address, err := mail.ParseAddress(email) if err != nil { return "", errors.Wrapf(err, "could not parse email address '%s'", email) } addressParts := strings.SplitN(address.Address, "@", 2) if len(addressParts) != 2 { // nolint: gomnd return "", errors.WithStack(ErrUnexpectedEmailAddressFormat) } domain := addressParts[1] return domain, nil } func generateMessageID(domain string) string { // Based on https://www.jwz.org/doc/mid.html timestamp := strconv.FormatInt(time.Now().UnixNano(), 36) random := strconv.FormatInt(rand.Int63(), 36) return fmt.Sprintf("<%s.%s@%s>", timestamp, random, domain) }