mirror of
https://github.com/knadh/listmonk.git
synced 2025-12-05 16:00:03 +01:00
Add external recipient support to /api/tx endpoint.
This patch introduces the long pending requirement of being able to send messages
to arbitrary recipients via the `/api/tx` endpoint without them having to be
subscribers in the database.
It maintains backwards compatibility and introduces just one new field,
`subscriber_mode` to the `/api/tx` endpoint.
- `default` - Recipients must exist as subscribers in the database.
Pass either `subscriber_emails` or `subscriber_ids`. |
- `fallback` - Only accepts `subscriber_emails` and looks up subscribers in the
database. If not found, sends the message to the e-mail anyway.
In the template, apart from `{{ .Subscriber.Email }}`, other
subscriber fields such as `.Name`. will be empty.
Use `{{ Tx.Data.* }}` instead. |
- `external` - Sends to the given `subscriber_emails` without subscriber lookup
in the database. In the template, apart from
`{{ .Subscriber.Email }}`, other subscriber fields such as
`.Name`. will be empty. Use `{{ Tx.Data.* }}` instead. |
This commit is contained in:
76
cmd/tx.go
76
cmd/tx.go
@@ -87,27 +87,45 @@ func (a *App) SendTxMessage(c echo.Context) error {
|
|||||||
|
|
||||||
notFound := []string{}
|
notFound := []string{}
|
||||||
for n := range num {
|
for n := range num {
|
||||||
var (
|
var sub models.Subscriber
|
||||||
subID int
|
|
||||||
subEmail string
|
|
||||||
)
|
|
||||||
|
|
||||||
if !isEmails {
|
if m.SubscriberMode == models.TxSubModeExternal {
|
||||||
subID = m.SubscriberIDs[n]
|
// `external`: Always create an ephemeral "subscriber" and don't
|
||||||
|
// lookup in the DB.
|
||||||
|
sub = models.Subscriber{
|
||||||
|
Email: m.SubscriberEmails[n],
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
subEmail = m.SubscriberEmails[n]
|
// Default/fallback mode: lookup subscriber in DB.
|
||||||
}
|
var (
|
||||||
|
subID int
|
||||||
|
subEmail string
|
||||||
|
)
|
||||||
|
|
||||||
// Get the subscriber.
|
if !isEmails {
|
||||||
sub, err := a.core.GetSubscriber(subID, "", subEmail)
|
subID = m.SubscriberIDs[n]
|
||||||
if err != nil {
|
} else {
|
||||||
// If the subscriber is not found, log that error and move on without halting on the list.
|
subEmail = m.SubscriberEmails[n]
|
||||||
if er, ok := err.(*echo.HTTPError); ok && er.Code == http.StatusBadRequest {
|
|
||||||
notFound = append(notFound, fmt.Sprintf("%v", er.Message))
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return err
|
var err error
|
||||||
|
sub, err = a.core.GetSubscriber(subID, "", subEmail)
|
||||||
|
if err != nil {
|
||||||
|
if er, ok := err.(*echo.HTTPError); ok && er.Code == http.StatusBadRequest {
|
||||||
|
// `fallback`: Create an ephemeral "subscriber" if the subscriber wasn't found.
|
||||||
|
if m.SubscriberMode == models.TxSubModeFallback {
|
||||||
|
sub = models.Subscriber{
|
||||||
|
Email: subEmail,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// `default`: log error and continue.
|
||||||
|
notFound = append(notFound, fmt.Sprintf("%v", er.Message))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render the message.
|
// Render the message.
|
||||||
@@ -175,9 +193,31 @@ func (a *App) validateTxMessage(m models.TxMessage) (models.TxMessage, error) {
|
|||||||
m.SubscriberIDs = append(m.SubscriberIDs, m.SubscriberID)
|
m.SubscriberIDs = append(m.SubscriberIDs, m.SubscriberID)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (len(m.SubscriberEmails) == 0 && len(m.SubscriberIDs) == 0) || (len(m.SubscriberEmails) > 0 && len(m.SubscriberIDs) > 0) {
|
// Validate subscriber_mode.
|
||||||
|
if m.SubscriberMode == "" {
|
||||||
|
m.SubscriberMode = models.TxSubModeDefault
|
||||||
|
}
|
||||||
|
|
||||||
|
switch m.SubscriberMode {
|
||||||
|
case models.TxSubModeDefault:
|
||||||
|
// Need subscriber_emails OR subscriber_ids, but not both.
|
||||||
|
if (len(m.SubscriberEmails) == 0 && len(m.SubscriberIDs) == 0) || (len(m.SubscriberEmails) > 0 && len(m.SubscriberIDs) > 0) {
|
||||||
|
return m, echo.NewHTTPError(http.StatusBadRequest,
|
||||||
|
a.i18n.Ts("globals.messages.invalidFields", "name", "send subscriber_emails OR subscriber_ids"))
|
||||||
|
}
|
||||||
|
case models.TxSubModeFallback, models.TxSubModeExternal:
|
||||||
|
// `fallback` and `external` can only use subscriber_emails.
|
||||||
|
if len(m.SubscriberIDs) > 0 {
|
||||||
|
return m, echo.NewHTTPError(http.StatusBadRequest,
|
||||||
|
a.i18n.Ts("globals.messages.invalidFields", "name", "subscriber_ids not allowed in fallback or external mode"))
|
||||||
|
}
|
||||||
|
if len(m.SubscriberEmails) == 0 {
|
||||||
|
return m, echo.NewHTTPError(http.StatusBadRequest,
|
||||||
|
a.i18n.Ts("globals.messages.invalidFields", "name", "subscriber_emails"))
|
||||||
|
}
|
||||||
|
default:
|
||||||
return m, echo.NewHTTPError(http.StatusBadRequest,
|
return m, echo.NewHTTPError(http.StatusBadRequest,
|
||||||
a.i18n.Ts("globals.messages.invalidFields", "name", "send subscriber_emails OR subscriber_ids"))
|
a.i18n.Ts("globals.messages.invalidFields", "name", "subscriber_mode"))
|
||||||
}
|
}
|
||||||
|
|
||||||
for n, email := range m.SubscriberEmails {
|
for n, email := range m.SubscriberEmails {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
# API / Transactional
|
# API / Transactional
|
||||||
|
|
||||||
| Method | Endpoint | Description |
|
| Method | Endpoint | Description |
|
||||||
|:-------|:---------|:-------------------------------|
|
| :----- | :------- | :-------------------------- |
|
||||||
| POST | /api/tx | Send transactional messages |
|
| POST | /api/tx | Send transactional messages |
|
||||||
|
|
||||||
______________________________________________________________________
|
______________________________________________________________________
|
||||||
|
|
||||||
@@ -12,19 +12,30 @@ Allows sending transactional messages to one or more subscribers via a preconfig
|
|||||||
|
|
||||||
##### Parameters
|
##### Parameters
|
||||||
|
|
||||||
| Name | Type | Required | Description |
|
| Name | Type | Required | Description |
|
||||||
|:------------------|:----------|:---------|:---------------------------------------------------------------------------|
|
| :---------------- | :--------- | :------- | :------------------------------------------------------------------------- |
|
||||||
| subscriber_email | string | | Email of the subscriber. Can substitute with `subscriber_id`. |
|
| subscriber_email | string | | Email of the subscriber. Can substitute with `subscriber_id`. |
|
||||||
| subscriber_id | number | | Subscriber's ID can substitute with `subscriber_email`. |
|
| subscriber_id | number | | Subscriber's ID can substitute with `subscriber_email`. |
|
||||||
| subscriber_emails | string\[\] | | Multiple subscriber emails as alternative to `subscriber_email`. |
|
| subscriber_emails | string\[\] | | Multiple subscriber emails as alternative to `subscriber_email`. |
|
||||||
| subscriber_ids | number\[\] | | Multiple subscriber IDs as an alternative to `subscriber_id`. |
|
| subscriber_ids | number\[\] | | Multiple subscriber IDs as an alternative to `subscriber_id`. |
|
||||||
| template_id | number | Yes | ID of the transactional template to be used for the message. |
|
| subscriber_mode | string | | Subscriber lookup mode: `default`, `fallback`, or `external` |
|
||||||
| from_email | string | | Optional sender email. |
|
| template_id | number | Yes | ID of the transactional template to be used for the message. |
|
||||||
| subject | string | | Optional subject. If empty, the subject defined on the template is used |
|
| from_email | string | | Optional sender email. |
|
||||||
| data | JSON | | Optional nested JSON map. Available in the template as `{{ .Tx.Data.* }}`. |
|
| subject | string | | Optional subject. If empty, the subject defined on the template is used |
|
||||||
| headers | JSON\[\] | | Optional array of email headers. |
|
| data | JSON | | Optional nested JSON map. Available in the template as `{{ .Tx.Data.* }}`. |
|
||||||
| messenger | string | | Messenger to send the message. Default is `email`. |
|
| headers | JSON\[\] | | Optional array of email headers. |
|
||||||
| content_type | string | | Email format options include `html`, `markdown`, and `plain`. |
|
| messenger | string | | Messenger to send the message. Default is `email`. |
|
||||||
|
| content_type | string | | Email format options include `html`, `markdown`, and `plain`. |
|
||||||
|
|
||||||
|
##### Subscriber modes
|
||||||
|
|
||||||
|
The `subscriber_mode` parameter controls how the recipients (subscribers or non-subscriber recipients) are resolved.
|
||||||
|
|
||||||
|
| Mode | Description |
|
||||||
|
| :--------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
|
| `default` | Recipients must exist as subscribers in the database. Pass either `subscriber_emails` or `subscriber_ids`. |
|
||||||
|
| `fallback` | Only accepts `subscriber_emails` and looks up subscribers in the database. If not found, sends the message to the e-mail anyway. In the template, apart from `{{ .Subscriber.Email }}`, other subscriber fields such as `.Name`. will be empty. Use `{{ Tx.Data.* }}` instead. |
|
||||||
|
| `external` | Sends to the given `subscriber_emails` without subscriber lookup in the database. In the template, apart from `{{ .Subscriber.Email }}`, other subscriber fields such as `.Name`. will be empty. Use `{{ Tx.Data.* }}` instead. |
|
||||||
|
|
||||||
##### Example
|
##### Example
|
||||||
|
|
||||||
@@ -49,6 +60,26 @@ EOF
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
##### Example with external mode
|
||||||
|
|
||||||
|
Send to arbitrary email addresses without requiring them to be subscribers:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
curl -u "api_user:token" "http://localhost:9000/api/tx" -X POST \
|
||||||
|
-H 'Content-Type: application/json; charset=utf-8' \
|
||||||
|
--data-binary @- << EOF
|
||||||
|
{
|
||||||
|
"subscriber_mode": "external",
|
||||||
|
"subscriber_emails": ["recipient@example.com"],
|
||||||
|
"template_id": 2,
|
||||||
|
"data": {"name": "John", "order_id": "1234"},
|
||||||
|
"content_type": "html"
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
```
|
||||||
|
|
||||||
|
In the template, use `{{ .Tx.Data.name }}`, `{{ .Tx.Data.order_id }}`, etc. to access the data.
|
||||||
|
|
||||||
______________________________________________________________________
|
______________________________________________________________________
|
||||||
|
|
||||||
#### File Attachments
|
#### File Attachments
|
||||||
|
|||||||
@@ -37,8 +37,16 @@ type Attachment struct {
|
|||||||
Content []byte
|
Content []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TxMessage subscriber modes.
|
||||||
|
const (
|
||||||
|
TxSubModeDefault = "default"
|
||||||
|
TxSubModeFallback = "fallback"
|
||||||
|
TxSubModeExternal = "external"
|
||||||
|
)
|
||||||
|
|
||||||
// TxMessage represents an e-mail campaign.
|
// TxMessage represents an e-mail campaign.
|
||||||
type TxMessage struct {
|
type TxMessage struct {
|
||||||
|
SubscriberMode string `json:"subscriber_mode"`
|
||||||
SubscriberEmails []string `json:"subscriber_emails"`
|
SubscriberEmails []string `json:"subscriber_emails"`
|
||||||
SubscriberIDs []int `json:"subscriber_ids"`
|
SubscriberIDs []int `json:"subscriber_ids"`
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user