github.com/oinume/lekcije@v0.0.0-20231017100347-5b4c5eb6ab24/backend/infrastructure/send_grid/email_sender.go (about) 1 package send_grid 2 3 import ( 4 "context" 5 "crypto/tls" 6 "fmt" 7 "net" 8 "net/http" 9 "os" 10 "strings" 11 "time" 12 13 "github.com/morikuni/failure" 14 "github.com/sendgrid/rest" 15 "github.com/sendgrid/sendgrid-go" 16 "github.com/sendgrid/sendgrid-go/helpers/mail" 17 "go.opentelemetry.io/otel" 18 "go.uber.org/zap" 19 20 "github.com/oinume/lekcije/backend/domain/config" 21 "github.com/oinume/lekcije/backend/domain/model/email" 22 "github.com/oinume/lekcije/backend/domain/repository" 23 ) 24 25 const ( 26 apiHost = "https://api.sendgrid.com" 27 apiPath = "/v3/mail/send" 28 ) 29 30 var ( 31 redirectErrorFunc = func(req *http.Request, via []*http.Request) error { 32 return http.ErrUseLastResponse 33 } 34 35 defaultHTTPClient = &http.Client{ 36 Timeout: 10 * time.Second, 37 CheckRedirect: redirectErrorFunc, 38 Transport: &http.Transport{ 39 MaxIdleConns: 100, 40 MaxIdleConnsPerHost: 100, 41 Proxy: http.ProxyFromEnvironment, 42 DialContext: (&net.Dialer{ 43 Timeout: 30 * time.Second, 44 KeepAlive: 1200 * time.Second, 45 }).DialContext, 46 IdleConnTimeout: 1200 * time.Second, 47 TLSHandshakeTimeout: 10 * time.Second, 48 TLSClientConfig: &tls.Config{ 49 ClientSessionCache: tls.NewLRUClientSessionCache(100), 50 }, 51 ExpectContinueTimeout: 1 * time.Second, 52 }, 53 } 54 ) 55 56 type emailSender struct { 57 client *rest.Client 58 appLogger *zap.Logger 59 } 60 61 func NewEmailSender(httpClient *http.Client, appLogger *zap.Logger) repository.EmailSender { 62 if httpClient == nil { 63 httpClient = defaultHTTPClient 64 } 65 client := &rest.Client{ 66 HTTPClient: httpClient, 67 } 68 return &emailSender{ 69 client: client, 70 appLogger: appLogger, 71 } 72 } 73 74 func (s *emailSender) Send(ctx context.Context, email *email.Email) error { 75 _, span := otel.Tracer(config.DefaultTracerName).Start(ctx, "emailSender.Send") 76 defer span.End() 77 78 from := mail.NewEmail(email.From.Name, email.From.Address) 79 content := mail.NewContent("text/html", strings.Replace(email.BodyString(), "\n", "<br>", -1)) 80 tos := make([]*mail.Email, len(email.Tos)) 81 for i, to := range email.Tos { 82 tos[i] = mail.NewEmail(to.Name, to.Address) 83 } 84 m := mail.NewV3MailInit(from, email.Subject, tos[0], content) 85 m.Personalizations[0].AddTos(tos[1:]...) 86 for k, v := range email.CustomArgs() { 87 m.SetCustomArg(k, v) 88 } 89 90 req := sendgrid.GetRequest(os.Getenv("SENDGRID_API_KEY"), apiPath, apiHost) 91 req.Method = "POST" 92 req.Body = mail.GetRequestBody(m) 93 //fmt.Printf("--- request ---\n%s\n", string(req.Body)) 94 resp, err := s.client.Send(req) 95 if err != nil { 96 return failure.Wrap(err, failure.Message("Failed to send email with SendGrid")) 97 } 98 //fmt.Printf("--- response ---\nstatus=%d\n%s\n", resp.StatusCode, resp.Body) 99 // No need to resp.Body.Close(). It's a string 100 if resp.StatusCode >= 300 { 101 message := fmt.Sprintf( 102 "Failed to send email by SendGrid: statusCode=%v, body=%v", 103 resp.StatusCode, strings.Replace(resp.Body, "\n", "\\n", -1), 104 ) 105 s.appLogger.Error(message) // TODO: remove and log in caller 106 return failure.Wrap(err, failure.Messagef("failed to send email with SendGrid: statusCode=%v", resp.StatusCode)) 107 } 108 109 return nil 110 }