Skip to content

Commit a0118d8

Browse files
authored
feat: Add CC and BCC support to email sending. (#16)
* feat: Add CC and BCC support to email sending. Also support mulitple To addresses. * Typo in from field
1 parent c22914c commit a0118d8

7 files changed

Lines changed: 108 additions & 23 deletions

File tree

.goreleaser.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ builds:
88
- CGO_ENABLED=0
99
ldflags:
1010
- -X github.com/commitdev/zero-notification-service/cmd.appVersion={{.Version}} -X github.com/commitdev/zero-notification-service/cmd.appBuild={{.ShortCommit}}
11+
ignore:
12+
- goos: darwin
13+
goarch: arm64
1114
archives:
1215
- replacements:
1316
darwin: Darwin

api/notification-service.yaml

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,10 @@ paths:
2222

2323
/email/send:
2424
post:
25-
summary: Send an email
25+
summary: |
26+
Send a single email.
27+
Note that if multiple to addresses are provided it will still only send a single email with multiple addresses in the to field.
28+
To send multiple emails to individual addresses, see the sendBulk endpoint.
2629
operationId: sendEmail
2730
tags:
2831
- email
@@ -49,7 +52,10 @@ paths:
4952

5053
/email/sendBulk:
5154
post:
52-
summary: Send a batch of emails to many users with the same content. Note that it is possible for only a subset of these to fail.
55+
summary: |
56+
Send a batch of multiple emails to individual recipients with the same content.
57+
Note that if cc or bcc address are provided, each email sent will also be sent to any addresses in these lists.
58+
Note that it is possible for only a subset of these to fail
5359
operationId: sendBulk
5460
tags:
5561
- email
@@ -210,12 +216,22 @@ components:
210216
SendMailRequest:
211217
type: object
212218
required:
213-
- to
214-
- from
219+
- toAddresses
220+
- fromAddress
215221
properties:
216-
to:
217-
$ref: '#/components/schemas/EmailRecipient'
218-
from:
222+
toAddresses:
223+
type: array
224+
items:
225+
$ref: '#/components/schemas/EmailRecipient'
226+
ccAddresses:
227+
type: array
228+
items:
229+
$ref: '#/components/schemas/EmailRecipient'
230+
bccAddresses:
231+
type: array
232+
items:
233+
$ref: '#/components/schemas/EmailRecipient'
234+
fromAddress:
219235
$ref: '#/components/schemas/EmailSender'
220236
message:
221237
$ref: '#/components/schemas/MailMessage'
@@ -224,13 +240,21 @@ components:
224240
type: object
225241
required:
226242
- toAddresses
227-
- from
243+
- fromAddress
228244
properties:
229245
toAddresses:
230246
type: array
231247
items:
232248
$ref: '#/components/schemas/EmailRecipient'
233-
from:
249+
ccAddresses:
250+
type: array
251+
items:
252+
$ref: '#/components/schemas/EmailRecipient'
253+
bccAddresses:
254+
type: array
255+
items:
256+
$ref: '#/components/schemas/EmailRecipient'
257+
fromAddress:
234258
$ref: '#/components/schemas/EmailSender'
235259
message:
236260
$ref: '#/components/schemas/MailMessage'

internal/mail/mail.go

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,14 @@ type Client interface {
2020

2121
// SendBulkMail sends a batch of email messages to all the specified recipients
2222
// All the calls to send mail happen in parallel, with their responses returned on the provided channel
23-
func SendBulkMail(toList []server.EmailRecipient, from server.EmailSender, message server.MailMessage, client Client, responseChannel chan BulkSendAttempt) {
23+
func SendBulkMail(toList []server.EmailRecipient, from server.EmailSender, cc []server.EmailRecipient, bcc []server.EmailRecipient, message server.MailMessage, client Client, responseChannel chan BulkSendAttempt) {
2424
wg := sync.WaitGroup{}
2525
wg.Add(len(toList))
2626

2727
// Create goroutines for each send
2828
for _, to := range toList {
2929
go func(to server.EmailRecipient) {
30-
response, err := SendIndividualMail(to, from, message, client)
30+
response, err := SendIndividualMail([]server.EmailRecipient{to}, from, cc, bcc, message, client)
3131
responseChannel <- BulkSendAttempt{to.Address, response, err}
3232
wg.Done()
3333
}(to)
@@ -40,11 +40,44 @@ func SendBulkMail(toList []server.EmailRecipient, from server.EmailSender, messa
4040
}
4141

4242
// SendIndividualMail sends an email message
43-
func SendIndividualMail(to server.EmailRecipient, from server.EmailSender, message server.MailMessage, client Client) (*rest.Response, error) {
44-
sendFrom := sendgridMail.NewEmail(from.Name, from.Address)
45-
sendTo := sendgridMail.NewEmail(to.Name, to.Address)
46-
sendMessage := sendgridMail.NewSingleEmail(sendFrom, message.Subject, sendTo, message.Body, message.RichBody)
43+
func SendIndividualMail(to []server.EmailRecipient, from server.EmailSender, cc []server.EmailRecipient, bcc []server.EmailRecipient, message server.MailMessage, client Client) (*rest.Response, error) {
44+
sendMessage := sendgridMail.NewV3Mail()
45+
46+
sendMessage.SetFrom(sendgridMail.NewEmail(from.Name, from.Address))
47+
48+
if message.Body != "" {
49+
sendMessage.AddContent(sendgridMail.NewContent("text/plain", message.Body))
50+
}
51+
if message.RichBody != "" {
52+
sendMessage.AddContent(sendgridMail.NewContent("text/html", message.RichBody))
53+
}
54+
4755
sendMessage.SetTemplateID(message.TemplateId)
4856
sendMessage.SetSendAt(int(message.ScheduleSendAtTimestamp))
57+
58+
personalization := sendgridMail.NewPersonalization()
59+
60+
personalization.Subject = message.Subject
61+
62+
personalization.AddTos(convertAddresses(to)...)
63+
64+
if len(cc) > 0 {
65+
personalization.AddCCs(convertAddresses(cc)...)
66+
}
67+
68+
if len(bcc) > 0 {
69+
personalization.AddBCCs(convertAddresses(bcc)...)
70+
}
71+
sendMessage.AddPersonalizations(personalization)
72+
4973
return client.Send(sendMessage)
5074
}
75+
76+
// convertAddresses converts a list of EmailRecipient structs to a list of sendgrid's email address type
77+
func convertAddresses(addresses []server.EmailRecipient) []*sendgridMail.Email {
78+
returnAddresses := make([]*sendgridMail.Email, len(addresses))
79+
for i, address := range addresses {
80+
returnAddresses[i] = sendgridMail.NewEmail(address.Name, address.Address)
81+
}
82+
return returnAddresses
83+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package mail
2+
3+
import (
4+
"testing"
5+
6+
"github.com/commitdev/zero-notification-service/internal/server"
7+
"github.com/stretchr/testify/assert"
8+
)
9+
10+
func TestConvertAddresses(t *testing.T) {
11+
12+
addresses := []server.EmailRecipient{
13+
server.EmailRecipient{Name: "User1", Address: "user1@address.com"},
14+
}
15+
convertedAddresses := convertAddresses(addresses)
16+
17+
assert.Len(t, convertedAddresses, 1, "Returned list should be 1 element")
18+
assert.Equal(t, convertedAddresses[0].Name, "User1", "Returned name should match")
19+
assert.Equal(t, convertedAddresses[0].Address, "user1@address.com", "Returned address should match")
20+
}

internal/mail/mail_test.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,14 +35,16 @@ func TestSendBulkMail(t *testing.T) {
3535
Address: fmt.Sprintf("address%d@domain.com", i),
3636
})
3737
}
38+
cc := make([]server.EmailRecipient, 0)
39+
bcc := make([]server.EmailRecipient, 0)
3840
from := server.EmailSender{Name: "Test User", Address: "address@domain.com"}
3941
message := server.MailMessage{Subject: "Subject", Body: "Body"}
4042
client := FakeClient{}
4143

4244
client.On("Send").Return(nil, nil)
4345

4446
responseChannel := make(chan mail.BulkSendAttempt)
45-
mail.SendBulkMail(toList, from, message, &client, responseChannel)
47+
mail.SendBulkMail(toList, from, cc, bcc, message, &client, responseChannel)
4648

4749
// Range over the channel until empty
4850
returnedCount := 0

internal/service/api_email_service.go

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"github.com/commitdev/zero-notification-service/internal/mail"
1010
"github.com/commitdev/zero-notification-service/internal/server"
1111
"github.com/sendgrid/sendgrid-go"
12+
"go.uber.org/zap"
1213
)
1314

1415
// EmailApiService is a service that implents the logic for the EmailApiServicer
@@ -26,15 +27,16 @@ func NewEmailApiService(c *config.Config) server.EmailApiServicer {
2627
// SendEmail - Send an email
2728
func (s *EmailApiService) SendEmail(ctx context.Context, sendMailRequest server.SendMailRequest) (server.ImplResponse, error) {
2829
client := sendgrid.NewSendClient(s.config.SendgridAPIKey)
29-
response, err := mail.SendIndividualMail(sendMailRequest.To, sendMailRequest.From, sendMailRequest.Message, client)
30+
response, err := mail.SendIndividualMail(sendMailRequest.ToAddresses, sendMailRequest.FromAddress, sendMailRequest.CcAddresses, sendMailRequest.BccAddresses, sendMailRequest.Message, client)
3031

3132
if err != nil {
32-
fmt.Printf("Error sending mail: %v\n", response)
33+
zap.S().Errorf("Error sending mail: %v", response)
34+
3335
return server.Response(http.StatusInternalServerError, nil), fmt.Errorf("Unable to send email: %v", err)
3436
}
3537

3638
if !(response.StatusCode >= 200 && response.StatusCode <= 299) {
37-
fmt.Printf("Failure from Sendgrid when sending mail: %v\n", response)
39+
zap.S().Errorf("Failure from Sendgrid when sending mail: %v", response)
3840
return server.Response(http.StatusInternalServerError, nil), fmt.Errorf("Unable to send email: %v from mail provider: %v", response.StatusCode, response.Body)
3941
}
4042

@@ -47,18 +49,18 @@ func (s *EmailApiService) SendBulk(ctx context.Context, sendBulkMailRequest serv
4749

4850
responseChannel := make(chan mail.BulkSendAttempt)
4951

50-
mail.SendBulkMail(sendBulkMailRequest.ToAddresses, sendBulkMailRequest.From, sendBulkMailRequest.Message, client, responseChannel)
52+
mail.SendBulkMail(sendBulkMailRequest.ToAddresses, sendBulkMailRequest.FromAddress, sendBulkMailRequest.CcAddresses, sendBulkMailRequest.BccAddresses, sendBulkMailRequest.Message, client, responseChannel)
5153

5254
var successful []server.SendBulkMailResponseSuccessful
5355
var failed []server.SendBulkMailResponseFailed
5456

5557
// Read all the responses from the channel. This will block if responses aren't ready and the channel is not yet closed
5658
for r := range responseChannel {
5759
if r.Error != nil {
58-
fmt.Printf("Error sending bulk mail: %v", r.Error)
60+
zap.S().Errorf("Error sending bulk mail: %v", r.Error)
5961
failed = append(failed, server.SendBulkMailResponseFailed{EmailAddress: r.EmailAddress, Error: fmt.Sprintf("Unable to send email: %v\n", r.Error)})
6062
} else if !(r.Response.StatusCode >= 200 && r.Response.StatusCode <= 299) {
61-
fmt.Printf("Failure from Sendgrid when sending bulk mail: %v", r.Response)
63+
zap.S().Errorf("Failure from Sendgrid when sending bulk mail: %v", r.Response)
6264
failed = append(failed, server.SendBulkMailResponseFailed{EmailAddress: r.EmailAddress, Error: fmt.Sprintf("Unable to send email: %v from mail provider: %v\n", r.Response.StatusCode, r.Response.Body)})
6365
} else {
6466
successful = append(successful, server.SendBulkMailResponseSuccessful{EmailAddress: r.EmailAddress, TrackingId: r.Response.Headers["X-Message-Id"][0]})

internal/service/api_notification_service.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"github.com/commitdev/zero-notification-service/internal/notification/slack"
1010
"github.com/commitdev/zero-notification-service/internal/server"
1111
slack_lib "github.com/slack-go/slack"
12+
"go.uber.org/zap"
1213
)
1314

1415
// NotificationApiService is a service that implents the logic for the NotificationApiServicer
@@ -28,7 +29,7 @@ func (s *NotificationApiService) SendSlackNotification(ctx context.Context, send
2829
client := slack_lib.New(s.config.SlackAPIKey)
2930
timestamp, err := slack.SendMessage(sendSlackMessageRequest.To, sendSlackMessageRequest.Message, sendSlackMessageRequest.ReplyToTimestamp, client)
3031
if err != nil {
31-
fmt.Printf("Error sending slack notification: %v\n", err)
32+
zap.S().Errorf("Error sending slack notification: %v", err)
3233
return server.Response(http.StatusInternalServerError, nil), fmt.Errorf("Unable to send slack notification: %v", err)
3334
}
3435

0 commit comments

Comments
 (0)