From 55f7eca2e8a81a8b6ae1e79386b2eef18104dc44 Mon Sep 17 00:00:00 2001 From: Kailash Nadh Date: Sun, 19 Mar 2023 15:50:44 +0530 Subject: [PATCH] Add support for file attachments in the transactional (tx) API. (#1243) The original PR accepts files to the `/tx` endpoints as Base64 encoded strings in the JSON payload. This isn't ideal as the payload size increase caused by Base64 for larger files can be significant, in addition to the added clientside API complexity. This PR adds supports for multipart form posts to `/tx` where the JSON data (name: `data`) and multiple files can be posted simultaenously (one or more `file` fields). --- PR: #1166 * Attachment model for TxMessage * Don't reassign values, just pass the manager.Messgage * Read attachment info from API; create attachment Header * Refactor tx attachments to use multipart form files. Closes #1166. --- Co-authored-by: MatiSSL --- cmd/tx.go | 53 ++++++++++++++++++++++++++++++++++++- internal/manager/manager.go | 11 +------- models/models.go | 11 ++++++++ 3 files changed, 64 insertions(+), 11 deletions(-) diff --git a/cmd/tx.go b/cmd/tx.go index 757a4ad..cb27900 100644 --- a/cmd/tx.go +++ b/cmd/tx.go @@ -1,12 +1,15 @@ package main import ( + "encoding/json" "fmt" + "io/ioutil" "net/http" "net/textproto" "strings" "github.com/knadh/listmonk/internal/manager" + "github.com/knadh/listmonk/internal/messenger" "github.com/knadh/listmonk/models" "github.com/labstack/echo/v4" ) @@ -18,7 +21,48 @@ func handleSendTxMessage(c echo.Context) error { m models.TxMessage ) - if err := c.Bind(&m); err != nil { + // If it's a multipart form, there may be file attachments. + if strings.HasPrefix(c.Request().Header.Get("Content-Type"), "multipart/form-data") { + form, err := c.MultipartForm() + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, + app.i18n.Ts("globals.messages.invalidFields", "name", err.Error())) + } + + data, ok := form.Value["data"] + if !ok || len(data) != 1 { + return echo.NewHTTPError(http.StatusBadRequest, + app.i18n.Ts("globals.messages.invalidFields", "name", "data")) + } + + // Parse the JSON data. + if err := json.Unmarshal([]byte(data[0]), &m); err != nil { + return echo.NewHTTPError(http.StatusBadRequest, + app.i18n.Ts("globals.messages.invalidFields", "name", fmt.Sprintf("data: %s", err.Error()))) + } + + // Attach files. + for _, f := range form.File["file"] { + file, err := f.Open() + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, + app.i18n.Ts("globals.messages.invalidFields", "name", fmt.Sprintf("file: %s", err.Error()))) + } + defer file.Close() + + b, err := ioutil.ReadAll(file) + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, + app.i18n.Ts("globals.messages.invalidFields", "name", fmt.Sprintf("file: %s", err.Error()))) + } + + m.Attachments = append(m.Attachments, models.TxAttachment{ + Name: f.Filename, + Header: messenger.MakeAttachmentHeader(f.Filename, "base64"), + Content: b, + }) + } + } else if err := c.Bind(&m); err != nil { return err } @@ -85,6 +129,13 @@ func handleSendTxMessage(c echo.Context) error { msg.ContentType = m.ContentType msg.Messenger = m.Messenger msg.Body = m.Body + for _, a := range m.Attachments { + msg.Attachments = append(msg.Attachments, messenger.Attachment{ + Name: a.Name, + Header: a.Header, + Content: a.Content, + }) + } // Optional headers. if len(m.Headers) != 0 { diff --git a/internal/manager/manager.go b/internal/manager/manager.go index aea79a6..fd044d7 100644 --- a/internal/manager/manager.go +++ b/internal/manager/manager.go @@ -411,16 +411,7 @@ func (m *Manager) worker() { return } - err := m.messengers[msg.Messenger].Push(messenger.Message{ - From: msg.From, - To: msg.To, - Subject: msg.Subject, - ContentType: msg.ContentType, - Body: msg.Body, - AltBody: msg.AltBody, - Subscriber: msg.Subscriber, - Campaign: msg.Campaign, - }) + err := m.messengers[msg.Messenger].Push(msg.Message) if err != nil { m.logger.Printf("error sending message '%s': %v", msg.Subject, err) } diff --git a/models/models.go b/models/models.go index 2d846a1..11bfc8a 100644 --- a/models/models.go +++ b/models/models.go @@ -7,6 +7,7 @@ import ( "errors" "fmt" "html/template" + "net/textproto" "regexp" "strings" txttpl "text/template" @@ -361,12 +362,22 @@ type TxMessage struct { ContentType string `json:"content_type"` Messenger string `json:"messenger"` + // File attachments added from multi-part form data. + Attachments []TxAttachment `json:"-"` + Subject string `json:"-"` Body []byte `json:"-"` Tpl *template.Template `json:"-"` SubjectTpl *txttpl.Template `json:"-"` } +// TxAttachment is used by TxMessage, consists of FileName and file Content in bytes +type TxAttachment struct { + Name string + Header textproto.MIMEHeader + Content []byte +} + // markdown is a global instance of Markdown parser and renderer. var markdown = goldmark.New( goldmark.WithParserOptions(