Skip to content
Snippets Groups Projects
Commit d82e4aa5 authored by Jano Hendriks's avatar Jano Hendriks
Browse files

Add shared SES email code to go-utils

parent 00481524
Branches
Tags v1.235.0
No related merge requests found
......@@ -7,7 +7,7 @@ require (
github.com/MindscapeHQ/raygun4go v1.1.1
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de
github.com/aws/aws-lambda-go v1.26.0
github.com/aws/aws-sdk-go-v2 v1.27.1
github.com/aws/aws-sdk-go-v2 v1.30.0
github.com/aws/aws-sdk-go-v2/config v1.27.10
github.com/aws/aws-sdk-go-v2/credentials v1.17.10
github.com/aws/aws-sdk-go-v2/service/apigatewaymanagementapi v1.19.9
......@@ -44,14 +44,15 @@ require (
github.com/aws/aws-sdk-go v1.44.180 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.2 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.1 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.8 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.8 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.12 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.12 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.5 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.7 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.7 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.5 // indirect
github.com/aws/aws-sdk-go-v2/service/ses v1.23.1 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.20.4 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.23.4 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.28.6 // indirect
......@@ -72,6 +73,7 @@ require (
github.com/mattn/go-runewidth v0.0.10 // indirect
github.com/olekukonko/tablewriter v0.0.5 // indirect
github.com/pborman/uuid v1.2.1 // indirect
github.com/prozz/aws-embedded-metrics-golang v1.2.0 // indirect
github.com/rivo/uniseg v0.1.0 // indirect
github.com/smartystreets/goconvey v1.7.2 // indirect
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
......@@ -89,5 +91,7 @@ require (
golang.org/x/term v0.19.0 // indirect
google.golang.org/appengine v1.6.6 // indirect
google.golang.org/protobuf v1.28.1 // indirect
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df // indirect
mellium.im/sasl v0.2.1 // indirect
)
......@@ -14,6 +14,8 @@ github.com/aws/aws-sdk-go v1.44.180/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8
github.com/aws/aws-sdk-go-v2 v1.17.3/go.mod h1:uzbQtefpm44goOPmdKyAlXSNcwlRgF3ePWVW6EtJvvw=
github.com/aws/aws-sdk-go-v2 v1.27.1 h1:xypCL2owhog46iFxBKKpBcw+bPTX/RJzwNj8uSilENw=
github.com/aws/aws-sdk-go-v2 v1.27.1/go.mod h1:ffIFB97e2yNsv4aTSGkqtHnppsIJzw7G7BReUZ3jCXM=
github.com/aws/aws-sdk-go-v2 v1.30.0 h1:6qAwtzlfcTtcL8NHtbDQAqgM5s6NDipQTkPxyH/6kAA=
github.com/aws/aws-sdk-go-v2 v1.30.0/go.mod h1:ffIFB97e2yNsv4aTSGkqtHnppsIJzw7G7BReUZ3jCXM=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.2 h1:x6xsQXGSmW6frevwDA+vi/wqhp1ct18mVXYN08/93to=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.2/go.mod h1:lPprDr1e6cJdyYeGXnRaJoP4Md+cDBvi2eOj00BlGmg=
github.com/aws/aws-sdk-go-v2/config v1.18.8/go.mod h1:5XCmmyutmzzgkpk/6NYTjeWb6lgo9N170m1j6pQkIBs=
......@@ -28,9 +30,13 @@ github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.1/go.mod h1:zusuAeqezXzAB24L
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.27/go.mod h1:a1/UpzeyBBerajpnP5nGZa9mGzsBn5cOKxm6NWQsvoI=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.8 h1:RnLB7p6aaFMRfyQkD6ckxR7myCC9SABIqSz4czYUUbU=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.8/go.mod h1:XH7dQJd+56wEbP1I4e4Duo+QhSMxNArE8VP7NuUOTeM=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.12 h1:SJ04WXGTwnHlWIODtC5kJzKbeuHt+OUNOgKg7nfnUGw=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.12/go.mod h1:FkpvXhA92gb3GE9LD6Og0pHHycTxW7xGpnEh5E7Opwo=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.21/go.mod h1:+Gxn8jYn5k9ebfHEqlhrMirFjSW0v0C9fI+KN5vk2kE=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.8 h1:jzApk2f58L9yW9q1GEab3BMMFWUkkiZhyrRUtbwUbKU=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.8/go.mod h1:WqO+FftfO3tGePUtQxPXM6iODVfqMwsVMgTbG/ZXIdQ=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.12 h1:hb5KgeYfObi5MHkSSZMEudnIvX30iB+E21evI4r6BnQ=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.12/go.mod h1:CroKe/eWJdyfy9Vx4rljP5wTUjNJfb+fPz1uMYUhEGM=
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.28/go.mod h1:yRZVr/iT0AqyHeep00SZ4YfBAKojXz08w3XMBscdi0c=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 h1:hT8rVHwugYE2lEfdFE0QWVo81lF7jMrYJVDWI+f+VxU=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0/go.mod h1:8tu/lYfQfFe6IGnaOdrpVgEL2IrrDOf6/m9RQum4NkY=
......@@ -53,6 +59,8 @@ github.com/aws/aws-sdk-go-v2/service/s3 v1.53.1 h1:6cnno47Me9bRykw9AEv9zkXE+5or7
github.com/aws/aws-sdk-go-v2/service/s3 v1.53.1/go.mod h1:qmdkIIAC+GCLASF7R2whgNrJADz0QZPX+Seiw/i4S3o=
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.28.6 h1:TIOEjw0i2yyhmhRry3Oeu9YtiiHWISZ6j/irS1W3gX4=
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.28.6/go.mod h1:3Ba++UwWd154xtP4FRX5pUK3Gt4up5sDHCve6kVfE+g=
github.com/aws/aws-sdk-go-v2/service/ses v1.23.1 h1:XDy5gu6vWlLrR964J3yOoefbuXPEjdMglBqeANCN3Do=
github.com/aws/aws-sdk-go-v2/service/ses v1.23.1/go.mod h1:V6akueJRZRsIvbSoFZha+H2n8ZhNcjlfR1rxuU2hZug=
github.com/aws/aws-sdk-go-v2/service/sqs v1.31.4 h1:mE2ysZMEeQ3ulHWs4mmc4fZEhOfeY1o6QXAfDqjbSgw=
github.com/aws/aws-sdk-go-v2/service/sqs v1.31.4/go.mod h1:lCN2yKnj+Sp9F6UzpoPPTir+tSaC9Jwf6LcmTqnXFZw=
github.com/aws/aws-sdk-go-v2/service/sso v1.12.0/go.mod h1:wo/B7uUm/7zw/dWhBJ4FXuw1sySU5lyIhVg1Bu2yL9A=
......@@ -163,6 +171,7 @@ github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
github.com/kinbiko/jsonassert v1.0.1/go.mod h1:QRwBwiAsrcJpjw+L+Q4WS8psLxuUY+HylVZS/4j74TM=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
......@@ -206,6 +215,8 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prozz/aws-embedded-metrics-golang v1.2.0 h1:b/LFb8J9LbgANow/9nYZE3M3bkb457/dj0zAB3hPyvo=
github.com/prozz/aws-embedded-metrics-golang v1.2.0/go.mod h1:MXOqF9cJCEHjj77LWq7NWK44/AOyaFzwmcAYqR3057M=
github.com/r3labs/diff/v2 v2.14.2 h1:1HVhQKwg1YnoCWzCYlOWYLG4C3yfTudZo5AcrTSgCTc=
github.com/r3labs/diff/v2 v2.14.2/go.mod h1:I8noH9Fc2fjSaMxqF3G2lhDdC0b+JXCfyx85tWFM9kc=
github.com/rivo/uniseg v0.1.0 h1:+2KBaVoUmb9XzDsrx/Ct0W/EYOSFf/nWTauy++DprtY=
......@@ -388,12 +399,16 @@ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp0
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE=
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw=
gopkg.in/h2non/gock.v1 v1.1.2 h1:jBbHXgGBK/AoPVfJh5x4r/WxIrElvbLel8TCZkkZJoY=
gopkg.in/h2non/gock.v1 v1.1.2/go.mod h1:n7UGz/ckNChHiK05rDoiC4MYSunEC/lyaUm2WWaDva0=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
......
package ses
import (
"bytes"
"context"
"fmt"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/service/ses"
sesTypes "github.com/aws/aws-sdk-go-v2/service/ses/types"
"github.com/prozz/aws-embedded-metrics-golang/emf"
"gitlab.bob.co.za/bob-public-utils/bobgroup-go-utils/errors"
"gitlab.bob.co.za/bob-public-utils/bobgroup-go-utils/logs"
"gitlab.bob.co.za/bob-public-utils/bobgroup-go-utils/s3"
"gitlab.bob.co.za/bob-public-utils/bobgroup-go-utils/utils"
"gopkg.in/gomail.v2"
"io"
"strings"
)
const (
InvalidParameterValueErrorString = "InvalidParameterValue"
)
var (
clients = map[string]*ClientWithHelpers{}
ErrorSESClientNotEstablished = errors.Error("could not establish an SES client")
)
type ClientWithHelpers struct {
SESClient *ses.Client
}
type Email struct {
Recipient string `json:"recipient"`
FromName string `json:"from_name"`
FromEmail string `json:"from_email"`
Subject string `json:"subject"`
EmailCcName string `json:"email_cc_name"`
EmailCcAddress string `json:"email_cc_address"`
ReplyToEmailAddress string `json:"reply_to_email_address"`
Body EmailBody `json:"body"`
Attachments *[]s3.S3UploadResponse `json:"attachments"`
}
type EmailBody struct {
HTMLBody string `json:"html_body"`
TextBody string `json:"text_body"`
}
func GetClient(region ...string) *ClientWithHelpers {
sesRegion := "eu-west-1"
// Set custom region
if region != nil && len(region) > 0 {
sesRegion = region[0]
}
// Check if client exists for region, if it does return it
if sesClient, ok := clients[sesRegion]; ok && sesClient != nil {
return sesClient
}
cfg, err := config.LoadDefaultConfig(context.TODO(),
config.WithRegion(sesRegion),
)
if err != nil {
return &ClientWithHelpers{}
}
sesClient := NewClient(cfg)
clients[sesRegion] = sesClient
return sesClient
}
func NewClient(cfg aws.Config) *ClientWithHelpers {
return &ClientWithHelpers{
SESClient: ses.NewFromConfig(cfg),
}
}
func (c *ClientWithHelpers) SendEmail(email Email, emfNamespace string, isDebug bool) (*ses.SendRawEmailOutput, error) {
if c.SESClient == nil {
return nil, ErrorSESClientNotEstablished
}
recipients := utils.SplitAndCleanEmailAddresses(email.Recipient)
if len(recipients) == 0 {
return nil, nil
}
// Create raw message
msg := gomail.NewMessage()
msg.SetHeader("To", recipients...)
// Set the from header, and use gomail's internal function if we have a name and email
from := email.FromEmail
if email.FromName != "" {
from = msg.FormatAddress(email.FromEmail, email.FromName)
}
msg.SetHeader("From", from)
msg.SetAddressHeader("Cc", email.EmailCcAddress, email.EmailCcName)
msg.SetHeader("Subject", email.Subject)
msg.SetBody("text/plain", email.Body.TextBody) // See comment for `AddAlternative`
msg.AddAlternative("text/html", email.Body.HTMLBody)
// Set the reply-to address
if email.ReplyToEmailAddress != "" {
msg.SetHeader("Reply-To", email.ReplyToEmailAddress)
}
// Add attachments
if email.Attachments != nil && len(*email.Attachments) != 0 {
for _, attachment := range *email.Attachments {
err := addAttachmentsToEmail(msg, attachment, isDebug)
if err != nil {
return nil, err
}
}
}
// create a new buffer to add raw data
var emailRaw bytes.Buffer
_, err := msg.WriteTo(&emailRaw)
if err != nil {
return nil, err
}
// Append the CC address to the recipients
if email.EmailCcAddress != "" {
recipients = append(recipients, email.EmailCcAddress)
}
// create new raw message
rawEmailInput := &ses.SendRawEmailInput{
Source: &from,
Destinations: recipients,
RawMessage: &sesTypes.RawMessage{
Data: emailRaw.Bytes(),
},
}
// Send it
result, err := c.SESClient.SendRawEmail(context.TODO(), rawEmailInput)
if err != nil {
messageRejectedErrorCode := errors.AWSErrorExceptionCode(&sesTypes.MessageRejected{})
mailFromDomainNotVerifiedErrorCode := errors.AWSErrorExceptionCode(&sesTypes.MailFromDomainNotVerifiedException{})
configurationSetDoesNotExistErrorCode := errors.AWSErrorExceptionCode(&sesTypes.ConfigurationSetDoesNotExistException{})
switch errors.AWSErrorExceptionCode(err) {
case messageRejectedErrorCode, mailFromDomainNotVerifiedErrorCode, configurationSetDoesNotExistErrorCode:
logs.ErrorWithMsg(errors.AWSErrorExceptionCode(err), err)
case InvalidParameterValueErrorString:
// Ignore errors due to invalid email addresses - we don't want to log it to raygun
logs.Warn(err.Error())
err = nil
default:
logs.ErrorWithMsg("Failed to send email", err)
}
if emfNamespace != "" {
emf.New().
Namespace(emfNamespace).
MetricAs("email_failed", 1, emf.Count).
Log()
}
} else {
if emfNamespace != "" {
emf.New().
Namespace(emfNamespace).
MetricAs("email_sent", 1, emf.Count).
Log()
}
}
return result, err
}
func addAttachmentsToEmail(msg *gomail.Message, attachment s3.S3UploadResponse, isDebug bool) error {
if attachment.Bucket == "" || attachment.Filename == "" {
logs.Warn("empty bucket and/or filename, cannot attach file")
return nil
}
// Get object from S3
object, err := s3.GetClient(isDebug).GetObject(attachment.Bucket, attachment.Filename, isDebug)
if err != nil {
logs.ErrorWithMsg(fmt.Sprintf("Failed to get S3 object from bucket %s filename %s", attachment.Bucket, attachment.Filename), err)
return err
}
defer object.Body.Close()
attachmentBytes, err := io.ReadAll(object.Body)
if err != nil {
logs.ErrorWithMsg("Could not encode attachment for email", err)
return err
}
// Get the filename
splitString := strings.Split(attachment.Filename, "/")
filename := splitString[len(splitString)-1]
msg.Attach(
filename,
gomail.SetCopyFunc(func(w io.Writer) error {
_, err := w.Write(attachmentBytes)
return err
}),
gomail.SetHeader(map[string][]string{"Content-Type": {*object.ContentType}}),
)
return nil
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment