-
Notifications
You must be signed in to change notification settings - Fork 3
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
Enqueue FFIS email downloads #53
Closed
jakekreider
wants to merge
10
commits into
usdigitalresponse:main
from
jakekreider:enqueue-ffis-email
Closed
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
200e2d1
Enqueue FFIS email download - WIP
jakekreider a6135a1
TF for SQS, logic to read from S3 and write to SQS, WIP
jakekreider dbc0c4b
Basic working SQS publish finally, still WIP
jakekreider e5ad6bc
WIP
jakekreider 1f125ac
Restrict SQS IAM policy
jakekreider c5c8afe
Merge remote-tracking branch 'origin/main' into enqueue-ffis-email
jakekreider 6f7fbdf
Remove unused var, add comment
jakekreider 05f1488
Incorporating feedback from PR
jakekreider 61ba83b
Merge remote-tracking branch 'origin/main' into enqueue-ffis-email
jakekreider d736ec4
Apply suggestions from code review
jakekreider File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -51,3 +51,6 @@ cover.html | |
# Environment variable files | ||
.env | ||
.envrc | ||
|
||
# Localstack | ||
volume/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
MIME-Version: 1.0 | ||
Date: Sat, 22 Apr 2023 14:55:26 -0500 | ||
Message-ID: <CAJZ0yfPKN1Q@mail.gmail.com> | ||
Subject: | ||
From: FFIS <ffis@ffis.org> | ||
To: Team <team@usdigitalresponse.org> | ||
Content-Type: multipart/alternative; boundary="0000000000008e64aa05f9f22750" | ||
|
||
--0000000000008e64aa05f9f22750 | ||
Content-Type: text/plain; charset="UTF-8" | ||
|
||
Click here to download competitive grant update | ||
<https://mcusercontent.com/123456/files/file-01.xlsx> | ||
|
||
-FFIS | ||
|
||
--0000000000008e64aa05f9f22750 | ||
Content-Type: text/html; charset="UTF-8" | ||
|
||
<div dir="ltr"><a href="https://mcusercontent.com/123456/files/file-01.xlsx">Click here to download competitive grant update</a><br clear="all"><div><div dir="ltr" class="gmail_signature" data-smartmail="gmail_signature"><br>-FFIS</div></div></div> | ||
|
||
--0000000000008e64aa05f9f22750-- |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
MIME-Version: 1.0 | ||
Date: Sat, 22 Apr 2023 14:55:26 -0500 | ||
Message-ID: <CAJZ0yfPKN1Q@mail.gmail.com> | ||
Subject: | ||
From: FFIS <ffis@ffis.org> | ||
To: Team <team@usdigitalresponse.org> | ||
Content-Type: multipart/alternative; boundary="0000000000008e64aa05f9f22750" | ||
|
||
--0000000000008e64aa05f9f22750 | ||
Content-Type: text/plain; charset="UTF-8" | ||
|
||
Click here to download competitive grant update | ||
<https://usdigitalresponse.org> | ||
|
||
-FFIS | ||
|
||
--0000000000008e64aa05f9f22750 | ||
Content-Type: text/html; charset="UTF-8" | ||
|
||
<div dir="ltr"><a href="https://mcusercontent.com/123456/files/file-01.xlsx">Click here to download competitive grant update</a><br clear="all"><div><div dir="ltr" class="gmail_signature" data-smartmail="gmail_signature"><br>-FFIS</div></div></div> | ||
|
||
--0000000000008e64aa05f9f22750-- |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
MIME-Version: 1.0 | ||
Date: Sat, 22 Apr 2023 14:55:26 -0500 | ||
Message-ID: <CAJZ0yfPKN1Q@mail.gmail.com> | ||
Subject: | ||
From: FFIS <ffis@ffis.org> | ||
To: Team <team@usdigitalresponse.org> | ||
Content-Type: multipart/alternative; boundary="0000000000008e64aa05f9f22750" | ||
|
||
--0000000000008e64aa05f9f22750 | ||
Content-Type: text/plain; charset="UTF-8" | ||
|
||
Click here to download competitive grant update | ||
<https://mcusercontent.com/123456/files/file-01.xlsx> | ||
|
||
Click here to download competitive grant update | ||
<https://mcusercontent.com/123456/files/file-01.xlsx> | ||
|
||
-FFIS | ||
|
||
--0000000000008e64aa05f9f22750 | ||
Content-Type: text/html; charset="UTF-8" | ||
|
||
<div dir="ltr"><a href="https://mcusercontent.com/123456/files/file-01.xlsx">Click here to download competitive grant update</a><br clear="all"><div><div dir="ltr" class="gmail_signature" data-smartmail="gmail_signature"><br>-FFIS</div></div></div> | ||
|
||
--0000000000008e64aa05f9f22750-- |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,135 @@ | ||
package main | ||
|
||
import ( | ||
"bytes" | ||
"context" | ||
"fmt" | ||
"io" | ||
"mime" | ||
"mime/multipart" | ||
"net/mail" | ||
"regexp" | ||
"strings" | ||
|
||
"github.com/aws/aws-lambda-go/events" | ||
"github.com/aws/aws-sdk-go-v2/aws" | ||
"github.com/aws/aws-sdk-go-v2/service/s3" | ||
"github.com/aws/aws-sdk-go-v2/service/sqs" | ||
"github.com/usdigitalresponse/grants-ingest/internal/log" | ||
) | ||
|
||
type SQSAPI interface { | ||
SendMessage(ctx context.Context, | ||
params *sqs.SendMessageInput, | ||
optFns ...func(*sqs.Options)) (*sqs.SendMessageOutput, error) | ||
} | ||
|
||
type S3API interface { | ||
GetObject(ctx context.Context, | ||
params *s3.GetObjectInput, | ||
optFns ...func(*s3.Options)) (*s3.GetObjectOutput, error) | ||
} | ||
|
||
func handleS3Event(ctx context.Context, s3Event events.S3Event, s3client S3API, sqsclient SQSAPI) error { | ||
emailBody, err := getEmailFromS3Event(s3client, s3Event, ctx) | ||
if err != nil { | ||
return log.Errorf("Error reading email from S3", err) | ||
} | ||
defer emailBody.Close() | ||
plaintext, err := plaintextMIMEFromEmailBody(emailBody) | ||
if err != nil { | ||
return log.Errorf("Missing plaintext mime part from email body", err) | ||
} | ||
// Parse the URL from the email body | ||
url, err := parseURLFromEmailBody(plaintext) | ||
if err != nil { | ||
return log.Errorf("Download URL could not be located in email plaintext", err) | ||
} | ||
|
||
log.Info(logger, "Parsed URL from email body", "url", url) | ||
|
||
// Enqueue the URL for download | ||
err = enqueueURLForDownload(url, sqsclient, ctx) | ||
if err != nil { | ||
return log.Errorf("Failed to enqueue parsed URL", err) | ||
} | ||
|
||
return nil | ||
} | ||
|
||
func plaintextMIMEFromEmailBody(email io.Reader) (string, error) { | ||
msg, err := mail.ReadMessage(email) | ||
if err != nil { | ||
return "", err | ||
} | ||
mediaType, params, err := mime.ParseMediaType(msg.Header.Get("Content-Type")) | ||
if mediaType != "multipart/alternative" { | ||
return "", fmt.Errorf("expected multipart/alternative, got %s", mediaType) | ||
} | ||
if err != nil { | ||
return "", err | ||
} | ||
mr := multipart.NewReader(msg.Body, params["boundary"]) | ||
for { | ||
p, err := mr.NextPart() | ||
if err == io.EOF { | ||
break | ||
} | ||
if err != nil { | ||
return "", err | ||
} | ||
if strings.HasPrefix(p.Header.Get("Content-Type"), "text/plain") { | ||
buf := new(bytes.Buffer) | ||
_, err := buf.ReadFrom(p) | ||
if err != nil { | ||
return "", err | ||
} | ||
return buf.String(), nil | ||
} | ||
} | ||
|
||
return "", fmt.Errorf("no text/plain part found") | ||
} | ||
|
||
func parseURLFromEmailBody(plaintext string) (string, error) { | ||
patternRegex := regexp.MustCompile(env.URLPattern) | ||
matches := patternRegex.FindStringSubmatch(plaintext) | ||
if len(matches) == 0 { | ||
return "", fmt.Errorf("no matches found") | ||
} | ||
if len(matches) > 1 { | ||
return "", fmt.Errorf("multiple matches found") | ||
} | ||
return matches[0], nil | ||
} | ||
|
||
func enqueueURLForDownload(ctx context.Context, url string, client SQSAPI) error { | ||
message := sqs.SendMessageInput{ | ||
MessageBody: aws.String(url), | ||
QueueUrl: aws.String(env.DestinationQueueURL), | ||
} | ||
output, err := client.SendMessage(ctx, &message) | ||
|
||
if err != nil { | ||
return err | ||
} | ||
log.Info(logger, "Sent SQS message", "messageId", *output.MessageId) | ||
return nil | ||
} | ||
|
||
func getEmailFromS3Event(s3client S3API, s3Event events.S3Event, ctx context.Context) (io.ReadCloser, error) { | ||
bucket := s3Event.Records[0].S3.Bucket.Name | ||
uploadedFile := s3Event.Records[0].S3.Object.Key | ||
logger := log.With(logger, "bucket", bucket, "key", key) | ||
log.Debug(logger, "Reading from bucket") | ||
// Get the email body | ||
resp, err := s3client.GetObject(ctx, &s3.GetObjectInput{ | ||
Bucket: aws.String(bucket), | ||
Key: aws.String(uploadedFile), | ||
}) | ||
if err != nil { | ||
return nil, err | ||
} | ||
log.Info(logger, "Retrieved new email file") | ||
return resp.Body, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
package main | ||
|
||
import ( | ||
"bytes" | ||
"context" | ||
"io" | ||
"os" | ||
"testing" | ||
|
||
"github.com/aws/aws-lambda-go/events" | ||
"github.com/aws/aws-sdk-go-v2/aws" | ||
"github.com/aws/aws-sdk-go-v2/service/s3" | ||
"github.com/aws/aws-sdk-go-v2/service/sqs" | ||
"github.com/go-kit/log" | ||
) | ||
|
||
type MockS3 struct { | ||
content string | ||
} | ||
|
||
func (mocks3 *MockS3) GetObject(ctx context.Context, | ||
params *s3.GetObjectInput, | ||
optFns ...func(*s3.Options)) (*s3.GetObjectOutput, error) { | ||
contentBytes := []byte(mocks3.content) | ||
return &s3.GetObjectOutput{ | ||
Body: io.NopCloser(bytes.NewReader(contentBytes)), | ||
ContentLength: int64(len(contentBytes)), | ||
}, nil | ||
} | ||
|
||
type MockSQS struct { | ||
message *string | ||
} | ||
|
||
func (mocksqs *MockSQS) SendMessage(ctx context.Context, | ||
params *sqs.SendMessageInput, | ||
optFns ...func(*sqs.Options)) (*sqs.SendMessageOutput, error) { | ||
mocksqs.message = params.MessageBody | ||
output := &sqs.SendMessageOutput{ | ||
MessageId: aws.String("123456789012345678901234567890"), | ||
} | ||
return output, nil | ||
} | ||
|
||
func TestHandleS3Event(t *testing.T) { | ||
logger = log.NewNopLogger() | ||
env.URLPattern = "https://mcusercontent.com/.+\\.xlsx" | ||
var tests = []struct { | ||
emailFixture, expectedURL string | ||
}{ | ||
{"good.eml", "https://mcusercontent.com/123456/files/file-01.xlsx"}, | ||
{"missing.eml", ""}, | ||
{"multiple.eml", ""}, | ||
} | ||
|
||
for _, test := range tests { | ||
t.Run(test.emailFixture, func(t *testing.T) { | ||
content, err := os.ReadFile("./fixtures/" + test.emailFixture) | ||
if err != nil { | ||
t.Errorf("Error opening file: %v", err) | ||
} | ||
mocks3, mocksqs := getMockClients() | ||
mocks3.content = string(content) | ||
ctx := context.Background() | ||
s3Event := events.S3Event{ | ||
Records: []events.S3EventRecord{ | ||
{ | ||
S3: events.S3Entity{ | ||
Bucket: events.S3Bucket{ | ||
Name: "test-bucket", | ||
}, | ||
Object: events.S3Object{ | ||
Key: "test/email/file.eml", | ||
}, | ||
}, | ||
}, | ||
}, | ||
} | ||
|
||
err = handleS3Event(ctx, s3Event, mocks3, mocksqs) | ||
|
||
if test.expectedURL != "" { | ||
if err != nil { | ||
t.Errorf("Error parsing S3 event: %v", err) | ||
} | ||
if *mocksqs.message != test.expectedURL { | ||
t.Errorf("Expected message %v, got %v", test.expectedURL, mocksqs.message) | ||
} | ||
} else { | ||
// parse expected bad message | ||
if mocksqs.message == nil && test.expectedURL != "" { | ||
t.Errorf("Expected message for %s to be empty", test.emailFixture) | ||
} | ||
} | ||
}) | ||
} | ||
} | ||
|
||
func getMockClients() (*MockS3, *MockSQS) { | ||
mocks3 := MockS3{content: "test"} | ||
mocksqs := MockSQS{} | ||
return &mocks3, &mocksqs | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Despite this test cases's fixture looking correct as far as I can tell, I don't see any coverage on the
if len(matches) > 1
branch ofparseURLFromEmailBody()
. Would you be able to look into that?Hint: if you have Taskfile installed, you can run
task test
to generate an HTML coverage report.