使用Go通过AWS SES v2发送带附件的原始电子邮件

huangapple go评论116阅读模式
英文:

Sending a raw email with attachments in Go via AWS SES v2

问题

我正在尝试创建一个HTTP端点,用于处理来自网站的表单提交。

该表单具有以下字段:

  • 姓名
  • 电子邮件
  • 电话
  • 电子邮件正文(邮件正文的文本)
  • 照片(最多5张)

然后,我的端点将向site@mysite.co.uk发送一封带有附件照片的电子邮件,邮件正文可能如下所示:

> John (john@example.com) 说:
>
> 邮件正文...

我对Go还不熟悉,但我已经尝试了两个星期,但仍然没有成功。

我目前的代码如下:

  1. package aj
  2. import (
  3. "bytes"
  4. "encoding/base64"
  5. "fmt"
  6. "io/ioutil"
  7. "mime"
  8. "net/http"
  9. "net/mail"
  10. "net/textproto"
  11. "os"
  12. "github.com/aws/aws-sdk-go-v2/aws"
  13. "github.com/aws/aws-sdk-go-v2/service/sesv2"
  14. "github.com/aws/aws-sdk-go-v2/service/sesv2/types"
  15. "go.uber.org/zap"
  16. )
  17. const expectedContentType string = "multipart/form-data"
  18. const charset string = "UTF-8"
  19. func FormSubmissionHandler(logger *zap.Logger, emailSender EmailSender) http.Handler {
  20. return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  21. logger.Info("running the form submission handler...")
  22. // 获取目标电子邮件地址
  23. destinationEmail := os.Getenv("DESTINATION_EMAIL")
  24. // 获取电子邮件的主题行
  25. emailSubject := os.Getenv("EMAIL_SUBJECT")
  26. // 强制要求使用multipart/form-data内容类型
  27. contentType := r.Header.Get("content-type")
  28. mediatype, _, err := mime.ParseMediaType(contentType)
  29. if err != nil {
  30. logger.Error("解析MIME类型时出错", zap.Error(err))
  31. http.Error(w, err.Error(), http.StatusBadRequest)
  32. return
  33. }
  34. if mediatype != expectedContentType {
  35. logger.Error("不支持的内容类型", zap.Error(err))
  36. http.Error(w, fmt.Sprintf("API预期的内容类型为%v", expectedContentType), http.StatusUnsupportedMediaType)
  37. return
  38. }
  39. err = r.ParseMultipartForm(10 << 20)
  40. if err != nil {
  41. logger.Error("解析表单数据时出错", zap.Error(err))
  42. http.Error(w, "解析表单数据时出错", http.StatusBadRequest)
  43. return
  44. }
  45. name := r.MultipartForm.Value["name"]
  46. if len(name) == 0 {
  47. logger.Error("未设置姓名", zap.Error(err))
  48. http.Error(w, "API要求设置姓名", http.StatusBadRequest)
  49. return
  50. }
  51. email := r.MultipartForm.Value["email"]
  52. if len(email) == 0 {
  53. logger.Error("未设置电子邮件", zap.Error(err))
  54. http.Error(w, "API要求设置电子邮件", http.StatusBadRequest)
  55. return
  56. }
  57. phone := r.MultipartForm.Value["phone"]
  58. if len(phone) == 0 {
  59. logger.Error("未设置电话", zap.Error(err))
  60. http.Error(w, "API要求设置电话", http.StatusBadRequest)
  61. return
  62. }
  63. body := r.MultipartForm.Value["body"]
  64. if len(body) == 0 {
  65. logger.Error("未设置正文", zap.Error(err))
  66. http.Error(w, "API要求设置正文", http.StatusBadRequest)
  67. return
  68. }
  69. files := r.MultipartForm.File["photos"]
  70. if len(files) == 0 {
  71. logger.Error("未提交任何文件", zap.Error(err))
  72. http.Error(w, "API要求提交一个或多个文件", http.StatusBadRequest)
  73. return
  74. }
  75. emailService := NewEmailService()
  76. sendEmailInput := sesv2.SendEmailInput{}
  77. destination := &types.Destination{
  78. ToAddresses: []string{destinationEmail},
  79. }
  80. // 将附件添加到电子邮件中
  81. for _, file := range files {
  82. f, err := file.Open()
  83. if err != nil {
  84. http.Error(w, err.Error(), http.StatusInternalServerError)
  85. return
  86. }
  87. defer f.Close()
  88. // 不确定如何在此处获取带有附件的电子邮件
  89. }
  90. message := &types.RawMessage{
  91. Data: make([]byte, 0), // 这必须更改为原始消息的字节
  92. }
  93. content := &types.EmailContent{
  94. Raw: message,
  95. }
  96. sendEmailInput.Content = content
  97. sendEmailInput.Destination = destination
  98. sendEmailInput.FromEmailAddress = aws.String(email[0])
  99. err = emailService.SendEmail(logger, r.Context(), &sendEmailInput)
  100. if err != nil {
  101. logger.Error("发送电子邮件时出错", zap.Error(err))
  102. http.Error(w, "发送电子邮件时出错", http.StatusBadRequest)
  103. return
  104. }
  105. w.WriteHeader(http.StatusOK)
  106. })
  107. }

我的理解是(如果我理解错了,请纠正我),我必须构建一个类似于这个链接中的原始消息格式。假设我是正确的,我只是不知道如何在Go中实现这一点。

英文:

I am trying to create an HTTP endpoint that will handle a form submission from a website.

The form has the following fields:

  • Name
  • Email
  • Phone
  • Email body (text for the body of the email)
  • Photos (up to 5 of them)

My endpoint will then fire off an email to site@mysite.co.uk with the photos as attachments and the email body being something like:

> John (john@example.com) says:
>
> Email body ...

I am new to Go, but I've been trying to get this working for 2 weeks, and still haven't had any luck.

My code at the moment is:

  1. package aj
  2. import (
  3. &quot;bytes&quot;
  4. &quot;encoding/base64&quot;
  5. &quot;fmt&quot;
  6. &quot;io/ioutil&quot;
  7. &quot;mime&quot;
  8. &quot;net/http&quot;
  9. &quot;net/mail&quot;
  10. &quot;net/textproto&quot;
  11. &quot;os&quot;
  12. &quot;github.com/aws/aws-sdk-go-v2/aws&quot;
  13. &quot;github.com/aws/aws-sdk-go-v2/service/sesv2&quot;
  14. &quot;github.com/aws/aws-sdk-go-v2/service/sesv2/types&quot;
  15. &quot;go.uber.org/zap&quot;
  16. )
  17. const expectedContentType string = &quot;multipart/form-data&quot;
  18. const charset string = &quot;UTF-8&quot;
  19. func FormSubmissionHandler(logger *zap.Logger, emailSender EmailSender) http.Handler {
  20. return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  21. logger.Info(&quot;running the form submission handler...&quot;)
  22. // get the destination email address
  23. destinationEmail := os.Getenv(&quot;DESTINATION_EMAIL&quot;)
  24. // get the subject line of the email
  25. emailSubject := os.Getenv(&quot;EMAIL_SUBJECT&quot;)
  26. // enforce a multipart/form-data content-type
  27. contentType := r.Header.Get(&quot;content-type&quot;)
  28. mediatype, _, err := mime.ParseMediaType(contentType)
  29. if err != nil {
  30. logger.Error(&quot;error when parsing the mime type&quot;, zap.Error(err))
  31. http.Error(w, err.Error(), http.StatusBadRequest)
  32. return
  33. }
  34. if mediatype != expectedContentType {
  35. logger.Error(&quot;unsupported content-type&quot;, zap.Error(err))
  36. http.Error(w, fmt.Sprintf(&quot;api expects %v content-type&quot;, expectedContentType), http.StatusUnsupportedMediaType)
  37. return
  38. }
  39. err = r.ParseMultipartForm(10 &lt;&lt; 20)
  40. if err != nil {
  41. logger.Error(&quot;error parsing form data&quot;, zap.Error(err))
  42. http.Error(w, &quot;error parsing form data&quot;, http.StatusBadRequest)
  43. return
  44. }
  45. name := r.MultipartForm.Value[&quot;name&quot;]
  46. if len(name) == 0 {
  47. logger.Error(&quot;name not set&quot;, zap.Error(err))
  48. http.Error(w, &quot;api expects name to be set&quot;, http.StatusBadRequest)
  49. return
  50. }
  51. email := r.MultipartForm.Value[&quot;email&quot;]
  52. if len(email) == 0 {
  53. logger.Error(&quot;email not set&quot;, zap.Error(err))
  54. http.Error(w, &quot;api expects email to be set&quot;, http.StatusBadRequest)
  55. return
  56. }
  57. phone := r.MultipartForm.Value[&quot;phone&quot;]
  58. if len(phone) == 0 {
  59. logger.Error(&quot;phone not set&quot;, zap.Error(err))
  60. http.Error(w, &quot;api expects phone to be set&quot;, http.StatusBadRequest)
  61. return
  62. }
  63. body := r.MultipartForm.Value[&quot;body&quot;]
  64. if len(body) == 0 {
  65. logger.Error(&quot;body not set&quot;, zap.Error(err))
  66. http.Error(w, &quot;api expects body to be set&quot;, http.StatusBadRequest)
  67. return
  68. }
  69. files := r.MultipartForm.File[&quot;photos&quot;]
  70. if len(files) == 0 {
  71. logger.Error(&quot;no files were submitted&quot;, zap.Error(err))
  72. http.Error(w, &quot;api expects one or more files to be submitted&quot;, http.StatusBadRequest)
  73. return
  74. }
  75. emailService := NewEmailService()
  76. sendEmailInput := sesv2.SendEmailInput{}
  77. destination := &amp;types.Destination{
  78. ToAddresses: []string{destinationEmail},
  79. }
  80. // add the attachments to the email
  81. for _, file := range files {
  82. f, err := file.Open()
  83. if err != nil {
  84. http.Error(w, err.Error(), http.StatusInternalServerError)
  85. return
  86. }
  87. defer f.Close()
  88. // not sure what to do here to get the email with the attachements
  89. }
  90. message := &amp;types.RawMessage{
  91. Data: make([]byte, 0), // This must change to be the bytes of the raw message
  92. }
  93. content := &amp;types.EmailContent{
  94. Raw: message,
  95. }
  96. sendEmailInput.Content = content
  97. sendEmailInput.Destination = destination
  98. sendEmailInput.FromEmailAddress = aws.String(email[0])
  99. err = emailService.SendEmail(logger, r.Context(), &amp;sendEmailInput)
  100. if err != nil {
  101. logger.Error(&quot;an error occured sending the email&quot;, zap.Error(err))
  102. http.Error(w, &quot;error sending email&quot;, http.StatusBadRequest)
  103. return
  104. }
  105. w.WriteHeader(http.StatusOK)
  106. })
  107. }

My understanding is (please correct me if I'm wrong) that I have to build out a raw message in a format similar to this. Assuming that is right, I'm just not sure how to do that in Go

答案1

得分: 1

为了创建附件,您需要使用base64对消息的内容进行编码

这里是一个发送csv文件作为附件的示例:

  1. import (
  2. // ...
  3. secretutils "github.com/alessiosavi/GoGPUtils/aws/secrets"
  4. sesutils "github.com/alessiosavi/GoGPUtils/aws/ses"
  5. )
  6. type MailConf struct {
  7. FromName string `json:"from_name,omitempty"`
  8. FromMail string `json:"from_mail,omitempty"`
  9. To string `json:"to,omitempty"`
  10. CC []string `json:"cc,omitempty"`
  11. }
  12. func SendRawMail(filename string, data []byte) error {
  13. var mailConf MailConf
  14. if err := secretutils.UnmarshalSecret(os.Getenv("XXX_YOUR_SECRET_STORED_IN_AWS"), &mailConf); err != nil {
  15. return err
  16. }
  17. subject := fmt.Sprintf("Found errors for the following file: %s", filename)
  18. var carbonCopy string
  19. if len(mailConf.CC) > 0 {
  20. carbonCopy = stringutils.JoinSeparator(",", mailConf.CC...)
  21. } else {
  22. carbonCopy = ""
  23. }
  24. raw := fmt.Sprintf(`From: "%[1]s" <%[2]s>
  25. To: %[3]s
  26. Cc: %[4]s
  27. Subject: %[5]s
  28. Content-Type: multipart/mixed;
  29. boundary="1"
  30. --1
  31. Content-Type: multipart/alternative;
  32. boundary="sub_1"
  33. --sub_1
  34. Content-Type: string/plain; charset=utf-8
  35. Content-Transfer-Encoding: quoted-printable
  36. Please see the attached file for a list of errors
  37. --sub_1
  38. Content-Type: string/html; charset=utf-8
  39. Content-Transfer-Encoding: quoted-printable
  40. <html>
  41. <head></head>
  42. <body>
  43. <h1>%[6]s</h1>
  44. <p><h2>Please see the attached file for the list of the rows.<h2></p>
  45. </body>
  46. </html>
  47. --sub_1--
  48. --1
  49. Content-Type: string/plain; name="errors_%[6]s"
  50. Content-Description: errors_%[6]s
  51. Content-Disposition: attachment;filename="errors_%[6]s";
  52. creation-date="%[7]s";
  53. Content-Transfer-Encoding: base64
  54. %[8]s
  55. --1--`, mailConf.FromName, mailConf.FromMail, mailConf.To, carbonCopy, subject, strings.Replace(filename, ".csv", ".json", 1), time.Now().Format("2-Jan-06 3.04.05 PM"), base64.StdEncoding.EncodeToString(data))
  56. return sesutils.SendMail([]byte(raw))
  57. }
英文:

In order to create an attachment, you have to encode using base64 the content of the message.

Here an example that send a csv as attacchemnt:

  1. import (
  2. // ...
  3. secretutils &quot;github.com/alessiosavi/GoGPUtils/aws/secrets&quot;
  4. sesutils &quot;github.com/alessiosavi/GoGPUtils/aws/ses&quot;
  5. )
  6. type MailConf struct {
  7. FromName string `json:&quot;from_name,omitempty&quot;`
  8. FromMail string `json:&quot;from_mail,omitempty&quot;`
  9. To string `json:&quot;to,omitempty&quot;`
  10. CC []string `json:&quot;cc,omitempty&quot;`
  11. }
  12. func SendRawMail(filename string, data []byte) error {
  13. var mailConf MailConf
  14. if err := secretutils.UnmarshalSecret(os.Getenv(&quot;XXX_YOUR_SECRET_STORED_IN_AWS&quot;), &amp;mailConf); err != nil {
  15. return err
  16. }
  17. subject := fmt.Sprintf(&quot;Found errors for the following file: %s&quot;, filename)
  18. var carbonCopy string
  19. if len(mailConf.CC) &gt; 0 {
  20. carbonCopy = stringutils.JoinSeparator(&quot;,&quot;, mailConf.CC...)
  21. } else {
  22. carbonCopy = &quot;&quot;
  23. }
  24. raw := fmt.Sprintf(`From: &quot;%[1]s&quot; &lt;%[2]s&gt;
  25. To: %[3]s
  26. Cc: %[4]s
  27. Subject: %[5]s
  28. Content-Type: multipart/mixed;
  29. boundary=&quot;1&quot;
  30. --1
  31. Content-Type: multipart/alternative;
  32. boundary=&quot;sub_1&quot;
  33. --sub_1
  34. Content-Type: string/plain; charset=utf-8
  35. Content-Transfer-Encoding: quoted-printable
  36. Please see the attached file for a list of errors
  37. --sub_1
  38. Content-Type: string/html; charset=utf-8
  39. Content-Transfer-Encoding: quoted-printable
  40. &lt;html&gt;
  41. &lt;head&gt;&lt;/head&gt;
  42. &lt;body&gt;
  43. &lt;h1&gt;%[6]s&lt;/h1&gt;
  44. &lt;p&gt;&lt;h2&gt;Please see the attached file for the list of the rows.&lt;h2&gt;&lt;/p&gt;
  45. &lt;/body&gt;
  46. &lt;/html&gt;
  47. --sub_1--
  48. --1
  49. Content-Type: string/plain; name=&quot;errors_%[6]s&quot;
  50. Content-Description: errors_%[6]s
  51. Content-Disposition: attachment;filename=&quot;errors_%[6]s&quot;;
  52. creation-date=&quot;%[7]s&quot;;
  53. Content-Transfer-Encoding: base64
  54. %[8]s
  55. --1--`, mailConf.FromName, mailConf.FromMail, mailConf.To, carbonCopy, subject, strings.Replace(filename, &quot;.csv&quot;, &quot;.json&quot;, 1), time.Now().Format(&quot;2-Jan-06 3.04.05 PM&quot;), base64.StdEncoding.EncodeToString(data))
  56. return sesutils.SendMail([]byte(raw))
  57. }

huangapple
  • 本文由 发表于 2023年3月12日 20:42:41
  • 转载请务必保留本文链接:https://go.coder-hub.com/75713184.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定