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  }