Skip to content

Commit

Permalink
Add support for file attachments in the transactional (tx) API. (#1243)
Browse files Browse the repository at this point in the history
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 <matiss.lidaka@nic.lv>
  • Loading branch information
knadh and MatiSSL committed Mar 19, 2023
1 parent 4181d8a commit 55f7eca
Show file tree
Hide file tree
Showing 3 changed files with 64 additions and 11 deletions.
53 changes: 52 additions & 1 deletion 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"
)
Expand All @@ -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
}

Expand Down Expand Up @@ -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 {
Expand Down
11 changes: 1 addition & 10 deletions internal/manager/manager.go
Expand Up @@ -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)
}
Expand Down
11 changes: 11 additions & 0 deletions models/models.go
Expand Up @@ -7,6 +7,7 @@ import (
"errors"
"fmt"
"html/template"
"net/textproto"
"regexp"
"strings"
txttpl "text/template"
Expand Down Expand Up @@ -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(
Expand Down

0 comments on commit 55f7eca

Please sign in to comment.