/* Copyright 2021 The Dapr Authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package sendgrid import ( "context" "encoding/json" "errors" "fmt" "strconv" "strings" "github.com/sendgrid/sendgrid-go" "github.com/sendgrid/sendgrid-go/helpers/mail" "github.com/dapr/components-contrib/bindings" "github.com/dapr/kit/logger" ) // SendGrid allows sending of emails using the 3rd party SendGrid service. type SendGrid struct { metadata sendGridMetadata logger logger.Logger } // Our metadata holds standard email properties. type sendGridMetadata struct { APIKey string `json:"apiKey"` EmailFrom string `json:"emailFrom"` EmailFromName string `json:"emailFromName"` EmailTo string `json:"emailTo"` EmailToName string `json:"emailToName"` Subject string `json:"subject"` EmailCc string `json:"emailCc"` EmailBcc string `json:"emailBcc"` } // Wrapper to help decode SendGrid API errors. type sendGridRestError struct { Errors []struct { Field interface{} `json:"field"` Message interface{} `json:"message"` Help interface{} `json:"help"` } `json:"errors"` } // NewSendGrid returns a new SendGrid bindings instance. func NewSendGrid(logger logger.Logger) bindings.OutputBinding { return &SendGrid{logger: logger} } // Helper to parse metadata. func (sg *SendGrid) parseMetadata(meta bindings.Metadata) (sendGridMetadata, error) { sgMeta := sendGridMetadata{} // Required properties if val, ok := meta.Properties["apiKey"]; ok && val != "" { sgMeta.APIKey = val } else { return sgMeta, errors.New("SendGrid binding error: apiKey field is required in metadata") } // Optional properties, these can be set on a per request basis sgMeta.EmailTo = meta.Properties["emailTo"] sgMeta.EmailToName = meta.Properties["emailToName"] sgMeta.EmailFrom = meta.Properties["emailFrom"] sgMeta.EmailFromName = meta.Properties["emailFromName"] sgMeta.Subject = meta.Properties["subject"] sgMeta.EmailCc = meta.Properties["emailCc"] sgMeta.EmailBcc = meta.Properties["emailBcc"] return sgMeta, nil } // Init does metadata parsing and not much else :). func (sg *SendGrid) Init(_ context.Context, metadata bindings.Metadata) error { // Parse input metadata meta, err := sg.parseMetadata(metadata) if err != nil { return err } // Um, yeah that's about it! sg.metadata = meta return nil } func (sg *SendGrid) Operations() []bindings.OperationKind { return []bindings.OperationKind{bindings.CreateOperation} } // Write does the work of sending message to SendGrid API. func (sg *SendGrid) Invoke(ctx context.Context, req *bindings.InvokeRequest) (*bindings.InvokeResponse, error) { // We allow two possible sources of the properties we need, // the component metadata or request metadata, request takes priority if present // Build email from address, this is required var fromAddress *mail.Email if sg.metadata.EmailFrom != "" { // Optionally set the email from name fromName := "" if sg.metadata.EmailFromName != "" { fromName = sg.metadata.EmailFromName } fromAddress = mail.NewEmail(fromName, sg.metadata.EmailFrom) } if req.Metadata["emailFrom"] != "" { // Optionally set the email from name fromName := "" if req.Metadata["emailFromName"] != "" { fromName = req.Metadata["emailFromName"] } fromAddress = mail.NewEmail(fromName, req.Metadata["emailFrom"]) } if fromAddress == nil { return nil, fmt.Errorf("error SendGrid from email not supplied") } // Build email to address, this is required var toAddress *mail.Email if sg.metadata.EmailTo != "" { // Optionally set the email to name toName := "" if sg.metadata.EmailToName != "" { toName = sg.metadata.EmailToName } toAddress = mail.NewEmail(toName, sg.metadata.EmailTo) } if req.Metadata["emailTo"] != "" { // Optionally set the email to name toName := "" if req.Metadata["emailToName"] != "" { toName = req.Metadata["emailToName"] } toAddress = mail.NewEmail(toName, req.Metadata["emailTo"]) } if toAddress == nil { return nil, fmt.Errorf("error SendGrid to email not supplied") } // Build email subject, this is required subject := "" if sg.metadata.Subject != "" { subject = sg.metadata.Subject } if req.Metadata["subject"] != "" { subject = req.Metadata["subject"] } if subject == "" { return nil, fmt.Errorf("error SendGrid subject not supplied") } // Build email cc address, this is optional var ccAddress *mail.Email if sg.metadata.EmailCc != "" { ccAddress = mail.NewEmail("", sg.metadata.EmailCc) } if req.Metadata["emailCc"] != "" { ccAddress = mail.NewEmail("", req.Metadata["emailCc"]) } // Build email bcc address, this is optional var bccAddress *mail.Email if sg.metadata.EmailBcc != "" { bccAddress = mail.NewEmail("", sg.metadata.EmailBcc) } if req.Metadata["emailBcc"] != "" { bccAddress = mail.NewEmail("", req.Metadata["emailBcc"]) } // Email body is held in req.Data, after we tidy it up a bit emailBody, err := strconv.Unquote(string(req.Data)) if err != nil { // Unquote will error if the string is not quoted (not exactly graceful!), so fallback using the string as is emailBody = string(req.Data) } // Construct email message email := mail.NewV3Mail() email.SetFrom(fromAddress) email.AddContent(mail.NewContent("text/html", emailBody)) // Add other fields to email personalization := mail.NewPersonalization() personalization.AddTos(toAddress) personalization.Subject = subject if ccAddress != nil { personalization.AddCCs(ccAddress) } if bccAddress != nil { personalization.AddBCCs(bccAddress) } email.AddPersonalizations(personalization) // Send the email client := sendgrid.NewSendClient(sg.metadata.APIKey) resp, err := client.SendWithContext(ctx, email) if err != nil { return nil, fmt.Errorf("error from SendGrid, sending email failed: %+v", err) } // Check SendGrid response is OK if !(resp.StatusCode >= 200 && resp.StatusCode < 300) { // Extract the underlying error message(s) returned from SendGrid REST API sendGridError := sendGridRestError{} json.NewDecoder(strings.NewReader(resp.Body)).Decode(&sendGridError) // Pass it back to the caller, so they have some idea what went wrong return nil, fmt.Errorf("error from SendGrid, sending email failed: %d %+v", resp.StatusCode, sendGridError) } sg.logger.Info("sent email with SendGrid") return nil, nil }