github.com/blend/go-sdk@v1.20220411.3/email/smtp_sender.go (about) 1 /* 2 3 Copyright (c) 2022 - Present. Blend Labs, Inc. All rights reserved 4 Use of this source code is governed by a MIT license that can be found in the LICENSE file. 5 6 */ 7 8 package email 9 10 import ( 11 "bufio" 12 "context" 13 "crypto/tls" 14 "fmt" 15 "net/smtp" 16 17 "github.com/blend/go-sdk/configutil" 18 "github.com/blend/go-sdk/env" 19 "github.com/blend/go-sdk/ex" 20 ) 21 22 var ( 23 _ Sender = (*SMTPSender)(nil) 24 ) 25 26 // SMTPSender is a sender for emails over smtp. 27 type SMTPSender struct { 28 LocalName string `json:"localname" yaml:"localname"` 29 Host string `json:"host" yaml:"host" env:"SMTP_HOST"` 30 Port string `json:"port" yaml:"port" env:"SMTP_PORT"` 31 PlainAuth SMTPPlainAuth `json:"plainAuth" yaml:"plainAuth"` 32 } 33 34 // Resolve implements configutil.ConfigResolver. 35 func (s *SMTPSender) Resolve(ctx context.Context) error { 36 return configutil.Resolve(ctx, 37 func(ictx context.Context) error { return env.GetVars(ictx).ReadInto(s) }, 38 s.PlainAuth.Resolve, 39 ) 40 } 41 42 // IsZero returns if the smtp sender is set or not. 43 func (s SMTPSender) IsZero() bool { 44 return s.Host == "" 45 } 46 47 // PortOrDefault returns a property or a default. 48 func (s SMTPSender) PortOrDefault() string { 49 if s.Port != "" { 50 return s.Port 51 } 52 return "465" 53 } 54 55 // LocalNameOrDefault returns a property or a default. 56 func (s SMTPSender) LocalNameOrDefault() string { 57 if s.LocalName != "" { 58 return s.LocalName 59 } 60 return s.Host 61 } 62 63 // Send sends an email via. smtp. 64 func (s SMTPSender) Send(ctx context.Context, message Message) error { 65 if s.Host == "" { 66 return ex.New("smtp host unset") 67 } 68 if err := message.Validate(); err != nil { 69 return err 70 } 71 72 tlsConfig := &tls.Config{ServerName: s.Host, InsecureSkipVerify: true} 73 conn, err := tls.Dial("tcp", fmt.Sprintf("%s:%s", s.Host, s.PortOrDefault()), tlsConfig) 74 if err != nil { 75 return ex.New(err) 76 } 77 78 client, err := smtp.NewClient(conn, s.Host) 79 if err != nil { 80 return ex.New(err) 81 } 82 defer client.Close() 83 84 if err := client.Hello(s.LocalNameOrDefault()); err != nil { 85 return ex.New(err) 86 } 87 if !s.PlainAuth.IsZero() { 88 if err := client.Auth(smtp.PlainAuth(s.PlainAuth.Identity, s.PlainAuth.Username, s.PlainAuth.Password, s.Host)); err != nil { 89 return ex.New(err) 90 } 91 } 92 if err := client.Mail(message.From); err != nil { 93 return ex.New(err) 94 } 95 96 for _, to := range message.To { 97 if err := client.Rcpt(to); err != nil { 98 return ex.New(err) 99 } 100 } 101 for _, cc := range message.CC { 102 if err := client.Rcpt(cc); err != nil { 103 return ex.New(err) 104 } 105 } 106 for _, bcc := range message.BCC { 107 if err := client.Rcpt(bcc); err != nil { 108 return ex.New(err) 109 } 110 } 111 112 w, err := client.Data() 113 if err != nil { 114 return ex.New(err) 115 } 116 117 // msg data 118 bufWriter := bufio.NewWriter(w) 119 if _, err := bufWriter.WriteString(fmt.Sprintf("From: %s\r\n", message.From)); err != nil { 120 return ex.New(err) 121 } 122 if err = bufWriter.Flush(); err != nil { 123 return ex.New(err) 124 } 125 for _, to := range message.To { 126 if _, err := bufWriter.WriteString(fmt.Sprintf("To: %s\r\n", to)); err != nil { 127 return ex.New(err) 128 } 129 } 130 if err = bufWriter.Flush(); err != nil { 131 return ex.New(err) 132 } 133 for _, cc := range message.CC { 134 if _, err := bufWriter.WriteString(fmt.Sprintf("Cc: %s\r\n", cc)); err != nil { 135 return ex.New(err) 136 } 137 } 138 if err = bufWriter.Flush(); err != nil { 139 return ex.New(err) 140 } 141 if message.Subject != "" { 142 if _, err := bufWriter.WriteString("Subject: " + message.Subject + "\r\n"); err != nil { 143 return ex.New(err) 144 } 145 } 146 if err = bufWriter.Flush(); err != nil { 147 return ex.New(err) 148 } 149 150 if message.HTMLBody != "" { 151 if _, err := bufWriter.WriteString("MIME-version: 1.0;\r\nContent-Type: text/html; charset=\"UTF-8\";\r\n"); err != nil { 152 return ex.New(err) 153 } 154 if _, err := bufWriter.WriteString(message.HTMLBody); err != nil { 155 return ex.New(err) 156 } 157 } else if message.TextBody != "" { 158 if _, err := bufWriter.WriteString(message.TextBody); err != nil { 159 return ex.New(err) 160 } 161 } 162 if err = bufWriter.Flush(); err != nil { 163 return ex.New(err) 164 } 165 if err := w.Close(); err != nil { 166 return ex.New(err) 167 } 168 169 return ex.New(client.Quit()) 170 } 171 172 // SMTPPlainAuth is a auth set for smtp. 173 type SMTPPlainAuth struct { 174 Identity string `json:"identity" yaml:"identity"` 175 Username string `json:"username" yaml:"username" env:"SMTP_USERNAME"` 176 Password string `json:"password" yaml:"password" env:"SMTP_PASSWORD"` 177 } 178 179 // Resolve implements configutil.ConfigResolver. 180 func (spa SMTPPlainAuth) Resolve(ctx context.Context) error { 181 return env.GetVars(ctx).ReadInto(&spa) //note(wc); i'm not sure this will always work 182 } 183 184 // IsZero returns if the plain auth is unset. 185 func (spa SMTPPlainAuth) IsZero() bool { 186 return spa.Username == "" && spa.Password == "" 187 }