diff --git a/cmd/tx.go b/cmd/tx.go index b020cb43..3b631414 100644 --- a/cmd/tx.go +++ b/cmd/tx.go @@ -87,27 +87,45 @@ func (a *App) SendTxMessage(c echo.Context) error { notFound := []string{} for n := range num { - var ( - subID int - subEmail string - ) + var sub models.Subscriber - if !isEmails { - subID = m.SubscriberIDs[n] + if m.SubscriberMode == models.TxSubModeExternal { + // `external`: Always create an ephemeral "subscriber" and don't + // lookup in the DB. + sub = models.Subscriber{ + Email: m.SubscriberEmails[n], + } } else { - subEmail = m.SubscriberEmails[n] - } + // Default/fallback mode: lookup subscriber in DB. + var ( + subID int + subEmail string + ) - // Get the subscriber. - sub, err := a.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 + if !isEmails { + subID = m.SubscriberIDs[n] + } else { + subEmail = m.SubscriberEmails[n] } - 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. @@ -175,9 +193,31 @@ func (a *App) validateTxMessage(m models.TxMessage) (models.TxMessage, error) { 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, - 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 { diff --git a/docs/docs/content/apis/transactional.md b/docs/docs/content/apis/transactional.md index 48f04095..b73bb0b5 100644 --- a/docs/docs/content/apis/transactional.md +++ b/docs/docs/content/apis/transactional.md @@ -1,8 +1,8 @@ # API / Transactional -| Method | Endpoint | Description | -|:-------|:---------|:-------------------------------| -| POST | /api/tx | Send transactional messages | +| Method | Endpoint | Description | +| :----- | :------- | :-------------------------- | +| POST | /api/tx | Send transactional messages | ______________________________________________________________________ @@ -12,19 +12,30 @@ Allows sending transactional messages to one or more subscribers via a preconfig ##### Parameters -| Name | Type | Required | Description | -|:------------------|:----------|:---------|:---------------------------------------------------------------------------| -| subscriber_email | string | | Email of the subscriber. Can substitute with `subscriber_id`. | -| subscriber_id | number | | Subscriber's ID can substitute with `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`. | -| template_id | number | Yes | ID of the transactional template to be used for the message. | -| from_email | string | | Optional sender email. | -| subject | string | | Optional subject. If empty, the subject defined on the template is used | -| data | JSON | | Optional nested JSON map. Available in the template as `{{ .Tx.Data.* }}`. | -| headers | JSON\[\] | | Optional array of email headers. | -| messenger | string | | Messenger to send the message. Default is `email`. | -| content_type | string | | Email format options include `html`, `markdown`, and `plain`. | +| Name | Type | Required | Description | +| :---------------- | :--------- | :------- | :------------------------------------------------------------------------- | +| subscriber_email | string | | Email of the subscriber. Can substitute with `subscriber_id`. | +| subscriber_id | number | | Subscriber's ID can substitute with `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_mode | string | | Subscriber lookup mode: `default`, `fallback`, or `external` | +| template_id | number | Yes | ID of the transactional template to be used for the message. | +| from_email | string | | Optional sender email. | +| subject | string | | Optional subject. If empty, the subject defined on the template is used | +| data | JSON | | Optional nested JSON map. Available in the template as `{{ .Tx.Data.* }}`. | +| headers | JSON\[\] | | Optional array of email headers. | +| 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 @@ -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 diff --git a/models/messages.go b/models/messages.go index 3e82dabd..6645b01e 100644 --- a/models/messages.go +++ b/models/messages.go @@ -37,8 +37,16 @@ type Attachment struct { Content []byte } +// TxMessage subscriber modes. +const ( + TxSubModeDefault = "default" + TxSubModeFallback = "fallback" + TxSubModeExternal = "external" +) + // TxMessage represents an e-mail campaign. type TxMessage struct { + SubscriberMode string `json:"subscriber_mode"` SubscriberEmails []string `json:"subscriber_emails"` SubscriberIDs []int `json:"subscriber_ids"`