Skip to content

Commit

Permalink
Support email replies with threading (#252)
Browse files Browse the repository at this point in the history
* Support email replies with threading

* Fix main function for create action

* Fix error check

* Fix error handling issue and add more logs

* Fix table name in one transaction

* Fix thread id generation

* Return draft email when requesting thread

* Update log

* Keep thread id when saving email drafts

* Fix required extra method for dynamodb client

* Return ThreadID from create and save requests

* Fix tests

* Use transaction to send email

* Fix lambda handler entry

* Include ThreadID after email is sent

* Fix go tests

* Avoid duplicate imports

* `References` for email should be a string

* Support send threaded email using raw MIME

* Improve error handling

* Support joining errors in go 1.19

* Correctly send email replys in `create` and `save`

* Add comments and correctly update thread when sending

* Set todo for email deleting

* Make imports consistant

* Include more logs

* Rename a helper function

* Fix a reply header issue and add more comments

* Add SendRawEmail permission

* Log TransactionCanceledException details

* Use correct message ID when replying to sent email

* References should have msg-id enclosed in angle brackets

* Return time sent for sent emails
  • Loading branch information
harryzcy authored Apr 2, 2023
1 parent 2cdb2e5 commit 9c8ac0f
Show file tree
Hide file tree
Showing 17 changed files with 925 additions and 288 deletions.
8 changes: 8 additions & 0 deletions api/emails/create/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ type createClient struct {
sesv2Svd *sesv2.Client
}

func (c createClient) GetItem(ctx context.Context, params *dynamodb.GetItemInput, optFns ...func(*dynamodb.Options)) (*dynamodb.GetItemOutput, error) {
return c.dynamodbSvc.GetItem(ctx, params, optFns...)
}

func (c createClient) PutItem(ctx context.Context, params *dynamodb.PutItemInput, optFns ...func(*dynamodb.Options)) (*dynamodb.PutItemOutput, error) {
return c.dynamodbSvc.PutItem(ctx, params, optFns...)
}
Expand All @@ -38,6 +42,10 @@ func (c createClient) SendEmail(ctx context.Context, params *sesv2.SendEmailInpu
return c.sesv2Svd.SendEmail(ctx, params, optFns...)
}

func (c createClient) TransactWriteItems(ctx context.Context, params *dynamodb.TransactWriteItemsInput, optFns ...func(*dynamodb.Options)) (*dynamodb.TransactWriteItemsOutput, error) {
return c.dynamodbSvc.TransactWriteItems(ctx, params, optFns...)
}

func newCreateClient(cfg aws.Config) createClient {
return createClient{
dynamodbSvc: dynamodb.NewFromConfig(cfg),
Expand Down
14 changes: 9 additions & 5 deletions api/emails/save/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,25 +23,29 @@ var region = os.Getenv("REGION")

type saveClient struct {
dynamodbSvc *dynamodb.Client
sesv2Svd *sesv2.Client
sesv2Svc *sesv2.Client
}

func (c saveClient) GetItem(ctx context.Context, params *dynamodb.GetItemInput, optFns ...func(*dynamodb.Options)) (*dynamodb.GetItemOutput, error) {
return c.dynamodbSvc.GetItem(ctx, params, optFns...)
}

func (c saveClient) PutItem(ctx context.Context, params *dynamodb.PutItemInput, optFns ...func(*dynamodb.Options)) (*dynamodb.PutItemOutput, error) {
return c.dynamodbSvc.PutItem(ctx, params, optFns...)
}

func (c saveClient) BatchWriteItem(ctx context.Context, params *dynamodb.BatchWriteItemInput, optFns ...func(*dynamodb.Options)) (*dynamodb.BatchWriteItemOutput, error) {
return c.dynamodbSvc.BatchWriteItem(ctx, params, optFns...)
func (c saveClient) TransactWriteItems(ctx context.Context, params *dynamodb.TransactWriteItemsInput, optFns ...func(*dynamodb.Options)) (*dynamodb.TransactWriteItemsOutput, error) {
return c.dynamodbSvc.TransactWriteItems(ctx, params, optFns...)
}

func (c saveClient) SendEmail(ctx context.Context, params *sesv2.SendEmailInput, optFns ...func(*sesv2.Options)) (*sesv2.SendEmailOutput, error) {
return c.sesv2Svd.SendEmail(ctx, params, optFns...)
return c.sesv2Svc.SendEmail(ctx, params, optFns...)
}

func newSaveClient(cfg aws.Config) saveClient {
return saveClient{
dynamodbSvc: dynamodb.NewFromConfig(cfg),
sesv2Svd: sesv2.NewFromConfig(cfg),
sesv2Svc: sesv2.NewFromConfig(cfg),
}
}

Expand Down
23 changes: 14 additions & 9 deletions api/emails/send/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,22 +22,27 @@ import (
var region = os.Getenv("REGION")

type sendClient struct {
cfg aws.Config
dynamodbSvc *dynamodb.Client
sesv2Svc *sesv2.Client
}

func (c sendClient) GetItem(ctx context.Context, params *dynamodb.GetItemInput, optFns ...func(*dynamodb.Options)) (*dynamodb.GetItemOutput, error) {
svc := dynamodb.NewFromConfig(c.cfg)
return svc.GetItem(ctx, params, optFns...)
return c.dynamodbSvc.GetItem(ctx, params, optFns...)
}

func (c sendClient) BatchWriteItem(ctx context.Context, params *dynamodb.BatchWriteItemInput, optFns ...func(*dynamodb.Options)) (*dynamodb.BatchWriteItemOutput, error) {
svc := dynamodb.NewFromConfig(c.cfg)
return svc.BatchWriteItem(ctx, params, optFns...)
func (c sendClient) TransactWriteItems(ctx context.Context, params *dynamodb.TransactWriteItemsInput, optFns ...func(*dynamodb.Options)) (*dynamodb.TransactWriteItemsOutput, error) {
return c.dynamodbSvc.TransactWriteItems(ctx, params, optFns...)
}

func (c sendClient) SendEmail(ctx context.Context, params *sesv2.SendEmailInput, optFns ...func(*sesv2.Options)) (*sesv2.SendEmailOutput, error) {
svc := sesv2.NewFromConfig(c.cfg)
return svc.SendEmail(ctx, params, optFns...)
return c.sesv2Svc.SendEmail(ctx, params, optFns...)
}

func newSendClient(cfg aws.Config) sendClient {
return sendClient{
dynamodbSvc: dynamodb.NewFromConfig(cfg),
sesv2Svc: sesv2.NewFromConfig(cfg),
}
}

func handler(ctx context.Context, req events.APIGatewayV2HTTPRequest) (apiutil.Response, error) {
Expand All @@ -55,7 +60,7 @@ func handler(ctx context.Context, req events.APIGatewayV2HTTPRequest) (apiutil.R
return apiutil.NewErrorResponse(http.StatusInternalServerError, "internal error"), nil
}

client := sendClient{cfg: cfg}
client := newSendClient(cfg)
result, err := email.Send(ctx, client, messageID)
if err != nil {
if err == email.ErrTooManyRequests {
Expand Down
21 changes: 18 additions & 3 deletions internal/email/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,19 @@ type GetItemContentAPI interface {
GetObject(ctx context.Context, params *s3.GetObjectInput, optFns ...func(*s3.Options)) (*s3.GetObjectOutput, error)
}

// DeleteItemAPI defines set of API required to delete an email
// DeleteItemAPI defines DynamoDB DeleteItem and S3 DeleteObject API
type DeleteItemAPI interface {
DeleteItem(ctx context.Context, params *dynamodb.DeleteItemInput, optFns ...func(*dynamodb.Options)) (*dynamodb.DeleteItemOutput, error)
DeleteObject(ctx context.Context, params *s3.DeleteObjectInput, optFns ...func(*s3.Options)) (*s3.DeleteObjectOutput, error)
}

// DeleteEmailAPI defines set of API required to delete an email
type DeleteEmailAPI interface {
DeleteItemAPI
GetItemAPI // to check if it's part of a thread
// TODO: delete from thread
}

// UpdateItemAPI defines set of API required to update an email
type UpdateItemAPI interface {
UpdateItem(ctx context.Context, params *dynamodb.UpdateItemInput, optFns ...func(*dynamodb.Options)) (*dynamodb.UpdateItemOutput, error)
Expand All @@ -39,14 +46,22 @@ type PutItemAPI interface {
PutItem(ctx context.Context, params *dynamodb.PutItemInput, optFns ...func(*dynamodb.Options)) (*dynamodb.PutItemOutput, error)
}

// SendEmailAPI defines et of API required to send a email
// SendEmailAPI defines set of API required to send a email
type SendEmailAPI interface {
BatchWriteItem(ctx context.Context, params *dynamodb.BatchWriteItemInput, optFns ...func(*dynamodb.Options)) (*dynamodb.BatchWriteItemOutput, error)
TransactWriteItemsAPI
SendEmail(ctx context.Context, params *sesv2.SendEmailInput, optFns ...func(*sesv2.Options)) (*sesv2.SendEmailOutput, error)
}

// CreateAndSendEmailAPI defines set of API required to create an email and send it
type CreateAndSendEmailAPI interface {
GetItemAPI
PutItemAPI
SendEmailAPI
}

// SaveAndSendEmailAPI defines set of API required to save an email and send it
type SaveAndSendEmailAPI interface {
GetItemAPI
PutItemAPI
SendEmailAPI
}
Expand Down
30 changes: 21 additions & 9 deletions internal/email/common_email.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,18 @@ package email
import "github.com/aws/aws-sdk-go-v2/service/dynamodb/types"

type EmailInput struct {
MessageID string `json:"messageID"`
Subject string `json:"subject"`
From []string `json:"from"`
To []string `json:"to"`
Cc []string `json:"cc"`
Bcc []string `json:"bcc"`
ReplyTo []string `json:"replyTo"`
Text string `json:"text"`
HTML string `json:"html"`
MessageID string `json:"messageID"`
Subject string `json:"subject"`
From []string `json:"from"`
To []string `json:"to"`
Cc []string `json:"cc"`
Bcc []string `json:"bcc"`
ReplyTo []string `json:"replyTo"`
InReplyTo string
References string
Text string `json:"text"`
HTML string `json:"html"`
ThreadID string `json:"threadID,omitempty"`
}

// GenerateAttributes generates DynamoDB AttributeValues
Expand Down Expand Up @@ -40,6 +43,15 @@ func (e EmailInput) GenerateAttributes(typeYearMonth, dateTime string) map[strin
if e.ReplyTo != nil && len(e.ReplyTo) > 0 {
item["ReplyTo"] = &types.AttributeValueMemberSS{Value: e.ReplyTo}
}
if e.InReplyTo != "" {
item["InReplyTo"] = &types.AttributeValueMemberS{Value: e.InReplyTo}
}
if e.References != "" {
item["References"] = &types.AttributeValueMemberS{Value: e.References}
}
if e.ThreadID != "" {
item["ThreadID"] = &types.AttributeValueMemberS{Value: e.ThreadID}
}

return item
}
Loading

0 comments on commit 9c8ac0f

Please sign in to comment.