Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[SDP-970][#102] Add Twilio SendGrid Email Client #444

Merged
merged 2 commits into from
Oct 26, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,9 +118,10 @@ The Message Service sends messages to users and recipients for the following rea
- Providing one-time passcodes (OTPs) to recipients
- Sending emails to users during account creation and account recovery flows

Note that the Message Service requires that both SMS and email services are configured. For emails, AWS SES is supported. For SMS messages to recipients, Twilio is supported. AWS SNS support is not integrated yet.
Note that the Message Service requires that both SMS and email services are configured. For emails, AWS SES and Twilio Sendgrid are supported. For SMS messages to recipients, Twilio is supported. AWS SNS support is not integrated yet.
marwen-abid marked this conversation as resolved.
Show resolved Hide resolved

If you're using the `AWS_EMAIL` or `TWILIO_EMAIL` sender types, you'll need to verify the email address you're using to send emails in order to prevent it from being flagged by email firewalls. You can do that by following the instructions in [this link for AWS SES](https://docs.aws.amazon.com/ses/latest/dg/email-authentication-methods.html) or [this link for Twilio Sendgrid](https://www.twilio.com/docs/sendgrid/glossary/sender-authentication).

If you're using the `AWS_EMAIL` sender type, you'll need to verify the email address you're using to send emails in order to prevent it from being flagged by email firewalls. You can do that by following the instructions in [this link](https://docs.aws.amazon.com/ses/latest/dg/email-authentication-methods.html).

#### Wallet Registration UI

Expand Down
2 changes: 1 addition & 1 deletion cmd/message.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ func (s *MessageCommand) Command(messengerService MessengerServiceInterface) *co
// message sender type
{
Name: "message-sender-type",
Usage: `Message Sender Type. Options: "TWILIO_SMS", "AWS_SMS", "AWS_EMAIL", "DRY_RUN"`,
Usage: `Message Sender Type. Options: "TWILIO_SMS", "TWILIO_EMAIL", AWS_SMS", "AWS_EMAIL", "DRY_RUN"`,
OptType: types.String,
CustomSetValue: cmdUtils.SetConfigOptionMessengerType,
ConfigKey: &opts.MessengerType,
Expand Down
1 change: 1 addition & 0 deletions cmd/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ func (s *ServerService) SetupConsumers(ctx context.Context, o SetupConsumersOpti
MessageDispatcher: o.ServeOpts.MessageDispatcher,
MaxInvitationResendAttempts: int64(o.ServeOpts.MaxInvitationResendAttempts),
Sep10SigningPrivateKey: o.ServeOpts.Sep10SigningPrivateKey,
CrashTrackerClient: o.ServeOpts.CrashTrackerClient.Clone(),
marwen-abid marked this conversation as resolved.
Show resolved Hide resolved
}),
)
if err != nil {
Expand Down
15 changes: 15 additions & 0 deletions cmd/utils/shared_config_options.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,21 @@ func TwilioConfigOptions(opts *message.MessengerOptions) []*config.ConfigOption
ConfigKey: &opts.TwilioServiceSID,
Required: false,
},
// Twilio Email (SendGrid)
{
Name: "twilio-sendgrid-api-key",
Usage: "The API key of the Twilio SendGrid account",
OptType: types.String,
ConfigKey: &opts.TwilioSendGridAPIKey,
Required: false,
},
{
Name: "twilio-sendgrid-sender-address",
Usage: "The email address that Twilio SendGrid will use to send emails",
OptType: types.String,
ConfigKey: &opts.TwilioSendGridSenderAddress,
Required: false,
},
}
}

Expand Down
22 changes: 22 additions & 0 deletions db/migrations/sdp-migrations/2024-10-25.0-add-twilio-email.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
-- This is to add `TWILIO_EMAIL` to the `message_type` enum.

-- +migrate Up
ALTER TYPE message_type ADD VALUE 'TWILIO_EMAIL';


-- +migrate Down
CREATE TYPE temp_message_type AS ENUM (
'TWILIO_SMS',
'AWS_SMS',
'AWS_EMAIL',
'DRY_RUN'
);

DELETE FROM messages WHERE type = 'TWILIO_EMAIL';

ALTER TABLE messages
ALTER COLUMN type TYPE temp_message_type USING type::text::temp_message_type;

DROP TYPE message_type;

ALTER TYPE temp_message_type RENAME TO message_type;
2 changes: 2 additions & 0 deletions go.list
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,8 @@ github.com/sanity-io/litter v1.5.5
github.com/schollz/closestmatch v2.1.0+incompatible
github.com/segmentio/go-loggly v0.5.1-0.20171222203950-eb91657e62b2
github.com/segmentio/kafka-go v0.4.47
github.com/sendgrid/rest v2.6.9+incompatible
github.com/sendgrid/sendgrid-go v3.16.0+incompatible
github.com/sergi/go-diff v1.2.0
github.com/shopspring/decimal v1.3.1
github.com/shurcooL/httpfs v0.0.0-20230704072500-f1e31cf0ba5c
Expand Down
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ require (
github.com/rs/cors v1.11.1
github.com/rubenv/sql-migrate v1.7.0
github.com/segmentio/kafka-go v0.4.47
github.com/sendgrid/rest v2.6.9+incompatible
github.com/sendgrid/sendgrid-go v3.16.0+incompatible
github.com/sirupsen/logrus v1.9.3
github.com/spf13/cobra v1.8.1
github.com/spf13/viper v1.19.0
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,10 @@ github.com/segmentio/go-loggly v0.5.1-0.20171222203950-eb91657e62b2 h1:S4OC0+OBK
github.com/segmentio/go-loggly v0.5.1-0.20171222203950-eb91657e62b2/go.mod h1:8zLRYR5npGjaOXgPSKat5+oOh+UHd8OdbS18iqX9F6Y=
github.com/segmentio/kafka-go v0.4.47 h1:IqziR4pA3vrZq7YdRxaT3w1/5fvIH5qpCwstUanQQB0=
github.com/segmentio/kafka-go v0.4.47/go.mod h1:HjF6XbOKh0Pjlkr5GVZxt6CsjjwnmhVOfURM5KMd8qg=
github.com/sendgrid/rest v2.6.9+incompatible h1:1EyIcsNdn9KIisLW50MKwmSRSK+ekueiEMJ7NEoxJo0=
github.com/sendgrid/rest v2.6.9+incompatible/go.mod h1:kXX7q3jZtJXK5c5qK83bSGMdV6tsOE70KbHoqJls4lE=
github.com/sendgrid/sendgrid-go v3.16.0+incompatible h1:i8eE6IMkiCy7vusSdacHHSBUpXyTcTXy/Rl9N9aZ/Qw=
github.com/sendgrid/sendgrid-go v3.16.0+incompatible/go.mod h1:QRQt+LX/NmgVEvmdRw0VT/QgUn499+iza2FnDca9fg8=
github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ=
github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
Expand Down
2 changes: 1 addition & 1 deletion helmchart/sdp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ Configuration parameters for the SDP Core Service which is the core backend serv
| `sdp.configMap.data.SEP10_SIGNING_PUBLIC_KEY` | Anchor platform SEP10 signing public key. | `nil` |
| `sdp.configMap.data.DISTRIBUTION_PUBLIC_KEY` | The public key of the HOST's Stellar distribution account, used to create channel accounts. | `nil` |
| `sdp.configMap.data.METRICS_TYPE` | Defines the type of metrics system in use. Options: "PROMETHEUS". | `PROMETHEUS` |
| `sdp.configMap.data.EMAIL_SENDER_TYPE` | The messenger type used to send invitations to new dashboard users. Options: "DRY_RUN", "AWS_EMAIL". | `DRY_RUN` |
| `sdp.configMap.data.EMAIL_SENDER_TYPE` | The messenger type used to send invitations to new dashboard users. Options: "DRY_RUN", "AWS_EMAIL", "TWILIO_EMAIL". | `DRY_RUN` |
| `sdp.configMap.data.SMS_SENDER_TYPE` | The messenger type used to send text messages to recipients. Options: "DRY_RUN", "TWILIO_SMS". | `DRY_RUN` |
| `sdp.configMap.data.RECAPTCHA_SITE_KEY` | Site key for ReCaptcha. Required if using ReCaptcha. | `nil` |
| `sdp.configMap.data.CORS_ALLOWED_ORIGINS` | Specifies the domains allowed to make cross-origin requests. "*" means all domains are allowed. | `*` |
Expand Down
2 changes: 1 addition & 1 deletion helmchart/sdp/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ sdp:
## @param sdp.configMap.data.SEP10_SIGNING_PUBLIC_KEY Anchor platform SEP10 signing public key.
## @param sdp.configMap.data.DISTRIBUTION_PUBLIC_KEY The public key of the HOST's Stellar distribution account, used to create channel accounts.
## @param sdp.configMap.data.METRICS_TYPE Defines the type of metrics system in use. Options: "PROMETHEUS".
## @param sdp.configMap.data.EMAIL_SENDER_TYPE The messenger type used to send invitations to new dashboard users. Options: "DRY_RUN", "AWS_EMAIL".
## @param sdp.configMap.data.EMAIL_SENDER_TYPE The messenger type used to send invitations to new dashboard users. Options: "DRY_RUN", "AWS_EMAIL", "TWILIO_EMAIL".
## @param sdp.configMap.data.SMS_SENDER_TYPE The messenger type used to send text messages to recipients. Options: "DRY_RUN", "TWILIO_SMS".
## @param sdp.configMap.data.RECAPTCHA_SITE_KEY Site key for ReCaptcha. Required if using ReCaptcha.
## @param sdp.configMap.data.CORS_ALLOWED_ORIGINS Specifies the domains allowed to make cross-origin requests. "*" means all domains are allowed.
Expand Down
12 changes: 9 additions & 3 deletions internal/message/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ type MessengerType string
const (
// MessengerTypeTwilioSMS is used to send SMS messages using Twilio.
MessengerTypeTwilioSMS MessengerType = "TWILIO_SMS"
// MessengerTypeTwilioEmail is used to send emails using Twilio SendGrid.
MessengerTypeTwilioEmail MessengerType = "TWILIO_EMAIL"
// MessengerTypeAWSSMS is used to send SMS messages using AWS SNS.
MessengerTypeAWSSMS MessengerType = "AWS_SMS"
// MessengerTypeAWSEmail is used to send emails using AWS SES.
Expand All @@ -21,7 +23,7 @@ const (
)

func (mt MessengerType) All() []MessengerType {
return []MessengerType{MessengerTypeTwilioSMS, MessengerTypeAWSSMS, MessengerTypeAWSEmail, MessengerTypeDryRun}
return []MessengerType{MessengerTypeTwilioSMS, MessengerTypeTwilioEmail, MessengerTypeAWSSMS, MessengerTypeAWSEmail, MessengerTypeDryRun}
}

func ParseMessengerType(messengerTypeStr string) (MessengerType, error) {
Expand All @@ -40,7 +42,7 @@ func (mt MessengerType) ValidSMSTypes() []MessengerType {
}

func (mt MessengerType) ValidEmailTypes() []MessengerType {
return []MessengerType{MessengerTypeDryRun, MessengerTypeAWSEmail}
return []MessengerType{MessengerTypeDryRun, MessengerTypeTwilioEmail, MessengerTypeAWSEmail}
}

func (mt MessengerType) IsSMS() bool {
Expand All @@ -59,6 +61,9 @@ type MessengerOptions struct {
TwilioAccountSID string
TwilioAuthToken string
TwilioServiceSID string
// Twilio Email (SendGrid)
TwilioSendGridAPIKey string
TwilioSendGridSenderAddress string

// AWS
AWSAccessKeyID string
Expand All @@ -74,10 +79,11 @@ func GetClient(opts MessengerOptions) (MessengerClient, error) {
switch opts.MessengerType {
case MessengerTypeTwilioSMS:
return NewTwilioClient(opts.TwilioAccountSID, opts.TwilioAuthToken, opts.TwilioServiceSID)
case MessengerTypeTwilioEmail:
return NewTwilioSendGridClient(opts.TwilioSendGridAPIKey, opts.TwilioSendGridSenderAddress)

case MessengerTypeAWSSMS:
return NewAWSSNSClient(opts.AWSAccessKeyID, opts.AWSSecretAccessKey, opts.AWSRegion, opts.AWSSNSSenderID)

case MessengerTypeAWSEmail:
return NewAWSSESClient(opts.AWSAccessKeyID, opts.AWSSecretAccessKey, opts.AWSRegion, opts.AWSSESSenderID)

Expand Down
1 change: 1 addition & 0 deletions internal/message/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ func Test_ParseMessengerType(t *testing.T) {
{wantErr: fmt.Errorf("invalid message sender type \"\"")},
{messengerType: "foo_BAR", wantErr: fmt.Errorf("invalid message sender type \"FOO_BAR\"")},
{messengerType: "TWILIO_SMS"},
{messengerType: "TWILIO_EMAIL"},
{messengerType: "tWiLiO_SMS"},
{messengerType: "AWS_SMS"},
{messengerType: "AWS_EMAIL"},
Expand Down
84 changes: 84 additions & 0 deletions internal/message/twilio_sendgrid_client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package message

import (
"fmt"
"html/template"
"strings"

"github.com/sendgrid/rest"
"github.com/sendgrid/sendgrid-go"
"github.com/sendgrid/sendgrid-go/helpers/mail"
"github.com/stellar/go/support/log"

"github.com/stellar/stellar-disbursement-platform-backend/internal/htmltemplate"
"github.com/stellar/stellar-disbursement-platform-backend/internal/utils"
)

type twilioSendGridInterface interface {
Send(email *mail.SGMailV3) (*rest.Response, error)
}

var _ twilioSendGridInterface = (*sendgrid.Client)(nil)

type twilioSendGridClient struct {
client twilioSendGridInterface
senderAddress string
}

func (t *twilioSendGridClient) MessengerType() MessengerType {
return MessengerTypeTwilioEmail
}

func (t *twilioSendGridClient) SendMessage(message Message) error {
err := message.ValidateFor(t.MessengerType())
if err != nil {
return fmt.Errorf("validating message to send an email through SendGrid: %w", err)
}

from := mail.NewEmail("", t.senderAddress)
to := mail.NewEmail("", message.ToEmail)

emailBody := message.Body
if !strings.Contains(emailBody, "<html") {
var htmlErr error
emailBody, htmlErr = htmltemplate.ExecuteHTMLTemplateForEmailEmptyBody(htmltemplate.EmptyBodyEmailTemplate{Body: template.HTML(emailBody)})
if htmlErr != nil {
return fmt.Errorf("generating html template: %w", htmlErr)
}
}

email := mail.NewSingleEmail(from, message.Title, to, "", emailBody)

response, err := t.client.Send(email)
if err != nil {
return fmt.Errorf("sending SendGrid email: %w", err)
}

if response.StatusCode >= 400 {
return fmt.Errorf("sendGrid API returned error status code= %d, body= %s",
response.StatusCode, response.Body)
}

log.Debugf("🎉 SendGrid sent an email to the receiver %q", utils.TruncateString(message.ToEmail, 3))
return nil
}

// NewTwilioSendGridClient creates a new SendGrid client that is used to send emails
func NewTwilioSendGridClient(apiKey string, senderAddress string) (MessengerClient, error) {
apiKey = strings.TrimSpace(apiKey)
if apiKey == "" {
return nil, fmt.Errorf("sendGrid API key is empty")
}

senderAddress = strings.TrimSpace(senderAddress)
if err := utils.ValidateEmail(senderAddress); err != nil {
return nil, fmt.Errorf("sendGrid senderAddress is invalid: %w", err)
}

return &twilioSendGridClient{
client: sendgrid.NewSendClient(apiKey),
senderAddress: senderAddress,
}, nil
}

var _ MessengerClient = (*twilioSendGridClient)(nil)
Loading