diff --git a/cmd/tx.go b/cmd/tx.go index 757a4ad0d..cb27900f1 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 aea79a60c..fd044d78f 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 2d846a169..11bfc8a6d 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(