1

I'm trying to send an email via AWS pinpoint containing attachments. To send attachments with an email, you must use the 'RAW' email content. The only documentation I can find about this is here: https://docs.aws.amazon.com/pinpoint-email/latest/APIReference/API_RawMessage.html, but it is missing quite a few things (like, what are the required headers?)

When I send an email using the 'simple' content, it works fine:

emailInput := &pinpointemail.SendEmailInput{
    Destination: &pinpointemail.Destination{
        ToAddresses: []*string{&address},
    },
    FromEmailAddress: &sender,
    Content: &pinpointemail.EmailContent{
                Simple: &pinpointemail.Message{
                    Body: &pinpointemail.Body{
                        Html: &pinpointemail.Content{
                            Charset: &charset,
                            Data:    &emailHTML,
                        },
                        Text: &pinpointemail.Content{
                            Charset: &charset,
                            Data:    &emailText,
                        },
                    },
                    Subject: &pinpointemail.Content{
                        Charset: &charset,
                        Data:    &emailSubject,
                    },
                },
}

Since I want to add attachments, I have to use the 'RAW' content type. I have written a function which generates the email content, based on: https://gist.github.com/douglasmakey/90753ecf37ac10c25873825097f46300:

func generateRawEmailContent(subject, to, from, HTMLBody string, attachments *[]EmailAttachment) []byte {
    buf := bytes.NewBuffer(nil)
    buf.WriteString(fmt.Sprintf("Subject: %s\n", subject))
    buf.WriteString(fmt.Sprintf("To: %s\n", to))
    buf.WriteString(fmt.Sprintf("From: %s\n\n", from))

    buf.WriteString("MIME-version: 1.0;\nContent-Type: text/html; charset=\"UTF-8\";\n\n")
    buf.WriteString(HTMLBody)

    writer := multipart.NewWriter(buf)
    boundary := writer.Boundary()

    buf.WriteString(fmt.Sprintf("Content-Type: multipart/mixed; boundary=%s\n", boundary))
    buf.WriteString(fmt.Sprintf("--%s\n", boundary))

    for _, attachment := range *attachments {
        buf.WriteString(fmt.Sprintf("\n\n--%s\n", boundary))
        buf.WriteString(fmt.Sprintf("Content-Type: %s\n", http.DetectContentType(attachment.Data)))
        buf.WriteString("Content-Transfer-Encoding: base64\n")
        buf.WriteString(fmt.Sprintf("Content-Disposition: attachment; filename=%s\n", attachment.FileName))

        b := make([]byte, base64.StdEncoding.EncodedLen(len(attachment.Data)))
        base64.StdEncoding.Encode(b, attachment.Data)
        buf.Write(b)
        buf.WriteString(fmt.Sprintf("\n--%s", boundary))
    }

    buf.WriteString("--")

    log.Println(string(buf.Bytes()))

    return buf.Bytes()
}

This generates the following (emails changed):

Subject: Welcome \nTo: xxxxx@gmail.com\nFrom: xxxxx@gmail.com\n\nMIME-version: 1.0;\nContent-Type: text/html; charset=\"UTF-8\";\n\n\u003ch1\u003eHello ,\u003c/h1\u003e\u003cp\u003eYou now have an account.\u003c/p\u003e\nContent-Type: multipart/mixed; boundary=8f6b2cc498b79f5a99550b930ba1ecab1fc1ee2d3425a0a69ab67b83b647\n--8f6b2cc498b79f5a99550b930ba1ecab1fc1ee2d3425a0a69ab67b83b647\n\n\n--8f6b2cc498b79f5a99550b930ba1ecab1fc1ee2d3425a0a69ab67b83b647\nContent-Type: text/plain; charset=utf-8\nContent-Transfer-Encoding: base64\nContent-Disposition: attachment; filename=test.json\newogICJ0ZXN0IjogdHJ1ZQp9\n--8f6b2cc498b79f5a99550b930ba1ecab1fc1ee2d3425a0a69ab67b83b647--

I then construct the email as follows:

&pinpointemail.SendEmailInput{
    Destination: &pinpointemail.Destination{
        ToAddresses: []*string{&address},
    },
    FromEmailAddress: &sender,
    Content: &pinpointemail.EmailContent{
                Raw: &pinpointemail.RawMessage{
                    Data: generateRawEmailContent(emailSubject, address, sender, emailHTML, emailAttachments),
                },
}

When sending this email via the github.com/aws/aws-sdk-go/service/pinpoint package, I get a 403 returned, and I have no idea why. A 403 means that the resource I'm trying to access is forbidden, but I don't see how that is relevant here? Also, there is no documentation about a 403 even being a possible response. Any help would be greatly appreciated!

I have also tried using libraries, like for instance the gomail-v2 library as follows:

m := gomail.NewMessage()
    m.SetHeader("From", from)
    m.SetHeader("To", to)
    m.SetHeader("Subject", subject)
    m.SetBody("text/plain", textBody)
    m.AddAlternative("text/html", HTMLBody)
    m.Attach("foo.txt", gomail.SetCopyFunc(func(w io.Writer) error {
        _, err := w.Write((*attachments)[0].Data)
        return err
    }))

    buf := bytes.NewBuffer(make([]byte, 0, 2048))
    _, werr := m.WriteTo(buf)
    if werr != nil {
        return nil, common.NewStackError(werr)
    }

But that still gives me a 403 error.

Thijs van der Heijden
  • 1,147
  • 1
  • 10
  • 25
  • I'm not a Go person, but the nesting seems wrong. The structure should be a multipart which contains two body parts which contain the HTML part and the attachment. Can you add a (minimized) version of the resulting MIME structure to the question? – tripleee Apr 11 '22 at 09:18
  • @tripleee added the output, let me know if it needs to be cleaned. – Thijs van der Heijden Apr 11 '22 at 09:23
  • 1
    Yeah, you have them inside out. It should look like `From: foo\nTo: bar\nMIME-Version: 1.0\nContent-type: multipart/related; boundary="eeeek"\n\n--eeeek\nContent-type: text/html; ... \n--eeek\nContent-type: application/octet-stream; ... \n\n--eeek--`. Notice how the first `Content-type:` is part of the main headers, where you have a spurious newline which pushes it all into the body, and how there should be a MIME boundary before every body part. – tripleee Apr 11 '22 at 09:29
  • @tripleee thanks for the help, this sadly does not fix the 403 but good to know my email format wasn't even correct! – Thijs van der Heijden Apr 11 '22 at 09:35
  • I'm vaguely guessing the 403 can also happen when you submit an invalid email message, but that's just speculation. – tripleee Apr 11 '22 at 09:41

2 Answers2

1

I'm not a Go person, so this is just a brutal attempt to shuffle around code lines to hopefully produce a valid MIME structure.

func generateRawEmailContent(subject, to, from, HTMLBody string, attachments *[]EmailAttachment) []byte {
    buf := bytes.NewBuffer(nil)
    // Creating headers by gluing together strings is precarious.
    // I'm sure there must be a better way.
    buf.WriteString(fmt.Sprintf("Subject: %s\n", subject))
    buf.WriteString(fmt.Sprintf("To: %s\n", to))
    // Remove spurious newline
    buf.WriteString(fmt.Sprintf("From: %s\n", from))

    writer := multipart.NewWriter(buf)
    boundary := writer.Boundary()

    buf.WriteString(fmt.Sprintf("MIME-Version: 1.0\n", boundary))
    buf.WriteString(fmt.Sprintf("Content-Type: multipart/mixed; boundary=%s\n", boundary))
    // End of headers
    buf.WriteString("\n")

    buf.WriteString(fmt.Sprintf("--%s\n", boundary))

    buf.WriteString("Content-Type: text/html; charset=\"UTF-8\";\n\n")
    buf.WriteString(HTMLBody)

    for _, attachment := range *attachments {
        buf.WriteString(fmt.Sprintf("\n\n--%s\n", boundary))
        buf.WriteString(fmt.Sprintf("Content-Type: %s\n", http.DetectContentType(attachment.Data)))
        buf.WriteString("Content-Transfer-Encoding: base64\n")
        buf.WriteString(fmt.Sprintf("Content-Disposition: attachment; filename=%s\n", attachment.FileName))

        b := make([]byte, base64.StdEncoding.EncodedLen(len(attachment.Data)))
        base64.StdEncoding.Encode(b, attachment.Data)
        buf.Write(b)
        // Don't add a second boundary here
        buf.WriteString("\n")
    }

    // Final terminating boundary, notice -- after
    buf.WriteString(fmt.Sprintf("\n--%s--\n", boundary))

    log.Println(string(buf.Bytes()))

    return buf.Bytes()
}

The resulting output should look something like

Subject: subject
To: recipient <victim@example.org>
From: me <sender@example.net>
MIME-Version: 1.0
Content-Type: multipart/mixed; boundary=foobar

--foobar
Content-Type: text/html; charset="UTF-8"

<h1>Tremble, victim</h1>
<p>We don't send <tt>text/plain</tt> because we
hate our users.</p>

--foobar
Content-Type: application/octet-stream
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename=skull_crossbones.jpg

YmluYXJ5ZGF0YQ==
--foobar--
tripleee
  • 175,061
  • 34
  • 275
  • 318
1

Okay, found the issue. Turns out that this 403 error has nothing to do with my code, but rather with IAM permissions in AWS. Apparently an IAM permission has to be turned on to enable RAW email content.

Thijs van der Heijden
  • 1,147
  • 1
  • 10
  • 25