fleet/server/mail/ses.go
Ian Littman 2ef729e473
Allow opting in users to email verification on login (#24273)
#22790 

Changes file is on the FE PR.

# Checklist for submitter

If some of the following don't apply, delete the relevant line.

<!-- Note that API documentation changes are now addressed by the
product design team. -->

- [x] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)
- [x] Added/updated tests
- [x] If database migrations are included, checked table schema to
confirm autoupdate
- For database migrations:
- [x] Checked schema for all modified table for columns that will
auto-update timestamps during migration.
- [x] Confirmed that updating the timestamps is acceptable, and will not
cause unwanted side effects.
- [x] Ensured the correct collation is explicitly set for character
columns (`COLLATE utf8mb4_unicode_ci`).
- [x] Manual QA for all new/changed functionality
2024-12-05 08:37:10 -06:00

99 lines
2.5 KiB
Go

package mail
import (
"errors"
"fmt"
"net/url"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/credentials/stscreds"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/ses"
"github.com/fleetdm/fleet/v4/server/fleet"
)
type fleetSESSender interface {
SendRawEmail(input *ses.SendRawEmailInput) (*ses.SendRawEmailOutput, error)
}
type sesSender struct {
client fleetSESSender
sourceArn string
}
func getFromSES(e fleet.Email) (string, error) {
serverURL, err := url.Parse(e.ServerURL)
if err != nil || len(serverURL.Host) == 0 {
return "", fmt.Errorf("failed to parse server url %s err: %w", e.ServerURL, err)
}
return fmt.Sprintf("From: %s\r\n", fmt.Sprintf("do-not-reply@%s", serverURL.Host)), nil
}
func (s *sesSender) SendEmail(e fleet.Email) error {
if s.client == nil {
return errors.New("ses sender not configured")
}
msg, err := getMessageBody(e, getFromSES)
if err != nil {
return err
}
return s.sendMail(e, msg)
}
func (s *sesSender) CanSendEmail(smtpSettings fleet.SMTPSettings) bool {
return s.client != nil
}
func NewSESSender(region, endpointURL, id, secret, stsAssumeRoleArn, stsExternalID, sourceArn string) (*sesSender, error) {
conf := &aws.Config{
Region: &region,
Endpoint: &endpointURL, // empty string or nil will use default values
}
// Only provide static credentials if we have them
// otherwise use the default credentials provider chain
if id != "" && secret != "" {
conf.Credentials = credentials.NewStaticCredentials(id, secret, "")
}
sess, err := session.NewSession(conf)
if err != nil {
return nil, fmt.Errorf("create SES client: %w", err)
}
if stsAssumeRoleArn != "" {
creds := stscreds.NewCredentials(sess, stsAssumeRoleArn, func(provider *stscreds.AssumeRoleProvider) {
if stsExternalID != "" {
provider.ExternalID = &stsExternalID
}
})
conf.Credentials = creds
sess, err = session.NewSession(conf)
if err != nil {
return nil, fmt.Errorf("create SES client: %w", err)
}
}
return &sesSender{client: ses.New(sess), sourceArn: sourceArn}, nil
}
func (s *sesSender) sendMail(e fleet.Email, msg []byte) error {
toAddresses := make([]*string, len(e.To))
for i := range e.To {
t := e.To[i]
toAddresses[i] = &t
}
_, err := s.client.SendRawEmail(&ses.SendRawEmailInput{
Destinations: toAddresses,
FromArn: &s.sourceArn,
RawMessage: &ses.RawMessage{Data: msg},
SourceArn: &s.sourceArn,
})
if err != nil {
return err
}
return nil
}