Skip to content

Commit

Permalink
Add support for multiple subscribers in a single transactional messag…
Browse files Browse the repository at this point in the history
…e call.

This patch adds new array fields on `POST /tx`: `subscriber_emails[]`, `subscriber_ids[]`.
Either of these array fields can be sent with multiple subscribers.

The individual non-array fields `subscriber_id` and `subscriber_email` are deprecated.

Closes #994.
  • Loading branch information
knadh committed Dec 25, 2022
1 parent 5d4f1ea commit 3cfbc64
Show file tree
Hide file tree
Showing 25 changed files with 668 additions and 587 deletions.
121 changes: 87 additions & 34 deletions cmd/tx.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"
"net/http"
"net/textproto"
"strings"

"github.com/knadh/listmonk/internal/manager"
"github.com/knadh/listmonk/models"
Expand Down Expand Up @@ -35,58 +36,110 @@ func handleSendTxMessage(c echo.Context) error {
app.i18n.Ts("globals.messages.notFound", "name", fmt.Sprintf("template %d", m.TemplateID)))
}

// Get the subscriber.
sub, err := app.core.GetSubscriber(m.SubscriberID, "", m.SubscriberEmail)
if err != nil {
return err
var (
num = len(m.SubscriberEmails)
isEmails = true
)
if len(m.SubscriberIDs) > 0 {
num = len(m.SubscriberIDs)
isEmails = false
}

// Render the message.
if err := m.Render(sub, tpl); err != nil {
return echo.NewHTTPError(http.StatusBadRequest,
app.i18n.Ts("globals.messages.errorFetching", "name"))
}
notFound := []string{}
for n := 0; n < num; n++ {
var (
subID int
subEmail string
)

if !isEmails {
subID = m.SubscriberIDs[n]
} else {
subEmail = m.SubscriberEmails[n]
}

// Get the subscriber.
sub, err := app.core.GetSubscriber(subID, "", subEmail)
if err != nil {
// If the subscriber is not found, log that error and move on without halting on the list.
if er, ok := err.(*echo.HTTPError); ok && er.Code == http.StatusBadRequest {
notFound = append(notFound, fmt.Sprintf("%v", er.Message))
continue
}

return err
}

// Render the message.
if err := m.Render(sub, tpl); err != nil {
return echo.NewHTTPError(http.StatusBadRequest,
app.i18n.Ts("globals.messages.errorFetching", "name"))
}

// Prepare the final message.
msg := manager.Message{}
msg.Subscriber = sub
msg.To = []string{sub.Email}
msg.From = m.FromEmail
msg.Subject = m.Subject
msg.ContentType = m.ContentType
msg.Messenger = m.Messenger
msg.Body = m.Body

// Optional headers.
if len(m.Headers) != 0 {
msg.Headers = make(textproto.MIMEHeader, len(m.Headers))
for _, set := range m.Headers {
for hdr, val := range set {
msg.Headers.Add(hdr, val)
// Prepare the final message.
msg := manager.Message{}
msg.Subscriber = sub
msg.To = []string{sub.Email}
msg.From = m.FromEmail
msg.Subject = m.Subject
msg.ContentType = m.ContentType
msg.Messenger = m.Messenger
msg.Body = m.Body

// Optional headers.
if len(m.Headers) != 0 {
msg.Headers = make(textproto.MIMEHeader, len(m.Headers))
for _, set := range m.Headers {
for hdr, val := range set {
msg.Headers.Add(hdr, val)
}
}
}

if err := app.manager.PushMessage(msg); err != nil {
app.log.Printf("error sending message (%s): %v", msg.Subject, err)
return err
}
}

if err := app.manager.PushMessage(msg); err != nil {
app.log.Printf("error sending message (%s): %v", msg.Subject, err)
return err
if len(notFound) > 0 {
return echo.NewHTTPError(http.StatusBadRequest, strings.Join(notFound, "; "))
}

return c.JSON(http.StatusOK, okResp{true})
}

func validateTxMessage(m models.TxMessage, app *App) (models.TxMessage, error) {
if m.SubscriberEmail == "" && m.SubscriberID == 0 {
if len(m.SubscriberEmails) > 0 && m.SubscriberEmail != "" {
return m, echo.NewHTTPError(http.StatusBadRequest,
app.i18n.Ts("globals.messages.missingFields", "name", "subscriber_email or subscriber_id"))
app.i18n.Ts("globals.messages.invalidFields", "name", "do not send `subscriber_email`"))
}
if len(m.SubscriberIDs) > 0 && m.SubscriberID != 0 {
return m, echo.NewHTTPError(http.StatusBadRequest,
app.i18n.Ts("globals.messages.invalidFields", "name", "do not send `subscriber_id`"))
}

if m.SubscriberEmail != "" {
em, err := app.importer.SanitizeEmail(m.SubscriberEmail)
if err != nil {
return m, echo.NewHTTPError(http.StatusBadRequest, err.Error())
m.SubscriberEmails = append(m.SubscriberEmails, m.SubscriberEmail)
}

if m.SubscriberID != 0 {
m.SubscriberIDs = append(m.SubscriberIDs, m.SubscriberID)
}

if (len(m.SubscriberEmails) == 0 && len(m.SubscriberIDs) == 0) || (len(m.SubscriberEmails) > 0 && len(m.SubscriberIDs) > 0) {
return m, echo.NewHTTPError(http.StatusBadRequest,
app.i18n.Ts("globals.messages.invalidFields", "name", "send subscriber_emails OR subscriber_ids"))
}

for n, email := range m.SubscriberEmails {
if m.SubscriberEmail != "" {
em, err := app.importer.SanitizeEmail(email)
if err != nil {
return m, echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
m.SubscriberEmails[n] = em
}
m.SubscriberEmail = em
}

if m.FromEmail == "" {
Expand Down
1 change: 1 addition & 0 deletions i18n/ca.json
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@
"globals.messages.errorUpdating": "Error en actualitzar {name}: {error}",
"globals.messages.internalError": "Error del servidor intern",
"globals.messages.invalidData": "Dades no vàlides",
"globals.messages.invalidFields": "Invalid fields: {name}",
"globals.messages.invalidID": "ID(s) no vàlid",
"globals.messages.invalidUUID": "UUID(s) no vàlid",
"globals.messages.missingFields": "Falten camps: {name}",
Expand Down
1 change: 1 addition & 0 deletions i18n/cs-cz.json
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@
"globals.messages.errorUpdating": "Chyba při aktualizaci {name}: {error}",
"globals.messages.internalError": "Interní chyba serveru",
"globals.messages.invalidData": "Neplatná data",
"globals.messages.invalidFields": "Invalid fields: {name}",
"globals.messages.invalidID": "Neplatné ID",
"globals.messages.invalidUUID": "Neplatné UUID",
"globals.messages.missingFields": "Chybějící pole: {name}",
Expand Down
Loading

0 comments on commit 3cfbc64

Please sign in to comment.