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

fix: Add missing err checks to inbound.Parse #461

Open
wants to merge 4 commits into
base: main
Choose a base branch
from

Conversation

m-terel
Copy link

@m-terel m-terel commented Jun 9, 2022

Fixes

Added missing err checks that cause panic for some emails.

Checklist

  • I acknowledge that all my contributions will be made under the project's license
  • I have made a material change to the repo (functionality, testing, spelling, grammar)
  • I have read the Contribution Guidelines and my PR follows them
  • I have titled the PR appropriately
  • I have updated my branch with the main branch
  • I have added tests that prove my fix is effective or that my feature works
  • I have added the necessary documentation about the functionality in the appropriate .md file
  • I have added inline documentation to the code I modified

@christopher-besch
Copy link

I ran into the exact same problem. This PR fixes it. I'm urging the maintainers to please merge this PR.

@darioleanbit
Copy link

Hi all,

Doing a few tests, I noticed that yes, this PR fixes the panics, but leads to leave out parts of the email, I had better luck with this version of the parseRawEmail function:

func (email *ParsedEmail) parseRawEmail(rawEmail string) error {
	sections := strings.SplitN(rawEmail, "\n\n", 2)
	email.parseHeaders(sections[0])
	raw, err := parseMultipart(strings.NewReader(sections[1]), email.Headers["Content-Type"])
	if err != nil {
		return err
	}

	// if Content-Type is not multipart just set the whole email
	if raw == nil {
		if len(sections) < 2 {
			return nil
		}

		wholeEmail := sections[1]
		// decode if needed
		if email.Headers["Content-Transfer-Encoding"] == "quoted-printable" {
			decoded, err := io.ReadAll(quotedprintable.NewReader(strings.NewReader(wholeEmail)))
			if err != nil {
				return err
			}
			wholeEmail = string(decoded)
		}

		email.Body[email.Headers["Content-Type"]] = wholeEmail
		return nil
	}

	for {
		emailPart, err := raw.NextPart()
		// Check for both io.EOF and the wrapped multipart: NextPart: EOF
		if err == io.EOF || (err != nil && err.Error() == "multipart: NextPart: EOF") {
			break
		}
		if err != nil {
			return err
		}
		rawEmailBody, err := parseMultipart(emailPart, emailPart.Header.Get("Content-Type"))
		if err != nil {
			return err
		}
		if rawEmailBody != nil {
			for {
				emailBodyPart, err := rawEmailBody.NextPart()
				// Check for both io.EOF and the wrapped multipart: NextPart: EOF
				if err == io.EOF || (err != nil && err.Error() == "multipart: NextPart: EOF") {
					break
				}
				if err != nil {
					return err
				}
				header := emailBodyPart.Header.Get("Content-Type")
				b, err := io.ReadAll(emailPart)
				if err != nil {
					return err
				}
				email.Body[header] = string(b)
			}

		} else if emailPart.FileName() != "" {
			b, err := io.ReadAll(emailPart)
			if err != nil {
				return err
			}
			email.Attachments[emailPart.FileName()] = b
		} else {
			header := emailPart.Header.Get("Content-Type")
			b, err := io.ReadAll(emailPart)
			if err != nil {
				return err
			}

			email.Body[header] = string(b)
		}
	}
	return nil
}

Basically, the diffence is that I added the (horrible, but errors.Unwrap() didn't work on that error) check (err != nil && err.Error() == "multipart: NextPart: EOF") along with the err == io.EOF one. In this way, I still catch the EOF, but I don't stop too early.

For the record, the wrapped error multipart: NextPart: EOF comes from here: multipart/multipart.go.

It's not clear to me why I can't just do

	if err == io.EOF || (err != nil && errors.Unwrap(err) == io.EOF) {
		break
	}

@darioleanbit
Copy link

If it can be useful, this is the same in patch format to highlight the changes:

diff --git a/helpers/inbound/inbound.go b/helpers/inbound/inbound.go
index fdda393..ab99188 100644
--- a/helpers/inbound/inbound.go
+++ b/helpers/inbound/inbound.go
@@ -4,7 +4,6 @@ import (
 	"encoding/json"
 	"fmt"
 	"io"
-	"io/ioutil"
 	"mime"
 	"mime/multipart"
 	"mime/quotedprintable"
@@ -178,7 +177,7 @@ func (email *ParsedEmail) parseRawEmail(rawEmail string) error {
 		wholeEmail := sections[1]
 		// decode if needed
 		if email.Headers["Content-Transfer-Encoding"] == "quoted-printable" {
-			decoded, err := ioutil.ReadAll(quotedprintable.NewReader(strings.NewReader(wholeEmail)))
+			decoded, err := io.ReadAll(quotedprintable.NewReader(strings.NewReader(wholeEmail)))
 			if err != nil {
 				return err
 			}
@@ -191,8 +190,12 @@ func (email *ParsedEmail) parseRawEmail(rawEmail string) error {
 
 	for {
 		emailPart, err := raw.NextPart()
-		if err == io.EOF {
-			return nil
+		// Check for both io.EOF and the wrapped multipart: NextPart: EOF
+		if err == io.EOF || (err != nil && err.Error() == "multipart: NextPart: EOF") {
+			break
+		}
+		if err != nil {
+			return err
 		}
 		rawEmailBody, err := parseMultipart(emailPart, emailPart.Header.Get("Content-Type"))
 		if err != nil {
@@ -201,9 +204,13 @@ func (email *ParsedEmail) parseRawEmail(rawEmail string) error {
 		if rawEmailBody != nil {
 			for {
 				emailBodyPart, err := rawEmailBody.NextPart()
-				if err == io.EOF {
+				// Check for both io.EOF and the wrapped multipart: NextPart: EOF
+				if err == io.EOF || (err != nil && err.Error() == "multipart: NextPart: EOF") {
 					break
 				}
+				if err != nil {
+					return err
+				}
 				header := emailBodyPart.Header.Get("Content-Type")
 				b, err := io.ReadAll(emailPart)
 				if err != nil {
@@ -228,6 +235,7 @@ func (email *ParsedEmail) parseRawEmail(rawEmail string) error {
 			email.Body[header] = string(b)
 		}
 	}
+	return nil
 }
 
 func parseMultipart(body io.Reader, contentType string) (*multipart.Reader, error) {

tadhunt added a commit to tadhunt/sendgrid-go that referenced this pull request Jun 7, 2023
@tadhunt
Copy link

tadhunt commented Jun 7, 2023

@darioleanbit's changes didn't fix the issue for me. Turns out that I'm getting inbound email requests with headers but an entirely empty body, so the crash is happening at the indexing ofsections[1] which is beyond the end of the array.

	sections := strings.SplitN(rawEmail, "\n\n", 2)
	email.parseHeaders(sections[0])
	raw, err := parseMultipart(strings.NewReader(sections[1]), email.Headers["Content-Type"])
	if err != nil {
		return err
	}

In my fork, I fixed this (on top of @darioleanbit's changes), and added a testcase to reproduce the crash and make sure it's fixed.

main...tadhunt:sendgrid-go:main

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants